셀프호스팅에 손대기 전의 강제 함수다. 아픈 자리를 전부 건드린다: R4 아래의 토큰 구조체, 태그드 유니온, 진단 출력, 유닛 경계. 토큰은 자기가 나온 글자를 담지 않는다. R4 가 대여를 집합 저장소에서 막으므로, 어디서 시작해 얼마나 긴지를 적고 소스는 옆에서 같이 다닌다. 위치도 &mut usize 로 옆에서 다닌다 -- 슬라이스와 함께 구조체에 들어갈 수 없기 때문이다. 이것이 R11 이 말하는 모양이고, 쓸 수 있다. first keyword unit @1 / number 42 @3 / text "hi" @3 / arrow -> @5 keyword 6 name 7 number 1 text 1 punct 15 / total 30 길에서 고친 것: - binding.Type.Variant 가 안 풀렸다. 유닛 경계 이름 조회가 심볼만 보고 타입을 보지 않았다. - 문자열 const 전역이 빈 슬라이스로 나갔다. 포인터는 링커만 아는 수라서 바이트에 구멍을 두고 링커가 채우게 한다. - exec.py 가 OUTPUT 마커를 여러 개 적어도 마지막 하나만 검사했다. 고치자마자 readfile 의 낡은 기대가 드러났다. run.py 217/217, exec.py 27/27.
114 lines
3.9 KiB
Python
114 lines
3.9 KiB
Python
"""Compile the programs under `fec/tests/exec/`, run them, and check what they do.
|
|
|
|
A program says what it should do in its first lines:
|
|
|
|
// EXIT:55 the process must exit with this code
|
|
// OUTPUT:hello this text must appear in what it wrote
|
|
// NOCHECKS:0 build it a second time with --no-checks and expect
|
|
this exit code instead
|
|
|
|
The point of this suite is different from `run.py`. That one checks what the
|
|
compiler says about a program; this one checks what the program does. A bounds
|
|
check that is reported but never emitted passes there and fails here.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import shutil
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
import build as builder # noqa: E402
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
PROGRAMS = ROOT / "fec" / "tests" / "exec"
|
|
|
|
|
|
def expectations(path: Path) -> dict:
|
|
want = {}
|
|
for line in path.read_text(encoding="utf-8").splitlines():
|
|
line = line.strip()
|
|
if not line.startswith("//"):
|
|
break
|
|
m = re.match(r"//\s*(EXIT|OUTPUT|NOCHECKS):(.*)", line)
|
|
if m:
|
|
key, value = m.group(1), m.group(2).strip()
|
|
if key == "OUTPUT":
|
|
# Every OUTPUT line has to appear. Keeping only the last one
|
|
# would let the earlier ones rot unnoticed.
|
|
want.setdefault("OUTPUT", []).append(value)
|
|
else:
|
|
want[key] = int(value)
|
|
return want
|
|
|
|
|
|
def check_one(fec: Path, path: Path, out_dir: Path) -> tuple[bool, str]:
|
|
want = expectations(path)
|
|
|
|
exe, log = builder.build(fec, path, out_dir)
|
|
if not exe:
|
|
detail = "\n".join(f" {n} (exit {c}): {t.strip()}"
|
|
for n, c, t in log if c != 0 or t.strip())
|
|
return False, "did not build\n" + detail
|
|
code, text = builder.run(exe)
|
|
if code != want["EXIT"]:
|
|
return False, f"exited {code}, expected {want['EXIT']}\n {text.strip()}"
|
|
for line in want.get("OUTPUT", []):
|
|
if line not in text:
|
|
return False, f"output has no {line!r}\n {text.strip()}"
|
|
|
|
if "NOCHECKS" in want:
|
|
exe2, log2 = builder.build(fec, path, out_dir / "nochecks",
|
|
no_checks=True)
|
|
if not exe2:
|
|
return False, "did not build with --no-checks"
|
|
code2, _ = builder.run(exe2)
|
|
if code2 != want["NOCHECKS"]:
|
|
return False, (f"--no-checks exited {code2}, expected "
|
|
f"{want['NOCHECKS']}")
|
|
return True, ""
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(description="run the compiled programs")
|
|
ap.add_argument("-k", dest="select")
|
|
ap.add_argument("-v", dest="verbose", action="store_true")
|
|
args = ap.parse_args()
|
|
|
|
fec = ROOT / ".build" / "fec.exe"
|
|
if not fec.is_file():
|
|
print("build the front end first: uv run python tests/run.py")
|
|
return 2
|
|
out_dir = ROOT / ".build" / "exec"
|
|
if out_dir.exists():
|
|
shutil.rmtree(out_dir, ignore_errors=True)
|
|
|
|
# A file with no `// EXIT:` is a unit some program imports, not a program.
|
|
cases = [p for p in sorted(PROGRAMS.rglob("*.fe"))
|
|
if "EXIT" in expectations(p)]
|
|
if args.select:
|
|
cases = [p for p in cases if args.select in p.as_posix()]
|
|
if not cases:
|
|
print("no programs found")
|
|
return 1
|
|
|
|
failed = []
|
|
for path in cases:
|
|
ok, why = check_one(fec, path, out_dir / path.stem)
|
|
rel = path.relative_to(PROGRAMS).as_posix()
|
|
if ok:
|
|
if args.verbose:
|
|
print(f" ok {rel}")
|
|
else:
|
|
failed.append((rel, why))
|
|
for rel, why in failed:
|
|
print(f"FAIL {rel}: {why}")
|
|
print(f"\n{len(cases) - len(failed)}/{len(cases)} programs behaved")
|
|
return 1 if failed else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|