You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
946 lines
29 KiB
946 lines
29 KiB
// 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)
|
|
}
|
|
}
|
|
}
|