diff options
Diffstat (limited to 'starlark/eval_test.go')
-rw-r--r-- | starlark/eval_test.go | 945 |
1 files changed, 945 insertions, 0 deletions
diff --git a/starlark/eval_test.go b/starlark/eval_test.go new file mode 100644 index 0000000..9752fe8 --- /dev/null +++ b/starlark/eval_test.go @@ -0,0 +1,945 @@ +// Copyright 2017 The Bazel Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package starlark_test + +import ( + "bytes" + "fmt" + "math" + "os/exec" + "path/filepath" + "reflect" + "sort" + "strings" + "testing" + + "go.starlark.net/internal/chunkedfile" + "go.starlark.net/resolve" + "go.starlark.net/starlark" + "go.starlark.net/starlarkjson" + "go.starlark.net/starlarkstruct" + "go.starlark.net/starlarktest" + "go.starlark.net/syntax" +) + +// A test may enable non-standard options by containing (e.g.) "option:recursion". +func setOptions(src string) { + resolve.AllowGlobalReassign = option(src, "globalreassign") + resolve.LoadBindsGlobally = option(src, "loadbindsglobally") + resolve.AllowRecursion = option(src, "recursion") + resolve.AllowSet = option(src, "set") +} + +func option(chunk, name string) bool { + return strings.Contains(chunk, "option:"+name) +} + +// Wrapper is the type of errors with an Unwrap method; see https://golang.org/pkg/errors. +type Wrapper interface { + Unwrap() error +} + +func TestEvalExpr(t *testing.T) { + // This is mostly redundant with the new *.star tests. + // TODO(adonovan): move checks into *.star files and + // reduce this to a mere unit test of starlark.Eval. + thread := new(starlark.Thread) + for _, test := range []struct{ src, want string }{ + {`123`, `123`}, + {`-1`, `-1`}, + {`"a"+"b"`, `"ab"`}, + {`1+2`, `3`}, + + // lists + {`[]`, `[]`}, + {`[1]`, `[1]`}, + {`[1,]`, `[1]`}, + {`[1, 2]`, `[1, 2]`}, + {`[2 * x for x in [1, 2, 3]]`, `[2, 4, 6]`}, + {`[2 * x for x in [1, 2, 3] if x > 1]`, `[4, 6]`}, + {`[(x, y) for x in [1, 2] for y in [3, 4]]`, + `[(1, 3), (1, 4), (2, 3), (2, 4)]`}, + {`[(x, y) for x in [1, 2] if x == 2 for y in [3, 4]]`, + `[(2, 3), (2, 4)]`}, + // tuples + {`()`, `()`}, + {`(1)`, `1`}, + {`(1,)`, `(1,)`}, + {`(1, 2)`, `(1, 2)`}, + {`(1, 2, 3, 4, 5)`, `(1, 2, 3, 4, 5)`}, + {`1, 2`, `(1, 2)`}, + // dicts + {`{}`, `{}`}, + {`{"a": 1}`, `{"a": 1}`}, + {`{"a": 1,}`, `{"a": 1}`}, + + // conditional + {`1 if 3 > 2 else 0`, `1`}, + {`1 if "foo" else 0`, `1`}, + {`1 if "" else 0`, `0`}, + + // indexing + {`["a", "b"][0]`, `"a"`}, + {`["a", "b"][1]`, `"b"`}, + {`("a", "b")[0]`, `"a"`}, + {`("a", "b")[1]`, `"b"`}, + {`"aΩb"[0]`, `"a"`}, + {`"aΩb"[1]`, `"\xce"`}, + {`"aΩb"[3]`, `"b"`}, + {`{"a": 1}["a"]`, `1`}, + {`{"a": 1}["b"]`, `key "b" not in dict`}, + {`{}[[]]`, `unhashable type: list`}, + {`{"a": 1}[[]]`, `unhashable type: list`}, + {`[x for x in range(3)]`, "[0, 1, 2]"}, + } { + var got string + if v, err := starlark.Eval(thread, "<expr>", test.src, nil); err != nil { + got = err.Error() + } else { + got = v.String() + } + if got != test.want { + t.Errorf("eval %s = %s, want %s", test.src, got, test.want) + } + } +} + +func TestExecFile(t *testing.T) { + defer setOptions("") + testdata := starlarktest.DataFile("starlark", ".") + thread := &starlark.Thread{Load: load} + starlarktest.SetReporter(thread, t) + for _, file := range []string{ + "testdata/assign.star", + "testdata/bool.star", + "testdata/builtins.star", + "testdata/bytes.star", + "testdata/control.star", + "testdata/dict.star", + "testdata/float.star", + "testdata/function.star", + "testdata/int.star", + "testdata/json.star", + "testdata/list.star", + "testdata/misc.star", + "testdata/set.star", + "testdata/string.star", + "testdata/tuple.star", + "testdata/recursion.star", + "testdata/module.star", + } { + filename := filepath.Join(testdata, file) + for _, chunk := range chunkedfile.Read(filename, t) { + predeclared := starlark.StringDict{ + "hasfields": starlark.NewBuiltin("hasfields", newHasFields), + "fibonacci": fib{}, + "struct": starlark.NewBuiltin("struct", starlarkstruct.Make), + } + + setOptions(chunk.Source) + resolve.AllowLambda = true // used extensively + + _, err := starlark.ExecFile(thread, filename, chunk.Source, predeclared) + switch err := err.(type) { + case *starlark.EvalError: + found := false + for i := range err.CallStack { + posn := err.CallStack.At(i).Pos + if posn.Filename() == filename { + chunk.GotError(int(posn.Line), err.Error()) + found = true + break + } + } + if !found { + t.Error(err.Backtrace()) + } + case nil: + // success + default: + t.Errorf("\n%s", err) + } + chunk.Done() + } + } +} + +// A fib is an iterable value representing the infinite Fibonacci sequence. +type fib struct{} + +func (t fib) Freeze() {} +func (t fib) String() string { return "fib" } +func (t fib) Type() string { return "fib" } +func (t fib) Truth() starlark.Bool { return true } +func (t fib) Hash() (uint32, error) { return 0, fmt.Errorf("fib is unhashable") } +func (t fib) Iterate() starlark.Iterator { return &fibIterator{0, 1} } + +type fibIterator struct{ x, y int } + +func (it *fibIterator) Next(p *starlark.Value) bool { + *p = starlark.MakeInt(it.x) + it.x, it.y = it.y, it.x+it.y + return true +} +func (it *fibIterator) Done() {} + +// load implements the 'load' operation as used in the evaluator tests. +func load(thread *starlark.Thread, module string) (starlark.StringDict, error) { + if module == "assert.star" { + return starlarktest.LoadAssertModule() + } + if module == "json.star" { + return starlark.StringDict{"json": starlarkjson.Module}, nil + } + + // TODO(adonovan): test load() using this execution path. + filename := filepath.Join(filepath.Dir(thread.CallFrame(0).Pos.Filename()), module) + return starlark.ExecFile(thread, filename, nil, nil) +} + +func newHasFields(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + if len(args)+len(kwargs) > 0 { + return nil, fmt.Errorf("%s: unexpected arguments", b.Name()) + } + return &hasfields{attrs: make(map[string]starlark.Value)}, nil +} + +// hasfields is a test-only implementation of HasAttrs. +// It permits any field to be set. +// Clients will likely want to provide their own implementation, +// so we don't have any public implementation. +type hasfields struct { + attrs starlark.StringDict + frozen bool +} + +var ( + _ starlark.HasAttrs = (*hasfields)(nil) + _ starlark.HasBinary = (*hasfields)(nil) +) + +func (hf *hasfields) String() string { return "hasfields" } +func (hf *hasfields) Type() string { return "hasfields" } +func (hf *hasfields) Truth() starlark.Bool { return true } +func (hf *hasfields) Hash() (uint32, error) { return 42, nil } + +func (hf *hasfields) Freeze() { + if !hf.frozen { + hf.frozen = true + for _, v := range hf.attrs { + v.Freeze() + } + } +} + +func (hf *hasfields) Attr(name string) (starlark.Value, error) { return hf.attrs[name], nil } + +func (hf *hasfields) SetField(name string, val starlark.Value) error { + if hf.frozen { + return fmt.Errorf("cannot set field on a frozen hasfields") + } + if strings.HasPrefix(name, "no") { // for testing + return starlark.NoSuchAttrError(fmt.Sprintf("no .%s field", name)) + } + hf.attrs[name] = val + return nil +} + +func (hf *hasfields) AttrNames() []string { + names := make([]string, 0, len(hf.attrs)) + for key := range hf.attrs { + names = append(names, key) + } + sort.Strings(names) + return names +} + +func (hf *hasfields) Binary(op syntax.Token, y starlark.Value, side starlark.Side) (starlark.Value, error) { + // This method exists so we can exercise 'list += x' + // where x is not Iterable but defines list+x. + if op == syntax.PLUS { + if _, ok := y.(*starlark.List); ok { + return starlark.MakeInt(42), nil // list+hasfields is 42 + } + } + return nil, nil +} + +func TestParameterPassing(t *testing.T) { + const filename = "parameters.go" + const src = ` +def a(): + return +def b(a, b): + return a, b +def c(a, b=42): + return a, b +def d(*args): + return args +def e(**kwargs): + return kwargs +def f(a, b=42, *args, **kwargs): + return a, b, args, kwargs +def g(a, b=42, *args, c=123, **kwargs): + return a, b, args, c, kwargs +def h(a, b=42, *, c=123, **kwargs): + return a, b, c, kwargs +def i(a, b=42, *, c, d=123, e, **kwargs): + return a, b, c, d, e, kwargs +def j(a, b=42, *args, c, d=123, e, **kwargs): + return a, b, args, c, d, e, kwargs +` + + thread := new(starlark.Thread) + globals, err := starlark.ExecFile(thread, filename, src, nil) + if err != nil { + t.Fatal(err) + } + + // All errors are dynamic; see resolver for static errors. + for _, test := range []struct{ src, want string }{ + // a() + {`a()`, `None`}, + {`a(1)`, `function a accepts no arguments (1 given)`}, + + // b(a, b) + {`b()`, `function b missing 2 arguments (a, b)`}, + {`b(1)`, `function b missing 1 argument (b)`}, + {`b(a=1)`, `function b missing 1 argument (b)`}, + {`b(b=1)`, `function b missing 1 argument (a)`}, + {`b(1, 2)`, `(1, 2)`}, + {`b`, `<function b>`}, // asserts that b's parameter b was treated as a local variable + {`b(1, 2, 3)`, `function b accepts 2 positional arguments (3 given)`}, + {`b(1, b=2)`, `(1, 2)`}, + {`b(1, a=2)`, `function b got multiple values for parameter "a"`}, + {`b(1, x=2)`, `function b got an unexpected keyword argument "x"`}, + {`b(a=1, b=2)`, `(1, 2)`}, + {`b(b=1, a=2)`, `(2, 1)`}, + {`b(b=1, a=2, x=1)`, `function b got an unexpected keyword argument "x"`}, + {`b(x=1, b=1, a=2)`, `function b got an unexpected keyword argument "x"`}, + + // c(a, b=42) + {`c()`, `function c missing 1 argument (a)`}, + {`c(1)`, `(1, 42)`}, + {`c(1, 2)`, `(1, 2)`}, + {`c(1, 2, 3)`, `function c accepts at most 2 positional arguments (3 given)`}, + {`c(1, b=2)`, `(1, 2)`}, + {`c(1, a=2)`, `function c got multiple values for parameter "a"`}, + {`c(a=1, b=2)`, `(1, 2)`}, + {`c(b=1, a=2)`, `(2, 1)`}, + + // d(*args) + {`d()`, `()`}, + {`d(1)`, `(1,)`}, + {`d(1, 2)`, `(1, 2)`}, + {`d(1, 2, k=3)`, `function d got an unexpected keyword argument "k"`}, + {`d(args=[])`, `function d got an unexpected keyword argument "args"`}, + + // e(**kwargs) + {`e()`, `{}`}, + {`e(1)`, `function e accepts 0 positional arguments (1 given)`}, + {`e(k=1)`, `{"k": 1}`}, + {`e(kwargs={})`, `{"kwargs": {}}`}, + + // f(a, b=42, *args, **kwargs) + {`f()`, `function f missing 1 argument (a)`}, + {`f(0)`, `(0, 42, (), {})`}, + {`f(0)`, `(0, 42, (), {})`}, + {`f(0, 1)`, `(0, 1, (), {})`}, + {`f(0, 1, 2)`, `(0, 1, (2,), {})`}, + {`f(0, 1, 2, 3)`, `(0, 1, (2, 3), {})`}, + {`f(a=0)`, `(0, 42, (), {})`}, + {`f(0, b=1)`, `(0, 1, (), {})`}, + {`f(0, a=1)`, `function f got multiple values for parameter "a"`}, + {`f(0, b=1, c=2)`, `(0, 1, (), {"c": 2})`}, + + // g(a, b=42, *args, c=123, **kwargs) + {`g()`, `function g missing 1 argument (a)`}, + {`g(0)`, `(0, 42, (), 123, {})`}, + {`g(0, 1)`, `(0, 1, (), 123, {})`}, + {`g(0, 1, 2)`, `(0, 1, (2,), 123, {})`}, + {`g(0, 1, 2, 3)`, `(0, 1, (2, 3), 123, {})`}, + {`g(a=0)`, `(0, 42, (), 123, {})`}, + {`g(0, b=1)`, `(0, 1, (), 123, {})`}, + {`g(0, a=1)`, `function g got multiple values for parameter "a"`}, + {`g(0, b=1, c=2, d=3)`, `(0, 1, (), 2, {"d": 3})`}, + + // h(a, b=42, *, c=123, **kwargs) + {`h()`, `function h missing 1 argument (a)`}, + {`h(0)`, `(0, 42, 123, {})`}, + {`h(0, 1)`, `(0, 1, 123, {})`}, + {`h(0, 1, 2)`, `function h accepts at most 2 positional arguments (3 given)`}, + {`h(a=0)`, `(0, 42, 123, {})`}, + {`h(0, b=1)`, `(0, 1, 123, {})`}, + {`h(0, a=1)`, `function h got multiple values for parameter "a"`}, + {`h(0, b=1, c=2)`, `(0, 1, 2, {})`}, + {`h(0, b=1, d=2)`, `(0, 1, 123, {"d": 2})`}, + {`h(0, b=1, c=2, d=3)`, `(0, 1, 2, {"d": 3})`}, + + // i(a, b=42, *, c, d=123, e, **kwargs) + {`i()`, `function i missing 3 arguments (a, c, e)`}, + {`i(0)`, `function i missing 2 arguments (c, e)`}, + {`i(0, 1)`, `function i missing 2 arguments (c, e)`}, + {`i(0, 1, 2)`, `function i accepts at most 2 positional arguments (3 given)`}, + {`i(0, 1, e=2)`, `function i missing 1 argument (c)`}, + {`i(0, 1, 2, 3)`, `function i accepts at most 2 positional arguments (4 given)`}, + {`i(a=0)`, `function i missing 2 arguments (c, e)`}, + {`i(0, b=1)`, `function i missing 2 arguments (c, e)`}, + {`i(0, a=1)`, `function i got multiple values for parameter "a"`}, + {`i(0, b=1, c=2)`, `function i missing 1 argument (e)`}, + {`i(0, b=1, d=2)`, `function i missing 2 arguments (c, e)`}, + {`i(0, b=1, c=2, d=3)`, `function i missing 1 argument (e)`}, + {`i(0, b=1, c=2, d=3, e=4)`, `(0, 1, 2, 3, 4, {})`}, + {`i(0, 1, b=1, c=2, d=3, e=4)`, `function i got multiple values for parameter "b"`}, + + // j(a, b=42, *args, c, d=123, e, **kwargs) + {`j()`, `function j missing 3 arguments (a, c, e)`}, + {`j(0)`, `function j missing 2 arguments (c, e)`}, + {`j(0, 1)`, `function j missing 2 arguments (c, e)`}, + {`j(0, 1, 2)`, `function j missing 2 arguments (c, e)`}, + {`j(0, 1, e=2)`, `function j missing 1 argument (c)`}, + {`j(0, 1, 2, 3)`, `function j missing 2 arguments (c, e)`}, + {`j(a=0)`, `function j missing 2 arguments (c, e)`}, + {`j(0, b=1)`, `function j missing 2 arguments (c, e)`}, + {`j(0, a=1)`, `function j got multiple values for parameter "a"`}, + {`j(0, b=1, c=2)`, `function j missing 1 argument (e)`}, + {`j(0, b=1, d=2)`, `function j missing 2 arguments (c, e)`}, + {`j(0, b=1, c=2, d=3)`, `function j missing 1 argument (e)`}, + {`j(0, b=1, c=2, d=3, e=4)`, `(0, 1, (), 2, 3, 4, {})`}, + {`j(0, 1, b=1, c=2, d=3, e=4)`, `function j got multiple values for parameter "b"`}, + {`j(0, 1, 2, c=3, e=4)`, `(0, 1, (2,), 3, 123, 4, {})`}, + } { + var got string + if v, err := starlark.Eval(thread, "<expr>", test.src, globals); err != nil { + got = err.Error() + } else { + got = v.String() + } + if got != test.want { + t.Errorf("eval %s = %s, want %s", test.src, got, test.want) + } + } +} + +// TestPrint ensures that the Starlark print function calls +// Thread.Print, if provided. +func TestPrint(t *testing.T) { + const src = ` +print("hello") +def f(): print("hello", "world", sep=", ") +f() +` + buf := new(bytes.Buffer) + print := func(thread *starlark.Thread, msg string) { + caller := thread.CallFrame(1) + fmt.Fprintf(buf, "%s: %s: %s\n", caller.Pos, caller.Name, msg) + } + thread := &starlark.Thread{Print: print} + if _, err := starlark.ExecFile(thread, "foo.star", src, nil); err != nil { + t.Fatal(err) + } + want := "foo.star:2:6: <toplevel>: hello\n" + + "foo.star:3:15: f: hello, world\n" + if got := buf.String(); got != want { + t.Errorf("output was %s, want %s", got, want) + } +} + +func reportEvalError(tb testing.TB, err error) { + if err, ok := err.(*starlark.EvalError); ok { + tb.Fatal(err.Backtrace()) + } + tb.Fatal(err) +} + +// TestInt exercises the Int.Int64 and Int.Uint64 methods. +// If we can move their logic into math/big, delete this test. +func TestInt(t *testing.T) { + one := starlark.MakeInt(1) + + for _, test := range []struct { + i starlark.Int + wantInt64 string + wantUint64 string + }{ + {starlark.MakeInt64(math.MinInt64).Sub(one), "error", "error"}, + {starlark.MakeInt64(math.MinInt64), "-9223372036854775808", "error"}, + {starlark.MakeInt64(-1), "-1", "error"}, + {starlark.MakeInt64(0), "0", "0"}, + {starlark.MakeInt64(1), "1", "1"}, + {starlark.MakeInt64(math.MaxInt64), "9223372036854775807", "9223372036854775807"}, + {starlark.MakeUint64(math.MaxUint64), "error", "18446744073709551615"}, + {starlark.MakeUint64(math.MaxUint64).Add(one), "error", "error"}, + } { + gotInt64, gotUint64 := "error", "error" + if i, ok := test.i.Int64(); ok { + gotInt64 = fmt.Sprint(i) + } + if u, ok := test.i.Uint64(); ok { + gotUint64 = fmt.Sprint(u) + } + if gotInt64 != test.wantInt64 { + t.Errorf("(%s).Int64() = %s, want %s", test.i, gotInt64, test.wantInt64) + } + if gotUint64 != test.wantUint64 { + t.Errorf("(%s).Uint64() = %s, want %s", test.i, gotUint64, test.wantUint64) + } + } +} + +func backtrace(t *testing.T, err error) string { + switch err := err.(type) { + case *starlark.EvalError: + return err.Backtrace() + case nil: + t.Fatalf("ExecFile succeeded unexpectedly") + default: + t.Fatalf("ExecFile failed with %v, wanted *EvalError", err) + } + panic("unreachable") +} + +func TestBacktrace(t *testing.T) { + // This test ensures continuity of the stack of active Starlark + // functions, including propagation through built-ins such as 'min'. + const src = ` +def f(x): return 1//x +def g(x): return f(x) +def h(): return min([1, 2, 0], key=g) +def i(): return h() +i() +` + thread := new(starlark.Thread) + _, err := starlark.ExecFile(thread, "crash.star", src, nil) + const want = `Traceback (most recent call last): + crash.star:6:2: in <toplevel> + crash.star:5:18: in i + crash.star:4:20: in h + <builtin>: in min + crash.star:3:19: in g + crash.star:2:19: in f +Error: floored division by zero` + if got := backtrace(t, err); got != want { + t.Errorf("error was %s, want %s", got, want) + } + + // Additionally, ensure that errors originating in + // Starlark and/or Go each have an accurate frame. + // The topmost frame, if built-in, is not shown, + // but the name of the built-in function is shown + // as "Error in fn: ...". + // + // This program fails in Starlark (f) if x==0, + // or in Go (string.join) if x is non-zero. + const src2 = ` +def f(): ''.join([1//i]) +f() +` + for i, want := range []string{ + 0: `Traceback (most recent call last): + crash.star:3:2: in <toplevel> + crash.star:2:20: in f +Error: floored division by zero`, + 1: `Traceback (most recent call last): + crash.star:3:2: in <toplevel> + crash.star:2:17: in f +Error in join: join: in list, want string, got int`, + } { + globals := starlark.StringDict{"i": starlark.MakeInt(i)} + _, err := starlark.ExecFile(thread, "crash.star", src2, globals) + if got := backtrace(t, err); got != want { + t.Errorf("error was %s, want %s", got, want) + } + } +} + +func TestLoadBacktrace(t *testing.T) { + // This test ensures that load() does NOT preserve stack traces, + // but that API callers can get them with Unwrap(). + // For discussion, see: + // https://github.com/google/starlark-go/pull/244 + const src = ` +load('crash.star', 'x') +` + const loadedSrc = ` +def f(x): + return 1 // x + +f(0) +` + thread := new(starlark.Thread) + thread.Load = func(t *starlark.Thread, module string) (starlark.StringDict, error) { + return starlark.ExecFile(new(starlark.Thread), module, loadedSrc, nil) + } + _, err := starlark.ExecFile(thread, "root.star", src, nil) + + const want = `Traceback (most recent call last): + root.star:2:1: in <toplevel> +Error: cannot load crash.star: floored division by zero` + if got := backtrace(t, err); got != want { + t.Errorf("error was %s, want %s", got, want) + } + + unwrapEvalError := func(err error) *starlark.EvalError { + var result *starlark.EvalError + for { + if evalErr, ok := err.(*starlark.EvalError); ok { + result = evalErr + } + + // TODO: use errors.Unwrap when go >=1.13 is everywhere. + wrapper, isWrapper := err.(Wrapper) + if !isWrapper { + break + } + err = wrapper.Unwrap() + } + return result + } + + unwrappedErr := unwrapEvalError(err) + const wantUnwrapped = `Traceback (most recent call last): + crash.star:5:2: in <toplevel> + crash.star:3:12: in f +Error: floored division by zero` + if got := backtrace(t, unwrappedErr); got != wantUnwrapped { + t.Errorf("error was %s, want %s", got, wantUnwrapped) + } + +} + +// TestRepeatedExec parses and resolves a file syntax tree once then +// executes it repeatedly with different values of its predeclared variables. +func TestRepeatedExec(t *testing.T) { + predeclared := starlark.StringDict{"x": starlark.None} + _, prog, err := starlark.SourceProgram("repeat.star", "y = 2 * x", predeclared.Has) + if err != nil { + t.Fatal(err) + } + + for _, test := range []struct { + x, want starlark.Value + }{ + {x: starlark.MakeInt(42), want: starlark.MakeInt(84)}, + {x: starlark.String("mur"), want: starlark.String("murmur")}, + {x: starlark.Tuple{starlark.None}, want: starlark.Tuple{starlark.None, starlark.None}}, + } { + predeclared["x"] = test.x // update the values in dictionary + thread := new(starlark.Thread) + if globals, err := prog.Init(thread, predeclared); err != nil { + t.Errorf("x=%v: %v", test.x, err) // exec error + } else if eq, err := starlark.Equal(globals["y"], test.want); err != nil { + t.Errorf("x=%v: %v", test.x, err) // comparison error + } else if !eq { + t.Errorf("x=%v: got y=%v, want %v", test.x, globals["y"], test.want) + } + } +} + +// TestEmptyFilePosition ensures that even Programs +// from empty files have a valid position. +func TestEmptyPosition(t *testing.T) { + var predeclared starlark.StringDict + for _, content := range []string{"", "empty = False"} { + _, prog, err := starlark.SourceProgram("hello.star", content, predeclared.Has) + if err != nil { + t.Fatal(err) + } + if got, want := prog.Filename(), "hello.star"; got != want { + t.Errorf("Program.Filename() = %q, want %q", got, want) + } + } +} + +// TestUnpackUserDefined tests that user-defined +// implementations of starlark.Value may be unpacked. +func TestUnpackUserDefined(t *testing.T) { + // success + want := new(hasfields) + var x *hasfields + if err := starlark.UnpackArgs("unpack", starlark.Tuple{want}, nil, "x", &x); err != nil { + t.Errorf("UnpackArgs failed: %v", err) + } + if x != want { + t.Errorf("for x, got %v, want %v", x, want) + } + + // failure + err := starlark.UnpackArgs("unpack", starlark.Tuple{starlark.MakeInt(42)}, nil, "x", &x) + if want := "unpack: for parameter x: got int, want hasfields"; fmt.Sprint(err) != want { + t.Errorf("unpack args error = %q, want %q", err, want) + } +} + +type optionalStringUnpacker struct { + str string + isSet bool +} + +func (o *optionalStringUnpacker) Unpack(v starlark.Value) error { + s, ok := starlark.AsString(v) + if !ok { + return fmt.Errorf("got %s, want string", v.Type()) + } + o.str = s + o.isSet = ok + return nil +} + +func TestUnpackCustomUnpacker(t *testing.T) { + a := optionalStringUnpacker{} + wantA := optionalStringUnpacker{str: "a", isSet: true} + b := optionalStringUnpacker{str: "b"} + wantB := optionalStringUnpacker{str: "b"} + + // Success + if err := starlark.UnpackArgs("unpack", starlark.Tuple{starlark.String("a")}, nil, "a?", &a, "b?", &b); err != nil { + t.Errorf("UnpackArgs failed: %v", err) + } + if a != wantA { + t.Errorf("for a, got %v, want %v", a, wantA) + } + if b != wantB { + t.Errorf("for b, got %v, want %v", b, wantB) + } + + // failure + err := starlark.UnpackArgs("unpack", starlark.Tuple{starlark.MakeInt(42)}, nil, "a", &a) + if want := "unpack: for parameter a: got int, want string"; fmt.Sprint(err) != want { + t.Errorf("unpack args error = %q, want %q", err, want) + } +} + +func TestAsInt(t *testing.T) { + for _, test := range []struct { + val starlark.Value + ptr interface{} + want string + }{ + {starlark.MakeInt(42), new(int32), "42"}, + {starlark.MakeInt(-1), new(int32), "-1"}, + // Use Lsh not 1<<40 as the latter exceeds int if GOARCH=386. + {starlark.MakeInt(1).Lsh(40), new(int32), "1099511627776 out of range (want value in signed 32-bit range)"}, + {starlark.MakeInt(-1).Lsh(40), new(int32), "-1099511627776 out of range (want value in signed 32-bit range)"}, + + {starlark.MakeInt(42), new(uint16), "42"}, + {starlark.MakeInt(0xffff), new(uint16), "65535"}, + {starlark.MakeInt(0x10000), new(uint16), "65536 out of range (want value in unsigned 16-bit range)"}, + {starlark.MakeInt(-1), new(uint16), "-1 out of range (want value in unsigned 16-bit range)"}, + } { + var got string + if err := starlark.AsInt(test.val, test.ptr); err != nil { + got = err.Error() + } else { + got = fmt.Sprint(reflect.ValueOf(test.ptr).Elem().Interface()) + } + if got != test.want { + t.Errorf("AsInt(%s, %T): got %q, want %q", test.val, test.ptr, got, test.want) + } + } +} + +func TestDocstring(t *testing.T) { + globals, _ := starlark.ExecFile(&starlark.Thread{}, "doc.star", ` +def somefunc(): + "somefunc doc" + return 0 +`, nil) + + if globals["somefunc"].(*starlark.Function).Doc() != "somefunc doc" { + t.Fatal("docstring not found") + } +} + +func TestFrameLocals(t *testing.T) { + // trace prints a nice stack trace including argument + // values of calls to Starlark functions. + trace := func(thread *starlark.Thread) string { + buf := new(bytes.Buffer) + for i := 0; i < thread.CallStackDepth(); i++ { + fr := thread.DebugFrame(i) + fmt.Fprintf(buf, "%s(", fr.Callable().Name()) + if fn, ok := fr.Callable().(*starlark.Function); ok { + for i := 0; i < fn.NumParams(); i++ { + if i > 0 { + buf.WriteString(", ") + } + name, _ := fn.Param(i) + fmt.Fprintf(buf, "%s=%s", name, fr.Local(i)) + } + } else { + buf.WriteString("...") // a built-in function + } + buf.WriteString(")\n") + } + return buf.String() + } + + var got string + builtin := func(thread *starlark.Thread, _ *starlark.Builtin, _ starlark.Tuple, _ []starlark.Tuple) (starlark.Value, error) { + got = trace(thread) + return starlark.None, nil + } + predeclared := starlark.StringDict{ + "builtin": starlark.NewBuiltin("builtin", builtin), + } + _, err := starlark.ExecFile(&starlark.Thread{}, "foo.star", ` +def f(x, y): builtin() +def g(z): f(z, z*z) +g(7) +`, predeclared) + if err != nil { + t.Errorf("ExecFile failed: %v", err) + } + + var want = ` +builtin(...) +f(x=7, y=49) +g(z=7) +<toplevel>() +`[1:] + if got != want { + t.Errorf("got <<%s>>, want <<%s>>", got, want) + } +} + +type badType string + +func (b *badType) String() string { return "badType" } +func (b *badType) Type() string { return "badType:" + string(*b) } // panics if b==nil +func (b *badType) Truth() starlark.Bool { return true } +func (b *badType) Hash() (uint32, error) { return 0, nil } +func (b *badType) Freeze() {} + +var _ starlark.Value = new(badType) + +// TestUnpackErrorBadType verifies that the Unpack functions fail +// gracefully when a parameter's default value's Type method panics. +func TestUnpackErrorBadType(t *testing.T) { + for _, test := range []struct { + x *badType + want string + }{ + {new(badType), "got NoneType, want badType"}, // Starlark type name + {nil, "got NoneType, want *starlark_test.badType"}, // Go type name + } { + err := starlark.UnpackArgs("f", starlark.Tuple{starlark.None}, nil, "x", &test.x) + if err == nil { + t.Errorf("UnpackArgs succeeded unexpectedly") + continue + } + if !strings.Contains(err.Error(), test.want) { + t.Errorf("UnpackArgs error %q does not contain %q", err, test.want) + } + } +} + +// Regression test for github.com/google/starlark-go/issues/233. +func TestREPLChunk(t *testing.T) { + thread := new(starlark.Thread) + globals := make(starlark.StringDict) + exec := func(src string) { + f, err := syntax.Parse("<repl>", src, 0) + if err != nil { + t.Fatal(err) + } + if err := starlark.ExecREPLChunk(f, thread, globals); err != nil { + t.Fatal(err) + } + } + + exec("x = 0; y = 0") + if got, want := fmt.Sprintf("%v %v", globals["x"], globals["y"]), "0 0"; got != want { + t.Fatalf("chunk1: got %s, want %s", got, want) + } + + exec("x += 1; y = y + 1") + if got, want := fmt.Sprintf("%v %v", globals["x"], globals["y"]), "1 1"; got != want { + t.Fatalf("chunk2: got %s, want %s", got, want) + } +} + +func TestCancel(t *testing.T) { + // A thread cancelled before it begins executes no code. + { + thread := new(starlark.Thread) + thread.Cancel("nope") + _, err := starlark.ExecFile(thread, "precancel.star", `x = 1//0`, nil) + if fmt.Sprint(err) != "Starlark computation cancelled: nope" { + t.Errorf("execution returned error %q, want cancellation", err) + } + + // cancellation is sticky + _, err = starlark.ExecFile(thread, "precancel.star", `x = 1//0`, nil) + if fmt.Sprint(err) != "Starlark computation cancelled: nope" { + t.Errorf("execution returned error %q, want cancellation", err) + } + } + // A thread cancelled during a built-in executes no more code. + { + thread := new(starlark.Thread) + predeclared := starlark.StringDict{ + "stopit": starlark.NewBuiltin("stopit", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + thread.Cancel(fmt.Sprint(args[0])) + return starlark.None, nil + }), + } + _, err := starlark.ExecFile(thread, "stopit.star", `msg = 'nope'; stopit(msg); x = 1//0`, predeclared) + if fmt.Sprint(err) != `Starlark computation cancelled: "nope"` { + t.Errorf("execution returned error %q, want cancellation", err) + } + } +} + +func TestExecutionSteps(t *testing.T) { + // A Thread records the number of computation steps. + thread := new(starlark.Thread) + countSteps := func(n int) (uint64, error) { + predeclared := starlark.StringDict{"n": starlark.MakeInt(n)} + steps0 := thread.ExecutionSteps() + _, err := starlark.ExecFile(thread, "steps.star", `squares = [x*x for x in range(n)]`, predeclared) + return thread.ExecutionSteps() - steps0, err + } + steps100, err := countSteps(1000) + if err != nil { + t.Errorf("execution failed: %v", err) + } + steps10000, err := countSteps(100000) + if err != nil { + t.Errorf("execution failed: %v", err) + } + if ratio := float64(steps10000) / float64(steps100); ratio < 99 || ratio > 101 { + t.Errorf("computation steps did not increase linearly: f(100)=%d, f(10000)=%d, ratio=%g, want ~100", steps100, steps10000, ratio) + } + + // Exceeding the step limit causes cancellation. + thread.SetMaxExecutionSteps(1000) + _, err = countSteps(1000) + if fmt.Sprint(err) != "Starlark computation cancelled: too many steps" { + t.Errorf("execution returned error %q, want cancellation", err) + } +} + +// TestDeps fails if the interpreter proper (not the REPL, etc) sprouts new external dependencies. +// We may expand the list of permitted dependencies, but should do so deliberately, not casually. +func TestDeps(t *testing.T) { + cmd := exec.Command("go", "list", "-deps") + out, err := cmd.Output() + if err != nil { + t.Skipf("'go list' failed: %s", err) + } + for _, pkg := range strings.Split(string(out), "\n") { + // Does pkg have form "domain.name/dir"? + slash := strings.IndexByte(pkg, '/') + dot := strings.IndexByte(pkg, '.') + if 0 < dot && dot < slash { + if strings.HasPrefix(pkg, "go.starlark.net/") || + strings.HasPrefix(pkg, "golang.org/x/sys/") { + continue // permitted dependencies + } + t.Errorf("new interpreter dependency: %s", pkg) + } + } +} |