The highest supported milestone was spelled out in six places across four
files: range(1, 7) and default="m6" in test_cli.py, the same pair in
test_milestones_dosboxx.py, and through=6 in both registry.py and suite.py.
Registering M7 meant finding all six, and missing one failed silently.
Derive MAX_MILESTONE and MILESTONES from CASES instead, and move the mN
selector parser to registry.milestone_number so the pytest module stops
carrying its own copy. Adding cases for a new milestone is now enough for
ferro-test to accept --through/--only for it.
No behaviour change: MAX_MILESTONE evaluates to 6, ferro-test still advertises
{m1..m6} with default m6, and the case snapshot is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BScg8CF1sAAM2zVHAu5zvW
209 lines
9.1 KiB
Python
209 lines
9.1 KiB
Python
"""Milestone case registry: DOS commands and their expected exit status.
|
|
|
|
Only commands live here; the ``.fe`` fixtures stay under ``fec/tests`` and are
|
|
copied into the disposable DOS filesystem by the runner. Case order is load
|
|
bearing -- ``emit`` must precede ``build`` must precede ``run`` for the same
|
|
fixture, because each step consumes the previous step's output.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from .suite import Case
|
|
|
|
PASS = "TESTS\\PASS"
|
|
FAIL = "TESTS\\FAIL"
|
|
STD = "STD"
|
|
M2 = "TESTS\\M2"
|
|
M3 = "TESTS\\M3"
|
|
M4 = "TESTS\\M4"
|
|
M5 = "TESTS\\M5"
|
|
M6 = "TESTS\\M6"
|
|
OUT = "OUT"
|
|
|
|
# Emitted-C basenames that were hand-shortened for DOS 8.3. Keyed by milestone
|
|
# because the same fixture name maps to different outputs across milestones
|
|
# (``bad-type`` is BAD-TY in M2 but BAD-TYP in M4). The shortenings are not
|
|
# consistent -- M2 cut to six characters, M4 to seven, and several were never
|
|
# required at all since BAD-COND is already a legal 8.3 name. Preserved verbatim;
|
|
# changing one renames a file inside the DOS run, so re-verify if you touch it.
|
|
_OUT83 = {
|
|
(2, "bad-cond"): "BAD-CO",
|
|
(2, "bad-cast"): "BAD-CA",
|
|
(2, "bad-asgn"): "BAD-AS",
|
|
(2, "bad-unk"): "BAD-UN",
|
|
(2, "bad-ari"): "BAD-AR",
|
|
(2, "bad-type"): "BAD-TY",
|
|
(2, "bad-ret"): "BAD-RE",
|
|
(2, "bad-unit"): "BAD-UI",
|
|
(2, "bad-void"): "BAD-VO",
|
|
(4, "bad-type"): "BAD-TYP",
|
|
(4, "bad-writ"): "BAD-WRI",
|
|
(5, "bad-dest"): "BAD-DES",
|
|
}
|
|
|
|
|
|
def _case(milestone: int, name: str, command: str, ok: bool = True) -> Case:
|
|
return Case(f"m{milestone}-{name}", milestone, command, ok)
|
|
|
|
|
|
def _fe(directory: str, name: str) -> str:
|
|
return f"{directory}\\{name.upper()}.FE"
|
|
|
|
|
|
def _emit(source: str, output: str, *, target: str = "bits32",
|
|
flags: tuple[str, ...] = (), output_first: bool = False) -> str:
|
|
"""``fec`` invocation that translates ``source`` to C at ``output``.
|
|
|
|
``output_first`` reproduces the M6 cases, which pass ``-o`` before the input
|
|
file while every other milestone passes it after.
|
|
"""
|
|
parts = ["FEC.EXE", f"--target={target}", *flags, "--emit-c"]
|
|
parts += ["-o", output, source] if output_first else [source, "-o", output]
|
|
return " ".join(parts)
|
|
|
|
|
|
def _wcl(exe: str, *sources: str, bits: int = 32, strict: bool = False,
|
|
defines: tuple[str, ...] = ()) -> str:
|
|
"""Open Watcom invocation. ``strict`` is the M4 ``-wx -wcd=202`` pairing:
|
|
warnings are errors except W202, which the generated C trips on unused
|
|
helpers (see AGENTS.md)."""
|
|
parts = ["WCL386" if bits == 32 else "WCL", "-q", "-za"]
|
|
if strict:
|
|
parts += ["-wx", "-wcd=202"]
|
|
parts += ["-bt=dos", *defines, f"-fe={exe}", *sources]
|
|
return " ".join(parts)
|
|
|
|
|
|
def _dump_ast(milestone: int, directory: str, names: tuple[str, ...], *,
|
|
suffix: str, ok: bool = True, prefix: str = "") -> list[Case]:
|
|
return [
|
|
_case(milestone, f"{prefix}{name}-{suffix}",
|
|
f"FEC.EXE --dump-ast {_fe(directory, name)}", ok)
|
|
for name in names
|
|
]
|
|
|
|
|
|
def _rejects(milestone: int, directory: str, names: tuple[str, ...], *,
|
|
suffix: str = "") -> list[Case]:
|
|
"""Fixtures that must fail to compile. The emitted-C path is still spelled
|
|
out because ``fec`` needs an ``-o`` even when it is expected to bail."""
|
|
return [
|
|
_case(milestone, f"{name}-{suffix}" if suffix else name,
|
|
_emit(_fe(directory, name),
|
|
f"{directory}\\{_OUT83.get((milestone, name), name.upper())}.C"),
|
|
False)
|
|
for name in names
|
|
]
|
|
|
|
|
|
def _triple(milestone: int, name: str, directory: str, *, stem: str | None = None,
|
|
target: str = "bits32", bits: int = 32, strict: bool = False,
|
|
build_source: str | None = None, emit_suffix: str | None = "emit",
|
|
run_suffix: str = "run", run_ok: bool = True) -> list[Case]:
|
|
"""emit -> build -> run for one fixture.
|
|
|
|
``stem`` renames the C/EXE pair when the fixture name does not fit 8.3 or
|
|
collides (M2 castwhil emits CAST16). ``build_source`` compiles a different
|
|
file than the one emitted (M4 prop emits PROP.C but builds PROPTEST.C, which
|
|
``#include``s it).
|
|
"""
|
|
stem = stem or name.upper()
|
|
cfile = f"{directory}\\{stem}.C"
|
|
exe = f"{directory}\\{stem}.EXE"
|
|
emit_id = f"{name}-{emit_suffix}" if emit_suffix else name
|
|
return [
|
|
_case(milestone, emit_id, _emit(_fe(directory, name), cfile, target=target)),
|
|
_case(milestone, f"{name}-build",
|
|
_wcl(exe, build_source or cfile, bits=bits, strict=strict)),
|
|
_case(milestone, f"{name}-{run_suffix}", exe, run_ok),
|
|
]
|
|
|
|
|
|
CASES: list[Case] = [
|
|
# -- M1: parse only -------------------------------------------------------
|
|
*_dump_ast(1, PASS, ("basic", "literals", "keybuilt", "v012form"), suffix="parse"),
|
|
*_dump_ast(1, STD, ("core", "fmt", "io", "list", "map", "mem", "str", "sys"),
|
|
suffix="parse", prefix="std-"),
|
|
*_dump_ast(1, FAIL, ("misssemi", "unclcomm", "logical"), suffix="reject", ok=False),
|
|
|
|
# -- M2: first generated C ------------------------------------------------
|
|
*_triple(2, "hello", M2),
|
|
*_triple(2, "scopes", M2),
|
|
*_triple(2, "castwhil", M2, stem="CAST16", target="bits16", bits=16),
|
|
*_rejects(2, M2, ("bad-cond", "bad-cast", "bad-asgn", "bad-unk", "bad-ari",
|
|
"bad-type", "bad-ret", "bad-unit", "bad-void"), suffix="reject"),
|
|
|
|
# -- M3: aggregates, strings, bounds checks -------------------------------
|
|
*_triple(3, "struct", M3),
|
|
*_triple(3, "enum", M3),
|
|
*_triple(3, "array", M3),
|
|
*_triple(3, "mutable", M3),
|
|
*_rejects(3, M3, ("bad-mlet", "bad-shwr"), suffix="reject"),
|
|
*_triple(3, "str", M3),
|
|
*_triple(3, "for", M3),
|
|
*_triple(3, "nested", M3),
|
|
*_triple(3, "char", M3),
|
|
*_triple(3, "arrayctx", M3),
|
|
# These two must trap at runtime: the bounds check is the feature under test.
|
|
*_triple(3, "bounds", M3, run_suffix="trap", run_ok=False),
|
|
*_triple(3, "slcbound", M3, run_suffix="trap", run_ok=False),
|
|
_case(3, "bounds-no-checks-emit",
|
|
_emit(_fe(M3, "bounds"), f"{M3}\\BOUNDS-N.C", flags=("--no-checks",))),
|
|
_case(3, "bounds-no-checks-build",
|
|
_wcl(f"{M3}\\BOUNDS-N.EXE", f"{M3}\\BOUNDS-N.C")),
|
|
*_rejects(3, M3, ("badfld", "badmat", "badarr", "badcycle", "badstr", "badchar",
|
|
"badfield", "badindex"), suffix="reject"),
|
|
|
|
# -- M4: formatting and error propagation ---------------------------------
|
|
*_triple(4, "format", M4, strict=True, emit_suffix=None),
|
|
*_triple(4, "try-fpr", M4, strict=True, emit_suffix=None),
|
|
*_triple(4, "prop", M4, strict=True, emit_suffix=None,
|
|
build_source=f"{M4}\\PROPTEST.C"),
|
|
*_rejects(4, M4, ("bad-ari", "bad-verb", "bad-run", "bad-type", "bad-try",
|
|
"bad-writ", "bad-bufw", "bad-many", "bad-open", "bad-cls")),
|
|
|
|
# -- M5: defer and ownership ----------------------------------------------
|
|
_case(5, "defer", _emit(_fe(M5, "defer"), f"{M5}\\DEFER.C")),
|
|
_case(5, "owned", _emit(_fe(M5, "owned"), f"{M5}\\OWNED.C")),
|
|
*_rejects(5, M5, ("bad-move", "bad-dest", "bad-drop", "bad-dbl", "bad-cond",
|
|
"bad-proj", "bad-clos", "bad-loop")),
|
|
# The runtime case links the generated C against a hand-written allocator
|
|
# shim, so malloc/free are redirected at compile time.
|
|
_case(5, "runtime", _emit(_fe(M5, "runtime"), f"{M5}\\RUNT-G.C")),
|
|
_case(5, "runtime-build",
|
|
_wcl(f"{M5}\\RUNTIME.EXE", f"{M5}\\RUNT-G.C", f"{M5}\\RUNTIME.C",
|
|
defines=("-dmalloc=m5_malloc", "-dfree=m5_free"))),
|
|
_case(5, "runtime-run", f"{M5}\\RUNTIME.EXE"),
|
|
|
|
# -- M6: borrow checking (R1--R8) -----------------------------------------
|
|
*[_case(6, name, f"FEC.EXE --check {_fe(M6, name)}", False) for name in (
|
|
"badarg", "badbinit", "badbrmov", "baddefer", "badfld", "badglob", "badgmut",
|
|
"badinv", "badlocsl", "badloop", "badmove", "badmut", "badmut2", "badptr",
|
|
"badret", "badrfld", "badridx", "badscop", "badself", "badshwr", "badslfld",
|
|
"badtwo", "badup", "badweak")],
|
|
*[_case(6, name, _emit(_fe(M6, name), f"{OUT}\\{name.upper()}.C",
|
|
output_first=True)) for name in (
|
|
"okbranch", "okdefer", "okglobcp", "oklast", "okr8free", "okr8join",
|
|
"okr8meth", "okr8stat", "okrebor", "okrtlast", "okshare", "okslreb",
|
|
"okstatic", "oktemp", "oktrim", "okwcall")],
|
|
]
|
|
|
|
MAX_MILESTONE: int = max(case.milestone for case in CASES)
|
|
MILESTONES: tuple[str, ...] = tuple(f"m{number}"
|
|
for number in range(1, MAX_MILESTONE + 1))
|
|
|
|
|
|
def milestone_number(name: str) -> int:
|
|
"""Parse an ``mN`` selector against the milestones the registry knows about."""
|
|
if not name.startswith("m") or not name[1:].isdigit():
|
|
raise ValueError(f"invalid milestone: {name}")
|
|
value = int(name[1:])
|
|
if value not in range(1, MAX_MILESTONE + 1):
|
|
raise ValueError(f"unsupported milestone: {name}")
|
|
return value
|
|
|
|
|
|
def all_cases(*, through: int = MAX_MILESTONE, only: int | None = None) -> list[Case]:
|
|
if only is not None:
|
|
return [case for case in CASES if case.milestone == only]
|
|
return [case for case in CASES if case.milestone <= through]
|