Browse Source

tests: make the TP3 source corpus reproducible

Eric Streit 2 days ago
parent
commit
4c8d410188
3 changed files with 123 additions and 12 deletions
  1. 12 12
      CORPUS-TEST.md
  2. 102 0
      shell/tests/corpus_check.py
  3. 9 0
      shell/tests/run_all.sh

+ 12 - 12
CORPUS-TEST.md

@@ -1,18 +1,18 @@
 # Pascal corpus census
 
-`Sources/Dezip/` is an external collection, not part of the build.  It contains
-**4,919 `.pas` files** (77 MB, about 354k lines).  Most are SWAG/FreePascal or
-Delphi units, so a failure count is not a compiler score; it is a map of what
-the TP3 subset does not cover yet.
+`Sources/Dezip/` is an external collection, not part of the build.  The
+working tree now retains only `pascsrc/`: **71 `.pas` files**, all of which
+compile.  The original dump contained **4,919 `.pas` files** (77 MB, about
+354k lines); its census below is kept as history, not as the current result.
 
 ## Method
 
-Every file was compiled separately with the current `shell/compiletest`, in a
-fresh process and with a 5-second timeout.  Separate processes matter: the
-first batch run aborted after a compiler bug, and one crashing source must not
-hide the rest of the corpus.
+Every file is compiled separately with `shell/compiletest`, in a fresh process
+and with a 5-second timeout.  Separate processes matter: one crashing source
+must not hide the rest.  The repeatable check is `tests/corpus_check.py`,
+wired into `tests/run_all.sh` when `Sources/Dezip/pascsrc` exists.
 
-## Current result
+## Historical census (pre-cleanup)
 
 | result | count |
 |---|---:|
@@ -58,9 +58,9 @@ Delphi units; only 5 compile.
 4. Includes whose names or search paths differ from the file's own directory
    (`ERROR 90`, 418 files).
 
-The 49 clean files are useful smoke material, but the corpus is intentionally
-not wired into `run_all.sh`: it lives outside the repository and most of it is
-outside the TP3 language subset.
+The surviving 71 files are wired into `run_all.sh` through
+`tests/corpus_check.py`; an absent `Sources/` tree is a skip, because that
+material is user-owned and untracked.  The current result is **71 / 71 OK**.
 
 ## Conservative cleanup
 

+ 102 - 0
shell/tests/corpus_check.py

@@ -0,0 +1,102 @@
+#!/usr/bin/env python3
+"""Compile the TP3 example corpus in Sources/Dezip/pascsrc.
+
+The corpus is deliberately outside the build tree, so it is easy for a compiler
+change to regress it without any of the fixture suites noticing.  This script
+runs each source in its own compiletest process (one crashing file must not hide
+the rest), prints a compact result, and requires the whole corpus to compile.
+
+    python3 tests/corpus_check.py
+    python3 tests/corpus_check.py --expected 71
+    python3 tests/corpus_check.py --verbose
+
+A missing Sources tree is a skip rather than a failure: the corpus is user
+material and is not tracked by git.  Once the tree exists, both the file count
+and every verdict are checked, so adding or losing a source is visible too.
+"""
+
+import argparse
+import pathlib
+import re
+import subprocess
+import sys
+
+HERE = pathlib.Path(__file__).resolve().parent
+SHELL = HERE.parent
+SOURCES = SHELL.parent / "Sources"
+CORPUS = SOURCES / "Dezip" / "pascsrc"
+COMPILETEST = SHELL / "compiletest"
+BUILD = HERE / "run_compile_tests.sh"
+
+
+def build_compiletest() -> bool:
+    """Build the harness through its single, timestamp-checked recipe."""
+    r = subprocess.run([str(BUILD), "--build-only"], cwd=SHELL,
+                       capture_output=True, text=True)
+    if r.returncode != 0:
+        print(r.stdout, end="")
+        print(r.stderr, end="", file=sys.stderr)
+        return False
+    return True
+
+
+def compile_one(path: pathlib.Path) -> tuple[bool, str]:
+    try:
+        r = subprocess.run([str(COMPILETEST)],
+                           input=str(path) + "\n",
+                           capture_output=True, text=True, timeout=5)
+    except subprocess.TimeoutExpired:
+        return False, "timeout"
+    out = r.stdout + r.stderr
+    if re.search(r"\bOK\s+code=\d+\s+data=\d+", out):
+        return True, "OK"
+    m = re.search(r"\bERROR\s+(\d+)\s+at pos\s+(\d+)", out)
+    if m:
+        return False, "ERROR %s at pos %s" % (m.group(1), m.group(2))
+    if "ERROR" in out:
+        return False, out.strip().splitlines()[-1]
+    return False, "no compile result (exit %d)" % r.returncode
+
+
+def main() -> int:
+    ap = argparse.ArgumentParser(description=__doc__)
+    ap.add_argument("--expected", type=int, default=71,
+                    help="required number of .pas files (default: 71)")
+    ap.add_argument("--verbose", action="store_true",
+                    help="print every passing source as well as failures")
+    args = ap.parse_args()
+
+    if not CORPUS.is_dir():
+        print("SKIP: %s does not exist" % CORPUS)
+        return 0
+
+    if not build_compiletest():
+        print("FAIL: compiletest did not build")
+        return 1
+
+    files = sorted(CORPUS.rglob("*.[pP][aA][sS]"))
+    if len(files) != args.expected:
+        print("FAIL: expected %d .pas files, found %d in %s"
+              % (args.expected, len(files), CORPUS))
+        for p in files:
+            print("  " + str(p.relative_to(SOURCES)))
+        return 1
+
+    failed = []
+    for p in files:
+        ok, detail = compile_one(p)
+        rel = p.relative_to(SOURCES)
+        if ok:
+            if args.verbose:
+                print("  PASS  %s" % rel)
+        else:
+            failed.append((rel, detail))
+            print("  FAIL  %s  %s" % (rel, detail))
+
+    print("\ncorpus: %d passed, %d failed (of %d)"
+          % (len(files) - len(failed), len(failed), len(files)))
+    return 1 if failed else 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())

+ 9 - 0
shell/tests/run_all.sh

@@ -169,6 +169,15 @@ run "layout helper"   python3 tests/check_comimage.py
 run "mod=11 table"     python3 tests/probe/modrm11.py
 run "runtime image"   python3 tests/check_runtime.py
 run "compile matrix"  tests/run_compile_tests.sh
+# The fixture matrix is the tracked regression set.  Sources/ is user-owned and
+# untracked, so the corpus check is skipped when that tree is absent; when it
+# is present, both its 71-file count and every compile verdict are enforced.
+if [ -d "$D/../Sources/Dezip/pascsrc" ]; then
+   run "corpus (Sources)" python3 tests/corpus_check.py
+else
+   echo
+   echo "== corpus (Sources): SKIP (no Sources/Dezip/pascsrc)"
+fi
 # The shell reads its terminal escapes from a text file (TP3.TRM).  This is
 # that loader's oracle: config in, decoded bytes out, compared exactly.
 run "terminal config" tests/term_config.sh