aboutsummaryrefslogtreecommitdiff
path: root/eval_test.go
diff options
context:
space:
mode:
authorAlan Donovan <adonovan@google.com>2017-10-02 10:10:28 -0400
committerAlan Donovan <adonovan@google.com>2017-10-02 10:10:28 -0400
commit312d1a5b5a9c50204aee186aeca0b7dbbd3eaaa0 (patch)
treeb766f2d515a7a3abcb0ebc6da796e04ab9739a97 /eval_test.go
downloadstarlark-go-312d1a5b5a9c50204aee186aeca0b7dbbd3eaaa0.tar.gz
skylark: create GitHub repository from google3@170697745
Diffstat (limited to 'eval_test.go')
-rw-r--r--eval_test.go435
1 files changed, 435 insertions, 0 deletions
diff --git a/eval_test.go b/eval_test.go
new file mode 100644
index 0000000..41cb716
--- /dev/null
+++ b/eval_test.go
@@ -0,0 +1,435 @@
+// 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 skylark_test
+
+import (
+ "bytes"
+ "fmt"
+ "math"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/google/skylark"
+ "github.com/google/skylark/internal/chunkedfile"
+ "github.com/google/skylark/resolve"
+ "github.com/google/skylark/skylarktest"
+)
+
+func init() {
+ // The tests make extensive use of these not-yet-standard features.
+ resolve.AllowLambda = true
+ resolve.AllowNestedDef = true
+ resolve.AllowFloat = true
+ resolve.AllowFreeze = true
+ resolve.AllowSet = true
+}
+
+func TestEvalExpr(t *testing.T) {
+ // This is mostly redundant with the new *.sky tests.
+ // TODO(adonovan): move checks into *.sky files and
+ // reduce this to a mere unit test of skylark.Eval.
+ thread := new(skylark.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)`},
+ // 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 := skylark.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) {
+ testdata := skylarktest.DataFile("skylark", ".")
+ thread := &skylark.Thread{Load: load}
+ skylarktest.SetReporter(thread, t)
+ for _, file := range []string{
+ "testdata/assign.sky",
+ "testdata/bool.sky",
+ "testdata/builtins.sky",
+ "testdata/control.sky",
+ "testdata/dict.sky",
+ "testdata/float.sky",
+ "testdata/function.sky",
+ "testdata/int.sky",
+ "testdata/list.sky",
+ "testdata/misc.sky",
+ "testdata/set.sky",
+ "testdata/string.sky",
+ "testdata/tuple.sky",
+ } {
+ filename := filepath.Join(testdata, file)
+ for _, chunk := range chunkedfile.Read(filename, t) {
+ globals := skylark.StringDict{
+ "hasfields": skylark.NewBuiltin("hasfields", newHasFields),
+ "fibonacci": fib{},
+ }
+ err := skylark.ExecFile(thread, filename, chunk.Source, globals)
+ switch err := err.(type) {
+ case *skylark.EvalError:
+ found := false
+ for _, fr := range err.Stack() {
+ posn := fr.Position()
+ 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.Error(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() skylark.Bool { return true }
+func (t fib) Hash() (uint32, error) { return 0, fmt.Errorf("fib is unhashable") }
+func (t fib) Iterate() skylark.Iterator { return &fibIterator{0, 1} }
+
+type fibIterator struct{ x, y int }
+
+func (it *fibIterator) Next(p *skylark.Value) bool {
+ *p = skylark.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 *skylark.Thread, module string) (skylark.StringDict, error) {
+ if module == "assert.sky" {
+ return skylarktest.LoadAssertModule()
+ }
+
+ // TODO(adonovan): test load() using this execution path.
+ globals := make(skylark.StringDict)
+ filename := filepath.Join(filepath.Dir(thread.Caller().Position().Filename()), module)
+ err := skylark.ExecFile(thread, filename, nil, globals)
+ return globals, err
+}
+
+func newHasFields(thread *skylark.Thread, _ *skylark.Builtin, args skylark.Tuple, kwargs []skylark.Tuple) (skylark.Value, error) {
+ return &hasfields{attrs: make(map[string]skylark.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 skylark.StringDict
+ frozen bool
+}
+
+var _ skylark.HasAttrs = (*hasfields)(nil)
+
+func (hf *hasfields) String() string { return "hasfields" }
+func (hf *hasfields) Type() string { return "hasfields" }
+func (hf *hasfields) Truth() skylark.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) (skylark.Value, error) { return hf.attrs[name], nil }
+
+func (hf *hasfields) SetField(name string, val skylark.Value) error {
+ if hf.frozen {
+ return fmt.Errorf("cannot set field on a frozen hasfields")
+ }
+ 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)
+ }
+ return names
+}
+
+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
+`
+
+ thread := new(skylark.Thread)
+ globals := make(skylark.StringDict)
+ if err := skylark.ExecFile(thread, filename, src, globals); err != nil {
+ t.Fatal(err)
+ }
+
+ for _, test := range []struct{ src, want string }{
+ {`a()`, `None`},
+ {`a(1)`, `function a takes no arguments (1 given)`},
+ {`b()`, `function b takes exactly 2 arguments (0 given)`},
+ {`b(1)`, `function b takes exactly 2 arguments (1 given)`},
+ {`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 takes exactly 2 arguments (3 given)`},
+ {`b(1, b=2)`, `(1, 2)`},
+ {`b(1, a=2)`, `function b got multiple values for keyword argument "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()`, `function c takes at least 1 argument (0 given)`},
+ {`c(1)`, `(1, 42)`},
+ {`c(1, 2)`, `(1, 2)`},
+ {`c(1, 2, 3)`, `function c takes at most 2 arguments (3 given)`},
+ {`c(1, b=2)`, `(1, 2)`},
+ {`c(1, a=2)`, `function c got multiple values for keyword argument "a"`},
+ {`c(a=1, b=2)`, `(1, 2)`},
+ {`c(b=1, a=2)`, `(2, 1)`},
+ {`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()`, `{}`},
+ {`e(1)`, `function e takes exactly 0 arguments (1 given)`},
+ {`e(k=1)`, `{"k": 1}`},
+ {`e(kwargs={})`, `{"kwargs": {}}`},
+ {`f()`, `function f takes at least 1 argument (0 given)`},
+ {`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 keyword argument "a"`},
+ {`f(0, b=1, c=2)`, `(0, 1, (), {"c": 2})`},
+ } {
+ var got string
+ if v, err := skylark.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 Skylark print function calls
+// Thread.Print, if provided.
+func TestPrint(t *testing.T) {
+ const src = `
+print("hello")
+def f(): print("world")
+f()
+`
+ buf := new(bytes.Buffer)
+ print := func(thread *skylark.Thread, msg string) {
+ caller := thread.Caller()
+ name := "<module>"
+ if caller.Function() != nil {
+ name = caller.Function().Name()
+ }
+ fmt.Fprintf(buf, "%s: %s: %s\n", caller.Position(), name, msg)
+ }
+ thread := &skylark.Thread{Print: print}
+ globals := make(skylark.StringDict)
+ if err := skylark.ExecFile(thread, "foo.go", src, globals); err != nil {
+ t.Fatal(err)
+ }
+ want := "foo.go:2:6: <module>: hello\n" +
+ "foo.go:3:15: f: world\n"
+ if got := buf.String(); got != want {
+ t.Errorf("output was %s, want %s", got, want)
+ }
+}
+
+func Benchmark(b *testing.B) {
+ testdata := skylarktest.DataFile("skylark", ".")
+ thread := new(skylark.Thread)
+ for _, file := range []string{
+ "testdata/benchmark.sky",
+ // ...
+ } {
+ filename := filepath.Join(testdata, file)
+
+ // Evaluate the file once.
+ globals := make(skylark.StringDict)
+ if err := skylark.ExecFile(thread, filename, nil, globals); err != nil {
+ reportEvalError(b, err)
+ }
+
+ // Repeatedly call each global function named bench_* as a benchmark.
+ for name, value := range globals {
+ if fn, ok := value.(*skylark.Function); ok && strings.HasPrefix(name, "bench_") {
+ b.Run(name, func(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ _, err := skylark.Call(thread, fn, nil, nil)
+ if err != nil {
+ reportEvalError(b, err)
+ }
+ }
+ })
+ }
+ }
+ }
+}
+
+func reportEvalError(tb testing.TB, err error) {
+ if err, ok := err.(*skylark.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 := skylark.MakeInt(1)
+
+ for _, test := range []struct {
+ i skylark.Int
+ wantInt64 string
+ wantUint64 string
+ }{
+ {skylark.MakeInt64(math.MinInt64).Sub(one), "error", "error"},
+ {skylark.MakeInt64(math.MinInt64), "-9223372036854775808", "error"},
+ {skylark.MakeInt64(-1), "-1", "error"},
+ {skylark.MakeInt64(0), "0", "0"},
+ {skylark.MakeInt64(1), "1", "1"},
+ {skylark.MakeInt64(math.MaxInt64), "9223372036854775807", "9223372036854775807"},
+ {skylark.MakeUint64(math.MaxUint64), "error", "18446744073709551615"},
+ {skylark.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 TestBacktrace(t *testing.T) {
+ // This test ensures continuity of the stack of active Skylark
+ // functions, including propagation through built-ins such as 'min'
+ // (though min does not itself appear in the stack).
+ const src = `
+def f(x): return 1//x
+def g(x): f(x)
+def h(): return min([1, 2, 0], key=g)
+def i(): return h()
+i()
+`
+ thread := new(skylark.Thread)
+ globals := make(skylark.StringDict)
+ err := skylark.ExecFile(thread, "crash.go", src, globals)
+ switch err := err.(type) {
+ case *skylark.EvalError:
+ got := err.Backtrace()
+ const want = `Traceback (most recent call last):
+ crash.go:6:2: in <toplevel>
+ crash.go:5:18: in i
+ crash.go:4:20: in h
+ crash.go:3:12: in g
+ crash.go:2:19: in f
+Error: floored division by zero`
+ if got != want {
+ t.Errorf("error was %s, want %s", got, want)
+ }
+ case nil:
+ t.Error("ExecFile succeeded unexpectedly")
+ default:
+ t.Errorf("ExecFile failed with %v, wanted *EvalError", err)
+ }
+}