From 90d04990ca4e9c15f33ea2ff1ad2043111464751 Mon Sep 17 00:00:00 2001 From: James Shubin Date: Wed, 15 Nov 2023 16:30:24 -0500 Subject: [PATCH] engine, lang: Add an AST test that looks at fields too This gives us more options for testing when we need those kinds of more extensive resource examination features. --- engine/util/util.go | 46 ++ lang/interpret_test.go | 774 ++++++++++++++++++ lang/interpret_test/TestAstFunc3/hello0.txtar | 16 + 3 files changed, 836 insertions(+) create mode 100644 lang/interpret_test/TestAstFunc3/hello0.txtar diff --git a/engine/util/util.go b/engine/util/util.go index 1b98af25..b5545e55 100644 --- a/engine/util/util.go +++ b/engine/util/util.go @@ -289,6 +289,52 @@ func LangFieldNameToStructType(kind string) (map[string]*types.Type, error) { return st.Map, nil } +// ResToParamValues returns a list of field names and their corresponding values +// if they are non-zero. This is meant for testing, and should be improved for +// robustness or with tests if it's ever used for value extraction. +func ResToParamValues(res engine.Res) (map[string]types.Value, error) { + + ret := make(map[string]types.Value) + st := reflect.ValueOf(res).Elem() // pointer to struct, then struct + tt := reflect.TypeOf(res).Elem() // pointer to struct, then struct + + fields := []string{} + // TODO: private fields inside of a struct are still printed + vf := reflect.VisibleFields(tt) // []reflect.StructField + for _, field := range vf { + if field.Tag == "" { + continue // skip + } + if _, ok := field.Tag.Lookup(types.StructTag); !ok { + continue + } + + fields = append(fields, field.Name) + } + + for _, name := range fields { + rval := st.FieldByName(name) // exported field type + + // TODO: zero fields inside of a struct are still printed + if rval.IsZero() { + continue // skip zero values + } + + val, err := types.ValueOf(rval) + if err != nil { + // This can happen for bad fields like "Base" and so on. + // They are supposed to be skipped by the struct tag, + // but if this changes and we need to label them, then + // we can improve our above heuristic. + return nil, fmt.Errorf("field `%s` does not have a valid value: %+v", name, err) + } + + ret[name] = val + } + + return ret, nil +} + // GetUID returns the UID of an user. It supports an UID or an username. Caller // should first check user is not empty. It will return an error if it can't // lookup the UID or username. diff --git a/lang/interpret_test.go b/lang/interpret_test.go index 3a022e0b..56ad726f 100644 --- a/lang/interpret_test.go +++ b/lang/interpret_test.go @@ -32,7 +32,9 @@ import ( "testing" "time" + "github.com/purpleidea/mgmt/engine" "github.com/purpleidea/mgmt/engine/graph/autoedge" + engineUtil "github.com/purpleidea/mgmt/engine/util" "github.com/purpleidea/mgmt/etcd" "github.com/purpleidea/mgmt/lang/ast" "github.com/purpleidea/mgmt/lang/funcs/dage" @@ -1239,3 +1241,775 @@ func TestAstFunc2(t *testing.T) { t.Skip("skipping all tests...") } } + +// TestAstFunc3 is an even more advanced version which also examines parameter +// values. It briefly runs the function engine and captures output. Only use +// with stable, static output. +func TestAstFunc3(t *testing.T) { + const magicError = "# err: " + const magicErrorLexParse = "errLexParse: " + const magicErrorInit = "errInit: " + const magicInterpolate = "errInterpolate: " + const magicErrorSetScope = "errSetScope: " + const magicErrorUnify = "errUnify: " + const magicErrorGraph = "errGraph: " + const magicErrorInterpret = "errInterpret: " + const magicErrorAutoEdge = "errAutoEdge: " + const magicEmpty = "# empty!" + dir, err := util.TestDirFull() + if err != nil { + t.Errorf("could not get tests directory: %+v", err) + return + } + t.Logf("tests directory is: %s", dir) + + variables := map[string]interfaces.Expr{ + "purpleidea": &ast.ExprStr{V: "hello world!"}, // james says hi + // TODO: change to a func when we can change hostname dynamically! + "hostname": &ast.ExprStr{V: ""}, // NOTE: empty b/c not used + } + consts := ast.VarPrefixToVariablesScope(vars.ConstNamespace) // strips prefix! + addback := vars.ConstNamespace + interfaces.ModuleSep // add it back... + variables, err = ast.MergeExprMaps(variables, consts, addback) + if err != nil { + t.Errorf("couldn't merge in consts: %+v", err) + return + } + + scope := &interfaces.Scope{ // global scope + Variables: variables, + // all the built-in top-level, core functions enter here... + Functions: ast.FuncPrefixToFunctionsScope(""), // runs funcs.LookupPrefix + } + + type test struct { // an individual test + name string + path string // relative txtar path inside tests dir + } + testCases := []test{} + + // build test array automatically from reading the dir + files, err := ioutil.ReadDir(dir) + if err != nil { + t.Errorf("could not read through tests directory: %+v", err) + return + } + sorted := []string{} + for _, f := range files { + if f.IsDir() { + continue + } + if !strings.HasSuffix(f.Name(), ".txtar") { + continue + } + + sorted = append(sorted, f.Name()) + } + sort.Strings(sorted) + for _, f := range sorted { + // add automatic test case + testCases = append(testCases, test{ + name: fmt.Sprintf("%s", f), + path: f, // .txtar + }) + } + + if testing.Short() { + t.Logf("available tests:") + } + names := []string{} + for index, tc := range testCases { // run all the tests + if tc.name == "" { + t.Errorf("test #%d: not named", index) + continue + } + if util.StrInList(tc.name, names) { + t.Errorf("test #%d: duplicate sub test name of: %s", index, tc.name) + continue + } + names = append(names, tc.name) + + //if index != 3 { // hack to run a subset (useful for debugging) + //if tc.name != "simple operators" { + // continue + //} + + testName := fmt.Sprintf("test #%d (%s)", index, tc.name) + if testing.Short() { // make listing tests easier + t.Logf("%s", testName) + continue + } + t.Run(testName, func(t *testing.T) { + name, path := tc.name, tc.path + tmpdir := t.TempDir() // gets cleaned up at end, new dir for each call + src := tmpdir // location of the test + txtarFile := dir + path + + archive, err := txtar.ParseFile(txtarFile) + if err != nil { + t.Errorf("err parsing txtar(%s): %+v", txtarFile, err) + return + } + comment := strings.TrimSpace(string(archive.Comment)) + t.Logf("comment: %s\n", comment) + + // copy files out into the test temp directory + var testOutput []byte + found := false + for _, file := range archive.Files { + if file.Name == "OUTPUT" { + testOutput = file.Data + found = true + continue + } + + name := filepath.Join(tmpdir, file.Name) + dir := filepath.Dir(name) + if err := os.MkdirAll(dir, 0770); err != nil { + t.Errorf("err making dir(%s): %+v", dir, err) + return + } + if err := ioutil.WriteFile(name, file.Data, 0660); err != nil { + t.Errorf("err writing file(%s): %+v", name, err) + return + } + } + + if !found { // skip missing tests + return + } + + expstr := string(testOutput) // expected graph + + // if the graph file has a magic error string, it's a failure + errStr := "" + failLexParse := false + failInit := false + failInterpolate := false + failSetScope := false + failUnify := false + failGraph := false + failInterpret := false + failAutoEdge := false + if strings.HasPrefix(expstr, magicError) { + errStr = strings.TrimPrefix(expstr, magicError) + expstr = errStr + + if strings.HasPrefix(expstr, magicErrorLexParse) { + errStr = strings.TrimPrefix(expstr, magicErrorLexParse) + expstr = errStr + failLexParse = true + } + if strings.HasPrefix(expstr, magicErrorInit) { + errStr = strings.TrimPrefix(expstr, magicErrorInit) + expstr = errStr + failInit = true + } + if strings.HasPrefix(expstr, magicInterpolate) { + errStr = strings.TrimPrefix(expstr, magicInterpolate) + expstr = errStr + failInterpolate = true + } + if strings.HasPrefix(expstr, magicErrorSetScope) { + errStr = strings.TrimPrefix(expstr, magicErrorSetScope) + expstr = errStr + failSetScope = true + } + if strings.HasPrefix(expstr, magicErrorUnify) { + errStr = strings.TrimPrefix(expstr, magicErrorUnify) + expstr = errStr + failUnify = true + } + if strings.HasPrefix(expstr, magicErrorGraph) { + errStr = strings.TrimPrefix(expstr, magicErrorGraph) + expstr = errStr + failGraph = true + } + if strings.HasPrefix(expstr, magicErrorInterpret) { + errStr = strings.TrimPrefix(expstr, magicErrorInterpret) + expstr = errStr + failInterpret = true + } + if strings.HasPrefix(expstr, magicErrorAutoEdge) { + errStr = strings.TrimPrefix(expstr, magicErrorAutoEdge) + expstr = errStr + failAutoEdge = true + } + } + + fail := errStr != "" + expstr = strings.Trim(expstr, "\n") + + t.Logf("\n\ntest #%d (%s) ----------------\npath: %s\n\n", index, name, src) + + logf := func(format string, v ...interface{}) { + t.Logf(fmt.Sprintf("test #%d", index)+": "+format, v...) + } + mmFs := afero.NewMemMapFs() + afs := &afero.Afero{Fs: mmFs} // wrap so that we're implementing ioutil + fs := &util.Fs{Afero: afs} + + // implementation of the World API (alternatives can be substituted in) + world := &etcd.World{ + //Hostname: hostname, + //Client: etcdClient, + //MetadataPrefix: /fs, // MetadataPrefix + //StoragePrefix: "/storage", // StoragePrefix + // TODO: is this correct? (seems to work for testing) + StandaloneFs: fs, // used for static deploys + Debug: testing.Verbose(), // set via the -test.v flag to `go test` + Logf: func(format string, v ...interface{}) { + logf("world: etcd: "+format, v...) + }, + } + + // use this variant, so that we don't copy the dir name + // this is the equivalent to running `rsync -a src/ /` + if err := util.CopyDiskContentsToFs(fs, src, "/", false); err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: CopyDiskContentsToFs failed: %+v", index, err) + return + } + + // this shows us what we pulled in from the test dir: + tree0, err := util.FsTree(fs, "/") + if err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: FsTree failed: %+v", index, err) + return + } + logf("tree:\n%s", tree0) + + input := "/" + logf("input: %s", input) + + output, err := inputs.ParseInput(input, fs) // raw code can be passed in + if err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: ParseInput failed: %+v", index, err) + return + } + for _, fn := range output.Workers { + if err := fn(fs); err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: worker execution failed: %+v", index, err) + return + } + } + tree, err := util.FsTree(fs, "/") + if err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: FsTree failed: %+v", index, err) + return + } + logf("tree:\n%s", tree) + + logf("main:\n%s", output.Main) // debug + + reader := bytes.NewReader(output.Main) + xast, err := parser.LexParse(reader) + if (!fail || !failLexParse) && err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: lex/parse failed with: %+v", index, err) + return + } + if failLexParse && err != nil { + s := err.Error() // convert to string + if s != expstr { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: expected different error", index) + t.Logf("test #%d: err: %s", index, s) + t.Logf("test #%d: exp: %s", index, expstr) + } + return // fail happened during lex parse, don't run init/interpolate! + } + if failLexParse && err == nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: lex/parse passed, expected fail", index) + return + } + + t.Logf("test #%d: AST: %+v", index, xast) + + importGraph, err := pgraph.NewGraph("importGraph") + if err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: could not create graph: %+v", index, err) + return + } + importVertex := &pgraph.SelfVertex{ + Name: "", // first node is the empty string + Graph: importGraph, // store a reference to ourself + } + importGraph.AddVertex(importVertex) + + data := &interfaces.Data{ + // TODO: add missing fields here if/when needed + Fs: fs, + FsURI: "memmapfs:///", // we're in standalone mode + Base: output.Base, // base dir (absolute path) the metadata file is in + Files: output.Files, // no really needed here afaict + Imports: importVertex, + Metadata: output.Metadata, + Modules: "/" + interfaces.ModuleDirectory, // not really needed here afaict + + LexParser: parser.LexParse, + StrInterpolater: interpolate.StrInterpolate, + + Debug: testing.Verbose(), // set via the -test.v flag to `go test` + Logf: func(format string, v ...interface{}) { + logf("ast: "+format, v...) + }, + } + // some of this might happen *after* interpolate in SetScope or Unify... + err = xast.Init(data) + if (!fail || !failInit) && err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: could not init and validate AST: %+v", index, err) + return + } + if failInit && err != nil { + s := err.Error() // convert to string + if s != expstr { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: expected different error", index) + t.Logf("test #%d: err: %s", index, s) + t.Logf("test #%d: exp: %s", index, expstr) + } + return // fail happened during lex parse, don't run init/interpolate! + } + if failInit && err == nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: Init passed, expected fail", index) + return + } + + iast, err := xast.Interpolate() + if (!fail || !failInterpolate) && err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: Interpolate failed with: %+v", index, err) + return + } + if failInterpolate && err != nil { + s := err.Error() // convert to string + if s != expstr { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: expected different error", index) + t.Logf("test #%d: err: %s", index, s) + t.Logf("test #%d: exp: %s", index, expstr) + } + return // fail happened during lex parse, don't run init/interpolate! + } + if failInterpolate && err == nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: Interpolate passed, expected fail", index) + return + } + + // propagate the scope down through the AST... + err = iast.SetScope(scope) + if (!fail || !failSetScope) && err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: could not set scope: %+v", index, err) + return + } + if failSetScope && err != nil { + s := err.Error() // convert to string + if s != expstr { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: expected different error", index) + t.Logf("test #%d: err: %s", index, s) + t.Logf("test #%d: exp: %s", index, expstr) + } + return // fail happened during set scope, don't run unification! + } + if failSetScope && err == nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: set scope passed, expected fail", index) + return + } + + if runGraphviz { + t.Logf("test #%d: Running graphviz after setScope...", index) + + // build a graph of the AST, to make sure everything is connected properly + graph, err := pgraph.NewGraph("setScope") + if err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: could not create setScope graph: %+v", index, err) + return + } + ast, ok := iast.(interfaces.ScopeGrapher) + if !ok { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: can't graph scope", index) + return + } + ast.ScopeGraph(graph) + + if err := graph.ExecGraphviz("/tmp/set-scope.dot"); err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: writing graph failed: %+v", index, err) + return + } + } + + // apply type unification + xlogf := func(format string, v ...interface{}) { + logf("unification: "+format, v...) + } + unifier := &unification.Unifier{ + AST: iast, + Solver: unification.SimpleInvariantSolverLogger(xlogf), + Debug: testing.Verbose(), + Logf: xlogf, + } + err = unifier.Unify() + if (!fail || !failUnify) && err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: could not unify types: %+v", index, err) + return + } + if failUnify && err != nil { + s := err.Error() // convert to string + if s != expstr { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: expected different error", index) + t.Logf("test #%d: err: %s", index, s) + t.Logf("test #%d: exp: %s", index, expstr) + } + return // fail happened during unification, don't run Graph! + } + if failUnify && err == nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: unification passed, expected fail", index) + return + } + // XXX: Should we do a kind of SetType on resources here + // to tell the ones with variant fields what their + // concrete field types are? They should only be dynamic + // in implementation and before unification, and static + // once we've unified the specific resource. + + // build the function graph + fgraph, err := iast.Graph() + if (!fail || !failGraph) && err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: functions failed with: %+v", index, err) + return + } + if failGraph && err != nil { // can't process graph if it's nil + s := err.Error() // convert to string + if s != expstr { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: expected different error", index) + t.Logf("test #%d: err: %s", index, s) + t.Logf("test #%d: exp: %s", index, expstr) + } + return + } + if failGraph && err == nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: functions passed, expected fail", index) + return + } + + if fgraph.NumVertices() == 0 { // no funcs to load! + //t.Errorf("test #%d: FAIL", index) + t.Logf("test #%d: function graph is empty", index) + //return // let's test the engine on empty + } + + t.Logf("test #%d: graph: %s", index, fgraph) + for i, v := range fgraph.Vertices() { + t.Logf("test #%d: vertex(%d): %+v", index, i, v) + } + for v1 := range fgraph.Adjacency() { + for v2, e := range fgraph.Adjacency()[v1] { + t.Logf("test #%d: edge(%+v): %+v -> %+v", index, e, v1, v2) + } + } + + if runGraphviz { + t.Logf("test #%d: Running graphviz...", index) + if err := fgraph.ExecGraphviz("/tmp/graphviz.dot"); err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: writing graph failed: %+v", index, err) + return + } + } + + // run the function engine once to get some real output + funcs := &dage.Engine{ + Name: "test", + Hostname: "", // NOTE: empty b/c not used + World: world, // used partially in some tests + Debug: testing.Verbose(), // set via the -test.v flag to `go test` + Logf: func(format string, v ...interface{}) { + logf("funcs: "+format, v...) + }, + } + + logf("function engine initializing...") + if err := funcs.Setup(); err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: init error with func engine: %+v", index, err) + return + } + defer funcs.Cleanup() + + // XXX: can we type check things somehow? + //logf("function engine validating...") + //if err := funcs.Validate(); err != nil { + // t.Errorf("test #%d: FAIL", index) + // t.Errorf("test #%d: validate error with func engine: %+v", index, err) + // return + //} + + logf("function engine starting...") + wg := &sync.WaitGroup{} + defer wg.Wait() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + wg.Add(1) + go func() { + defer wg.Done() + if err := funcs.Run(ctx); err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: run error with func engine: %+v", index, err) + return + } + }() + + //wg.Add(1) + //go func() { // XXX: debugging + // defer wg.Done() + // for { + // select { + // case <-time.After(100 * time.Millisecond): // blocked functions + // t.Logf("test #%d: graphviz...", index) + // funcs.Graphviz("") // log to /tmp/... + // + // case <-ctx.Done(): + // return + // } + // } + //}() + + <-funcs.Started() // wait for startup (will not block forever) + + // Sanity checks for graph size. + if count := funcs.NumVertices(); count != 0 { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: expected empty graph on start, got %d vertices", index, count) + } + defer func() { + if count := funcs.NumVertices(); count != 0 { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: expected empty graph on exit, got %d vertices", index, count) + } + }() + defer wg.Wait() + defer cancel() + + txn := funcs.Txn() + defer txn.Free() // remember to call Free() + txn.AddGraph(fgraph) + if err := txn.Commit(); err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: run error with initial commit: %+v", index, err) + return + } + defer txn.Reverse() // should remove everything we added + + isEmpty := make(chan struct{}) + if fgraph.NumVertices() == 0 { // no funcs to load! + close(isEmpty) + } + + // wait for some activity + logf("stream...") + stream := funcs.Stream() + //select { + //case err, ok := <-stream: + // if !ok { + // t.Errorf("test #%d: FAIL", index) + // t.Errorf("test #%d: stream closed", index) + // return + // } + // if err != nil { + // t.Errorf("test #%d: FAIL", index) + // t.Errorf("test #%d: stream errored: %+v", index, err) + // return + // } + // + //case <-time.After(60 * time.Second): // blocked functions + // t.Errorf("test #%d: FAIL", index) + // t.Errorf("test #%d: stream timeout", index) + // return + //} + + // sometimes the <-stream seems to constantly (or for a + // long time?) win the races against the <-time.After(), + // so add some limit to how many times we need to stream + max := 1 + Loop: + for { + select { + case err, ok := <-stream: + if !ok { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: stream closed", index) + return + } + if err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: stream errored: %+v", index, err) + return + } + t.Logf("test #%d: got stream event!", index) + max-- + if max == 0 { + break Loop + } + + case <-isEmpty: + break Loop + + case <-time.After(10 * time.Second): // blocked functions + t.Errorf("test #%d: unblocking because no event was sent by the function engine for a while", index) + break Loop + + case <-time.After(60 * time.Second): // blocked functions + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: stream timeout", index) + return + } + } + + t.Logf("test #%d: %s", index, funcs.Stats()) + + // run interpret! + table := funcs.Table() // map[interfaces.Func]types.Value + + ograph, err := interpret.Interpret(iast, table) + if (!fail || !failInterpret) && err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: interpret failed with: %+v", index, err) + return + } + if failInterpret && err != nil { // can't process graph if it's nil + s := err.Error() // convert to string + if s != expstr { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: expected different error", index) + t.Logf("test #%d: err: %s", index, s) + t.Logf("test #%d: exp: %s", index, expstr) + } + return + } + if failInterpret && err == nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: interpret passed, expected fail", index) + return + } + + // add automatic edges... + err = autoedge.AutoEdge(ograph, testing.Verbose(), logf) + if (!fail || !failAutoEdge) && err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: automatic edges failed with: %+v", index, err) + return + } + if failAutoEdge && err != nil { + s := err.Error() // convert to string + if s != expstr { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: expected different error", index) + t.Logf("test #%d: err: %s", index, s) + t.Logf("test #%d: exp: %s", index, expstr) + } + return + } + if failAutoEdge && err == nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: automatic edges passed, expected fail", index) + return + } + + // TODO: perform autogrouping? + + t.Logf("test #%d: graph: %+v", index, ograph) + str := strings.Trim(ograph.Sprint(), "\n") // text format of output graph + + for i, v := range ograph.Vertices() { + res, ok := v.(engine.Res) + if !ok { + t.Errorf("test #%d: FAIL\n\n", index) + t.Logf("test #%d: unexpected non-resource: %+v", index, v) + return + } + m, err := engineUtil.ResToParamValues(res) + if err != nil { + t.Errorf("test #%d: FAIL\n\n", index) + t.Logf("test #%d: can't read resource: %+v", index, err) + return + } + if i == 0 { + str += "\n" + } + keys := []string{} + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) // sort for determinism + for _, field := range keys { + v := m[field] + str += fmt.Sprintf("Field: %s[%s].%s = %s\n", res.Kind(), res.Name(), field, v) + } + if i < len(ograph.Vertices()) { + str += "\n" + } + } + + if expstr == magicEmpty { + expstr = "" + } + // XXX: something isn't consistent, and I can't figure + // out what, so workaround this by sorting these :( + sortHack := func(x string) string { + l := strings.Split(strings.TrimSpace(x), "\n") + sort.Strings(l) + return strings.TrimSpace(strings.Join(l, "\n")) + } + str = sortHack(str) + expstr = sortHack(expstr) + if expstr != str { + t.Errorf("test #%d: FAIL\n\n", index) + t.Logf("test #%d: actual (g1):\n%s\n\n", index, str) + t.Logf("test #%d: expected (g2):\n%s\n\n", index, expstr) + diff := pretty.Compare(str, expstr) + if diff != "" { // bonus + t.Logf("test #%d: diff:\n%s", index, diff) + } + return + } + + for i, v := range ograph.Vertices() { + t.Logf("test #%d: vertex(%d): %+v", index, i, v) + } + for v1 := range ograph.Adjacency() { + for v2, e := range ograph.Adjacency()[v1] { + t.Logf("test #%d: edge(%+v): %+v -> %+v", index, e, v1, v2) + } + } + + if !t.Failed() { + t.Logf("test #%d: Passed!", index) + } + }) + } + if testing.Short() { + t.Skip("skipping all tests...") + } +} diff --git a/lang/interpret_test/TestAstFunc3/hello0.txtar b/lang/interpret_test/TestAstFunc3/hello0.txtar new file mode 100644 index 00000000..f40ff828 --- /dev/null +++ b/lang/interpret_test/TestAstFunc3/hello0.txtar @@ -0,0 +1,16 @@ +-- main.mcl -- +test "hello" { + boolptr => true, + anotherstr => "bye", + mixedstruct => struct{ + somebool => true, + somestr => "inside struct", + someint => 42, + somefloat => 3.14, + }, +} +-- OUTPUT -- +Field: test[hello].AnotherStr = "bye" +Field: test[hello].BoolPtr = true +Field: test[hello].MixedStruct = struct{somebool: true; somestr: "inside struct"; someint: 42; somefloat: 3.14; somePrivatefield: ""} +Vertex: test[hello]