439 lines
11 KiB
Go
439 lines
11 KiB
Go
package compiler
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"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)
|
|
|
|
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)
|
|
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)
|
|
}
|
|
}
|