Reviewing an M7 branch took six DOSBox-X runs to find three build blockers that each take a second to explain. The runner threw away everything needed to see them. Capture the compiler build's output. Case commands were redirected to RESULTS\<key>.LOG but `call BUILD.BAT` was not, so the step that fails first and blocks every case left only BUILD.FAIL containing the string "FAIL". The twelve wcl invocations inside it were invisible; finding "Unable to open src\emit_c_m7.c" meant hand-editing build-dos.bat to add a redirect and re-running the VM. Record exit codes. The batch collapsed every outcome to `if errorlevel 1`, so a compiler that aborted and one that exited 1 with a diagnostic were the same FAIL. RC.BAT now walks a descending errorlevel ladder into RESULTS\<key>.RC and the host derives pass/fail from it, which immediately separates an ordinary rejection (1) from a trap (255). Note the space in `echo 0 >FILE`: without it DOS parses `0>` as a redirect of handle 0. Stop falling back to CONSOLE.LOG. That is DOSBox-X's own log -- display enumeration and INT15 chatter -- so a crashed command reported fifty lines of emulator noise instead of saying it produced no output. Add tools/tests/test_dos_names.py. An over-long source name reaches the DOS build as `Unable to open "src\..."`, which reads as a missing file rather than a name FAT cannot represent, and only after a VM boot and ten object builds. The check runs on the host in 0.03s and flags emit_c_m7.c (9-character stem) on the branch that prompted this. Also pass -k through to pytest so a single case can be re-run without its whole milestone, and print the resolved ROOT at startup: an editable install plus a git worktree will otherwise silently build a different checkout than the one the shell is in. Verified on master: 155 passed, unchanged. Recorded codes are 0 for success, 1 for rejections, 255 for the three bounds traps. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BScg8CF1sAAM2zVHAu5zvW
73 lines
2.7 KiB
Python
73 lines
2.7 KiB
Python
"""Host-side checks for constraints the DOS toolchain enforces far too late.
|
|
|
|
Everything here runs without DOSBox-X. The point is to fail in a tenth of a
|
|
second with the offending name, instead of after a DOSBox-X boot and ten
|
|
successful object builds -- and with a message that says what is actually wrong.
|
|
A 9-character source name reaches the DOS build as ``Unable to open "src\\x.c"``,
|
|
which reads as a missing file rather than a name that cannot be represented.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from ferrolang_vm.paths import ROOT
|
|
from ferrolang_vm.registry import CASES
|
|
|
|
# The runner copies these onto a FAT filesystem, where a name is at most eight
|
|
# characters plus a three-character extension.
|
|
COPIED_TREES = ("fec/src", "fec/std", "fec/tests")
|
|
|
|
|
|
def _offenders(root: Path) -> list[str]:
|
|
bad = []
|
|
for path in sorted(root.rglob("*")):
|
|
name = path.name
|
|
if name.startswith("."):
|
|
continue
|
|
stem, _, suffix = name.rpartition(".") if "." in name else (name, "", "")
|
|
if len(stem) > 8 or len(suffix) > 3:
|
|
bad.append(f"{path.relative_to(ROOT).as_posix()} (stem {len(stem)}, ext {len(suffix)})")
|
|
return bad
|
|
|
|
|
|
@pytest.mark.parametrize("tree", COPIED_TREES)
|
|
def test_copied_files_fit_dos_8_3(tree: str) -> None:
|
|
root = ROOT / tree
|
|
if not root.is_dir():
|
|
pytest.skip(f"{tree} is absent")
|
|
bad = _offenders(root)
|
|
assert not bad, (
|
|
f"{len(bad)} name(s) under {tree} cannot be represented on the DOS side.\n"
|
|
"The DOS build will report them as missing files, not as long names:\n "
|
|
+ "\n ".join(bad)
|
|
)
|
|
|
|
|
|
def test_registry_paths_exist_on_the_host() -> None:
|
|
"""Every ``.FE`` a case names must exist, matched case-insensitively.
|
|
|
|
DOS is case-insensitive, so a registry typo survives until the command runs
|
|
inside the VM and fails with a message about the wrong thing.
|
|
"""
|
|
available = {
|
|
path.relative_to(ROOT / "fec").as_posix().upper()
|
|
for path in (ROOT / "fec").rglob("*.fe")
|
|
}
|
|
available |= {
|
|
path.relative_to(ROOT / "fec").as_posix().upper()
|
|
for path in (ROOT / "fec").rglob("*.FE")
|
|
}
|
|
missing = []
|
|
for case in CASES:
|
|
for token in case.command.split():
|
|
if not token.upper().endswith(".FE"):
|
|
continue
|
|
wanted = token.replace("\\", "/").upper()
|
|
if wanted.startswith("STD/"):
|
|
wanted = f"STD/{wanted[4:]}"
|
|
if not any(entry.endswith(wanted) for entry in available):
|
|
missing.append(f"{case.id}: {token}")
|
|
assert not missing, "registry names fixtures that do not exist:\n " + "\n ".join(missing)
|