package sandbox import ( "context" "strings" "testing" "time" "go.starlark.net/starlark" ) func TestExecBasic(t *testing.T) { sb := New(DefaultConfig()) res, err := sb.Exec(context.Background(), "test.star", ` x = 1 + 2 name = "hello" `, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } if v, ok := res.Globals["x"]; !ok { t.Fatal("missing global 'x'") } else if v.String() != "3" { t.Fatalf("expected x=3, got %s", v.String()) } if v, ok := res.Globals["name"]; !ok { t.Fatal("missing global 'name'") } else if v.(starlark.String).GoString() != "hello" { t.Fatalf("expected name='hello', got %s", v.String()) } } func TestExecPrintCapture(t *testing.T) { sb := New(DefaultConfig()) res, err := sb.Exec(context.Background(), "test.star", ` print("line one") print("line two") `, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } expected := "line one\nline two\n" if res.Output != expected { t.Fatalf("expected output %q, got %q", expected, res.Output) } } func TestExecStepsTracked(t *testing.T) { sb := New(DefaultConfig()) res, err := sb.Exec(context.Background(), "test.star", ` def work(): x = 0 for i in range(100): x += i return x result = work() `, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } if res.Steps == 0 { t.Fatal("expected steps > 0") } } func TestExecStepLimitExceeded(t *testing.T) { sb := New(Config{MaxSteps: 100, Name: "limited"}) _, err := sb.Exec(context.Background(), "test.star", ` def work(): x = 0 for i in range(10000): x += i return x result = work() `, nil) if err == nil { t.Fatal("expected step limit error") } // Error message varies: "Starlark computation cancelled: exceeded ..." // or "context canceled" — just verify it failed. } func TestExecContextTimeout(t *testing.T) { // Use step limit of 0 (unlimited) so only context timeout triggers sb := New(Config{MaxSteps: 0, Name: "timeout"}) ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) defer cancel() _, err := sb.Exec(ctx, "test.star", ` def work(): x = 0 for i in range(100000000): x += i return x result = work() `, nil) if err == nil { t.Fatal("expected timeout error") } } func TestExecLoadDisabled(t *testing.T) { sb := New(DefaultConfig()) _, err := sb.Exec(context.Background(), "test.star", ` load("other.star", "foo") `, nil) if err == nil { t.Fatal("expected load error") } if !strings.Contains(err.Error(), "disabled") { t.Fatalf("expected 'disabled' in error, got: %v", err) } } func TestExecModuleInjection(t *testing.T) { getFn := starlark.NewBuiltin("test.get", func( thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple, ) (starlark.Value, error) { return starlark.String("secret_value"), nil }) mod := MakeModule("test", starlark.StringDict{ "get": getFn, }) sb := New(DefaultConfig()) res, err := sb.Exec(context.Background(), "test.star", ` result = test.get() `, map[string]starlark.Value{"test": mod}) if err != nil { t.Fatalf("unexpected error: %v", err) } v, ok := res.Globals["result"] if !ok { t.Fatal("missing global 'result'") } if v.(starlark.String).GoString() != "secret_value" { t.Fatalf("expected 'secret_value', got %s", v.String()) } } func TestExecSyntaxError(t *testing.T) { sb := New(DefaultConfig()) _, err := sb.Exec(context.Background(), "bad.star", ` def broken( `, nil) if err == nil { t.Fatal("expected syntax error") } se, ok := err.(*SandboxError) if !ok { t.Fatalf("expected *SandboxError, got %T", err) } _ = se // syntax errors may not have backtrace } func TestExecRuntimeError(t *testing.T) { sb := New(DefaultConfig()) _, err := sb.Exec(context.Background(), "runtime.star", ` x = 1 / 0 `, nil) if err == nil { t.Fatal("expected runtime error") } se, ok := err.(*SandboxError) if !ok { t.Fatalf("expected *SandboxError, got %T", err) } if se.Backtrace == "" { t.Fatal("expected backtrace for runtime error") } } func TestCallFunction(t *testing.T) { sb := New(DefaultConfig()) res, err := sb.Exec(context.Background(), "funcs.star", ` def greet(name): return "hello, " + name `, nil) if err != nil { t.Fatalf("exec error: %v", err) } fn := res.Globals["greet"].(starlark.Callable) val, _, err := sb.Call(context.Background(), fn, starlark.Tuple{starlark.String("world")}, nil) if err != nil { t.Fatalf("call error: %v", err) } if val.(starlark.String).GoString() != "hello, world" { t.Fatalf("expected 'hello, world', got %s", val.String()) } } func TestCallWithPrint(t *testing.T) { sb := New(DefaultConfig()) res, err := sb.Exec(context.Background(), "funcs.star", ` def run(): print("running") return 42 `, nil) if err != nil { t.Fatalf("exec error: %v", err) } fn := res.Globals["run"].(starlark.Callable) val, output, err := sb.Call(context.Background(), fn, nil, nil) if err != nil { t.Fatalf("call error: %v", err) } if val.String() != "42" { t.Fatalf("expected 42, got %s", val.String()) } if !strings.Contains(output, "running") { t.Fatalf("expected 'running' in output, got %q", output) } } func TestCallRuntimeError(t *testing.T) { sb := New(DefaultConfig()) res, err := sb.Exec(context.Background(), "funcs.star", ` def boom(): return 1 / 0 `, nil) if err != nil { t.Fatalf("exec error: %v", err) } fn := res.Globals["boom"].(starlark.Callable) _, _, err = sb.Call(context.Background(), fn, nil, nil) if err == nil { t.Fatal("expected runtime error from Call") } se, ok := err.(*SandboxError) if !ok { t.Fatalf("expected *SandboxError, got %T", err) } if se.Backtrace == "" { t.Fatal("expected backtrace") } } func TestNewDefaults(t *testing.T) { sb := New(Config{}) if sb.config.MaxSteps != 0 { t.Fatalf("expected MaxSteps=0 (unlimited), got %d", sb.config.MaxSteps) } if sb.config.Name != "extension" { t.Fatalf("expected default Name='extension', got %q", sb.config.Name) } } func TestNewWithDefaultConfig(t *testing.T) { sb := New(DefaultConfig()) if sb.config.MaxSteps != 1_000_000 { t.Fatalf("expected MaxSteps=1000000, got %d", sb.config.MaxSteps) } } func TestMakeModule(t *testing.T) { mod := MakeModule("mymod", starlark.StringDict{ "version": starlark.String("1.0"), }) if mod.Name != "mymod" { t.Fatalf("expected name 'mymod', got %q", mod.Name) } v, ok := mod.Members["version"] if !ok { t.Fatal("missing 'version' member") } if v.(starlark.String).GoString() != "1.0" { t.Fatalf("expected '1.0', got %s", v.String()) } } func TestSandboxErrorUnwrap(t *testing.T) { inner := starlark.String("test").Truth() // just need any value _ = inner se := &SandboxError{ Err: context.DeadlineExceeded, Backtrace: "", } if se.Unwrap() != context.DeadlineExceeded { t.Fatal("Unwrap should return inner error") } if se.Error() != "context deadline exceeded" { t.Fatalf("unexpected error string: %q", se.Error()) } } func TestSandboxErrorWithBacktrace(t *testing.T) { se := &SandboxError{ Err: context.DeadlineExceeded, Backtrace: " funcs.star:3:5: in boom", } if !strings.Contains(se.Error(), "context deadline exceeded") { t.Fatal("should contain original error") } if !strings.Contains(se.Error(), "funcs.star:3:5") { t.Fatal("should contain backtrace") } }