Mako/parser/tests/breakexit_test.go

429 lines
10 KiB
Go

package parser_test
import (
"strings"
"testing"
"git.sharkk.net/Sharkk/Mako/parser"
)
func TestBreakStatement(t *testing.T) {
tests := []struct {
input string
desc string
}{
{"break", "standalone break"},
{"while true do\nbreak\nend", "break in while loop"},
{"for i = 1, 10 do\nbreak\nend", "break in numeric for loop"},
{"for k, v in table do\nbreak\nend", "break in for-in loop"},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
l := parser.NewLexer(tt.input)
p := parser.NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
found := false
for _, stmt := range program.Statements {
if _, ok := stmt.(*parser.BreakStatement); ok {
found = true
break
}
// Check nested statements in loops
if whileStmt, ok := stmt.(*parser.WhileStatement); ok {
for _, bodyStmt := range whileStmt.Body {
if _, ok := bodyStmt.(*parser.BreakStatement); ok {
found = true
break
}
}
}
if forStmt, ok := stmt.(*parser.ForStatement); ok {
for _, bodyStmt := range forStmt.Body {
if _, ok := bodyStmt.(*parser.BreakStatement); ok {
found = true
break
}
}
}
if forInStmt, ok := stmt.(*parser.ForInStatement); ok {
for _, bodyStmt := range forInStmt.Body {
if _, ok := bodyStmt.(*parser.BreakStatement); ok {
found = true
break
}
}
}
}
if !found {
t.Error("expected to find BreakStatement")
}
})
}
}
func TestBreakStringRepresentation(t *testing.T) {
tests := []struct {
input string
expected string
desc string
}{
{"break", "break", "simple break"},
{"while true do\nbreak\nend", "while true do\n\tbreak\nend", "break in while"},
{"for i = 1, 10 do\necho i\nbreak\nend", "for i = 1.00, 10.00 do\n\techo i\n\tbreak\nend", "break with other statements"},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
l := parser.NewLexer(tt.input)
p := parser.NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
result := strings.TrimSpace(program.String())
if result != tt.expected {
t.Errorf("expected %q, got %q", tt.expected, result)
}
})
}
}
func TestExitStatement(t *testing.T) {
tests := []struct {
input string
hasValue bool
desc string
}{
{"exit", false, "exit without value"},
{"exit 0", true, "exit with number"},
{"exit 1", true, "exit with error code"},
{"exit \"success\"", true, "exit with string"},
{"exit x", true, "exit with variable"},
{"exit 1 + 2", true, "exit with expression"},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
l := parser.NewLexer(tt.input)
p := parser.NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 1 {
t.Fatalf("expected 1 statement, got %d", len(program.Statements))
}
stmt, ok := program.Statements[0].(*parser.ExitStatement)
if !ok {
t.Fatalf("expected ExitStatement, got %T", program.Statements[0])
}
if tt.hasValue && stmt.Value == nil {
t.Error("expected exit value but got nil")
}
if !tt.hasValue && stmt.Value != nil {
t.Error("expected no exit value but got one")
}
})
}
}
func TestExitStringRepresentation(t *testing.T) {
tests := []struct {
input string
expected string
desc string
}{
{"exit", "exit", "simple exit"},
{"exit 0", "exit 0.00", "exit with number"},
{"exit \"message\"", "exit \"message\"", "exit with string"},
{"exit x + 1", "exit (x + 1.00)", "exit with expression"},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
l := parser.NewLexer(tt.input)
p := parser.NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
result := strings.TrimSpace(program.String())
if result != tt.expected {
t.Errorf("expected %q, got %q", tt.expected, result)
}
})
}
}
func TestNestedBreakInComplexLoops(t *testing.T) {
input := `for i = 1, 10 do
while condition do
if found then
break
end
x = x + 1
end
for k, v in data do
if v == target then
break
end
end
end`
l := parser.NewLexer(input)
p := parser.NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 1 {
t.Fatalf("expected 1 statement, got %d", len(program.Statements))
}
outerFor, ok := program.Statements[0].(*parser.ForStatement)
if !ok {
t.Fatalf("expected ForStatement, got %T", program.Statements[0])
}
if len(outerFor.Body) != 2 {
t.Fatalf("expected 2 body statements, got %d", len(outerFor.Body))
}
// First: while loop with break
whileStmt, ok := outerFor.Body[0].(*parser.WhileStatement)
if !ok {
t.Fatalf("expected WhileStatement, got %T", outerFor.Body[0])
}
// Check that while body contains if with break
ifStmt, ok := whileStmt.Body[0].(*parser.IfStatement)
if !ok {
t.Fatalf("expected IfStatement in while body, got %T", whileStmt.Body[0])
}
_, ok = ifStmt.Body[0].(*parser.BreakStatement)
if !ok {
t.Fatalf("expected BreakStatement in if body, got %T", ifStmt.Body[0])
}
// Second: for-in loop with break
forInStmt, ok := outerFor.Body[1].(*parser.ForInStatement)
if !ok {
t.Fatalf("expected ForInStatement, got %T", outerFor.Body[1])
}
// Check that for-in body contains if with break
ifStmt2, ok := forInStmt.Body[0].(*parser.IfStatement)
if !ok {
t.Fatalf("expected IfStatement in for-in body, got %T", forInStmt.Body[0])
}
_, ok = ifStmt2.Body[0].(*parser.BreakStatement)
if !ok {
t.Fatalf("expected BreakStatement in if body, got %T", ifStmt2.Body[0])
}
}
func TestExitInMixedProgram(t *testing.T) {
input := `x = 42
if x > 50 then
exit 1
else
echo "continuing"
end
y = "done"
exit "success"`
l := parser.NewLexer(input)
p := parser.NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 4 {
t.Fatalf("expected 4 statements, got %d", len(program.Statements))
}
// First: assignment
_, ok := program.Statements[0].(*parser.Assignment)
if !ok {
t.Fatalf("statement 0: expected Assignment, got %T", program.Statements[0])
}
// Second: if statement with exit in body
ifStmt, ok := program.Statements[1].(*parser.IfStatement)
if !ok {
t.Fatalf("statement 1: expected IfStatement, got %T", program.Statements[1])
}
_, ok = ifStmt.Body[0].(*parser.ExitStatement)
if !ok {
t.Fatalf("if body: expected ExitStatement, got %T", ifStmt.Body[0])
}
// Third: assignment
_, ok = program.Statements[2].(*parser.Assignment)
if !ok {
t.Fatalf("statement 2: expected AssignStatement, got %T", program.Statements[2])
}
// Fourth: final exit
_, ok = program.Statements[3].(*parser.ExitStatement)
if !ok {
t.Fatalf("statement 3: expected ExitStatement, got %T", program.Statements[3])
}
}
func TestExitWithComplexExpressions(t *testing.T) {
tests := []struct {
input string
desc string
}{
{"exit table.errorCode", "exit with member access"},
{"exit arr[0]", "exit with index access"},
{"exit status + 1", "exit with arithmetic"},
{"exit {code = 1, msg = \"error\"}", "exit with table"},
{"exit obj.nested.value", "exit with chained access"},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
l := parser.NewLexer(tt.input)
p := parser.NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 1 {
t.Fatalf("expected 1 statement, got %d", len(program.Statements))
}
stmt, ok := program.Statements[0].(*parser.ExitStatement)
if !ok {
t.Fatalf("expected ExitStatement, got %T", program.Statements[0])
}
if stmt.Value == nil {
t.Error("expected exit value but got nil")
}
})
}
}
func TestControlFlowErrors(t *testing.T) {
tests := []struct {
input string
expectedError string
desc string
}{
{"break x", "unexpected identifier", "break with argument"},
{"exit +", "unexpected token '+'", "exit with invalid expression"},
{"exit (", "unexpected end of input", "exit with incomplete expression"},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
l := parser.NewLexer(tt.input)
p := parser.NewParser(l)
p.ParseProgram()
if !p.HasErrors() {
t.Fatal("expected parsing errors")
}
errors := p.Errors()
found := false
for _, err := range errors {
if strings.Contains(err.Message, tt.expectedError) {
found = true
break
}
}
if !found {
errorMsgs := make([]string, len(errors))
for i, err := range errors {
errorMsgs[i] = err.Message
}
t.Errorf("expected error containing %q, got %v", tt.expectedError, errorMsgs)
}
})
}
}
func TestProgramExitCode(t *testing.T) {
input := `x = 42
echo x`
l := parser.NewLexer(input)
p := parser.NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
// Program should have default exit code of 0
if program.ExitCode != 0 {
t.Errorf("expected default exit code 0, got %d", program.ExitCode)
}
}
func TestBreakAndExitInSameProgram(t *testing.T) {
input := `for i = 1, 100 do
if i == 10 then
break
end
if i > 50 then
exit 1
end
echo i
end
exit "completed"`
l := parser.NewLexer(input)
p := parser.NewParser(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 2 {
t.Fatalf("expected 2 statements, got %d", len(program.Statements))
}
// First: for loop with break and exit
forStmt, ok := program.Statements[0].(*parser.ForStatement)
if !ok {
t.Fatalf("statement 0: expected ForStatement, got %T", program.Statements[0])
}
if len(forStmt.Body) != 3 {
t.Fatalf("expected 3 body statements, got %d", len(forStmt.Body))
}
// Check for break in first if
if1, ok := forStmt.Body[0].(*parser.IfStatement)
if !ok {
t.Fatalf("body[0]: expected IfStatement, got %T", forStmt.Body[0])
}
_, ok = if1.Body[0].(*parser.BreakStatement)
if !ok {
t.Fatalf("if1 body: expected BreakStatement, got %T", if1.Body[0])
}
// Check for exit in second if
if2, ok := forStmt.Body[1].(*parser.IfStatement)
if !ok {
t.Fatalf("body[1]: expected IfStatement, got %T", forStmt.Body[1])
}
_, ok = if2.Body[0].(*parser.ExitStatement)
if !ok {
t.Fatalf("if2 body: expected ExitStatement, got %T", if2.Body[0])
}
// Second: final exit
_, ok = program.Statements[1].(*parser.ExitStatement)
if !ok {
t.Fatalf("statement 1: expected ExitStatement, got %T", program.Statements[1])
}
}