Parcourir la source

tests: add CALC qemu launcher CLI

Eric Streit il y a 3 jours
Parent
commit
ef2816d6ed
3 fichiers modifiés avec 117 ajouts et 1 suppressions
  1. 5 0
      CALC-COMPILE.md
  2. 2 1
      SUMMARY.md
  3. 110 0
      shell/tests/calc_cli.py

+ 5 - 0
CALC-COMPILE.md

@@ -1257,6 +1257,11 @@ and TEXT file output is done (`t91_textout`), both compile-only because the
 harness has no filesystem.  `Str` real formatting is done (`t92_strreal`),
 pointer types with `New`/`Dispose`/dereference are done (`t93_pointer`), and
 text `Eof`/`Readln` plus integer `Str` are done (`t94_textread`/`t95_strint`).
+**CALC CLI:** `python3 tests/calc_cli.py` builds CALC.COM in a temporary
+include directory, boots it under qemu and feeds the default welcome/quit keys.
+`--keys '\r...'`, `--keys-file`, `--com`, `--raw`, `--timeout` and `--verbose`
+are available; input is a static boot-time key stream, not a live terminal.
+
 **CALC.PAS compiles and runs.**  The runtime is 9,373 bytes; the generated
 program is 25,908 bytes of code and 19,694 bytes of data, with
 `DataStart=0xA000` and `CodeMax=0xA000`.  The boot harness now loads images at

+ 2 - 1
SUMMARY.md

@@ -2074,7 +2074,8 @@ bytes.  `Insert`, CASE/ELSE trailing semicolons and string-vs-CHAR comparisons
 are also landed.  **CALC.PAS now compiles to its final `.`** with code 25,908
 and data 19,694.  The boot loader now uses segment 0x1000 with input at
 0x9000; `tests/calc_smoke.py` boots CALC.COM, checks the welcome/grid/menu and
-quits via `/Q` with exit 0.  The runtime is 9,373 bytes / 56 entries; the
+quits via `/Q` with exit 0.  `tests/calc_cli.py` is the manual launcher
+(`--keys`, `--keys-file`, `--com`, `--raw`, `--timeout`).  The runtime is 9,373 bytes / 56 entries; the
 rt_exec scaffold is at HDR=0x4000 / data 0x5000.  Items 1-4 below are
 independent of both tracks.
 

+ 110 - 0
shell/tests/calc_cli.py

@@ -0,0 +1,110 @@
+#!/usr/bin/env python3
+"""Launch CALC.COM under the project's qemu boot harness.
+
+Examples:
+    python3 tests/calc_cli.py
+    python3 tests/calc_cli.py --keys '\\r\\r/Q'
+    python3 tests/calc_cli.py --keys-file keys.txt --raw > screen.bin
+    python3 tests/calc_cli.py --com /path/to/CALC.COM --keys '\\r/Q'
+
+The default keys are the welcome-return, a second return for the main loop and
+`/Q' in CALC's command menu, so the command terminates instead of waiting for a
+keyboard.  `--keys' accepts Python-style escapes (\\r, \\n, \\x1b, ...).
+"""
+import argparse
+import codecs
+import os
+import shutil
+import subprocess
+import sys
+import tempfile
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+SHELL = os.path.dirname(HERE)
+RES = os.path.join(os.path.dirname(SHELL), "Resources", "TP3")
+sys.path.insert(0, HERE)
+import run_com_exec as r  # noqa: E402
+
+
+def build_calc_com(verbose=False):
+    """Compile Resources/TP3/CALC.PAS in a temporary include directory."""
+    td = tempfile.mkdtemp(prefix="tpcalc-build-")
+    for name in ("CALC.PAS", "CALC.INC"):
+        shutil.copy(os.path.join(RES, name), os.path.join(td, name))
+    exe = r.ensure_comtest()
+    src = os.path.join(td, "CALC.PAS")
+    p = subprocess.run([exe], cwd=td, input=(src + "\n").encode(),
+                       capture_output=True)
+    coms = [os.path.join(td, n) for n in os.listdir(td)
+            if n.upper().endswith(".COM")]
+    if p.returncode != 0 or not coms:
+        raise RuntimeError("comtest did not produce CALC.COM:\n%s"
+                           % p.stdout.decode(errors="replace"))
+    com = open(coms[0], "rb").read()
+    if verbose:
+        sys.stderr.write(p.stdout.decode(errors="replace"))
+        sys.stderr.write("built %d-byte CALC.COM\n" % len(com))
+    return com, td
+
+
+def decode_keys(text):
+    """Decode the small escape language accepted by --keys."""
+    return codecs.decode(text, "unicode_escape").encode("latin-1")
+
+
+def main(argv=None):
+    ap = argparse.ArgumentParser(description=__doc__,
+                                 formatter_class=argparse.RawDescriptionHelpFormatter)
+    ap.add_argument("--com", help="use an existing CALC.COM instead of building")
+    ap.add_argument("--keys", default="\\r\\r/Q",
+                    help="keys to feed CALC (Python escapes; default: welcome+quit)")
+    ap.add_argument("--keys-file", help="read raw key bytes from this file")
+    ap.add_argument("--timeout", type=int, default=15,
+                    help="qemu timeout in seconds (default 15)")
+    ap.add_argument("--raw", action="store_true",
+                    help="write CALC's serial output as raw bytes")
+    ap.add_argument("--quiet", action="store_true",
+                    help="only print the exit status")
+    ap.add_argument("--verbose", action="store_true",
+                    help="show the compiler output and image size")
+    args = ap.parse_args(argv)
+
+    td = None
+    try:
+        if args.com:
+            com = open(args.com, "rb").read()
+        else:
+            com, td = build_calc_com(args.verbose)
+        if len(com) > r.COM_MAX:
+            sys.exit("CALC.COM is %d bytes; loader maximum is %d"
+                     % (len(com), r.COM_MAX))
+        keys = (open(args.keys_file, "rb").read()
+                if args.keys_file else decode_keys(args.keys))
+        boot = r.build_boot_sector()
+        with tempfile.TemporaryDirectory(prefix="tpcalc-run-") as runtd:
+            img = os.path.join(runtd, "calc.img")
+            open(img, "wb").write(r.build_floppy(boot, com, keys))
+            old_timeout = r.TIMEOUT
+            r.TIMEOUT = args.timeout
+            try:
+                out, rc, note = r.run_qemu(img)
+            finally:
+                r.TIMEOUT = old_timeout
+        code = r.exit_code_of(rc)
+        if args.raw:
+            sys.stdout.buffer.write(out)
+        elif not args.quiet:
+            if note:
+                sys.stderr.write(note + "\n")
+            sys.stderr.write("CALC.COM: %d bytes, exit=%s\n" % (len(com), code))
+            sys.stdout.write(r.visible_output(out))
+            if out and not out.endswith(b"\n"):
+                sys.stdout.write("\n")
+        return 0 if code == 0 else 1
+    finally:
+        if td:
+            shutil.rmtree(td, ignore_errors=True)
+
+
+if __name__ == "__main__":
+    sys.exit(main())