go.starlark.net@v0.0.0-20231101134539-556fd59b42f6/starlark/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 starlark_test 6 7 import ( 8 "bytes" 9 "errors" 10 "fmt" 11 "math" 12 "os/exec" 13 "path/filepath" 14 "reflect" 15 "sort" 16 "strings" 17 "testing" 18 19 "go.starlark.net/internal/chunkedfile" 20 "go.starlark.net/lib/json" 21 starlarkmath "go.starlark.net/lib/math" 22 "go.starlark.net/lib/proto" 23 "go.starlark.net/lib/time" 24 "go.starlark.net/starlark" 25 "go.starlark.net/starlarkstruct" 26 "go.starlark.net/starlarktest" 27 "go.starlark.net/syntax" 28 "google.golang.org/protobuf/reflect/protoregistry" 29 30 _ "google.golang.org/protobuf/types/descriptorpb" // example descriptor needed for lib/proto tests 31 ) 32 33 // A test may enable non-standard options by containing (e.g.) "option:recursion". 34 func getOptions(src string) *syntax.FileOptions { 35 return &syntax.FileOptions{ 36 Set: option(src, "set"), 37 While: option(src, "while"), 38 TopLevelControl: option(src, "toplevelcontrol"), 39 GlobalReassign: option(src, "globalreassign"), 40 LoadBindsGlobally: option(src, "loadbindsglobally"), 41 Recursion: option(src, "recursion"), 42 } 43 } 44 45 func option(chunk, name string) bool { 46 return strings.Contains(chunk, "option:"+name) 47 } 48 49 func TestEvalExpr(t *testing.T) { 50 // This is mostly redundant with the new *.star tests. 51 // TODO(adonovan): move checks into *.star files and 52 // reduce this to a mere unit test of starlark.Eval. 53 thread := new(starlark.Thread) 54 for _, test := range []struct{ src, want string }{ 55 {`123`, `123`}, 56 {`-1`, `-1`}, 57 {`"a"+"b"`, `"ab"`}, 58 {`1+2`, `3`}, 59 60 // lists 61 {`[]`, `[]`}, 62 {`[1]`, `[1]`}, 63 {`[1,]`, `[1]`}, 64 {`[1, 2]`, `[1, 2]`}, 65 {`[2 * x for x in [1, 2, 3]]`, `[2, 4, 6]`}, 66 {`[2 * x for x in [1, 2, 3] if x > 1]`, `[4, 6]`}, 67 {`[(x, y) for x in [1, 2] for y in [3, 4]]`, 68 `[(1, 3), (1, 4), (2, 3), (2, 4)]`}, 69 {`[(x, y) for x in [1, 2] if x == 2 for y in [3, 4]]`, 70 `[(2, 3), (2, 4)]`}, 71 // tuples 72 {`()`, `()`}, 73 {`(1)`, `1`}, 74 {`(1,)`, `(1,)`}, 75 {`(1, 2)`, `(1, 2)`}, 76 {`(1, 2, 3, 4, 5)`, `(1, 2, 3, 4, 5)`}, 77 {`1, 2`, `(1, 2)`}, 78 // dicts 79 {`{}`, `{}`}, 80 {`{"a": 1}`, `{"a": 1}`}, 81 {`{"a": 1,}`, `{"a": 1}`}, 82 83 // conditional 84 {`1 if 3 > 2 else 0`, `1`}, 85 {`1 if "foo" else 0`, `1`}, 86 {`1 if "" else 0`, `0`}, 87 88 // indexing 89 {`["a", "b"][0]`, `"a"`}, 90 {`["a", "b"][1]`, `"b"`}, 91 {`("a", "b")[0]`, `"a"`}, 92 {`("a", "b")[1]`, `"b"`}, 93 {`"aΩb"[0]`, `"a"`}, 94 {`"aΩb"[1]`, `"\xce"`}, 95 {`"aΩb"[3]`, `"b"`}, 96 {`{"a": 1}["a"]`, `1`}, 97 {`{"a": 1}["b"]`, `key "b" not in dict`}, 98 {`{}[[]]`, `unhashable type: list`}, 99 {`{"a": 1}[[]]`, `unhashable type: list`}, 100 {`[x for x in range(3)]`, "[0, 1, 2]"}, 101 } { 102 var got string 103 if v, err := starlark.Eval(thread, "<expr>", test.src, nil); err != nil { 104 got = err.Error() 105 } else { 106 got = v.String() 107 } 108 if got != test.want { 109 t.Errorf("eval %s = %s, want %s", test.src, got, test.want) 110 } 111 } 112 } 113 114 func TestExecFile(t *testing.T) { 115 testdata := starlarktest.DataFile("starlark", ".") 116 thread := &starlark.Thread{Load: load} 117 starlarktest.SetReporter(thread, t) 118 proto.SetPool(thread, protoregistry.GlobalFiles) 119 for _, file := range []string{ 120 "testdata/assign.star", 121 "testdata/bool.star", 122 "testdata/builtins.star", 123 "testdata/bytes.star", 124 "testdata/control.star", 125 "testdata/dict.star", 126 "testdata/float.star", 127 "testdata/function.star", 128 "testdata/int.star", 129 "testdata/json.star", 130 "testdata/list.star", 131 "testdata/math.star", 132 "testdata/misc.star", 133 "testdata/proto.star", 134 "testdata/set.star", 135 "testdata/string.star", 136 "testdata/time.star", 137 "testdata/tuple.star", 138 "testdata/recursion.star", 139 "testdata/module.star", 140 "testdata/while.star", 141 } { 142 filename := filepath.Join(testdata, file) 143 for _, chunk := range chunkedfile.Read(filename, t) { 144 predeclared := starlark.StringDict{ 145 "hasfields": starlark.NewBuiltin("hasfields", newHasFields), 146 "fibonacci": fib{}, 147 "struct": starlark.NewBuiltin("struct", starlarkstruct.Make), 148 } 149 150 opts := getOptions(chunk.Source) 151 _, err := starlark.ExecFileOptions(opts, thread, filename, chunk.Source, predeclared) 152 switch err := err.(type) { 153 case *starlark.EvalError: 154 found := false 155 for i := range err.CallStack { 156 posn := err.CallStack.At(i).Pos 157 if posn.Filename() == filename { 158 chunk.GotError(int(posn.Line), err.Error()) 159 found = true 160 break 161 } 162 } 163 if !found { 164 t.Error(err.Backtrace()) 165 } 166 case nil: 167 // success 168 default: 169 t.Errorf("\n%s", err) 170 } 171 chunk.Done() 172 } 173 } 174 } 175 176 // A fib is an iterable value representing the infinite Fibonacci sequence. 177 type fib struct{} 178 179 func (t fib) Freeze() {} 180 func (t fib) String() string { return "fib" } 181 func (t fib) Type() string { return "fib" } 182 func (t fib) Truth() starlark.Bool { return true } 183 func (t fib) Hash() (uint32, error) { return 0, fmt.Errorf("fib is unhashable") } 184 func (t fib) Iterate() starlark.Iterator { return &fibIterator{0, 1} } 185 186 type fibIterator struct{ x, y int } 187 188 func (it *fibIterator) Next(p *starlark.Value) bool { 189 *p = starlark.MakeInt(it.x) 190 it.x, it.y = it.y, it.x+it.y 191 return true 192 } 193 func (it *fibIterator) Done() {} 194 195 // load implements the 'load' operation as used in the evaluator tests. 196 func load(thread *starlark.Thread, module string) (starlark.StringDict, error) { 197 if module == "assert.star" { 198 return starlarktest.LoadAssertModule() 199 } 200 if module == "json.star" { 201 return starlark.StringDict{"json": json.Module}, nil 202 } 203 if module == "time.star" { 204 return starlark.StringDict{"time": time.Module}, nil 205 } 206 if module == "math.star" { 207 return starlark.StringDict{"math": starlarkmath.Module}, nil 208 } 209 if module == "proto.star" { 210 return starlark.StringDict{"proto": proto.Module}, nil 211 } 212 213 // TODO(adonovan): test load() using this execution path. 214 filename := filepath.Join(filepath.Dir(thread.CallFrame(0).Pos.Filename()), module) 215 return starlark.ExecFile(thread, filename, nil, nil) 216 } 217 218 func newHasFields(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 219 if len(args)+len(kwargs) > 0 { 220 return nil, fmt.Errorf("%s: unexpected arguments", b.Name()) 221 } 222 return &hasfields{attrs: make(map[string]starlark.Value)}, nil 223 } 224 225 // hasfields is a test-only implementation of HasAttrs. 226 // It permits any field to be set. 227 // Clients will likely want to provide their own implementation, 228 // so we don't have any public implementation. 229 type hasfields struct { 230 attrs starlark.StringDict 231 frozen bool 232 } 233 234 var ( 235 _ starlark.HasAttrs = (*hasfields)(nil) 236 _ starlark.HasBinary = (*hasfields)(nil) 237 ) 238 239 func (hf *hasfields) String() string { return "hasfields" } 240 func (hf *hasfields) Type() string { return "hasfields" } 241 func (hf *hasfields) Truth() starlark.Bool { return true } 242 func (hf *hasfields) Hash() (uint32, error) { return 42, nil } 243 244 func (hf *hasfields) Freeze() { 245 if !hf.frozen { 246 hf.frozen = true 247 for _, v := range hf.attrs { 248 v.Freeze() 249 } 250 } 251 } 252 253 func (hf *hasfields) Attr(name string) (starlark.Value, error) { return hf.attrs[name], nil } 254 255 func (hf *hasfields) SetField(name string, val starlark.Value) error { 256 if hf.frozen { 257 return fmt.Errorf("cannot set field on a frozen hasfields") 258 } 259 if strings.HasPrefix(name, "no") { // for testing 260 return starlark.NoSuchAttrError(fmt.Sprintf("no .%s field", name)) 261 } 262 hf.attrs[name] = val 263 return nil 264 } 265 266 func (hf *hasfields) AttrNames() []string { 267 names := make([]string, 0, len(hf.attrs)) 268 for key := range hf.attrs { 269 names = append(names, key) 270 } 271 sort.Strings(names) 272 return names 273 } 274 275 func (hf *hasfields) Binary(op syntax.Token, y starlark.Value, side starlark.Side) (starlark.Value, error) { 276 // This method exists so we can exercise 'list += x' 277 // where x is not Iterable but defines list+x. 278 if op == syntax.PLUS { 279 if _, ok := y.(*starlark.List); ok { 280 return starlark.MakeInt(42), nil // list+hasfields is 42 281 } 282 } 283 return nil, nil 284 } 285 286 func TestParameterPassing(t *testing.T) { 287 const filename = "parameters.go" 288 const src = ` 289 def a(): 290 return 291 def b(a, b): 292 return a, b 293 def c(a, b=42): 294 return a, b 295 def d(*args): 296 return args 297 def e(**kwargs): 298 return kwargs 299 def f(a, b=42, *args, **kwargs): 300 return a, b, args, kwargs 301 def g(a, b=42, *args, c=123, **kwargs): 302 return a, b, args, c, kwargs 303 def h(a, b=42, *, c=123, **kwargs): 304 return a, b, c, kwargs 305 def i(a, b=42, *, c, d=123, e, **kwargs): 306 return a, b, c, d, e, kwargs 307 def j(a, b=42, *args, c, d=123, e, **kwargs): 308 return a, b, args, c, d, e, kwargs 309 ` 310 311 thread := new(starlark.Thread) 312 globals, err := starlark.ExecFile(thread, filename, src, nil) 313 if err != nil { 314 t.Fatal(err) 315 } 316 317 // All errors are dynamic; see resolver for static errors. 318 for _, test := range []struct{ src, want string }{ 319 // a() 320 {`a()`, `None`}, 321 {`a(1)`, `function a accepts no arguments (1 given)`}, 322 323 // b(a, b) 324 {`b()`, `function b missing 2 arguments (a, b)`}, 325 {`b(1)`, `function b missing 1 argument (b)`}, 326 {`b(a=1)`, `function b missing 1 argument (b)`}, 327 {`b(b=1)`, `function b missing 1 argument (a)`}, 328 {`b(1, 2)`, `(1, 2)`}, 329 {`b`, `<function b>`}, // asserts that b's parameter b was treated as a local variable 330 {`b(1, 2, 3)`, `function b accepts 2 positional arguments (3 given)`}, 331 {`b(1, b=2)`, `(1, 2)`}, 332 {`b(1, a=2)`, `function b got multiple values for parameter "a"`}, 333 {`b(1, x=2)`, `function b got an unexpected keyword argument "x"`}, 334 {`b(a=1, b=2)`, `(1, 2)`}, 335 {`b(b=1, a=2)`, `(2, 1)`}, 336 {`b(b=1, a=2, x=1)`, `function b got an unexpected keyword argument "x"`}, 337 {`b(x=1, b=1, a=2)`, `function b got an unexpected keyword argument "x"`}, 338 339 // c(a, b=42) 340 {`c()`, `function c missing 1 argument (a)`}, 341 {`c(1)`, `(1, 42)`}, 342 {`c(1, 2)`, `(1, 2)`}, 343 {`c(1, 2, 3)`, `function c accepts at most 2 positional arguments (3 given)`}, 344 {`c(1, b=2)`, `(1, 2)`}, 345 {`c(1, a=2)`, `function c got multiple values for parameter "a"`}, 346 {`c(a=1, b=2)`, `(1, 2)`}, 347 {`c(b=1, a=2)`, `(2, 1)`}, 348 349 // d(*args) 350 {`d()`, `()`}, 351 {`d(1)`, `(1,)`}, 352 {`d(1, 2)`, `(1, 2)`}, 353 {`d(1, 2, k=3)`, `function d got an unexpected keyword argument "k"`}, 354 {`d(args=[])`, `function d got an unexpected keyword argument "args"`}, 355 356 // e(**kwargs) 357 {`e()`, `{}`}, 358 {`e(1)`, `function e accepts 0 positional arguments (1 given)`}, 359 {`e(k=1)`, `{"k": 1}`}, 360 {`e(kwargs={})`, `{"kwargs": {}}`}, 361 362 // f(a, b=42, *args, **kwargs) 363 {`f()`, `function f missing 1 argument (a)`}, 364 {`f(0)`, `(0, 42, (), {})`}, 365 {`f(0)`, `(0, 42, (), {})`}, 366 {`f(0, 1)`, `(0, 1, (), {})`}, 367 {`f(0, 1, 2)`, `(0, 1, (2,), {})`}, 368 {`f(0, 1, 2, 3)`, `(0, 1, (2, 3), {})`}, 369 {`f(a=0)`, `(0, 42, (), {})`}, 370 {`f(0, b=1)`, `(0, 1, (), {})`}, 371 {`f(0, a=1)`, `function f got multiple values for parameter "a"`}, 372 {`f(0, b=1, c=2)`, `(0, 1, (), {"c": 2})`}, 373 374 // g(a, b=42, *args, c=123, **kwargs) 375 {`g()`, `function g missing 1 argument (a)`}, 376 {`g(0)`, `(0, 42, (), 123, {})`}, 377 {`g(0, 1)`, `(0, 1, (), 123, {})`}, 378 {`g(0, 1, 2)`, `(0, 1, (2,), 123, {})`}, 379 {`g(0, 1, 2, 3)`, `(0, 1, (2, 3), 123, {})`}, 380 {`g(a=0)`, `(0, 42, (), 123, {})`}, 381 {`g(0, b=1)`, `(0, 1, (), 123, {})`}, 382 {`g(0, a=1)`, `function g got multiple values for parameter "a"`}, 383 {`g(0, b=1, c=2, d=3)`, `(0, 1, (), 2, {"d": 3})`}, 384 385 // h(a, b=42, *, c=123, **kwargs) 386 {`h()`, `function h missing 1 argument (a)`}, 387 {`h(0)`, `(0, 42, 123, {})`}, 388 {`h(0, 1)`, `(0, 1, 123, {})`}, 389 {`h(0, 1, 2)`, `function h accepts at most 2 positional arguments (3 given)`}, 390 {`h(a=0)`, `(0, 42, 123, {})`}, 391 {`h(0, b=1)`, `(0, 1, 123, {})`}, 392 {`h(0, a=1)`, `function h got multiple values for parameter "a"`}, 393 {`h(0, b=1, c=2)`, `(0, 1, 2, {})`}, 394 {`h(0, b=1, d=2)`, `(0, 1, 123, {"d": 2})`}, 395 {`h(0, b=1, c=2, d=3)`, `(0, 1, 2, {"d": 3})`}, 396 397 // i(a, b=42, *, c, d=123, e, **kwargs) 398 {`i()`, `function i missing 3 arguments (a, c, e)`}, 399 {`i(0)`, `function i missing 2 arguments (c, e)`}, 400 {`i(0, 1)`, `function i missing 2 arguments (c, e)`}, 401 {`i(0, 1, 2)`, `function i accepts at most 2 positional arguments (3 given)`}, 402 {`i(0, 1, e=2)`, `function i missing 1 argument (c)`}, 403 {`i(0, 1, 2, 3)`, `function i accepts at most 2 positional arguments (4 given)`}, 404 {`i(a=0)`, `function i missing 2 arguments (c, e)`}, 405 {`i(0, b=1)`, `function i missing 2 arguments (c, e)`}, 406 {`i(0, a=1)`, `function i got multiple values for parameter "a"`}, 407 {`i(0, b=1, c=2)`, `function i missing 1 argument (e)`}, 408 {`i(0, b=1, d=2)`, `function i missing 2 arguments (c, e)`}, 409 {`i(0, b=1, c=2, d=3)`, `function i missing 1 argument (e)`}, 410 {`i(0, b=1, c=2, d=3, e=4)`, `(0, 1, 2, 3, 4, {})`}, 411 {`i(0, 1, b=1, c=2, d=3, e=4)`, `function i got multiple values for parameter "b"`}, 412 413 // j(a, b=42, *args, c, d=123, e, **kwargs) 414 {`j()`, `function j missing 3 arguments (a, c, e)`}, 415 {`j(0)`, `function j missing 2 arguments (c, e)`}, 416 {`j(0, 1)`, `function j missing 2 arguments (c, e)`}, 417 {`j(0, 1, 2)`, `function j missing 2 arguments (c, e)`}, 418 {`j(0, 1, e=2)`, `function j missing 1 argument (c)`}, 419 {`j(0, 1, 2, 3)`, `function j missing 2 arguments (c, e)`}, 420 {`j(a=0)`, `function j missing 2 arguments (c, e)`}, 421 {`j(0, b=1)`, `function j missing 2 arguments (c, e)`}, 422 {`j(0, a=1)`, `function j got multiple values for parameter "a"`}, 423 {`j(0, b=1, c=2)`, `function j missing 1 argument (e)`}, 424 {`j(0, b=1, d=2)`, `function j missing 2 arguments (c, e)`}, 425 {`j(0, b=1, c=2, d=3)`, `function j missing 1 argument (e)`}, 426 {`j(0, b=1, c=2, d=3, e=4)`, `(0, 1, (), 2, 3, 4, {})`}, 427 {`j(0, 1, b=1, c=2, d=3, e=4)`, `function j got multiple values for parameter "b"`}, 428 {`j(0, 1, 2, c=3, e=4)`, `(0, 1, (2,), 3, 123, 4, {})`}, 429 } { 430 var got string 431 if v, err := starlark.Eval(thread, "<expr>", test.src, globals); err != nil { 432 got = err.Error() 433 } else { 434 got = v.String() 435 } 436 if got != test.want { 437 t.Errorf("eval %s = %s, want %s", test.src, got, test.want) 438 } 439 } 440 } 441 442 // TestPrint ensures that the Starlark print function calls 443 // Thread.Print, if provided. 444 func TestPrint(t *testing.T) { 445 const src = ` 446 print("hello") 447 def f(): print("hello", "world", sep=", ") 448 f() 449 ` 450 buf := new(bytes.Buffer) 451 print := func(thread *starlark.Thread, msg string) { 452 caller := thread.CallFrame(1) 453 fmt.Fprintf(buf, "%s: %s: %s\n", caller.Pos, caller.Name, msg) 454 } 455 thread := &starlark.Thread{Print: print} 456 if _, err := starlark.ExecFile(thread, "foo.star", src, nil); err != nil { 457 t.Fatal(err) 458 } 459 want := "foo.star:2:6: <toplevel>: hello\n" + 460 "foo.star:3:15: f: hello, world\n" 461 if got := buf.String(); got != want { 462 t.Errorf("output was %s, want %s", got, want) 463 } 464 } 465 466 func reportEvalError(tb testing.TB, err error) { 467 if err, ok := err.(*starlark.EvalError); ok { 468 tb.Fatal(err.Backtrace()) 469 } 470 tb.Fatal(err) 471 } 472 473 // TestInt exercises the Int.Int64 and Int.Uint64 methods. 474 // If we can move their logic into math/big, delete this test. 475 func TestInt(t *testing.T) { 476 one := starlark.MakeInt(1) 477 478 for _, test := range []struct { 479 i starlark.Int 480 wantInt64 string 481 wantUint64 string 482 }{ 483 {starlark.MakeInt64(math.MinInt64).Sub(one), "error", "error"}, 484 {starlark.MakeInt64(math.MinInt64), "-9223372036854775808", "error"}, 485 {starlark.MakeInt64(-1), "-1", "error"}, 486 {starlark.MakeInt64(0), "0", "0"}, 487 {starlark.MakeInt64(1), "1", "1"}, 488 {starlark.MakeInt64(math.MaxInt64), "9223372036854775807", "9223372036854775807"}, 489 {starlark.MakeUint64(math.MaxUint64), "error", "18446744073709551615"}, 490 {starlark.MakeUint64(math.MaxUint64).Add(one), "error", "error"}, 491 } { 492 gotInt64, gotUint64 := "error", "error" 493 if i, ok := test.i.Int64(); ok { 494 gotInt64 = fmt.Sprint(i) 495 } 496 if u, ok := test.i.Uint64(); ok { 497 gotUint64 = fmt.Sprint(u) 498 } 499 if gotInt64 != test.wantInt64 { 500 t.Errorf("(%s).Int64() = %s, want %s", test.i, gotInt64, test.wantInt64) 501 } 502 if gotUint64 != test.wantUint64 { 503 t.Errorf("(%s).Uint64() = %s, want %s", test.i, gotUint64, test.wantUint64) 504 } 505 } 506 } 507 508 func backtrace(t *testing.T, err error) string { 509 switch err := err.(type) { 510 case *starlark.EvalError: 511 return err.Backtrace() 512 case nil: 513 t.Fatalf("ExecFile succeeded unexpectedly") 514 default: 515 t.Fatalf("ExecFile failed with %v, wanted *EvalError", err) 516 } 517 panic("unreachable") 518 } 519 520 func TestBacktrace(t *testing.T) { 521 // This test ensures continuity of the stack of active Starlark 522 // functions, including propagation through built-ins such as 'min'. 523 const src = ` 524 def f(x): return 1//x 525 def g(x): return f(x) 526 def h(): return min([1, 2, 0], key=g) 527 def i(): return h() 528 i() 529 ` 530 thread := new(starlark.Thread) 531 _, err := starlark.ExecFile(thread, "crash.star", src, nil) 532 const want = `Traceback (most recent call last): 533 crash.star:6:2: in <toplevel> 534 crash.star:5:18: in i 535 crash.star:4:20: in h 536 <builtin>: in min 537 crash.star:3:19: in g 538 crash.star:2:19: in f 539 Error: floored division by zero` 540 if got := backtrace(t, err); got != want { 541 t.Errorf("error was %s, want %s", got, want) 542 } 543 544 // Additionally, ensure that errors originating in 545 // Starlark and/or Go each have an accurate frame. 546 // The topmost frame, if built-in, is not shown, 547 // but the name of the built-in function is shown 548 // as "Error in fn: ...". 549 // 550 // This program fails in Starlark (f) if x==0, 551 // or in Go (string.join) if x is non-zero. 552 const src2 = ` 553 def f(): ''.join([1//i]) 554 f() 555 ` 556 for i, want := range []string{ 557 0: `Traceback (most recent call last): 558 crash.star:3:2: in <toplevel> 559 crash.star:2:20: in f 560 Error: floored division by zero`, 561 1: `Traceback (most recent call last): 562 crash.star:3:2: in <toplevel> 563 crash.star:2:17: in f 564 Error in join: join: in list, want string, got int`, 565 } { 566 globals := starlark.StringDict{"i": starlark.MakeInt(i)} 567 _, err := starlark.ExecFile(thread, "crash.star", src2, globals) 568 if got := backtrace(t, err); got != want { 569 t.Errorf("error was %s, want %s", got, want) 570 } 571 } 572 } 573 574 func TestLoadBacktrace(t *testing.T) { 575 // This test ensures that load() does NOT preserve stack traces, 576 // but that API callers can get them with Unwrap(). 577 // For discussion, see: 578 // https://github.com/google/starlark-go/pull/244 579 const src = ` 580 load('crash.star', 'x') 581 ` 582 const loadedSrc = ` 583 def f(x): 584 return 1 // x 585 586 f(0) 587 ` 588 thread := new(starlark.Thread) 589 thread.Load = func(t *starlark.Thread, module string) (starlark.StringDict, error) { 590 return starlark.ExecFile(new(starlark.Thread), module, loadedSrc, nil) 591 } 592 _, err := starlark.ExecFile(thread, "root.star", src, nil) 593 594 const want = `Traceback (most recent call last): 595 root.star:2:1: in <toplevel> 596 Error: cannot load crash.star: floored division by zero` 597 if got := backtrace(t, err); got != want { 598 t.Errorf("error was %s, want %s", got, want) 599 } 600 601 unwrapEvalError := func(err error) *starlark.EvalError { 602 var result *starlark.EvalError 603 for { 604 if evalErr, ok := err.(*starlark.EvalError); ok { 605 result = evalErr 606 } 607 608 err = errors.Unwrap(err) 609 if err == nil { 610 break 611 } 612 } 613 return result 614 } 615 616 unwrappedErr := unwrapEvalError(err) 617 const wantUnwrapped = `Traceback (most recent call last): 618 crash.star:5:2: in <toplevel> 619 crash.star:3:12: in f 620 Error: floored division by zero` 621 if got := backtrace(t, unwrappedErr); got != wantUnwrapped { 622 t.Errorf("error was %s, want %s", got, wantUnwrapped) 623 } 624 625 } 626 627 // TestRepeatedExec parses and resolves a file syntax tree once then 628 // executes it repeatedly with different values of its predeclared variables. 629 func TestRepeatedExec(t *testing.T) { 630 predeclared := starlark.StringDict{"x": starlark.None} 631 _, prog, err := starlark.SourceProgram("repeat.star", "y = 2 * x", predeclared.Has) 632 if err != nil { 633 t.Fatal(err) 634 } 635 636 for _, test := range []struct { 637 x, want starlark.Value 638 }{ 639 {x: starlark.MakeInt(42), want: starlark.MakeInt(84)}, 640 {x: starlark.String("mur"), want: starlark.String("murmur")}, 641 {x: starlark.Tuple{starlark.None}, want: starlark.Tuple{starlark.None, starlark.None}}, 642 } { 643 predeclared["x"] = test.x // update the values in dictionary 644 thread := new(starlark.Thread) 645 if globals, err := prog.Init(thread, predeclared); err != nil { 646 t.Errorf("x=%v: %v", test.x, err) // exec error 647 } else if eq, err := starlark.Equal(globals["y"], test.want); err != nil { 648 t.Errorf("x=%v: %v", test.x, err) // comparison error 649 } else if !eq { 650 t.Errorf("x=%v: got y=%v, want %v", test.x, globals["y"], test.want) 651 } 652 } 653 } 654 655 // TestEmptyFilePosition ensures that even Programs 656 // from empty files have a valid position. 657 func TestEmptyPosition(t *testing.T) { 658 var predeclared starlark.StringDict 659 for _, content := range []string{"", "empty = False"} { 660 _, prog, err := starlark.SourceProgram("hello.star", content, predeclared.Has) 661 if err != nil { 662 t.Fatal(err) 663 } 664 if got, want := prog.Filename(), "hello.star"; got != want { 665 t.Errorf("Program.Filename() = %q, want %q", got, want) 666 } 667 } 668 } 669 670 // TestUnpackUserDefined tests that user-defined 671 // implementations of starlark.Value may be unpacked. 672 func TestUnpackUserDefined(t *testing.T) { 673 // success 674 want := new(hasfields) 675 var x *hasfields 676 if err := starlark.UnpackArgs("unpack", starlark.Tuple{want}, nil, "x", &x); err != nil { 677 t.Errorf("UnpackArgs failed: %v", err) 678 } 679 if x != want { 680 t.Errorf("for x, got %v, want %v", x, want) 681 } 682 683 // failure 684 err := starlark.UnpackArgs("unpack", starlark.Tuple{starlark.MakeInt(42)}, nil, "x", &x) 685 if want := "unpack: for parameter x: got int, want hasfields"; fmt.Sprint(err) != want { 686 t.Errorf("unpack args error = %q, want %q", err, want) 687 } 688 } 689 690 type optionalStringUnpacker struct { 691 str string 692 isSet bool 693 } 694 695 func (o *optionalStringUnpacker) Unpack(v starlark.Value) error { 696 s, ok := starlark.AsString(v) 697 if !ok { 698 return fmt.Errorf("got %s, want string", v.Type()) 699 } 700 o.str = s 701 o.isSet = ok 702 return nil 703 } 704 705 func TestUnpackCustomUnpacker(t *testing.T) { 706 a := optionalStringUnpacker{} 707 wantA := optionalStringUnpacker{str: "a", isSet: true} 708 b := optionalStringUnpacker{str: "b"} 709 wantB := optionalStringUnpacker{str: "b"} 710 711 // Success 712 if err := starlark.UnpackArgs("unpack", starlark.Tuple{starlark.String("a")}, nil, "a?", &a, "b?", &b); err != nil { 713 t.Errorf("UnpackArgs failed: %v", err) 714 } 715 if a != wantA { 716 t.Errorf("for a, got %v, want %v", a, wantA) 717 } 718 if b != wantB { 719 t.Errorf("for b, got %v, want %v", b, wantB) 720 } 721 722 // failure 723 err := starlark.UnpackArgs("unpack", starlark.Tuple{starlark.MakeInt(42)}, nil, "a", &a) 724 if want := "unpack: for parameter a: got int, want string"; fmt.Sprint(err) != want { 725 t.Errorf("unpack args error = %q, want %q", err, want) 726 } 727 } 728 729 func TestUnpackNoneCoalescing(t *testing.T) { 730 a := optionalStringUnpacker{str: "a"} 731 wantA := optionalStringUnpacker{str: "a", isSet: false} 732 b := optionalStringUnpacker{str: "b"} 733 wantB := optionalStringUnpacker{str: "b", isSet: false} 734 735 // Success 736 args := starlark.Tuple{starlark.None} 737 kwargs := []starlark.Tuple{starlark.Tuple{starlark.String("b"), starlark.None}} 738 if err := starlark.UnpackArgs("unpack", args, kwargs, "a??", &a, "b??", &a); err != nil { 739 t.Errorf("UnpackArgs failed: %v", err) 740 } 741 if a != wantA { 742 t.Errorf("for a, got %v, want %v", a, wantA) 743 } 744 if b != wantB { 745 t.Errorf("for b, got %v, want %v", b, wantB) 746 } 747 748 // failure 749 err := starlark.UnpackArgs("unpack", starlark.Tuple{starlark.MakeInt(42)}, nil, "a??", &a) 750 if want := "unpack: for parameter a: got int, want string"; fmt.Sprint(err) != want { 751 t.Errorf("unpack args error = %q, want %q", err, want) 752 } 753 754 err = starlark.UnpackArgs("unpack", nil, []starlark.Tuple{ 755 starlark.Tuple{starlark.String("a"), starlark.None}, 756 starlark.Tuple{starlark.String("a"), starlark.None}, 757 }, "a??", &a) 758 if want := "unpack: got multiple values for keyword argument \"a\""; fmt.Sprint(err) != want { 759 t.Errorf("unpack args error = %q, want %q", err, want) 760 } 761 } 762 763 func TestUnpackRequiredAfterOptional(t *testing.T) { 764 // Assert 'c' is implicitly optional 765 var a, b, c string 766 args := starlark.Tuple{starlark.String("a")} 767 if err := starlark.UnpackArgs("unpack", args, nil, "a", &a, "b?", &b, "c", &c); err != nil { 768 t.Errorf("UnpackArgs failed: %v", err) 769 } 770 } 771 772 func TestAsInt(t *testing.T) { 773 for _, test := range []struct { 774 val starlark.Value 775 ptr interface{} 776 want string 777 }{ 778 {starlark.MakeInt(42), new(int32), "42"}, 779 {starlark.MakeInt(-1), new(int32), "-1"}, 780 // Use Lsh not 1<<40 as the latter exceeds int if GOARCH=386. 781 {starlark.MakeInt(1).Lsh(40), new(int32), "1099511627776 out of range (want value in signed 32-bit range)"}, 782 {starlark.MakeInt(-1).Lsh(40), new(int32), "-1099511627776 out of range (want value in signed 32-bit range)"}, 783 784 {starlark.MakeInt(42), new(uint16), "42"}, 785 {starlark.MakeInt(0xffff), new(uint16), "65535"}, 786 {starlark.MakeInt(0x10000), new(uint16), "65536 out of range (want value in unsigned 16-bit range)"}, 787 {starlark.MakeInt(-1), new(uint16), "-1 out of range (want value in unsigned 16-bit range)"}, 788 } { 789 var got string 790 if err := starlark.AsInt(test.val, test.ptr); err != nil { 791 got = err.Error() 792 } else { 793 got = fmt.Sprint(reflect.ValueOf(test.ptr).Elem().Interface()) 794 } 795 if got != test.want { 796 t.Errorf("AsInt(%s, %T): got %q, want %q", test.val, test.ptr, got, test.want) 797 } 798 } 799 } 800 801 func TestDocstring(t *testing.T) { 802 globals, _ := starlark.ExecFile(&starlark.Thread{}, "doc.star", ` 803 def somefunc(): 804 "somefunc doc" 805 return 0 806 `, nil) 807 808 if globals["somefunc"].(*starlark.Function).Doc() != "somefunc doc" { 809 t.Fatal("docstring not found") 810 } 811 } 812 813 func TestFrameLocals(t *testing.T) { 814 // trace prints a nice stack trace including argument 815 // values of calls to Starlark functions. 816 trace := func(thread *starlark.Thread) string { 817 buf := new(bytes.Buffer) 818 for i := 0; i < thread.CallStackDepth(); i++ { 819 fr := thread.DebugFrame(i) 820 fmt.Fprintf(buf, "%s(", fr.Callable().Name()) 821 if fn, ok := fr.Callable().(*starlark.Function); ok { 822 for i := 0; i < fn.NumParams(); i++ { 823 if i > 0 { 824 buf.WriteString(", ") 825 } 826 name, _ := fn.Param(i) 827 fmt.Fprintf(buf, "%s=%s", name, fr.Local(i)) 828 } 829 } else { 830 buf.WriteString("...") // a built-in function 831 } 832 buf.WriteString(")\n") 833 } 834 return buf.String() 835 } 836 837 var got string 838 builtin := func(thread *starlark.Thread, _ *starlark.Builtin, _ starlark.Tuple, _ []starlark.Tuple) (starlark.Value, error) { 839 got = trace(thread) 840 return starlark.None, nil 841 } 842 predeclared := starlark.StringDict{ 843 "builtin": starlark.NewBuiltin("builtin", builtin), 844 } 845 _, err := starlark.ExecFile(&starlark.Thread{}, "foo.star", ` 846 def f(x, y): builtin() 847 def g(z): f(z, z*z) 848 g(7) 849 `, predeclared) 850 if err != nil { 851 t.Errorf("ExecFile failed: %v", err) 852 } 853 854 var want = ` 855 builtin(...) 856 f(x=7, y=49) 857 g(z=7) 858 <toplevel>() 859 `[1:] 860 if got != want { 861 t.Errorf("got <<%s>>, want <<%s>>", got, want) 862 } 863 } 864 865 type badType string 866 867 func (b *badType) String() string { return "badType" } 868 func (b *badType) Type() string { return "badType:" + string(*b) } // panics if b==nil 869 func (b *badType) Truth() starlark.Bool { return true } 870 func (b *badType) Hash() (uint32, error) { return 0, nil } 871 func (b *badType) Freeze() {} 872 873 var _ starlark.Value = new(badType) 874 875 // TestUnpackErrorBadType verifies that the Unpack functions fail 876 // gracefully when a parameter's default value's Type method panics. 877 func TestUnpackErrorBadType(t *testing.T) { 878 for _, test := range []struct { 879 x *badType 880 want string 881 }{ 882 {new(badType), "got NoneType, want badType"}, // Starlark type name 883 {nil, "got NoneType, want *starlark_test.badType"}, // Go type name 884 } { 885 err := starlark.UnpackArgs("f", starlark.Tuple{starlark.None}, nil, "x", &test.x) 886 if err == nil { 887 t.Errorf("UnpackArgs succeeded unexpectedly") 888 continue 889 } 890 if !strings.Contains(err.Error(), test.want) { 891 t.Errorf("UnpackArgs error %q does not contain %q", err, test.want) 892 } 893 } 894 } 895 896 // Regression test for github.com/google/starlark-go/issues/233. 897 func TestREPLChunk(t *testing.T) { 898 thread := new(starlark.Thread) 899 globals := make(starlark.StringDict) 900 exec := func(src string) { 901 f, err := syntax.Parse("<repl>", src, 0) 902 if err != nil { 903 t.Fatal(err) 904 } 905 if err := starlark.ExecREPLChunk(f, thread, globals); err != nil { 906 t.Fatal(err) 907 } 908 } 909 910 exec("x = 0; y = 0") 911 if got, want := fmt.Sprintf("%v %v", globals["x"], globals["y"]), "0 0"; got != want { 912 t.Fatalf("chunk1: got %s, want %s", got, want) 913 } 914 915 exec("x += 1; y = y + 1") 916 if got, want := fmt.Sprintf("%v %v", globals["x"], globals["y"]), "1 1"; got != want { 917 t.Fatalf("chunk2: got %s, want %s", got, want) 918 } 919 } 920 921 func TestCancel(t *testing.T) { 922 // A thread cancelled before it begins executes no code. 923 { 924 thread := new(starlark.Thread) 925 thread.Cancel("nope") 926 _, err := starlark.ExecFile(thread, "precancel.star", `x = 1//0`, nil) 927 if fmt.Sprint(err) != "Starlark computation cancelled: nope" { 928 t.Errorf("execution returned error %q, want cancellation", err) 929 } 930 931 // cancellation is sticky 932 _, err = starlark.ExecFile(thread, "precancel.star", `x = 1//0`, nil) 933 if fmt.Sprint(err) != "Starlark computation cancelled: nope" { 934 t.Errorf("execution returned error %q, want cancellation", err) 935 } 936 } 937 // A thread cancelled during a built-in executes no more code. 938 { 939 thread := new(starlark.Thread) 940 predeclared := starlark.StringDict{ 941 "stopit": starlark.NewBuiltin("stopit", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 942 thread.Cancel(fmt.Sprint(args[0])) 943 return starlark.None, nil 944 }), 945 } 946 _, err := starlark.ExecFile(thread, "stopit.star", `msg = 'nope'; stopit(msg); x = 1//0`, predeclared) 947 if fmt.Sprint(err) != `Starlark computation cancelled: "nope"` { 948 t.Errorf("execution returned error %q, want cancellation", err) 949 } 950 } 951 } 952 953 func TestExecutionSteps(t *testing.T) { 954 // A Thread records the number of computation steps. 955 thread := new(starlark.Thread) 956 countSteps := func(n int) (uint64, error) { 957 predeclared := starlark.StringDict{"n": starlark.MakeInt(n)} 958 steps0 := thread.ExecutionSteps() 959 _, err := starlark.ExecFile(thread, "steps.star", `squares = [x*x for x in range(n)]`, predeclared) 960 return thread.ExecutionSteps() - steps0, err 961 } 962 steps100, err := countSteps(1000) 963 if err != nil { 964 t.Errorf("execution failed: %v", err) 965 } 966 steps10000, err := countSteps(100000) 967 if err != nil { 968 t.Errorf("execution failed: %v", err) 969 } 970 if ratio := float64(steps10000) / float64(steps100); ratio < 99 || ratio > 101 { 971 t.Errorf("computation steps did not increase linearly: f(100)=%d, f(10000)=%d, ratio=%g, want ~100", steps100, steps10000, ratio) 972 } 973 974 // Exceeding the step limit causes cancellation. 975 thread.SetMaxExecutionSteps(1000) 976 _, err = countSteps(1000) 977 if fmt.Sprint(err) != "Starlark computation cancelled: too many steps" { 978 t.Errorf("execution returned error %q, want cancellation", err) 979 } 980 981 thread.Steps = 0 982 thread.Uncancel() 983 _, err = countSteps(1) 984 if err != nil { 985 t.Errorf("execution returned error %q, want nil", err) 986 } 987 } 988 989 // TestDeps fails if the interpreter proper (not the REPL, etc) sprouts new external dependencies. 990 // We may expand the list of permitted dependencies, but should do so deliberately, not casually. 991 func TestDeps(t *testing.T) { 992 cmd := exec.Command("go", "list", "-deps") 993 out, err := cmd.Output() 994 if err != nil { 995 t.Skipf("'go list' failed: %s", err) 996 } 997 for _, pkg := range strings.Split(string(out), "\n") { 998 // Does pkg have form "domain.name/dir"? 999 slash := strings.IndexByte(pkg, '/') 1000 dot := strings.IndexByte(pkg, '.') 1001 if 0 < dot && dot < slash { 1002 if strings.HasPrefix(pkg, "go.starlark.net/") || 1003 strings.HasPrefix(pkg, "golang.org/x/sys/") { 1004 continue // permitted dependencies 1005 } 1006 t.Errorf("new interpreter dependency: %s", pkg) 1007 } 1008 } 1009 } 1010 1011 // TestPanicSafety ensures that a panic from an application-defined 1012 // built-in may traverse the interpreter safely; see issue #411. 1013 func TestPanicSafety(t *testing.T) { 1014 predeclared := starlark.StringDict{ 1015 "panic": starlark.NewBuiltin("panic", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 1016 panic(args[0]) 1017 }), 1018 "list": starlark.NewList([]starlark.Value{starlark.MakeInt(0)}), 1019 } 1020 1021 // This program is executed twice, using the same Thread, 1022 // and panics both times, with values 1 and 2, while 1023 // main is on the stack and a for-loop is active. 1024 // 1025 // It mutates list, a predeclared variable. 1026 // This operation would fail if the previous 1027 // for-loop failed to close its iterator during the panic. 1028 // 1029 // It also calls main a second time without recursion enabled. 1030 // This operation would fail if the previous 1031 // call failed to pop main from the stack during the panic. 1032 const src = ` 1033 list[0] += 1 1034 1035 def main(): 1036 for x in list: 1037 panic(x) 1038 1039 main() 1040 ` 1041 thread := new(starlark.Thread) 1042 for _, i := range []int{1, 2} { 1043 // Use a func to limit the scope of recover. 1044 func() { 1045 defer func() { 1046 if got := fmt.Sprint(recover()); got != fmt.Sprint(i) { 1047 t.Fatalf("recover: got %v, want %v", got, i) 1048 } 1049 }() 1050 v, err := starlark.ExecFile(thread, "panic.star", src, predeclared) 1051 if err != nil { 1052 t.Fatalf("ExecFile returned error %q, expected panic", err) 1053 } else { 1054 t.Fatalf("ExecFile returned %v, expected panic", v) 1055 } 1056 }() 1057 } 1058 }