go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/starlark/interpreter/interpreter_test.go (about)

     1  // Copyright 2018 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package interpreter
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"testing"
    21  
    22  	"go.starlark.net/starlark"
    23  
    24  	. "github.com/smartystreets/goconvey/convey"
    25  	. "go.chromium.org/luci/common/testing/assertions"
    26  )
    27  
    28  func TestMakeModuleKey(t *testing.T) {
    29  	t.Parallel()
    30  
    31  	th := &starlark.Thread{}
    32  	th.SetLocal(threadModKey, ModuleKey{"cur_pkg", "dir/cur.star"})
    33  
    34  	Convey("Works", t, func() {
    35  		k, err := MakeModuleKey(th, "//some/mod")
    36  		So(err, ShouldBeNil)
    37  		So(k, ShouldResemble, ModuleKey{"cur_pkg", "some/mod"})
    38  
    39  		k, err = MakeModuleKey(th, "//some/mod/../blah")
    40  		So(err, ShouldBeNil)
    41  		So(k, ShouldResemble, ModuleKey{"cur_pkg", "some/blah"})
    42  
    43  		k, err = MakeModuleKey(th, "some/mod")
    44  		So(err, ShouldBeNil)
    45  		So(k, ShouldResemble, ModuleKey{"cur_pkg", "dir/some/mod"})
    46  
    47  		k, err = MakeModuleKey(th, "./mod")
    48  		So(err, ShouldBeNil)
    49  		So(k, ShouldResemble, ModuleKey{"cur_pkg", "dir/mod"})
    50  
    51  		k, err = MakeModuleKey(th, "../mod")
    52  		So(err, ShouldBeNil)
    53  		So(k, ShouldResemble, ModuleKey{"cur_pkg", "mod"})
    54  
    55  		// For absolute paths the thread is optional.
    56  		k, err = MakeModuleKey(nil, "@pkg//some/mod")
    57  		So(err, ShouldBeNil)
    58  		So(k, ShouldResemble, ModuleKey{"pkg", "some/mod"})
    59  	})
    60  
    61  	Convey("Fails", t, func() {
    62  		_, err := MakeModuleKey(th, "@//mod")
    63  		So(err, ShouldNotBeNil)
    64  
    65  		_, err = MakeModuleKey(th, "@mod")
    66  		So(err, ShouldNotBeNil)
    67  
    68  		// Imports outside of the package root are forbidden.
    69  		_, err = MakeModuleKey(th, "//..")
    70  		So(err, ShouldNotBeNil)
    71  
    72  		_, err = MakeModuleKey(th, "../../mod")
    73  		So(err, ShouldNotBeNil)
    74  
    75  		// If the thread is given, it must have the package name.
    76  		_, err = MakeModuleKey(&starlark.Thread{}, "//some/mod")
    77  		So(err, ShouldNotBeNil)
    78  	})
    79  }
    80  
    81  func TestInterpreter(t *testing.T) {
    82  	t.Parallel()
    83  
    84  	Convey("Stdlib scripts can load each other", t, func() {
    85  		keys, logs, err := runIntr(intrParams{
    86  			stdlib: map[string]string{
    87  				"builtins.star": `
    88  					load("//loaded.star", "loaded_sym")
    89  					exported_sym = "exported_sym_val"
    90  					reimported_sym = loaded_sym
    91  				`,
    92  				"loaded.star": `loaded_sym = "loaded_sym_val"`,
    93  			},
    94  			scripts: map[string]string{
    95  				"main.star": `print(reimported_sym, exported_sym)`,
    96  			},
    97  		})
    98  		So(err, ShouldBeNil)
    99  		So(keys, ShouldHaveLength, 0) // main.star doesn't export anything itself
   100  		So(logs, ShouldResemble, []string{"[//main.star:1] loaded_sym_val exported_sym_val"})
   101  	})
   102  
   103  	Convey("User scripts can load each other and stdlib scripts", t, func() {
   104  		keys, _, err := runIntr(intrParams{
   105  			stdlib: map[string]string{
   106  				"lib.star": `lib_sym = True`,
   107  			},
   108  			scripts: map[string]string{
   109  				"main.star": `
   110  					load("//sub/loaded.star", _loaded_sym="loaded_sym")
   111  					load("@stdlib//lib.star", _lib_sym="lib_sym")
   112  					main_sym = True
   113  					loaded_sym = _loaded_sym
   114  					lib_sym = _lib_sym
   115  				`,
   116  				"sub/loaded.star": `loaded_sym = True`,
   117  			},
   118  		})
   119  		So(err, ShouldBeNil)
   120  		So(keys, ShouldResemble, []string{
   121  			"lib_sym",
   122  			"loaded_sym",
   123  			"main_sym",
   124  		})
   125  	})
   126  
   127  	Convey("Missing module", t, func() {
   128  		_, _, err := runIntr(intrParams{
   129  			scripts: map[string]string{
   130  				"main.star": `load("//some.star", "some")`,
   131  			},
   132  		})
   133  		So(err, ShouldErrLike, `cannot load //some.star: no such module`)
   134  	})
   135  
   136  	Convey("Missing package", t, func() {
   137  		_, _, err := runIntr(intrParams{
   138  			scripts: map[string]string{
   139  				"main.star": `load("@pkg//some.star", "some")`,
   140  			},
   141  		})
   142  		So(err, ShouldErrLike, `cannot load @pkg//some.star: no such package`)
   143  	})
   144  
   145  	Convey("Malformed module reference", t, func() {
   146  		_, _, err := runIntr(intrParams{
   147  			scripts: map[string]string{
   148  				"main.star": `load("@@", "some")`,
   149  			},
   150  		})
   151  		So(err, ShouldErrLike, `cannot load @@: a module path should be either '//<path>', '<path>' or '@<package>//<path>'`)
   152  	})
   153  
   154  	Convey("Double dot module reference", t, func() {
   155  		_, _, err := runIntr(intrParams{
   156  			scripts: map[string]string{
   157  				"main.star": `load("../some.star", "some")`,
   158  			},
   159  		})
   160  		So(err, ShouldErrLike, `cannot load ../some.star: outside the package root`)
   161  	})
   162  
   163  	Convey("Predeclared are exposed to stdlib and user scripts", t, func() {
   164  		_, logs, err := runIntr(intrParams{
   165  			predeclared: starlark.StringDict{
   166  				"imported_sym": starlark.MakeInt(123),
   167  			},
   168  			stdlib: map[string]string{
   169  				"builtins.star": `print(imported_sym)`,
   170  			},
   171  			scripts: map[string]string{
   172  				"main.star": `print(imported_sym)`,
   173  			},
   174  		})
   175  		So(err, ShouldBeNil)
   176  		So(logs, ShouldResemble, []string{
   177  			"[@stdlib//builtins.star:1] 123",
   178  			"[//main.star:1] 123",
   179  		})
   180  	})
   181  
   182  	Convey("Predeclared can access the context", t, func() {
   183  		fromCtx := ""
   184  		type key struct{}
   185  		_, _, err := runIntr(intrParams{
   186  			ctx: context.WithValue(context.Background(), key{}, "ctx value"),
   187  			predeclared: starlark.StringDict{
   188  				"call_me": starlark.NewBuiltin("call_me", func(th *starlark.Thread, _ *starlark.Builtin, _ starlark.Tuple, _ []starlark.Tuple) (starlark.Value, error) {
   189  					fromCtx = Context(th).Value(key{}).(string)
   190  					return starlark.None, nil
   191  				}),
   192  			},
   193  			scripts: map[string]string{
   194  				"main.star": `call_me()`,
   195  			},
   196  		})
   197  		So(err, ShouldBeNil)
   198  		So(fromCtx, ShouldEqual, "ctx value")
   199  	})
   200  
   201  	Convey("Modules are loaded only once", t, func() {
   202  		_, logs, err := runIntr(intrParams{
   203  			scripts: map[string]string{
   204  				"main.star": `
   205  					load("//mod.star", "a")
   206  					load("//mod.star", "b")
   207  
   208  					print(a, b)
   209  				`,
   210  				"mod.star": `
   211  					print("Loading")
   212  
   213  					a = 1
   214  					b = 2
   215  				`,
   216  			},
   217  		})
   218  		So(err, ShouldBeNil)
   219  		So(logs, ShouldResemble, []string{
   220  			"[//mod.star:2] Loading", // only once
   221  			"[//main.star:5] 1 2",
   222  		})
   223  	})
   224  
   225  	Convey("Module cycles are caught", t, func() {
   226  		_, _, err := runIntr(intrParams{
   227  			scripts: map[string]string{
   228  				"main.star": `load("//mod1.star", "a")`,
   229  				"mod1.star": `load("//mod2.star", "a")`,
   230  				"mod2.star": `load("//mod1.star", "a")`,
   231  			},
   232  		})
   233  		So(normalizeErr(err), ShouldEqual, `Traceback (most recent call last):
   234    //main.star: in <toplevel>
   235  Error: cannot load //mod1.star: Traceback (most recent call last):
   236    //mod1.star: in <toplevel>
   237  Error: cannot load //mod2.star: Traceback (most recent call last):
   238    //mod2.star: in <toplevel>
   239  Error: cannot load //mod1.star: cycle in the module dependency graph`)
   240  	})
   241  
   242  	Convey("Error in loaded module", t, func() {
   243  		_, _, err := runIntr(intrParams{
   244  			scripts: map[string]string{
   245  				"main.star": `load("//mod.star", "z")`,
   246  				"mod.star": `
   247  					def f():
   248  						boom = None()
   249  					f()
   250  				`,
   251  			},
   252  		})
   253  		So(normalizeErr(err), ShouldEqual, `Traceback (most recent call last):
   254    //main.star: in <toplevel>
   255  Error: cannot load //mod.star: Traceback (most recent call last):
   256    //mod.star: in <toplevel>
   257    //mod.star: in f
   258  Error: invalid call of non-function (NoneType)`)
   259  	})
   260  
   261  	Convey("Exec works", t, func() {
   262  		_, logs, err := runIntr(intrParams{
   263  			scripts: map[string]string{
   264  				"main.star": `
   265  					res = exec("//execed.star")
   266  					print(res.a)
   267  				`,
   268  
   269  				"execed.star": `
   270  					print('hi')
   271  					a = 123
   272  				`,
   273  			},
   274  		})
   275  		So(err, ShouldBeNil)
   276  		So(logs, ShouldResemble, []string{
   277  			"[//execed.star:2] hi",
   278  			"[//main.star:3] 123",
   279  		})
   280  	})
   281  
   282  	Convey("Exec using relative path", t, func() {
   283  		_, logs, err := runIntr(intrParams{
   284  			scripts: map[string]string{
   285  				"main.star":  `exec("//sub/1.star")`,
   286  				"sub/1.star": `exec("./2.star")`,
   287  				"sub/2.star": `print('hi')`,
   288  			},
   289  		})
   290  		So(err, ShouldBeNil)
   291  		So(logs, ShouldResemble, []string{
   292  			"[//sub/2.star:1] hi",
   293  		})
   294  	})
   295  
   296  	Convey("Exec into another package", t, func() {
   297  		_, logs, err := runIntr(intrParams{
   298  			scripts: map[string]string{
   299  				"main.star": `exec("@stdlib//exec1.star")`,
   300  			},
   301  			stdlib: map[string]string{
   302  				"exec1.star": `exec("//exec2.star")`,
   303  				"exec2.star": `print("hi")`,
   304  			},
   305  		})
   306  		So(err, ShouldBeNil)
   307  		So(logs, ShouldResemble, []string{
   308  			"[@stdlib//exec2.star:1] hi",
   309  		})
   310  	})
   311  
   312  	Convey("Error in execed module", t, func() {
   313  		_, _, err := runIntr(intrParams{
   314  			scripts: map[string]string{
   315  				"main.star": `
   316  					def f():
   317  						exec("//exec.star")
   318  					f()
   319  				`,
   320  				"exec.star": `
   321  					def f():
   322  						boom = None()
   323  					f()
   324  				`,
   325  			},
   326  		})
   327  		So(normalizeErr(err), ShouldEqual, `Traceback (most recent call last):
   328    //main.star: in <toplevel>
   329    //main.star: in f
   330  Error in exec: exec //exec.star failed: Traceback (most recent call last):
   331    //exec.star: in <toplevel>
   332    //exec.star: in f
   333  Error: invalid call of non-function (NoneType)`)
   334  	})
   335  
   336  	Convey("Exec cycle", t, func() {
   337  		_, _, err := runIntr(intrParams{
   338  			scripts: map[string]string{
   339  				"main.star":  `exec("//exec1.star")`,
   340  				"exec1.star": `exec("//exec2.star")`,
   341  				"exec2.star": `exec("//exec1.star")`,
   342  			},
   343  		})
   344  		So(err, ShouldErrLike, `the module has already been executed, 'exec'-ing same code twice is forbidden`)
   345  	})
   346  
   347  	Convey("Trying to exec loaded module", t, func() {
   348  		_, _, err := runIntr(intrParams{
   349  			scripts: map[string]string{
   350  				"main.star": `
   351  					load("//mod.star", "z")
   352  					exec("//mod.star")
   353  				`,
   354  				"mod.star": `z = 123`,
   355  			},
   356  		})
   357  		So(err, ShouldErrLike, "cannot exec //mod.star: the module has been loaded before and therefore is not executable")
   358  	})
   359  
   360  	Convey("Trying load execed module", t, func() {
   361  		_, _, err := runIntr(intrParams{
   362  			scripts: map[string]string{
   363  				"main.star": `
   364  					exec("//mod.star")
   365  					load("//mod.star", "z")
   366  				`,
   367  				"mod.star": `z = 123`,
   368  			},
   369  		})
   370  		So(err, ShouldErrLike, "cannot load //mod.star: the module has been exec'ed before and therefore is not loadable")
   371  	})
   372  
   373  	Convey("Trying to exec from loading module", t, func() {
   374  		_, _, err := runIntr(intrParams{
   375  			scripts: map[string]string{
   376  				"main.star": `load("//mod.star", "z")`,
   377  				"mod.star":  `exec("//zzz.star")`,
   378  			},
   379  		})
   380  		So(err, ShouldErrLike, "exec //zzz.star: forbidden in this context, only exec'ed scripts can exec other scripts")
   381  	})
   382  
   383  	Convey("PreExec/PostExec hooks on success", t, func() {
   384  		var hooks []string
   385  		_, _, err := runIntr(intrParams{
   386  			scripts: map[string]string{
   387  				"main.star": `exec("@stdlib//exec1.star")`,
   388  			},
   389  			stdlib: map[string]string{
   390  				"exec1.star": `exec("//exec2.star")`,
   391  				"exec2.star": `print("hi")`,
   392  			},
   393  			preExec: func(th *starlark.Thread, module ModuleKey) {
   394  				hooks = append(hooks, fmt.Sprintf("pre %s", module))
   395  			},
   396  			postExec: func(th *starlark.Thread, module ModuleKey) {
   397  				hooks = append(hooks, fmt.Sprintf("post %s", module))
   398  			},
   399  		})
   400  		So(err, ShouldBeNil)
   401  		So(hooks, ShouldResemble, []string{
   402  			"pre //main.star",
   403  			"pre @stdlib//exec1.star",
   404  			"pre @stdlib//exec2.star",
   405  			"post @stdlib//exec2.star",
   406  			"post @stdlib//exec1.star",
   407  			"post //main.star",
   408  		})
   409  	})
   410  
   411  	Convey("PreExec/PostExec hooks on errors", t, func() {
   412  		var hooks []string
   413  		_, _, err := runIntr(intrParams{
   414  			scripts: map[string]string{
   415  				"main.star": `exec("@stdlib//exec1.star")`,
   416  			},
   417  			stdlib: map[string]string{
   418  				"exec1.star": `exec("//exec2.star")`,
   419  				"exec2.star": `BOOOM`,
   420  			},
   421  			preExec: func(th *starlark.Thread, module ModuleKey) {
   422  				hooks = append(hooks, fmt.Sprintf("pre %s", module))
   423  			},
   424  			postExec: func(th *starlark.Thread, module ModuleKey) {
   425  				hooks = append(hooks, fmt.Sprintf("post %s", module))
   426  			},
   427  		})
   428  		So(err, ShouldNotBeNil)
   429  		So(hooks, ShouldResemble, []string{
   430  			"pre //main.star",
   431  			"pre @stdlib//exec1.star",
   432  			"pre @stdlib//exec2.star",
   433  			"post @stdlib//exec2.star",
   434  			"post @stdlib//exec1.star",
   435  			"post //main.star",
   436  		})
   437  	})
   438  
   439  	Convey("Collects list of visited modules", t, func() {
   440  		var visited []ModuleKey
   441  		_, _, err := runIntr(intrParams{
   442  			scripts: map[string]string{
   443  				"main.star": `
   444  					load("//a.star", "sym")
   445  					exec("//c.star")
   446  				`,
   447  				"a.star": `
   448  					load("//b.star", _sym="sym")
   449  					sym = _sym
   450  				`,
   451  				"b.star": `sym = 1`,
   452  				"c.star": `load("//b.star", "sym")`,
   453  			},
   454  			visited: &visited,
   455  		})
   456  		So(err, ShouldBeNil)
   457  		So(visited, ShouldResemble, []ModuleKey{
   458  			{MainPkg, "main.star"},
   459  			{MainPkg, "a.star"},
   460  			{MainPkg, "b.star"},
   461  			{MainPkg, "c.star"},
   462  		})
   463  	})
   464  
   465  	loadSrcBuiltin := starlark.NewBuiltin("load_src", func(th *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, _ []starlark.Tuple) (starlark.Value, error) {
   466  		src, err := GetThreadInterpreter(th).LoadSource(th, args[0].(starlark.String).GoString())
   467  		return starlark.String(src), err
   468  	})
   469  
   470  	Convey("LoadSource works with abs paths", t, func() {
   471  		_, logs, err := runIntr(intrParams{
   472  			predeclared: starlark.StringDict{"load_src": loadSrcBuiltin},
   473  			scripts: map[string]string{
   474  				"main.star": `
   475  					print(load_src("//data1.txt"))
   476  					print(load_src("@stdlib//data2.txt"))
   477  					exec("@stdlib//execed.star")
   478  				`,
   479  				"data1.txt": "blah 1",
   480  			},
   481  			stdlib: map[string]string{
   482  				"execed.star": `print(load_src("//data3.txt"))`,
   483  				"data2.txt":   "blah 2",
   484  				"data3.txt":   "blah 3",
   485  			},
   486  		})
   487  		So(err, ShouldBeNil)
   488  		So(logs, ShouldResemble, []string{
   489  			"[//main.star:2] blah 1",
   490  			"[//main.star:3] blah 2",
   491  			"[@stdlib//execed.star:1] blah 3",
   492  		})
   493  	})
   494  
   495  	Convey("LoadSource works with rel paths", t, func() {
   496  		_, logs, err := runIntr(intrParams{
   497  			predeclared: starlark.StringDict{"load_src": loadSrcBuiltin},
   498  			scripts: map[string]string{
   499  				"main.star": `
   500  					print(load_src("data1.txt"))
   501  					print(load_src("inner/data2.txt"))
   502  					exec("//inner/execed.star")
   503  					exec("@stdlib//inner/execed.star")
   504  				`,
   505  				"inner/execed.star": `
   506  					print(load_src("../data1.txt"))
   507  					print(load_src("data2.txt"))
   508  				`,
   509  				"data1.txt":       "blah 1",
   510  				"inner/data2.txt": "blah 2",
   511  			},
   512  			stdlib: map[string]string{
   513  				"inner/execed.star": `print(load_src("data3.txt"))`,
   514  				"inner/data3.txt":   "blah 3",
   515  			},
   516  		})
   517  		So(err, ShouldBeNil)
   518  		So(logs, ShouldResemble, []string{
   519  			"[//main.star:2] blah 1",
   520  			"[//main.star:3] blah 2",
   521  			"[//inner/execed.star:2] blah 1",
   522  			"[//inner/execed.star:3] blah 2",
   523  			"[@stdlib//inner/execed.star:1] blah 3",
   524  		})
   525  	})
   526  
   527  	Convey("LoadSource handles missing files", t, func() {
   528  		_, _, err := runIntr(intrParams{
   529  			predeclared: starlark.StringDict{"load_src": loadSrcBuiltin},
   530  			scripts: map[string]string{
   531  				"main.star": `load_src("data1.txt")`,
   532  			},
   533  		})
   534  		So(err, ShouldErrLike, "cannot load //data1.txt: no such file")
   535  	})
   536  
   537  	Convey("LoadSource handles go modules", t, func() {
   538  		_, _, err := runIntr(intrParams{
   539  			predeclared: starlark.StringDict{"load_src": loadSrcBuiltin},
   540  			scripts: map[string]string{
   541  				"main.star": `load_src("@custom//something.txt")`,
   542  			},
   543  			custom: func(string) (starlark.StringDict, string, error) {
   544  				return starlark.StringDict{}, "", nil
   545  			},
   546  		})
   547  		So(err, ShouldErrLike, "cannot load @custom//something.txt: it is a native Go module")
   548  	})
   549  }