c65gm/internal/compiler/scriptexec_test.go

844 lines
23 KiB
Go

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)
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)
}
}
// --- 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")
}
}
// --- 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])
}
}