github.com/hashicorp/hcl/v2@v2.20.0/ext/tryfunc/tryfunc_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package tryfunc 5 6 import ( 7 "testing" 8 9 "github.com/hashicorp/hcl/v2" 10 "github.com/hashicorp/hcl/v2/hclsyntax" 11 "github.com/zclconf/go-cty/cty" 12 "github.com/zclconf/go-cty/cty/function" 13 ) 14 15 func TestTryFunc(t *testing.T) { 16 tests := map[string]struct { 17 expr string 18 vars map[string]cty.Value 19 want cty.Value 20 wantErr string 21 }{ 22 "one argument succeeds": { 23 `try(1)`, 24 nil, 25 cty.NumberIntVal(1), 26 ``, 27 }, 28 "one marked argument succeeds": { 29 `try(sensitive)`, 30 map[string]cty.Value{ 31 "sensitive": cty.StringVal("secret").Mark("porpoise"), 32 }, 33 cty.StringVal("secret").Mark("porpoise"), 34 ``, 35 }, 36 "two arguments, first succeeds": { 37 `try(1, 2)`, 38 nil, 39 cty.NumberIntVal(1), 40 ``, 41 }, 42 "two arguments, first fails": { 43 `try(nope, 2)`, 44 nil, 45 cty.NumberIntVal(2), 46 ``, 47 }, 48 "two arguments, first depends on unknowns": { 49 `try(unknown, 2)`, 50 map[string]cty.Value{ 51 "unknown": cty.UnknownVal(cty.Number), 52 }, 53 cty.DynamicVal, // can't proceed until first argument is known 54 ``, 55 }, 56 "two arguments, first succeeds and second depends on unknowns": { 57 `try(1, unknown)`, 58 map[string]cty.Value{ 59 "unknown": cty.UnknownVal(cty.Number), 60 }, 61 cty.NumberIntVal(1), // we know 1st succeeds, so it doesn't matter that 2nd is unknown 62 ``, 63 }, 64 "two arguments, first depends on unknowns deeply": { 65 `try(has_unknowns, 2)`, 66 map[string]cty.Value{ 67 "has_unknowns": cty.ListVal([]cty.Value{cty.UnknownVal(cty.Bool)}), 68 }, 69 cty.DynamicVal, // can't proceed until first argument is wholly known 70 ``, 71 }, 72 "two arguments, first traverses through an unkown": { 73 `try(unknown.baz, 2)`, 74 map[string]cty.Value{ 75 "unknown": cty.UnknownVal(cty.Map(cty.String)), 76 }, 77 cty.DynamicVal, // can't proceed until first argument is wholly known 78 ``, 79 }, 80 "two arguments, both marked, first succeeds": { 81 `try(sensitive, other)`, 82 map[string]cty.Value{ 83 "sensitive": cty.StringVal("secret").Mark("porpoise"), 84 "other": cty.StringVal("that").Mark("a"), 85 }, 86 cty.StringVal("secret").Mark("porpoise"), 87 ``, 88 }, 89 "two arguments, both marked, second succeeds": { 90 `try(sensitive, other)`, 91 map[string]cty.Value{ 92 "other": cty.StringVal("that").Mark("a"), 93 }, 94 cty.StringVal("that").Mark("a"), 95 ``, 96 }, 97 "two arguments, result is element of marked list ": { 98 `try(sensitive[0], other)`, 99 map[string]cty.Value{ 100 "sensitive": cty.ListVal([]cty.Value{ 101 cty.StringVal("list"), 102 cty.StringVal("of "), 103 cty.StringVal("secrets"), 104 }).Mark("secret"), 105 "other": cty.StringVal("not"), 106 }, 107 cty.StringVal("list").Mark("secret"), 108 ``, 109 }, 110 "nested known expression from unknown": { 111 // this expression contains an unknown, but will always return in 112 // "bar" 113 `try({u: false ? unknown : "bar"}, other)`, 114 map[string]cty.Value{ 115 "unknown": cty.UnknownVal(cty.String), 116 "other": cty.MapVal(map[string]cty.Value{ 117 "v": cty.StringVal("oops"), 118 }), 119 }, 120 cty.ObjectVal(map[string]cty.Value{ 121 "u": cty.StringVal("bar"), 122 }), 123 ``, 124 }, 125 "nested index op on unknown": { 126 // unknown and other have identical types, but we must return a 127 // dynamic value since v could change within the final result value 128 // after the first argument becomes known. 129 `try({u: unknown["foo"], v: "orig"}, other)`, 130 map[string]cty.Value{ 131 "unknown": cty.UnknownVal(cty.Map(cty.String)), 132 "other": cty.MapVal(map[string]cty.Value{ 133 "u": cty.StringVal("oops"), 134 "v": cty.StringVal("oops"), 135 }), 136 }, 137 cty.DynamicVal, 138 ``, 139 }, 140 "three arguments, all fail": { 141 `try(this, that, this_thing_in_particular)`, 142 nil, 143 cty.NumberIntVal(2), 144 // The grammar of this stringification of the message is unfortunate, 145 // but caller can type-assert our result to get the original 146 // diagnostics directly in order to produce a better result. 147 `test.hcl:1,1-5: Error in function call; Call to function "try" failed: no expression succeeded: 148 - Variables not allowed (at test.hcl:1,5-9) 149 Variables may not be used here. 150 - Variables not allowed (at test.hcl:1,11-15) 151 Variables may not be used here. 152 - Variables not allowed (at test.hcl:1,17-41) 153 Variables may not be used here. 154 155 At least one expression must produce a successful result.`, 156 }, 157 "no arguments": { 158 `try()`, 159 nil, 160 cty.NilVal, 161 `test.hcl:1,1-5: Error in function call; Call to function "try" failed: at least one argument is required.`, 162 }, 163 } 164 165 for k, test := range tests { 166 t.Run(k, func(t *testing.T) { 167 expr, diags := hclsyntax.ParseExpression([]byte(test.expr), "test.hcl", hcl.Pos{Line: 1, Column: 1}) 168 if diags.HasErrors() { 169 t.Fatalf("unexpected problems: %s", diags.Error()) 170 } 171 172 ctx := &hcl.EvalContext{ 173 Variables: test.vars, 174 Functions: map[string]function.Function{ 175 "try": TryFunc, 176 }, 177 } 178 179 got, err := expr.Value(ctx) 180 181 if err != nil { 182 if test.wantErr != "" { 183 if got, want := err.Error(), test.wantErr; got != want { 184 t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) 185 } 186 } else { 187 t.Errorf("unexpected error\ngot: %s\nwant: <nil>", err) 188 } 189 return 190 } 191 if test.wantErr != "" { 192 t.Errorf("wrong error\ngot: <nil>\nwant: %s", test.wantErr) 193 } 194 195 if !test.want.RawEquals(got) { 196 t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want) 197 } 198 }) 199 } 200 } 201 202 func TestCanFunc(t *testing.T) { 203 tests := map[string]struct { 204 expr string 205 vars map[string]cty.Value 206 want cty.Value 207 }{ 208 "succeeds": { 209 `can(1)`, 210 nil, 211 cty.True, 212 }, 213 "fails": { 214 `can(nope)`, 215 nil, 216 cty.False, 217 }, 218 "simple unknown": { 219 `can(unknown)`, 220 map[string]cty.Value{ 221 "unknown": cty.UnknownVal(cty.Number), 222 }, 223 cty.UnknownVal(cty.Bool), 224 }, 225 "traversal through unknown": { 226 `can(unknown.foo)`, 227 map[string]cty.Value{ 228 "unknown": cty.UnknownVal(cty.Map(cty.Number)), 229 }, 230 cty.UnknownVal(cty.Bool), 231 }, 232 "deep unknown": { 233 `can(has_unknown)`, 234 map[string]cty.Value{ 235 "has_unknown": cty.ListVal([]cty.Value{cty.UnknownVal(cty.Bool)}), 236 }, 237 cty.UnknownVal(cty.Bool), 238 }, 239 } 240 241 for k, test := range tests { 242 t.Run(k, func(t *testing.T) { 243 expr, diags := hclsyntax.ParseExpression([]byte(test.expr), "test.hcl", hcl.Pos{Line: 1, Column: 1}) 244 if diags.HasErrors() { 245 t.Fatalf("unexpected problems: %s", diags.Error()) 246 } 247 248 ctx := &hcl.EvalContext{ 249 Variables: test.vars, 250 Functions: map[string]function.Function{ 251 "can": CanFunc, 252 }, 253 } 254 255 got, err := expr.Value(ctx) 256 if err != nil { 257 t.Errorf("unexpected error\ngot: %s\nwant: <nil>", err) 258 } 259 if !test.want.RawEquals(got) { 260 t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want) 261 } 262 }) 263 } 264 }