mirror of
https://github.com/hashicorp/terraform.git
synced 2026-02-03 20:50:59 -05:00
636 lines
19 KiB
Go
636 lines
19 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package funcs
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/ext/customdecode"
|
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
|
"github.com/zclconf/go-cty/cty"
|
|
"github.com/zclconf/go-cty/cty/function"
|
|
|
|
"github.com/hashicorp/terraform/internal/collections"
|
|
)
|
|
|
|
func TestReplace(t *testing.T) {
|
|
tests := []struct {
|
|
String cty.Value
|
|
Substr cty.Value
|
|
Replace cty.Value
|
|
Want cty.Value
|
|
Err bool
|
|
}{
|
|
{ // Regular search and replace
|
|
cty.StringVal("hello"),
|
|
cty.StringVal("hel"),
|
|
cty.StringVal("bel"),
|
|
cty.StringVal("bello"),
|
|
false,
|
|
},
|
|
{ // Search string doesn't match
|
|
cty.StringVal("hello"),
|
|
cty.StringVal("nope"),
|
|
cty.StringVal("bel"),
|
|
cty.StringVal("hello"),
|
|
false,
|
|
},
|
|
{ // Regular expression
|
|
cty.StringVal("hello"),
|
|
cty.StringVal("/l/"),
|
|
cty.StringVal("L"),
|
|
cty.StringVal("heLLo"),
|
|
false,
|
|
},
|
|
{
|
|
cty.StringVal("helo"),
|
|
cty.StringVal("/(l)/"),
|
|
cty.StringVal("$1$1"),
|
|
cty.StringVal("hello"),
|
|
false,
|
|
},
|
|
{ // Bad regexp
|
|
cty.StringVal("hello"),
|
|
cty.StringVal("/(l/"),
|
|
cty.StringVal("$1$1"),
|
|
cty.UnknownVal(cty.String),
|
|
true,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(fmt.Sprintf("replace(%#v, %#v, %#v)", test.String, test.Substr, test.Replace), func(t *testing.T) {
|
|
got, err := Replace(test.String, test.Substr, test.Replace)
|
|
|
|
if test.Err {
|
|
if err == nil {
|
|
t.Fatal("succeeded; want error")
|
|
}
|
|
return
|
|
} else if err != nil {
|
|
t.Fatalf("unexpected error: %s", err)
|
|
}
|
|
|
|
if !got.RawEquals(test.Want) {
|
|
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStrContains(t *testing.T) {
|
|
tests := []struct {
|
|
String cty.Value
|
|
Substr cty.Value
|
|
Want cty.Value
|
|
Err bool
|
|
}{
|
|
{
|
|
cty.StringVal("hello"),
|
|
cty.StringVal("hel"),
|
|
cty.BoolVal(true),
|
|
false,
|
|
},
|
|
{
|
|
cty.StringVal("hello"),
|
|
cty.StringVal("lo"),
|
|
cty.BoolVal(true),
|
|
false,
|
|
},
|
|
{
|
|
cty.StringVal("hello1"),
|
|
cty.StringVal("1"),
|
|
cty.BoolVal(true),
|
|
false,
|
|
},
|
|
{
|
|
cty.StringVal("hello1"),
|
|
cty.StringVal("heo"),
|
|
cty.BoolVal(false),
|
|
false,
|
|
},
|
|
{
|
|
cty.StringVal("hello1"),
|
|
cty.NumberIntVal(1),
|
|
cty.UnknownVal(cty.Bool),
|
|
true,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(fmt.Sprintf("includes(%#v, %#v)", test.String, test.Substr), func(t *testing.T) {
|
|
got, err := StrContains(test.String, test.Substr)
|
|
|
|
if test.Err {
|
|
if err == nil {
|
|
t.Fatal("succeeded; want error")
|
|
}
|
|
return
|
|
} else if err != nil {
|
|
t.Fatalf("unexpected error: %s", err)
|
|
}
|
|
|
|
if !got.RawEquals(test.Want) {
|
|
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStartsWith(t *testing.T) {
|
|
tests := []struct {
|
|
String, Prefix cty.Value
|
|
Want cty.Value
|
|
WantError string
|
|
}{
|
|
{
|
|
cty.StringVal("hello world"),
|
|
cty.StringVal("hello"),
|
|
cty.True,
|
|
``,
|
|
},
|
|
{
|
|
cty.StringVal("hey world"),
|
|
cty.StringVal("hello"),
|
|
cty.False,
|
|
``,
|
|
},
|
|
{
|
|
cty.StringVal(""),
|
|
cty.StringVal(""),
|
|
cty.True,
|
|
``,
|
|
},
|
|
{
|
|
cty.StringVal("a"),
|
|
cty.StringVal(""),
|
|
cty.True,
|
|
``,
|
|
},
|
|
{
|
|
cty.StringVal(""),
|
|
cty.StringVal("a"),
|
|
cty.False,
|
|
``,
|
|
},
|
|
{
|
|
cty.UnknownVal(cty.String),
|
|
cty.StringVal("a"),
|
|
cty.UnknownVal(cty.Bool).RefineNotNull(),
|
|
``,
|
|
},
|
|
{
|
|
cty.UnknownVal(cty.String),
|
|
cty.StringVal(""),
|
|
cty.True,
|
|
``,
|
|
},
|
|
{
|
|
cty.UnknownVal(cty.String).Refine().StringPrefix("https:").NewValue(),
|
|
cty.StringVal(""),
|
|
cty.True,
|
|
``,
|
|
},
|
|
{
|
|
cty.UnknownVal(cty.String).Refine().StringPrefix("https:").NewValue(),
|
|
cty.StringVal("a"),
|
|
cty.False,
|
|
``,
|
|
},
|
|
{
|
|
cty.UnknownVal(cty.String).Refine().StringPrefix("https:").NewValue(),
|
|
cty.StringVal("ht"),
|
|
cty.True,
|
|
``,
|
|
},
|
|
{
|
|
cty.UnknownVal(cty.String).Refine().StringPrefix("https:").NewValue(),
|
|
cty.StringVal("https:"),
|
|
cty.True,
|
|
``,
|
|
},
|
|
{
|
|
cty.UnknownVal(cty.String).Refine().StringPrefix("https:").NewValue(),
|
|
cty.StringVal("https-"),
|
|
cty.False,
|
|
``,
|
|
},
|
|
{
|
|
cty.UnknownVal(cty.String).Refine().StringPrefix("https:").NewValue(),
|
|
cty.StringVal("https://"),
|
|
cty.UnknownVal(cty.Bool).RefineNotNull(),
|
|
``,
|
|
},
|
|
{
|
|
// Unicode combining characters edge-case: we match the prefix
|
|
// in terms of unicode code units rather than grapheme clusters,
|
|
// which is inconsistent with our string processing elsewhere but
|
|
// would be a breaking change to fix that bug now.
|
|
cty.StringVal("\U0001f937\u200d\u2642"), // "Man Shrugging" is encoded as "Person Shrugging" followed by zero-width joiner and then the masculine gender presentation modifier
|
|
cty.StringVal("\U0001f937"), // Just the "Person Shrugging" character without any modifiers
|
|
cty.True,
|
|
``,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(fmt.Sprintf("StartsWith(%#v, %#v)", test.String, test.Prefix), func(t *testing.T) {
|
|
got, err := StartsWithFunc.Call([]cty.Value{test.String, test.Prefix})
|
|
|
|
if test.WantError != "" {
|
|
gotErr := fmt.Sprintf("%s", err)
|
|
if gotErr != test.WantError {
|
|
t.Errorf("wrong error\ngot: %s\nwant: %s", gotErr, test.WantError)
|
|
}
|
|
return
|
|
} else if err != nil {
|
|
t.Fatalf("unexpected error: %s", err)
|
|
}
|
|
|
|
if !got.RawEquals(test.Want) {
|
|
t.Errorf(
|
|
"wrong result\nstring: %#v\nprefix: %#v\ngot: %#v\nwant: %#v",
|
|
test.String, test.Prefix, got, test.Want,
|
|
)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTemplateString(t *testing.T) {
|
|
// This function has some special restrictions on what syntax is valid
|
|
// in its first argument, so we'll test this one using HCL expressions
|
|
// as the inputs, rather than direct cty values as we do for most other
|
|
// functions in this package.
|
|
tests := []struct {
|
|
templateExpr string
|
|
exprScope map[string]cty.Value
|
|
vars cty.Value
|
|
want cty.Value
|
|
wantErr string
|
|
}{
|
|
{ // a single string interpolation that evaluates to null should fail
|
|
`template`,
|
|
map[string]cty.Value{
|
|
"template": cty.StringVal(`${test}`),
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"test": cty.NullVal(cty.String),
|
|
}),
|
|
cty.NilVal,
|
|
`<templatestring argument>:1,1-8: Template result is null; The result of the template is null, which is not a valid result for a templatestring call.`,
|
|
},
|
|
{ // a single string interpolation that evaluates to unknown should not fail
|
|
`template`,
|
|
map[string]cty.Value{
|
|
"template": cty.StringVal(`${test}`),
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"test": cty.UnknownVal(cty.String),
|
|
}),
|
|
cty.UnknownVal(cty.String).RefineNotNull(),
|
|
``,
|
|
},
|
|
{
|
|
`template`,
|
|
map[string]cty.Value{
|
|
"template": cty.StringVal(`it's ${a}`),
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("a value"),
|
|
}),
|
|
cty.StringVal(`it's a value`),
|
|
``,
|
|
},
|
|
{
|
|
`template`,
|
|
map[string]cty.Value{
|
|
"template": cty.StringVal(`${a}`),
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.True,
|
|
}),
|
|
// The special treatment of a template with only a single
|
|
// interpolation sequence does not apply to templatestring, because
|
|
// we're expecting to be evaluating templates fetched dynamically
|
|
// from somewhere else and want to avoid callers needing to deal
|
|
// with anything other than string results.
|
|
cty.StringVal(`true`),
|
|
``,
|
|
},
|
|
{
|
|
`template`,
|
|
map[string]cty.Value{
|
|
"template": cty.StringVal(`${a}`),
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.EmptyTupleVal,
|
|
}),
|
|
// The special treatment of a template with only a single
|
|
// interpolation sequence does not apply to templatestring, because
|
|
// we're expecting to be evaluating templates fetched dynamically
|
|
// from somewhere else and want to avoid callers needing to deal
|
|
// with anything other than string results.
|
|
cty.NilVal,
|
|
`invalid template result: string required, but have tuple`,
|
|
},
|
|
{
|
|
`data.whatever.whatever["foo"].result`,
|
|
map[string]cty.Value{
|
|
"data": cty.ObjectVal(map[string]cty.Value{
|
|
"whatever": cty.ObjectVal(map[string]cty.Value{
|
|
"whatever": cty.MapVal(map[string]cty.Value{
|
|
"foo": cty.ObjectVal(map[string]cty.Value{
|
|
"result": cty.StringVal("it's ${a}"),
|
|
}),
|
|
}),
|
|
}),
|
|
}),
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("a value"),
|
|
}),
|
|
cty.StringVal(`it's a value`),
|
|
``,
|
|
},
|
|
{
|
|
`data.whatever.whatever[each.key].result`,
|
|
map[string]cty.Value{
|
|
"data": cty.ObjectVal(map[string]cty.Value{
|
|
"whatever": cty.ObjectVal(map[string]cty.Value{
|
|
"whatever": cty.MapVal(map[string]cty.Value{
|
|
"foo": cty.ObjectVal(map[string]cty.Value{
|
|
"result": cty.StringVal("it's ${a}"),
|
|
}),
|
|
}),
|
|
}),
|
|
}),
|
|
"each": cty.ObjectVal(map[string]cty.Value{
|
|
"key": cty.StringVal("foo"),
|
|
}),
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("a value"),
|
|
}),
|
|
cty.StringVal(`it's a value`),
|
|
``,
|
|
},
|
|
{
|
|
`data.whatever.whatever[*].result`,
|
|
map[string]cty.Value{
|
|
"data": cty.ObjectVal(map[string]cty.Value{
|
|
"whatever": cty.ObjectVal(map[string]cty.Value{
|
|
"whatever": cty.TupleVal([]cty.Value{
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"result": cty.StringVal("it's ${a}"),
|
|
}),
|
|
}),
|
|
}),
|
|
}),
|
|
"each": cty.ObjectVal(map[string]cty.Value{
|
|
"key": cty.StringVal("foo"),
|
|
}),
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("a value"),
|
|
}),
|
|
cty.NilVal,
|
|
// We have an intentional hole in our heuristic for whether the
|
|
// first argument is a suitable expression which permits splat
|
|
// expressions just so that we can return the type mismatch error
|
|
// from the result not being a string, instead of the more general
|
|
// error about it not being a supported expression type.
|
|
`invalid template value: a string is required`,
|
|
},
|
|
{
|
|
`"can't write $${not_allowed}"`,
|
|
map[string]cty.Value{},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"not_allowed": cty.StringVal("a literal template"),
|
|
}),
|
|
cty.NilVal,
|
|
`invalid template expression: templatestring is only for rendering templates retrieved dynamically from elsewhere, and so does not support providing a literal template; consider using a template string expression instead`,
|
|
},
|
|
{
|
|
`"can't write ${not_allowed}"`,
|
|
map[string]cty.Value{},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"not_allowed": cty.StringVal("a literal template"),
|
|
}),
|
|
cty.NilVal,
|
|
`invalid template expression: templatestring is only for rendering templates retrieved dynamically from elsewhere; to render an inline template, consider using a plain template string expression`,
|
|
},
|
|
{
|
|
`"can't write %%{for x in things}a literal template%%{endfor}"`,
|
|
map[string]cty.Value{},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"things": cty.ListVal([]cty.Value{cty.True}),
|
|
}),
|
|
cty.NilVal,
|
|
`invalid template expression: templatestring is only for rendering templates retrieved dynamically from elsewhere, and so does not support providing a literal template; consider using a template string expression instead`,
|
|
},
|
|
{
|
|
`"can't write %{for x in things}a literal template%{endfor}"`,
|
|
map[string]cty.Value{},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"things": cty.ListVal([]cty.Value{cty.True}),
|
|
}),
|
|
cty.NilVal,
|
|
`invalid template expression: templatestring is only for rendering templates retrieved dynamically from elsewhere; to render an inline template, consider using a plain template string expression`,
|
|
},
|
|
{
|
|
`"${not_allowed}"`,
|
|
map[string]cty.Value{},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"not allowed": cty.StringVal("an interp-only template"),
|
|
}),
|
|
cty.NilVal,
|
|
`invalid template expression: templatestring is only for rendering templates retrieved dynamically from elsewhere; to treat the inner expression as template syntax, write the reference expression directly without any template interpolation syntax`,
|
|
},
|
|
{
|
|
`1 + 1`,
|
|
map[string]cty.Value{},
|
|
cty.ObjectVal(map[string]cty.Value{}),
|
|
cty.NilVal,
|
|
`invalid template expression: must be a direct reference to a single string from elsewhere, containing valid Terraform template syntax`,
|
|
},
|
|
{
|
|
`not_a_string`,
|
|
map[string]cty.Value{
|
|
"not_a_string": cty.True,
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{}),
|
|
cty.NilVal,
|
|
`invalid template value: a string is required`,
|
|
},
|
|
{
|
|
`with_lower`,
|
|
map[string]cty.Value{
|
|
"with_lower": cty.StringVal(`it's ${lower(a)}`),
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("A VALUE"),
|
|
}),
|
|
cty.StringVal("it's a value"),
|
|
``,
|
|
},
|
|
{
|
|
`with_core_lower`,
|
|
map[string]cty.Value{
|
|
"with_core_lower": cty.StringVal(`it's ${core::lower(a)}`),
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{
|
|
"a": cty.StringVal("A VALUE"),
|
|
}),
|
|
cty.StringVal("it's a value"),
|
|
``,
|
|
},
|
|
{
|
|
`with_fsfunc`,
|
|
map[string]cty.Value{
|
|
"with_fsfunc": cty.StringVal(`it's ${fsfunc()}`),
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{}),
|
|
cty.NilVal,
|
|
`<templatestring argument>:1,8-15: Error in function call; Call to function "fsfunc" failed: cannot use filesystem access functions like fsfunc in templatestring templates; consider passing the function result as a template variable instead.`,
|
|
},
|
|
{
|
|
`with_core_fsfunc`,
|
|
map[string]cty.Value{
|
|
"with_core_fsfunc": cty.StringVal(`it's ${core::fsfunc()}`),
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{}),
|
|
cty.NilVal,
|
|
`<templatestring argument>:1,8-21: Error in function call; Call to function "core::fsfunc" failed: cannot use filesystem access functions like fsfunc in templatestring templates; consider passing the function result as a template variable instead.`,
|
|
},
|
|
{
|
|
`with_templatefunc`,
|
|
map[string]cty.Value{
|
|
"with_templatefunc": cty.StringVal(`it's ${templatefunc()}`),
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{}),
|
|
cty.NilVal,
|
|
`<templatestring argument>:1,8-21: Error in function call; Call to function "templatefunc" failed: cannot recursively call templatefunc from inside another template function.`,
|
|
},
|
|
{
|
|
`with_core_templatefunc`,
|
|
map[string]cty.Value{
|
|
"with_core_templatefunc": cty.StringVal(`it's ${core::templatefunc()}`),
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{}),
|
|
cty.NilVal,
|
|
`<templatestring argument>:1,8-27: Error in function call; Call to function "core::templatefunc" failed: cannot recursively call templatefunc from inside another template function.`,
|
|
},
|
|
{
|
|
`with_fstemplatefunc`,
|
|
map[string]cty.Value{
|
|
"with_fstemplatefunc": cty.StringVal(`it's ${fstemplatefunc()}`),
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{}),
|
|
cty.NilVal,
|
|
// The template function error takes priority over the filesystem
|
|
// function error if calling a function that's in both categories.
|
|
`<templatestring argument>:1,8-23: Error in function call; Call to function "fstemplatefunc" failed: cannot recursively call fstemplatefunc from inside another template function.`,
|
|
},
|
|
{
|
|
`with_core_fstemplatefunc`,
|
|
map[string]cty.Value{
|
|
"with_core_fstemplatefunc": cty.StringVal(`it's ${core::fstemplatefunc()}`),
|
|
},
|
|
cty.ObjectVal(map[string]cty.Value{}),
|
|
cty.NilVal,
|
|
// The template function error takes priority over the filesystem
|
|
// function error if calling a function that's in both categories.
|
|
`<templatestring argument>:1,8-29: Error in function call; Call to function "core::fstemplatefunc" failed: cannot recursively call fstemplatefunc from inside another template function.`,
|
|
},
|
|
}
|
|
|
|
funcToTest := MakeTemplateStringFunc(func() (funcs map[string]function.Function, fsFuncs collections.Set[string], templateFuncs collections.Set[string]) {
|
|
// These are the functions available for use inside the nested template
|
|
// evaluation context. These are here only to test that we can call
|
|
// functions and that the template/filesystem functions get blocked
|
|
// with suitable error messages. This is not a realistic set of
|
|
// functions that would be available in a real call.
|
|
funcs = map[string]function.Function{
|
|
"lower": function.New(&function.Spec{
|
|
Params: []function.Parameter{
|
|
{
|
|
Name: "str",
|
|
Type: cty.String,
|
|
},
|
|
},
|
|
Type: function.StaticReturnType(cty.String),
|
|
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
|
s := args[0].AsString()
|
|
return cty.StringVal(strings.ToLower(s)), nil
|
|
},
|
|
}),
|
|
"fsfunc": function.New(&function.Spec{
|
|
Type: function.StaticReturnType(cty.String),
|
|
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
|
return cty.UnknownVal(retType), fmt.Errorf("should not be able to call fsfunc")
|
|
},
|
|
}),
|
|
"templatefunc": function.New(&function.Spec{
|
|
Type: function.StaticReturnType(cty.String),
|
|
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
|
return cty.UnknownVal(retType), fmt.Errorf("should not be able to call templatefunc")
|
|
},
|
|
}),
|
|
"fstemplatefunc": function.New(&function.Spec{
|
|
Type: function.StaticReturnType(cty.String),
|
|
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
|
return cty.UnknownVal(retType), fmt.Errorf("should not be able to call fstemplatefunc")
|
|
},
|
|
}),
|
|
}
|
|
funcs["core::lower"] = funcs["lower"]
|
|
funcs["core::fsfunc"] = funcs["fsfunc"]
|
|
funcs["core::templatefunc"] = funcs["templatefunc"]
|
|
funcs["core::fstemplatefunc"] = funcs["fstemplatefunc"]
|
|
return funcs, collections.NewSetCmp("fsfunc", "fstemplatefunc"), collections.NewSetCmp("templatefunc", "fstemplatefunc")
|
|
})
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.templateExpr, func(t *testing.T) {
|
|
// The following mimics what HCL itself would do when preparing
|
|
// the first argument to this function, since the parameter
|
|
// uses the special "expression closure type" which causes
|
|
// HCL to delay evaluation of the expression and let the
|
|
// function handle it directly itself.
|
|
expr, diags := hclsyntax.ParseExpression([]byte(test.templateExpr), "", hcl.InitialPos)
|
|
if diags.HasErrors() {
|
|
t.Fatalf("unexpected errors: %s", diags.Error())
|
|
}
|
|
exprClosure := &customdecode.ExpressionClosure{
|
|
Expression: expr,
|
|
EvalContext: &hcl.EvalContext{
|
|
Variables: test.exprScope,
|
|
},
|
|
}
|
|
exprClosureVal := customdecode.ExpressionClosureVal(exprClosure)
|
|
|
|
got, gotErr := funcToTest.Call([]cty.Value{exprClosureVal, test.vars})
|
|
|
|
if test.wantErr != "" {
|
|
if gotErr == nil {
|
|
t.Fatalf("unexpected success\ngot: %#v\nwant error: %s", got, test.wantErr)
|
|
}
|
|
if got, want := gotErr.Error(), test.wantErr; got != want {
|
|
t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want)
|
|
}
|
|
return
|
|
}
|
|
if gotErr != nil {
|
|
t.Errorf("unexpected error: %s", gotErr.Error())
|
|
}
|
|
if !test.want.RawEquals(got) {
|
|
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|