func TestMerge(t *testing.T) { YAML := func(s string) map[interface{}]interface{} { y, err := simpleyaml.NewYaml([]byte(s)) So(err, ShouldBeNil) data, err := y.Map() So(err, ShouldBeNil) return data } valueIs := func(tree interface{}, path string, expect string) { c, err := ParseCursor(path) So(err, ShouldBeNil) v, err := c.ResolveString(tree) So(err, ShouldBeNil) So(v, ShouldEqual, expect) } notPresent := func(tree interface{}, path string) { c, err := ParseCursor(path) So(err, ShouldBeNil) _, err = c.ResolveString(tree) So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, "could not be found") } Convey("Merge()", t, func() { Convey("leaves original object untouched when merging", func() { template := YAML(`props: toplevel: TEMPLATE VALUE sub: key: ANOTHER TEMPLATE VALUE `) other := YAML(`props: toplevel: override `) merged, err := Merge(template, other) So(err, ShouldBeNil) valueIs(template, "props.toplevel", "TEMPLATE VALUE") valueIs(template, "props.sub.key", "ANOTHER TEMPLATE VALUE") valueIs(other, "props.toplevel", "override") notPresent(other, "props.sub.key") valueIs(merged, "props.toplevel", "override") valueIs(merged, "props.sub.key", "ANOTHER TEMPLATE VALUE") }) }) }
func parseYAML(data []byte) (map[interface{}]interface{}, error) { y, err := simpleyaml.NewYaml(data) if err != nil { return nil, err } doc, err := y.Map() if err != nil { return nil, fmt.Errorf("Root of YAML document is not a hash/map: %s\n", err.Error()) } return doc, nil }
func jsonifyData(data []byte) (string, error) { y, err := simpleyaml.NewYaml(data) if err != nil { return "", err } doc, err := y.Map() if err != nil { return "", ansi.Errorf("@R{Root of YAML document is not a hash/map}: %s\n", err.Error()) } b, err := json.Marshal(deinterface(doc)) if err != nil { return "", err } return string(b), nil }
func TestOperators(t *testing.T) { cursor := func(s string) *tree.Cursor { c, err := tree.ParseCursor(s) So(err, ShouldBeNil) return c } YAML := func(s string) map[interface{}]interface{} { y, err := simpleyaml.NewYaml([]byte(s)) So(err, ShouldBeNil) data, err := y.Map() So(err, ShouldBeNil) return data } ref := func(s string) *Expr { return &Expr{Type: Reference, Reference: cursor(s)} } str := func(s string) *Expr { return &Expr{Type: Literal, Literal: s} } num := func(v int64) *Expr { return &Expr{Type: Literal, Literal: v} } null := func() *Expr { return &Expr{Type: Literal, Literal: nil} } env := func(s string) *Expr { return &Expr{Type: EnvVar, Name: s} } or := func(l *Expr, r *Expr) *Expr { return &Expr{Type: LogicalOr, Left: l, Right: r} } var exprOk func(*Expr, *Expr) exprOk = func(got *Expr, want *Expr) { So(got, ShouldNotBeNil) So(want, ShouldNotBeNil) So(got.Type, ShouldEqual, want.Type) switch want.Type { case Literal: So(got.Literal, ShouldEqual, want.Literal) case Reference: So(got.Reference.String(), ShouldEqual, want.Reference.String()) case LogicalOr: exprOk(got.Left, want.Left) exprOk(got.Right, want.Right) } } Convey("Parser", t, func() { Convey("parses op calls in their entirety", func() { phase := EvalPhase opOk := func(code string, name string, args ...*Expr) { op, err := ParseOpcall(phase, code) So(err, ShouldBeNil) So(op, ShouldNotBeNil) _, ok := op.op.(NullOperator) So(ok, ShouldBeTrue) So(op.op.(NullOperator).Missing, ShouldEqual, name) So(len(op.args), ShouldEqual, len(args)) for i, expect := range args { exprOk(op.args[i], expect) } } opErr := func(code string, msg string) { _, err := ParseOpcall(phase, code) So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, msg) } Convey("handles opcodes with and without arguments", func() { opOk(`(( null ))`, "null") opOk(`(( null 42 ))`, "null", num(42)) opOk(`(( null 1 2 3 4 ))`, "null", num(1), num(2), num(3), num(4)) }) Convey("ignores optional whitespace", func() { opOk(`((null))`, "null") opOk(`(( null ))`, "null") opOk(`(( null ))`, "null") args := []*Expr{num(1), num(2), num(3)} opOk(`((null 1 2 3))`, "null", args...) opOk(`((null 1 2 3))`, "null", args...) opOk(`((null 1 2 3 ))`, "null", args...) opOk(`((null 1 2 3 ))`, "null", args...) }) Convey("allows use of commas to separate arguments", func() { args := []*Expr{num(1), num(2), num(3)} opOk(`((null 1, 2, 3))`, "null", args...) opOk(`((null 1, 2, 3))`, "null", args...) opOk(`((null 1, 2, 3, ))`, "null", args...) opOk(`((null 1 , 2 , 3 , ))`, "null", args...) }) Convey("allows use of parentheses around arguments", func() { args := []*Expr{num(1), num(2), num(3)} opOk(`((null(1,2,3)))`, "null", args...) opOk(`((null(1, 2, 3) ))`, "null", args...) opOk(`((null( 1, 2, 3)))`, "null", args...) opOk(`((null (1, 2, 3) ))`, "null", args...) opOk(`((null (1 , 2 , 3) ))`, "null", args...) }) Convey("handles string literal arguments", func() { opOk(`(( null "string" ))`, "null", str("string")) opOk(`(( null "string with whitespace" ))`, "null", str("string with whitespace")) opOk(`(( null "a \"quoted\" string" ))`, "null", str(`a "quoted" string`)) opOk(`(( null "\\escaped" ))`, "null", str(`\escaped`)) }) Convey("handles reference (cursor) arguments", func() { opOk(`(( null x.y.z ))`, "null", ref("x.y.z")) opOk(`(( null x.[0].z ))`, "null", ref("x.0.z")) opOk(`(( null x[0].z ))`, "null", ref("x.0.z")) opOk(`(( null x[0]z ))`, "null", ref("x.0.z")) }) Convey("handles mixed collections of argument types", func() { opOk(`(( xyzzy "string" x.y.z 42 ))`, "xyzzy", str("string"), ref("x.y.z"), num(42)) opOk(`(( xyzzy("string" x.y.z 42) ))`, "xyzzy", str("string"), ref("x.y.z"), num(42)) }) Convey("handles expression-based operands", func() { opOk(`(( null meta.key || "default" ))`, "null", or(ref("meta.key"), str("default"))) opOk(`(( null meta.key || "default" "second" ))`, "null", or(ref("meta.key"), str("default")), str("second")) opOk(`(( null meta.key || "default", "second" ))`, "null", or(ref("meta.key"), str("default")), str("second")) opOk(`(( null meta.key || "default", meta.other || nil ))`, "null", or(ref("meta.key"), str("default")), or(ref("meta.other"), null())) opOk(`(( null meta.key || "default" meta.other || nil ))`, "null", or(ref("meta.key"), str("default")), or(ref("meta.other"), null())) }) Convey("handles environment variables as operands", func() { os.Setenv("SPRUCE_FOO", "first test") os.Setenv("_SPRUCE", "_sprucify!") os.Setenv("ENOENT", "") os.Setenv("http_proxy", "no://thank/you") opOk(`(( null $SPRUCE_FOO ))`, "null", env("SPRUCE")) opOk(`(( null $_SPRUCE ))`, "null", env("_SPRUCE")) opOk(`(( null $ENOENT || $SPRUCE_FOO ))`, "null", or(env("ENOENT"), env("SPRUCE_FOO"))) opOk(`(( null $http_proxy))`, "null", env("http_proxy")) }) Convey("throws errors for malformed expression", func() { opErr(`(( null meta.key ||, nil ))`, `syntax error near: meta.key ||, nil`) opErr(`(( null || ))`, `syntax error near: ||`) opErr(`(( null || meta.key ))`, `syntax error near: || meta.key`) opErr(`(( null meta.key || || ))`, `syntax error near: meta.key || ||`) }) }) }) Convey("Expression Engine", t, func() { var e *Expr var tree map[interface{}]interface{} evaluate := func(e *Expr, tree map[interface{}]interface{}) interface{} { v, err := e.Evaluate(tree) So(err, ShouldBeNil) return v } Convey("Literals evaluate to themselves", func() { e = &Expr{Type: Literal, Literal: "value"} So(evaluate(e, tree), ShouldEqual, "value") e = &Expr{Type: Literal, Literal: ""} So(evaluate(e, tree), ShouldEqual, "") e = &Expr{Type: Literal, Literal: nil} So(evaluate(e, tree), ShouldEqual, nil) }) Convey("References evaluate to the referenced part of the YAML tree", func() { tree = YAML(`--- meta: foo: FOO bar: BAR `) e = &Expr{Type: Reference, Reference: cursor("meta.foo")} So(evaluate(e, tree), ShouldEqual, "FOO") e = &Expr{Type: Reference, Reference: cursor("meta.bar")} So(evaluate(e, tree), ShouldEqual, "BAR") }) Convey("|| operator evaluates to the first found value", func() { tree = YAML(`--- meta: foo: FOO bar: BAR `) So(evaluate(or(str("first"), str("second")), tree), ShouldEqual, "first") So(evaluate(or(ref("meta.foo"), str("second")), tree), ShouldEqual, "FOO") So(evaluate(or(ref("meta.ENOENT"), ref("meta.foo")), tree), ShouldEqual, "FOO") }) Convey("|| operator treats nil as a found value", func() { tree = YAML(`--- meta: foo: FOO bar: BAR `) So(evaluate(or(null(), str("second")), tree), ShouldBeNil) So(evaluate(or(ref("meta.ENOENT"), null()), tree), ShouldBeNil) }) }) Convey("Expression Reduction Algorithm", t, func() { var orig, final *Expr var err error Convey("ignores singleton expression", func() { orig = str("string") final, err = orig.Reduce() So(err, ShouldBeNil) exprOk(final, orig) orig = null() final, err = orig.Reduce() So(err, ShouldBeNil) exprOk(final, orig) orig = ref("meta.key") final, err = orig.Reduce() So(err, ShouldBeNil) exprOk(final, orig) }) Convey("handles normal alternates that terminated in a literal", func() { orig = or(ref("a.b.c"), str("default")) final, err = orig.Reduce() So(err, ShouldBeNil) exprOk(final, orig) }) Convey("throws errors (warnings) for unreachable alternates", func() { orig = or(null(), str("ignored")) final, err = orig.Reduce() So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, `literal nil short-circuits expression (nil || "ignored")`) exprOk(final, null()) orig = or(ref("some.key"), or(str("default"), ref("ignored.key"))) final, err = orig.Reduce() So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, `literal "default" short-circuits expression (some.key || "default" || ignored.key)`) exprOk(final, or(ref("some.key"), str("default"))) orig = or(or(ref("some.key"), str("default")), ref("ignored.key")) final, err = orig.Reduce() So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, `literal "default" short-circuits expression (some.key || "default" || ignored.key)`) exprOk(final, or(ref("some.key"), str("default"))) }) }) Convey("File Operator", t, func() { op := FileOperator{} ev := &Evaluator{ Tree: YAML( `meta: sample_file: assets/file_operator/sample.txt `), } basedir, _ := os.Getwd() Convey("can read a direct file", func() { r, err := op.Run(ev, []*Expr{ str("assets/file_operator/test.txt"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "This is a test\n") }) Convey("can read a file from a reference", func() { r, err := op.Run(ev, []*Expr{ ref("meta.sample_file"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) content, err := ioutil.ReadFile("assets/file_operator/sample.txt") So(r.Value.(string), ShouldEqual, string(content)) }) Convey("can read a file relative to a specified base path", func() { os.Setenv("SPRUCE_FILE_BASE_PATH", filepath.Join(basedir, "assets/file_operator")) r, err := op.Run(ev, []*Expr{ str("test.txt"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "This is a test\n") }) if _, err := os.Stat("/etc/hosts"); err == nil { Convey("can read an absolute path", func() { os.Setenv("SPRUCE_FILE_BASE_PATH", filepath.Join(basedir, "assets/file_operator")) r, err := op.Run(ev, []*Expr{ str("/etc/hosts"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) content, err := ioutil.ReadFile("/etc/hosts") So(r.Value.(string), ShouldEqual, string(content)) }) } Convey("can handle a missing file", func() { r, err := op.Run(ev, []*Expr{ str("no_one_should_ever_name_a_file_that_doesnt_exist_this_name"), }) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) }) Convey("Grab Operator", t, func() { op := GrabOperator{} ev := &Evaluator{ Tree: YAML( `key: subkey: value: found it other: value 2 list1: - first - second list2: - third - fourth lonely: - one `), } Convey("can grab a single value", func() { r, err := op.Run(ev, []*Expr{ ref("key.subkey.value"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "found it") }) Convey("can grab a single list value", func() { r, err := op.Run(ev, []*Expr{ ref("key.lonely"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) l, ok := r.Value.([]interface{}) So(ok, ShouldBeTrue) So(len(l), ShouldEqual, 1) So(l[0], ShouldEqual, "one") }) Convey("can grab a multiple lists and flatten them", func() { r, err := op.Run(ev, []*Expr{ ref("key.list1"), ref("key.lonely"), ref("key.list2"), ref("key.lonely.0"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) l, ok := r.Value.([]interface{}) So(ok, ShouldBeTrue) So(len(l), ShouldEqual, 6) So(l[0], ShouldEqual, "first") So(l[1], ShouldEqual, "second") So(l[2], ShouldEqual, "one") So(l[3], ShouldEqual, "third") So(l[4], ShouldEqual, "fourth") So(l[5], ShouldEqual, "one") }) Convey("can grab multiple values", func() { r, err := op.Run(ev, []*Expr{ ref("key.subkey.value"), ref("key.subkey.other"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) v, ok := r.Value.([]interface{}) So(ok, ShouldBeTrue) So(len(v), ShouldEqual, 2) So(v[0], ShouldEqual, "found it") So(v[1], ShouldEqual, "value 2") }) Convey("flattens constituent arrays", func() { r, err := op.Run(ev, []*Expr{ ref("key.list2"), ref("key.list1"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) v, ok := r.Value.([]interface{}) So(ok, ShouldBeTrue) So(len(v), ShouldEqual, 4) So(v[0], ShouldEqual, "third") So(v[1], ShouldEqual, "fourth") So(v[2], ShouldEqual, "first") So(v[3], ShouldEqual, "second") }) Convey("throws errors for missing arguments", func() { _, err := op.Run(ev, []*Expr{}) So(err, ShouldNotBeNil) }) Convey("throws errors for dangling references", func() { _, err := op.Run(ev, []*Expr{ ref("key.that.does.not.exist"), }) So(err, ShouldNotBeNil) }) }) Convey("Environment Variable Resolution (via grab)", t, func() { op := GrabOperator{} ev := &Evaluator{} os.Setenv("GRAB_ONE", "one") os.Setenv("GRAB_TWO", "two") os.Setenv("GRAB_NOT", "") Convey("can grab a single environment value", func() { r, err := op.Run(ev, []*Expr{ env("GRAB_ONE"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "one") }) Convey("tries alternates until it finds a set environment variable", func() { r, err := op.Run(ev, []*Expr{ or(env("GRAB_THREE"), or(env("GRAB_TWO"), env("GRAB_ONE"))), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "two") }) Convey("throws errors for unset environment variables", func() { _, err := op.Run(ev, []*Expr{ env("GRAB_NOT"), }) So(err, ShouldNotBeNil) }) }) Convey("Concat Operator", t, func() { op := ConcatOperator{} ev := &Evaluator{ Tree: YAML( `key: subkey: value: found it other: value 2 list1: - first - second list2: - third - fourth douglas: adams: 42 math: PI: 3.14159 `), } Convey("can concat a single value", func() { r, err := op.Run(ev, []*Expr{ ref("key.subkey.value"), ref("key.list1.0"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "found itfirst") }) Convey("can concat a literal values", func() { r, err := op.Run(ev, []*Expr{ str("a literal "), str("value"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "a literal value") }) Convey("can concat multiple values", func() { r, err := op.Run(ev, []*Expr{ str("I "), ref("key.subkey.value"), str("!"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "I found it!") }) Convey("can concat integer literals", func() { r, err := op.Run(ev, []*Expr{ str("the answer = "), ref("douglas.adams"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "the answer = 42") }) Convey("can concat float literals", func() { r, err := op.Run(ev, []*Expr{ ref("math.PI"), str(" is PI"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "3.14159 is PI") }) Convey("throws errors for missing arguments", func() { _, err := op.Run(ev, []*Expr{}) So(err, ShouldNotBeNil) _, err = op.Run(ev, []*Expr{str("one")}) So(err, ShouldNotBeNil) }) Convey("throws errors for dangling references", func() { _, err := op.Run(ev, []*Expr{ ref("key.that.does.not.exist"), str("string"), }) So(err, ShouldNotBeNil) }) }) Convey("static_ips Operator", t, func() { op := StaticIPOperator{} Reset(func() { UsedIPs = map[string]string{} }) Convey("can resolve valid networks inside of job contexts", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: - static: [ 10.0.0.5 - 10.0.0.10 ] jobs: - name: job1 instances: 3 networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)}) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) v, ok := r.Value.([]interface{}) So(ok, ShouldBeTrue) So(len(v), ShouldEqual, 3) So(v[0], ShouldEqual, "10.0.0.5") So(v[1], ShouldEqual, "10.0.0.6") So(v[2], ShouldEqual, "10.0.0.7") }) Convey("works with new style bosh manifests", func() { ev := &Evaluator{ Here: cursor("instance_groups.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-net subnets: - static: [ 10.0.0.5 - 10.0.0.10 ] instance_groups: - name: job1 instances: 2 networks: - name: test-net static_ips: <------------- HERE ------------ `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)}) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) v, ok := r.Value.([]interface{}) So(ok, ShouldBeTrue) So(len(v), ShouldEqual, 2) So(v[0], ShouldEqual, "10.0.0.5") So(v[1], ShouldEqual, "10.0.0.6") }) Convey("works with multiple subnets", func() { ev := &Evaluator{ Here: cursor("instance_groups.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-net subnets: - static: [ 10.0.0.2 - 10.0.0.3 ] - static: [ 10.0.1.5 - 10.0.1.10 ] instance_groups: - name: job1 instances: 4 networks: - name: test-net static_ips: <------------- HERE ------------ `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2), num(3)}) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) v, ok := r.Value.([]interface{}) So(ok, ShouldBeTrue) So(len(v), ShouldEqual, 4) So(v[0], ShouldEqual, "10.0.0.2") So(v[1], ShouldEqual, "10.0.0.3") So(v[2], ShouldEqual, "10.0.1.5") So(v[3], ShouldEqual, "10.0.1.6") }) Convey("works with multiple subnets with an availability zone", func() { ev := &Evaluator{ Here: cursor("instance_groups.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-net subnets: - static: [ 10.0.0.2 - 10.0.0.3 ] az: z2 - static: [ 10.0.1.5 - 10.0.1.10 ] az: z1 instance_groups: - name: job1 instances: 4 networks: - name: test-net static_ips: <------------- HERE ------------ `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2), num(3)}) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) v, ok := r.Value.([]interface{}) So(ok, ShouldBeTrue) So(len(v), ShouldEqual, 4) So(v[0], ShouldEqual, "10.0.0.2") So(v[1], ShouldEqual, "10.0.0.3") So(v[2], ShouldEqual, "10.0.1.5") So(v[3], ShouldEqual, "10.0.1.6") }) Convey("works with instance_group availability zones", func() { ev := &Evaluator{ Here: cursor("instance_groups.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-net subnets: - static: [ 10.0.0.2 - 10.0.0.3 ] az: z1 - static: [ 10.0.1.5 - 10.0.1.10 ] az: z2 instance_groups: - name: job1 instances: 3 azs: [z2] networks: - name: test-net static_ips: <------------- HERE ------------ `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)}) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) v, ok := r.Value.([]interface{}) So(ok, ShouldBeTrue) So(len(v), ShouldEqual, 3) So(v[0], ShouldEqual, "10.0.1.5") So(v[1], ShouldEqual, "10.0.1.6") So(v[2], ShouldEqual, "10.0.1.7") }) Convey("works with directly specified availability zones", func() { ev := &Evaluator{ Here: cursor("instance_groups.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-net subnets: - static: [ 10.0.0.2 - 10.0.0.4 ] az: z1 - static: [ 10.0.2.6 - 10.0.2.10 ] az: z2 instance_groups: - name: job1 instances: 6 azs: [z1,z2] networks: - name: test-net static_ips: <------------- HERE ------------ `), } r, err := op.Run(ev, []*Expr{ str("z2:1"), num(0), str("z1:2"), str("z2:2"), num(1), str("z2:4"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) v, ok := r.Value.([]interface{}) So(ok, ShouldBeTrue) So(len(v), ShouldEqual, 6) So(v[0], ShouldEqual, "10.0.2.7") So(v[1], ShouldEqual, "10.0.0.2") So(v[2], ShouldEqual, "10.0.0.4") So(v[3], ShouldEqual, "10.0.2.8") So(v[4], ShouldEqual, "10.0.0.3") So(v[5], ShouldEqual, "10.0.2.10") }) Convey("throws an error if an unknown availability zone is used in operator", func() { ev := &Evaluator{ Here: cursor("instance_groups.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-net subnets: - static: [ 10.0.0.2 - 10.0.0.4 ] az: z1 - static: [ 10.0.2.6 - 10.0.2.10 ] az: z2 instance_groups: - name: job1 instances: 2 azs: [z1,z2] networks: - name: test-net static_ips: <------------- HERE ------------ `), } r, err := op.Run(ev, []*Expr{ str("z2:0"), str("z3:1"), }) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if offset for an availability zone is out of bounds", func() { ev := &Evaluator{ Here: cursor("instance_groups.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-net subnets: - static: [ 10.0.0.1 - 10.0.0.5 ] az: z1 - static: [ 10.0.2.1 - 10.0.2.5 ] az: z2 instance_groups: - name: job1 instances: 2 azs: [z1,z2] networks: - name: test-net static_ips: <------------- HERE ------------ `), } r, err := op.Run(ev, []*Expr{ str("z1:4"), str("z1:5"), }) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if an instance_group availability zone is not found in subnets", func() { ev := &Evaluator{ Here: cursor("instance_groups.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-net subnets: - static: [ 10.0.0.2 - 10.0.0.4 ] az: z1 - static: [ 10.0.2.6 - 10.0.2.10 ] az: z2 instance_groups: - name: job1 instances: 2 azs: [z1,z2,z3] networks: - name: test-net static_ips: <------------- HERE ------------ `), } r, err := op.Run(ev, []*Expr{ str("z1:0"), str("z2:1"), }) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("can resolve valid large networks inside of job contexts", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: - static: [ 10.0.0.0 - 10.1.0.1 ] jobs: - name: job1 instances: 7 networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []*Expr{ num(0), num(255), // 2^8 - 1 num(256), // 2^8 num(257), // 2^8 + 1 num(65535), // 2^16 - 1 num(65536), // 2^16 num(65537), // 2^16 + 1 // 1st octet rollover testing disabled due to improve speed. // but verified working on 11/30/2015 - gfranks // num(16777215), // 2^24 - 1 // num(16777216), // 2^24 // num(16777217), // 2^24 + 1 }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) v, ok := r.Value.([]interface{}) So(ok, ShouldBeTrue) So(len(v), ShouldEqual, 7) So(v[0], ShouldEqual, "10.0.0.0") So(v[1], ShouldEqual, "10.0.0.255") So(v[2], ShouldEqual, "10.0.1.0") // 3rd octet rollover So(v[3], ShouldEqual, "10.0.1.1") So(v[4], ShouldEqual, "10.0.255.255") So(v[5], ShouldEqual, "10.1.0.0") // 2nd octet rollover So(v[6], ShouldEqual, "10.1.0.1") // 1st octet rollover testing disabled due to improve speed. // but verified working on 11/30/2015 - gfranks // So(v[7], ShouldEqual, "10.255.255.255") // So(v[8], ShouldEqual, "11.0.0.0") // 1st octet rollover // So(v[9], ShouldEqual, "11.0.0.1") }) Convey("throws an error if no job name is specified", func() { ev := &Evaluator{ Here: cursor("jobs.0.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: - static: [ 10.0.0.5 - 10.0.0.10 ] jobs: - instances: 3 networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)}) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if no job instances specified", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: - static: [ 10.0.0.5 - 10.0.0.10 ] jobs: - name: job1 networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)}) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if job instances is not a number", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: - static: [ 10.0.0.5 - 10.0.0.10 ] jobs: - name: job1 instances: PI networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)}) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if job has no network name", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: - static: [ 10.0.0.5 - 10.0.0.10 ] jobs: - name: job1 instances: 3 networks: - static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)}) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if network has no subnets key", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network jobs: - name: job1 instances: 3 networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)}) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if network has no subnets", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: [] jobs: - name: job1 instances: 3 networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)}) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if network has no static ranges", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: - {} jobs: - name: job1 instances: 3 networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)}) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if network has malformed static range array(s)", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: - static: [ 10.0.0.1, 10.0.0.254 ] jobs: - name: job1 instances: 3 networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)}) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if network static range has malformed IP addresses", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: - static: 10.0.0.0.0.0.0.1 - geoff jobs: - name: job1 instances: 3 networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)}) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if the static address pool is too small", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: - static: 172.16.31.10 - 172.16.31.11 jobs: - name: job1 instances: 3 networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)}) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if the address pool ends before it starts", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: - static: [ 10.8.0.1 - 10.0.0.255 ] jobs: - name: job1 instances: 3 networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)}) So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, "ends before it starts") So(r, ShouldBeNil) }) }) Convey("inject Operator", t, func() { op := InjectOperator{} ev := &Evaluator{ Tree: YAML( `key: subkey: value: found it other: value 2 subkey2: value: overridden third: trois list1: - first - second list2: - third - fourth `), } Convey("can inject a single sub-map", func() { r, err := op.Run(ev, []*Expr{ ref("key.subkey"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Inject) v, ok := r.Value.(map[interface{}]interface{}) So(ok, ShouldBeTrue) So(v["value"], ShouldEqual, "found it") So(v["other"], ShouldEqual, "value 2") }) Convey("can inject multiple sub-maps", func() { r, err := op.Run(ev, []*Expr{ ref("key.subkey"), ref("key.subkey2"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Inject) v, ok := r.Value.(map[interface{}]interface{}) So(ok, ShouldBeTrue) So(len(v), ShouldEqual, 3) So(v["value"], ShouldEqual, "overridden") So(v["other"], ShouldEqual, "value 2") So(v["third"], ShouldEqual, "trois") }) Convey("handles non-existent references", func() { _, err := op.Run(ev, []*Expr{ ref("key.subkey"), ref("key.subkey2"), ref("key.subkey2.ENOENT"), }) So(err, ShouldNotBeNil) }) Convey("throws an error when trying to inject a scalar", func() { _, err := op.Run(ev, []*Expr{ ref("key.subkey.value"), }) So(err, ShouldNotBeNil) }) Convey("throws an error when trying to inject a list", func() { _, err := op.Run(ev, []*Expr{ ref("key.list1"), }) So(err, ShouldNotBeNil) }) }) Convey("param Operator", t, func() { op := ParamOperator{} ev := &Evaluator{} Convey("always causes an error", func() { r, err := op.Run(ev, []*Expr{ str("this is the error"), }) So(err, ShouldNotBeNil) So(err.Error(), ShouldEqual, "this is the error") So(r, ShouldBeNil) }) }) Convey("Join Operator", t, func() { op := JoinOperator{} ev := &Evaluator{ Tree: YAML( `--- meta: authorities: - password.write - clients.write - clients.read - scim.write - scim.read - uaa.admin - clients.secret secondlist: - admin.write - admin.read emptylist: [] anotherkey: - entry1 - somekey: value - entry2 somestanza: foo: bar wom: bat `), } Convey("can join a simple list", func() { r, err := op.Run(ev, []*Expr{ str(","), ref("meta.authorities"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "password.write,clients.write,clients.read,scim.write,scim.read,uaa.admin,clients.secret") }) Convey("can join multiple lists", func() { r, err := op.Run(ev, []*Expr{ str(","), ref("meta.authorities"), ref("meta.secondlist"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "password.write,clients.write,clients.read,scim.write,scim.read,uaa.admin,clients.secret,admin.write,admin.read") }) Convey("can join an empty list", func() { r, err := op.Run(ev, []*Expr{ str(","), ref("meta.emptylist"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "") }) Convey("can join string literals", func() { r, err := op.Run(ev, []*Expr{ str(","), str("password.write"), str("clients.write"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "password.write,clients.write") }) Convey("can join integer literals", func() { r, err := op.Run(ev, []*Expr{ str(":"), num(4), num(8), num(15), num(16), num(23), num(42), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "4:8:15:16:23:42") }) Convey("can join referenced string entry", func() { r, err := op.Run(ev, []*Expr{ str(","), ref("meta.somestanza.foo"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "bar") }) Convey("can join referenced string entries", func() { r, err := op.Run(ev, []*Expr{ str(","), ref("meta.somestanza.foo"), ref("meta.somestanza.wom"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "bar,bat") }) Convey("can join multiple referenced entries", func() { r, err := op.Run(ev, []*Expr{ str(","), ref("meta.authorities"), ref("meta.somestanza.foo"), ref("meta.somestanza.wom"), str("ending"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "password.write,clients.write,clients.read,scim.write,scim.read,uaa.admin,clients.secret,bar,bat,ending") }) Convey("throws an error when there are no arguments", func() { r, err := op.Run(ev, []*Expr{}) So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, "no arguments specified") So(r, ShouldBeNil) }) Convey("throws an error when there are too few arguments", func() { r, err := op.Run(ev, []*Expr{ str(","), }) So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, "too few arguments supplied") So(r, ShouldBeNil) }) Convey("throws an error when seperator argument is not a literal", func() { r, err := op.Run(ev, []*Expr{ ref("meta.emptylist"), ref("meta.authorities"), }) So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, "join operator only accepts literal argument for the seperator") So(r, ShouldBeNil) }) Convey("throws an error when referenced entry is not a list or literal", func() { r, err := op.Run(ev, []*Expr{ str(","), ref("meta.somestanza"), }) So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, "referenced entry is not a list or string") So(r, ShouldBeNil) }) Convey("throws an error when referenced list contains non-string entries", func() { r, err := op.Run(ev, []*Expr{ str(","), ref("meta.anotherkey"), }) So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, "is not compatible for") So(r, ShouldBeNil) }) Convey("throws an error when there are unresolvable references", func() { r, err := op.Run(ev, []*Expr{ str(","), ref("meta.non-existent"), }) So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, "Unable to resolve") So(r, ShouldBeNil) }) Convey("calculates dependencies correctly", func() { //TODO: Move this to a higher scope when more dependencies tests are added shouldHaveDeps := func(actual interface{}, expected ...interface{}) string { deps := actual.([]*tree.Cursor) paths := []string{} for _, path := range expected { normalizedPath, err := tree.ParseCursor(path.(string)) if err != nil { panic(fmt.Sprintf("improper path %s passed to test", path.(string))) } paths = append(paths, normalizedPath.String()) } actualPaths := []string{} //make an array so we can give some coherent output on error for _, dep := range deps { //Pass through tree so that tests can tolerate changes to the cursor lib actualPaths = append(actualPaths, dep.String()) } //sort and compare sort.Strings(actualPaths) sort.Strings(paths) match := reflect.DeepEqual(actualPaths, paths) //give result if !match { return fmt.Sprintf("actual: %+v\n expected: %+v", actualPaths, paths) } return "" } Convey("with a single list", func() { deps := op.Dependencies(ev, []*Expr{ str(" "), ref("meta.secondlist"), }, nil) So(deps, shouldHaveDeps, "meta.secondlist.[0]", "meta.secondlist.[1]") }) Convey("with multiple lists", func() { deps := op.Dependencies(ev, []*Expr{ str(" "), ref("meta.authorities"), ref("meta.secondlist"), }, nil) So(deps, shouldHaveDeps, "meta.authorities.[0]", "meta.authorities.[1]", "meta.authorities.[2]", "meta.authorities.[3]", "meta.authorities.[4]", "meta.authorities.[5]", "meta.authorities.[6]", "meta.secondlist.[0]", "meta.secondlist.[1]") }) Convey("with a reference string", func() { deps := op.Dependencies(ev, []*Expr{ str(" "), ref("meta.somestanza.foo"), }, nil) So(deps, shouldHaveDeps, "meta.somestanza.foo") }) Convey("with multiple reference strings", func() { deps := op.Dependencies(ev, []*Expr{ str(" "), ref("meta.somestanza.foo"), ref("meta.somestanza.wom"), }, nil) So(deps, shouldHaveDeps, "meta.somestanza.foo", "meta.somestanza.wom") }) Convey("with a reference string and a list", func() { deps := op.Dependencies(ev, []*Expr{ str(" "), ref("meta.somestanza.foo"), ref("meta.secondlist"), }, nil) So(deps, shouldHaveDeps, "meta.somestanza.foo", "meta.secondlist.[0]", "meta.secondlist.[1]") }) Convey("with a literal string", func() { deps := op.Dependencies(ev, []*Expr{ str(" "), str("literally literal"), }, nil) So(deps, shouldHaveDeps) }) Convey("with a literal string and a reference string", func() { deps := op.Dependencies(ev, []*Expr{ str(" "), str("beep"), ref("meta.somestanza.foo"), }, nil) So(deps, shouldHaveDeps, "meta.somestanza.foo") }) }) }) Convey("empty operator", t, func() { op := EmptyOperator{} ev := &Evaluator{ Tree: YAML( `--- meta: authorities: meep `), } //These three are with unquoted arguments (references) Convey("can replace with a hash", func() { r, err := op.Run(ev, []*Expr{ ref("hash"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) val, isHash := r.Value.(map[string]interface{}) So(isHash, ShouldBeTrue) So(val, ShouldResemble, map[string]interface{}{}) }) Convey("can replace with an array", func() { r, err := op.Run(ev, []*Expr{ ref("array"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) val, isArray := r.Value.([]interface{}) So(isArray, ShouldBeTrue) So(val, ShouldResemble, []interface{}{}) }) Convey("can replace with an empty string", func() { r, err := op.Run(ev, []*Expr{ ref("string"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) val, isString := r.Value.(string) So(isString, ShouldBeTrue) So(val, ShouldEqual, "") }) Convey("throws an error for unrecognized types", func() { r, err := op.Run(ev, []*Expr{ ref("void"), }) So(r, ShouldBeNil) So(err, ShouldNotBeNil) }) Convey("works with string literals", func() { r, err := op.Run(ev, []*Expr{ str("hash"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) val, isHash := r.Value.(map[string]interface{}) So(isHash, ShouldBeTrue) So(val, ShouldResemble, map[string]interface{}{}) }) Convey("throws an error with no args", func() { r, err := op.Run(ev, []*Expr{}) So(r, ShouldBeNil) So(err, ShouldNotBeNil) }) Convey("throws an error with too many args", func() { r, err := op.Run(ev, []*Expr{ ref("hash"), ref("array"), }) So(r, ShouldBeNil) So(err, ShouldNotBeNil) }) }) }
func TestOperators(t *testing.T) { cursor := func(s string) *Cursor { c, err := ParseCursor(s) So(err, ShouldBeNil) return c } YAML := func(s string) map[interface{}]interface{} { y, err := simpleyaml.NewYaml([]byte(s)) So(err, ShouldBeNil) data, err := y.Map() So(err, ShouldBeNil) return data } Convey("Parser", t, func() { Convey("parses op calls in their entirety", func() { opOk := func(code string, name string, args ...interface{}) { op, err := ParseOpcall(code) So(err, ShouldBeNil) So(op, ShouldNotBeNil) _, ok := op.op.(NullOperator) So(ok, ShouldBeTrue) So(op.op.(NullOperator).Missing, ShouldEqual, name) So(len(op.args), ShouldEqual, len(args)) for i, expect := range args { switch expect.(type) { case string: So(op.args[i], ShouldEqual, expect.(string)) case *Cursor: _, ok := op.args[i].(*Cursor) So(ok, ShouldBeTrue) So(op.args[i].(*Cursor).String(), ShouldEqual, expect.(*Cursor).String()) } } } cursor := func(s string) *Cursor { c, err := ParseCursor(s) So(err, ShouldBeNil) return c } Convey("handles opcodes with and without arguments", func() { opOk(`(( null ))`, "null") opOk(`(( null 42 ))`, "null", "42") opOk(`(( null 1 2 3 4 ))`, "null", "1", "2", "3", "4") }) Convey("ignores optional whitespace", func() { opOk(`((null))`, "null") opOk(`(( null ))`, "null") opOk(`(( null ))`, "null") args := []interface{}{"1", "2", "3"} opOk(`((null 1 2 3))`, "null", args...) opOk(`((null 1 2 3))`, "null", args...) opOk(`((null 1 2 3 ))`, "null", args...) opOk(`((null 1 2 3 ))`, "null", args...) }) Convey("allows use of commas to separate arguments", func() { args := []interface{}{"1", "2", "3"} opOk(`((null 1, 2, 3))`, "null", args...) opOk(`((null 1, 2, 3))`, "null", args...) opOk(`((null 1, 2, 3, ))`, "null", args...) opOk(`((null 1 , 2 , 3 , ))`, "null", args...) }) Convey("allows use of parentheses around arguments", func() { args := []interface{}{"1", "2", "3"} opOk(`((null(1,2,3)))`, "null", args...) opOk(`((null(1, 2, 3) ))`, "null", args...) opOk(`((null( 1, 2, 3)))`, "null", args...) opOk(`((null (1, 2, 3) ))`, "null", args...) opOk(`((null (1 , 2 , 3) ))`, "null", args...) }) Convey("handles string literal arguments", func() { opOk(`(( null "string" ))`, "null", "string") opOk(`(( null "string with whitespace" ))`, "null", "string with whitespace") opOk(`(( null "a \"quoted\" string" ))`, "null", `a "quoted" string`) opOk(`(( null "\\escaped" ))`, "null", `\escaped`) }) Convey("handles reference (cursor) arguments", func() { opOk(`(( null x.y.z ))`, "null", cursor("x.y.z")) opOk(`(( null x.[0].z ))`, "null", cursor("x.0.z")) opOk(`(( null x[0].z ))`, "null", cursor("x.0.z")) opOk(`(( null x[0]z ))`, "null", cursor("x.0.z")) }) Convey("handles mixed collections of argument types", func() { opOk(`(( xyzzy "string" x.y.z 42 ))`, "xyzzy", "string", cursor("x.y.z"), "42") opOk(`(( xyzzy("string" x.y.z 42) ))`, "xyzzy", "string", cursor("x.y.z"), "42") }) }) }) Convey("Grab Operator", t, func() { op := GrabOperator{} ev := &Evaluator{ Tree: YAML( `key: subkey: value: found it other: value 2 list1: - first - second list2: - third - fourth lonely: - one `), } Convey("can grab a single value", func() { r, err := op.Run(ev, []interface{}{ cursor("key.subkey.value"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "found it") }) Convey("can grab a single list value", func() { r, err := op.Run(ev, []interface{}{ cursor("key.lonely"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) l, ok := r.Value.([]interface{}) So(ok, ShouldBeTrue) So(len(l), ShouldEqual, 1) So(l[0], ShouldEqual, "one") }) Convey("can grab a multiple lists and flatten them", func() { r, err := op.Run(ev, []interface{}{ cursor("key.list1"), cursor("key.lonely"), cursor("key.list2"), cursor("key.lonely.0"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) l, ok := r.Value.([]interface{}) So(ok, ShouldBeTrue) So(len(l), ShouldEqual, 6) So(l[0], ShouldEqual, "first") So(l[1], ShouldEqual, "second") So(l[2], ShouldEqual, "one") So(l[3], ShouldEqual, "third") So(l[4], ShouldEqual, "fourth") So(l[5], ShouldEqual, "one") }) Convey("can grab multiple values", func() { r, err := op.Run(ev, []interface{}{ cursor("key.subkey.value"), cursor("key.subkey.other"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) v, ok := r.Value.([]interface{}) So(ok, ShouldBeTrue) So(len(v), ShouldEqual, 2) So(v[0], ShouldEqual, "found it") So(v[1], ShouldEqual, "value 2") }) Convey("flattens constituent arrays", func() { r, err := op.Run(ev, []interface{}{ cursor("key.list2"), cursor("key.list1"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) v, ok := r.Value.([]interface{}) So(ok, ShouldBeTrue) So(len(v), ShouldEqual, 4) So(v[0], ShouldEqual, "third") So(v[1], ShouldEqual, "fourth") So(v[2], ShouldEqual, "first") So(v[3], ShouldEqual, "second") }) Convey("throws errors for missing arguments", func() { _, err := op.Run(ev, []interface{}{}) So(err, ShouldNotBeNil) }) Convey("throws errors for dangling references", func() { _, err := op.Run(ev, []interface{}{ cursor("key.that.does.not.exist"), }) So(err, ShouldNotBeNil) }) }) Convey("Concat Operator", t, func() { op := ConcatOperator{} ev := &Evaluator{ Tree: YAML( `key: subkey: value: found it other: value 2 list1: - first - second list2: - third - fourth douglas: adams: 42 math: PI: 3.14159 `), } Convey("can concat a single value", func() { r, err := op.Run(ev, []interface{}{ cursor("key.subkey.value"), cursor("key.list1.0"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "found itfirst") }) Convey("can concat a literal values", func() { r, err := op.Run(ev, []interface{}{ "a literal ", "value", }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "a literal value") }) Convey("can concat multiple values", func() { r, err := op.Run(ev, []interface{}{ "I ", cursor("key.subkey.value"), "!", }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "I found it!") }) Convey("can concat integer literals", func() { r, err := op.Run(ev, []interface{}{ "the answer = ", cursor("douglas.adams"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "the answer = 42") }) Convey("can concat float literals", func() { r, err := op.Run(ev, []interface{}{ cursor("math.PI"), " is PI", }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "3.14159 is PI") }) Convey("throws errors for missing arguments", func() { _, err := op.Run(ev, []interface{}{}) So(err, ShouldNotBeNil) _, err = op.Run(ev, []interface{}{"one"}) So(err, ShouldNotBeNil) }) Convey("throws errors for dangling references", func() { _, err := op.Run(ev, []interface{}{ cursor("key.that.does.not.exist"), "string", }) So(err, ShouldNotBeNil) }) }) Convey("static_ips Operator", t, func() { op := StaticIPOperator{} Convey("can resolve valid networks inside of job contexts", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: - static: [ 10.0.0.5 - 10.0.0.10 ] jobs: - name: job1 instances: 3 networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []interface{}{"0", "1", "2"}) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) v, ok := r.Value.([]interface{}) So(ok, ShouldBeTrue) So(len(v), ShouldEqual, 3) So(v[0], ShouldEqual, "10.0.0.5") So(v[1], ShouldEqual, "10.0.0.6") So(v[2], ShouldEqual, "10.0.0.7") }) Convey("throws an error if no job name is specified", func() { ev := &Evaluator{ Here: cursor("jobs.0.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: - static: [ 10.0.0.5 - 10.0.0.10 ] jobs: - instances: 3 networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []interface{}{"0", "1", "2"}) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if no job instances specified", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: - static: [ 10.0.0.5 - 10.0.0.10 ] jobs: - name: job1 networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []interface{}{"0", "1", "2"}) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if job instances is not a number", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: - static: [ 10.0.0.5 - 10.0.0.10 ] jobs: - name: job1 instances: PI networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []interface{}{"0", "1", "2"}) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if job has no network name", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: - static: [ 10.0.0.5 - 10.0.0.10 ] jobs: - name: job1 instances: 3 networks: - static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []interface{}{"0", "1", "2"}) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if network has no subnets key", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network jobs: - name: job1 instances: 3 networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []interface{}{"0", "1", "2"}) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if network has no subnets", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: [] jobs: - name: job1 instances: 3 networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []interface{}{"0", "1", "2"}) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if network has no static ranges", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: - {} jobs: - name: job1 instances: 3 networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []interface{}{"0", "1", "2"}) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if network has malformed static range array(s)", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: - static: [ 10.0.0.1, 10.0.0.254 ] jobs: - name: job1 instances: 3 networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []interface{}{"0", "1", "2"}) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if network static range has malformed IP addresses", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: - static: 10.0.0.0.0.0.0.1 - geoff jobs: - name: job1 instances: 3 networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []interface{}{"0", "1", "2"}) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if the static address pool is too small", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: - static: 172.16.31.10 - 172.16.31.11 jobs: - name: job1 instances: 3 networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []interface{}{"0", "1", "2"}) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) }) Convey("inject Operator", t, func() { op := InjectOperator{} ev := &Evaluator{ Tree: YAML( `key: subkey: value: found it other: value 2 subkey2: value: overridden third: trois list1: - first - second list2: - third - fourth `), } Convey("can inject a single sub-map", func() { r, err := op.Run(ev, []interface{}{ cursor("key.subkey"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Inject) v, ok := r.Value.(map[interface{}]interface{}) So(ok, ShouldBeTrue) So(v["value"], ShouldEqual, "found it") So(v["other"], ShouldEqual, "value 2") }) Convey("can inject multiple sub-maps", func() { r, err := op.Run(ev, []interface{}{ cursor("key.subkey"), cursor("key.subkey2"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Inject) v, ok := r.Value.(map[interface{}]interface{}) So(ok, ShouldBeTrue) So(len(v), ShouldEqual, 3) So(v["value"], ShouldEqual, "overridden") So(v["other"], ShouldEqual, "value 2") So(v["third"], ShouldEqual, "trois") }) Convey("handles non-existent references", func() { _, err := op.Run(ev, []interface{}{ cursor("key.subkey"), cursor("key.subkey2"), cursor("key.subkey2.ENOENT"), }) So(err, ShouldNotBeNil) }) Convey("throws an error when trying to inject a scalar", func() { _, err := op.Run(ev, []interface{}{ cursor("key.subkey.value"), }) So(err, ShouldNotBeNil) }) Convey("throws an error when trying to inject a list", func() { _, err := op.Run(ev, []interface{}{ cursor("key.list1"), }) So(err, ShouldNotBeNil) }) }) Convey("param Operator", t, func() { op := ParamOperator{} ev := &Evaluator{} Convey("always causes an error", func() { r, err := op.Run(ev, []interface{}{ "this is the error", }) So(err, ShouldNotBeNil) So(err.Error(), ShouldEqual, "this is the error") So(r, ShouldBeNil) }) }) }
func TestVault(t *testing.T) { YAML := func(s string) map[interface{}]interface{} { y, err := simpleyaml.NewYaml([]byte(s)) So(err, ShouldBeNil) data, err := y.Map() So(err, ShouldBeNil) return data } ToYAML := func(tree map[interface{}]interface{}) string { y, err := yaml.Marshal(tree) So(err, ShouldBeNil) return string(y) } ReYAML := func(s string) string { return ToYAML(YAML(s)) } RunTests := func(src string) { var test, input, output string var current *string testPat := regexp.MustCompile(`^##+\s+(.*)\s*$`) convey := func() { if test != "" { Convey(test, func() { ev := &Evaluator{Tree: YAML(input)} err := ev.RunPhase(EvalPhase) So(err, ShouldBeNil) So(ToYAML(ev.Tree), ShouldEqual, ReYAML(output)) }) } } s := bufio.NewScanner(strings.NewReader(src)) for s.Scan() { if testPat.MatchString(s.Text()) { m := testPat.FindStringSubmatch(s.Text()) convey() test, input, output = m[1], "", "" continue } if s.Text() == "---" { if input == "" { current = &input } else { current = &output } continue } if current != nil { *current = *current + s.Text() + "\n" } } convey() } RunErrorTests := func(src string) { var test, input, errors string var current *string testPat := regexp.MustCompile(`^##+\s+(.*)\s*$`) convey := func() { if test != "" { Convey(test, func() { ev := &Evaluator{Tree: YAML(input)} err := ev.RunPhase(EvalPhase) So(err, ShouldNotBeNil) So(strings.Trim(err.Error(), " \t"), ShouldEqual, errors) }) } } s := bufio.NewScanner(strings.NewReader(src)) for s.Scan() { if testPat.MatchString(s.Text()) { m := testPat.FindStringSubmatch(s.Text()) convey() test, input, errors = m[1], "", "" continue } if s.Text() == "---" { if input == "" { current = &input } else { current = &errors } continue } if current != nil { *current = *current + s.Text() + "\n" } } convey() } Convey("Disconnected Vault", t, func() { os.Setenv("REDACT", "yes") RunTests(` ################################################## emits REDACTED when asked to --- secret: (( vault "secret/hand:shake" )) --- secret: REDACTED `) }) Convey("Connected Vault", t, func() { mock := httptest.NewServer( http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("X-Vault-Token") != "sekrit-toekin" { w.WriteHeader(403) fmt.Fprintf(w, `{"errors":["missing client token"]}`) return } switch r.URL.Path { case "/v1/secret/hand": w.WriteHeader(200) fmt.Fprintf(w, `{"data":{"shake":"knock, knock"}}`) case "/v1/secret/admin": w.WriteHeader(200) fmt.Fprintf(w, `{"data":{"username":"******","password":"******"}}`) case "/v1/secret/key": w.WriteHeader(200) fmt.Fprintf(w, `{"data":{"test":"testing"}}`) case "/v1/secret/malformed": w.WriteHeader(200) fmt.Fprintf(w, `wait, this isn't JSON`) case "/v1/secret/structure": w.WriteHeader(200) fmt.Fprintf(w, `{"data":{"data":[1,2,3]}}`) default: w.WriteHeader(404) fmt.Fprintf(w, `{"errors":[]}`) } }, ), ) defer mock.Close() os.Setenv("REDACT", "") os.Setenv("VAULT_ADDR", mock.URL) os.Setenv("VAULT_TOKEN", "sekrit-toekin") RunTests(` ################################################ emits sensitive credentials --- meta: prefix: secret key: secret/key:test secret: (( vault "secret/hand:shake" )) username: (( vault "secret/admin:username" )) password: (( vault "secret/admin:password" )) prefixed: (( vault meta.prefix "/admin:password" )) key: (( vault $.meta.key )) --- meta: key: secret/key:test prefix: secret secret: knock, knock username: admin password: x12345 prefixed: x12345 key: testing `) os.Setenv("VAULT_ADDR", mock.URL) oldhome := os.Getenv("HOME") os.Setenv("HOME", "assets/home/auth") os.Setenv("VAULT_TOKEN", "") RunTests(` ########################## retrieves token transparently from ~/.vault-token --- secret: (( vault "secret/hand:shake" )) --- secret: knock, knock `) os.Setenv("VAULT_ADDR", "garbage") os.Setenv("VAULT_TOKEN", "") os.Setenv("HOME", "assets/home/svtoken") ioutil.WriteFile("assets/home/svtoken/.svtoken", []byte("vault: "+mock.URL+"\n"+ "token: sekrit-toekin\n"), 0644) RunTests(` ############################## retrieves token transparently from ~/.svtoken --- secret: (( vault "secret/hand:shake" )) --- secret: knock, knock `) /* RESET TO A VALID, AUTHENTICATED STATE */ os.Setenv("VAULT_ADDR", mock.URL) os.Setenv("HOME", "assets/home/auth") RunErrorTests(` ######################################### fails when missing its argument --- secret: (( vault )) --- 1 error(s) detected: - $.secret: vault operator requires at least one argument ######################################### fails on non-existent reference --- meta: {} secret: (( vault $.meta.key )) --- 1 error(s) detected: - $.secret: Unable to resolve ` + "`" + `meta.key` + "`" + `: ` + "`" + `$.meta.key` + "`" + ` could not be found in the datastructure #################################################### fails on map reference --- meta: key: secret/hand:shake secret: (( vault $.meta )) --- 1 error(s) detected: - $.secret: tried to look up $.meta, which is not a string scalar ################################################## fails on list reference --- meta: - first secret: (( vault $.meta )) --- 1 error(s) detected: - $.secret: tried to look up $.meta, which is not a string scalar ######################################### fails on non-existent credentials --- secret: (( vault "secret/e:noent" )) --- 1 error(s) detected: - $.secret: secret secret/e:noent not found ############################################## fails on non-string argument --- secret: (( vault 42 )) --- 1 error(s) detected: - $.secret: invalid argument 42; must be in the form path/to/secret:key ################################################# fails on non-JSON response --- secret: (( vault "secret/malformed:key" )) --- 1 error(s) detected: - $.secret: bad JSON response received from Vault: "wait, this isn't JSON" ################################################# fails on non-string data --- secret: (( vault "secret/structure:data" )) --- 1 error(s) detected: - $.secret: secret secret/structure:data is not a string `) os.Setenv("VAULT_TOKEN", "incorrect") RunErrorTests(` ##################################################### fails on a bad token --- secret: (( vault "secret/hand:shake" )) --- 1 error(s) detected: - $.secret: failed to retrieve secret/hand:shake from Vault (` + os.Getenv("VAULT_ADDR") + `): missing client token `) oldhome = os.Getenv("HOME") os.Setenv("HOME", "assets/home/unauth") os.Setenv("REDACT", "") os.Setenv("VAULT_TOKEN", "") RunErrorTests(` ################################################ fails on a missing token --- secret: (( vault "secret/hand:shake" )) --- 1 error(s) detected: - $.secret: Failed to determine Vault URL / token, and the $REDACT environment variable is not set. `) os.Setenv("HOME", oldhome) }) Convey("It correctly parses path", t, func() { for _, test := range []struct { path string //The full path to run through the parse function expSecret string //What is expected to be left of the colon expKey string //What is expected to be right of the colon }{ //-----TEST CASES GO HERE----- // { "path to parse", "expected secret", "expected key" } {"just/a/secret", "just/a/secret", ""}, {"secret/with/colon:", "secret/with/colon", ""}, {":", "", ""}, {"a:", "a", ""}, {"", "", ""}, {"secret/and:key", "secret/and", "key"}, {":justakey", "", "justakey"}, {"secretwithcolon://127.0.0.1:", "secretwithcolon://127.0.0.1", ""}, {"secretwithcolons://127.0.0.1:8500:", "secretwithcolons://127.0.0.1:8500", ""}, {"secretwithcolons://127.0.0.1:8500:andkey", "secretwithcolons://127.0.0.1:8500", "andkey"}, } { Convey(test.path, func() { secret, key := parsePath(test.path) So(secret, ShouldEqual, test.expSecret) So(key, ShouldEqual, test.expKey) }) } }) }
func TestExamples(t *testing.T) { var stdout string printfStdOut = func(format string, args ...interface{}) { stdout = fmt.Sprintf(format, args...) } var stderr string printfStdErr = func(format string, args ...interface{}) { stderr = fmt.Sprintf(format, args...) } rc := 256 // invalid return code to catch any issues exit = func(code int) { rc = code } YAML := func(path string) string { s, err := ioutil.ReadFile(path) So(err, ShouldBeNil) y, err := simpleyaml.NewYaml([]byte(s)) So(err, ShouldBeNil) data, err := y.Map() So(err, ShouldBeNil) out, err := yaml.Marshal(data) So(err, ShouldBeNil) return string(out) + "\n" } Convey("Examples from README.md", t, func() { example := func(args ...string) { expect := args[len(args)-1] args = args[:len(args)-1] os.Args = []string{"spruce", "merge"} os.Args = append(os.Args, args...) stdout, stderr = "", "" main() So(stderr, ShouldEqual, "") So(stdout, ShouldEqual, YAML(expect)) } Convey("Basic Example", func() { example( "../../examples/basic/main.yml", "../../examples/basic/merge.yml", "../../examples/basic/output.yml", ) }) Convey("Map Replacements", func() { example( "../../examples/map-replacement/original.yml", "../../examples/map-replacement/delete.yml", "../../examples/map-replacement/insert.yml", "../../examples/map-replacement/output.yml", ) }) Convey("Key Removal", func() { example( "--prune", "deleteme", "../../examples/key-removal/original.yml", "../../examples/key-removal/things.yml", "../../examples/key-removal/output.yml", ) example( "../../examples/pruning/base.yml", "../../examples/pruning/jobs.yml", "../../examples/pruning/networks.yml", "../../examples/pruning/output.yml", ) }) Convey("Lists of Maps", func() { example( "../../examples/list-of-maps/original.yml", "../../examples/list-of-maps/new.yml", "../../examples/list-of-maps/output.yml", ) }) Convey("Static IPs", func() { example( "../../examples/static-ips/jobs.yml", "../../examples/static-ips/properties.yml", "../../examples/static-ips/networks.yml", "../../examples/static-ips/output.yml", ) }) Convey("Static IPs with availability zones", func() { example( "../../examples/availability-zones/jobs.yml", "../../examples/availability-zones/properties.yml", "../../examples/availability-zones/networks.yml", "../../examples/availability-zones/output.yml", ) }) Convey("Injecting Subtrees", func() { example( "--prune", "meta", "../../examples/inject/all-in-one.yml", "../../examples/inject/output.yml", ) example( "--prune", "meta", "../../examples/inject/templates.yml", "../../examples/inject/green.yml", "../../examples/inject/output.yml", ) }) Convey("Pruning", func() { example( "../../examples/pruning/base.yml", "../../examples/pruning/jobs.yml", "../../examples/pruning/networks.yml", "../../examples/pruning/output.yml", ) }) Convey("Inserting", func() { example( "../../examples/inserting/main.yml", "../../examples/inserting/addon.yml", "../../examples/inserting/result.yml", ) }) }) }
func TestEvaluator(t *testing.T) { YAML := func(s string) map[interface{}]interface{} { y, err := simpleyaml.NewYaml([]byte(s)) So(err, ShouldBeNil) data, err := y.Map() So(err, ShouldBeNil) return data } Convey("Evaluator", t, func() { Convey("Data Flow", func() { Convey("Generates a sequential list of operator calls, in order", func() { ev := &Evaluator{ Tree: YAML( `meta: foo: FOO bar: (( grab meta.foo )) baz: (( grab meta.bar )) quux: (( grab meta.baz )) boz: (( grab meta.quux )) `), } err := ev.DataFlow() So(err, ShouldBeNil) So(ev.DataOps, ShouldNotBeNil) // expect: meta.bar (( grab meta.foo )) // meta.baz (( grab meta.bar )) // meta.quux (( grab meta.baz )) // meta.boz (( grab meta.quux )) So(len(ev.DataOps), ShouldEqual, 4) So(ev.DataOps[0].where.String(), ShouldEqual, "meta.bar") So(ev.DataOps[0].src, ShouldEqual, "(( grab meta.foo ))") So(ev.DataOps[1].where.String(), ShouldEqual, "meta.baz") So(ev.DataOps[1].src, ShouldEqual, "(( grab meta.bar ))") So(ev.DataOps[2].where.String(), ShouldEqual, "meta.quux") So(ev.DataOps[2].src, ShouldEqual, "(( grab meta.baz ))") So(ev.DataOps[3].where.String(), ShouldEqual, "meta.boz") So(ev.DataOps[3].src, ShouldEqual, "(( grab meta.quux ))") }) Convey("detects direct (a -> b -> a) cycles in data flow graph", func() { ev := &Evaluator{ Tree: YAML( `meta: bar: (( grab meta.foo )) foo: (( grab meta.bar )) `), } err := ev.DataFlow() So(err, ShouldNotBeNil) }) Convey("detects indirect (a -> b -> c -> a) cycles in data flow graph", func() { ev := &Evaluator{ Tree: YAML( `meta: foo: (( grab meta.bar )) bar: (( grab meta.baz )) baz: (( grab meta.foo )) `), } err := ev.DataFlow() So(err, ShouldNotBeNil) }) Convey("handles data flow regardless of operator type", func() { ev := &Evaluator{ Tree: YAML( `meta: foo: FOO bar: (( grab meta.foo )) baz: (( grab meta.bar )) quux: (( concat "literal:" meta.baz )) boz: (( grab meta.quux )) `), } err := ev.DataFlow() So(err, ShouldBeNil) // expect: meta.bar (( grab meta.foo )) // meta.baz (( grab meta.bar )) // meta.quux (( concat "literal:" meta.baz )) // meta.boz (( grab meta.quux )) So(len(ev.DataOps), ShouldEqual, 4) So(ev.DataOps[0].where.String(), ShouldEqual, "meta.bar") So(ev.DataOps[0].src, ShouldEqual, "(( grab meta.foo ))") So(ev.DataOps[1].where.String(), ShouldEqual, "meta.baz") So(ev.DataOps[1].src, ShouldEqual, "(( grab meta.bar ))") So(ev.DataOps[2].where.String(), ShouldEqual, "meta.quux") So(ev.DataOps[2].src, ShouldEqual, `(( concat "literal:" meta.baz ))`) So(ev.DataOps[3].where.String(), ShouldEqual, "meta.boz") So(ev.DataOps[3].src, ShouldEqual, "(( grab meta.quux ))") }) Convey("handles data flow for operators in lists", func() { ev := &Evaluator{ Tree: YAML( `meta: - FOO - (( grab meta.0 )) - (( grab meta.1 )) - (( concat "literal:" meta.2 )) - (( grab meta.3 )) `), } err := ev.DataFlow() So(err, ShouldBeNil) // expect: meta.1 (( grab meta.0 )) // meta.2 (( grab meta.1 )) // meta.3 (( concat "literal:" meta.2 )) // meta.4 (( grab meta.3 )) So(len(ev.DataOps), ShouldEqual, 4) So(ev.DataOps[0].where.String(), ShouldEqual, "meta.1") So(ev.DataOps[0].src, ShouldEqual, "(( grab meta.0 ))") So(ev.DataOps[1].where.String(), ShouldEqual, "meta.2") So(ev.DataOps[1].src, ShouldEqual, "(( grab meta.1 ))") So(ev.DataOps[2].where.String(), ShouldEqual, "meta.3") So(ev.DataOps[2].src, ShouldEqual, `(( concat "literal:" meta.2 ))`) So(ev.DataOps[3].where.String(), ShouldEqual, "meta.4") So(ev.DataOps[3].src, ShouldEqual, "(( grab meta.3 ))") }) Convey("handles deep copy in data flow graph", func() { ev := &Evaluator{ Tree: YAML( `meta: first: [ a, b, c ] second: (( grab meta.first )) third: (( grab meta.second )) gotcha: (( grab meta.third.0 )) `), } err := ev.DataFlow() So(err, ShouldBeNil) // expect: meta.second (( grab meta.first )) // meta.third (( grab meta.second )) // meta.gotcha (( grab meta.third.0 )) // // (the key point here is that meta.third.0 doesn't // exist in the tree until we start evaluating, but // we still need to get the order correct; we should // have a dep on meta.third, and hope that run-time // resolution puts an array there for us to find...) // So(len(ev.DataOps), ShouldEqual, 3) So(ev.DataOps[0].where.String(), ShouldEqual, "meta.second") So(ev.DataOps[0].src, ShouldEqual, "(( grab meta.first ))") So(ev.DataOps[1].where.String(), ShouldEqual, "meta.third") So(ev.DataOps[1].src, ShouldEqual, "(( grab meta.second ))") So(ev.DataOps[2].where.String(), ShouldEqual, "meta.gotcha") So(ev.DataOps[2].src, ShouldEqual, "(( grab meta.third.0 ))") }) Convey("handles implicit static_ip dependency on jobs.*.networks.*.name", func() { ev := &Evaluator{ Tree: YAML( `meta: environment: prod size: 4 networks: - name: sandbox subnets: - static: [ 10.2.0.5 - 10.2.0.10 ] - name: prod subnets: - static: [ 10.0.0.5 - 10.0.0.100 ] jobs: - name: job1 instances: 4 networks: - name: (( grab meta.environment )) static_ips: (( static_ips 1 2 3 4 )) `), } err := ev.DataFlow() So(err, ShouldBeNil) // expect: jobs.0.networks.0.name (( grab meta.environment )) // jobs.0.networks.0.static_ips (( static_ips 1 2 3 4 )) So(len(ev.DataOps), ShouldEqual, 2) So(ev.DataOps[0].where.String(), ShouldEqual, "jobs.0.networks.0.name") So(ev.DataOps[0].src, ShouldEqual, "(( grab meta.environment ))") So(ev.DataOps[1].where.String(), ShouldEqual, "jobs.0.networks.0.static_ips") So(ev.DataOps[1].src, ShouldEqual, "(( static_ips 1 2 3 4 ))") }) Convey("handles implicit static_ip dependency on networks.*.name", func() { ev := &Evaluator{ Tree: YAML( `meta: net: real environment: prod size: 4 networks: - name: (( concat meta.net "-prod" )) subnets: - static: [ 10.0.0.5 - 10.0.0.100 ] jobs: - name: job1 instances: 4 networks: - name: prod-net # must be literal to avoid non-determinism static_ips: (( static_ips 1 2 3 4 )) `), } err := ev.DataFlow() So(err, ShouldBeNil) // expect: networks.0.name (( concat meta.net "-prod" )) // jobs.0.networks.0.static_ips (( static_ips 1 2 3 4 )) So(len(ev.DataOps), ShouldEqual, 2) So(ev.DataOps[0].where.String(), ShouldEqual, "networks.0.name") So(ev.DataOps[0].src, ShouldEqual, `(( concat meta.net "-prod" ))`) So(ev.DataOps[1].where.String(), ShouldEqual, "jobs.0.networks.0.static_ips") So(ev.DataOps[1].src, ShouldEqual, "(( static_ips 1 2 3 4 ))") }) Convey("handles dependency on static_ips() calls via (( grab )) calls", func() { ev := &Evaluator{ Tree: YAML( `meta: networks: - name: net1 subnets: - static: [ 10.0.0.5 - 10.0.0.100 ] jobs: - name: job1 instances: 4 networks: - name: net1 static_ips: (( static_ips 1 2 3 4 )) properties: job_ips: (( grab jobs.job1.networks.net1.static_ips )) `), } err := ev.DataFlow() So(err, ShouldBeNil) So(ev.DataOps, ShouldNotBeNil) // expect: jobs.0.networks.0.static_ips (( static_ips 1 2 3 4 )) // properties.job_ips (( grab jobs.job1.networks.net1.static_ips )) So(len(ev.DataOps), ShouldEqual, 2) So(ev.DataOps[0].where.String(), ShouldEqual, "jobs.0.networks.0.static_ips") So(ev.DataOps[0].src, ShouldEqual, "(( static_ips 1 2 3 4 ))") So(ev.DataOps[1].where.String(), ShouldEqual, "properties.job_ips") So(ev.DataOps[1].src, ShouldEqual, "(( grab jobs.job1.networks.net1.static_ips ))") }) Convey("handles implicit deps on sub-tree operations in (( inject ... )) targets", func() { ev := &Evaluator{ Tree: YAML( `meta: template: foo: bar baz: (( grab meta.template.foo )) thing: <<<: (( inject meta.template )) `), } err := ev.DataFlow() So(err, ShouldBeNil) So(len(ev.DataOps), ShouldEqual, 2) So(ev.DataOps[0].where.String(), ShouldEqual, "meta.template.baz") So(ev.DataOps[0].src, ShouldEqual, "(( grab meta.template.foo ))") So(ev.DataOps[1].where.String(), ShouldEqual, "thing.<<<") So(ev.DataOps[1].src, ShouldEqual, "(( inject meta.template ))") }) Convey("handles inject of an inject of a grab (so meta)", func() { ev := &Evaluator{ Tree: YAML( `meta: template1: foo: bar baz: (( grab meta.template1.foo )) template2: <<<: (( inject meta.template1 )) xyzzy: nothing happens thing: <<<: (( inject meta.template2 )) `), } err := ev.DataFlow() So(err, ShouldBeNil) So(len(ev.DataOps), ShouldEqual, 3) So(ev.DataOps[0].where.String(), ShouldEqual, "meta.template1.baz") So(ev.DataOps[0].src, ShouldEqual, "(( grab meta.template1.foo ))") So(ev.DataOps[1].where.String(), ShouldEqual, "meta.template2.<<<") So(ev.DataOps[1].src, ShouldEqual, "(( inject meta.template1 ))") So(ev.DataOps[2].where.String(), ShouldEqual, "thing.<<<") So(ev.DataOps[2].src, ShouldEqual, "(( inject meta.template2 ))") }) }) Convey("Patching", func() { valueIs := func(ev *Evaluator, path string, expect string) { c, err := ParseCursor(path) So(err, ShouldBeNil) v, err := c.ResolveString(ev.Tree) So(err, ShouldBeNil) So(v, ShouldEqual, expect) } notPresent := func(ev *Evaluator, path string) { c, err := ParseCursor(path) So(err, ShouldBeNil) _, err = c.ResolveString(ev.Tree) So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, "could not be found") } Convey("can handle simple map-based Replace actions", func() { ev := &Evaluator{ Tree: YAML( `meta: domain: sandbox.example.com web: (( grab meta.domain )) urls: home: (( concat "http://www." meta.web )) `), } err := ev.DataFlow() So(err, ShouldBeNil) err = ev.Patch() So(err, ShouldBeNil) valueIs(ev, "meta.web", "sandbox.example.com") valueIs(ev, "urls.home", "http://www.sandbox.example.com") }) Convey("can handle Replacement actions where the new value is a list", func() { ev := &Evaluator{ Tree: YAML( `meta: things: - one - two grocery: list: (( grab meta.things )) `), } err := ev.DataFlow() So(err, ShouldBeNil) err = ev.Patch() So(err, ShouldBeNil) valueIs(ev, "grocery.list.0", "one") valueIs(ev, "grocery.list.1", "two") }) Convey("can handle Replacement actions where the call site is a list", func() { ev := &Evaluator{ Tree: YAML( `meta: first: 2nd second: 1st sorted: list: - (( grab meta.second )) - (( grab meta.first )) `), } err := ev.DataFlow() So(err, ShouldBeNil) err = ev.Patch() So(err, ShouldBeNil) valueIs(ev, "sorted.list.0", "1st") valueIs(ev, "sorted.list.1", "2nd") }) Convey("can handle Replacement actions where the call site is inside of a list", func() { ev := &Evaluator{ Tree: YAML( `meta: prod: production sandbox: sb322 boxen: - name: www env: (( grab meta.prod )) - name: wwwtest env: (( grab meta.sandbox )) `), } err := ev.DataFlow() So(err, ShouldBeNil) err = ev.Patch() So(err, ShouldBeNil) valueIs(ev, "boxen.www.env", "production") valueIs(ev, "boxen.wwwtest.env", "sb322") }) Convey("can handle simple Inject actions", func() { ev := &Evaluator{ Tree: YAML( `templates: www: HA: enabled DR: disabled host: web1: type: www <<<: (( inject templates.www )) `), } err := ev.DataFlow() So(err, ShouldBeNil) err = ev.Patch() So(err, ShouldBeNil) valueIs(ev, "host.web1.HA", "enabled") valueIs(ev, "host.web1.DR", "disabled") valueIs(ev, "host.web1.type", "www") notPresent(ev, "host.web1.<<<") }) Convey("can handle Inject actions where call site is in a list", func() { ev := &Evaluator{ Tree: YAML( `meta: jobs: api: template: api worker: template: worker db: template: database jobs: - name: api_z1 <<<: (( inject meta.jobs.api )) - name: api_z2 <<<: (( inject meta.jobs.api )) - name: worker_z3 <<<: (( inject meta.jobs.worker )) - name: db_z3 <<<: (( inject meta.jobs.db )) `), } err := ev.DataFlow() So(err, ShouldBeNil) err = ev.Patch() So(err, ShouldBeNil) valueIs(ev, "jobs.api_z1.template", "api") valueIs(ev, "jobs.api_z2.template", "api") valueIs(ev, "jobs.db_z3.template", "database") valueIs(ev, "jobs.worker_z3.template", "worker") }) Convey("preserves call-site keys on conflict in an Inject scenario", func() { ev := &Evaluator{ Tree: YAML( `meta: template: foo: FOO bar: BAR example: <<<: (( inject meta.template )) foo: foooo `), } err := ev.DataFlow() So(err, ShouldBeNil) err = ev.Patch() So(err, ShouldBeNil) valueIs(ev, "example.foo", "foooo") valueIs(ev, "example.bar", "BAR") }) Convey("merges sub-trees common to inject site and injected values", func() { ev := &Evaluator{ Tree: YAML( `meta: template: properties: foo: bar baz: NOT-OVERRIDDEN thing: <<<: (( inject meta.template )) properties: bar: baz baz: overridden `), } err := ev.DataFlow() So(err, ShouldBeNil) err = ev.Patch() So(err, ShouldBeNil) valueIs(ev, "thing.properties.bar", "baz") valueIs(ev, "thing.properties.baz", "overridden") valueIs(ev, "thing.properties.foo", "bar") }) Convey("uses deep-copy semantics to handle overrides correctly on template re-use", func() { ev := &Evaluator{ Tree: YAML( `meta: template: properties: key: DEFAULT sub: key: DEFAULT foo: <<<: (( inject meta.template )) properties: key: FOO sub: key: FOO bar: <<<: (( inject meta.template )) properties: key: BAR sub: key: BAR boz: <<<: (( inject meta.template )) properties: key: BOZ sub: key: BOZ `), } err := ev.DataFlow() So(err, ShouldBeNil) err = ev.Patch() So(err, ShouldBeNil) valueIs(ev, "foo.properties.key", "FOO") valueIs(ev, "foo.properties.sub.key", "FOO") valueIs(ev, "bar.properties.key", "BAR") valueIs(ev, "bar.properties.sub.key", "BAR") valueIs(ev, "boz.properties.key", "BOZ") valueIs(ev, "boz.properties.sub.key", "BOZ") }) Convey("uses deep-copy semantics for re-use of injected templates with embedded lists", func() { ev := &Evaluator{ Tree: YAML( `meta: template: properties: key: DEFAULT list: - key: DEFAULT foo: <<<: (( inject meta.template )) properties: key: FOO list: - key: FOO bar: <<<: (( inject meta.template )) properties: key: BAR list: - key: BAR boz: <<<: (( inject meta.template )) properties: key: BOZ list: - key: BOZ `), } err := ev.DataFlow() So(err, ShouldBeNil) err = ev.Patch() So(err, ShouldBeNil) valueIs(ev, "foo.properties.key", "FOO") valueIs(ev, "foo.properties.list[0].key", "FOO") valueIs(ev, "bar.properties.key", "BAR") valueIs(ev, "bar.properties.list[0].key", "BAR") valueIs(ev, "boz.properties.key", "BOZ") valueIs(ev, "boz.properties.list[0].key", "BOZ") }) Convey("handles static_ips() call and a subsequent grab", func() { ev := &Evaluator{ Tree: YAML( `jobs: - name: api_z1 instances: 1 networks: - name: net1 static_ips: (( static_ips(0, 1, 2) )) networks: - name: net1 subnets: - static: [192.168.1.2 - 192.168.1.30] properties: api_servers: (( grab jobs.api_z1.networks.net1.static_ips )) `), } err := ev.DataFlow() So(err, ShouldBeNil) err = ev.Patch() So(err, ShouldBeNil) valueIs(ev, "jobs.api_z1.networks.net1.static_ips.0", "192.168.1.2") valueIs(ev, "properties.api_servers.0", "192.168.1.2") }) Convey("handles allocation conflicts of static IP addresses", func() { ev := &Evaluator{ Tree: YAML( `jobs: - name: api_z1 instances: 1 networks: - name: net1 static_ips: (( static_ips(0, 1, 2) )) - name: api_z2 instances: 1 networks: - name: net1 static_ips: (( static_ips(0, 1, 2) )) networks: - name: net1 subnets: - static: [192.168.1.2 - 192.168.1.30] `), } err := ev.DataFlow() So(err, ShouldBeNil) err = ev.Patch() So(err, ShouldNotBeNil) }) }) }) }
func TestEvaluator(t *testing.T) { YAML := func(s string) map[interface{}]interface{} { y, err := simpleyaml.NewYaml([]byte(s)) So(err, ShouldBeNil) data, err := y.Map() So(err, ShouldBeNil) return data } ToYAML := func(tree map[interface{}]interface{}) string { y, err := yaml.Marshal(tree) So(err, ShouldBeNil) return string(y) } ReYAML := func(s string) string { return ToYAML(YAML(s)) } RunPhaseTests := func(phase OperatorPhase, src string) { var test, input, dataflow, output string var current *string testPat := regexp.MustCompile(`^##+\s+(.*)\s*$`) convey := func() { if test != "" { Convey(test, func() { ev := &Evaluator{Tree: YAML(input)} ops, err := ev.DataFlow(phase) So(err, ShouldBeNil) // map data flow ops into 'dataflow:' YAML list var flow []map[string]string for _, op := range ops { flow = append(flow, map[string]string{op.where.String(): op.src}) } So(ToYAML(map[interface{}]interface{}{"dataflow": flow}), ShouldEqual, ReYAML(dataflow)) err = ev.RunPhase(phase) So(err, ShouldBeNil) So(ToYAML(ev.Tree), ShouldEqual, ReYAML(output)) }) } } s := bufio.NewScanner(strings.NewReader(src)) for s.Scan() { if testPat.MatchString(s.Text()) { m := testPat.FindStringSubmatch(s.Text()) convey() test, input, dataflow, output = m[1], "", "", "" continue } if s.Text() == "---" { if input == "" { current = &input } else if dataflow == "" { current = &dataflow } else { current = &output } continue } if current != nil { *current = *current + s.Text() + "\n" } } convey() } /* ## ## ######## ######## ###### ######## ### ### ## ## ## ## ## ## #### #### ## ## ## ## ## ## ### ## ###### ######## ## #### ###### ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ######## ## ## ###### ######## */ Convey("Merge Phase", t, func() { RunPhaseTests(MergePhase, ` ################################################## can handle simplest case --- templates: www: HA: enabled DR: disabled host: web1: type: www <<<: (( inject templates.www )) --- dataflow: - host.web1.<<<: (( inject templates.www )) --- templates: www: HA: enabled DR: disabled host: web1: type: www HA: enabled DR: disabled ################################################ ignores EvalPhase operators --- meta: template: check: (( param "stuff" )) baz: (( grab meta.template.foo )) str: (( concat "foo" meta.template.baz )) foo: bar thing: <<<: (( inject meta.template )) --- dataflow: - thing.<<<: (( inject meta.template )) --- meta: template: check: (( param "stuff" )) baz: (( grab meta.template.foo )) str: (( concat "foo" meta.template.baz )) foo: bar thing: check: (( param "stuff" )) baz: (( grab meta.template.foo )) str: (( concat "foo" meta.template.baz )) foo: bar ############################################ handles nested (( inject ... )) calls --- meta: template1: foo: bar baz: (( grab meta.template1.foo )) template2: <<<: (( inject meta.template1 )) xyzzy: nothing happens thing: <<<: (( inject meta.template2 )) --- dataflow: - meta.template2.<<<: (( inject meta.template1 )) - thing.<<<: (( inject meta.template2 )) --- meta: template1: foo: bar baz: (( grab meta.template1.foo )) template2: foo: bar baz: (( grab meta.template1.foo )) xyzzy: nothing happens thing: foo: bar baz: (( grab meta.template1.foo )) xyzzy: nothing happens ############################################ handles nested (( inject ... )) through array aliasing --- a: foo: bar jobs: - name: b <<<: (( inject a )) boz: baz c: <<<: (( inject jobs.b )) xy: zzy --- dataflow: - jobs.b.<<<: (( inject a )) - c.<<<: (( inject jobs.b )) --- a: foo: bar jobs: - name: b foo: bar boz: baz c: name: b foo: bar boz: baz xy: zzy ######################################################### handles Inject into a list --- meta: jobs: api: template: api worker: template: worker db: template: database jobs: - name: api_z1 <<<: (( inject meta.jobs.api )) - name: api_z2 <<<: (( inject meta.jobs.api )) - name: worker_z3 <<<: (( inject meta.jobs.worker )) - name: db_z3 <<<: (( inject meta.jobs.db )) --- dataflow: - jobs.api_z1.<<<: (( inject meta.jobs.api )) - jobs.api_z2.<<<: (( inject meta.jobs.api )) - jobs.worker_z3.<<<: (( inject meta.jobs.worker )) - jobs.db_z3.<<<: (( inject meta.jobs.db )) --- meta: jobs: api: template: api worker: template: worker db: template: database jobs: - name: api_z1 template: api - name: api_z2 template: api - name: worker_z3 template: worker - name: db_z3 template: database ################################# preserves call-site keys on conflict in an Inject scenario --- meta: template: foo: FOO bar: BAR example: <<<: (( inject meta.template )) foo: foooo --- dataflow: - example.<<<: (( inject meta.template )) --- meta: template: foo: FOO bar: BAR example: foo: foooo bar: BAR ############################# merges sub-tress common to inject sites and injected values --- meta: template: properties: foo: bar baz: NOT-OVERRIDDEN thing: <<<: (( inject meta.template )) properties: bar: baz baz: overridden --- dataflow: - thing.<<<: (( inject meta.template )) --- meta: template: properties: foo: bar baz: NOT-OVERRIDDEN thing: properties: foo: bar bar: baz baz: overridden ################# merges name-indexed sub-arrays properly between call-site and inject site --- meta: api_node: templates: - name: my_job release: my_release - name: my_other_job release: my_other_release properties: foo: bar jobs: api_node: .: (( inject meta.api_node )) properties: this: that templates: - name: my_superspecial_job release: my_superspecial_release --- dataflow: - jobs.api_node..: (( inject meta.api_node )) --- meta: api_node: templates: - name: my_job release: my_release - name: my_other_job release: my_other_release properties: foo: bar jobs: api_node: properties: foo: bar this: that templates: - name: my_job release: my_release - name: my_other_job release: my_other_release - name: my_superspecial_job release: my_superspecial_release ################# uses deep-copy semantics to handle overrides correctly on template re-use --- meta: template: properties: key: DEFAULT sub: key: DEFAULT foo: <<<: (( inject meta.template )) properties: key: FOO sub: key: FOO bar: <<<: (( inject meta.template )) properties: key: BAR sub: key: BAR boz: <<<: (( inject meta.template )) properties: key: BOZ sub: key: BOZ --- dataflow: - bar.<<<: (( inject meta.template )) - boz.<<<: (( inject meta.template )) - foo.<<<: (( inject meta.template )) --- meta: template: properties: key: DEFAULT sub: key: DEFAULT foo: properties: key: FOO sub: key: FOO bar: properties: key: BAR sub: key: BAR boz: properties: key: BOZ sub: key: BOZ ######################################################### appends to injected arrays --- meta: job: templates: - first - second foo: <<<: (( inject meta.job )) templates: - third --- dataflow: - foo.<<<: (( inject meta.job )) --- meta: job: templates: - first - second foo: templates: - first - second - third `) }) Convey("Merge Phase Error Detection", t, func() { Convey("detects direct (a -> b -> a) cycles in data flow graph", func() { ev := &Evaluator{ Tree: YAML(` meta: bar: <<<: (( inject meta.foo )) foo: <<<: (( inject meta.bar )) `), } _, err := ev.DataFlow(MergePhase) So(err, ShouldNotBeNil) }) }) /* ######## ## ## ### ## ## ## ## ## ## ## ## ## ## ## ## ## ###### ## ## ## ## ## ## ## ## ######### ## ## ## ## ## ## ## ######## ### ## ## ######## */ Convey("Eval Phase", t, func() { RunPhaseTests(EvalPhase, ` ############################################################# handles simple expressions --- foo: (( concat "foo" ":" "bar" )) --- dataflow: - foo: (( concat "foo" ":" "bar" )) --- foo: foo:bar #################################################### handles simple reference expressions --- meta: domain: foo.bar domain: (( grab meta.domain )) --- dataflow: - domain: (( grab meta.domain )) --- meta: domain: foo.bar domain: foo.bar ######################################### handles simple reference-or-literal expressions --- meta: env: prod domain: (( grab meta.domain || "default-domain" )) env: (( grab meta.env || "sandbox" )) instances: (( grab meta.size || 42 )) nice: (( grab meta.nice || -5 )) pi: (( grab math.CONSTANTS.pi || 3.14159 )) delta: (( grab meta.delta || .001 )) secure: (( grab meta.secure || true )) online: (( grab meta.online || false )) --- dataflow: - delta: (( grab meta.delta || .001 )) - domain: (( grab meta.domain || "default-domain" )) - env: (( grab meta.env || "sandbox" )) - instances: (( grab meta.size || 42 )) - nice: (( grab meta.nice || -5 )) - online: (( grab meta.online || false )) - pi: (( grab math.CONSTANTS.pi || 3.14159 )) - secure: (( grab meta.secure || true )) --- meta: env: prod domain: default-domain env: prod instances: 42 nice: -5 pi: 3.14159 delta: 0.001 secure: true online: false ##################################### handles true, TRUE, and True as boolean keywords --- TrUe: sure things: - (( grab meta.enoent || true )) - (( grab meta.enoent || TRUE )) - (( grab meta.enoent || True )) - (( grab meta.enoent || TrUe )) --- dataflow: - things.0: (( grab meta.enoent || true )) - things.1: (( grab meta.enoent || TRUE )) - things.2: (( grab meta.enoent || True )) - things.3: (( grab meta.enoent || TrUe )) --- TrUe: sure things: - true - true - true - sure ##################################### handles false, FALSE, and False as boolean keywords --- FaLSe: why not? things: - (( grab meta.enoent || false )) - (( grab meta.enoent || FALSE )) - (( grab meta.enoent || False )) - (( grab meta.enoent || FaLSe )) --- dataflow: - things.0: (( grab meta.enoent || false )) - things.1: (( grab meta.enoent || FALSE )) - things.2: (( grab meta.enoent || False )) - things.3: (( grab meta.enoent || FaLSe )) --- FaLSe: why not? things: - false - false - false - why not? ###################### handles ~, nil, Nil, NIL, null, Null, and NULL as the nil keyword --- NuLL: 4 (haha ruby joke) things: - (( grab meta.enoent || ~ )) - (( grab meta.enoent || nil )) - (( grab meta.enoent || Nil )) - (( grab meta.enoent || NIL )) - (( grab meta.enoent || null )) - (( grab meta.enoent || Null )) - (( grab meta.enoent || NULL )) - (( grab meta.enoent || NuLL )) --- dataflow: - things.0: (( grab meta.enoent || ~ )) - things.1: (( grab meta.enoent || nil )) - things.2: (( grab meta.enoent || Nil )) - things.3: (( grab meta.enoent || NIL )) - things.4: (( grab meta.enoent || null )) - things.5: (( grab meta.enoent || Null )) - things.6: (( grab meta.enoent || NULL )) - things.7: (( grab meta.enoent || NuLL )) --- NuLL: 4 (haha ruby joke) things: - null - null - null - null - null - null - null - 4 (haha ruby joke) ######################################### handles simple reference-or-nil expressions --- domain: (( grab meta.domain || nil )) env: (( grab meta.env || ~ )) site: (( grab meta.site || null )) --- dataflow: - domain: (( grab meta.domain || nil )) - env: (( grab meta.env || ~ )) - site: (( grab meta.site || null )) --- domain: ~ env: ~ site: ~ ################################## stops at the first concrete (possibly false) expression --- meta: other: FAIL # # should stop here ----------. (because it's resolvable, even if it # | evaluates to a traditionally non-true value) # v foo: (( grab meta.enoent || false || meta.other || "failed" )) --- dataflow: - foo: (( grab meta.enoent || false || meta.other || "failed" )) --- meta: other: FAIL foo: false ################################## stops at the first concrete (possibly 0) expression --- meta: other: FAIL # # should stop here ----------. (because it's resolvable, even if it # | evaluates to a traditionally non-true value) # v foo: (( grab meta.enoent || 0 || meta.other || "failed" )) --- dataflow: - foo: (( grab meta.enoent || 0 || meta.other || "failed" )) --- meta: other: FAIL foo: 0 ################################## stops at the first concrete (possibly nil) expression --- meta: other: FAIL # # should stop here ----------. (because it's resolvable, even if it # | evaluates to a traditionally non-true value) # v foo: (( grab meta.enoent || nil || meta.other || "failed" )) --- dataflow: - foo: (( grab meta.enoent || nil || meta.other || "failed" )) --- meta: other: FAIL foo: ~ ############################################### handles concrete expression in the middle --- meta: second: SECOND foo: (( grab meta.first || meta.second || "unspecified" )) --- dataflow: - foo: (( grab meta.first || meta.second || "unspecified" )) --- meta: second: SECOND foo: SECOND ################################# skips short-circuited alternates in Data Flow analysis --- meta: foo: FOO bar: (( grab meta.foo )) boz: (( grab meta.foo )) foo: (( grab meta.bar || "foo?" || meta.boz )) # NOTE: meta.boz in $.foo is exempt from DFA, because the "foo?" literal # will *always* stop evaluation of the expression bar: (( grab meta.xyzzy || "bar?" || meta.boz )) # NOTE: same with $.bar; meta.boz is not in play --- dataflow: - bar: (( grab meta.xyzzy || "bar?" || meta.boz )) - meta.bar: (( grab meta.foo )) - meta.boz: (( grab meta.foo )) - foo: (( grab meta.bar || "foo?" || meta.boz )) --- meta: foo: FOO bar: FOO boz: FOO foo: FOO bar: bar? ##################################### handles Data Flow dependencies for all expressions --- meta: domain: example.com web: (( concat "www.", meta.domain || "sandbox.example.com" )) api: endpoint: (( grab meta.web || meta.domain || ~ )) --- dataflow: - meta.web: (( concat "www.", meta.domain || "sandbox.example.com" )) - api.endpoint: (( grab meta.web || meta.domain || ~ )) --- meta: domain: example.com web: www.example.com api: endpoint: www.example.com ############################### handles indirect addressing of lists-of-maps in Data Flow --- a: (( grab b.squad.value )) b: - name: squad value: (( grab c.value )) c: value: VALUE d: (( grab b.squad.value )) e: (( grab c.value )) --- dataflow: - b.squad.value: (( grab c.value )) - e: (( grab c.value )) - a: (( grab b.squad.value )) - d: (( grab b.squad.value )) --- a: VALUE b: - name: squad value: VALUE c: value: VALUE d: VALUE e: VALUE ################################# handles multiple space-separated or-operands properly --- meta: foo: FOO bar: BAR foobar: (( concat meta.foo || "foo" meta.bar || "bar" )) fooboz: (( concat meta.foo || "foo" meta.boz || "boz" )) --- dataflow: - foobar: (( concat meta.foo || "foo" meta.bar || "bar" )) - fooboz: (( concat meta.foo || "foo" meta.boz || "boz" )) --- meta: foo: FOO bar: BAR foobar: FOOBAR fooboz: FOOboz ################################# handles multiple comma-seaprated or-operands properly --- meta: foo: FOO bar: BAR foobar: (( concat meta.foo || "foo", meta.bar || "bar" )) fooboz: (( concat meta.foo || "foo", meta.boz || "boz" )) --- dataflow: - foobar: (( concat meta.foo || "foo", meta.bar || "bar" )) - fooboz: (( concat meta.foo || "foo", meta.boz || "boz" )) --- meta: foo: FOO bar: BAR foobar: FOOBAR fooboz: FOOboz ############################################ can handle simple map-based Replace actions --- meta: domain: sandbox.example.com web: (( grab meta.domain )) urls: home: (( concat "http://www." meta.web )) --- dataflow: - meta.web: (( grab meta.domain )) - urls.home: (( concat "http://www." meta.web )) --- meta: domain: sandbox.example.com web: sandbox.example.com urls: home: http://www.sandbox.example.com ############################ can handle Replacement actions where the new value is a list --- meta: things: - one - two grocery: list: (( grab meta.things )) --- dataflow: - grocery.list: (( grab meta.things )) --- meta: things: - one - two grocery: list: - one - two ############################ can handle Replacement actions where the call site is a list --- meta: first: 2nd second: 1st sorted: list: - (( grab meta.second )) - (( grab meta.first )) --- dataflow: - sorted.list.0: (( grab meta.second )) - sorted.list.1: (( grab meta.first )) --- meta: first: 2nd second: 1st sorted: list: - 1st - 2nd ################### can handle Replacement actions where the call site is inside of a list --- meta: prod: production sandbox: sb322 boxen: - name: www env: (( grab meta.prod )) - name: wwwtest env: (( grab meta.sandbox )) --- dataflow: - boxen.www.env: (( grab meta.prod )) - boxen.wwwtest.env: (( grab meta.sandbox )) --- meta: prod: production sandbox: sb322 boxen: - name: www env: production - name: wwwtest env: sb322 ######################################## handles sequentially dependent (( grab ... )) calls --- meta: foo: FOO bar: (( grab meta.foo )) baz: (( grab meta.bar )) quux: (( grab meta.baz )) boz: (( grab meta.quux )) --- dataflow: - meta.bar: (( grab meta.foo )) - meta.baz: (( grab meta.bar )) - meta.quux: (( grab meta.baz )) - meta.boz: (( grab meta.quux )) --- meta: foo: FOO bar: FOO baz: FOO quux: FOO boz: FOO ################################ handles sequentially dependent calls, regardless of operator --- meta: foo: FOO bar: (( grab meta.foo )) baz: (( grab meta.bar )) quux: (( concat "literal:" meta.baz )) boz: (( grab meta.quux )) --- dataflow: - meta.bar: (( grab meta.foo )) - meta.baz: (( grab meta.bar )) - meta.quux: (( concat "literal:" meta.baz )) - meta.boz: (( grab meta.quux )) --- meta: foo: FOO bar: FOO baz: FOO quux: literal:FOO boz: literal:FOO ############################################## handles operators dependencies inside of lists --- meta: - FOO - (( grab meta.0 )) - (( grab meta.1 )) - (( concat "literal:" meta.2 )) - (( grab meta.3 )) --- dataflow: - meta.1: (( grab meta.0 )) - meta.2: (( grab meta.1 )) - meta.3: (( concat "literal:" meta.2 )) - meta.4: (( grab meta.3 )) --- meta: - FOO - FOO - FOO - literal:FOO - literal:FOO ################################################# handles deep copy in data flow graph --- meta: first: [ a, b, c ] second: (( grab meta.first )) third: (( grab meta.second )) gotcha: (( grab meta.third.0 )) --- dataflow: - meta.second: (( grab meta.first )) - meta.third: (( grab meta.second )) - meta.gotcha: (( grab meta.third.0 )) # (the key point here is that meta.third.0 doesn't exist in the tree until we start # evaluating, but we still need to get the order correct; we should have a dep on # meta.third, and hope that run-time resolution puts an array there for us to find...) --- meta: first: [ a, b, c ] second: [ a, b, c ] third: [ a, b, c ] gotcha: a ############################### handles implicit static_ip dependency on networks.*.name --- meta: net: real environment: prod size: 4 networks: - name: (( concat meta.net "-prod" )) subnets: - static: [ 10.0.0.5 - 10.0.0.100 ] jobs: - name: job1 instances: 4 networks: - name: real-prod # must be literal to avoid non-determinism static_ips: (( static_ips 1 2 3 4 )) --- dataflow: - networks.0.name: (( concat meta.net "-prod" )) - jobs.job1.networks.real-prod.static_ips: (( static_ips 1 2 3 4 )) --- meta: net: real environment: prod size: 4 networks: - name: real-prod subnets: - static: [ 10.0.0.5 - 10.0.0.100 ] jobs: - name: job1 instances: 4 networks: - name: real-prod static_ips: # skip IP[0]! - 10.0.0.6 - 10.0.0.7 - 10.0.0.8 - 10.0.0.9 #################### handles implicit static_ip dependency on jobs.*.networks.*.name --- meta: environment: prod size: 4 networks: - name: sandbox subnets: - static: [ 10.2.0.5 - 10.2.0.10 ] - name: prod subnets: - static: [ 10.0.0.5 - 10.0.0.100 ] jobs: - name: job1 instances: 4 networks: - name: (( grab meta.environment )) static_ips: (( static_ips 1 2 3 4 )) --- dataflow: - jobs.job1.networks.0.name: (( grab meta.environment )) - jobs.job1.networks.0.static_ips: (( static_ips 1 2 3 4 )) --- meta: environment: prod size: 4 networks: - name: sandbox subnets: - static: [ 10.2.0.5 - 10.2.0.10 ] - name: prod subnets: - static: [ 10.0.0.5 - 10.0.0.100 ] jobs: - name: job1 instances: 4 networks: - name: prod static_ips: # skip IP[0]! - 10.0.0.6 - 10.0.0.7 - 10.0.0.8 - 10.0.0.9 ########################## handles (( static_ips ... )) call and a subsequent (( grab ... )) --- jobs: - name: api_z1 instances: 1 networks: - name: net1 static_ips: (( static_ips(0, 1, 2) )) networks: - name: net1 subnets: - static: [192.168.1.2 - 192.168.1.30] properties: api_servers: (( grab jobs.api_z1.networks.net1.static_ips )) --- dataflow: - jobs.api_z1.networks.net1.static_ips: (( static_ips(0, 1, 2) )) - properties.api_servers: (( grab jobs.api_z1.networks.net1.static_ips )) --- jobs: - name: api_z1 instances: 1 networks: - name: net1 static_ips: - 192.168.1.2 networks: - name: net1 subnets: - static: [192.168.1.2 - 192.168.1.30] properties: api_servers: - 192.168.1.2 ############################################# handles static_ip across multiple static ranges --- jobs: - name: api_z1 instances: 4 networks: - name: net1 static_ips: (( static_ips 0 1 2 3 )) networks: - name: net1 subnets: - static: - 10.0.0.2 - 10.0.0.3 # 2 ips - 10.0.0.90 # +1 - 10.0.0.100 - 10.0.0.103 # +4 --- dataflow: - jobs.api_z1.networks.net1.static_ips: (( static_ips 0 1 2 3 )) --- jobs: - name: api_z1 instances: 4 networks: - name: net1 static_ips: - 10.0.0.2 - 10.0.0.3 - 10.0.0.90 - 10.0.0.100 networks: - name: net1 subnets: - static: - 10.0.0.2 - 10.0.0.3 # 2 ips - 10.0.0.90 # +1 - 10.0.0.100 - 10.0.0.103 # +4 ######################################### Basic test of (( cartesian-product .... )) operator --- meta: hosts: - a.example.com - b.example.com - c.example.com port: 8088 hosts: (( cartesian-product meta.hosts ":" meta.port )) --- dataflow: - hosts: (( cartesian-product meta.hosts ":" meta.port )) --- meta: hosts: - a.example.com - b.example.com - c.example.com port: 8088 hosts: - a.example.com:8088 - b.example.com:8088 - c.example.com:8088 #################################### (( cartesian-product .... )) of an empty component array --- meta: hosts: [] port: 8088 hosts: (( cartesian-product meta.hosts ":" meta.port )) ports: (( cartesian-product meta.port ":" meta.hosts )) --- dataflow: - hosts: (( cartesian-product meta.hosts ":" meta.port )) - ports: (( cartesian-product meta.port ":" meta.hosts )) --- meta: hosts: [] port: 8088 hosts: [] ports: [] ########################################################## 1-ary (( cartesian-product .... )) --- meta: - [a, b, c] all: (( cartesian-product meta[0] )) --- dataflow: - all: (( cartesian-product meta[0] )) --- meta: - [a, b, c] all: [a, b, c] ########################################################## n-ary (( cartesian-product .... )) --- meta: - [a, b, c] - [1, 2, 3] - [x, 'y', z] all: (( cartesian-product meta[0] meta[1] meta[2] )) --- dataflow: - all: (( cartesian-product meta[0] meta[1] meta[2] )) --- meta: - [a, b, c] - [1, 2, 3] - [x, 'y', z] all: - a1x - a1y - a1z - a2x - a2y - a2z - a3x - a3y - a3z - b1x - b1y - b1z - b2x - b2y - b2z - b3x - b3y - b3z - c1x - c1y - c1z - c2x - c2y - c2z - c3x - c3y - c3z ########################################### (( cartesian-product ... )) with grab'd arguments --- meta: first: [a, b] second: [1, 2] third: (( grab meta.second )) all: (( cartesian-product meta.first "," meta.third )) --- dataflow: - meta.third: (( grab meta.second )) - all: (( cartesian-product meta.first "," meta.third )) --- meta: first: [a, b] second: [1, 2] third: [1, 2] all: - a,1 - a,2 - b,1 - b,2 ################################### (( cartesian-product ... )) with grab'd sublist arguments --- meta: first: [a, b] second: - x - (( grab meta.first[0] )) all: (( cartesian-product meta.first "," meta.second )) --- dataflow: - meta.second.1: (( grab meta.first[0] )) - all: (( cartesian-product meta.first "," meta.second )) --- meta: first: [a, b] second: [x, a] all: - a,x - a,a - b,x - b,a ########################################### can extract keys via the (( keys ... )) operator --- meta: config: first: this is the first value second: value: the second keys: (( keys meta.config )) --- dataflow: - keys: (( keys meta.config )) --- meta: config: first: this is the first value second: value: the second keys: - first - second ########################################### can extract keys from multiple maps --- meta: config: first: this is the first value second: value: the second alt: third: third config keys: (( keys meta.config meta.alt )) --- dataflow: - keys: (( keys meta.config meta.alt )) --- meta: config: first: this is the first value second: value: the second alt: third: third config keys: - first - second - third #################################### (( join ... )) an array with (( grab ...)) --- greeting: hello z: - (( grab greeting )) - world output: (( join " " z )) --- dataflow: - z.0: (( grab greeting )) - output: (( join " " z )) --- greeting: hello output: hello world z: - hello - world #################################### (( join ... )) an array with several (( grab ...))s --- greeting: hello greeting2: world z: - (( grab greeting )) - (( grab greeting2 )) output: (( join " " z )) --- dataflow: - z.0: (( grab greeting )) - z.1: (( grab greeting2 )) - output: (( join " " z )) --- greeting: hello greeting2: world output: hello world z: - hello - world #################################### (( join ... )) a string reference with a grab --- greeting: hello z_one: (( grab greeting )) z_two: world output: - (( join " " z_one z_two )) --- dataflow: - z_one: (( grab greeting )) - output.0: (( join " " z_one z_two )) --- greeting: hello output: - hello world z_one: hello z_two: world ################################################ basic escape sequence handling --- test: "" cr: (( concat test "a\rb" )) nl: (( concat test "a\nb" )) tab: (( concat test "a\tb" )) back: (( concat test "a\\b" )) dq: (( concat test "a\"b" )) sq: (( concat test "a\'b" )) --- dataflow: - back: (( concat test "a\\b" )) - cr: (( concat test "a\rb" )) - dq: (( concat test "a\"b" )) - nl: (( concat test "a\nb" )) - sq: (( concat test "a\'b" )) - tab: (( concat test "a\tb" )) --- test: "" cr: "a\rb" nl: "a\nb" tab: "a\tb" back: a\b dq: 'a"b' sq: "a'b" ############################################# repeated escape sequence handling --- compound: (( concat "Line1\nLine2\nLine3" "\n" "Line4\ttabbed\n" )) --- dataflow: - compound: (( concat "Line1\nLine2\nLine3" "\n" "Line4\ttabbed\n" )) --- compound: | Line1 Line2 Line3 Line4 tabbed ######################################## concat certs with newlines (escape seq) --- cert: |- -- BEGIN CERT -- unei3Eet2mahbou8 weiXi7choo7ufei8 --- END CERT --- key: |- -- BEGIN KEY --- chaev0Gai3Baedul noithaifu0ree0Ka shoowuBaoti4chee -- END KEY ----- combined: (( concat cert "\n" key "\n" )) --- dataflow: - combined: (( concat cert "\n" key "\n" )) --- cert: |- -- BEGIN CERT -- unei3Eet2mahbou8 weiXi7choo7ufei8 --- END CERT --- key: |- -- BEGIN KEY --- chaev0Gai3Baedul noithaifu0ree0Ka shoowuBaoti4chee -- END KEY ----- combined: | -- BEGIN CERT -- unei3Eet2mahbou8 weiXi7choo7ufei8 --- END CERT --- -- BEGIN KEY --- chaev0Gai3Baedul noithaifu0ree0Ka shoowuBaoti4chee -- END KEY ----- `) }) Convey("Eval Phase Error Detection", t, func() { Convey("detects direct (a -> b -> a) cycles in data flow graph", func() { ev := &Evaluator{ Tree: YAML(` meta: bar: (( grab meta.foo )) foo: (( grab meta.bar )) `), } _, err := ev.DataFlow(EvalPhase) So(err, ShouldNotBeNil) }) Convey("detects indirect (a -> b -> c -> a) cycles in data flow graph", func() { ev := &Evaluator{ Tree: YAML(` meta: foo: (( grab meta.bar )) bar: (( grab meta.baz )) baz: (( grab meta.foo )) `), } _, err := ev.DataFlow(EvalPhase) So(err, ShouldNotBeNil) }) Convey("detects indirect cycles created through operand data flow", func() { ev := &Evaluator{ Tree: YAML(` meta: foo: (( grab meta.bar )) bar: (( grab meta.baz )) baz: (( grab meta.enoent || meta.foo )) `), } _, err := ev.DataFlow(EvalPhase) So(err, ShouldNotBeNil) }) Convey("detects allocation conflicts of static IP addresses", func() { ev := &Evaluator{ Tree: YAML( `jobs: - name: api_z1 instances: 1 networks: - name: net1 static_ips: (( static_ips(0, 1, 2) )) - name: api_z2 instances: 1 networks: - name: net1 static_ips: (( static_ips(0, 1, 2) )) networks: - name: net1 subnets: - static: [192.168.1.2 - 192.168.1.30] `), } err := ev.RunPhase(EvalPhase) So(err, ShouldNotBeNil) }) Convey("detects unsatisfied (( param )) inside of a (( grab ... )) call", func() { ev := &Evaluator{ Tree: YAML(` meta: key: (( param "you must specify this" )) value: (( grab meta.key )) `), } err := ev.RunPhase(ParamPhase) So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, "1 error(s) detected") So(err.Error(), ShouldContainSubstring, "you must specify this") err = ev.RunPhase(EvalPhase) So(err, ShouldBeNil) }) Convey("detects unsatisfied (( param )) inside of a (( concat ... )) call", func() { ev := &Evaluator{ Tree: YAML(` --- meta: key: (( param "you must specify this" )) value: (( concat "key=" meta.key )) `), } err := ev.RunPhase(ParamPhase) So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, "1 error(s) detected") So(err.Error(), ShouldContainSubstring, "you must specify this") err = ev.RunPhase(EvalPhase) So(err, ShouldBeNil) }) Convey("handles non-list (direct) args to (( cartesian-product ... ))", func() { ev := &Evaluator{ Tree: YAML(` meta: list: [a,b,c] all: (( cartesian-product meta meta.list )) `), } err := ev.RunPhase(EvalPhase) So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, "cartesian-product operator only accepts arrays and string values") }) Convey("treats list-of-lists args to (( cartesian-product ... )) as an error", func() { ev := &Evaluator{ Tree: YAML(` meta: list: - [a,b,c] - [d,e,f] - [g] all: (( cartesian-product meta.list meta.list )) `), } err := ev.RunPhase(EvalPhase) So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, "cartesian-product operator can only operate on lists of scalar values") }) Convey("treats list-of-maps args to (( cartesian-product ... )) as an error", func() { ev := &Evaluator{ Tree: YAML(` meta: list: - name: a - name: b - name: c all: (( cartesian-product meta.list meta.list )) `), } err := ev.RunPhase(EvalPhase) So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, "cartesian-product operator can only operate on lists of scalar values") }) Convey("(( cartesian-product ... )) requires an argument", func() { ev := &Evaluator{ Tree: YAML(` all: (( cartesian-product )) `), } err := ev.RunPhase(EvalPhase) So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, "no arguments specified to (( cartesian-product ... ))") }) Convey("(( keys ... )) requires an argument", func() { ev := &Evaluator{ Tree: YAML(` keys: (( keys )) `), } err := ev.RunPhase(EvalPhase) So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, "no arguments specified to (( keys ... ))") }) Convey("treats attempt to call (( keys ... )) on a literal as an error", func() { ev := &Evaluator{ Tree: YAML(` meta: test: is this a map? keys: (( keys meta.test )) `), } err := ev.RunPhase(EvalPhase) So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, "$.keys: meta.test is not a map") }) Convey("treats attempt to call (( keys ... )) on a list as an error", func() { ev := &Evaluator{ Tree: YAML(` meta: test: - but wait - this is not - a map... keys: (( keys meta.test )) `), } err := ev.RunPhase(EvalPhase) So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, "$.keys: meta.test is not a map") }) }) }