Jelajahi Sumber

tests: session summary for the nonvacuity interrupt-restore work

Records ff7b2dd and 5e0312e: Compiler.mod and the mod=11 probe sources join the
nonvacuity EXIT trap, and the signal handlers that make it fire at all.

Two findings the write-up exists to keep.  An unhandled signal does not run an
EXIT trap, so `trap restore_all EXIT' had never protected against interruption
despite the comment beside it promising it.  And on bash 5.2 the INT handler
usually never fires: a foreground child that dies of SIGINT makes bash treat the
signal as the child's and continue the script instead, so Ctrl-C does not abort
this run at all and SIGTERM is what actually stops it.  The first draft of that
comment claimed INT worked; an interrupt test disproved it.

Each claim in it came from killing a live run while a given file was dirty and
checking it came back byte-identical, not from reading the script.

Tag: v-TP3-INTERRUPT-RESTORE.
Eric Streit 2 hari lalu
induk
melakukan
2ae00d1c98
1 mengubah file dengan 153 tambahan dan 0 penghapusan
  1. 153 0
      v-TP3-INTERRUPT-RESTORE.md

+ 153 - 0
v-TP3-INTERRUPT-RESTORE.md

@@ -0,0 +1,153 @@
+v-TP3-INTERRUPT-RESTORE — Session summary
+========================================
+
+Tag:    v-TP3-INTERRUPT-RESTORE
+Commit: (the commit that adds this file)
+Fixes:  ff7b2dd "tests: nonvacuity restores Compiler.mod when the run is
+        interrupted" and 5e0312e "tests: the nonvacuity trap also restores the
+        mod=11 probe sources" — the two commits immediately before this one
+Covers: the nonvacuity harness's interrupt safety
+
+`v-TP3-STRCAT` (tag `v-TP3-STRCAT`, commit `1bed3bb`) is the string
+concatenation milestone this session began with; its write-up is
+`v-TP3-STRCAT.md`. This document covers the test-harness repair that finishing
+that milestone turned up, which was small in code and turned out to be two
+problems deep.
+
+The bug
+-------
+
+`shell/tests/nonvacuity.sh` mutates sources on purpose and has to put them back.
+It had an `EXIT` trap that restored `Runtime.mod`, `Exec86.mod`, `Shell.mod`,
+`tests/comimage.py` and `tests/comtest.py` — but not `Compiler.mod`, the file it
+mutates more than any other, because `SAVED_C` was defined at the BP-displacement
+section halfway down the file, well **after** the trap was set at line 153. An
+interrupted run therefore left `Compiler.mod` half-mutated, and `git status`
+showed a source that no case had finished with — the exact confusion that two
+agents colliding on one tree produces.
+
+Finding one: the trap had never fired for an interruption
+---------------------------------------------------------
+
+Moving `SAVED_C` above the trap was the obvious half. Testing it disproved the
+comment next to it:
+
+```
+bash -c 'trap "echo EXIT-TRAP" EXIT; sleep 30' &
+kill -TERM $!          # prints nothing
+```
+
+An **unhandled signal does not run an `EXIT` trap at all** — bash dies on the
+spot. So `trap restore_all EXIT` had only ever protected against an `exit`
+*inside* the script; `Runtime.mod` and the rest were never protected against
+interruption either, despite the comment promising "an interrupted run cannot
+leave the layout half-written".
+
+The fix is that a handler which only `exit`s is enough, because `exit` from a
+trap **does** fire the `EXIT` trap — and it has to exit, or the run would go on
+using a tree it has just put back:
+
+```
+trap restore_all EXIT
+trap 'exit 129' HUP
+trap 'exit 130' INT
+trap 'exit 143' TERM
+```
+
+Finding two: not every signal behaves the same, and Ctrl-C does not stop the run
+-------------------------------------------------------------------------------
+
+Measured on bash 5.2.37, not assumed:
+
+| signal | does the handler fire? |
+|---|---|
+| `TERM` | yes — restores everything |
+| `HUP` | yes — restores everything |
+| `INT` | **usually never arrives** |
+
+For `INT` the rule is: a foreground child that dies of `SIGINT` makes bash treat
+the signal as the *child's* and **continue the script instead**. So **Ctrl-C
+does not abort `nonvacuity.sh` at all** — it kills the current case's child and
+the next case starts. `SIGTERM` is what actually stops it. The first version of
+the comment claimed `INT` worked; the interrupt test disproved it, and the
+comment now records what was measured.
+
+`tests/probe/modrm11.{py,s}`
+---------------------------
+
+The last file group, committed separately. The mod=11 table section mutates the
+probe's own sources and puts them back after each case, so a run that died
+between the two left them edited. `SAVED_PY` / `SAVED_S` moved to the top with
+the other six copies.
+
+`restore_all` copies them **directly** rather than calling `restore_probe`, which
+is defined halfway down the file: a trap firing before that definition would
+abort the rest of the restore with `command not found`, putting back the copies
+before it and silently leaving the ones after it. Same failure mode the whole
+exercise is about.
+
+The trap now covers all eight:
+
+    Runtime.mod  Exec86.mod  Shell.mod  Compiler.mod
+    tests/comimage.py  tests/comtest.py
+    tests/probe/modrm11.py  tests/probe/modrm11.s
+
+Evidence
+--------
+
+Each claim came from killing a live run at the moment a given file was dirty:
+
+| file live | before | after |
+|---|---|---|
+| `Compiler.mod` | `DIRTY`, byte-different | `clean`, **byte-identical** |
+| `tests/probe/modrm11.py` | `DIRTY` | both probe sources **byte-identical** |
+
+The healthy path is unchanged: `non-vacuity: 78 ok, 0 failed`, and
+`run_all.sh` reports `OVERALL: ALL PASS` across all 15 suites.
+
+How to verify this work
+-----------------------
+
+    cd shell && tests/nonvacuity.sh        # expect: non-vacuity: 78 ok, 0 failed
+    cd shell && tests/run_all.sh           # expect: OVERALL: ALL PASS
+
+To reproduce the interrupt evidence — kill a run while a mutation is live and
+check the file comes back byte-identical:
+
+    cd shell
+    cp Compiler.mod /tmp/before
+    ./tests/nonvacuity.sh > /dev/null 2>&1 &
+    PID=$!
+    # wait until Compiler.mod differs, then:
+    kill -TERM $PID
+    wait $PID 2>/dev/null
+    cmp /tmp/before Compiler.mod && echo restored
+
+Note that `Compiler.mod` is restored by the trap but is a `.mod` source, so the
+trap's trailing `make` also rebuilds `Compiler.o` and `tpshell` from it — a
+restore that left a binary built from the mutation would still be reporting on
+the mutation.
+
+Next work
+---------
+
+Item 2 (the large one) is untouched: nested procedures / recursion, `var`
+parameters (the `SEG:OFF` push, RESUME-TP3.md 3.11), range/index checks
+(`TU_RANGE_CHECK`, `TU_INDEX_CHECK`), typed constants (3.14), `array` at its
+point of use (`t14`), `case` with subrange labels.
+
+Still `ENoLib` as unknown identifiers: the string standard functions `length`,
+`copy`, `pos`, `concat`, `delete`, `insert`, `str`. `+` and the six comparisons
+are done (`v-TP3-STRCAT`, `v-TP3-STRCMP`).
+
+Also still open:
+
+  - Harden the program-header parameter loop against non-advancing input
+    (`program p(1;)`) with a `BOOLEAN` flag — **not** `EXIT`, which ICEs gm2.
+  - FreeDOS (`freedos.qcow2`, FD14-LiveCD) is still untried; it is the only
+    third opinion on the `INT 21h` shim.
+  - `SAVED_C2`/`SAVED_R2` (the 8086-opcodes section) and `SAVED_R3` are per-case
+    snapshots taken mid-file. They are **fine as they are**: the files they
+    guard (`Compiler.mod`, `Runtime.mod`) are already trap-covered, so an
+    interruption there is restored by `restore_all` regardless of which
+    per-case copy was live.