The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/pkg/schema/load/load_test.go

283 lines
9.2 KiB

package load
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"io/fs"
"io/ioutil"
"os"
"path/filepath"
"sort"
Schema: get all devenv dashboards passing validation (#37857) * Strip nulls (again) * Add stripnulls script * Add transformations field * Close FieldConfig struct; proper plugin validating * s/graph/viz/ field in histogram dashboard * Use ui.GraphFieldConfig in histogram model * Add models for stat, gauge, barguage panel plugins Also toss necessary shared types into cue/ui/gen.cue, with TODOs to move them appropriately later. * Add required license headers * Heap of updates to cue UI components * Fix barchart types and one old devenv input * Use the GraphFieldConfig directly for timeseries * Add models.cue for a few panel plugins Barchart, state-timeline, and status-history * Enable the test validating all devenv dashboards!! * Fix effects of not checking after making comments * Update packages/grafana-ui/src/options/models.gen.ts Co-authored-by: Ryan McKinley <ryantxu@gmail.com> * Realign and unalign cue with ts types * Update devenv test to sniff for null errors Best option we have right now for helping people to know they need to strip nulls from devenv dashboards. * Add speculative default for barchart stacking * Fixup some dated devenv dashboards timeline-modes needed to be regenerated with the appropriate tooltip values included, per typing requirements, and timeline-demo needed to have the `mode` field removed, as it is not intended to be persisted. * Add necessary missing options for various panels * Regenerate devenv dashboards Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
4 years ago
"strings"
"testing"
"testing/fstest"
"cuelang.org/go/cue"
"cuelang.org/go/cue/errors"
"cuelang.org/go/cue/load"
cuejson "cuelang.org/go/pkg/encoding/json"
"github.com/grafana/grafana/pkg/schema"
"github.com/laher/mergefs"
"github.com/stretchr/testify/require"
)
var (
p = GetDefaultLoadPaths()
update = flag.Bool("update", false, "update golden files")
)
type testfunc func(*testing.T, schema.VersionedCueSchema, []byte, fs.FileInfo, string)
// for now we keep the validdir as input parameter since for trim apply default we can't use devenv directory yet,
// otherwise we can hardcoded validdir and just pass the testtype is more than enough.
// TODO: remove validdir once we can test directly with devenv folder
var doTestAgainstDevenv = func(sch schema.VersionedCueSchema, validdir string, fn testfunc) func(t *testing.T) {
return func(t *testing.T) {
t.Parallel()
require.NoError(t, filepath.Walk(validdir, func(path string, d fs.FileInfo, err error) error {
require.NoError(t, err)
if d.IsDir() || filepath.Ext(d.Name()) != ".json" {
return nil
}
// Ignore gosec warning G304 since it's a test
// nolint:gosec
b, err := os.Open(path)
require.NoError(t, err, "failed to open dashboard file")
// Only try to validate dashboards with schemaVersion >= 30
jtree := make(map[string]interface{})
byt, err := io.ReadAll(b)
if err != nil {
t.Fatal(err)
}
require.NoError(t, json.Unmarshal(byt, &jtree))
if oldschemav, has := jtree["schemaVersion"]; !has {
t.Logf("no schemaVersion in %s", path)
return nil
} else {
Chore: Add some e2e tests for repeating behaviour (#43457) * user essentials mob! :trident: * user essentials mob! :trident: * WIP: Mob session work :construction: :trident: * user essentials mob! :trident: * user essentials mob! :trident: * user essentials mob! :trident: * user essentials mob! :trident: * user essentials mob! :trident: * mob next [ci-skip] [ci skip] [skip ci] * user essentials mob! :trident: * user essentials mob! :trident: * user essentials mob! :trident: * user essentials mob! :trident: * user essentials mob! :trident: * user essentials mob! :trident: * :construction:: Mob session work :trident: * user essentials mob! :trident: * user essentials mob! :trident: * Move repeats suite under dashboard suite * remove these generated files * move repeats-suite into dashboards-suite * Reexport dashboard jsons from play and update them * :construction:: Mob session work :trident: * :construction:: Mob session work :trident: * Rename dashboards to work with stripnulls * Run stripnulls * Add repeat to row schema * Clean up the rest of the repeating dashboards * Fix tooltip sorting * Update older dashboards * Update golden files so tests pass * format this to ensure consistent tabs/spaces * undo whitespace changes * Update scripts/stripnulls.sh Co-authored-by: sam boyer <sam.boyer@grafana.com> * update schema versions and test Co-authored-by: thisisobate <obasiuche62@gmail.com> Co-authored-by: Hugo Häggmark <hugo.haggmark@gmail.com> Co-authored-by: joshhunt <josh@trtr.co> Co-authored-by: kay delaney <kay@grafana.com> Co-authored-by: Alexandra Vargas <alexa1866@gmail.com> Co-authored-by: sam boyer <sam.boyer@grafana.com>
4 years ago
if !(oldschemav.(float64) > 32) {
if testing.Verbose() {
Chore: Add some e2e tests for repeating behaviour (#43457) * user essentials mob! :trident: * user essentials mob! :trident: * WIP: Mob session work :construction: :trident: * user essentials mob! :trident: * user essentials mob! :trident: * user essentials mob! :trident: * user essentials mob! :trident: * user essentials mob! :trident: * mob next [ci-skip] [ci skip] [skip ci] * user essentials mob! :trident: * user essentials mob! :trident: * user essentials mob! :trident: * user essentials mob! :trident: * user essentials mob! :trident: * user essentials mob! :trident: * :construction:: Mob session work :trident: * user essentials mob! :trident: * user essentials mob! :trident: * Move repeats suite under dashboard suite * remove these generated files * move repeats-suite into dashboards-suite * Reexport dashboard jsons from play and update them * :construction:: Mob session work :trident: * :construction:: Mob session work :trident: * Rename dashboards to work with stripnulls * Run stripnulls * Add repeat to row schema * Clean up the rest of the repeating dashboards * Fix tooltip sorting * Update older dashboards * Update golden files so tests pass * format this to ensure consistent tabs/spaces * undo whitespace changes * Update scripts/stripnulls.sh Co-authored-by: sam boyer <sam.boyer@grafana.com> * update schema versions and test Co-authored-by: thisisobate <obasiuche62@gmail.com> Co-authored-by: Hugo Häggmark <hugo.haggmark@gmail.com> Co-authored-by: joshhunt <josh@trtr.co> Co-authored-by: kay delaney <kay@grafana.com> Co-authored-by: Alexandra Vargas <alexa1866@gmail.com> Co-authored-by: sam boyer <sam.boyer@grafana.com>
4 years ago
t.Logf("schemaVersion is %v, older than 33, skipping %s", oldschemav, path)
}
return nil
}
}
t.Run(filepath.Base(path), func(t *testing.T) {
fn(t, sch, byt, d, path)
})
return nil
}))
}
}
// Basic well-formedness tests on core scuemata.
func TestScuemataBasics(t *testing.T) {
all := make(map[string]schema.VersionedCueSchema)
dash, err := BaseDashboardFamily(p)
require.NoError(t, err, "error while loading base dashboard scuemata")
all["basedash"] = dash
ddash, err := DistDashboardFamily(p)
require.NoError(t, err, "error while loading dist dashboard scuemata")
all["distdash"] = ddash
for set, sch := range all {
t.Run(set, func(t *testing.T) {
require.NotNil(t, sch, "scuemata for %q linked to empty chain", set)
maj, min := sch.Version()
t.Run(fmt.Sprintf("%v.%v", maj, min), func(t *testing.T) {
cv := sch.CUE()
t.Run("Exists", func(t *testing.T) {
require.True(t, cv.Exists(), "cue value for schema does not exist")
})
t.Run("Validate", func(t *testing.T) {
require.NoError(t, cv.Validate(), "all schema should be valid with respect to basic CUE rules")
})
})
})
}
}
func TestDevenvDashboardValidity(t *testing.T) {
// TODO will need to expand this appropriately when the scuemata contain
// more than one schema
var validdir = filepath.Join("..", "..", "..", "devenv", "dev-dashboards")
dash, err := BaseDashboardFamily(p)
require.NoError(t, err, "error while loading base dashboard scuemata")
dashboardValidity := func(t *testing.T, sch schema.VersionedCueSchema, byt []byte, d fs.FileInfo, path string) {
err := sch.Validate(schema.Resource{Value: string(byt), Name: path})
if err != nil {
// Testify trims errors to short length. We want the full text
errstr := errors.Details(err, nil)
t.Log(errstr)
if strings.Contains(errstr, "null") {
t.Log("validation failure appears to involve nulls - see if scripts/stripnulls.sh has any effect?")
}
t.FailNow()
}
}
t.Run("base", doTestAgainstDevenv(dash, validdir, dashboardValidity))
Schema: get all devenv dashboards passing validation (#37857) * Strip nulls (again) * Add stripnulls script * Add transformations field * Close FieldConfig struct; proper plugin validating * s/graph/viz/ field in histogram dashboard * Use ui.GraphFieldConfig in histogram model * Add models for stat, gauge, barguage panel plugins Also toss necessary shared types into cue/ui/gen.cue, with TODOs to move them appropriately later. * Add required license headers * Heap of updates to cue UI components * Fix barchart types and one old devenv input * Use the GraphFieldConfig directly for timeseries * Add models.cue for a few panel plugins Barchart, state-timeline, and status-history * Enable the test validating all devenv dashboards!! * Fix effects of not checking after making comments * Update packages/grafana-ui/src/options/models.gen.ts Co-authored-by: Ryan McKinley <ryantxu@gmail.com> * Realign and unalign cue with ts types * Update devenv test to sniff for null errors Best option we have right now for helping people to know they need to strip nulls from devenv dashboards. * Add speculative default for barchart stacking * Fixup some dated devenv dashboards timeline-modes needed to be regenerated with the appropriate tooltip values included, per typing requirements, and timeline-demo needed to have the `mode` field removed, as it is not intended to be persisted. * Add necessary missing options for various panels * Regenerate devenv dashboards Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
4 years ago
ddash, err := DistDashboardFamily(p)
require.NoError(t, err, "error while loading dist dashboard scuemata")
t.Run("dist", doTestAgainstDevenv(ddash, validdir, dashboardValidity))
}
// TO update the golden file located in pkg/schema/testdata/devenvgoldenfiles
// run go test -v ./pkg/schema/load/... -update
func TestUpdateDevenvDashboardGoldenFiles(t *testing.T) {
flag.Parse()
if *update {
ddash, err := DistDashboardFamily(p)
require.NoError(t, err, "error while loading dist dashboard scuemata")
var validdir = filepath.Join("..", "..", "..", "devenv", "dev-dashboards")
goldenFileUpdate := func(t *testing.T, sch schema.VersionedCueSchema, byt []byte, d fs.FileInfo, _ string) {
dsSchema, err := schema.SearchAndValidate(sch, string(byt))
require.NoError(t, err)
origin, err := schema.ApplyDefaults(schema.Resource{Value: string(byt)}, dsSchema.CUE())
require.NoError(t, err)
var prettyJSON bytes.Buffer
err = json.Indent(&prettyJSON, []byte(origin.Value.(string)), "", "\t")
require.NoError(t, err)
err = ioutil.WriteFile(filepath.Join("..", "testdata", "devenvgoldenfiles", d.Name()), prettyJSON.Bytes(), 0644)
require.NoError(t, err)
}
t.Run("updategoldenfile", doTestAgainstDevenv(ddash, validdir, goldenFileUpdate))
}
}
func TestDevenvDashboardTrimApplyDefaults(t *testing.T) {
ddash, err := DistDashboardFamily(p)
require.NoError(t, err, "error while loading dist dashboard scuemata")
// TODO will need to expand this appropriately when the scuemata contain
// more than one schema
validdir := filepath.Join("..", "testdata", "devenvgoldenfiles")
trimApplyDefaults := func(t *testing.T, sch schema.VersionedCueSchema, byt []byte, d fs.FileInfo, path string) {
dsSchema, err := schema.SearchAndValidate(sch, string(byt))
require.NoError(t, err)
// Trimmed default json file
trimmed, err := schema.TrimDefaults(schema.Resource{Value: string(byt)}, dsSchema.CUE())
require.NoError(t, err)
// store the trimmed result into testdata for easy debug
out, err := schema.ApplyDefaults(trimmed, dsSchema.CUE())
require.NoError(t, err)
require.JSONEq(t, string(byt), out.Value.(string))
}
t.Run("defaults", doTestAgainstDevenv(ddash, validdir, trimApplyDefaults))
}
func TestPanelValidity(t *testing.T) {
t.Skip()
validdir := os.DirFS(filepath.Join("testdata", "artifacts", "panels"))
ddash, err := DistDashboardFamily(p)
require.NoError(t, err, "error while loading dist dashboard scuemata")
// TODO hmm, it's awkward for this test's structure to have to pick just one
// type of panel plugin, but we can change the test structure. However, is
// there any other situation where we want the panel subschema with all
// possible disjunctions? If so, maybe the interface needs work. Or maybe
// just defer that until the proper generic composite scuemata impl.
dpan, err := ddash.(CompositeDashboardSchema).LatestPanelSchemaFor("table")
require.NoError(t, err, "error while loading panel subschema")
require.NoError(t, fs.WalkDir(validdir, ".", func(path string, d fs.DirEntry, err error) error {
require.NoError(t, err)
if d.IsDir() || filepath.Ext(d.Name()) != ".json" {
return nil
}
t.Run(path, func(t *testing.T) {
// TODO FIXME stop skipping once we actually have the schema filled in
// enough that the tests pass, lol
b, err := validdir.Open(path)
require.NoError(t, err, "failed to open panel file")
err = dpan.Validate(schema.Resource{Value: b})
require.NoError(t, err, "panel failed validation")
})
return nil
}))
}
func TestCueErrorWrapper(t *testing.T) {
a := fstest.MapFS{
filepath.Join(dashboardDir, "dashboard.cue"): &fstest.MapFile{Data: []byte("package dashboard\n{;;;;;;;;}")},
}
filesystem := mergefs.Merge(a, GetDefaultLoadPaths().BaseCueFS)
var baseLoadPaths = BaseLoadPaths{
BaseCueFS: filesystem,
DistPluginCueFS: GetDefaultLoadPaths().DistPluginCueFS,
}
_, err := BaseDashboardFamily(baseLoadPaths)
require.Error(t, err)
require.Contains(t, err.Error(), "in file")
require.Contains(t, err.Error(), "line: ")
_, err = DistDashboardFamily(baseLoadPaths)
require.Error(t, err)
require.Contains(t, err.Error(), "in file")
require.Contains(t, err.Error(), "line: ")
}
func TestAllPluginsInDist(t *testing.T) {
overlay, err := defaultOverlay(p)
require.NoError(t, err)
cfg := &load.Config{
Overlay: overlay,
ModuleRoot: prefix,
Module: "github.com/grafana/grafana",
Dir: filepath.Join(prefix, dashboardDir, "dist"),
Package: "dist",
}
inst := ctx.BuildInstance(load.Instances(nil, cfg)[0])
require.NoError(t, inst.Err())
dinst := ctx.CompileString(`
Family: compose: Panel: {}
typs: [for typ, _ in Family.compose.Panel {typ}]
`, cue.Filename("str"))
require.NoError(t, dinst.Err())
typs := dinst.Unify(inst).LookupPath(cue.MakePath(cue.Str("typs")))
j, err := cuejson.Marshal(typs)
require.NoError(t, err)
var importedPanelTypes, loadedPanelTypes []string
require.NoError(t, json.Unmarshal([]byte(j), &importedPanelTypes))
// TODO a more canonical way of getting all the dist plugin types with
// models.cue would be nice.
m, err := loadPanelScuemata(p)
require.NoError(t, err)
for typ := range m {
loadedPanelTypes = append(loadedPanelTypes, typ)
}
sort.Strings(importedPanelTypes)
sort.Strings(loadedPanelTypes)
require.Equal(t, loadedPanelTypes, importedPanelTypes, "%s/family.cue needs updating, it must compose the same set of panel plugin models that are found by the plugin loader", cfg.Dir)
}