comtest.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. #!/usr/bin/env python3
  2. """End-to-end UI test: C writes a real .COM, verified by reading the disk.
  3. uitest.py covers the error path (compile fails -> editor opens on the error).
  4. This covers the success path, which is the one that produces an artifact:
  5. 1. W loads a work file (LoadWorkFile ends with a Pause: needs a filler key)
  6. 2. O opens the Options submenu and C selects Destination = .COM
  7. 3. ESC returns to the main menu
  8. 4. C compiles, and the shell reports the .COM it wrote
  9. 5. The .COM is then INDEPENDENTLY checked on disk: the entry jump and the
  10. runtime's first bytes, a code-only file ending at hdrCS, the fixed data
  11. base outside the file, and the program header words
  12. Step 5 is the point. "The shell said it wrote a file" is not evidence that
  13. the file is right, and the shell reports success from the same state that
  14. produced the bytes, so the check that matters happens outside the shell.
  15. The .COM is removed afterwards so the test can be re-run.
  16. Usage: comtest.py [fixture.pas]
  17. Passing a fixture that does NOT compile is expected to be RED - that is the
  18. test being non-vacuous, not a failure of the shell. It was checked that way
  19. (t14_types.pas, which raises ENoLib for 'array' at its point of use).
  20. """
  21. import os
  22. import sys
  23. sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
  24. from ptyharness import SHELL_DIR, Screen, drain, reap, send, spawn, status_str, visible
  25. FIXTURE = os.path.abspath(sys.argv[1]) if len(sys.argv) > 1 else os.path.join(
  26. os.path.dirname(os.path.abspath(__file__)), "fixtures", "t26_str_mixed_args.pas")
  27. COM = os.path.splitext(FIXTURE)[0] + ".COM"
  28. # The image layout - the entry jump, the header's size, the load bias,
  29. # initmem's first bytes - and the function that MEASURES where the header is
  30. # live in tests/comimage.py and are imported, not copied. This checker had one
  31. # copy of both and the independent checker in tests/run_com_tests.sh had a
  32. # second; the third reader (tests/check_framedisp.py) had neither and wrote
  33. # down RT_SZ = 391 instead, which had long since drifted from the runtime it
  34. # claimed to track, so its sweep began inside the runtime rather than at the
  35. # program's first instruction and reported a clean PASS over bytes the program
  36. # never executes. Two copies of a constant that moves are two chances to be
  37. # confidently wrong, so there is one now, and tests/check_comimage.py asserts
  38. # that it stays one.
  39. #
  40. # RTSZ, PROLOG and DATAB below are derived per .COM from find_header(d) - never
  41. # written down. The image starts with a three-byte JMP (Compiler.Inittur) and
  42. # a .COM is entered at file offset 0, so before that jump existed this checker
  43. # ASSERTED that the runtime was at offset 0, which was precisely the bug: every
  44. # .COM began by executing initmem with whatever the loader left in AX. A
  45. # checker that pins a wrong invariant is worse than none, because it makes the
  46. # wrong thing look tested.
  47. from comimage import (ENT_SZ, HDR_SZ, LOAD_BIAS, DATA_IMAGE, HEAD,
  48. HDR_DS_WORD, HDR_HEAP_WORD, find_header)
  49. def check_com(path, expected_src_len):
  50. """Return a list of problems (empty = the .COM is correct)."""
  51. errs = []
  52. if not os.path.exists(path):
  53. return ["no .COM file was written"]
  54. d = open(path, "rb").read()
  55. # Everything below is in terms of where the header actually is in THIS
  56. # file, measured, rather than where a literal once said it should be.
  57. hdr_off = find_header(d)
  58. if hdr_off is None:
  59. return ["no program header found: the layout this checker knows how to "
  60. "look for is not the one in the file, so every offset below "
  61. "would be a guess - reporting the guesses individually would "
  62. "be worse than saying so once"]
  63. RTSZ = hdr_off
  64. PROLOG = hdr_off + HDR_SZ
  65. DATAB = DATA_IMAGE
  66. cs = (int.from_bytes(d[RTSZ + 2:RTSZ + 4], "little") - LOAD_BIAS) \
  67. if len(d) >= RTSZ + 4 else 0
  68. # The entry jump. This is the only assertion in the project that can see
  69. # where execution STARTS, because it is the only one that cares. It has
  70. # caught three real bugs in these same three bytes: no jump at all; a jump
  71. # to `pc` (the END of the code, in the zero gap); and a jump to RTSZ,
  72. # which is the program HEADER - data, which the CPU then decodes as
  73. # instructions. That last one is why the target is PROLOG and not RTSZ:
  74. # whether it works depends on how the header happens to decode, so
  75. # writeln('hi') slid through it unharmed while t07 hung on a LOCK-prefixed
  76. # ADD. Every other check here reads bytes at an offset the compiler chose
  77. # for itself.
  78. if len(d) >= ENT_SZ:
  79. if d[0] != 0xE9:
  80. errs.append("byte 0 is %02X, not the E9 of the entry jump" % d[0])
  81. want_rel = PROLOG - ENT_SZ
  82. got_rel = int.from_bytes(d[1:ENT_SZ], "little")
  83. if got_rel != want_rel:
  84. errs.append("entry jump rel16=%d, want %d; lands on image offset "
  85. "%d, want %d (the first instruction, %d bytes past the "
  86. "header - not the header at %d)"
  87. % (got_rel, want_rel, ENT_SZ + got_rel, PROLOG,
  88. HDR_SZ, RTSZ))
  89. if d[ENT_SZ:ENT_SZ + len(HEAD.split())].hex(" ").upper() != HEAD:
  90. errs.append("runtime not at offset %d: bytes there %s, want %s"
  91. % (ENT_SZ,
  92. d[ENT_SZ:ENT_SZ + len(HEAD.split())].hex(" ").upper(),
  93. HEAD))
  94. # The .COM is code-only. hdrCS - bias is the code end, which must be the
  95. # file length; the globals at DATA_IMAGE are outside the file and initmem
  96. # zeroes them.
  97. if cs != len(d):
  98. errs.append("file is %d bytes, but hdrCS says the code ends at %d"
  99. % (len(d), cs))
  100. # the runtime must be followed immediately by the program header,
  101. # whose first word is the "image is valid" flag
  102. if len(d) >= RTSZ + 2:
  103. flag = int.from_bytes(d[RTSZ:RTSZ + 2], "little")
  104. if flag != 1:
  105. errs.append("hdrFlag=%d at the program header, want 1" % flag)
  106. if len(d) >= RTSZ + 10:
  107. ds = int.from_bytes(d[RTSZ + HDR_DS_WORD:RTSZ + HDR_DS_WORD + 2], "little")
  108. heap = int.from_bytes(d[RTSZ + HDR_HEAP_WORD:RTSZ + HDR_HEAP_WORD + 2], "little")
  109. if ds != DATAB + LOAD_BIAS:
  110. errs.append("hdrDS=%d, want %d (= data base %d + load bias %d)"
  111. % (ds, DATAB + LOAD_BIAS, DATAB, LOAD_BIAS))
  112. if heap < ds:
  113. errs.append("hdrHeap=%d < hdrDS=%d" % (heap, ds))
  114. if [d[ENT_SZ + 4], d[ENT_SZ + 7]] != [HDR_DS_WORD, HDR_HEAP_WORD]:
  115. errs.append("initmem reads header words +%d/+%d, but the data base "
  116. "and data end are at +%d/+%d"
  117. % (d[ENT_SZ + 4], d[ENT_SZ + 7],
  118. HDR_DS_WORD, HDR_HEAP_WORD))
  119. return errs
  120. def main():
  121. if os.path.exists(COM):
  122. os.unlink(COM)
  123. pid, fd = spawn()
  124. checks = []
  125. try:
  126. drain(fd, quiet=0.6)
  127. send(fd, b"w")
  128. send(fd, FIXTURE.encode())
  129. send(fd, b"\r", quiet=0.8)
  130. send(fd, b" ", quiet=0.5) # clears LoadWorkFile's Pause
  131. # Options submenu -> Destination = .COM.
  132. # The submenu is left with Q (that is its Quit key, as in the
  133. # original's optmenu); ESC is swallowed by its REPEAT loop.
  134. send(fd, b"o", quiet=0.8)
  135. opt = visible(send(fd, b"c", quiet=0.5))
  136. checks.append(("Options submenu opened", "Compile" in opt))
  137. checks.append(("selecting C set Destination = COM", "COM" in opt))
  138. after = visible(send(fd, b"q", quiet=0.8))
  139. checks.append(("Q left the submenu for the main menu", "Main file" in after))
  140. comp = visible(send(fd, b"c", quiet=2.0))
  141. checks.append(("compile reported success", "Compiled OK" in comp))
  142. checks.append(("shell reported the Destination it used",
  143. "Destination" in comp))
  144. checks.append(("shell named the .COM it wrote",
  145. os.path.basename(COM) in comp))
  146. send(fd, b"\x1b", quiet=0.6)
  147. send(fd, b"q", quiet=0.8)
  148. finally:
  149. status = reap(fd, pid)
  150. checks.append(("shell exited cleanly (status 0)", os.WIFEXITED(status)
  151. and os.WEXITSTATUS(status) == 0))
  152. # now verify the artifact independently
  153. errs = check_com(COM, len(open(FIXTURE, "rb").read()))
  154. checks.append((".COM exists on disk and passes the independent check"
  155. if not errs else ".COM check: " + "; ".join(errs), not errs))
  156. checks.append((".COM was written next to the source, not in the shell's cwd",
  157. os.path.dirname(os.path.abspath(COM)) == os.path.dirname(FIXTURE)))
  158. print("UI TEST: %s" % os.path.basename(FIXTURE))
  159. print("-" * 60)
  160. ok = True
  161. for name, passed in checks:
  162. print("%-58s %s" % (name[:58], "PASS" if passed else "FAIL"))
  163. ok = ok and passed
  164. print("-" * 60)
  165. print("child: %s" % status_str(status))
  166. print("RESULT: %s" % ("ALL PASS" if ok else "FAILURES PRESENT"))
  167. if os.path.exists(COM):
  168. print("(removed %s)" % COM)
  169. os.unlink(COM)
  170. return 0 if ok else 1
  171. if __name__ == "__main__":
  172. sys.exit(main())