calc_cli.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. #!/usr/bin/env python3
  2. """Launch CALC.COM under the project's qemu boot harness.
  3. Examples:
  4. python3 tests/calc_cli.py
  5. python3 tests/calc_cli.py --keys '\\r\\r/Q'
  6. python3 tests/calc_cli.py --keys-file keys.txt --raw > screen.bin
  7. python3 tests/calc_cli.py --com /path/to/CALC.COM --keys '\\r/Q'
  8. The default keys are the welcome-return, a second return for the main loop and
  9. `/Q' in CALC's command menu, so the command terminates instead of waiting for a
  10. keyboard. `--keys' accepts Python-style escapes (\\r, \\n, \\x1b, ...).
  11. """
  12. import argparse
  13. import codecs
  14. import os
  15. import shutil
  16. import subprocess
  17. import sys
  18. import tempfile
  19. HERE = os.path.dirname(os.path.abspath(__file__))
  20. SHELL = os.path.dirname(HERE)
  21. RES = os.path.join(os.path.dirname(SHELL), "Resources", "TP3")
  22. sys.path.insert(0, HERE)
  23. import run_com_exec as r # noqa: E402
  24. def build_calc_com(verbose=False):
  25. """Compile Resources/TP3/CALC.PAS in a temporary include directory."""
  26. td = tempfile.mkdtemp(prefix="tpcalc-build-")
  27. for name in ("CALC.PAS", "CALC.INC"):
  28. shutil.copy(os.path.join(RES, name), os.path.join(td, name))
  29. exe = r.ensure_comtest()
  30. src = os.path.join(td, "CALC.PAS")
  31. p = subprocess.run([exe], cwd=td, input=(src + "\n").encode(),
  32. capture_output=True)
  33. coms = [os.path.join(td, n) for n in os.listdir(td)
  34. if n.upper().endswith(".COM")]
  35. if p.returncode != 0 or not coms:
  36. raise RuntimeError("comtest did not produce CALC.COM:\n%s"
  37. % p.stdout.decode(errors="replace"))
  38. com = open(coms[0], "rb").read()
  39. if verbose:
  40. sys.stderr.write(p.stdout.decode(errors="replace"))
  41. sys.stderr.write("built %d-byte CALC.COM\n" % len(com))
  42. return com, td
  43. def decode_keys(text):
  44. """Decode the small escape language accepted by --keys."""
  45. return codecs.decode(text, "unicode_escape").encode("latin-1")
  46. def main(argv=None):
  47. ap = argparse.ArgumentParser(description=__doc__,
  48. formatter_class=argparse.RawDescriptionHelpFormatter)
  49. ap.add_argument("--com", help="use an existing CALC.COM instead of building")
  50. ap.add_argument("--keys", default="\\r\\r/Q",
  51. help="keys to feed CALC (Python escapes; default: welcome+quit)")
  52. ap.add_argument("--keys-file", help="read raw key bytes from this file")
  53. ap.add_argument("--timeout", type=int, default=15,
  54. help="qemu timeout in seconds (default 15)")
  55. ap.add_argument("--raw", action="store_true",
  56. help="write CALC's serial output as raw bytes")
  57. ap.add_argument("--quiet", action="store_true",
  58. help="only print the exit status")
  59. ap.add_argument("--verbose", action="store_true",
  60. help="show the compiler output and image size")
  61. ap.add_argument("--no-keypress", action="store_true",
  62. help="make AH=0Bh report no queued key (needed for scripted CALC input)")
  63. args = ap.parse_args(argv)
  64. td = None
  65. try:
  66. if args.com:
  67. com = open(args.com, "rb").read()
  68. else:
  69. com, td = build_calc_com(args.verbose)
  70. if len(com) > r.COM_MAX:
  71. sys.exit("CALC.COM is %d bytes; loader maximum is %d"
  72. % (len(com), r.COM_MAX))
  73. keys = (open(args.keys_file, "rb").read()
  74. if args.keys_file else decode_keys(args.keys))
  75. boot = r.build_boot_sector(no_keypress=args.no_keypress)
  76. with tempfile.TemporaryDirectory(prefix="tpcalc-run-") as runtd:
  77. img = os.path.join(runtd, "calc.img")
  78. open(img, "wb").write(r.build_floppy(boot, com, keys))
  79. old_timeout = r.TIMEOUT
  80. r.TIMEOUT = args.timeout
  81. try:
  82. out, rc, note = r.run_qemu(img)
  83. finally:
  84. r.TIMEOUT = old_timeout
  85. code = r.exit_code_of(rc)
  86. if args.raw:
  87. sys.stdout.buffer.write(out)
  88. elif not args.quiet:
  89. if note:
  90. sys.stderr.write(note + "\n")
  91. sys.stderr.write("CALC.COM: %d bytes, exit=%s\n" % (len(com), code))
  92. sys.stdout.write(r.visible_output(out))
  93. if out and not out.endswith(b"\n"):
  94. sys.stdout.write("\n")
  95. return 0 if code == 0 else 1
  96. finally:
  97. if td:
  98. shutil.rmtree(td, ignore_errors=True)
  99. if __name__ == "__main__":
  100. sys.exit(main())