comtest.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  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 runtime's first
  10. bytes, a fully zero code/data gap, a size that covers the data area, and
  11. 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. # Restated here on purpose - the checker must not ask the code under test.
  29. # RTSZ was re-baselined 385 -> 391 together with the runtime bug fixes; see
  30. # the rationale in tests/run_com_tests.sh and tests/runtime.golden. HEAD is
  31. # initmem's prologue, whose two SI displacements are the header words this
  32. # checker goes on to verify, so the two are tied together by assertion rather
  33. # than by coincidence (that is how initmem came to read hdrMax instead of
  34. # hdrHeap and silently zero nothing).
  35. RTSZ = 391 # Runtime.RT_Size()
  36. DATAB = RTSZ + 0x1000 # Compiler: data base = rtSz + 1000H
  37. HEAD = "8B F0 8B 54 04 8B 4C 06" # MOV SI,AX / MOV DX,[SI+4] / MOV CX,[SI+6]
  38. HDR_DS_WORD = 4 # header word holding the data base
  39. HDR_HEAP_WORD = 6 # header word holding the data end
  40. assert [int(HEAD.split()[4], 16), int(HEAD.split()[7], 16)] == \
  41. [HDR_DS_WORD, HDR_HEAP_WORD], \
  42. "initmem no longer reads the two header words this checker verifies"
  43. def check_com(path, expected_src_len):
  44. """Return a list of problems (empty = the .COM is correct)."""
  45. errs = []
  46. if not os.path.exists(path):
  47. return ["no .COM file was written"]
  48. d = open(path, "rb").read()
  49. if d[:len(HEAD.split())].hex(" ").upper() != HEAD:
  50. errs.append("runtime not at offset 0: first bytes %s, want %s"
  51. % (d[:len(HEAD.split())].hex(" ").upper(), HEAD))
  52. if len(d) < DATAB:
  53. errs.append("file is %d bytes, shorter than the data base %d - the "
  54. "globals would be outside the file" % (len(d), DATAB))
  55. # the runtime must be followed immediately by the program header,
  56. # whose first word is the "image is valid" flag
  57. if len(d) >= RTSZ + 2:
  58. flag = int.from_bytes(d[RTSZ:RTSZ + 2], "little")
  59. if flag != 1:
  60. errs.append("hdrFlag=%d at the program header, want 1" % flag)
  61. if len(d) >= RTSZ + 10:
  62. ds = int.from_bytes(d[RTSZ + HDR_DS_WORD:RTSZ + HDR_DS_WORD + 2], "little")
  63. heap = int.from_bytes(d[RTSZ + HDR_HEAP_WORD:RTSZ + HDR_HEAP_WORD + 2], "little")
  64. if ds != DATAB:
  65. errs.append("hdrDS=%d, want %d" % (ds, DATAB))
  66. if heap < ds:
  67. errs.append("hdrHeap=%d < hdrDS=%d" % (heap, ds))
  68. if [d[4], d[7]] != [HDR_DS_WORD, HDR_HEAP_WORD]:
  69. errs.append("initmem reads header words +%d/+%d, but the data base "
  70. "and data end are at +%d/+%d"
  71. % (d[4], d[7], HDR_DS_WORD, HDR_HEAP_WORD))
  72. # the gap between the end of the code and the data area must be all zero
  73. cs = int.from_bytes(d[RTSZ + 2:RTSZ + 4], "little") if len(d) >= RTSZ + 4 else 0
  74. if cs and cs < DATAB:
  75. nz = sum(1 for b in d[cs:DATAB] if b)
  76. if nz:
  77. errs.append("%d non-zero bytes in the code/data gap" % nz)
  78. return errs
  79. def main():
  80. if os.path.exists(COM):
  81. os.unlink(COM)
  82. pid, fd = spawn()
  83. checks = []
  84. try:
  85. drain(fd, quiet=0.6)
  86. send(fd, b"w")
  87. send(fd, FIXTURE.encode())
  88. send(fd, b"\r", quiet=0.8)
  89. send(fd, b" ", quiet=0.5) # clears LoadWorkFile's Pause
  90. # Options submenu -> Destination = .COM.
  91. # The submenu is left with Q (that is its Quit key, as in the
  92. # original's optmenu); ESC is swallowed by its REPEAT loop.
  93. send(fd, b"o", quiet=0.8)
  94. opt = visible(send(fd, b"c", quiet=0.5))
  95. checks.append(("Options submenu opened", "Compile" in opt))
  96. checks.append(("selecting C set Destination = COM", "COM" in opt))
  97. after = visible(send(fd, b"q", quiet=0.8))
  98. checks.append(("Q left the submenu for the main menu", "Main file" in after))
  99. comp = visible(send(fd, b"c", quiet=2.0))
  100. checks.append(("compile reported success", "Compiled OK" in comp))
  101. checks.append(("shell reported the Destination it used",
  102. "Destination" in comp))
  103. checks.append(("shell named the .COM it wrote",
  104. os.path.basename(COM) in comp))
  105. send(fd, b"\x1b", quiet=0.6)
  106. send(fd, b"q", quiet=0.8)
  107. finally:
  108. status = reap(fd, pid)
  109. checks.append(("shell exited cleanly (status 0)", os.WIFEXITED(status)
  110. and os.WEXITSTATUS(status) == 0))
  111. # now verify the artifact independently
  112. errs = check_com(COM, len(open(FIXTURE, "rb").read()))
  113. checks.append((".COM exists on disk and passes the independent check"
  114. if not errs else ".COM check: " + "; ".join(errs), not errs))
  115. checks.append((".COM was written next to the source, not in the shell's cwd",
  116. os.path.dirname(os.path.abspath(COM)) == os.path.dirname(FIXTURE)))
  117. print("UI TEST: %s" % os.path.basename(FIXTURE))
  118. print("-" * 60)
  119. ok = True
  120. for name, passed in checks:
  121. print("%-58s %s" % (name[:58], "PASS" if passed else "FAIL"))
  122. ok = ok and passed
  123. print("-" * 60)
  124. print("child: %s" % status_str(status))
  125. print("RESULT: %s" % ("ALL PASS" if ok else "FAILURES PRESENT"))
  126. if os.path.exists(COM):
  127. print("(removed %s)" % COM)
  128. os.unlink(COM)
  129. return 0 if ok else 1
  130. if __name__ == "__main__":
  131. sys.exit(main())