github.com/lab47/exprcore@v0.0.0-20210525052339-fb7d6bd9331e/exprcore/eval_test.go (about) 1 // Copyright 2017 The Bazel Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package exprcore_test 6 7 import ( 8 "bytes" 9 "fmt" 10 "math" 11 "path/filepath" 12 "sort" 13 "strings" 14 "testing" 15 16 "github.com/lab47/exprcore/exprcore" 17 "github.com/lab47/exprcore/exprcoretest" 18 "github.com/lab47/exprcore/internal/chunkedfile" 19 "github.com/lab47/exprcore/resolve" 20 "github.com/lab47/exprcore/syntax" 21 ) 22 23 // A test may enable non-standard options by containing (e.g.) "option:recursion". 24 func setOptions(src string) { 25 resolve.AllowFloat = option(src, "float") 26 resolve.AllowGlobalReassign = option(src, "globalreassign") 27 resolve.LoadBindsGlobally = option(src, "loadbindsglobally") 28 resolve.AllowLambda = option(src, "lambda") 29 resolve.AllowNestedDef = option(src, "nesteddef") 30 resolve.AllowRecursion = option(src, "recursion") 31 resolve.AllowSet = option(src, "set") 32 } 33 34 func option(chunk, name string) bool { 35 return strings.Contains(chunk, "option:"+name) 36 } 37 38 // Wrapper is the type of errors with an Unwrap method; see https://golang.org/pkg/errors. 39 type Wrapper interface { 40 Unwrap() error 41 } 42 43 func TestEvalExpr(t *testing.T) { 44 // This is mostly redundant with the new *.star tests. 45 // TODO(adonovan): move checks into *.star files and 46 // reduce this to a mere unit test of exprcore.Eval. 47 thread := new(exprcore.Thread) 48 for _, test := range []struct{ src, want string }{ 49 {`123`, `123`}, 50 {`-1`, `-1`}, 51 {`"a"+"b"`, `"ab"`}, 52 {`1+2`, `3`}, 53 54 // lists 55 {`[]`, `[]`}, 56 {`[1]`, `[1]`}, 57 {`[1,]`, `[1]`}, 58 {`[1, 2]`, `[1, 2]`}, 59 {`[2 * x for x in [1, 2, 3]]`, `[2, 4, 6]`}, 60 {`[2 * x for x in [1, 2, 3] if x > 1]`, `[4, 6]`}, 61 {`[(x, y) for x in [1, 2] for y in [3, 4]]`, 62 `[(1, 3), (1, 4), (2, 3), (2, 4)]`}, 63 {`[(x, y) for x in [1, 2] if x == 2 for y in [3, 4]]`, 64 `[(2, 3), (2, 4)]`}, 65 // tuples 66 {`()`, `()`}, 67 {`(1)`, `1`}, 68 {`(1,)`, `(1,)`}, 69 {`(1, 2)`, `(1, 2)`}, 70 {`(1, 2, 3, 4, 5)`, `(1, 2, 3, 4, 5)`}, 71 {`1, 2`, `(1, 2)`}, 72 // dicts 73 {`%{}`, `%{}`}, 74 {`%{"a": 1}`, `%{"a": 1}`}, 75 {`%{"a": 1,}`, `%{"a": 1}`}, 76 77 // conditional 78 {`1 if 3 > 2 else 0`, `1`}, 79 {`1 if "foo" else 0`, `1`}, 80 {`1 if "" else 0`, `0`}, 81 82 // indexing 83 {`["a", "b"][0]`, `"a"`}, 84 {`["a", "b"][1]`, `"b"`}, 85 {`("a", "b")[0]`, `"a"`}, 86 {`("a", "b")[1]`, `"b"`}, 87 {`"aΩb"[0]`, `"a"`}, 88 {`"aΩb"[1]`, `"\xce"`}, 89 {`"aΩb"[3]`, `"b"`}, 90 {`%{"a": 1}["a"]`, `1`}, 91 {`%{"a": 1}["b"]`, `key "b" not in dict`}, 92 {`%{}[[]]`, `unhashable type: list`}, 93 {`%{"a": 1}[[]]`, `unhashable type: list`}, 94 {`[x for x in range(3)]`, "[0, 1, 2]"}, 95 } { 96 var got string 97 if v, err := exprcore.Eval(thread, "<expr>", test.src, nil); err != nil { 98 got = err.Error() 99 } else { 100 got = v.String() 101 } 102 if got != test.want { 103 t.Errorf("eval %s = %s, want %s", test.src, got, test.want) 104 } 105 } 106 } 107 108 func TestExecFile(t *testing.T) { 109 defer setOptions("") 110 testdata := "." // exprcoretest.DataFile("exprcore", ".") 111 thread := &exprcore.Thread{Load: load} 112 exprcoretest.SetReporter(thread, t) 113 for _, file := range []string{ 114 "testdata/assign.star", 115 "testdata/bool.star", 116 "testdata/builtins.star", 117 "testdata/control.star", 118 "testdata/dict.star", 119 "testdata/float.star", 120 "testdata/function.star", 121 "testdata/int.star", 122 "testdata/list.star", 123 "testdata/misc.star", 124 "testdata/set.star", 125 "testdata/string.star", 126 "testdata/tuple.star", 127 "testdata/recursion.star", 128 "testdata/module.star", 129 "testdata/exprcore_eval.star", 130 } { 131 filename := filepath.Join(testdata, file) 132 for _, chunk := range chunkedfile.Read(filename, t) { 133 predeclared := exprcore.StringDict{ 134 "hasfields": exprcore.NewBuiltin("hasfields", newHasFields), 135 "fibonacci": fib{}, 136 } 137 138 setOptions(chunk.Source) 139 resolve.AllowLambda = true // used extensively 140 141 _, err := exprcore.ExecFile(thread, filename, chunk.Source, predeclared) 142 switch err := err.(type) { 143 case *exprcore.EvalError: 144 found := false 145 for i := range err.CallStack { 146 posn := err.CallStack.At(i).Pos 147 if posn.Filename() == filename { 148 chunk.GotError(int(posn.Line), err.Error()) 149 found = true 150 break 151 } 152 } 153 if !found { 154 t.Error(err.Backtrace()) 155 } 156 case nil: 157 // success 158 default: 159 t.Errorf("\n%s", err) 160 } 161 chunk.Done() 162 } 163 } 164 } 165 166 // A fib is an iterable value representing the infinite Fibonacci sequence. 167 type fib struct{} 168 169 func (t fib) Freeze() {} 170 func (t fib) String() string { return "fib" } 171 func (t fib) Type() string { return "fib" } 172 func (t fib) Truth() exprcore.Bool { return true } 173 func (t fib) Hash() (uint32, error) { return 0, fmt.Errorf("fib is unhashable") } 174 func (t fib) Iterate() exprcore.Iterator { return &fibIterator{0, 1} } 175 176 type fibIterator struct{ x, y int } 177 178 func (it *fibIterator) Next(p *exprcore.Value) bool { 179 *p = exprcore.MakeInt(it.x) 180 it.x, it.y = it.y, it.x+it.y 181 return true 182 } 183 func (it *fibIterator) Done() {} 184 185 // load implements the 'load' operation as used in the evaluator tests. 186 func load(thread *exprcore.Thread, module string) (exprcore.StringDict, error) { 187 if module == "assert.star" { 188 return exprcoretest.LoadAssertModule() 189 } 190 191 // TODO(adonovan): test load() using this execution path. 192 filename := filepath.Join(filepath.Dir(thread.CallFrame(0).Pos.Filename()), module) 193 return exprcore.ExecFile(thread, filename, nil, nil) 194 } 195 196 func newHasFields(thread *exprcore.Thread, b *exprcore.Builtin, args exprcore.Tuple, kwargs []exprcore.Tuple) (exprcore.Value, error) { 197 if len(args)+len(kwargs) > 0 { 198 return nil, fmt.Errorf("%s: unexpected arguments", b.Name()) 199 } 200 return &hasfields{attrs: make(map[string]exprcore.Value)}, nil 201 } 202 203 // hasfields is a test-only implementation of HasAttrs. 204 // It permits any field to be set. 205 // Clients will likely want to provide their own implementation, 206 // so we don't have any public implementation. 207 type hasfields struct { 208 attrs exprcore.StringDict 209 frozen bool 210 } 211 212 var ( 213 _ exprcore.HasAttrs = (*hasfields)(nil) 214 _ exprcore.HasBinary = (*hasfields)(nil) 215 ) 216 217 func (hf *hasfields) String() string { return "hasfields" } 218 func (hf *hasfields) Type() string { return "hasfields" } 219 func (hf *hasfields) Truth() exprcore.Bool { return true } 220 func (hf *hasfields) Hash() (uint32, error) { return 42, nil } 221 222 func (hf *hasfields) Freeze() { 223 if !hf.frozen { 224 hf.frozen = true 225 for _, v := range hf.attrs { 226 v.Freeze() 227 } 228 } 229 } 230 231 func (hf *hasfields) Attr(name string) (exprcore.Value, error) { return hf.attrs[name], nil } 232 233 func (hf *hasfields) SetField(name string, val exprcore.Value) error { 234 if hf.frozen { 235 return fmt.Errorf("cannot set field on a frozen hasfields") 236 } 237 if strings.HasPrefix(name, "no") { // for testing 238 return exprcore.NoSuchAttrError(fmt.Sprintf("no .%s field", name)) 239 } 240 hf.attrs[name] = val 241 return nil 242 } 243 244 func (hf *hasfields) AttrNames() []string { 245 names := make([]string, 0, len(hf.attrs)) 246 for key := range hf.attrs { 247 names = append(names, key) 248 } 249 sort.Strings(names) 250 return names 251 } 252 253 func (hf *hasfields) Binary(op syntax.Token, y exprcore.Value, side exprcore.Side) (exprcore.Value, error) { 254 // This method exists so we can exercise 'list += x' 255 // where x is not Iterable but defines list+x. 256 if op == syntax.PLUS { 257 if _, ok := y.(*exprcore.List); ok { 258 return exprcore.MakeInt(42), nil // list+hasfields is 42 259 } 260 } 261 return nil, nil 262 } 263 264 func TestParameterPassing(t *testing.T) { 265 const filename = "parameters.go" 266 const src = ` 267 def a() { 268 return 269 } 270 def b(a, b) { 271 return a, b 272 } 273 def c(a, b=42) { 274 return a, b 275 } 276 def d(*args) { 277 return args 278 } 279 def e(**kwargs) { 280 return kwargs 281 } 282 def f(a, b=42, *args, **kwargs) { 283 return a, b, args, kwargs 284 } 285 def g(a, b=42, *args, c=123, **kwargs) { 286 return a, b, args, c, kwargs 287 } 288 def h(a, b=42, *, c=123, **kwargs) { 289 return a, b, c, kwargs 290 } 291 def i(a, b=42, *, c, d=123, e, **kwargs) { 292 return a, b, c, d, e, kwargs 293 } 294 def j(a, b=42, *args, c, d=123, e, **kwargs) { 295 return a, b, args, c, d, e, kwargs 296 } 297 ` 298 299 thread := new(exprcore.Thread) 300 globals, err := exprcore.ExecFile(thread, filename, src, nil) 301 if err != nil { 302 t.Fatal(err) 303 } 304 305 for _, test := range []struct{ src, want string }{ 306 // a() 307 {`a()`, `None`}, 308 {`a(1)`, `function a accepts no arguments (1 given)`}, 309 310 // b(a, b) 311 {`b()`, `function b missing 2 arguments (a, b)`}, 312 {`b(1)`, `function b missing 1 argument (b)`}, 313 {`b(a:1)`, `function b missing 1 argument (b)`}, 314 {`b(b:1)`, `function b missing 1 argument (a)`}, 315 {`b(1, 2)`, `(1, 2)`}, 316 {`b`, `<function b>`}, // asserts that b's parameter b was treated as a local variable 317 {`b(1, 2, 3)`, `function b accepts 2 positional arguments (3 given)`}, 318 {`b(1, b:2)`, `(1, 2)`}, 319 {`b(1, a:2)`, `function b got multiple values for parameter "a"`}, 320 {`b(1, x:2)`, `function b got an unexpected keyword argument "x"`}, 321 {`b(a:1, b:2)`, `(1, 2)`}, 322 {`b(b:1, a:2)`, `(2, 1)`}, 323 {`b(b:1, a:2, x:1)`, `function b got an unexpected keyword argument "x"`}, 324 {`b(x:1, b:1, a:2)`, `function b got an unexpected keyword argument "x"`}, 325 326 // c(a, b:42) 327 {`c()`, `function c missing 1 argument (a)`}, 328 {`c(1)`, `(1, 42)`}, 329 {`c(1, 2)`, `(1, 2)`}, 330 {`c(1, 2, 3)`, `function c accepts at most 2 positional arguments (3 given)`}, 331 {`c(1, b:2)`, `(1, 2)`}, 332 {`c(1, a:2)`, `function c got multiple values for parameter "a"`}, 333 {`c(a:1, b:2)`, `(1, 2)`}, 334 {`c(b:1, a:2)`, `(2, 1)`}, 335 336 // d(*args) 337 {`d()`, `()`}, 338 {`d(1)`, `(1,)`}, 339 {`d(1, 2)`, `(1, 2)`}, 340 {`d(1, 2, k:3)`, `function d got an unexpected keyword argument "k"`}, 341 {`d(args:[])`, `function d got an unexpected keyword argument "args"`}, 342 343 // e(**kwargs) 344 {`e()`, `%{}`}, 345 {`e(1)`, `function e accepts 0 positional arguments (1 given)`}, 346 {`e(k:1)`, `%{"k": 1}`}, 347 {`e(kwargs:%{})`, `%{"kwargs": %{}}`}, 348 349 // f(a, b:42, *args, **kwargs) 350 {`f()`, `function f missing 1 argument (a)`}, 351 {`f(0)`, `(0, 42, (), %{})`}, 352 {`f(0)`, `(0, 42, (), %{})`}, 353 {`f(0, 1)`, `(0, 1, (), %{})`}, 354 {`f(0, 1, 2)`, `(0, 1, (2,), %{})`}, 355 {`f(0, 1, 2, 3)`, `(0, 1, (2, 3), %{})`}, 356 {`f(a:0)`, `(0, 42, (), %{})`}, 357 {`f(0, b:1)`, `(0, 1, (), %{})`}, 358 {`f(0, a:1)`, `function f got multiple values for parameter "a"`}, 359 {`f(0, b:1, c:2)`, `(0, 1, (), %{"c": 2})`}, 360 {`f(0, 1, x:2, *[3, 4], y:5, **dict(z:6))`, // github.com/google/skylark/issues/135 361 `(0, 1, (3, 4), %{"x": 2, "y": 5, "z": 6})`}, 362 363 // g(a, b:42, *args, c:123, **kwargs) 364 {`g()`, `function g missing 1 argument (a)`}, 365 {`g(0)`, `(0, 42, (), 123, %{})`}, 366 {`g(0, 1)`, `(0, 1, (), 123, %{})`}, 367 {`g(0, 1, 2)`, `(0, 1, (2,), 123, %{})`}, 368 {`g(0, 1, 2, 3)`, `(0, 1, (2, 3), 123, %{})`}, 369 {`g(a:0)`, `(0, 42, (), 123, %{})`}, 370 {`g(0, b:1)`, `(0, 1, (), 123, %{})`}, 371 {`g(0, a:1)`, `function g got multiple values for parameter "a"`}, 372 {`g(0, b:1, c:2, d:3)`, `(0, 1, (), 2, %{"d": 3})`}, 373 {`g(0, 1, x:2, *[3, 4], y:5, **dict(z:6))`, 374 `(0, 1, (3, 4), 123, %{"x": 2, "y": 5, "z": 6})`}, 375 376 // h(a, b:42, *, c:123, **kwargs) 377 {`h()`, `function h missing 1 argument (a)`}, 378 {`h(0)`, `(0, 42, 123, %{})`}, 379 {`h(0, 1)`, `(0, 1, 123, %{})`}, 380 {`h(0, 1, 2)`, `function h accepts at most 2 positional arguments (3 given)`}, 381 {`h(a:0)`, `(0, 42, 123, %{})`}, 382 {`h(0, b:1)`, `(0, 1, 123, %{})`}, 383 {`h(0, a:1)`, `function h got multiple values for parameter "a"`}, 384 {`h(0, b:1, c:2)`, `(0, 1, 2, %{})`}, 385 {`h(0, b:1, d:2)`, `(0, 1, 123, %{"d": 2})`}, 386 {`h(0, b:1, c:2, d:3)`, `(0, 1, 2, %{"d": 3})`}, 387 {`h(0, b:1, c:2, d:3)`, `(0, 1, 2, %{"d": 3})`}, 388 389 // i(a, b:42, *, c, d:123, e, **kwargs) 390 {`i()`, `function i missing 3 arguments (a, c, e)`}, 391 {`i(0)`, `function i missing 2 arguments (c, e)`}, 392 {`i(0, 1)`, `function i missing 2 arguments (c, e)`}, 393 {`i(0, 1, 2)`, `function i accepts at most 2 positional arguments (3 given)`}, 394 {`i(0, 1, e:2)`, `function i missing 1 argument (c)`}, 395 {`i(0, 1, 2, 3)`, `function i accepts at most 2 positional arguments (4 given)`}, 396 {`i(a:0)`, `function i missing 2 arguments (c, e)`}, 397 {`i(0, b:1)`, `function i missing 2 arguments (c, e)`}, 398 {`i(0, a:1)`, `function i got multiple values for parameter "a"`}, 399 {`i(0, b:1, c:2)`, `function i missing 1 argument (e)`}, 400 {`i(0, b:1, d:2)`, `function i missing 2 arguments (c, e)`}, 401 {`i(0, b:1, c:2, d:3)`, `function i missing 1 argument (e)`}, 402 {`i(0, b:1, c:2, d:3, e:4)`, `(0, 1, 2, 3, 4, %{})`}, 403 {`i(0, 1, b:1, c:2, d:3, e:4)`, `function i got multiple values for parameter "b"`}, 404 405 // j(a, b:42, *args, c, d:123, e, **kwargs) 406 {`j()`, `function j missing 3 arguments (a, c, e)`}, 407 {`j(0)`, `function j missing 2 arguments (c, e)`}, 408 {`j(0, 1)`, `function j missing 2 arguments (c, e)`}, 409 {`j(0, 1, 2)`, `function j missing 2 arguments (c, e)`}, 410 {`j(0, 1, e:2)`, `function j missing 1 argument (c)`}, 411 {`j(0, 1, 2, 3)`, `function j missing 2 arguments (c, e)`}, 412 {`j(a:0)`, `function j missing 2 arguments (c, e)`}, 413 {`j(0, b:1)`, `function j missing 2 arguments (c, e)`}, 414 {`j(0, a:1)`, `function j got multiple values for parameter "a"`}, 415 {`j(0, b:1, c:2)`, `function j missing 1 argument (e)`}, 416 {`j(0, b:1, d:2)`, `function j missing 2 arguments (c, e)`}, 417 {`j(0, b:1, c:2, d:3)`, `function j missing 1 argument (e)`}, 418 {`j(0, b:1, c:2, d:3, e:4)`, `(0, 1, (), 2, 3, 4, %{})`}, 419 {`j(0, 1, b:1, c:2, d:3, e:4)`, `function j got multiple values for parameter "b"`}, 420 {`j(0, 1, 2, c:3, e:4)`, `(0, 1, (2,), 3, 123, 4, %{})`}, 421 } { 422 var got string 423 if v, err := exprcore.Eval(thread, "<expr>", test.src, globals); err != nil { 424 got = err.Error() 425 } else { 426 got = v.String() 427 } 428 if got != test.want { 429 t.Errorf("eval %s = %s, want %s", test.src, got, test.want) 430 } 431 } 432 } 433 434 // TestPrint ensures that the exprcore print function calls 435 // Thread.Print, if provided. 436 func TestPrint(t *testing.T) { 437 const src = ` 438 print("hello") 439 def f() { print("hello", "world", sep:", ") } 440 f() 441 ` 442 buf := new(bytes.Buffer) 443 print := func(thread *exprcore.Thread, msg string) { 444 caller := thread.CallFrame(1) 445 fmt.Fprintf(buf, "%s: %s: %s\n", caller.Pos, caller.Name, msg) 446 } 447 thread := &exprcore.Thread{Print: print} 448 if _, err := exprcore.ExecFile(thread, "foo.star", src, nil); err != nil { 449 t.Fatal(err) 450 } 451 want := "foo.star:2:6: <toplevel>: hello\n" + 452 "foo.star:3:16: f: hello, world\n" 453 if got := buf.String(); got != want { 454 t.Errorf("output was <<%s>>, want <<%s>>", got, want) 455 } 456 } 457 458 func reportEvalError(tb testing.TB, err error) { 459 if err, ok := err.(*exprcore.EvalError); ok { 460 tb.Fatal(err.Backtrace()) 461 } 462 tb.Fatal(err) 463 } 464 465 // TestInt exercises the Int.Int64 and Int.Uint64 methods. 466 // If we can move their logic into math/big, delete this test. 467 func TestInt(t *testing.T) { 468 one := exprcore.MakeInt(1) 469 470 for _, test := range []struct { 471 i exprcore.Int 472 wantInt64 string 473 wantUint64 string 474 }{ 475 {exprcore.MakeInt64(math.MinInt64).Sub(one), "error", "error"}, 476 {exprcore.MakeInt64(math.MinInt64), "-9223372036854775808", "error"}, 477 {exprcore.MakeInt64(-1), "-1", "error"}, 478 {exprcore.MakeInt64(0), "0", "0"}, 479 {exprcore.MakeInt64(1), "1", "1"}, 480 {exprcore.MakeInt64(math.MaxInt64), "9223372036854775807", "9223372036854775807"}, 481 {exprcore.MakeUint64(math.MaxUint64), "error", "18446744073709551615"}, 482 {exprcore.MakeUint64(math.MaxUint64).Add(one), "error", "error"}, 483 } { 484 gotInt64, gotUint64 := "error", "error" 485 if i, ok := test.i.Int64(); ok { 486 gotInt64 = fmt.Sprint(i) 487 } 488 if u, ok := test.i.Uint64(); ok { 489 gotUint64 = fmt.Sprint(u) 490 } 491 if gotInt64 != test.wantInt64 { 492 t.Errorf("(%s).Int64() = %s, want %s", test.i, gotInt64, test.wantInt64) 493 } 494 if gotUint64 != test.wantUint64 { 495 t.Errorf("(%s).Uint64() = %s, want %s", test.i, gotUint64, test.wantUint64) 496 } 497 } 498 } 499 500 func backtrace(t *testing.T, err error) string { 501 t.Helper() 502 switch err := err.(type) { 503 case *exprcore.EvalError: 504 return err.Backtrace() 505 case nil: 506 t.Fatalf("ExecFile succeeded unexpectedly") 507 default: 508 t.Fatalf("ExecFile failed with %v, wanted *EvalError", err) 509 } 510 panic("unreachable") 511 } 512 513 func TestBacktrace(t *testing.T) { 514 // This test ensures continuity of the stack of active exprcore 515 // functions, including propagation through built-ins such as 'min'. 516 const src = ` 517 def f(x) { return 1//x } 518 def g(x) { f(x) } 519 def h() { return min([1, 2, 0], key:g) } 520 def i() { return h() } 521 i() 522 ` 523 thread := new(exprcore.Thread) 524 _, err := exprcore.ExecFile(thread, "crash.star", src, nil) 525 // Compiled code currently has no column information. 526 const want = `Traceback (most recent call last): 527 crash.star:6:2: in <toplevel> 528 crash.star:5:19: in i 529 crash.star:4:21: in h 530 <builtin>: in min 531 crash.star:3:13: in g 532 crash.star:2:20: in f 533 Error: floored division by zero` 534 if got := backtrace(t, err); got != want { 535 t.Errorf("error was %s, want %s", got, want) 536 } 537 538 // Additionally, ensure that errors originating in 539 // exprcore and/or Go each have an accurate frame. 540 // 541 // This program fails in exprcore (f) if x==0, 542 // or in Go (string.join) if x is non-zero. 543 const src2 = ` 544 def f() { ''.join([1//i]) } 545 f() 546 ` 547 for i, want := range []string{ 548 0: `Traceback (most recent call last): 549 crash.star:3:2: in <toplevel> 550 crash.star:2:21: in f 551 Error: floored division by zero`, 552 1: `Traceback (most recent call last): 553 crash.star:3:2: in <toplevel> 554 crash.star:2:18: in f 555 <builtin>: in join 556 Error: join: in list, want string, got int`, 557 } { 558 globals := exprcore.StringDict{"i": exprcore.MakeInt(i)} 559 _, err := exprcore.ExecFile(thread, "crash.star", src2, globals) 560 if got := backtrace(t, err); got != want { 561 t.Errorf("error was %s, want %s", got, want) 562 } 563 } 564 } 565 566 func TestLoadBacktrace(t *testing.T) { 567 // This test ensures that load() does NOT preserve stack traces, 568 // but that API callers can get them with Unwrap(). 569 // For discussion, see: 570 // https://github.com/google/exprcore-go/pull/244 571 const src = ` 572 load('crash.star', 'x') 573 ` 574 const loadedSrc = ` 575 def f(x) { 576 return 1 // x 577 } 578 f(0) 579 ` 580 thread := new(exprcore.Thread) 581 thread.Load = func(t *exprcore.Thread, module string) (exprcore.StringDict, error) { 582 return exprcore.ExecFile(new(exprcore.Thread), module, loadedSrc, nil) 583 } 584 _, err := exprcore.ExecFile(thread, "root.star", src, nil) 585 586 const want = `Traceback (most recent call last): 587 root.star:2:1: in <toplevel> 588 Error: cannot load crash.star: floored division by zero` 589 if got := backtrace(t, err); got != want { 590 t.Errorf("error was <<%s>>, want <<%s>>", got, want) 591 } 592 593 unwrapEvalError := func(err error) *exprcore.EvalError { 594 var result *exprcore.EvalError 595 for { 596 if evalErr, ok := err.(*exprcore.EvalError); ok { 597 result = evalErr 598 } 599 600 // TODO: use errors.Unwrap when go >=1.13 is everywhere. 601 wrapper, isWrapper := err.(Wrapper) 602 if !isWrapper { 603 break 604 } 605 err = wrapper.Unwrap() 606 } 607 return result 608 } 609 610 unwrappedErr := unwrapEvalError(err) 611 const wantUnwrapped = `Traceback (most recent call last): 612 crash.star:5:2: in <toplevel> 613 crash.star:3:12: in f 614 Error: floored division by zero` 615 if got := backtrace(t, unwrappedErr); got != wantUnwrapped { 616 t.Errorf("error was %s, want %s", got, wantUnwrapped) 617 } 618 619 } 620 621 // TestRepeatedExec parses and resolves a file syntax tree once then 622 // executes it repeatedly with different values of its predeclared variables. 623 func TestRepeatedExec(t *testing.T) { 624 predeclared := exprcore.StringDict{"x": exprcore.None} 625 _, prog, err := exprcore.SourceProgram("repeat.star", "y = 2 * x", predeclared.Has) 626 if err != nil { 627 t.Fatal(err) 628 } 629 630 for _, test := range []struct { 631 x, want exprcore.Value 632 }{ 633 {x: exprcore.MakeInt(42), want: exprcore.MakeInt(84)}, 634 {x: exprcore.String("mur"), want: exprcore.String("murmur")}, 635 {x: exprcore.Tuple{exprcore.None}, want: exprcore.Tuple{exprcore.None, exprcore.None}}, 636 } { 637 predeclared["x"] = test.x // update the values in dictionary 638 thread := new(exprcore.Thread) 639 if globals, err := prog.Init(thread, predeclared); err != nil { 640 t.Errorf("x=%v: %v", test.x, err) // exec error 641 } else if eq, err := exprcore.Equal(globals["y"], test.want); err != nil { 642 t.Errorf("x=%v: %v", test.x, err) // comparison error 643 } else if !eq { 644 t.Errorf("x=%v: got y=%v, want %v", test.x, globals["y"], test.want) 645 } 646 } 647 } 648 649 // TestEmptyFilePosition ensures that even Programs 650 // from empty files have a valid position. 651 func TestEmptyPosition(t *testing.T) { 652 var predeclared exprcore.StringDict 653 for _, content := range []string{"", "empty = False"} { 654 _, prog, err := exprcore.SourceProgram("hello.star", content, predeclared.Has) 655 if err != nil { 656 t.Fatal(err) 657 } 658 if got, want := prog.Filename(), "hello.star"; got != want { 659 t.Errorf("Program.Filename() = %q, want %q", got, want) 660 } 661 } 662 } 663 664 // TestUnpackUserDefined tests that user-defined 665 // implementations of exprcore.Value may be unpacked. 666 func TestUnpackUserDefined(t *testing.T) { 667 // success 668 want := new(hasfields) 669 var x *hasfields 670 if err := exprcore.UnpackArgs("unpack", exprcore.Tuple{want}, nil, "x", &x); err != nil { 671 t.Errorf("UnpackArgs failed: %v", err) 672 } 673 if x != want { 674 t.Errorf("for x, got %v, want %v", x, want) 675 } 676 677 // failure 678 err := exprcore.UnpackArgs("unpack", exprcore.Tuple{exprcore.MakeInt(42)}, nil, "x", &x) 679 if want := "unpack: for parameter x: got int, want hasfields"; fmt.Sprint(err) != want { 680 t.Errorf("unpack args error = %q, want %q", err, want) 681 } 682 } 683 684 type optionalStringUnpacker struct { 685 str string 686 isSet bool 687 } 688 689 func (o *optionalStringUnpacker) Unpack(v exprcore.Value) error { 690 s, ok := exprcore.AsString(v) 691 if !ok { 692 return fmt.Errorf("got %s, want string", v.Type()) 693 } 694 o.str = s 695 o.isSet = ok 696 return nil 697 } 698 699 func TestUnpackCustomUnpacker(t *testing.T) { 700 a := optionalStringUnpacker{} 701 wantA := optionalStringUnpacker{str: "a", isSet: true} 702 b := optionalStringUnpacker{str: "b"} 703 wantB := optionalStringUnpacker{str: "b"} 704 705 // Success 706 if err := exprcore.UnpackArgs("unpack", exprcore.Tuple{exprcore.String("a")}, nil, "a?", &a, "b?", &b); err != nil { 707 t.Errorf("UnpackArgs failed: %v", err) 708 } 709 if a != wantA { 710 t.Errorf("for a, got %v, want %v", a, wantA) 711 } 712 if b != wantB { 713 t.Errorf("for b, got %v, want %v", b, wantB) 714 } 715 716 // failure 717 err := exprcore.UnpackArgs("unpack", exprcore.Tuple{exprcore.MakeInt(42)}, nil, "a", &a) 718 if want := "unpack: for parameter a: got int, want string"; fmt.Sprint(err) != want { 719 t.Errorf("unpack args error = %q, want %q", err, want) 720 } 721 } 722 723 func TestDocstring(t *testing.T) { 724 globals, _ := exprcore.ExecFile(&exprcore.Thread{}, "doc.star", ` 725 def somefunc() { 726 "somefunc doc" 727 return 0 728 } 729 `, nil) 730 731 if globals["somefunc"].(*exprcore.Function).Doc() != "somefunc doc" { 732 t.Fatal("docstring not found") 733 } 734 } 735 736 func TestFrameLocals(t *testing.T) { 737 // trace prints a nice stack trace including argument 738 // values of calls to exprcore functions. 739 trace := func(thread *exprcore.Thread) string { 740 buf := new(bytes.Buffer) 741 for i := 0; i < thread.CallStackDepth(); i++ { 742 fr := thread.DebugFrame(i) 743 fmt.Fprintf(buf, "%s(", fr.Callable().Name()) 744 if fn, ok := fr.Callable().(*exprcore.Function); ok { 745 for i := 0; i < fn.NumParams(); i++ { 746 if i > 0 { 747 buf.WriteString(", ") 748 } 749 name, _ := fn.Param(i) 750 fmt.Fprintf(buf, "%s=%s", name, fr.Local(i)) 751 } 752 } else { 753 buf.WriteString("...") // a built-in function 754 } 755 buf.WriteString(")\n") 756 } 757 return buf.String() 758 } 759 760 var got string 761 builtin := func(thread *exprcore.Thread, _ *exprcore.Builtin, _ exprcore.Tuple, _ []exprcore.Tuple) (exprcore.Value, error) { 762 got = trace(thread) 763 return exprcore.None, nil 764 } 765 predeclared := exprcore.StringDict{ 766 "builtin": exprcore.NewBuiltin("builtin", builtin), 767 } 768 _, err := exprcore.ExecFile(&exprcore.Thread{}, "foo.star", ` 769 def f(x, y) { builtin() } 770 def g(z) { f(z, z*z) } 771 g(7) 772 `, predeclared) 773 if err != nil { 774 t.Errorf("ExecFile failed: %v", err) 775 } 776 777 var want = ` 778 builtin(...) 779 f(x=7, y=49) 780 g(z=7) 781 <toplevel>() 782 `[1:] 783 if got != want { 784 t.Errorf("got <<%s>>, want <<%s>>", got, want) 785 } 786 } 787 788 type badType string 789 790 func (b *badType) String() string { return "badType" } 791 func (b *badType) Type() string { return "badType:" + string(*b) } // panics if b==nil 792 func (b *badType) Truth() exprcore.Bool { return true } 793 func (b *badType) Hash() (uint32, error) { return 0, nil } 794 func (b *badType) Freeze() {} 795 796 var _ exprcore.Value = new(badType) 797 798 // TestUnpackErrorBadType verifies that the Unpack functions fail 799 // gracefully when a parameter's default value's Type method panics. 800 func TestUnpackErrorBadType(t *testing.T) { 801 for _, test := range []struct { 802 x *badType 803 want string 804 }{ 805 {new(badType), "got NoneType, want badType"}, // exprcore type name 806 {nil, "got NoneType, want *exprcore_test.badType"}, // Go type name 807 } { 808 err := exprcore.UnpackArgs("f", exprcore.Tuple{exprcore.None}, nil, "x", &test.x) 809 if err == nil { 810 t.Errorf("UnpackArgs succeeded unexpectedly") 811 continue 812 } 813 if !strings.Contains(err.Error(), test.want) { 814 t.Errorf("UnpackArgs error %q does not contain %q", err, test.want) 815 } 816 } 817 } 818 819 // Regression test for github.com/google/exprcore-go/issues/233. 820 func TestREPLChunk(t *testing.T) { 821 thread := new(exprcore.Thread) 822 globals := make(exprcore.StringDict) 823 exec := func(src string) { 824 f, err := syntax.Parse("<repl>", src, 0) 825 if err != nil { 826 t.Fatal(err) 827 } 828 if err := exprcore.ExecREPLChunk(f, thread, globals); err != nil { 829 t.Fatal(err) 830 } 831 } 832 833 exec("x = 0; y = 0") 834 if got, want := fmt.Sprintf("%v %v", globals["x"], globals["y"]), "0 0"; got != want { 835 t.Fatalf("chunk1: got %s, want %s", got, want) 836 } 837 838 exec("x += 1; y = y + 1") 839 if got, want := fmt.Sprintf("%v %v", globals["x"], globals["y"]), "1 1"; got != want { 840 t.Fatalf("chunk2: got %s, want %s", got, want) 841 } 842 }