package compiler import ( "bytes" "fmt" "os" "path/filepath" "strings" "testing" "c65gm/internal/preproc" "go.starlark.net/lib/math" "go.starlark.net/starlark" ) // --- Unit tests for mapStarlarkLine --- func TestMapStarlarkLine_Regular(t *testing.T) { tests := []struct { starlarkLine int numLines int isLibrary bool want int }{ {2, 5, false, 0}, // first script line {3, 5, false, 1}, // second script line {6, 5, false, 4}, // last script line {1, 5, false, -1}, // def _main(): line — out of range {7, 5, false, -1}, // _main() call — out of range {0, 5, false, -1}, // before start {2, 1, false, 0}, // single line script {3, 1, false, -1}, // single line, past it } for _, tt := range tests { got := mapStarlarkLine(tt.starlarkLine, tt.numLines, tt.isLibrary) if got != tt.want { t.Errorf("mapStarlarkLine(%d, %d, %v) = %d, want %d", tt.starlarkLine, tt.numLines, tt.isLibrary, got, tt.want) } } } func TestMapStarlarkLine_Library(t *testing.T) { tests := []struct { starlarkLine int numLines int want int }{ {1, 5, 0}, // first script line {2, 5, 1}, // second script line {5, 5, 4}, // last script line {0, 5, -1}, // before start {6, 5, -1}, // past end {1, 1, 0}, // single line {2, 1, -1}, // single line, past it } for _, tt := range tests { got := mapStarlarkLine(tt.starlarkLine, tt.numLines, true) if got != tt.want { t.Errorf("mapStarlarkLine(%d, %d, true) = %d, want %d", tt.starlarkLine, tt.numLines, got, tt.want) } } } // --- Tests for starlarkPosition and starlarkErrorMsg --- func TestStarlarkPosition_NoEvalError(t *testing.T) { got := starlarkPosition(fmt.Errorf("plain error")) if got != 0 { t.Errorf("starlarkPosition(plain error) = %d, want 0", got) } } func TestStarlarkPosition_WithCallStack(t *testing.T) { // Create an EvalError by executing Starlark code with a runtime error thread := &starlark.Thread{} predeclared := starlark.StringDict{"math": math.Module} _, err := starlark.ExecFile(thread, "test.star", `def f(): x = 1 // 0 f()`, predeclared) if err == nil { t.Fatal("expected Starlark error") } pos := starlarkPosition(err) if pos <= 0 { t.Errorf("starlarkPosition() = %d, want > 0 (error: %v)", pos, err) } } func TestStarlarkErrorMsg_StripsCallStack(t *testing.T) { thread := &starlark.Thread{} predeclared := starlark.StringDict{"math": math.Module} _, err := starlark.ExecFile(thread, "test.star", `x = 1 // 0`, predeclared) if err == nil { t.Fatal("expected Starlark error") } msg := starlarkErrorMsg(err) if !strings.Contains(msg, "division by zero") { t.Errorf("starlarkErrorMsg() = %q, want division by zero", msg) } // Should NOT contain traceback prefixes if strings.Contains(msg, "Traceback") { t.Errorf("starlarkErrorMsg() should not contain Traceback, got %q", msg) } } func TestStarlarkErrorMsg_PlainError(t *testing.T) { msg := starlarkErrorMsg(fmt.Errorf("hello")) if msg != "hello" { t.Errorf("starlarkErrorMsg(plain) = %q, want 'hello'", msg) } } // --- Tests for printScriptErrorContext --- func TestPrintScriptErrorContext_Basic(t *testing.T) { scriptLines := []preproc.Line{ {Text: " for i in range(3):", Filename: "test.c65", LineNo: 7, Kind: preproc.Script}, {Text: " print(i)", Filename: "test.c65", LineNo: 8, Kind: preproc.Script}, {Text: " end", Filename: "test.c65", LineNo: 9, Kind: preproc.Script}, } // Capture stderr old := os.Stderr r, w, _ := os.Pipe() os.Stderr = w err := fmt.Errorf("test error") printScriptErrorContext(err, "test error", scriptLines, 1, "SCRIPT") w.Close() os.Stderr = old var buf bytes.Buffer buf.ReadFrom(r) output := buf.String() // Should contain SCRIPT marker if !strings.Contains(output, "SCRIPT") { t.Errorf("output should contain SCRIPT marker, got:\n%s", output) } // Should contain ENDSCRIPT if !strings.Contains(output, "ENDSCRIPT") { t.Errorf("output should contain ENDSCRIPT, got:\n%s", output) } // Should contain the error line if !strings.Contains(output, "print(i)") { t.Errorf("output should contain the error line, got:\n%s", output) } // Should contain Starlark error header if !strings.Contains(output, "Starlark error:") { t.Errorf("output should contain 'Starlark error:', got:\n%s", output) } // Should contain source marker if !strings.Contains(output, "-->") { t.Errorf("output should contain '-->', got:\n%s", output) } } func TestPrintScriptErrorContext_EdgeCases(t *testing.T) { tests := []struct { name string lines []preproc.Line errorIdx int blockType string wantMarker bool }{ { name: "empty lines", lines: []preproc.Line{}, errorIdx: 0, blockType: "SCRIPT", wantMarker: false, }, { name: "invalid index", lines: []preproc.Line{{Text: "x", Filename: "t", LineNo: 1}}, errorIdx: 5, blockType: "SCRIPT", wantMarker: false, }, { name: "first line", lines: []preproc.Line{{Text: "x", Filename: "t", LineNo: 1}}, errorIdx: 0, blockType: "SCRIPT LIBRARY", wantMarker: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { old := os.Stderr r, w, _ := os.Pipe() os.Stderr = w printScriptErrorContext(fmt.Errorf("err"), "err", tt.lines, tt.errorIdx, tt.blockType) w.Close() os.Stderr = old var buf bytes.Buffer buf.ReadFrom(r) output := buf.String() if tt.wantMarker && !strings.Contains(output, tt.blockType) { t.Errorf("output should contain %q, got:\n%s", tt.blockType, output) } if !tt.wantMarker && !strings.Contains(output, "Starlark error") { // Should at minimum print the error t.Errorf("output should contain error even in edge case, got:\n%s", output) } }) } } // --- Tests for executeScript error prefix --- func TestExecuteScript_ErrorPrefix(t *testing.T) { pragma := preproc.NewPragma() ctx := NewCompilerContext(pragma) old := os.Stderr r, w, _ := os.Pipe() os.Stderr = w scriptLines := []preproc.Line{ {Text: "def f():\n return 1 // 0\nf()", Filename: "test.c65", LineNo: 1, Kind: preproc.Script}, } _, err := executeScript(scriptLines, ctx, false) w.Close() os.Stderr = old var buf bytes.Buffer buf.ReadFrom(r) if err == nil { t.Fatal("expected error") } if !strings.Contains(err.Error(), "Starlark error:") { t.Errorf("error should contain 'Starlark error:', got: %v", err) } } func TestExecuteScript_LibraryRuntimeError(t *testing.T) { pragma := preproc.NewPragma() ctx := NewCompilerContext(pragma) // Define a library function that errors libLines := []preproc.Line{ {Text: "def bad():\n return 1 // 0", Filename: "test.c65", LineNo: 5, Kind: preproc.ScriptLibrary}, } _, err := executeScript(libLines, ctx, true) if err != nil { t.Fatalf("library definition should succeed: %v", err) } // Capture stderr old := os.Stderr r, w, _ := os.Pipe() os.Stderr = w // Call the library function — should error callLines := []preproc.Line{ {Text: "bad()", Filename: "test.c65", LineNo: 10, Kind: preproc.Script}, } _, err = executeScript(callLines, ctx, false) w.Close() os.Stderr = old var buf bytes.Buffer buf.ReadFrom(r) stderr := buf.String() if err == nil { t.Fatal("expected error from library call") } if !strings.Contains(err.Error(), "Starlark error:") { t.Errorf("error should contain 'Starlark error:', got: %v", err) } // Should show bounded context for the calling SCRIPT block if !strings.Contains(stderr, "SCRIPT") { t.Errorf("stderr should contain SCRIPT marker, got:\n%s", stderr) } if !strings.Contains(stderr, "ENDSCRIPT") { t.Errorf("stderr should contain ENDSCRIPT, got:\n%s", stderr) } } func TestExecuteScript_DirectRuntimeError(t *testing.T) { pragma := preproc.NewPragma() ctx := NewCompilerContext(pragma) // Capture stderr old := os.Stderr r, w, _ := os.Pipe() os.Stderr = w scriptLines := []preproc.Line{ {Text: " print(1 // 0)", Filename: "test.c65", LineNo: 3, Kind: preproc.Script}, } _, err := executeScript(scriptLines, ctx, false) w.Close() os.Stderr = old var buf bytes.Buffer buf.ReadFrom(r) stderr := buf.String() if err == nil { t.Fatal("expected error") } if !strings.Contains(err.Error(), "Starlark error:") { t.Errorf("error should contain 'Starlark error:', got: %v", err) } // Should show the error source line in stderr context if !strings.Contains(stderr, "print(1 // 0)") { t.Errorf("stderr should contain the error line, got:\n%s", stderr) } } // --- Tests for ExecuteMacro error mapping --- func TestExecuteMacro_ErrorDefinitionSite(t *testing.T) { pragma := preproc.NewPragma() ctx := NewCompilerContext(pragma) // Register a macro with source provenance ctx.ScriptMacros["div_macro"] = &ScriptMacro{ Name: "div_macro", Params: []string{"a", "b"}, Body: []string{"result = a // b"}, SourceFile: "defs.c65", StartLine: 12, } // Capture stderr old := os.Stderr r, w, _ := os.Pipe() os.Stderr = w _, err := ExecuteMacro("div_macro", []string{"10", "0"}, ctx, 0) w.Close() os.Stderr = old var buf bytes.Buffer buf.ReadFrom(r) if err == nil { t.Fatal("expected error") } // Error should mention the definition site if !strings.Contains(err.Error(), "defs.c65:12") { t.Errorf("error should reference definition site 'defs.c65:12', got: %v", err) } if !strings.Contains(err.Error(), "Starlark error:") { t.Errorf("error should contain 'Starlark error:', got: %v", err) } } func TestExecuteMacro_ErrorFallbackNoSource(t *testing.T) { pragma := preproc.NewPragma() ctx := NewCompilerContext(pragma) // Register a macro WITHOUT source provenance (backward compatibility) ctx.ScriptMacros["m"] = &ScriptMacro{ Name: "m", Params: []string{}, Body: []string{"1 // 0"}, } _, err := ExecuteMacro("m", []string{}, ctx, 0) if err == nil { t.Fatal("expected error") } if !strings.Contains(err.Error(), "Starlark error:") { t.Errorf("error should contain 'Starlark error:', got: %v", err) } } // --- Tests for printStarlarkBacktrace --- func TestPrintStarlarkBacktrace_NoFrames(t *testing.T) { old := os.Stderr r, w, _ := os.Pipe() os.Stderr = w printStarlarkBacktrace(fmt.Errorf("plain error"), 10, "test.c65") w.Close() os.Stderr = old var buf bytes.Buffer buf.ReadFrom(r) output := buf.String() if output != "" { t.Errorf("expected empty output for plain error, got: %q", output) } } func TestPrintScriptErrorFallback(t *testing.T) { old := os.Stderr r, w, _ := os.Pipe() os.Stderr = w printScriptErrorFallback(fmt.Errorf("test message")) w.Close() os.Stderr = old var buf bytes.Buffer buf.ReadFrom(r) output := buf.String() if !strings.Contains(output, "Starlark error:") { t.Errorf("output should contain 'Starlark error:', got: %q", output) } if !strings.Contains(output, "test message") { t.Errorf("output should contain the error message, got: %q", output) } } // --- Tests for validateScriptFilePath --- func TestValidateScriptFilePath_Valid(t *testing.T) { projectRoot := "/home/user/project" tests := []struct { path string want string }{ {"file.bin", filepath.Join(projectRoot, "file.bin")}, {"subdir/file.bin", filepath.Join(projectRoot, "subdir/file.bin")}, {"./file.bin", filepath.Join(projectRoot, "file.bin")}, } for _, tt := range tests { got, err := validateScriptFilePath(projectRoot, tt.path) if err != nil { t.Errorf("validateScriptFilePath(%q, %q) unexpected error: %v", projectRoot, tt.path, err) continue } if got != tt.want { t.Errorf("validateScriptFilePath(%q, %q) = %q, want %q", projectRoot, tt.path, got, tt.want) } } } func TestValidateScriptFilePath_RejectsAbsolute(t *testing.T) { _, err := validateScriptFilePath("/home/user/project", "/etc/passwd") if err == nil { t.Error("expected error for absolute path") } } func TestValidateScriptFilePath_RejectsTraversal(t *testing.T) { projectRoot := "/home/user/project" tests := []string{ "../secret.bin", "subdir/../../secret.bin", } for _, path := range tests { _, err := validateScriptFilePath(projectRoot, path) if err == nil { t.Errorf("expected error for traversal path: %s", path) } } } func TestValidateScriptFilePath_RejectsEmptyProjectRoot(t *testing.T) { _, err := validateScriptFilePath("", "file.bin") if err == nil { t.Error("expected error for empty project root") } } func TestValidateScriptFilePath_RejectsEmptyPath(t *testing.T) { _, err := validateScriptFilePath("/home/user/project", "") if err == nil { t.Error("expected error for empty path") } } func TestValidateScriptFilePath_RejectsSymlinkEscape(t *testing.T) { projectRoot := t.TempDir() // Symlink pointing outside the project root outsideTarget := filepath.Join(projectRoot, "..", "outside.txt") if err := os.WriteFile(outsideTarget, []byte("secret"), 0644); err != nil { t.Fatal(err) } symlinkPath := filepath.Join(projectRoot, "evil_link") if err := os.Symlink(outsideTarget, symlinkPath); err != nil { t.Fatal(err) } _, err := validateScriptFilePath(projectRoot, "evil_link") if err == nil { t.Error("expected error for symlink escaping project root, got nil") } } func TestValidateScriptFilePath_AllowsSymlinkWithinProject(t *testing.T) { projectRoot := t.TempDir() // Create a file inside the project targetPath := filepath.Join(projectRoot, "real_file.txt") if err := os.WriteFile(targetPath, []byte("data"), 0644); err != nil { t.Fatal(err) } // Create a symlink pointing inside the project symlinkPath := filepath.Join(projectRoot, "link_to_real.txt") if err := os.Symlink("real_file.txt", symlinkPath); err != nil { t.Fatal(err) } got, err := validateScriptFilePath(projectRoot, "link_to_real.txt") if err != nil { t.Fatalf("unexpected error: %v", err) } if got != targetPath { t.Errorf("got %q, want %q", got, targetPath) } } // --- Tests for load_binary builtin --- func TestExecuteScript_LoadBinary(t *testing.T) { tmpDir := t.TempDir() binData := []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07} if err := os.WriteFile(filepath.Join(tmpDir, "data.bin"), binData, 0644); err != nil { t.Fatal(err) } pragma := preproc.NewPragma() ctx := NewCompilerContext(pragma) ctx.ProjectRoot = tmpDir scriptLines := []preproc.Line{ {Text: "data = load_binary(\"data.bin\")", Filename: "test.c65", LineNo: 1, Kind: preproc.Script}, {Text: "print(\"len=\" + str(len(data)))", Filename: "test.c65", LineNo: 2, Kind: preproc.Script}, {Text: "print(\"first=\" + str(data[0]))", Filename: "test.c65", LineNo: 3, Kind: preproc.Script}, {Text: "print(\"last=\" + str(data[7]))", Filename: "test.c65", LineNo: 4, Kind: preproc.Script}, } output, err := executeScript(scriptLines, ctx, false) if err != nil { t.Fatalf("executeScript failed: %v", err) } if len(output) < 3 { t.Fatalf("expected at least 3 output lines, got %d: %v", len(output), output) } if output[0] != "len=8" { t.Errorf("output[0] = %q, want \"len=8\"", output[0]) } if output[1] != "first=0" { t.Errorf("output[1] = %q, want \"first=0\"", output[1]) } if output[2] != "last=7" { t.Errorf("output[2] = %q, want \"last=7\"", output[2]) } } func TestExecuteScript_LoadBinary_Offset(t *testing.T) { tmpDir := t.TempDir() binData := []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05} if err := os.WriteFile(filepath.Join(tmpDir, "data.bin"), binData, 0644); err != nil { t.Fatal(err) } pragma := preproc.NewPragma() ctx := NewCompilerContext(pragma) ctx.ProjectRoot = tmpDir // Load with offset 2: should return [0x02, 0x03, 0x04, 0x05] scriptLines := []preproc.Line{ {Text: "data = load_binary(\"data.bin\", 2)", Filename: "test.c65", LineNo: 1, Kind: preproc.Script}, {Text: "print(\"len=\" + str(len(data)))", Filename: "test.c65", LineNo: 2, Kind: preproc.Script}, {Text: "print(\"first=\" + str(data[0]))", Filename: "test.c65", LineNo: 3, Kind: preproc.Script}, } output, err := executeScript(scriptLines, ctx, false) if err != nil { t.Fatalf("executeScript failed: %v", err) } if len(output) < 2 { t.Fatalf("expected at least 2 output lines, got %d: %v", len(output), output) } if output[0] != "len=4" { t.Errorf("output[0] = %q, want \"len=4\"", output[0]) } if output[1] != "first=2" { t.Errorf("output[1] = %q, want \"first=2\"", output[1]) } } func TestExecuteScript_LoadBinary_OffsetAndLength(t *testing.T) { tmpDir := t.TempDir() binData := []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07} if err := os.WriteFile(filepath.Join(tmpDir, "data.bin"), binData, 0644); err != nil { t.Fatal(err) } pragma := preproc.NewPragma() ctx := NewCompilerContext(pragma) ctx.ProjectRoot = tmpDir // Load with offset 3, length 2: should return [0x03, 0x04] scriptLines := []preproc.Line{ {Text: "data = load_binary(\"data.bin\", 3, 2)", Filename: "test.c65", LineNo: 1, Kind: preproc.Script}, {Text: "print(\"len=\" + str(len(data)))", Filename: "test.c65", LineNo: 2, Kind: preproc.Script}, {Text: "print(\"first=\" + str(data[0]))", Filename: "test.c65", LineNo: 3, Kind: preproc.Script}, {Text: "print(\"last=\" + str(data[1]))", Filename: "test.c65", LineNo: 4, Kind: preproc.Script}, } output, err := executeScript(scriptLines, ctx, false) if err != nil { t.Fatalf("executeScript failed: %v", err) } if len(output) < 3 { t.Fatalf("expected at least 3 output lines, got %d: %v", len(output), output) } if output[0] != "len=2" { t.Errorf("output[0] = %q, want \"len=2\"", output[0]) } if output[1] != "first=3" { t.Errorf("output[1] = %q, want \"first=3\"", output[1]) } if output[2] != "last=4" { t.Errorf("output[2] = %q, want \"last=4\"", output[2]) } } func TestExecuteScript_LoadBinary_NonexistentFile(t *testing.T) { tmpDir := t.TempDir() pragma := preproc.NewPragma() ctx := NewCompilerContext(pragma) ctx.ProjectRoot = tmpDir scriptLines := []preproc.Line{ {Text: "load_binary(\"nonexistent.bin\")", Filename: "test.c65", LineNo: 1, Kind: preproc.Script}, } _, err := executeScript(scriptLines, ctx, false) if err == nil { t.Error("expected error for nonexistent file") } if err != nil && !strings.Contains(err.Error(), "Starlark error") { t.Errorf("error should contain 'Starlark error', got: %v", err) } } func TestExecuteScript_LoadBinary_TraversalRejected(t *testing.T) { tmpDir := t.TempDir() pragma := preproc.NewPragma() ctx := NewCompilerContext(pragma) ctx.ProjectRoot = tmpDir scriptLines := []preproc.Line{ {Text: "load_binary(\"../outside.bin\")", Filename: "test.c65", LineNo: 1, Kind: preproc.Script}, } _, err := executeScript(scriptLines, ctx, false) if err == nil { t.Error("expected error for path traversal") } } func TestExecuteScript_LoadBinary_AbsoluteRejected(t *testing.T) { tmpDir := t.TempDir() pragma := preproc.NewPragma() ctx := NewCompilerContext(pragma) ctx.ProjectRoot = tmpDir scriptLines := []preproc.Line{ {Text: "load_binary(\"/etc/passwd\")", Filename: "test.c65", LineNo: 1, Kind: preproc.Script}, } _, err := executeScript(scriptLines, ctx, false) if err == nil { t.Error("expected error for absolute path") } } // --- Tests for load_text builtin --- func TestExecuteScript_LoadText(t *testing.T) { tmpDir := t.TempDir() textContent := "hello\nworld\nfoo" if err := os.WriteFile(filepath.Join(tmpDir, "data.txt"), []byte(textContent), 0644); err != nil { t.Fatal(err) } pragma := preproc.NewPragma() ctx := NewCompilerContext(pragma) ctx.ProjectRoot = tmpDir scriptLines := []preproc.Line{ {Text: "lines = load_text(\"data.txt\")", Filename: "test.c65", LineNo: 1, Kind: preproc.Script}, {Text: "print(\"len=\" + str(len(lines)))", Filename: "test.c65", LineNo: 2, Kind: preproc.Script}, {Text: "print(\"line0=\" + lines[0])", Filename: "test.c65", LineNo: 3, Kind: preproc.Script}, {Text: "print(\"line1=\" + lines[1])", Filename: "test.c65", LineNo: 4, Kind: preproc.Script}, {Text: "print(\"line2=\" + lines[2])", Filename: "test.c65", LineNo: 5, Kind: preproc.Script}, } output, err := executeScript(scriptLines, ctx, false) if err != nil { t.Fatalf("executeScript failed: %v", err) } if len(output) < 4 { t.Fatalf("expected at least 4 output lines, got %d: %v", len(output), output) } if output[0] != "len=3" { t.Errorf("output[0] = %q, want \"len=3\"", output[0]) } if output[1] != "line0=hello" { t.Errorf("output[1] = %q, want \"line0=hello\"", output[1]) } if output[2] != "line1=world" { t.Errorf("output[2] = %q, want \"line1=world\"", output[2]) } if output[3] != "line2=foo" { t.Errorf("output[3] = %q, want \"line2=foo\"", output[3]) } } func TestExecuteScript_LoadText_TrailingNewline(t *testing.T) { tmpDir := t.TempDir() textContent := "hello\nworld\n" if err := os.WriteFile(filepath.Join(tmpDir, "data.txt"), []byte(textContent), 0644); err != nil { t.Fatal(err) } pragma := preproc.NewPragma() ctx := NewCompilerContext(pragma) ctx.ProjectRoot = tmpDir scriptLines := []preproc.Line{ {Text: "lines = load_text(\"data.txt\")", Filename: "test.c65", LineNo: 1, Kind: preproc.Script}, {Text: "print(\"len=\" + str(len(lines)))", Filename: "test.c65", LineNo: 2, Kind: preproc.Script}, } output, err := executeScript(scriptLines, ctx, false) if err != nil { t.Fatalf("executeScript failed: %v", err) } if len(output) < 1 { t.Fatalf("expected at least 1 output line, got %d: %v", len(output), output) } if output[0] != "len=2" { t.Errorf("output[0] = %q, want \"len=2\"", output[0]) } } func TestExecuteScript_LoadText_EmptyFile(t *testing.T) { tmpDir := t.TempDir() if err := os.WriteFile(filepath.Join(tmpDir, "empty.txt"), []byte{}, 0644); err != nil { t.Fatal(err) } pragma := preproc.NewPragma() ctx := NewCompilerContext(pragma) ctx.ProjectRoot = tmpDir scriptLines := []preproc.Line{ {Text: "lines = load_text(\"empty.txt\")", Filename: "test.c65", LineNo: 1, Kind: preproc.Script}, {Text: "print(\"len=\" + str(len(lines)))", Filename: "test.c65", LineNo: 2, Kind: preproc.Script}, } output, err := executeScript(scriptLines, ctx, false) if err != nil { t.Fatalf("executeScript failed: %v", err) } if len(output) < 1 { t.Fatalf("expected at least 1 output line, got %d: %v", len(output), output) } if output[0] != "len=0" { t.Errorf("output[0] = %q, want \"len=0\"", output[0]) } } func TestExecuteScript_LoadText_CRLF(t *testing.T) { tmpDir := t.TempDir() textContent := "hello\r\nworld\r\n" if err := os.WriteFile(filepath.Join(tmpDir, "data.txt"), []byte(textContent), 0644); err != nil { t.Fatal(err) } pragma := preproc.NewPragma() ctx := NewCompilerContext(pragma) ctx.ProjectRoot = tmpDir scriptLines := []preproc.Line{ {Text: "lines = load_text(\"data.txt\")", Filename: "test.c65", LineNo: 1, Kind: preproc.Script}, {Text: "print(\"len=\" + str(len(lines)))", Filename: "test.c65", LineNo: 2, Kind: preproc.Script}, {Text: "print(\"line0=\" + lines[0])", Filename: "test.c65", LineNo: 3, Kind: preproc.Script}, {Text: "print(\"line1=\" + lines[1])", Filename: "test.c65", LineNo: 4, Kind: preproc.Script}, } output, err := executeScript(scriptLines, ctx, false) if err != nil { t.Fatalf("executeScript failed: %v", err) } if len(output) < 3 { t.Fatalf("expected at least 3 output lines, got %d: %v", len(output), output) } if output[0] != "len=2" { t.Errorf("output[0] = %q, want \"len=2\"", output[0]) } if output[1] != "line0=hello" { t.Errorf("output[1] = %q, want \"line0=hello\"", output[1]) } if output[2] != "line1=world" { t.Errorf("output[2] = %q, want \"line1=world\"", output[2]) } } func TestExecuteScript_LoadText_LibraryFunction(t *testing.T) { tmpDir := t.TempDir() textContent := "foo\nbar\nbaz" if err := os.WriteFile(filepath.Join(tmpDir, "data.txt"), []byte(textContent), 0644); err != nil { t.Fatal(err) } pragma := preproc.NewPragma() ctx := NewCompilerContext(pragma) ctx.ProjectRoot = tmpDir // Define a library function that uses load_text libLines := []preproc.Line{ {Text: "def get_lines():\n return load_text(\"data.txt\")", Filename: "test.c65", LineNo: 1, Kind: preproc.ScriptLibrary}, } _, err := executeScript(libLines, ctx, true) if err != nil { t.Fatalf("library definition failed: %v", err) } // Use the library function from a regular script scriptLines := []preproc.Line{ {Text: "lines = get_lines()", Filename: "test.c65", LineNo: 5, Kind: preproc.Script}, {Text: "print(\"len=\" + str(len(lines)))", Filename: "test.c65", LineNo: 6, Kind: preproc.Script}, {Text: "print(\"line1=\" + lines[1])", Filename: "test.c65", LineNo: 7, Kind: preproc.Script}, } output, err := executeScript(scriptLines, ctx, false) if err != nil { t.Fatalf("executeScript failed: %v", err) } if len(output) < 2 { t.Fatalf("expected at least 2 output lines, got %d: %v", len(output), output) } if output[0] != "len=3" { t.Errorf("output[0] = %q, want \"len=3\"", output[0]) } if output[1] != "line1=bar" { t.Errorf("output[1] = %q, want \"line1=bar\"", output[1]) } } // --- Tests for _P_SCRIPT_MAX_STEPS pragma --- func TestReadScriptMaxSteps_Default(t *testing.T) { pragma := preproc.NewPragma() ctx := NewCompilerContext(pragma) // No pragma set — index 0 has empty set got := readScriptMaxSteps(ctx, 0) if got != 1000000 { t.Errorf("readScriptMaxSteps() with no pragma = %d, want 1000000", got) } } func TestReadScriptMaxSteps_CustomValue(t *testing.T) { pragma := preproc.NewPragma() pragma.AddPragma("_P_SCRIPT_MAX_STEPS", "5000000") ctx := NewCompilerContext(pragma) got := readScriptMaxSteps(ctx, pragma.GetCurrentPragmaSetIndex()) if got != 5000000 { t.Errorf("readScriptMaxSteps() = %d, want 5000000", got) } } func TestReadScriptMaxSteps_Zero(t *testing.T) { pragma := preproc.NewPragma() pragma.AddPragma("_P_SCRIPT_MAX_STEPS", "0") ctx := NewCompilerContext(pragma) got := readScriptMaxSteps(ctx, pragma.GetCurrentPragmaSetIndex()) if got != 1000000 { t.Errorf("readScriptMaxSteps(0) = %d, want 1000000 (default)", got) } } func TestReadScriptMaxSteps_Invalid(t *testing.T) { pragma := preproc.NewPragma() pragma.AddPragma("_P_SCRIPT_MAX_STEPS", "not-a-number") ctx := NewCompilerContext(pragma) got := readScriptMaxSteps(ctx, pragma.GetCurrentPragmaSetIndex()) if got != 1000000 { t.Errorf("readScriptMaxSteps(invalid) = %d, want 1000000 (default)", got) } } func TestExecuteScript_MaxStepsPragma_LowLimit(t *testing.T) { pragma := preproc.NewPragma() pragma.AddPragma("_P_SCRIPT_MAX_STEPS", "5") // very low limit ctx := NewCompilerContext(pragma) // A simple loop will exceed 5 steps scriptLines := []preproc.Line{ {Text: " for i in range(10):", Filename: "test.c65", LineNo: 1, Kind: preproc.Script, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex()}, {Text: " x = i", Filename: "test.c65", LineNo: 2, Kind: preproc.Script, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex()}, } _, err := executeScript(scriptLines, ctx, false) if err == nil { t.Fatal("expected step-limit error, got nil") } if !strings.Contains(err.Error(), "Starlark error") { t.Errorf("error should contain 'Starlark error', got: %v", err) } } func TestExecuteScript_MaxStepsPragma_HighLimit(t *testing.T) { pragma := preproc.NewPragma() pragma.AddPragma("_P_SCRIPT_MAX_STEPS", "500000") // generous limit ctx := NewCompilerContext(pragma) scriptLines := []preproc.Line{ {Text: " result = 0", Filename: "test.c65", LineNo: 1, Kind: preproc.Script, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex()}, {Text: " for i in range(100):", Filename: "test.c65", LineNo: 2, Kind: preproc.Script, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex()}, {Text: " result = result + i", Filename: "test.c65", LineNo: 3, Kind: preproc.Script, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex()}, {Text: " print(str(result))", Filename: "test.c65", LineNo: 4, Kind: preproc.Script, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex()}, } output, err := executeScript(scriptLines, ctx, false) if err != nil { t.Fatalf("executeScript failed with generous limit: %v", err) } if len(output) < 1 || output[0] != "4950" { t.Errorf("expected output '4950', got %v", output) } } func TestExecuteMacro_MaxStepsPragma_LowLimit(t *testing.T) { pragma := preproc.NewPragma() pragma.AddPragma("_P_SCRIPT_MAX_STEPS", "5") // very low limit ctx := NewCompilerContext(pragma) // Register a simple macro ctx.ScriptMacros["loop_macro"] = &ScriptMacro{ Name: "loop_macro", Params: []string{}, Body: []string{"for i in range(10):", " x = i"}, SourceFile: "test.c65", StartLine: 1, } _, err := ExecuteMacro("loop_macro", []string{}, ctx, pragma.GetCurrentPragmaSetIndex()) if err == nil { t.Fatal("expected step-limit error, got nil") } if !strings.Contains(err.Error(), "Starlark error") { t.Errorf("error should contain 'Starlark error', got: %v", err) } } // Test that two SCRIPT blocks in the same compilation get different step limits // when a #PRAGMA _P_SCRIPT_MAX_STEPS change appears between them. func TestExecuteScript_MaxStepsPragma_ChangesBetweenBlocks(t *testing.T) { pragma := preproc.NewPragma() ctx := NewCompilerContext(pragma) // First block: default limit (index 0, no pragmas) — should succeed firstLines := []preproc.Line{ {Text: " result = 0", Filename: "test.c65", LineNo: 1, Kind: preproc.Script, PragmaSetIndex: 0}, {Text: " for i in range(10):", Filename: "test.c65", LineNo: 2, Kind: preproc.Script, PragmaSetIndex: 0}, {Text: " result = result + i", Filename: "test.c65", LineNo: 3, Kind: preproc.Script, PragmaSetIndex: 0}, {Text: " print(str(result))", Filename: "test.c65", LineNo: 4, Kind: preproc.Script, PragmaSetIndex: 0}, } output, err := executeScript(firstLines, ctx, false) if err != nil { t.Fatalf("first script (default limit) should succeed: %v", err) } if len(output) < 1 || output[0] != "45" { t.Errorf("first script output = %v, want ['45']", output) } // Simulate #PRAGMA _P_SCRIPT_MAX_STEPS 5 between the blocks pragma.AddPragma("_P_SCRIPT_MAX_STEPS", "5") lowLimitIdx := pragma.GetCurrentPragmaSetIndex() // Second block: low limit (index from pragma) — should fail secondLines := []preproc.Line{ {Text: " for i in range(10):", Filename: "test.c65", LineNo: 10, Kind: preproc.Script, PragmaSetIndex: lowLimitIdx}, {Text: " x = i", Filename: "test.c65", LineNo: 11, Kind: preproc.Script, PragmaSetIndex: lowLimitIdx}, } _, err = executeScript(secondLines, ctx, false) if err == nil { t.Fatal("second script (limit=5) should fail with step-limit error, got nil") } if !strings.Contains(err.Error(), "Starlark error") { t.Errorf("second script error should contain 'Starlark error', got: %v", err) } } // Test that two macro invocations in the same compilation get different step limits // when a #PRAGMA _P_SCRIPT_MAX_STEPS change appears between them. func TestExecuteMacro_MaxStepsPragma_ChangesBetweenBlocks(t *testing.T) { pragma := preproc.NewPragma() ctx := NewCompilerContext(pragma) ctx.ScriptMacros["loop_macro"] = &ScriptMacro{ Name: "loop_macro", Params: []string{}, Body: []string{"result = 0", "for i in range(10):", " result = result + i", "print(str(result))"}, SourceFile: "test.c65", StartLine: 1, } // First invocation: default limit (first pragma set) — should succeed output, err := ExecuteMacro("loop_macro", []string{}, ctx, pragma.GetCurrentPragmaSetIndex()) if err != nil { t.Fatalf("first macro (default limit) should succeed: %v", err) } if len(output) < 1 || output[0] != "45" { t.Errorf("first macro output = %v, want ['45']", output) } // Simulate #PRAGMA _P_SCRIPT_MAX_STEPS 5 between the invocations pragma.AddPragma("_P_SCRIPT_MAX_STEPS", "5") lowLimitIdx := pragma.GetCurrentPragmaSetIndex() // Second invocation: low limit — should fail _, err = ExecuteMacro("loop_macro", []string{}, ctx, lowLimitIdx) if err == nil { t.Fatal("second macro (limit=5) should fail with step-limit error, got nil") } if !strings.Contains(err.Error(), "Starlark error") { t.Errorf("second macro error should contain 'Starlark error', got: %v", err) } } func TestExecuteMacro_MaxStepsPragma_HighLimit(t *testing.T) { pragma := preproc.NewPragma() pragma.AddPragma("_P_SCRIPT_MAX_STEPS", "500000") // generous limit ctx := NewCompilerContext(pragma) ctx.ScriptMacros["sum_macro"] = &ScriptMacro{ Name: "sum_macro", Params: []string{}, Body: []string{"result = 0", "for i in range(100):", " result = result + i", "print(str(result))"}, SourceFile: "test.c65", StartLine: 1, } output, err := ExecuteMacro("sum_macro", []string{}, ctx, pragma.GetCurrentPragmaSetIndex()) if err != nil { t.Fatalf("ExecuteMacro failed with generous limit: %v", err) } if len(output) < 1 || output[0] != "4950" { t.Errorf("expected output '4950', got %v", output) } }