Added meaningful error messages for Starlark code
This commit is contained in:
parent
205ae92d0e
commit
d4f1213c06
5 changed files with 693 additions and 29 deletions
|
|
@ -42,11 +42,13 @@ func (c *Compiler) Registry() *CommandRegistry {
|
||||||
func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
||||||
var codeOutput []string
|
var codeOutput []string
|
||||||
var lastKind = preproc.Source
|
var lastKind = preproc.Source
|
||||||
var scriptBuffer []string
|
var scriptBuffer []preproc.Line
|
||||||
var scriptIsLibrary bool
|
var scriptIsLibrary bool
|
||||||
var macroBuffer []string
|
var macroBuffer []string
|
||||||
var currentMacroName string
|
var currentMacroName string
|
||||||
var currentMacroParams []string
|
var currentMacroParams []string
|
||||||
|
var currentMacroSourceFile string
|
||||||
|
var currentMacroStartLine int
|
||||||
var currentAsmTarget *[]string // nil = no active ASM block, or points to target slice
|
var currentAsmTarget *[]string // nil = no active ASM block, or points to target slice
|
||||||
|
|
||||||
// Reset deferred ASM storage for this compilation
|
// Reset deferred ASM storage for this compilation
|
||||||
|
|
@ -74,15 +76,19 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
||||||
if lastKind == preproc.ScriptMacroDef {
|
if lastKind == preproc.ScriptMacroDef {
|
||||||
if currentMacroName != "" {
|
if currentMacroName != "" {
|
||||||
c.ctx.ScriptMacros[currentMacroName] = &ScriptMacro{
|
c.ctx.ScriptMacros[currentMacroName] = &ScriptMacro{
|
||||||
Name: currentMacroName,
|
Name: currentMacroName,
|
||||||
Params: currentMacroParams,
|
Params: currentMacroParams,
|
||||||
Body: macroBuffer,
|
Body: macroBuffer,
|
||||||
|
SourceFile: currentMacroSourceFile,
|
||||||
|
StartLine: currentMacroStartLine,
|
||||||
}
|
}
|
||||||
codeOutput = append(codeOutput, fmt.Sprintf("; ENDSCRIPT MACRO %s", currentMacroName))
|
codeOutput = append(codeOutput, fmt.Sprintf("; ENDSCRIPT MACRO %s", currentMacroName))
|
||||||
}
|
}
|
||||||
macroBuffer = nil
|
macroBuffer = nil
|
||||||
currentMacroName = ""
|
currentMacroName = ""
|
||||||
currentMacroParams = nil
|
currentMacroParams = nil
|
||||||
|
currentMacroSourceFile = ""
|
||||||
|
currentMacroStartLine = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close previous Assembler block
|
// Close previous Assembler block
|
||||||
|
|
@ -199,12 +205,17 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
|
||||||
*currentAsmTarget = append(*currentAsmTarget, codePart+commentPart)
|
*currentAsmTarget = append(*currentAsmTarget, codePart+commentPart)
|
||||||
} else if line.Kind == preproc.Script || line.Kind == preproc.ScriptLibrary {
|
} else if line.Kind == preproc.Script || line.Kind == preproc.ScriptLibrary {
|
||||||
// Collect script lines for execution
|
// Collect script lines for execution
|
||||||
scriptBuffer = append(scriptBuffer, line.Text)
|
scriptBuffer = append(scriptBuffer, line)
|
||||||
} else if line.Kind == preproc.ScriptMacroDef {
|
} else if line.Kind == preproc.ScriptMacroDef {
|
||||||
// Skip the header line (already parsed in transition)
|
// Skip the header line (already parsed in transition)
|
||||||
if strings.HasPrefix(strings.TrimSpace(line.Text), "SCRIPT MACRO ") {
|
if strings.HasPrefix(strings.TrimSpace(line.Text), "SCRIPT MACRO ") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// Capture source provenance from first body line
|
||||||
|
if len(macroBuffer) == 0 {
|
||||||
|
currentMacroSourceFile = line.Filename
|
||||||
|
currentMacroStartLine = line.LineNo
|
||||||
|
}
|
||||||
// Collect macro body lines
|
// Collect macro body lines
|
||||||
macroBuffer = append(macroBuffer, line.Text)
|
macroBuffer = append(macroBuffer, line.Text)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -165,7 +165,7 @@ func TestExecuteScript_BasicPrint(t *testing.T) {
|
||||||
" print(' nop')",
|
" print(' nop')",
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err := executeScript(scriptLines, ctx, false)
|
output, err := testExecuteScript(scriptLines, ctx, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("executeScript failed: %v", err)
|
t.Fatalf("executeScript failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -189,7 +189,7 @@ func TestExecuteScript_EmptyOutput(t *testing.T) {
|
||||||
"x = 1 + 1",
|
"x = 1 + 1",
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err := executeScript(scriptLines, ctx, false)
|
output, err := testExecuteScript(scriptLines, ctx, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("executeScript failed: %v", err)
|
t.Fatalf("executeScript failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -210,7 +210,7 @@ func TestExecuteScript_Library_DefinesFunction(t *testing.T) {
|
||||||
" print(' nop')",
|
" print(' nop')",
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := executeScript(libraryLines, ctx, true)
|
_, err := testExecuteScript(libraryLines, ctx, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("library executeScript failed: %v", err)
|
t.Fatalf("library executeScript failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -232,7 +232,7 @@ func TestExecuteScript_Library_FunctionCallableFromScript(t *testing.T) {
|
||||||
" print(' nop')",
|
" print(' nop')",
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := executeScript(libraryLines, ctx, true)
|
_, err := testExecuteScript(libraryLines, ctx, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("library executeScript failed: %v", err)
|
t.Fatalf("library executeScript failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -242,7 +242,7 @@ func TestExecuteScript_Library_FunctionCallableFromScript(t *testing.T) {
|
||||||
"emit_nops(2)",
|
"emit_nops(2)",
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err := executeScript(scriptLines, ctx, false)
|
output, err := testExecuteScript(scriptLines, ctx, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("script executeScript failed: %v", err)
|
t.Fatalf("script executeScript failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -267,7 +267,7 @@ func TestExecuteScript_MultipleLibraries_Accumulate(t *testing.T) {
|
||||||
"def func_a():",
|
"def func_a():",
|
||||||
" print(' ; from a')",
|
" print(' ; from a')",
|
||||||
}
|
}
|
||||||
_, err := executeScript(lib1, ctx, true)
|
_, err := testExecuteScript(lib1, ctx, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("lib1 failed: %v", err)
|
t.Fatalf("lib1 failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -277,7 +277,7 @@ func TestExecuteScript_MultipleLibraries_Accumulate(t *testing.T) {
|
||||||
"def func_b():",
|
"def func_b():",
|
||||||
" print(' ; from b')",
|
" print(' ; from b')",
|
||||||
}
|
}
|
||||||
_, err = executeScript(lib2, ctx, true)
|
_, err = testExecuteScript(lib2, ctx, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("lib2 failed: %v", err)
|
t.Fatalf("lib2 failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -295,7 +295,7 @@ func TestExecuteScript_MultipleLibraries_Accumulate(t *testing.T) {
|
||||||
"func_a()",
|
"func_a()",
|
||||||
"func_b()",
|
"func_b()",
|
||||||
}
|
}
|
||||||
output, err := executeScript(scriptLines, ctx, false)
|
output, err := testExecuteScript(scriptLines, ctx, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("script failed: %v", err)
|
t.Fatalf("script failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -322,7 +322,7 @@ func TestExecuteScript_RegularScript_DoesNotPersist(t *testing.T) {
|
||||||
"local_func()",
|
"local_func()",
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err := executeScript(scriptLines, ctx, false)
|
output, err := testExecuteScript(scriptLines, ctx, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("script failed: %v", err)
|
t.Fatalf("script failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -377,7 +377,7 @@ func TestExecuteMacro_WithLibraryFunction(t *testing.T) {
|
||||||
"def emit_nop():",
|
"def emit_nop():",
|
||||||
" print(' nop')",
|
" print(' nop')",
|
||||||
}
|
}
|
||||||
_, err := executeScript(lib, ctx, true)
|
_, err := testExecuteScript(lib, ctx, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("library failed: %v", err)
|
t.Fatalf("library failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -540,7 +540,7 @@ func TestExecuteScript_LocalVariableExpansion(t *testing.T) {
|
||||||
"print(' inc |counter|')",
|
"print(' inc |counter|')",
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err := executeScript(scriptLines, ctx, false)
|
output, err := testExecuteScript(scriptLines, ctx, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("executeScript failed: %v", err)
|
t.Fatalf("executeScript failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -569,7 +569,7 @@ func TestExecuteScript_Library_GlobalVariableExpansion(t *testing.T) {
|
||||||
" print(' inc |global_counter|')",
|
" print(' inc |global_counter|')",
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := executeScript(libraryLines, ctx, true)
|
_, err := testExecuteScript(libraryLines, ctx, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("library script failed: %v", err)
|
t.Fatalf("library script failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -579,7 +579,7 @@ func TestExecuteScript_Library_GlobalVariableExpansion(t *testing.T) {
|
||||||
"inc_global()",
|
"inc_global()",
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err := executeScript(scriptLines, ctx, false)
|
output, err := testExecuteScript(scriptLines, ctx, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("executeScript failed: %v", err)
|
t.Fatalf("executeScript failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -610,7 +610,7 @@ func TestExecuteScript_Library_VariableExpansionAtDefinitionTime(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Library defined at global scope - |local_var| won't find caller's local
|
// Library defined at global scope - |local_var| won't find caller's local
|
||||||
_, err := executeScript(libraryLines, ctx, true)
|
_, err := testExecuteScript(libraryLines, ctx, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("library script failed: %v", err)
|
t.Fatalf("library script failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -625,7 +625,7 @@ func TestExecuteScript_Library_VariableExpansionAtDefinitionTime(t *testing.T) {
|
||||||
"use_local()",
|
"use_local()",
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err := executeScript(scriptLines, ctx, false)
|
output, err := testExecuteScript(scriptLines, ctx, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("executeScript failed: %v", err)
|
t.Fatalf("executeScript failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -1219,3 +1219,16 @@ func TestAsmAfterVarsWithVariables(t *testing.T) {
|
||||||
t.Errorf("expected ASM block in deferred section")
|
t.Errorf("expected ASM block in deferred section")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// testExecuteScript is a test helper that wraps executeScript with convenient types
|
||||||
|
func testExecuteScript(scriptLines []string, ctx *CompilerContext, isLibrary bool) ([]string, error) {
|
||||||
|
lines := make([]preproc.Line, len(scriptLines))
|
||||||
|
for i, text := range scriptLines {
|
||||||
|
lines[i] = preproc.Line{
|
||||||
|
Text: text,
|
||||||
|
Filename: "test.c65",
|
||||||
|
LineNo: i + 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return executeScript(lines, ctx, isLibrary)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,11 @@ import (
|
||||||
|
|
||||||
// ScriptMacro represents a named, parameterized script macro
|
// ScriptMacro represents a named, parameterized script macro
|
||||||
type ScriptMacro struct {
|
type ScriptMacro struct {
|
||||||
Name string // macro name
|
Name string // macro name
|
||||||
Params []string // parameter names
|
Params []string // parameter names
|
||||||
Body []string // Starlark code lines (the macro body)
|
Body []string // Starlark code lines (the macro body)
|
||||||
|
SourceFile string // source file where macro is defined
|
||||||
|
StartLine int // 1-based line number in source file of first body line
|
||||||
}
|
}
|
||||||
|
|
||||||
// CompilerContext holds all shared resources needed by commands during compilation
|
// CompilerContext holds all shared resources needed by commands during compilation
|
||||||
|
|
|
||||||
|
|
@ -2,29 +2,195 @@ package compiler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"c65gm/internal/preproc"
|
||||||
"c65gm/internal/utils"
|
"c65gm/internal/utils"
|
||||||
|
|
||||||
"go.starlark.net/lib/math"
|
"go.starlark.net/lib/math"
|
||||||
"go.starlark.net/starlark"
|
"go.starlark.net/starlark"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// mapStarlarkLine maps a Starlark 1-based line number to an index into scriptLines
|
||||||
|
// (a slice of preproc.Line). Returns -1 if the line cannot be mapped.
|
||||||
|
// For non-library scripts, the Starlark source is:
|
||||||
|
//
|
||||||
|
// Line 1: def _main():
|
||||||
|
// Line 2..N+1: indented script lines
|
||||||
|
// Line N+2: _main()
|
||||||
|
//
|
||||||
|
// For library scripts, lines are used as-is.
|
||||||
|
func mapStarlarkLine(starlarkLine int, numScriptLines int, isLibrary bool) int {
|
||||||
|
var idx int
|
||||||
|
if isLibrary {
|
||||||
|
idx = starlarkLine - 1
|
||||||
|
} else {
|
||||||
|
idx = starlarkLine - 2
|
||||||
|
}
|
||||||
|
if idx < 0 || idx >= numScriptLines {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return idx
|
||||||
|
}
|
||||||
|
|
||||||
|
// starlarkPosition extracts the source position from a Starlark error.
|
||||||
|
// Returns the 1-based line number (0 if unknown).
|
||||||
|
func starlarkPosition(err error) int {
|
||||||
|
var evalErr *starlark.EvalError
|
||||||
|
if errors.As(err, &evalErr) {
|
||||||
|
if len(evalErr.CallStack) > 0 {
|
||||||
|
top := evalErr.CallStack.At(0)
|
||||||
|
return int(top.Pos.Line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// starlarkErrorMsg extracts just the message from a Starlark error.
|
||||||
|
func starlarkErrorMsg(err error) string {
|
||||||
|
var evalErr *starlark.EvalError
|
||||||
|
if errors.As(err, &evalErr) {
|
||||||
|
return evalErr.Msg
|
||||||
|
}
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// printScriptErrorContext prints a Starlark error with script-block-bounded source context,
|
||||||
|
// synthesizing SCRIPT/ENDSCRIPT boundary lines that the preprocessor discards.
|
||||||
|
// When the original Starlark error has a call stack with frames beyond the script block
|
||||||
|
// (e.g. a library function), those frames are shown as a backtrace below the source context.
|
||||||
|
func printScriptErrorContext(err error, errMsg string, scriptLines []preproc.Line, errorIdx int, blockType string) {
|
||||||
|
if len(scriptLines) == 0 || errorIdx < 0 || errorIdx >= len(scriptLines) {
|
||||||
|
fmt.Fprintf(os.Stderr, "\nError: Starlark error: %s\n\n", errMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
line := scriptLines[errorIdx]
|
||||||
|
filename := line.Filename
|
||||||
|
|
||||||
|
const contextLines = 3
|
||||||
|
|
||||||
|
fmt.Fprintf(os.Stderr, "\nError: Starlark error: %s\n", errMsg)
|
||||||
|
fmt.Fprintf(os.Stderr, " --> %s:%d\n\n", filename, line.LineNo)
|
||||||
|
|
||||||
|
startIdx := errorIdx - contextLines
|
||||||
|
if startIdx < 0 {
|
||||||
|
startIdx = 0
|
||||||
|
}
|
||||||
|
endIdx := errorIdx + contextLines
|
||||||
|
if endIdx >= len(scriptLines) {
|
||||||
|
endIdx = len(scriptLines) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
scriptMarkerLineNo := scriptLines[0].LineNo - 1
|
||||||
|
endScriptLineNo := scriptLines[len(scriptLines)-1].LineNo + 1
|
||||||
|
|
||||||
|
maxLineNo := endScriptLineNo
|
||||||
|
if scriptLines[endIdx].LineNo > maxLineNo {
|
||||||
|
maxLineNo = scriptLines[endIdx].LineNo
|
||||||
|
}
|
||||||
|
if scriptMarkerLineNo > maxLineNo {
|
||||||
|
maxLineNo = scriptMarkerLineNo
|
||||||
|
}
|
||||||
|
lineNumWidth := len(fmt.Sprintf("%d", maxLineNo))
|
||||||
|
|
||||||
|
if startIdx == 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, " %*d | %s\n", lineNumWidth, scriptMarkerLineNo, blockType)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := startIdx; i <= endIdx; i++ {
|
||||||
|
l := scriptLines[i]
|
||||||
|
marker := " "
|
||||||
|
if i == errorIdx {
|
||||||
|
marker = ">> "
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "%s%*d | %s\n", marker, lineNumWidth, l.LineNo, l.Text)
|
||||||
|
}
|
||||||
|
|
||||||
|
if endIdx == len(scriptLines)-1 {
|
||||||
|
fmt.Fprintf(os.Stderr, " %*d | END%s\n", lineNumWidth, endScriptLineNo, blockType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show call stack if the error has additional frames beyond the script block
|
||||||
|
printStarlarkBacktrace(err, line.LineNo, filename)
|
||||||
|
|
||||||
|
fmt.Fprintf(os.Stderr, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// printStarlarkBacktrace prints a concise call stack from a Starlark error,
|
||||||
|
// filtering out frames from the given scriptBlockLine and <toplevel>/_main wrappers.
|
||||||
|
func printStarlarkBacktrace(err error, scriptBlockLine int, scriptFile string) {
|
||||||
|
var evalErr *starlark.EvalError
|
||||||
|
if !errors.As(err, &evalErr) || len(evalErr.CallStack) < 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var frames []string
|
||||||
|
for _, cf := range evalErr.CallStack {
|
||||||
|
name := cf.Name
|
||||||
|
pos := cf.Pos
|
||||||
|
// Skip frames that are already shown in the bounded source context
|
||||||
|
if name == "<toplevel>" || name == "_main" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if int(pos.Line) == scriptBlockLine && pos.Filename() == scriptFile {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if pos.Filename() != "" && int(pos.Line) > 0 {
|
||||||
|
frames = append(frames, fmt.Sprintf(" %s: in %s", pos.String(), name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(frames) > 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Call stack:\n%s\n", strings.Join(frames, "\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// printScriptErrorFallback prints what we can from a Starlark error when
|
||||||
|
// we couldn't map it to a specific source line within a script block.
|
||||||
|
func printScriptErrorFallback(err error) {
|
||||||
|
fmt.Fprintf(os.Stderr, "\nError: Starlark error: %s\n", err)
|
||||||
|
var evalErr *starlark.EvalError
|
||||||
|
if errors.As(err, &evalErr) && len(evalErr.CallStack) > 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Call stack:\n")
|
||||||
|
for _, cf := range evalErr.CallStack {
|
||||||
|
name := cf.Name
|
||||||
|
pos := cf.Pos
|
||||||
|
if pos.Filename() != "" && int(pos.Line) > 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, " %s: in %s\n", pos.String(), name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
// executeScript runs a Starlark script and returns the output lines.
|
// executeScript runs a Starlark script and returns the output lines.
|
||||||
// If isLibrary is true, the script is executed at top level (no _main wrapper)
|
// If isLibrary is true, the script is executed at top level (no _main wrapper)
|
||||||
// and resulting globals are persisted to ctx.ScriptLibraryGlobals.
|
// and resulting globals are persisted to ctx.ScriptLibraryGlobals.
|
||||||
func executeScript(scriptLines []string, ctx *CompilerContext, isLibrary bool) ([]string, error) {
|
func executeScript(scriptLines []preproc.Line, ctx *CompilerContext, isLibrary bool) ([]string, error) {
|
||||||
|
// Extract text from preproc.Lines
|
||||||
|
texts := make([]string, len(scriptLines))
|
||||||
|
for i, l := range scriptLines {
|
||||||
|
texts[i] = l.Text
|
||||||
|
}
|
||||||
|
|
||||||
// Join script lines
|
// Join script lines
|
||||||
scriptText := strings.Join(scriptLines, "\n")
|
scriptText := strings.Join(texts, "\n")
|
||||||
|
|
||||||
// Expand |varname| -> actual variable names
|
// Expand |varname| -> actual variable names
|
||||||
scriptText = expandVariables(scriptText, ctx)
|
scriptText = expandVariables(scriptText, ctx)
|
||||||
|
|
||||||
|
// Determine the source filename for Starlark
|
||||||
|
sourceFile := scriptLines[0].Filename
|
||||||
|
|
||||||
var finalScript string
|
var finalScript string
|
||||||
|
var starlarkFilename string
|
||||||
if isLibrary {
|
if isLibrary {
|
||||||
// LIBRARY: execute at top level so defs become globals
|
// LIBRARY: execute at top level so defs become globals
|
||||||
finalScript = scriptText
|
finalScript = scriptText
|
||||||
|
starlarkFilename = sourceFile
|
||||||
} else {
|
} else {
|
||||||
// Regular SCRIPT: wrap in function (Starlark requires control flow inside functions)
|
// Regular SCRIPT: wrap in function (Starlark requires control flow inside functions)
|
||||||
finalScript = "def _main():\n"
|
finalScript = "def _main():\n"
|
||||||
|
|
@ -32,6 +198,7 @@ func executeScript(scriptLines []string, ctx *CompilerContext, isLibrary bool) (
|
||||||
finalScript += " " + line + "\n"
|
finalScript += " " + line + "\n"
|
||||||
}
|
}
|
||||||
finalScript += "_main()\n"
|
finalScript += "_main()\n"
|
||||||
|
starlarkFilename = sourceFile
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture print output
|
// Capture print output
|
||||||
|
|
@ -55,9 +222,25 @@ func executeScript(scriptLines []string, ctx *CompilerContext, isLibrary bool) (
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
globals, err := starlark.ExecFile(thread, "script.star", finalScript, predeclared)
|
globals, err := starlark.ExecFile(thread, starlarkFilename, finalScript, predeclared)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
// Map Starlark error position back to source
|
||||||
|
starLine := starlarkPosition(err)
|
||||||
|
if starLine > 0 {
|
||||||
|
idx := mapStarlarkLine(starLine, len(scriptLines), isLibrary)
|
||||||
|
if idx >= 0 {
|
||||||
|
msg := starlarkErrorMsg(err)
|
||||||
|
blockType := "SCRIPT"
|
||||||
|
if isLibrary {
|
||||||
|
blockType = "SCRIPT LIBRARY"
|
||||||
|
}
|
||||||
|
printScriptErrorContext(err, msg, scriptLines, idx, blockType)
|
||||||
|
return nil, fmt.Errorf("Starlark error: %s", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback: print whatever info we can extract
|
||||||
|
printScriptErrorFallback(err)
|
||||||
|
return nil, fmt.Errorf("Starlark error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// For LIBRARY: persist new globals (functions, variables defined at top level)
|
// For LIBRARY: persist new globals (functions, variables defined at top level)
|
||||||
|
|
@ -130,6 +313,12 @@ func ExecuteMacro(macroName string, args []string, ctx *CompilerContext) ([]stri
|
||||||
}
|
}
|
||||||
finalScript += "_macro()\n"
|
finalScript += "_macro()\n"
|
||||||
|
|
||||||
|
// Use the source file where the macro was defined
|
||||||
|
starlarkFilename := macro.SourceFile
|
||||||
|
if starlarkFilename == "" {
|
||||||
|
starlarkFilename = "macro.star"
|
||||||
|
}
|
||||||
|
|
||||||
// Capture print output
|
// Capture print output
|
||||||
var output bytes.Buffer
|
var output bytes.Buffer
|
||||||
thread := &starlark.Thread{
|
thread := &starlark.Thread{
|
||||||
|
|
@ -154,9 +343,19 @@ func ExecuteMacro(macroName string, args []string, ctx *CompilerContext) ([]stri
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
_, err := starlark.ExecFile(thread, "macro.star", finalScript, predeclared)
|
_, err := starlark.ExecFile(thread, starlarkFilename, finalScript, predeclared)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
// Map error position back to macro definition site
|
||||||
|
starLine := starlarkPosition(err)
|
||||||
|
if starLine > 0 && macro.SourceFile != "" {
|
||||||
|
idx := mapStarlarkLine(starLine, len(macro.Body), false) // macro is always wrapped
|
||||||
|
if idx >= 0 {
|
||||||
|
sourceLine := macro.StartLine + idx
|
||||||
|
msg := starlarkErrorMsg(err)
|
||||||
|
return nil, fmt.Errorf("Starlark error: at %s:%d: %s", macro.SourceFile, sourceLine, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("Starlark error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split output into lines
|
// Split output into lines
|
||||||
|
|
|
||||||
439
internal/compiler/scriptexec_test.go
Normal file
439
internal/compiler/scriptexec_test.go
Normal file
|
|
@ -0,0 +1,439 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue