| // 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" |
| "github.com/google/skylark/syntax" |
| ) |
| |
| 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) |
| } |
| } |
| |
| // TestRepeatedExec parses and resolves a file syntax tree once then |
| // executes it repeatedly with different values of its global variables. |
| func TestRepeatedExec(t *testing.T) { |
| f, err := syntax.Parse("repeat.sky", "y = 2 * x") |
| if err != nil { |
| t.Fatal(f) // parse error |
| } |
| |
| isPredeclaredGlobal := func(name string) bool { return name == "x" } // x, but not y |
| |
| if err := resolve.File(f, isPredeclaredGlobal, skylark.Universe.Has); err != nil { |
| t.Fatal(err) // resolve error |
| } |
| |
| thread := new(skylark.Thread) |
| for _, test := range []struct { |
| x, want skylark.Value |
| }{ |
| {x: skylark.MakeInt(42), want: skylark.MakeInt(84)}, |
| {x: skylark.String("mur"), want: skylark.String("murmur")}, |
| {x: skylark.Tuple{skylark.None}, want: skylark.Tuple{skylark.None, skylark.None}}, |
| } { |
| globals := skylark.StringDict{"x": test.x} |
| fr := thread.Push(globals, len(f.Locals)) |
| if err := fr.ExecStmts(f.Stmts); err != nil { |
| t.Errorf("x=%v: %v", test.x, err) // exec error |
| } else if eq, err := skylark.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) |
| } |
| thread.Pop() |
| } |
| } |
| |
| // TestUnpackUserDefined tests that user-defined |
| // implementations of skylark.Value may be unpacked. |
| func TestUnpackUserDefined(t *testing.T) { |
| // success |
| want := new(hasfields) |
| var x *hasfields |
| if err := skylark.UnpackArgs("unpack", skylark.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 := skylark.UnpackArgs("unpack", skylark.Tuple{skylark.MakeInt(42)}, nil, "x", &x) |
| if want := "unpack: for parameter 1: got int, want hasfields"; fmt.Sprint(err) != want { |
| t.Errorf("unpack args error = %q, want %q", err, want) |
| } |
| } |