Scuemata: Checking json validity by enabling skipped tests (#34385) (#35226)

* Make sure we don't skip any tests - refactoring

* Remove commented lines

* Move test folder

(cherry picked from commit 4c8ce8a450)

Co-authored-by: Dimitris Sotirakis <dimitrios.sotirakis@grafana.com>
This commit is contained in:
Grot (@grafanabot)
2021-06-04 10:03:58 +02:00
committed by GitHub
co-authored by Dimitris Sotirakis
parent 9439b6acba
commit 7b67612f7d
14 changed files with 66 additions and 289 deletions
+1
View File
@@ -63,6 +63,7 @@ require (
github.com/jmespath/go-jmespath v0.4.0
github.com/json-iterator/go v1.1.11
github.com/jung-kurt/gofpdf v1.16.2
github.com/laher/mergefs v0.1.1
github.com/lib/pq v1.10.0
github.com/linkedin/goavro/v2 v2.10.0
github.com/magefile/mage v1.11.0
+4
View File
@@ -1201,6 +1201,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/laher/mergefs v0.1.1 h1:nV2bTS57vrmbMxeR6uvJpI8LyGl3QHj4bLBZO3aUV58=
github.com/laher/mergefs v0.1.1/go.mod h1:FSY1hYy94on4Tz60waRMGdO1awwS23BacqJlqf9lJ9Q=
github.com/lann/builder v0.0.0-20150808151131-f22ce00fd939/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
@@ -1241,6 +1243,8 @@ github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
github.com/mattermost/xml-roundtrip-validator v0.0.0-20201213122252-bcd7e1b9601e h1:qqXczln0qwkVGcpQ+sQuPOVntt2FytYarXXxYSNJkgw=
github.com/mattermost/xml-roundtrip-validator v0.0.0-20201213122252-bcd7e1b9601e/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
-92
View File
@@ -1,92 +0,0 @@
package commands
import (
"errors"
"io/fs"
"os"
"sort"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
)
// MergeFS contains a slice of different filesystems that can be merged together
type MergeFS struct {
filesystems []fs.FS
}
// Merge filesystems
func Merge(filesystems ...fs.FS) fs.FS {
return MergeFS{filesystems: filesystems}
}
// Open opens the named file.
func (mfs MergeFS) Open(name string) (fs.File, error) {
for _, filesystem := range mfs.filesystems {
file, err := filesystem.Open(name)
if err == nil {
return file, nil
}
}
return nil, os.ErrNotExist
}
// ReadDir reads from the directory, and produces a DirEntry array of different
// directories.
//
// It iterates through all different filesystems that exist in the mfs MergeFS
// filesystem slice and it identifies overlapping directories that exist in different
// filesystems
func (mfs MergeFS) ReadDir(name string) ([]fs.DirEntry, error) {
dirsMap := make(map[string]fs.DirEntry)
for _, filesystem := range mfs.filesystems {
if fsys, ok := filesystem.(fs.ReadDirFS); ok {
dir, err := fsys.ReadDir(name)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
logger.Debugf("directory in filepath %s was not found in filesystem", name)
continue
}
return nil, err
}
for _, v := range dir {
if _, ok := dirsMap[v.Name()]; !ok {
dirsMap[v.Name()] = v
}
}
continue
}
file, err := filesystem.Open(name)
if err != nil {
logger.Debugf("filepath %s was not found in filesystem", name)
continue
}
dir, ok := file.(fs.ReadDirFile)
if !ok {
return nil, &fs.PathError{Op: "readdir", Path: name, Err: errors.New("not implemented")}
}
fsDirs, err := dir.ReadDir(-1)
if err != nil {
return nil, err
}
sort.Slice(fsDirs, func(i, j int) bool { return fsDirs[i].Name() < fsDirs[j].Name() })
for _, v := range fsDirs {
if _, ok := dirsMap[v.Name()]; !ok {
dirsMap[v.Name()] = v
}
}
if err := file.Close(); err != nil {
logger.Error("failed to close file", "err", err)
}
}
dirs := make([]fs.DirEntry, 0, len(dirsMap))
for _, value := range dirsMap {
dirs = append(dirs, value)
}
sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() })
return dirs, nil
}
@@ -1,68 +0,0 @@
package commands
import (
"io/fs"
"os"
"path/filepath"
"testing"
"testing/fstest"
"github.com/stretchr/testify/require"
)
func TestMergeFS(t *testing.T) {
var filePaths = []struct {
path string
dirArrayLength int
child string
}{
// MapFS takes in account the current directory in addition to all included directories and produces a "" dir
{"a", 1, "z"},
{"a/z", 1, "bar.cue"},
{"b", 1, "z"},
{"b/z", 1, "foo.cue"},
}
tempDir := os.DirFS(filepath.Join("testdata", "mergefs"))
a := fstest.MapFS{
"a": &fstest.MapFile{Mode: fs.ModeDir},
"a/z": &fstest.MapFile{Mode: fs.ModeDir},
"a/z/bar.cue": &fstest.MapFile{Data: []byte("bar")},
}
filesystem := Merge(tempDir, a)
t.Run("testing mergefs.ReadDir", func(t *testing.T) {
for _, fp := range filePaths {
t.Run("testing path: "+fp.path, func(t *testing.T) {
dirs, err := fs.ReadDir(filesystem, fp.path)
require.NoError(t, err)
require.Len(t, dirs, fp.dirArrayLength)
for i := 0; i < len(dirs); i++ {
require.Equal(t, dirs[i].Name(), fp.child)
}
})
}
})
t.Run("testing mergefs.Open", func(t *testing.T) {
data := make([]byte, 3)
file, err := filesystem.Open("a/z/bar.cue")
require.NoError(t, err)
_, err = file.Read(data)
require.NoError(t, err)
require.Equal(t, "bar", string(data))
file, err = filesystem.Open("b/z/foo.cue")
require.NoError(t, err)
_, err = file.Read(data)
require.NoError(t, err)
require.Equal(t, "foo", string(data))
err = file.Close()
require.NoError(t, err)
})
}
@@ -8,6 +8,7 @@ import (
"testing/fstest"
"github.com/grafana/grafana/pkg/schema/load"
"github.com/laher/mergefs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -35,7 +36,7 @@ func TestValidateScuemataBasics(t *testing.T) {
filesystem := fstest.MapFS{
"cue/data/gen.cue": &fstest.MapFile{Data: genCue},
}
mergedFS := Merge(filesystem, defaultBaseLoadPaths.BaseCueFS)
mergedFS := mergefs.Merge(filesystem, defaultBaseLoadPaths.BaseCueFS)
var baseLoadPaths = load.BaseLoadPaths{
BaseCueFS: mergedFS,
@@ -53,7 +54,7 @@ func TestValidateScuemataBasics(t *testing.T) {
filesystem := fstest.MapFS{
"cue/data/gen.cue": &fstest.MapFile{Data: genCue},
}
mergedFS := Merge(filesystem, defaultBaseLoadPaths.BaseCueFS)
mergedFS := mergefs.Merge(filesystem, defaultBaseLoadPaths.BaseCueFS)
var baseLoadPaths = load.BaseLoadPaths{
BaseCueFS: mergedFS,
@@ -78,7 +79,7 @@ func TestValidateScuemataBasics(t *testing.T) {
"valid.json": &fstest.MapFile{Data: validPanel},
"invalid.json": &fstest.MapFile{Data: invalidPanel},
}
mergedFS := Merge(filesystem, defaultBaseLoadPaths.BaseCueFS)
mergedFS := mergefs.Merge(filesystem, defaultBaseLoadPaths.BaseCueFS)
var baseLoadPaths = load.BaseLoadPaths{
BaseCueFS: mergedFS,
+2 -22
View File
@@ -5,18 +5,10 @@ import (
"fmt"
"cuelang.org/go/cue"
errs "cuelang.org/go/cue/errors"
"cuelang.org/go/cue/load"
"github.com/grafana/grafana/pkg/schema"
)
// cueError wraps errors caused by malformed cue files.
type cueError struct {
errors []errs.Error
filename string
line int
}
var panelSubpath = cue.MakePath(cue.Def("#Panel"))
func defaultOverlay(p BaseLoadPaths) (map[string]load.Source, error) {
@@ -47,9 +39,9 @@ func BaseDashboardFamily(p BaseLoadPaths) (schema.VersionedCueSchema, error) {
cfg := &load.Config{Overlay: overlay}
inst, err := rt.Build(load.Instances([]string{"/cue/data/gen.cue"}, cfg)[0])
if err != nil {
cueErrors := wrapCUEError(err)
cueError := schema.WrapCUEError(err)
if err != nil {
return nil, fmt.Errorf("errors: %q, in file: %s, on line: %d", cueErrors.errors, cueErrors.filename, cueErrors.line)
return nil, cueError
}
}
@@ -196,15 +188,3 @@ type CompositeDashboardSchema interface {
schema.VersionedCueSchema
LatestPanelSchemaFor(id string) (schema.VersionedCueSchema, error)
}
func wrapCUEError(err error) cueError {
var cErr errs.Error
if ok := errors.As(err, &cErr); ok {
return cueError{
errors: errs.Errors(err),
filename: errs.Errors(err)[0].Position().File().Name(),
line: errs.Errors(err)[0].Position().Line(),
}
}
return cueError{}
}
+12 -17
View File
@@ -6,16 +6,14 @@ import (
"os"
"path/filepath"
"testing"
"testing/fstest"
"github.com/grafana/grafana"
"github.com/grafana/grafana/pkg/schema"
"github.com/laher/mergefs"
"github.com/stretchr/testify/require"
)
var p BaseLoadPaths = BaseLoadPaths{
BaseCueFS: grafana.CoreSchema,
DistPluginCueFS: grafana.PluginSchema,
}
var p = GetDefaultLoadPaths()
// Basic well-formedness tests on core scuemata.
func TestScuemataBasics(t *testing.T) {
@@ -48,9 +46,6 @@ func TestScuemataBasics(t *testing.T) {
}
func TestDashboardValidity(t *testing.T) {
// TODO FIXME remove this once we actually have dashboard schema filled in
// enough that the tests pass, lol
t.Skip()
validdir := os.DirFS(filepath.Join("testdata", "artifacts", "dashboards"))
dash, err := BaseDashboardFamily(p)
@@ -87,9 +82,6 @@ func TestDashboardValidity(t *testing.T) {
func TestPanelValidity(t *testing.T) {
validdir := os.DirFS(filepath.Join("testdata", "artifacts", "panels"))
// dash, err := BaseDashboardFamily(p)
// require.NoError(t, err, "error while loading base dashboard scuemata")
ddash, err := DistDashboardFamily(p)
require.NoError(t, err, "error while loading dist dashboard scuemata")
@@ -111,7 +103,6 @@ func TestPanelValidity(t *testing.T) {
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
t.Skip()
b, err := validdir.Open(path)
require.NoError(t, err, "failed to open panel file")
@@ -125,22 +116,26 @@ func TestPanelValidity(t *testing.T) {
}
func TestCueErrorWrapper(t *testing.T) {
t.Run("Testing scuemata validity with valid cue schemas", func(t *testing.T) {
tempDir := os.DirFS(filepath.Join("testdata", "malformed_cue"))
t.Run("Testing cue error wrapper", func(t *testing.T) {
a := fstest.MapFS{
"cue/data/gen.cue": &fstest.MapFile{Data: []byte("{;;;;;;;;}")},
}
filesystem := mergefs.Merge(a, GetDefaultLoadPaths().BaseCueFS)
var baseLoadPaths = BaseLoadPaths{
BaseCueFS: tempDir,
BaseCueFS: filesystem,
DistPluginCueFS: GetDefaultLoadPaths().DistPluginCueFS,
}
_, err := BaseDashboardFamily(baseLoadPaths)
require.Error(t, err)
require.Contains(t, err.Error(), "in file")
require.Contains(t, err.Error(), "on line")
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(), "on line")
require.Contains(t, err.Error(), "line: ")
})
}
+1 -2
View File
@@ -58,7 +58,6 @@
"filterable": false
},
"decimals": 3,
"mappings": [],
"unit": "watt"
},
"overrides": [
@@ -150,4 +149,4 @@
"title": "with table",
"uid": "emal8gQMz",
"version": 2
}
}
+1 -2
View File
@@ -7,7 +7,6 @@
"filterable": false
},
"decimals": 3,
"mappings": [],
"unit": "watt"
},
"overrides": [
@@ -82,4 +81,4 @@
"maj": 0,
"min": 0
}
}
}
@@ -1 +0,0 @@
;;;;;;;
@@ -1,22 +0,0 @@
package scuemata
// Definition of the shape of a panel plugin's schema declarations in its
// schema.cue file.
//
// Note that these keys do not appear directly in any real JSON artifact;
// rather, they are composed into panel structures as they are defined within
// the larger Dashboard schema.
#PanelSchema: {
PanelOptions: {...}
PanelFieldConfig?: {...}
...
}
// A lineage of panel schema
#PanelLineage: [#PanelSchema, ...#PanelSchema]
// Panel plugin-specific Family
#PanelFamily: {
lineages: [#PanelLineage, ...#PanelLineage]
migrations: [...#Migration]
}
@@ -1,60 +0,0 @@
package scuemata
// A family is a collection of schemas that specify a single kind of object,
// allowing evolution of the canonical schema for that kind of object over time.
//
// The schemas are organized into a list of Lineages, which are themselves ordered
// lists of schemas where each schema with its predecessor in the lineage.
//
// If it is desired to define a schema with a breaking schema relative to its
// predecessors, a new Lineage must be created, as well as a Migration that defines
// a mapping to the new schema from the latest schema in prior Lineage.
//
// The version number of a schema is not controlled by the schema itself, but by
// its position in the list of lineages - e.g., 0.0 corresponds to the first
// schema in the first lineage.
#Family: {
lineages: [#Lineage, ...#Lineage]
migrations: [...#Migration]
let lseq = lineages[len(lineages)-1]
latest: #LastSchema & {_p: lseq}
}
// A Lineage is a non-empty list containing an ordered series of schemas that
// all describe a single kind of object, where each schema is backwards
// compatible with its predecessor.
#Lineage: [{...}, ...{...}]
#LastSchema: {
_p: #Lineage
_p[len(_p)-1]
}
// A Migration defines a relation between two schemas, "_from" and "_to". The
// relation expresses any complex mappings that must be performed to
// transform an input artifact valid with respect to the _from schema, into
// an artifact valid with respect to the _to schema. This is accomplished
// in two stages:
// 1. A Migration is initially defined by passing in schemas for _from and _to,
// and mappings that translate _from to _to are defined in _rel.
// 2. A concrete object may then be unified with _to, resulting in its values
// being mapped onto "result" by way of _rel.
//
// This is the absolute simplest possible definition of a Migration. It's
// incumbent on the implementor to manually ensure the correctness and
// completeness of the mapping. The primary value in defining such a generic
// structure is to allow comparably generic logic for migrating concrete
// artifacts through schema changes.
//
// If _to isn't backwards compatible (accretion-only) with _from, then _rel must
// explicitly enumerate every field in _from and map it to a field in _to, even
// if they're identical. This is laborious for anything outside trivially tiny
// schema. We'll want to eventually add helpers for whitelisting or blacklisting
// of paths in _from, so that migrations of larger schema can focus narrowly on
// the points of actual change.
#Migration: {
from: {...}
to: {...}
rel: {...}
result: to & rel
}
+41
View File
@@ -8,11 +8,28 @@ import (
"strings"
"cuelang.org/go/cue"
errs "cuelang.org/go/cue/errors"
cuejson "cuelang.org/go/pkg/encoding/json"
)
var rt = &cue.Runtime{}
// CueError wraps Errors caused by malformed cue files.
type CueError struct {
ErrorMap map[int]string
}
// Error func needed to implement standard golang error
func (cErr *CueError) Error() string {
var errorString string
if cErr.ErrorMap != nil {
for k, v := range cErr.ErrorMap {
errorString = errorString + fmt.Sprintf("line: %d, %s \n", k, v)
}
}
return errorString
}
// CueSchema represents a single, complete CUE-based schema that can perform
// operations on Resources.
//
@@ -93,6 +110,10 @@ func SearchAndValidate(s VersionedCueSchema, v interface{}) (VersionedCueSchema,
// collates all the individual errors, relates them to the schema that
// produced them, and ideally deduplicates repeated errors across each
// schema.
cueErrors := WrapCUEError(err)
if err != nil {
return nil, cueErrors
}
return nil, err
}
@@ -402,4 +423,24 @@ type Resource struct {
Value interface{}
}
// WrapCUEError is a wrapper for cueErrors that occur and are not self explanatory.
// If an error is of type cueErr, then iterate through the error array, export line number
// and filename, otherwise return usual error.
func WrapCUEError(err error) error {
var cErr errs.Error
m := make(map[int]string)
if ok := errors.As(err, &cErr); ok {
for _, e := range errs.Errors(err) {
if e.Position().File() != nil {
line := e.Position().Line()
m[line] = fmt.Sprintf("%q: in file %s", err, e.Position().File().Name())
}
}
}
if len(m) != 0 {
return &CueError{m}
}
return err
}
// TODO add migrator with SearchOption for stopping criteria