calc_cli.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  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. args = ap.parse_args(argv)
  62. td = None
  63. try:
  64. if args.com:
  65. com = open(args.com, "rb").read()
  66. else:
  67. com, td = build_calc_com(args.verbose)
  68. if len(com) > r.COM_MAX:
  69. sys.exit("CALC.COM is %d bytes; loader maximum is %d"
  70. % (len(com), r.COM_MAX))
  71. keys = (open(args.keys_file, "rb").read()
  72. if args.keys_file else decode_keys(args.keys))
  73. boot = r.build_boot_sector()
  74. with tempfile.TemporaryDirectory(prefix="tpcalc-run-") as runtd:
  75. img = os.path.join(runtd, "calc.img")
  76. open(img, "wb").write(r.build_floppy(boot, com, keys))
  77. old_timeout = r.TIMEOUT
  78. r.TIMEOUT = args.timeout
  79. try:
  80. out, rc, note = r.run_qemu(img)
  81. finally:
  82. r.TIMEOUT = old_timeout
  83. code = r.exit_code_of(rc)
  84. if args.raw:
  85. sys.stdout.buffer.write(out)
  86. elif not args.quiet:
  87. if note:
  88. sys.stderr.write(note + "\n")
  89. sys.stderr.write("CALC.COM: %d bytes, exit=%s\n" % (len(com), code))
  90. sys.stdout.write(r.visible_output(out))
  91. if out and not out.endswith(b"\n"):
  92. sys.stdout.write("\n")
  93. return 0 if code == 0 else 1
  94. finally:
  95. if td:
  96. shutil.rmtree(td, ignore_errors=True)
  97. if __name__ == "__main__":
  98. sys.exit(main())