dev: run host syntax gates before starting DOSBox-X
There is an Open Watcom install on this host (C:\WATCOM19), and its Windows build compiles the compiler's own sources in about a second. Every declaration mismatch in the unification commits was found that way; each one would otherwise have cost a DOSBox-X boot and a full compiler build to surface, with a DOS-side message that names the wrong thing. Add tools/tests/test_host_syntax.py: compile all twelve sources with the flags build-dos.bat uses (-za -wx -wcd=202) and fail on any diagnostic. It skips when Watcom is absent, so the suite still runs elsewhere. Two structural checks come with it -- that build-dos.bat, the Makefile and the test agree on the source list, and that no .c under fec/src is compiled by nothing. Both would have caught check.c and emit_c.c quietly leaving the build when the M7 wrappers included them textually. ferro-test now runs these and the 8.3 name check before starting the VM, and stops if they fail. This is not verification and does not claim to be: wcc386 targets 32-bit where the real build is 16-bit large model, so it sees syntax, types and declarations and nothing about code generation. The DOS build and the milestone suite remain the gate. It only moves the cheap failures earlier.
This commit is contained in:
@@ -62,7 +62,18 @@ def main() -> int:
|
|||||||
# shell is sitting in -- an editable install plus a git worktree is enough
|
# shell is sitting in -- an editable install plus a git worktree is enough
|
||||||
# to silently build and test the wrong tree. Say which tree this is.
|
# to silently build and test the wrong tree. Say which tree this is.
|
||||||
print(f"ferro-test: building {ROOT}", file=sys.stderr)
|
print(f"ferro-test: building {ROOT}", file=sys.stderr)
|
||||||
test_file = os.fspath(ROOT / "tools" / "tests" / "test_milestones_dosboxx.py")
|
tests = ROOT / "tools" / "tests"
|
||||||
|
# The host gates run first and take under a second. A missing declaration
|
||||||
|
# or an 8.3-illegal name would otherwise be found only after a DOSBox-X
|
||||||
|
# boot and a full compiler build, and the DOS-side message for either is
|
||||||
|
# unhelpful. They do not replace the DOS run; they precede it.
|
||||||
|
gates = [os.fspath(tests / name) for name in
|
||||||
|
("test_host_syntax.py", "test_dos_names.py")]
|
||||||
|
if pytest.main([*gates, "-q", "--no-header"]) != 0:
|
||||||
|
print("ferro-test: host gates failed; not starting DOSBox-X",
|
||||||
|
file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
test_file = os.fspath(tests / "test_milestones_dosboxx.py")
|
||||||
pytest_args = [test_file, "--tb=short", "-v" if args.verbose else "-q"]
|
pytest_args = [test_file, "--tb=short", "-v" if args.verbose else "-q"]
|
||||||
if args.select:
|
if args.select:
|
||||||
pytest_args.extend(["-k", args.select])
|
pytest_args.extend(["-k", args.select])
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
"""Compile the compiler's own sources on the host, as a syntax gate.
|
||||||
|
|
||||||
|
This is not verification. AGENTS.md is explicit that a host compiler's result
|
||||||
|
does not count, and it still does not: the DOS build and the milestone suite
|
||||||
|
decide whether anything works. What this buys is the turnaround. A missing
|
||||||
|
declaration or a signature that disagrees with its definition used to surface
|
||||||
|
only after a DOSBox-X boot and a full compiler build; here it surfaces in about
|
||||||
|
a second, with the line number.
|
||||||
|
|
||||||
|
It uses the same Open Watcom the project targets, just the Windows-hosted build,
|
||||||
|
and the same strictness as ``fec/build-dos.bat`` (``-za -wx -wcd=202``). The
|
||||||
|
target differs -- wcc386 is 32-bit where the DOS build is 16-bit large model --
|
||||||
|
so this catches syntax, types and declarations, not code generation or memory
|
||||||
|
model problems.
|
||||||
|
|
||||||
|
Skipped when Watcom is not installed, so the suite still runs anywhere.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from ferrolang_vm.paths import ROOT
|
||||||
|
|
||||||
|
SRC = ROOT / "fec" / "src"
|
||||||
|
# Mirrors the compile order in fec/build-dos.bat.
|
||||||
|
SOURCES = ("arena", "diag", "lexer", "ast", "parser", "types", "m7", "own",
|
||||||
|
"check", "lower", "emit_c", "driver")
|
||||||
|
|
||||||
|
|
||||||
|
def _watcom() -> Path | None:
|
||||||
|
root = os.environ.get("WATCOM")
|
||||||
|
candidates = [Path(root)] if root else []
|
||||||
|
candidates.append(Path("C:/WATCOM19"))
|
||||||
|
for base in candidates:
|
||||||
|
if (base / "binnt" / "wcc386.exe").is_file():
|
||||||
|
return base
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def watcom() -> Path:
|
||||||
|
base = _watcom()
|
||||||
|
if base is None:
|
||||||
|
pytest.skip("Open Watcom is not installed on the host; set WATCOM to enable")
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def objdir(tmp_path_factory: pytest.TempPathFactory) -> Path:
|
||||||
|
return tmp_path_factory.mktemp("wcc")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("name", SOURCES)
|
||||||
|
def test_source_compiles_clean(name: str, watcom: Path, objdir: Path) -> None:
|
||||||
|
source = SRC / f"{name}.c"
|
||||||
|
if not source.is_file():
|
||||||
|
pytest.fail(f"{source} is missing but build-dos.bat compiles it")
|
||||||
|
env = dict(os.environ)
|
||||||
|
env["WATCOM"] = os.fspath(watcom)
|
||||||
|
env["INCLUDE"] = os.fspath(watcom / "h")
|
||||||
|
completed = subprocess.run(
|
||||||
|
[os.fspath(watcom / "binnt" / "wcc386.exe"), "-q", "-za", "-wx",
|
||||||
|
"-wcd=202", "-zq", f"-i={SRC}", os.fspath(source)],
|
||||||
|
cwd=objdir, capture_output=True, text=True, env=env, timeout=120,
|
||||||
|
)
|
||||||
|
output = (completed.stdout + completed.stderr).strip()
|
||||||
|
# -wx keeps warnings meaningful, so treat any diagnostic as a failure: the
|
||||||
|
# DOS build runs the same flags and stops on them.
|
||||||
|
assert completed.returncode == 0 and not output, (
|
||||||
|
f"{name}.c does not compile clean\n{output}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_scripts_agree_on_sources() -> None:
|
||||||
|
"""The two build files and this test must name the same translation units.
|
||||||
|
|
||||||
|
They drifted apart while the M7 wrapper existed, which is how a source could
|
||||||
|
stop being compiled without anyone noticing.
|
||||||
|
"""
|
||||||
|
batch = (ROOT / "fec" / "build-dos.bat").read_text(encoding="utf-8", errors="replace")
|
||||||
|
makefile = (ROOT / "fec" / "Makefile").read_text(encoding="utf-8", errors="replace")
|
||||||
|
in_batch = set(re.findall(r"src\\(\w+)\.c", batch))
|
||||||
|
srcline = next(line for line in makefile.splitlines() if line.startswith("SRC ="))
|
||||||
|
in_make = set(re.findall(r"src/(\w+)\.c", srcline))
|
||||||
|
assert in_batch == set(SOURCES), f"build-dos.bat compiles {sorted(in_batch)}"
|
||||||
|
assert in_make == set(SOURCES), f"Makefile compiles {sorted(in_make)}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_source_is_orphaned() -> None:
|
||||||
|
"""Every .c under fec/src must be compiled by something.
|
||||||
|
|
||||||
|
check_m7.c and emitcm7.c hid check.c and emit_c.c from the build by
|
||||||
|
including them textually; nothing flagged that they had stopped being
|
||||||
|
translation units of their own.
|
||||||
|
"""
|
||||||
|
on_disk = {p.stem for p in SRC.glob("*.c")}
|
||||||
|
assert on_disk == set(SOURCES), (
|
||||||
|
f"fec/src has {sorted(on_disk - set(SOURCES))} that no build step compiles"
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user