// 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, "", 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`, ``}, // 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, "", 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: : 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 crash.star:5:18: in i crash.star:4:20: in h : 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 crash.star:2:20: in f Error: floored division by zero`, 1: `Traceback (most recent call last): crash.star:3:2: in 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 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 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) () `[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("", 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) } } }