run_exec86.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. #!/usr/bin/env python3
  2. """run_exec86.py -- run the same .COM under Exec86 and under qemu, and require
  3. the two machines to agree byte for byte.
  4. .pas --Compiler.mod--> image --Linker.mod--> .COM
  5. .COM --Exec86Run--> our interpreter ----------\
  6. .COM --bootcom.s--> floppy --qemu-----------> compare
  7. This is the cross-validation the summary asked for, and it is a claim about a
  8. *pair*: either one alone would pass while being wrong in the same direction.
  9. * the expected output is still the fixture's hand-derived .out, so the
  10. interpreter is not being compared only against qemu. If both machines
  11. were wrong the same way, .out - which no machine has ever written - would
  12. still catch it;
  13. * and the two machines are compared against each other directly, so a
  14. disagreement is reported as the two byte strings rather than as one test
  15. going red for a reason three assertions away;
  16. * the exit code is compared too, so "produced the right bytes and then fell
  17. over" cannot pass.
  18. Nothing here is copied from run_com_exec.py: the boot sector, the floppy
  19. layout, the input descriptor's offsets, qemu's exit-code convention and the
  20. fixture list are all imported from it. Two copies of that layout would be two
  21. things that can drift apart, and the layout has already produced two silent
  22. bugs once (see to_file's docstring there).
  23. Usage:
  24. tests/run_exec86.py # every fixture that has a .out
  25. tests/run_exec86.py t02_writeln # one, by name
  26. tests/run_exec86.py --list
  27. tests/run_exec86.py --show t02 # print what came out, assert nothing
  28. tests/run_exec86.py --no-qemu # skip the cross-check, for iteration
  29. There is deliberately no --rebless. The .out files are derived by hand from
  30. what the Pascal means, and a script that can regenerate them from what a
  31. machine did is a way to turn a red test into a green one without ever looking
  32. at the program. run_com_exec.py still has it, under that project's rules.
  33. """
  34. import os
  35. import subprocess
  36. import sys
  37. import tempfile
  38. HERE = os.path.dirname(os.path.abspath(__file__))
  39. SHELL = os.path.dirname(HERE)
  40. GM2 = "/home/eric/bin/Modula2/Gm2/bin/gm2"
  41. # run_com_exec.py owns the boot sector, the floppy, the input descriptor and
  42. # the qemu exit-code convention. Importing is what keeps this file from
  43. # becoming a second, subtly different copy of all four.
  44. sys.path.insert(0, HERE)
  45. import run_com_exec as rce # noqa: E402
  46. # ------------------------------------------------------------ the interpreter
  47. def ensure_exec86run():
  48. """Build tests/Exec86Run.mod into ./exec86run if it is out of date.
  49. Freshness is measured against the .o files that get LINKED, not against
  50. the sources they came from, for the same reason rce.ensure_comtest does
  51. it that way: `make` relinks an .o from an edited .mod, and if only the
  52. .mod were watched, a freshly rebuilt Exec86.o could sit next to an
  53. exec86run that still contained the old interpreter - so the fix under
  54. test would never actually be in the binary being tested.
  55. """
  56. exe = os.path.join(SHELL, "exec86run")
  57. objs = ["Posix.o", "Exec86.o"]
  58. newer = objs + ["Exec86.mod", "Posix.c", os.path.join("tests", "Exec86Run.mod")]
  59. if os.path.exists(exe):
  60. t = os.path.getmtime(exe)
  61. if all(os.path.exists(os.path.join(SHELL, f))
  62. and os.path.getmtime(os.path.join(SHELL, f)) <= t
  63. for f in newer):
  64. return exe
  65. lst = os.path.join(SHELL, "tests", "e86.lst")
  66. link = [os.path.join(SHELL, "tests", "Exec86Run.mod")]
  67. objs_abs = [os.path.join(SHELL, o) for o in objs]
  68. subprocess.run([GM2, "-fiso", "-fgen-module-list=" + lst, "-o", "/dev/null"]
  69. + link + objs_abs, capture_output=True, cwd=SHELL)
  70. r = subprocess.run([GM2, "-fiso", "-fuse-list=" + lst, "-o", exe]
  71. + link + objs_abs, capture_output=True, cwd=SHELL)
  72. if r.returncode != 0:
  73. sys.exit("linking exec86run failed:\n"
  74. + r.stderr.decode(errors="replace"))
  75. return exe
  76. def parse_status(err):
  77. """The driver's one report line, or None if it is not there.
  78. Parsed strictly: a driver that printed no line, or a line this does not
  79. understand, is a failure to report rather than something to guess at.
  80. """
  81. for line in err.decode("latin-1").splitlines():
  82. if not line.startswith("exec86: status="):
  83. continue
  84. out = {}
  85. for field in line[len("exec86: "):].split():
  86. k, _, v = field.partition("=")
  87. if not v.isdigit():
  88. return None
  89. out[k] = int(v)
  90. if set(out) != {"status", "exit", "steps"}:
  91. return None
  92. return out
  93. return None
  94. def run_exec86(exe, com, guest_in):
  95. """Run one .COM under Exec86. Return (output, report or None, note)."""
  96. payload = com.encode() + b"\n" + guest_in
  97. try:
  98. r = subprocess.run([exe], input=payload, capture_output=True,
  99. timeout=rce.TIMEOUT)
  100. except subprocess.TimeoutExpired:
  101. return b"", None, "the interpreter never halted (timeout %ds)" % rce.TIMEOUT
  102. rep = parse_status(r.stderr)
  103. if rep is None:
  104. return r.stdout, None, ("no report line; stderr was:\n "
  105. + r.stderr.decode("latin-1").replace("\n", "\n "))
  106. return r.stdout, rep, ""
  107. # ------------------------------------------------------------------- reporting
  108. def visible(raw):
  109. return raw.decode("latin-1").replace("\r", "\\r").replace("\n", "\\n")
  110. def main(argv):
  111. if "--list" in argv:
  112. for c in rce.cases():
  113. print(c["name"])
  114. return 0
  115. show = "--show" in argv
  116. cross = "--no-qemu" not in argv
  117. names = [a for a in argv if not a.startswith("-")]
  118. all_cases = list(rce.cases())
  119. if names:
  120. sel = [c for c in all_cases if c["name"] in names]
  121. missing = set(names) - set(c["name"] for c in sel)
  122. if missing:
  123. sys.exit("no such fixture: " + ", ".join(sorted(missing)))
  124. else:
  125. sel = all_cases
  126. exe = ensure_exec86run()
  127. boot = rce.build_boot_sector() if cross else None
  128. workdir = tempfile.mkdtemp(prefix="tpexec86-")
  129. passed = failed = 0
  130. for c in sel:
  131. pas = os.path.join(HERE, "fixtures", c["name"] + ".pas")
  132. com, log = rce.emit_com(rce.ensure_comtest(), pas, workdir)
  133. if com is None:
  134. print(" %-22s FAIL comtest wrote no .COM" % c["name"])
  135. if log:
  136. print(" %s" % log)
  137. failed += 1
  138. continue
  139. got, rep, note = run_exec86(exe, com, c["stdin"])
  140. want = open(c["out"], "rb").read()
  141. if show:
  142. print(" %-22s %s" % (c["name"], visible(got)))
  143. if rep:
  144. print(" %s" % rep)
  145. continue
  146. problems = []
  147. if rep is None:
  148. problems.append(note)
  149. elif rep["status"] != 0:
  150. problems.append("interpreter status=%d (0 = halted through INT 21h "
  151. "AH=4Ch)" % rep["status"])
  152. if got != want:
  153. problems.append("output differs from the hand-derived .out\n"
  154. " want %s\n"
  155. " got %s" % (visible(want), visible(got)))
  156. if cross:
  157. img = os.path.join(workdir, c["name"] + ".img")
  158. open(img, "wb").write(rce.build_floppy(boot, open(com, "rb").read(),
  159. c["stdin"]))
  160. qout, rc, qnote = rce.run_qemu(img)
  161. if rc is None:
  162. problems.append("qemu: " + qnote)
  163. else:
  164. qcode = rce.exit_code_of(rc)
  165. if qcode is None:
  166. problems.append("qemu rc=%d, which no .COM exit can produce"
  167. % rc)
  168. else:
  169. if qout != got:
  170. problems.append("disagrees with qemu\n"
  171. " qemu %s\n"
  172. " exec86 %s"
  173. % (visible(qout), visible(got)))
  174. if rep is not None and rep["status"] == 0 \
  175. and rep["exit"] != qcode:
  176. problems.append("exit code differs: exec86=%d qemu=%d"
  177. % (rep["exit"], qcode))
  178. if problems:
  179. failed += 1
  180. print(" %-22s FAIL" % c["name"])
  181. for p in problems:
  182. print(" %s" % p)
  183. else:
  184. passed += 1
  185. tail = "" if rep is None else ", exit %d, %d steps" % (rep["exit"],
  186. rep["steps"])
  187. print(" %-22s PASS %d bytes%s" % (c["name"], len(got), tail))
  188. if show:
  189. return 0
  190. print()
  191. print("exec86: %d passed, %d failed (of %d)%s"
  192. % (passed, failed, len(sel),
  193. ", cross-checked against qemu" if cross else ", qemu cross-check off"))
  194. return 1 if failed else 0
  195. if __name__ == "__main__":
  196. sys.exit(main(sys.argv[1:]))