Files
doslang-mirror/tools/tests/test_dos_names.py
T
coolguyandClaude Opus 5 8b7d6a8d09 dev: make DOS build failures diagnosable
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
2026-08-16 23:02:28 +09:00

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)