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:
@@ -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.
|
||||||
|
|||||||
@@ -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...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
16
lang/interpret_test/TestAstFunc3/hello0.txtar
Normal file
16
lang/interpret_test/TestAstFunc3/hello0.txtar
Normal 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]
|
||||||
Reference in New Issue
Block a user