From 97953bda40c2ad57757d92e37c01a9d3ba3b1ad5 Mon Sep 17 00:00:00 2001 From: Mihir Patel Date: Fri, 29 May 2026 15:30:55 +0000 Subject: [PATCH] Lets see if the new testing framework works --- .github/workflows/test.yml | 12 +- README.md | 32 +- .../test_runner.cpython-312-pytest-7.4.4.pyc | Bin 0 -> 7643 bytes main.cpp | 1 + requirements-dev.txt | 1 + src/parser/Parser.cpp | 2 +- .../conftest.cpython-312-pytest-7.4.4.pyc | Bin 0 -> 5076 bytes tests/__pycache__/conftest.cpython-312.pyc | Bin 0 -> 4946 bytes ...test_pipeline.cpython-312-pytest-7.4.4.pyc | Bin 0 -> 20399 bytes .../__pycache__/test_pipeline.cpython-312.pyc | Bin 0 -> 20269 bytes .../compile_fail/syntax/missing_semicolon.zz | 3 + tests/compile_fail/type/bad_assignment.zz | 4 + tests/conftest.py | 102 ++++ .../syntax/missing_semicolon.stderr.contains | 3 + .../compile_fail/type/bad_assignment.exitcode | 1 + .../type/bad_assignment.stderr.contains | 3 + .../conditionals/if-elif-else.exitcode | 1 + .../runtime/conditionals/if-elif-else.stderr | 0 .../runtime/conditionals/if-elif-else.stdout | 1 + .../runtime/conditionals/if-elif.exitcode | 1 + .../runtime/conditionals/if-elif.stderr | 0 .../runtime/conditionals/if-elif.stdout | 0 .../runtime/conditionals/if-else.exitcode | 1 + .../runtime/conditionals/if-else.stderr | 0 .../runtime/conditionals/if-else.stdout | 0 .../expected/runtime/conditionals/if.exitcode | 1 + tests/expected/runtime/conditionals/if.stderr | 0 tests/expected/runtime/conditionals/if.stdout | 0 .../runtime/functions/basics.exitcode | 1 + .../expected/runtime/functions/basics.stderr | 0 .../expected/runtime/functions/basics.stdout | 3 + .../runtime/functions/recursive.exitcode | 1 + .../runtime/functions/recursive.stderr | 0 .../runtime/functions/recursive.stdout | 3 + .../runtime/functions/scopes.exitcode | 1 + .../expected/runtime/functions/scopes.stderr | 0 .../expected/runtime/functions/scopes.stdout | 1 + tests/expected/runtime/loops/control.exitcode | 1 + tests/expected/runtime/loops/control.stderr | 0 tests/expected/runtime/loops/control.stdout | 0 .../runtime/operations/binary.exitcode | 1 + .../expected/runtime/operations/binary.stderr | 0 .../expected/runtime/operations/binary.stdout | 24 + .../runtime/operations/unary.exitcode | 1 + .../expected/runtime/operations/unary.stderr | 0 .../expected/runtime/operations/unary.stdout | 5 + .../expected/runtime/strings/escapes.exitcode | 1 + tests/expected/runtime/strings/escapes.stderr | 0 tests/expected/runtime/strings/escapes.stdout | 0 tests/expected/runtime/types.exitcode | 1 + tests/expected/runtime/types.stderr | 0 tests/expected/runtime/types.stdout | 1 + tests/expected/runtime/variables.exitcode | 1 + tests/expected/runtime/variables.linux.stdout | 1 + tests/expected/runtime/variables.llvm.stdout | 1 + tests/expected/runtime/variables.stderr | 0 tests/expected/runtime/variables.stdout | 1 + .../expected/runtime/variables.windows.stdout | 1 + .../runtime_fail/exit/nonzero.exitcode | 1 + .../expected/runtime_fail/exit/nonzero.stderr | 0 .../expected/runtime_fail/exit/nonzero.stdout | 1 + .../conditionals/if-elif-else.zz | 0 tests/{zz => runtime}/conditionals/if-elif.zz | 0 tests/{zz => runtime}/conditionals/if-else.zz | 0 tests/{zz => runtime}/conditionals/if.zz | 0 tests/{zz => runtime}/functions/basics.zz | 0 tests/{zz => runtime}/functions/recursive.zz | 0 tests/{zz => runtime}/functions/scopes.zz | 0 tests/{zz => runtime}/loops/control.zz | 0 tests/{zz => runtime}/operations/binary.zz | 0 tests/{zz => runtime}/operations/unary.zz | 0 tests/{zz => runtime}/strings/escapes.zz | 0 tests/{zz => runtime}/types.zz | 0 tests/{zz => runtime}/variables.zz | 0 tests/runtime_fail/exit/nonzero.zz | 7 + tests/test_pipeline.py | 568 ++++++++++++++++++ 76 files changed, 782 insertions(+), 14 deletions(-) create mode 100644 __pycache__/test_runner.cpython-312-pytest-7.4.4.pyc create mode 100644 requirements-dev.txt create mode 100644 tests/__pycache__/conftest.cpython-312-pytest-7.4.4.pyc create mode 100644 tests/__pycache__/conftest.cpython-312.pyc create mode 100644 tests/__pycache__/test_pipeline.cpython-312-pytest-7.4.4.pyc create mode 100644 tests/__pycache__/test_pipeline.cpython-312.pyc create mode 100644 tests/compile_fail/syntax/missing_semicolon.zz create mode 100644 tests/compile_fail/type/bad_assignment.zz create mode 100644 tests/conftest.py create mode 100644 tests/expected/compile_fail/syntax/missing_semicolon.stderr.contains create mode 100644 tests/expected/compile_fail/type/bad_assignment.exitcode create mode 100644 tests/expected/compile_fail/type/bad_assignment.stderr.contains create mode 100644 tests/expected/runtime/conditionals/if-elif-else.exitcode create mode 100644 tests/expected/runtime/conditionals/if-elif-else.stderr create mode 100644 tests/expected/runtime/conditionals/if-elif-else.stdout create mode 100644 tests/expected/runtime/conditionals/if-elif.exitcode create mode 100644 tests/expected/runtime/conditionals/if-elif.stderr create mode 100644 tests/expected/runtime/conditionals/if-elif.stdout create mode 100644 tests/expected/runtime/conditionals/if-else.exitcode create mode 100644 tests/expected/runtime/conditionals/if-else.stderr create mode 100644 tests/expected/runtime/conditionals/if-else.stdout create mode 100644 tests/expected/runtime/conditionals/if.exitcode create mode 100644 tests/expected/runtime/conditionals/if.stderr create mode 100644 tests/expected/runtime/conditionals/if.stdout create mode 100644 tests/expected/runtime/functions/basics.exitcode create mode 100644 tests/expected/runtime/functions/basics.stderr create mode 100644 tests/expected/runtime/functions/basics.stdout create mode 100644 tests/expected/runtime/functions/recursive.exitcode create mode 100644 tests/expected/runtime/functions/recursive.stderr create mode 100644 tests/expected/runtime/functions/recursive.stdout create mode 100644 tests/expected/runtime/functions/scopes.exitcode create mode 100644 tests/expected/runtime/functions/scopes.stderr create mode 100644 tests/expected/runtime/functions/scopes.stdout create mode 100644 tests/expected/runtime/loops/control.exitcode create mode 100644 tests/expected/runtime/loops/control.stderr create mode 100644 tests/expected/runtime/loops/control.stdout create mode 100644 tests/expected/runtime/operations/binary.exitcode create mode 100644 tests/expected/runtime/operations/binary.stderr create mode 100644 tests/expected/runtime/operations/binary.stdout create mode 100644 tests/expected/runtime/operations/unary.exitcode create mode 100644 tests/expected/runtime/operations/unary.stderr create mode 100644 tests/expected/runtime/operations/unary.stdout create mode 100644 tests/expected/runtime/strings/escapes.exitcode create mode 100644 tests/expected/runtime/strings/escapes.stderr create mode 100644 tests/expected/runtime/strings/escapes.stdout create mode 100644 tests/expected/runtime/types.exitcode create mode 100644 tests/expected/runtime/types.stderr create mode 100644 tests/expected/runtime/types.stdout create mode 100644 tests/expected/runtime/variables.exitcode create mode 100644 tests/expected/runtime/variables.linux.stdout create mode 100644 tests/expected/runtime/variables.llvm.stdout create mode 100644 tests/expected/runtime/variables.stderr create mode 100644 tests/expected/runtime/variables.stdout create mode 100644 tests/expected/runtime/variables.windows.stdout create mode 100644 tests/expected/runtime_fail/exit/nonzero.exitcode create mode 100644 tests/expected/runtime_fail/exit/nonzero.stderr create mode 100644 tests/expected/runtime_fail/exit/nonzero.stdout rename tests/{zz => runtime}/conditionals/if-elif-else.zz (100%) rename tests/{zz => runtime}/conditionals/if-elif.zz (100%) rename tests/{zz => runtime}/conditionals/if-else.zz (100%) rename tests/{zz => runtime}/conditionals/if.zz (100%) rename tests/{zz => runtime}/functions/basics.zz (100%) rename tests/{zz => runtime}/functions/recursive.zz (100%) rename tests/{zz => runtime}/functions/scopes.zz (100%) rename tests/{zz => runtime}/loops/control.zz (100%) rename tests/{zz => runtime}/operations/binary.zz (100%) rename tests/{zz => runtime}/operations/unary.zz (100%) rename tests/{zz => runtime}/strings/escapes.zz (100%) rename tests/{zz => runtime}/types.zz (100%) rename tests/{zz => runtime}/variables.zz (100%) create mode 100644 tests/runtime_fail/exit/nonzero.zz create mode 100644 tests/test_pipeline.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7ae51b1..3a9d353 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,6 +28,9 @@ jobs: sudo apt-get update -y sudo apt-get install -y build-essential llvm clang cmake python3-pip + - name: Install Python Dependencies + run: python3 -m pip install -r requirements-dev.txt + - name: Build zpiler run: | mkdir -p build && cd build @@ -35,7 +38,7 @@ jobs: make -j - name: Run Tests - run: python3 test_runner.py + run: python3 -m pytest -q test-windows: runs-on: windows-latest @@ -58,7 +61,7 @@ jobs: uses: actions/cache@v4 with: path: C:\Users\runneradmin\AppData\Local\pip\Cache - key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }} + key: ${{ runner.os }}-pip-${{ hashFiles('requirements-dev.txt') }} restore-keys: | ${{ runner.os }}-pip- @@ -68,6 +71,9 @@ jobs: - name: Install CMake run: choco install --no-progress -y cmake + - name: Install Python Dependencies + run: python -m pip install -r requirements-dev.txt + - name: Build zpiler shell: cmd run: | @@ -80,4 +86,4 @@ jobs: shell: cmd run: | chcp 65001 - python test_runner.py + python -m pytest -q diff --git a/README.md b/README.md index b2523ee..4a00354 100644 --- a/README.md +++ b/README.md @@ -145,18 +145,24 @@ Generates: `program` (executable) ### Automated Testing -Run all test cases for a specific target: +Install test dependencies: ```bash -# Test native platform (auto-detected) -python test_runner.py +python3 -m pip install -r requirements-dev.txt +``` + +Run strict golden-output tests with `pytest`: +```bash +# Auto-detect native target when TARGET is not set +pytest -q -# Test specific targets -TARGET=linux python test_runner.py # Linux -TARGET=windows python test_runner.py # Windows (if ml64 available) -TARGET=llvm python test_runner.py # LLVM +# Linux + LLVM +TARGET=linux,llvm pytest -q -# Test multiple targets -TARGET=linux,llvm python test_runner.py +# Windows +TARGET=windows pytest -q + +# Update/create runtime goldens explicitly +TARGET=linux pytest -q --bless ``` --- @@ -195,7 +201,13 @@ fn main() { ## 🧪 Testing -You can add `.zz` programs inside a `tests/` folder and run them individually through the pipeline. Type errors, parsing errors, and codegen output are printed to the terminal. +Test suites are directory-based: + +- `tests/runtime`: program must compile, run, and match exact `stdout`/`stderr`/exit code +- `tests/runtime_fail`: program must compile, run, and match exact failing output + nonzero exit code +- `tests/compile_fail`: program must fail compilation with expected exit code and required stderr substrings + +Expected files live under `tests/expected//...` with mirrored paths. --- diff --git a/__pycache__/test_runner.cpython-312-pytest-7.4.4.pyc b/__pycache__/test_runner.cpython-312-pytest-7.4.4.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e112eae8cb823725e8063a6de995366923ee127d GIT binary patch literal 7643 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 LGaW%!D#H3-u>BQ1 literal 0 HcmV?d00001 diff --git a/main.cpp b/main.cpp index d7022c9..e9942db 100644 --- a/main.cpp +++ b/main.cpp @@ -130,6 +130,7 @@ int main(int argc, char *argv[]) { cg->generate(std::move(program)); } catch (std::exception const &exc) { std::cerr << "ERROR: " << exc.what() << "\n"; + return 1; } return 0; diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..40543aa --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1 @@ +pytest==8.3.3 diff --git a/src/parser/Parser.cpp b/src/parser/Parser.cpp index 1686647..b9f6413 100644 --- a/src/parser/Parser.cpp +++ b/src/parser/Parser.cpp @@ -200,7 +200,7 @@ namespace zust errMsg + " at line " + std::to_string(currentToken.line) + ", column " + std::to_string(currentToken.column)}); shouldTypecheck = false; - exit(0); + exit(1); return; } } diff --git a/tests/__pycache__/conftest.cpython-312-pytest-7.4.4.pyc b/tests/__pycache__/conftest.cpython-312-pytest-7.4.4.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e9f967c23826788a9c4c8a953bb4be9f058d9878 GIT binary patch literal 5076 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 O{HTKb@Li-F*!~}_Ur(g~ literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_pipeline.cpython-312-pytest-7.4.4.pyc b/tests/__pycache__/test_pipeline.cpython-312-pytest-7.4.4.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4b31f17cf3ea77ddce424f4a195002064eecb5ed GIT binary patch literal 20399 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

