// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package tryfunc import ( "testing" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/function" ) func TestTryFunc(t *testing.T) { tests := map[string]struct { expr string vars map[string]cty.Value want cty.Value wantErr string }{ "one argument succeeds": { `try(1)`, nil, cty.NumberIntVal(1), ``, }, "one marked argument succeeds": { `try(sensitive)`, map[string]cty.Value{ "sensitive": cty.StringVal("secret").Mark("porpoise"), }, cty.StringVal("secret").Mark("porpoise"), ``, }, "two arguments, first succeeds": { `try(1, 2)`, nil, cty.NumberIntVal(1), ``, }, "two arguments, first fails": { `try(nope, 2)`, nil, cty.NumberIntVal(2), ``, }, "two arguments, first depends on unknowns": { `try(unknown, 2)`, map[string]cty.Value{ "unknown": cty.UnknownVal(cty.Number), }, cty.DynamicVal, // can't proceed until first argument is known ``, }, "two arguments, first succeeds and second depends on unknowns": { `try(1, unknown)`, map[string]cty.Value{ "unknown": cty.UnknownVal(cty.Number), }, cty.NumberIntVal(1), // we know 1st succeeds, so it doesn't matter that 2nd is unknown ``, }, "two arguments, first depends on unknowns deeply": { `try(has_unknowns, 2)`, map[string]cty.Value{ "has_unknowns": cty.ListVal([]cty.Value{cty.UnknownVal(cty.Bool)}), }, cty.DynamicVal, // can't proceed until first argument is wholly known ``, }, "two arguments, first traverses through an unkown": { `try(unknown.baz, 2)`, map[string]cty.Value{ "unknown": cty.UnknownVal(cty.Map(cty.String)), }, cty.DynamicVal, // can't proceed until first argument is wholly known ``, }, "two arguments, both marked, first succeeds": { `try(sensitive, other)`, map[string]cty.Value{ "sensitive": cty.StringVal("secret").Mark("porpoise"), "other": cty.StringVal("that").Mark("a"), }, cty.StringVal("secret").Mark("porpoise"), ``, }, "two arguments, both marked, second succeeds": { `try(sensitive, other)`, map[string]cty.Value{ "other": cty.StringVal("that").Mark("a"), }, cty.StringVal("that").Mark("a"), ``, }, "two arguments, result is element of marked list ": { `try(sensitive[0], other)`, map[string]cty.Value{ "sensitive": cty.ListVal([]cty.Value{ cty.StringVal("list"), cty.StringVal("of "), cty.StringVal("secrets"), }).Mark("secret"), "other": cty.StringVal("not"), }, cty.StringVal("list").Mark("secret"), ``, }, "nested known expression from unknown": { // this expression contains an unknown, but will always return in // "bar" `try({u: false ? unknown : "bar"}, other)`, map[string]cty.Value{ "unknown": cty.UnknownVal(cty.String), "other": cty.MapVal(map[string]cty.Value{ "v": cty.StringVal("oops"), }), }, cty.ObjectVal(map[string]cty.Value{ "u": cty.StringVal("bar"), }), ``, }, "nested index op on unknown": { // unknown and other have identical types, but we must return a // dynamic value since v could change within the final result value // after the first argument becomes known. `try({u: unknown["foo"], v: "orig"}, other)`, map[string]cty.Value{ "unknown": cty.UnknownVal(cty.Map(cty.String)), "other": cty.MapVal(map[string]cty.Value{ "u": cty.StringVal("oops"), "v": cty.StringVal("oops"), }), }, cty.DynamicVal, ``, }, "three arguments, all fail": { `try(this, that, this_thing_in_particular)`, nil, cty.NumberIntVal(2), // The grammar of this stringification of the message is unfortunate, // but caller can type-assert our result to get the original // diagnostics directly in order to produce a better result. `test.hcl:1,1-5: Error in function call; Call to function "try" failed: no expression succeeded: - Variables not allowed (at test.hcl:1,5-9) Variables may not be used here. - Variables not allowed (at test.hcl:1,11-15) Variables may not be used here. - Variables not allowed (at test.hcl:1,17-41) Variables may not be used here. At least one expression must produce a successful result.`, }, "no arguments": { `try()`, nil, cty.NilVal, `test.hcl:1,1-5: Error in function call; Call to function "try" failed: at least one argument is required.`, }, } for k, test := range tests { t.Run(k, func(t *testing.T) { expr, diags := hclsyntax.ParseExpression([]byte(test.expr), "test.hcl", hcl.Pos{Line: 1, Column: 1}) if diags.HasErrors() { t.Fatalf("unexpected problems: %s", diags.Error()) } ctx := &hcl.EvalContext{ Variables: test.vars, Functions: map[string]function.Function{ "try": TryFunc, }, } got, err := expr.Value(ctx) if err != nil { if test.wantErr != "" { if got, want := err.Error(), test.wantErr; got != want { t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) } } else { t.Errorf("unexpected error\ngot: %s\nwant: ", err) } return } if test.wantErr != "" { t.Errorf("wrong error\ngot: \nwant: %s", test.wantErr) } if !test.want.RawEquals(got) { t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want) } }) } } func TestCanFunc(t *testing.T) { tests := map[string]struct { expr string vars map[string]cty.Value want cty.Value }{ "succeeds": { `can(1)`, nil, cty.True, }, "fails": { `can(nope)`, nil, cty.False, }, "simple unknown": { `can(unknown)`, map[string]cty.Value{ "unknown": cty.UnknownVal(cty.Number), }, cty.UnknownVal(cty.Bool), }, "traversal through unknown": { `can(unknown.foo)`, map[string]cty.Value{ "unknown": cty.UnknownVal(cty.Map(cty.Number)), }, cty.UnknownVal(cty.Bool), }, "deep unknown": { `can(has_unknown)`, map[string]cty.Value{ "has_unknown": cty.ListVal([]cty.Value{cty.UnknownVal(cty.Bool)}), }, cty.UnknownVal(cty.Bool), }, } for k, test := range tests { t.Run(k, func(t *testing.T) { expr, diags := hclsyntax.ParseExpression([]byte(test.expr), "test.hcl", hcl.Pos{Line: 1, Column: 1}) if diags.HasErrors() { t.Fatalf("unexpected problems: %s", diags.Error()) } ctx := &hcl.EvalContext{ Variables: test.vars, Functions: map[string]function.Function{ "can": CanFunc, }, } got, err := expr.Value(ctx) if err != nil { t.Errorf("unexpected error\ngot: %s\nwant: ", err) } if !test.want.RawEquals(got) { t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want) } }) } }