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 }