o8XgtrEcBO8~6kMeUPZP>jdsP_Z{n1{x;uD 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 none; + +fn main() { +} diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..552a1e5 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +import os +import sys +from dataclasses import dataclass +from pathlib import Path + +import pytest + + +@dataclass(frozen=True) +class TargetConfig: + name: str + zpiler_format: str + asm_ext: str + + +TARGETS: dict[str, TargetConfig] = { + "linux": TargetConfig(name="linux", zpiler_format="x86_64-linux", asm_ext=".s"), + "windows": TargetConfig( + name="windows", zpiler_format="x86_64-mswin", asm_ext=".asm" + ), + "llvm": TargetConfig(name="llvm", zpiler_format="llvm-ir", asm_ext=".ll"), +} + + +def detect_native_target() -> str: + plat = sys.platform + if plat.startswith("linux"): + return "linux" + if plat.startswith("win32") or plat.startswith("cygwin"): + return "windows" + return "llvm" + + +def parse_target_env() -> list[str]: + env = os.getenv("TARGET") + if env: + wanted = [t.strip() for t in env.split(",") if t.strip() in TARGETS] + if not wanted: + raise pytest.UsageError( + f"TARGET={env} contains no known backends. " + f"Known targets: {', '.join(TARGETS)}" + ) + return wanted + return [detect_native_target()] + + +def pytest_addoption(parser: pytest.Parser) -> None: + parser.addoption( + "--bless", + action="store_true", + default=False, + help="Create or update runtime golden output files.", + ) + + +def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: + if "target_name" in metafunc.fixturenames: + targets = parse_target_env() + metafunc.parametrize( + "target_name", targets, ids=[f"target={target}" for target in targets] + ) + + +@pytest.fixture(scope="session") +def bless(request: pytest.FixtureRequest) -> bool: + return bool(request.config.getoption("--bless")) + + +@pytest.fixture(scope="session") +def project_root() -> Path: + return Path(__file__).resolve().parents[1] + + +@pytest.fixture(scope="session") +def zpiler_path(project_root: Path) -> Path: + candidates = [ + project_root / "build" / "zpiler", + project_root / "build" / "Debug" / "zpiler.exe", + project_root / "build" / "zpiler.exe", + ] + for candidate in candidates: + if candidate.exists(): + return candidate + + candidate_text = "\n".join(str(path) for path in candidates) + pytest.exit( + "Could not find zpiler executable. Build project first.\n" + f"Searched:\n{candidate_text}", + returncode=2, + ) + + +@pytest.fixture(scope="session") +def artifacts_root(tmp_path_factory: pytest.TempPathFactory) -> Path: + return tmp_path_factory.mktemp("pipeline_artifacts") + + +@pytest.fixture +def target_config(target_name: str) -> TargetConfig: + return TARGETS[target_name] diff --git a/tests/expected/compile_fail/syntax/missing_semicolon.stderr.contains b/tests/expected/compile_fail/syntax/missing_semicolon.stderr.contains new file mode 100644 index 0000000..5ec650b --- /dev/null +++ b/tests/expected/compile_fail/syntax/missing_semicolon.stderr.contains @@ -0,0 +1,3 @@ +# Required syntax-failure markers +[Syntax Error] +Expected ';' after declaration diff --git a/tests/expected/compile_fail/type/bad_assignment.exitcode b/tests/expected/compile_fail/type/bad_assignment.exitcode new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/tests/expected/compile_fail/type/bad_assignment.exitcode @@ -0,0 +1 @@ +1 diff --git a/tests/expected/compile_fail/type/bad_assignment.stderr.contains b/tests/expected/compile_fail/type/bad_assignment.stderr.contains new file mode 100644 index 0000000..16d4c26 --- /dev/null +++ b/tests/expected/compile_fail/type/bad_assignment.stderr.contains @@ -0,0 +1,3 @@ +# Required semantic/type-failure markers +[Type Error] +Undefined type 'not_a_type' diff --git a/tests/expected/runtime/conditionals/if-elif-else.exitcode b/tests/expected/runtime/conditionals/if-elif-else.exitcode new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/runtime/conditionals/if-elif-else.exitcode @@ -0,0 +1 @@ +0 diff --git a/tests/expected/runtime/conditionals/if-elif-else.stderr b/tests/expected/runtime/conditionals/if-elif-else.stderr new file mode 100644 index 0000000..e69de29 diff --git a/tests/expected/runtime/conditionals/if-elif-else.stdout b/tests/expected/runtime/conditionals/if-elif-else.stdout new file mode 100644 index 0000000..b4de394 --- /dev/null +++ b/tests/expected/runtime/conditionals/if-elif-else.stdout @@ -0,0 +1 @@ +11 diff --git a/tests/expected/runtime/conditionals/if-elif.exitcode b/tests/expected/runtime/conditionals/if-elif.exitcode new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/runtime/conditionals/if-elif.exitcode @@ -0,0 +1 @@ +0 diff --git a/tests/expected/runtime/conditionals/if-elif.stderr b/tests/expected/runtime/conditionals/if-elif.stderr new file mode 100644 index 0000000..e69de29 diff --git a/tests/expected/runtime/conditionals/if-elif.stdout b/tests/expected/runtime/conditionals/if-elif.stdout new file mode 100644 index 0000000..e69de29 diff --git a/tests/expected/runtime/conditionals/if-else.exitcode b/tests/expected/runtime/conditionals/if-else.exitcode new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/runtime/conditionals/if-else.exitcode @@ -0,0 +1 @@ +0 diff --git a/tests/expected/runtime/conditionals/if-else.stderr b/tests/expected/runtime/conditionals/if-else.stderr new file mode 100644 index 0000000..e69de29 diff --git a/tests/expected/runtime/conditionals/if-else.stdout b/tests/expected/runtime/conditionals/if-else.stdout new file mode 100644 index 0000000..e69de29 diff --git a/tests/expected/runtime/conditionals/if.exitcode b/tests/expected/runtime/conditionals/if.exitcode new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/runtime/conditionals/if.exitcode @@ -0,0 +1 @@ +0 diff --git a/tests/expected/runtime/conditionals/if.stderr b/tests/expected/runtime/conditionals/if.stderr new file mode 100644 index 0000000..e69de29 diff --git a/tests/expected/runtime/conditionals/if.stdout b/tests/expected/runtime/conditionals/if.stdout new file mode 100644 index 0000000..e69de29 diff --git a/tests/expected/runtime/functions/basics.exitcode b/tests/expected/runtime/functions/basics.exitcode new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/runtime/functions/basics.exitcode @@ -0,0 +1 @@ +0 diff --git a/tests/expected/runtime/functions/basics.stderr b/tests/expected/runtime/functions/basics.stderr new file mode 100644 index 0000000..e69de29 diff --git a/tests/expected/runtime/functions/basics.stdout b/tests/expected/runtime/functions/basics.stdout new file mode 100644 index 0000000..d9a9da2 --- /dev/null +++ b/tests/expected/runtime/functions/basics.stdout @@ -0,0 +1,3 @@ +42 +9.000000 +true diff --git a/tests/expected/runtime/functions/recursive.exitcode b/tests/expected/runtime/functions/recursive.exitcode new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/runtime/functions/recursive.exitcode @@ -0,0 +1 @@ +0 diff --git a/tests/expected/runtime/functions/recursive.stderr b/tests/expected/runtime/functions/recursive.stderr new file mode 100644 index 0000000..e69de29 diff --git a/tests/expected/runtime/functions/recursive.stdout b/tests/expected/runtime/functions/recursive.stdout new file mode 100644 index 0000000..39af040 --- /dev/null +++ b/tests/expected/runtime/functions/recursive.stdout @@ -0,0 +1,3 @@ +120 +true +false diff --git a/tests/expected/runtime/functions/scopes.exitcode b/tests/expected/runtime/functions/scopes.exitcode new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/runtime/functions/scopes.exitcode @@ -0,0 +1 @@ +0 diff --git a/tests/expected/runtime/functions/scopes.stderr b/tests/expected/runtime/functions/scopes.stderr new file mode 100644 index 0000000..e69de29 diff --git a/tests/expected/runtime/functions/scopes.stdout b/tests/expected/runtime/functions/scopes.stdout new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/tests/expected/runtime/functions/scopes.stdout @@ -0,0 +1 @@ +22 diff --git a/tests/expected/runtime/loops/control.exitcode b/tests/expected/runtime/loops/control.exitcode new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/runtime/loops/control.exitcode @@ -0,0 +1 @@ +0 diff --git a/tests/expected/runtime/loops/control.stderr b/tests/expected/runtime/loops/control.stderr new file mode 100644 index 0000000..e69de29 diff --git a/tests/expected/runtime/loops/control.stdout b/tests/expected/runtime/loops/control.stdout new file mode 100644 index 0000000..e69de29 diff --git a/tests/expected/runtime/operations/binary.exitcode b/tests/expected/runtime/operations/binary.exitcode new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/runtime/operations/binary.exitcode @@ -0,0 +1 @@ +0 diff --git a/tests/expected/runtime/operations/binary.stderr b/tests/expected/runtime/operations/binary.stderr new file mode 100644 index 0000000..e69de29 diff --git a/tests/expected/runtime/operations/binary.stdout b/tests/expected/runtime/operations/binary.stdout new file mode 100644 index 0000000..b6ac604 --- /dev/null +++ b/tests/expected/runtime/operations/binary.stdout @@ -0,0 +1,24 @@ +30 +-10 +200 +2 +12.500000 +-7.500000 +25.000000 +4.000000 +5.859500 +0.423500 +8.538597 +1.155813 +3 +true +true +false +false +false +true +false +false +false +false +true diff --git a/tests/expected/runtime/operations/unary.exitcode b/tests/expected/runtime/operations/unary.exitcode new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/runtime/operations/unary.exitcode @@ -0,0 +1 @@ +0 diff --git a/tests/expected/runtime/operations/unary.stderr b/tests/expected/runtime/operations/unary.stderr new file mode 100644 index 0000000..e69de29 diff --git a/tests/expected/runtime/operations/unary.stdout b/tests/expected/runtime/operations/unary.stdout new file mode 100644 index 0000000..553e21a --- /dev/null +++ b/tests/expected/runtime/operations/unary.stdout @@ -0,0 +1,5 @@ +5 +255 +100 +0 +1 diff --git a/tests/expected/runtime/strings/escapes.exitcode b/tests/expected/runtime/strings/escapes.exitcode new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/runtime/strings/escapes.exitcode @@ -0,0 +1 @@ +0 diff --git a/tests/expected/runtime/strings/escapes.stderr b/tests/expected/runtime/strings/escapes.stderr new file mode 100644 index 0000000..e69de29 diff --git a/tests/expected/runtime/strings/escapes.stdout b/tests/expected/runtime/strings/escapes.stdout new file mode 100644 index 0000000..e69de29 diff --git a/tests/expected/runtime/types.exitcode b/tests/expected/runtime/types.exitcode new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/runtime/types.exitcode @@ -0,0 +1 @@ +0 diff --git a/tests/expected/runtime/types.stderr b/tests/expected/runtime/types.stderr new file mode 100644 index 0000000..e69de29 diff --git a/tests/expected/runtime/types.stdout b/tests/expected/runtime/types.stdout new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/runtime/types.stdout @@ -0,0 +1 @@ +0 diff --git a/tests/expected/runtime/variables.exitcode b/tests/expected/runtime/variables.exitcode new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/runtime/variables.exitcode @@ -0,0 +1 @@ +0 diff --git a/tests/expected/runtime/variables.linux.stdout b/tests/expected/runtime/variables.linux.stdout new file mode 100644 index 0000000..ebce988 --- /dev/null +++ b/tests/expected/runtime/variables.linux.stdout @@ -0,0 +1 @@ +It is what it is \ No newline at end of file diff --git a/tests/expected/runtime/variables.llvm.stdout b/tests/expected/runtime/variables.llvm.stdout new file mode 100644 index 0000000..ebce988 --- /dev/null +++ b/tests/expected/runtime/variables.llvm.stdout @@ -0,0 +1 @@ +It is what it is \ No newline at end of file diff --git a/tests/expected/runtime/variables.stderr b/tests/expected/runtime/variables.stderr new file mode 100644 index 0000000..e69de29 diff --git a/tests/expected/runtime/variables.stdout b/tests/expected/runtime/variables.stdout new file mode 100644 index 0000000..89c8485 --- /dev/null +++ b/tests/expected/runtime/variables.stdout @@ -0,0 +1 @@ +DO_NOT_USE_SHARED_VARIABLES_STDOUT diff --git a/tests/expected/runtime/variables.windows.stdout b/tests/expected/runtime/variables.windows.stdout new file mode 100644 index 0000000..ebce988 --- /dev/null +++ b/tests/expected/runtime/variables.windows.stdout @@ -0,0 +1 @@ +It is what it is \ No newline at end of file diff --git a/tests/expected/runtime_fail/exit/nonzero.exitcode b/tests/expected/runtime_fail/exit/nonzero.exitcode new file mode 100644 index 0000000..7f8f011 --- /dev/null +++ b/tests/expected/runtime_fail/exit/nonzero.exitcode @@ -0,0 +1 @@ +7 diff --git a/tests/expected/runtime_fail/exit/nonzero.stderr b/tests/expected/runtime_fail/exit/nonzero.stderr new file mode 100644 index 0000000..e69de29 diff --git a/tests/expected/runtime_fail/exit/nonzero.stdout b/tests/expected/runtime_fail/exit/nonzero.stdout new file mode 100644 index 0000000..050fd9d --- /dev/null +++ b/tests/expected/runtime_fail/exit/nonzero.stdout @@ -0,0 +1 @@ +about-to-fail diff --git a/tests/zz/conditionals/if-elif-else.zz b/tests/runtime/conditionals/if-elif-else.zz similarity index 100% rename from tests/zz/conditionals/if-elif-else.zz rename to tests/runtime/conditionals/if-elif-else.zz diff --git a/tests/zz/conditionals/if-elif.zz b/tests/runtime/conditionals/if-elif.zz similarity index 100% rename from tests/zz/conditionals/if-elif.zz rename to tests/runtime/conditionals/if-elif.zz diff --git a/tests/zz/conditionals/if-else.zz b/tests/runtime/conditionals/if-else.zz similarity index 100% rename from tests/zz/conditionals/if-else.zz rename to tests/runtime/conditionals/if-else.zz diff --git a/tests/zz/conditionals/if.zz b/tests/runtime/conditionals/if.zz similarity index 100% rename from tests/zz/conditionals/if.zz rename to tests/runtime/conditionals/if.zz diff --git a/tests/zz/functions/basics.zz b/tests/runtime/functions/basics.zz similarity index 100% rename from tests/zz/functions/basics.zz rename to tests/runtime/functions/basics.zz diff --git a/tests/zz/functions/recursive.zz b/tests/runtime/functions/recursive.zz similarity index 100% rename from tests/zz/functions/recursive.zz rename to tests/runtime/functions/recursive.zz diff --git a/tests/zz/functions/scopes.zz b/tests/runtime/functions/scopes.zz similarity index 100% rename from tests/zz/functions/scopes.zz rename to tests/runtime/functions/scopes.zz diff --git a/tests/zz/loops/control.zz b/tests/runtime/loops/control.zz similarity index 100% rename from tests/zz/loops/control.zz rename to tests/runtime/loops/control.zz diff --git a/tests/zz/operations/binary.zz b/tests/runtime/operations/binary.zz similarity index 100% rename from tests/zz/operations/binary.zz rename to tests/runtime/operations/binary.zz diff --git a/tests/zz/operations/unary.zz b/tests/runtime/operations/unary.zz similarity index 100% rename from tests/zz/operations/unary.zz rename to tests/runtime/operations/unary.zz diff --git a/tests/zz/strings/escapes.zz b/tests/runtime/strings/escapes.zz similarity index 100% rename from tests/zz/strings/escapes.zz rename to tests/runtime/strings/escapes.zz diff --git a/tests/zz/types.zz b/tests/runtime/types.zz similarity index 100% rename from tests/zz/types.zz rename to tests/runtime/types.zz diff --git a/tests/zz/variables.zz b/tests/runtime/variables.zz similarity index 100% rename from tests/zz/variables.zz rename to tests/runtime/variables.zz diff --git a/tests/runtime_fail/exit/nonzero.zz b/tests/runtime_fail/exit/nonzero.zz new file mode 100644 index 0000000..301d201 --- /dev/null +++ b/tests/runtime_fail/exit/nonzero.zz @@ -0,0 +1,7 @@ +extern fn printf(fmt: string, ...) -> int32_t; +extern fn exit(code: int32_t) -> none; + +fn main() { + printf("about-to-fail\n"); + exit(7); +} diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py new file mode 100644 index 0000000..eb74446 --- /dev/null +++ b/tests/test_pipeline.py @@ -0,0 +1,568 @@ +from __future__ import annotations + +import difflib +import shutil +import subprocess +from pathlib import Path + +import pytest + + +TESTS_DIR = Path(__file__).resolve().parent +RUNTIME_DIR = TESTS_DIR / "runtime" +RUNTIME_FAIL_DIR = TESTS_DIR / "runtime_fail" +COMPILE_FAIL_DIR = TESTS_DIR / "compile_fail" +EXPECTED_DIR = TESTS_DIR / "expected" + + +def discover_cases(root: Path) -> list[Path]: + if not root.exists(): + return [] + return sorted(path.relative_to(root) for path in root.rglob("*.zz")) + + +RUNTIME_CASES = discover_cases(RUNTIME_DIR) +RUNTIME_FAIL_CASES = discover_cases(RUNTIME_FAIL_DIR) +COMPILE_FAIL_CASES = discover_cases(COMPILE_FAIL_DIR) + + +def normalize_line_endings(data: bytes) -> bytes: + return data.replace(b"\r\n", b"\n").replace(b"\r", b"\n") + + +def decode_text(data: bytes) -> str: + return data.decode("utf-8", errors="replace") + + +def diff_text(expected: bytes, actual: bytes, label: str) -> str: + expected_text = decode_text(expected) + actual_text = decode_text(actual) + diff = "".join( + difflib.unified_diff( + expected_text.splitlines(keepends=True), + actual_text.splitlines(keepends=True), + fromfile=f"expected/{label}", + tofile=f"actual/{label}", + ) + ) + if diff: + return diff + return ( + f"Byte mismatch for {label}, but textual diff is empty.\n" + f"expected={expected!r}\nactual={actual!r}\n" + ) + + +def run_process( + cmd: list[str], *, cwd: Path | None = None +) -> subprocess.CompletedProcess[bytes]: + return subprocess.run( + cmd, + cwd=str(cwd) if cwd is not None else None, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + +def format_process_result( + step: str, cmd: list[str], proc: subprocess.CompletedProcess[bytes] +) -> str: + stdout = decode_text(normalize_line_endings(proc.stdout)) + stderr = decode_text(normalize_line_endings(proc.stderr)) + return ( + f"{step} failed.\n" + f"Command: {' '.join(cmd)}\n" + f"Exit code: {proc.returncode}\n" + f"--- stdout ---\n{stdout}\n" + f"--- stderr ---\n{stderr}" + ) + + +def compile_source( + *, + source: Path, + asm_out: Path, + target_config, + zpiler_path: Path, +) -> tuple[list[str], subprocess.CompletedProcess[bytes]]: + cmd = [ + str(zpiler_path), + "--format", + target_config.zpiler_format, + "-o", + str(asm_out), + str(source), + ] + return cmd, run_process(cmd) + + +def assemble_source( + *, + target_name: str, + asm_out: Path, + obj_out: Path, + work_dir: Path, +) -> tuple[list[str], subprocess.CompletedProcess[bytes]]: + if target_name == "linux": + cmd = ["as", str(asm_out), "-o", str(obj_out)] + return cmd, run_process(cmd) + + if target_name == "llvm": + cmd = ["llc", "-filetype=obj", str(asm_out), "-o", str(obj_out)] + return cmd, run_process(cmd) + + if target_name == "windows": + cmd = ["ml64", "/nologo", "/c", str(asm_out)] + proc = run_process(cmd, cwd=work_dir) + if proc.returncode != 0: + return cmd, proc + + obj_temp = work_dir / f"{asm_out.stem}.obj" + if not obj_temp.exists(): + return ( + cmd, + subprocess.CompletedProcess( + cmd, + returncode=1, + stdout=proc.stdout, + stderr=proc.stderr + + f"\nExpected object file not found: {obj_temp}\n".encode(), + ), + ) + + obj_out.parent.mkdir(parents=True, exist_ok=True) + shutil.move(str(obj_temp), str(obj_out)) + return cmd, proc + + raise ValueError(f"Unknown target: {target_name}") + + +def link_object( + *, + target_name: str, + obj_out: Path, + exe_out: Path, +) -> tuple[list[str], subprocess.CompletedProcess[bytes]]: + cmd = ["gcc", str(obj_out), "-o", str(exe_out)] + if target_name == "llvm": + cmd.append("-no-pie") + return cmd, run_process(cmd) + + +def run_executable(exe_out: Path, work_dir: Path) -> subprocess.CompletedProcess[bytes]: + return run_process([str(exe_out)], cwd=work_dir) + + +def expectation_candidates(base: Path, target_name: str, ext: str) -> tuple[Path, Path]: + specific = Path(f"{base}.{target_name}.{ext}") + shared = Path(f"{base}.{ext}") + return specific, shared + + +def resolve_existing_expectation(base: Path, target_name: str, ext: str) -> Path: + specific, shared = expectation_candidates(base, target_name, ext) + if specific.exists(): + return specific + return shared + + +def resolve_write_expectation(base: Path, target_name: str, ext: str) -> Path: + specific, shared = expectation_candidates(base, target_name, ext) + if specific.exists(): + return specific + if shared.exists(): + return shared + return shared + + +def expected_base(mode: str, rel_path: Path) -> Path: + return EXPECTED_DIR / mode / rel_path.with_suffix("") + + +def parse_exit_code(path: Path) -> int: + text = decode_text(normalize_line_endings(path.read_bytes())).strip() + if not text: + pytest.fail(f"Expected exit code file is empty: {path}", pytrace=False) + try: + return int(text) + except ValueError as exc: + pytest.fail(f"Invalid exit code in {path}: {text!r}", pytrace=False) + raise exc + + +def load_or_bless_output( + *, + base: Path, + target_name: str, + ext: str, + actual: bytes, + bless: bool, +) -> tuple[bytes, Path]: + actual_norm = normalize_line_endings(actual) + read_path = resolve_existing_expectation(base, target_name, ext) + + if bless: + write_path = resolve_write_expectation(base, target_name, ext) + write_path.parent.mkdir(parents=True, exist_ok=True) + if (not write_path.exists()) or ( + normalize_line_endings(write_path.read_bytes()) != actual_norm + ): + write_path.write_bytes(actual_norm) + return actual_norm, write_path + + if not read_path.exists(): + pytest.fail( + f"Missing expected file: {read_path}\n" + "Re-run with --bless to create runtime expectations.", + pytrace=False, + ) + + return normalize_line_endings(read_path.read_bytes()), read_path + + +def load_or_bless_exit_code( + *, + base: Path, + target_name: str, + actual: int, + bless: bool, +) -> tuple[int, Path]: + read_path = resolve_existing_expectation(base, target_name, "exitcode") + + if bless: + write_path = resolve_write_expectation(base, target_name, "exitcode") + write_path.parent.mkdir(parents=True, exist_ok=True) + rendered = f"{actual}\n".encode() + if (not write_path.exists()) or (write_path.read_bytes() != rendered): + write_path.write_bytes(rendered) + return actual, write_path + + if not read_path.exists(): + pytest.fail( + f"Missing expected file: {read_path}\n" + "Re-run with --bless to create runtime expectations.", + pytrace=False, + ) + return parse_exit_code(read_path), read_path + + +def assert_bytes_equal( + *, + label: str, + expected: bytes, + actual: bytes, + expected_path: Path, + source: Path, + target_name: str, +) -> None: + if expected == actual: + return + diff = diff_text(expected, actual, label) + raise AssertionError( + f"{source} on target={target_name} mismatched {label}.\n" + f"Expectation file: {expected_path}\n{diff}" + ) + + +def build_and_run( + *, + mode: str, + rel_path: Path, + source_root: Path, + target_name: str, + target_config, + zpiler_path: Path, + artifacts_root: Path, +) -> subprocess.CompletedProcess[bytes]: + source = source_root / rel_path + case_root = artifacts_root / mode / target_name / rel_path.with_suffix("") + case_root.mkdir(parents=True, exist_ok=True) + + asm_out = case_root / f"{source.stem}{target_config.asm_ext}" + obj_out = case_root / f"{source.stem}.obj" + exe_out = case_root / f"{source.stem}.exe" + + compile_cmd, compile_proc = compile_source( + source=source, + asm_out=asm_out, + target_config=target_config, + zpiler_path=zpiler_path, + ) + if compile_proc.returncode != 0: + pytest.fail( + format_process_result("Compile", compile_cmd, compile_proc), pytrace=False + ) + + assemble_cmd, assemble_proc = assemble_source( + target_name=target_name, + asm_out=asm_out, + obj_out=obj_out, + work_dir=case_root, + ) + if assemble_proc.returncode != 0: + pytest.fail( + format_process_result("Assemble", assemble_cmd, assemble_proc), pytrace=False + ) + + link_cmd, link_proc = link_object( + target_name=target_name, + obj_out=obj_out, + exe_out=exe_out, + ) + if link_proc.returncode != 0: + pytest.fail(format_process_result("Link", link_cmd, link_proc), pytrace=False) + + return run_executable(exe_out, case_root) + + +def run_compile_only( + *, + rel_path: Path, + source_root: Path, + target_config, + zpiler_path: Path, + artifacts_root: Path, + target_name: str, +) -> subprocess.CompletedProcess[bytes]: + source = source_root / rel_path + case_root = artifacts_root / "compile_fail" / target_name / rel_path.with_suffix("") + case_root.mkdir(parents=True, exist_ok=True) + asm_out = case_root / f"{source.stem}{target_config.asm_ext}" + _, compile_proc = compile_source( + source=source, + asm_out=asm_out, + target_config=target_config, + zpiler_path=zpiler_path, + ) + return compile_proc + + +def load_required_substrings(base: Path, target_name: str) -> tuple[list[str], Path]: + contains_path = resolve_existing_expectation(base, target_name, "stderr.contains") + if not contains_path.exists(): + pytest.fail( + f"Missing required compile_fail expectation: {contains_path}", pytrace=False + ) + + lines = decode_text(normalize_line_endings(contains_path.read_bytes())).splitlines() + required = [] + for line in lines: + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + required.append(stripped) + + if not required: + pytest.fail( + f"No required substrings found in {contains_path}. " + "Add at least one non-comment line.", + pytrace=False, + ) + return required, contains_path + + +def load_compile_fail_expected_exit(base: Path, target_name: str) -> tuple[int, Path | None]: + exit_path = resolve_existing_expectation(base, target_name, "exitcode") + if not exit_path.exists(): + return 1, None + return parse_exit_code(exit_path), exit_path + + +@pytest.mark.parametrize("rel_path", RUNTIME_CASES, ids=lambda p: p.as_posix()) +def test_runtime( + rel_path: Path, + target_name: str, + target_config, + project_root: Path, + zpiler_path: Path, + artifacts_root: Path, + bless: bool, +) -> None: + proc = build_and_run( + mode="runtime", + rel_path=rel_path, + source_root=project_root / "tests" / "runtime", + target_name=target_name, + target_config=target_config, + zpiler_path=zpiler_path, + artifacts_root=artifacts_root, + ) + + base = expected_base("runtime", rel_path) + + expected_stdout, stdout_path = load_or_bless_output( + base=base, + target_name=target_name, + ext="stdout", + actual=proc.stdout, + bless=bless, + ) + expected_stderr, stderr_path = load_or_bless_output( + base=base, + target_name=target_name, + ext="stderr", + actual=proc.stderr, + bless=bless, + ) + expected_exit, exit_path = load_or_bless_exit_code( + base=base, + target_name=target_name, + actual=proc.returncode, + bless=bless, + ) + + actual_stdout = normalize_line_endings(proc.stdout) + actual_stderr = normalize_line_endings(proc.stderr) + assert_bytes_equal( + label="stdout", + expected=expected_stdout, + actual=actual_stdout, + expected_path=stdout_path, + source=rel_path, + target_name=target_name, + ) + assert_bytes_equal( + label="stderr", + expected=expected_stderr, + actual=actual_stderr, + expected_path=stderr_path, + source=rel_path, + target_name=target_name, + ) + if proc.returncode != expected_exit: + raise AssertionError( + f"{rel_path} on target={target_name} mismatched exit code.\n" + f"Expectation file: {exit_path}\n" + f"Expected: {expected_exit}\nActual: {proc.returncode}" + ) + + +@pytest.mark.parametrize("rel_path", RUNTIME_FAIL_CASES, ids=lambda p: p.as_posix()) +def test_runtime_fail( + rel_path: Path, + target_name: str, + target_config, + project_root: Path, + zpiler_path: Path, + artifacts_root: Path, + bless: bool, +) -> None: + proc = build_and_run( + mode="runtime_fail", + rel_path=rel_path, + source_root=project_root / "tests" / "runtime_fail", + target_name=target_name, + target_config=target_config, + zpiler_path=zpiler_path, + artifacts_root=artifacts_root, + ) + + base = expected_base("runtime_fail", rel_path) + + expected_stdout, stdout_path = load_or_bless_output( + base=base, + target_name=target_name, + ext="stdout", + actual=proc.stdout, + bless=bless, + ) + expected_stderr, stderr_path = load_or_bless_output( + base=base, + target_name=target_name, + ext="stderr", + actual=proc.stderr, + bless=bless, + ) + expected_exit, exit_path = load_or_bless_exit_code( + base=base, + target_name=target_name, + actual=proc.returncode, + bless=bless, + ) + + if expected_exit == 0: + pytest.fail( + f"runtime_fail expectation cannot use exit code 0: {exit_path}", + pytrace=False, + ) + + actual_stdout = normalize_line_endings(proc.stdout) + actual_stderr = normalize_line_endings(proc.stderr) + assert_bytes_equal( + label="stdout", + expected=expected_stdout, + actual=actual_stdout, + expected_path=stdout_path, + source=rel_path, + target_name=target_name, + ) + assert_bytes_equal( + label="stderr", + expected=expected_stderr, + actual=actual_stderr, + expected_path=stderr_path, + source=rel_path, + target_name=target_name, + ) + if proc.returncode != expected_exit: + raise AssertionError( + f"{rel_path} on target={target_name} mismatched exit code.\n" + f"Expectation file: {exit_path}\n" + f"Expected: {expected_exit}\nActual: {proc.returncode}" + ) + + +@pytest.mark.parametrize("rel_path", COMPILE_FAIL_CASES, ids=lambda p: p.as_posix()) +def test_compile_fail( + rel_path: Path, + target_name: str, + target_config, + project_root: Path, + zpiler_path: Path, + artifacts_root: Path, +) -> None: + proc = run_compile_only( + rel_path=rel_path, + source_root=project_root / "tests" / "compile_fail", + target_config=target_config, + zpiler_path=zpiler_path, + artifacts_root=artifacts_root, + target_name=target_name, + ) + + base = expected_base("compile_fail", rel_path) + expected_exit, exit_path = load_compile_fail_expected_exit(base, target_name) + if proc.returncode != expected_exit: + exit_source = str(exit_path) if exit_path is not None else "default(1)" + pytest.fail( + "compile_fail exit code mismatch.\n" + f"Source: {rel_path}\n" + f"Target: {target_name}\n" + f"Expected ({exit_source}): {expected_exit}\n" + f"Actual: {proc.returncode}\n" + f"--- stdout ---\n{decode_text(normalize_line_endings(proc.stdout))}\n" + f"--- stderr ---\n{decode_text(normalize_line_endings(proc.stderr))}", + pytrace=False, + ) + + if proc.returncode == 0: + pytest.fail( + "compile_fail test unexpectedly succeeded.\n" + f"Source: {rel_path}\nTarget: {target_name}", + pytrace=False, + ) + + required_substrings, contains_path = load_required_substrings(base, target_name) + stderr_text = decode_text(normalize_line_endings(proc.stderr)) + missing = [needle for needle in required_substrings if needle not in stderr_text] + if missing: + joined = "\n".join(f"- {needle}" for needle in missing) + pytest.fail( + "compile_fail stderr missing required substrings.\n" + f"Source: {rel_path}\n" + f"Target: {target_name}\n" + f"Expectation file: {contains_path}\n" + f"Missing:\n{joined}\n" + f"--- stderr ---\n{stderr_text}", + pytrace=False, + )