From c2fc11bbeb72bf531dda2dce95d537542d90ea02 Mon Sep 17 00:00:00 2001 From: Jooris Hadeler Date: Wed, 22 Apr 2026 21:52:31 +0200 Subject: [PATCH] feat: replace end2end test and add more test --- e2e.sh | 116 ----------------- runner.py | 119 ++++++++++++++++++ tests/{assign.src => assign.test} | 4 + tests/booleans.src | 7 -- tests/{booleans.c => booleans.test} | 13 ++ tests/cond_assign.test | 21 ++++ tests/cond_else_if.test | 17 +++ tests/cond_nested.test | 18 +++ tests/cond_no_else.test | 17 +++ tests/{fibonacci.src => fibonacci.test} | 4 + tests/if-else.c | 9 -- tests/if-else.src | 7 -- tests/if-else.test | 22 ++++ tests/{let_stmt.src => let_stmt.test} | 4 + tests/{return_42.src => return_42.test} | 4 + .../{return_neg_69.src => return_neg_69.test} | 4 + ...continue.src => while_break_continue.test} | 4 + 17 files changed, 251 insertions(+), 139 deletions(-) delete mode 100755 e2e.sh create mode 100755 runner.py rename tests/{assign.src => assign.test} (70%) delete mode 100644 tests/booleans.src rename tests/{booleans.c => booleans.test} (64%) create mode 100644 tests/cond_assign.test create mode 100644 tests/cond_else_if.test create mode 100644 tests/cond_nested.test create mode 100644 tests/cond_no_else.test rename tests/{fibonacci.src => fibonacci.test} (85%) delete mode 100644 tests/if-else.c delete mode 100644 tests/if-else.src create mode 100644 tests/if-else.test rename tests/{let_stmt.src => let_stmt.test} (88%) rename tests/{return_42.src => return_42.test} (55%) rename tests/{return_neg_69.src => return_neg_69.test} (50%) rename tests/{while_break_continue.src => while_break_continue.test} (87%) diff --git a/e2e.sh b/e2e.sh deleted file mode 100755 index e233634..0000000 --- a/e2e.sh +++ /dev/null @@ -1,116 +0,0 @@ -#!/bin/bash - -# Exit immediately if a command exits with a non-zero status. -set -e - -# --- Helper Function --- -run_test() { - local src_file="$1" - local expected_code="$2" - local base_name - base_name=$(basename "$src_file" .src) - local obj_file="tests/$base_name.o" - local exec_file="tests/$base_name" - - echo "--- Running test: $src_file ---" - - # 1. Compile the source file using our compiler - echo " [1/4] Compiling..." - cargo run --release -- "$src_file" -o "$obj_file" > /dev/null 2>&1 - - # 2. Link the object file with the system linker (gcc) - echo " [2/4] Linking with gcc..." - gcc "$obj_file" -o "$exec_file" - - # 3. Run the executable - echo " [3/4] Running..." - set +e - ./"$exec_file" - local actual_code=$? - set -e - - # 4. Check the exit code - echo " [4/4] Verifying exit code..." - if [ "$actual_code" -eq "$expected_code" ]; then - echo "SUCCESS: Exit code is $actual_code as expected." - else - echo "FAILURE: Expected exit code $expected_code, but got $actual_code." - exit 1 - fi - - # Clean up generated files - rm "$obj_file" "$exec_file" - echo "------------------------------------" - echo -} - -# --- Helper Function --- -run_harness_test() { - local src_file="$1" - local harness_file="$2" - local expected_code="$3" - local base_name - base_name=$(basename "$src_file" .src) - local obj_file="tests/$base_name.o" - local exec_file="tests/$base_name" - - echo "--- Running test: $src_file ---" - - # 1. Compile the source file using our compiler - echo " [1/4] Compiling..." - cargo run --release -- "$src_file" -o "$obj_file" > /dev/null 2>&1 - - # 2. Link the object file with the system linker (gcc) - echo " [2/4] Linking with gcc..." - gcc "$obj_file" "$harness_file" -o "$exec_file" - - # 3. Run the executable - echo " [3/4] Running..." - set +e - ./"$exec_file" - local actual_code=$? - set -e - - # 4. Check the exit code - echo " [4/4] Verifying exit code..." - if [ "$actual_code" -eq "$expected_code" ]; then - echo "SUCCESS: Exit code is $actual_code as expected." - else - echo "FAILURE: Expected exit code $expected_code, but got $actual_code." - exit 1 - fi - - # Clean up generated files - rm "$obj_file" "$exec_file" - echo "------------------------------------" - echo -} - -# --- Test Cases --- - -# Test a simple positive return value. -run_test "tests/return_42.src" 42 - -# Test a negative return value. Shell exit codes are unsigned 8-bit integers, -# so -69 (i8) wraps around to 187. -run_test "tests/return_neg_69.src" 187 - -# Test boolean operations. -run_harness_test "tests/booleans.src" "tests/booleans.c" 0 - -# Test if and else statements. -run_harness_test "tests/if-else.src" "tests/if-else.c" 0 - -# Test variable declarations and scoping. -run_test "tests/let_stmt.src" 30 - -# Test assignments and multi-assignments -run_test "tests/assign.src" 42 - -# Test while loops -run_test "tests/fibonacci.src" 55 - -# Test break and continue inside while loops -run_test "tests/while_break_continue.src" 16 - -echo "All end-to-end tests passed!" \ No newline at end of file diff --git a/runner.py b/runner.py new file mode 100755 index 0000000..a9b43f3 --- /dev/null +++ b/runner.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +import os +import sys +import subprocess +import tempfile +from pathlib import Path + +def run_test(test_file: Path, compiler_bin: Path): + with open(test_file, "r", encoding="utf-8") as f: + lines = f.readlines() + + expected_code = 0 + code_lines = [] + harness_lines = None + current_section = None + + for line in lines: + trimmed = line.strip() + if trimmed.startswith("[") and trimmed.endswith("]"): + current_section = trimmed[1:-1] + if current_section == "harness": + harness_lines = [] + elif current_section: + if current_section == "expected_return_code": + if trimmed: + expected_code = int(trimmed) + elif current_section == "code": + code_lines.append(line) + elif current_section == "harness": + harness_lines.append(line) + + code = "".join(code_lines) + harness = "".join(harness_lines) if harness_lines is not None else None + + with tempfile.TemporaryDirectory(prefix=f"compiler_test_{test_file.stem}_") as temp_dir_name: + temp_dir = Path(temp_dir_name) + + code_path = temp_dir / "code.src" + obj_path = temp_dir / "code.o" + exec_path = temp_dir / "exec" + + code_path.write_text(code, encoding="utf-8") + + # 1. Invoke the custom compiler + comp_res = subprocess.run( + [str(compiler_bin), str(code_path), "-o", str(obj_path)], + capture_output=True, + text=True + ) + + if comp_res.returncode != 0: + out = comp_res.stdout.strip() + err = comp_res.stderr.strip() + error_msg = f"{out}\n{err}".strip() or "Compiler failed with no output" + raise Exception(error_msg) + + # 2. Invoke GCC to link the object file (and compile harness if provided) + gcc_cmd = ["gcc", str(obj_path), "-o", str(exec_path)] + if harness is not None: + harness_path = temp_dir / "harness.c" + harness_path.write_text(harness, encoding="utf-8") + gcc_cmd.append(str(harness_path)) + + gcc_res = subprocess.run(gcc_cmd, capture_output=True, text=True) + + if gcc_res.returncode != 0: + out = gcc_res.stdout.strip() + err = gcc_res.stderr.strip() + error_msg = f"{out}\n{err}".strip() or "GCC failed with no output" + raise Exception(error_msg) + + # 3. Run the resulting executable + exec_res = subprocess.run([str(exec_path)], capture_output=True, text=True) + + if exec_res.returncode != expected_code: + raise Exception(f"Expected return code {expected_code}, but got {exec_res.returncode}") + +def main(): + GREEN = '\033[92m' + RED = '\033[91m' + RESET = '\033[0m' + + project_root = Path(__file__).resolve().parent + tests_dir = project_root / "tests" + compiler_bin = project_root / "target" / "debug" / "compiler" + + # Automatically run cargo build to ensure the compiler is compiled and up to date + build_status = subprocess.run(["cargo", "build"], cwd=project_root) + if build_status.returncode != 0: + print("Failed to build the compiler binary.", file=sys.stderr) + sys.exit(1) + + test_files = sorted(tests_dir.glob("*.test")) + + all_passed = True + failed_tests = [] + + for test_file in test_files: + test_name = test_file.name + + # Format the output to align results, padding with dots + msg = f"Running {test_name} " + print(f"{msg:.<50} ", end="", file=sys.stderr, flush=True) + + try: + run_test(test_file, compiler_bin) + print(f"{GREEN}ok{RESET}", file=sys.stderr) + except Exception as e: + print(f"{RED}FAIL{RESET}", file=sys.stderr) + print(f" Reason: {e}", file=sys.stderr) + all_passed = False + failed_tests.append(test_name) + + if not all_passed: + print(f"\nOne or more E2E tests failed: {failed_tests}", file=sys.stderr) + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/assign.src b/tests/assign.test similarity index 70% rename from tests/assign.src rename to tests/assign.test index f6fb5c2..665b603 100644 --- a/tests/assign.src +++ b/tests/assign.test @@ -1,3 +1,7 @@ +[expected_return_code] +42 + +[code] fn main() -> i32 { let a = 0; let b = 0; diff --git a/tests/booleans.src b/tests/booleans.src deleted file mode 100644 index d3b41b1..0000000 --- a/tests/booleans.src +++ /dev/null @@ -1,7 +0,0 @@ -fn eq(a: i32, b: i32) -> bool { return a == b; } -fn neq(a: i32, b: i32) -> bool { return a != b; } -fn lt(a: i32, b: i32) -> bool { return a < b; } -fn lte(a: i32, b: i32) -> bool { return a <= b; } -fn gt(a: i32, b: i32) -> bool { return a > b; } -fn gte(a: i32, b: i32) -> bool { return a >= b; } -fn not_bool(a: bool) -> bool { return !a; } \ No newline at end of file diff --git a/tests/booleans.c b/tests/booleans.test similarity index 64% rename from tests/booleans.c rename to tests/booleans.test index 54ee56d..7bac73b 100644 --- a/tests/booleans.c +++ b/tests/booleans.test @@ -1,3 +1,16 @@ +[expected_return_code] +0 + +[code] +fn eq(a: i32, b: i32) -> bool { return a == b; } +fn neq(a: i32, b: i32) -> bool { return a != b; } +fn lt(a: i32, b: i32) -> bool { return a < b; } +fn lte(a: i32, b: i32) -> bool { return a <= b; } +fn gt(a: i32, b: i32) -> bool { return a > b; } +fn gte(a: i32, b: i32) -> bool { return a >= b; } +fn not_bool(a: bool) -> bool { return !a; } + +[harness] #include extern bool eq(int a, int b); diff --git a/tests/cond_assign.test b/tests/cond_assign.test new file mode 100644 index 0000000..3416596 --- /dev/null +++ b/tests/cond_assign.test @@ -0,0 +1,21 @@ +[expected_return_code] +15 + +[code] +fn main() -> i32 { + let val = 0; + + if true { + val = 10; + } else { + val = 20; + } + + if false { + val = val + 100; + } else { + val = val + 5; + } + + return val; +} \ No newline at end of file diff --git a/tests/cond_else_if.test b/tests/cond_else_if.test new file mode 100644 index 0000000..929b821 --- /dev/null +++ b/tests/cond_else_if.test @@ -0,0 +1,17 @@ +[expected_return_code] +7 + +[code] +fn main() -> i32 { + let n = 4; + + if n == 1 { + return 10; + } else if n == 2 { + return 20; + } else if n == 3 { + return 30; + } else { + return 7; + } +} \ No newline at end of file diff --git a/tests/cond_nested.test b/tests/cond_nested.test new file mode 100644 index 0000000..8639ca3 --- /dev/null +++ b/tests/cond_nested.test @@ -0,0 +1,18 @@ +[expected_return_code] +42 + +[code] +fn main() -> i32 { + let x = 5; + let y = 10; + + if x > 0 { + if y < 5 { + return 1; + } else { + return 42; + } + } else { + return 2; + } +} \ No newline at end of file diff --git a/tests/cond_no_else.test b/tests/cond_no_else.test new file mode 100644 index 0000000..49b9e5f --- /dev/null +++ b/tests/cond_no_else.test @@ -0,0 +1,17 @@ +[expected_return_code] +100 + +[code] +fn main() -> i32 { + let a = 10; + + if a == 10 { + a = 100; + } + + if a == 5 { + a = 0; // Should be skipped + } + + return a; +} \ No newline at end of file diff --git a/tests/fibonacci.src b/tests/fibonacci.test similarity index 85% rename from tests/fibonacci.src rename to tests/fibonacci.test index 7ef4e6c..e2c9e37 100644 --- a/tests/fibonacci.src +++ b/tests/fibonacci.test @@ -1,3 +1,7 @@ +[expected_return_code] +55 + +[code] fn main() -> i32 { let n = 10; let a = 0; diff --git a/tests/if-else.c b/tests/if-else.c deleted file mode 100644 index c87d4a7..0000000 --- a/tests/if-else.c +++ /dev/null @@ -1,9 +0,0 @@ -extern int min(int a, int b); - -int main() { - if (min(12, 15)) { - return 0; - } else { - return -1; - } -} \ No newline at end of file diff --git a/tests/if-else.src b/tests/if-else.src deleted file mode 100644 index 49c8ac9..0000000 --- a/tests/if-else.src +++ /dev/null @@ -1,7 +0,0 @@ -fn min(a: i32, b: i32) -> i32 { - if a < b { - return a; - } else { - return b; - } -} \ No newline at end of file diff --git a/tests/if-else.test b/tests/if-else.test new file mode 100644 index 0000000..acc2f9c --- /dev/null +++ b/tests/if-else.test @@ -0,0 +1,22 @@ +[expected_return_code] +0 + +[code] +fn min(a: i32, b: i32) -> i32 { + if a < b { + return a; + } else { + return b; + } +} + +[harness] +extern int min(int a, int b); + +int main() { + if (min(12, 15)) { + return 0; + } else { + return -1; + } +} \ No newline at end of file diff --git a/tests/let_stmt.src b/tests/let_stmt.test similarity index 88% rename from tests/let_stmt.src rename to tests/let_stmt.test index 606612f..3a5d763 100644 --- a/tests/let_stmt.src +++ b/tests/let_stmt.test @@ -1,3 +1,7 @@ +[expected_return_code] +30 + +[code] fn main() -> i32 { let a = 10; let b: i32 = 20; diff --git a/tests/return_42.src b/tests/return_42.test similarity index 55% rename from tests/return_42.src rename to tests/return_42.test index 3f7494b..f2c440d 100644 --- a/tests/return_42.src +++ b/tests/return_42.test @@ -1,3 +1,7 @@ +[expected_return_code] +42 + +[code] fn main() -> i32 { return 10 * 4 + 2; } \ No newline at end of file diff --git a/tests/return_neg_69.src b/tests/return_neg_69.test similarity index 50% rename from tests/return_neg_69.src rename to tests/return_neg_69.test index 2eaeebc..da94c04 100644 --- a/tests/return_neg_69.src +++ b/tests/return_neg_69.test @@ -1,3 +1,7 @@ +[expected_return_code] +187 + +[code] fn main() -> i8 { return -69; } \ No newline at end of file diff --git a/tests/while_break_continue.src b/tests/while_break_continue.test similarity index 87% rename from tests/while_break_continue.src rename to tests/while_break_continue.test index cf48779..545bbf8 100644 --- a/tests/while_break_continue.src +++ b/tests/while_break_continue.test @@ -1,3 +1,7 @@ +[expected_return_code] +16 + +[code] fn main() -> i32 { let i = 0; let sum = 0;