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.
This commit is contained in:
James Shubin
2023-11-15 16:30:24 -05:00
parent aa001ed2dc
commit 90d04990ca
3 changed files with 836 additions and 0 deletions

View File

@@ -289,6 +289,52 @@ func LangFieldNameToStructType(kind string) (map[string]*types.Type, error) {
return st.Map, nil 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 // 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 // should first check user is not empty. It will return an error if it can't
// lookup the UID or username. // lookup the UID or username.

View File

@@ -32,7 +32,9 @@ import (
"testing" "testing"
"time" "time"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/graph/autoedge" "github.com/purpleidea/mgmt/engine/graph/autoedge"
engineUtil "github.com/purpleidea/mgmt/engine/util"
"github.com/purpleidea/mgmt/etcd" "github.com/purpleidea/mgmt/etcd"
"github.com/purpleidea/mgmt/lang/ast" "github.com/purpleidea/mgmt/lang/ast"
"github.com/purpleidea/mgmt/lang/funcs/dage" "github.com/purpleidea/mgmt/lang/funcs/dage"
@@ -1239,3 +1241,775 @@ func TestAstFunc2(t *testing.T) {
t.Skip("skipping all tests...") 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, // <something>.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...")
}
}

View File

@@ -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]