blob: bf7213e4e53197251b4f0a4d2c4dcc934d66a5dd [file] [log] [blame]
// Copyright 2018 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package interpreter
import (
"context"
"fmt"
"testing"
"go.starlark.net/starlark"
"go.chromium.org/luci/common/testing/ftt"
"go.chromium.org/luci/common/testing/truth/assert"
"go.chromium.org/luci/common/testing/truth/should"
)
func TestMakeModuleKey(t *testing.T) {
t.Parallel()
th := &starlark.Thread{}
th.SetLocal(threadModKey, ModuleKey{"cur_pkg", "dir/cur.star"})
ftt.Run("Works", t, func(t *ftt.Test) {
k, err := MakeModuleKey(th, "//some/mod")
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, k, should.Resemble(ModuleKey{"cur_pkg", "some/mod"}))
k, err = MakeModuleKey(th, "//some/mod/../blah")
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, k, should.Resemble(ModuleKey{"cur_pkg", "some/blah"}))
k, err = MakeModuleKey(th, "some/mod")
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, k, should.Resemble(ModuleKey{"cur_pkg", "dir/some/mod"}))
k, err = MakeModuleKey(th, "./mod")
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, k, should.Resemble(ModuleKey{"cur_pkg", "dir/mod"}))
k, err = MakeModuleKey(th, "../mod")
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, k, should.Resemble(ModuleKey{"cur_pkg", "mod"}))
// For absolute paths the thread is optional.
k, err = MakeModuleKey(nil, "@pkg//some/mod")
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, k, should.Resemble(ModuleKey{"pkg", "some/mod"}))
})
ftt.Run("Fails", t, func(t *ftt.Test) {
_, err := MakeModuleKey(th, "@//mod")
assert.Loosely(t, err, should.NotBeNil)
_, err = MakeModuleKey(th, "@mod")
assert.Loosely(t, err, should.NotBeNil)
// Imports outside of the package root are forbidden.
_, err = MakeModuleKey(th, "//..")
assert.Loosely(t, err, should.NotBeNil)
_, err = MakeModuleKey(th, "../../mod")
assert.Loosely(t, err, should.NotBeNil)
// If the thread is given, it must have the package name.
_, err = MakeModuleKey(&starlark.Thread{}, "//some/mod")
assert.Loosely(t, err, should.NotBeNil)
})
}
func TestInterpreter(t *testing.T) {
t.Parallel()
ftt.Run("Stdlib scripts can load each other", t, func(t *ftt.Test) {
keys, logs, err := runIntr(intrParams{
stdlib: map[string]string{
"builtins.star": `
load("//loaded.star", "loaded_sym")
exported_sym = "exported_sym_val"
reimported_sym = loaded_sym
`,
"loaded.star": `loaded_sym = "loaded_sym_val"`,
},
scripts: map[string]string{
"main.star": `print(reimported_sym, exported_sym)`,
},
})
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, keys, should.HaveLength(0)) // main.star doesn't export anything itself
assert.Loosely(t, logs, should.Resemble([]string{"[//main.star:1] loaded_sym_val exported_sym_val"}))
})
ftt.Run("User scripts can load each other and stdlib scripts", t, func(t *ftt.Test) {
keys, _, err := runIntr(intrParams{
stdlib: map[string]string{
"lib.star": `lib_sym = True`,
},
scripts: map[string]string{
"main.star": `
load("//sub/loaded.star", _loaded_sym="loaded_sym")
load("@stdlib//lib.star", _lib_sym="lib_sym")
main_sym = True
loaded_sym = _loaded_sym
lib_sym = _lib_sym
`,
"sub/loaded.star": `loaded_sym = True`,
},
})
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, keys, should.Resemble([]string{
"lib_sym",
"loaded_sym",
"main_sym",
}))
})
ftt.Run("Missing module", t, func(t *ftt.Test) {
_, _, err := runIntr(intrParams{
scripts: map[string]string{
"main.star": `load("//some.star", "some")`,
},
})
assert.Loosely(t, err, should.ErrLike(`cannot load //some.star: no such module`))
})
ftt.Run("Missing package", t, func(t *ftt.Test) {
_, _, err := runIntr(intrParams{
scripts: map[string]string{
"main.star": `load("@pkg//some.star", "some")`,
},
})
assert.Loosely(t, err, should.ErrLike(`cannot load @pkg//some.star: no such package`))
})
ftt.Run("Malformed module reference", t, func(t *ftt.Test) {
_, _, err := runIntr(intrParams{
scripts: map[string]string{
"main.star": `load("@@", "some")`,
},
})
assert.Loosely(t, err, should.ErrLike(`cannot load @@: a module path should be either '//<path>', '<path>' or '@<package>//<path>'`))
})
ftt.Run("Double dot module reference", t, func(t *ftt.Test) {
_, _, err := runIntr(intrParams{
scripts: map[string]string{
"main.star": `load("../some.star", "some")`,
},
})
assert.Loosely(t, err, should.ErrLike(`cannot load ../some.star: outside the package root`))
})
ftt.Run("Predeclared are exposed to stdlib and user scripts", t, func(t *ftt.Test) {
_, logs, err := runIntr(intrParams{
predeclared: starlark.StringDict{
"imported_sym": starlark.MakeInt(123),
},
stdlib: map[string]string{
"builtins.star": `print(imported_sym)`,
},
scripts: map[string]string{
"main.star": `print(imported_sym)`,
},
})
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, logs, should.Resemble([]string{
"[@stdlib//builtins.star:1] 123",
"[//main.star:1] 123",
}))
})
ftt.Run("Predeclared can access the context", t, func(t *ftt.Test) {
fromCtx := ""
type key struct{}
_, _, err := runIntr(intrParams{
ctx: context.WithValue(context.Background(), key{}, "ctx value"),
predeclared: starlark.StringDict{
"call_me": starlark.NewBuiltin("call_me", func(th *starlark.Thread, _ *starlark.Builtin, _ starlark.Tuple, _ []starlark.Tuple) (starlark.Value, error) {
fromCtx = Context(th).Value(key{}).(string)
return starlark.None, nil
}),
},
scripts: map[string]string{
"main.star": `call_me()`,
},
})
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, fromCtx, should.Equal("ctx value"))
})
ftt.Run("Modules are loaded only once", t, func(t *ftt.Test) {
_, logs, err := runIntr(intrParams{
scripts: map[string]string{
"main.star": `
load("//mod.star", "a")
load("//mod.star", "b")
print(a, b)
`,
"mod.star": `
print("Loading")
a = 1
b = 2
`,
},
})
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, logs, should.Resemble([]string{
"[//mod.star:2] Loading", // only once
"[//main.star:5] 1 2",
}))
})
ftt.Run("Module cycles are caught", t, func(t *ftt.Test) {
_, _, err := runIntr(intrParams{
scripts: map[string]string{
"main.star": `load("//mod1.star", "a")`,
"mod1.star": `load("//mod2.star", "a")`,
"mod2.star": `load("//mod1.star", "a")`,
},
})
assert.Loosely(t, normalizeErr(err), should.Equal(`Traceback (most recent call last):
//main.star: in <toplevel>
Error: cannot load //mod1.star: Traceback (most recent call last):
//mod1.star: in <toplevel>
Error: cannot load //mod2.star: Traceback (most recent call last):
//mod2.star: in <toplevel>
Error: cannot load //mod1.star: cycle in the module dependency graph`))
})
ftt.Run("Error in loaded module", t, func(t *ftt.Test) {
_, _, err := runIntr(intrParams{
scripts: map[string]string{
"main.star": `load("//mod.star", "z")`,
"mod.star": `
def f():
boom = None()
f()
`,
},
})
assert.Loosely(t, normalizeErr(err), should.Equal(`Traceback (most recent call last):
//main.star: in <toplevel>
Error: cannot load //mod.star: Traceback (most recent call last):
//mod.star: in <toplevel>
//mod.star: in f
Error: invalid call of non-function (NoneType)`))
})
ftt.Run("Exec works", t, func(t *ftt.Test) {
_, logs, err := runIntr(intrParams{
scripts: map[string]string{
"main.star": `
res = exec("//execed.star")
print(res.a)
`,
"execed.star": `
print('hi')
a = 123
`,
},
})
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, logs, should.Resemble([]string{
"[//execed.star:2] hi",
"[//main.star:3] 123",
}))
})
ftt.Run("Exec using relative path", t, func(t *ftt.Test) {
_, logs, err := runIntr(intrParams{
scripts: map[string]string{
"main.star": `exec("//sub/1.star")`,
"sub/1.star": `exec("./2.star")`,
"sub/2.star": `print('hi')`,
},
})
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, logs, should.Resemble([]string{
"[//sub/2.star:1] hi",
}))
})
ftt.Run("Exec into another package", t, func(t *ftt.Test) {
_, logs, err := runIntr(intrParams{
scripts: map[string]string{
"main.star": `exec("@stdlib//exec1.star")`,
},
stdlib: map[string]string{
"exec1.star": `exec("//exec2.star")`,
"exec2.star": `print("hi")`,
},
})
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, logs, should.Resemble([]string{
"[@stdlib//exec2.star:1] hi",
}))
})
ftt.Run("Error in execed module", t, func(t *ftt.Test) {
_, _, err := runIntr(intrParams{
scripts: map[string]string{
"main.star": `
def f():
exec("//exec.star")
f()
`,
"exec.star": `
def f():
boom = None()
f()
`,
},
})
assert.Loosely(t, normalizeErr(err), should.Equal(`Traceback (most recent call last):
//main.star: in <toplevel>
//main.star: in f
Error in exec: exec //exec.star failed: Traceback (most recent call last):
//exec.star: in <toplevel>
//exec.star: in f
Error: invalid call of non-function (NoneType)`))
})
ftt.Run("Exec cycle", t, func(t *ftt.Test) {
_, _, err := runIntr(intrParams{
scripts: map[string]string{
"main.star": `exec("//exec1.star")`,
"exec1.star": `exec("//exec2.star")`,
"exec2.star": `exec("//exec1.star")`,
},
})
assert.Loosely(t, err, should.ErrLike(`the module has already been executed, 'exec'-ing same code twice is forbidden`))
})
ftt.Run("Trying to exec loaded module", t, func(t *ftt.Test) {
_, _, err := runIntr(intrParams{
scripts: map[string]string{
"main.star": `
load("//mod.star", "z")
exec("//mod.star")
`,
"mod.star": `z = 123`,
},
})
assert.Loosely(t, err, should.ErrLike("cannot exec //mod.star: the module has been loaded before and therefore is not executable"))
})
ftt.Run("Trying load execed module", t, func(t *ftt.Test) {
_, _, err := runIntr(intrParams{
scripts: map[string]string{
"main.star": `
exec("//mod.star")
load("//mod.star", "z")
`,
"mod.star": `z = 123`,
},
})
assert.Loosely(t, err, should.ErrLike("cannot load //mod.star: the module has been exec'ed before and therefore is not loadable"))
})
ftt.Run("Trying to exec from loading module", t, func(t *ftt.Test) {
_, _, err := runIntr(intrParams{
scripts: map[string]string{
"main.star": `load("//mod.star", "z")`,
"mod.star": `exec("//zzz.star")`,
},
})
assert.Loosely(t, err, should.ErrLike("exec //zzz.star: forbidden in this context, only exec'ed scripts can exec other scripts"))
})
ftt.Run("PreExec/PostExec hooks on success", t, func(t *ftt.Test) {
var hooks []string
_, _, err := runIntr(intrParams{
scripts: map[string]string{
"main.star": `exec("@stdlib//exec1.star")`,
},
stdlib: map[string]string{
"exec1.star": `exec("//exec2.star")`,
"exec2.star": `print("hi")`,
},
preExec: func(th *starlark.Thread, module ModuleKey) {
hooks = append(hooks, fmt.Sprintf("pre %s", module))
},
postExec: func(th *starlark.Thread, module ModuleKey) {
hooks = append(hooks, fmt.Sprintf("post %s", module))
},
})
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, hooks, should.Resemble([]string{
"pre //main.star",
"pre @stdlib//exec1.star",
"pre @stdlib//exec2.star",
"post @stdlib//exec2.star",
"post @stdlib//exec1.star",
"post //main.star",
}))
})
ftt.Run("PreExec/PostExec hooks on errors", t, func(t *ftt.Test) {
var hooks []string
_, _, err := runIntr(intrParams{
scripts: map[string]string{
"main.star": `exec("@stdlib//exec1.star")`,
},
stdlib: map[string]string{
"exec1.star": `exec("//exec2.star")`,
"exec2.star": `BOOOM`,
},
preExec: func(th *starlark.Thread, module ModuleKey) {
hooks = append(hooks, fmt.Sprintf("pre %s", module))
},
postExec: func(th *starlark.Thread, module ModuleKey) {
hooks = append(hooks, fmt.Sprintf("post %s", module))
},
})
assert.Loosely(t, err, should.NotBeNil)
assert.Loosely(t, hooks, should.Resemble([]string{
"pre //main.star",
"pre @stdlib//exec1.star",
"pre @stdlib//exec2.star",
"post @stdlib//exec2.star",
"post @stdlib//exec1.star",
"post //main.star",
}))
})
ftt.Run("Collects list of visited modules", t, func(t *ftt.Test) {
var visited []ModuleKey
_, _, err := runIntr(intrParams{
scripts: map[string]string{
"main.star": `
load("//a.star", "sym")
exec("//c.star")
`,
"a.star": `
load("//b.star", _sym="sym")
sym = _sym
`,
"b.star": `sym = 1`,
"c.star": `load("//b.star", "sym")`,
},
visited: &visited,
})
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, visited, should.Resemble([]ModuleKey{
{MainPkg, "main.star"},
{MainPkg, "a.star"},
{MainPkg, "b.star"},
{MainPkg, "c.star"},
}))
})
loadSrcBuiltin := starlark.NewBuiltin("load_src", func(th *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, _ []starlark.Tuple) (starlark.Value, error) {
src, err := GetThreadInterpreter(th).LoadSource(th, args[0].(starlark.String).GoString())
return starlark.String(src), err
})
ftt.Run("LoadSource works with abs paths", t, func(t *ftt.Test) {
_, logs, err := runIntr(intrParams{
predeclared: starlark.StringDict{"load_src": loadSrcBuiltin},
scripts: map[string]string{
"main.star": `
print(load_src("//data1.txt"))
print(load_src("@stdlib//data2.txt"))
exec("@stdlib//execed.star")
`,
"data1.txt": "blah 1",
},
stdlib: map[string]string{
"execed.star": `print(load_src("//data3.txt"))`,
"data2.txt": "blah 2",
"data3.txt": "blah 3",
},
})
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, logs, should.Resemble([]string{
"[//main.star:2] blah 1",
"[//main.star:3] blah 2",
"[@stdlib//execed.star:1] blah 3",
}))
})
ftt.Run("LoadSource works with rel paths", t, func(t *ftt.Test) {
_, logs, err := runIntr(intrParams{
predeclared: starlark.StringDict{"load_src": loadSrcBuiltin},
scripts: map[string]string{
"main.star": `
print(load_src("data1.txt"))
print(load_src("inner/data2.txt"))
exec("//inner/execed.star")
exec("@stdlib//inner/execed.star")
`,
"inner/execed.star": `
print(load_src("../data1.txt"))
print(load_src("data2.txt"))
`,
"data1.txt": "blah 1",
"inner/data2.txt": "blah 2",
},
stdlib: map[string]string{
"inner/execed.star": `print(load_src("data3.txt"))`,
"inner/data3.txt": "blah 3",
},
})
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, logs, should.Resemble([]string{
"[//main.star:2] blah 1",
"[//main.star:3] blah 2",
"[//inner/execed.star:2] blah 1",
"[//inner/execed.star:3] blah 2",
"[@stdlib//inner/execed.star:1] blah 3",
}))
})
ftt.Run("LoadSource handles missing files", t, func(t *ftt.Test) {
_, _, err := runIntr(intrParams{
predeclared: starlark.StringDict{"load_src": loadSrcBuiltin},
scripts: map[string]string{
"main.star": `load_src("data1.txt")`,
},
})
assert.Loosely(t, err, should.ErrLike("cannot load //data1.txt: no such file"))
})
ftt.Run("LoadSource handles go modules", t, func(t *ftt.Test) {
_, _, err := runIntr(intrParams{
predeclared: starlark.StringDict{"load_src": loadSrcBuiltin},
scripts: map[string]string{
"main.star": `load_src("@custom//something.txt")`,
},
custom: func(string) (starlark.StringDict, string, error) {
return starlark.StringDict{}, "", nil
},
})
assert.Loosely(t, err, should.ErrLike("cannot load @custom//something.txt: it is a native Go module"))
})
}