a73x

461245b2

test: a tile the zoom is not on paints nothing at all

a73x   2026-08-20 04:19

Commit message
test: a tile the zoom is not on paints nothing at all

The suite could not see this. A capture is a byte STREAM, so a stripe and
a full-screen paint of the same rows leave the same bytes and overpainting
is invisible to grep — which is why dropping the paint gate outright
passed 51 scenarios. What IS visible is a marker arriving at a moment it
should not have arrived at all.

Two daemons, two shells: the hidden session blocks on a flag file and the
ZOOMED session's own shell — typed at through the zoom — creates it. The
hidden output therefore happens strictly inside the zoom, with no sleep to
race. Its marker must reach this terminal exactly once, at the stripe
repaint after the unzoom.

test/e2e.sh
Old New
@@ -345,6 +345,18 @@ SOCK42="${TMPDIR:-/tmp}/muxd-e2e-zoomappmouse-$$.sock"
345 D39PID="" 345 D39PID=""
346 ZMOUSESH="${TMPDIR:-/tmp}/mux-e2e-zoomappmouse-$$.sh" 346 ZMOUSESH="${TMPDIR:-/tmp}/mux-e2e-zoomappmouse-$$.sh"
347 347
348 # A tile the zoom is NOT on. Two daemons because the leg needs two sessions
349 # with DIFFERENT shells — one typed at through the zoom, one blocking on a
350 # flag that shell creates — and a daemon has one shell for all its sessions.
351 SOCK43="${TMPDIR:-/tmp}/muxd-e2e-hidetile-a-$$.sock"
352 D40PID=""
353 ZHASH="${TMPDIR:-/tmp}/mux-e2e-hidetile-a-$$.sh"
354 SOCK44="${TMPDIR:-/tmp}/muxd-e2e-hidetile-b-$$.sock"
355 D41PID=""
356 ZHBSH="${TMPDIR:-/tmp}/mux-e2e-hidetile-b-$$.sh"
357 ZHFLAG="${TMPDIR:-/tmp}/mux-e2e-hidetile-$$.flag"
358 ZHSCRIPT="${TMPDIR:-/tmp}/mux-e2e-hidetile-$$.script"
359
348 # The wall as attach HISTORY (phase 2). A daemon AND a state home of its 360 # The wall as attach HISTORY (phase 2). A daemon AND a state home of its
349 # own, for the dynamic-wall leg's reason turned up one notch: what these 361 # own, for the dynamic-wall leg's reason turned up one notch: what these
350 # two blocks read back is the wall FILE, and every `mux` in this suite now 362 # two blocks read back is the wall FILE, and every `mux` in this suite now
@@ -983,6 +995,8 @@ cleanup() {
983 [ -n "$D37PID" ] && kill "$D37PID" 2>/dev/null || true 995 [ -n "$D37PID" ] && kill "$D37PID" 2>/dev/null || true
984 [ -n "$D38PID" ] && kill "$D38PID" 2>/dev/null || true 996 [ -n "$D38PID" ] && kill "$D38PID" 2>/dev/null || true
985 [ -n "$D39PID" ] && kill "$D39PID" 2>/dev/null || true 997 [ -n "$D39PID" ] && kill "$D39PID" 2>/dev/null || true
998 [ -n "$D40PID" ] && kill "$D40PID" 2>/dev/null || true
999 [ -n "$D41PID" ] && kill "$D41PID" 2>/dev/null || true
986 # The stops still precede the socket rm below, like SOCK14-17 above: 1000 # The stops still precede the socket rm below, like SOCK14-17 above:
987 # unlinking a socket first would leave a live daemon nothing could reach 1001 # unlinking a socket first would leave a live daemon nothing could reach
988 # by path. 1002 # by path.
@@ -1007,6 +1021,8 @@ cleanup() {
1007 [ -S "$SOCK40" ] && "$MUXD" stop --sock "$SOCK40" 2>/dev/null || true 1021 [ -S "$SOCK40" ] && "$MUXD" stop --sock "$SOCK40" 2>/dev/null || true
1008 [ -S "$SOCK41" ] && "$MUXD" stop --sock "$SOCK41" 2>/dev/null || true 1022 [ -S "$SOCK41" ] && "$MUXD" stop --sock "$SOCK41" 2>/dev/null || true
1009 [ -S "$SOCK42" ] && "$MUXD" stop --sock "$SOCK42" 2>/dev/null || true 1023 [ -S "$SOCK42" ] && "$MUXD" stop --sock "$SOCK42" 2>/dev/null || true
1024 [ -S "$SOCK43" ] && "$MUXD" stop --sock "$SOCK43" 2>/dev/null || true
1025 [ -S "$SOCK44" ] && "$MUXD" stop --sock "$SOCK44" 2>/dev/null || true
1010 1026
1011 # ---- the leak sweep (hygiene kit, 6a) ---- 1027 # ---- the leak sweep (hygiene kit, 6a) ----
1012 # Here rather than at the bottom of the file, which `set -e` reaches only 1028 # Here rather than at the bottom of the file, which `set -e` reaches only
@@ -1021,7 +1037,7 @@ cleanup() {
1021 "$D20PID" "$D21PID" "$D22PID" "$D23PID" "$D24PID" "$D25PID" \ 1037 "$D20PID" "$D21PID" "$D22PID" "$D23PID" "$D24PID" "$D25PID" \
1022 "$D26PID" "$D27PID" "$D28PID" "$D29PID" "$D30PID" "$D31PID" \ 1038 "$D26PID" "$D27PID" "$D28PID" "$D29PID" "$D30PID" "$D31PID" \
1023 "$D32PID" "$D33PID" "$D34PID" "$D35PID" "$D36PID" "$D37PID" \ 1039 "$D32PID" "$D33PID" "$D34PID" "$D35PID" "$D36PID" "$D37PID" \
1024 "$D38PID" "$D39PID" 1040 "$D38PID" "$D39PID" "$D40PID" "$D41PID"
1025 _leak=0 1041 _leak=0
1026 leak_sweep "$_rc" || _leak=1 1042 leak_sweep "$_rc" || _leak=1
1027 1043
@@ -1081,6 +1097,9 @@ cleanup() {
1081 "$OUT.zwcapg" "$OUT.zwstop" "$ZWHEELSH" \ 1097 "$OUT.zwcapg" "$OUT.zwstop" "$ZWHEELSH" \
1082 "$SOCK42" "$OUT.zm2.d" "$OUT.zm2cap" "$OUT.zm2cap.err" "$OUT.zm2pc" \ 1098 "$SOCK42" "$OUT.zm2.d" "$OUT.zm2cap" "$OUT.zm2cap.err" "$OUT.zm2pc" \
1083 "$OUT.zm2capg" "$OUT.zm2stop" "$ZMOUSESH" \ 1099 "$OUT.zm2capg" "$OUT.zm2stop" "$ZMOUSESH" \
1100 "$SOCK43" "$SOCK44" "$OUT.zh.da" "$OUT.zh.db" "$OUT.zhcap" \
1101 "$OUT.zhcap.err" "$OUT.zhpc" "$OUT.zhstopa" "$OUT.zhstopb" \
1102 "$ZHASH" "$ZHBSH" "$ZHFLAG" "$ZHSCRIPT" \
1084 "$SOCK33" "$SOCK34" "$OUT.whlcap" "$OUT.msecap" \ 1103 "$SOCK33" "$SOCK34" "$OUT.whlcap" "$OUT.msecap" \
1085 "$OUT.whlstop" "$OUT.msestop" 1104 "$OUT.whlstop" "$OUT.msestop"
1086 rm -f "$OUT.pgr" "$OUT.pgr.err" "$OUT.pgr.log" "$OUT.pgr.d" "$LESSSH" \ 1105 rm -f "$OUT.pgr" "$OUT.pgr.err" "$OUT.pgr.log" "$OUT.pgr.d" "$LESSSH" \
@@ -5954,6 +5973,117 @@ assert_stopped "$SOCK42" "$D39PID" "zoom app mouse" "$OUT.zm2stop"
5954 D39PID="" 5973 D39PID=""
5955 ok "an application in a zoomed tile gets the wheel, and the tile does not" 5974 ok "an application in a zoomed tile gets the wheel, and the tile does not"
5956 5975
5976 # ---- a tile the zoom is not on paints NOTHING --------------------------
5977 #
5978 # The other half of "zoom is a lens": while one tile owns the terminal, the
5979 # tiles that do not own it must be silent — not merely cropped to their
5980 # stripes, SILENT. A hidden tile's session goes on producing frames the
5981 # whole time (that is the point of keeping every pump hot), and each one is
5982 # a full-screen repaint waiting to happen on somebody else's zoom.
5983 #
5984 # Phase 1 spelled that decision at four call sites inside the pump; phase 3b
5985 # spells it once, in the paint sink every Core paint now asks
5986 # (`tilePaintBegin`). One place is worth a test that fires when it goes
5987 # wrong, and nothing in this suite could see it before: a capture is a byte
5988 # STREAM, so a stripe and a full-screen paint of the same rows leave the
5989 # same bytes, and overpainting is invisible to grep. What IS visible is a
5990 # marker arriving at a moment it should not have arrived at all.
5991 #
5992 # So the leg is a COUNT, and the marker is produced on demand rather than on
5993 # a timer:
5994 #
5995 # * two daemons, two default sessions, one wall. The second session's
5996 # shell blocks on a flag file and prints `bhidden-pin` when it appears.
5997 # * the zoom goes to the FIRST tile, and it is the first session's own
5998 # shell — typed at, through the zoom — that creates the flag. So the
5999 # hidden tile's output happens strictly INSIDE the zoom, with no sleep
6000 # to race and no ordering to hope for.
6001 # * `bhidden-pin` must therefore appear exactly ONCE in this terminal's
6002 # whole life: the stripe repaint after the unzoom, from the replica that
6003 # was hot the entire time. A second occurrence is the hidden tile having
6004 # painted over the session the user was typing at.
6005 #
6006 # The zoomed session gets its own marker in between, so a leg that lost the
6007 # zoom (and would then pass the count for the wrong reason) fails loudly on
6008 # the expect instead.
6009 cat > "$ZHASH" <<'EOF'
6010 #!/bin/sh
6011 printf 'zha-%s\n' pin
6012 exec /bin/sh
6013 EOF
6014 chmod +x "$ZHASH"
6015 # Written with an UNQUOTED heredoc: the flag path carries this run's pid and
6016 # has to be baked in. Nothing else in it is a shell expansion.
6017 cat > "$ZHBSH" <<EOF
6018 #!/bin/sh
6019 printf 'zhb-%s\n' pin
6020 while [ ! -f "$ZHFLAG" ]; do sleep 0.2; done
6021 printf 'bhidden-%s\n' pin
6022 exec /bin/cat
6023 EOF
6024 chmod +x "$ZHBSH"
6025 rm -f "$ZHFLAG"
6026 "$MUXD" run --sock "$SOCK43" --shell "$ZHASH" > "$OUT.zh.da" 2>&1 &
6027 D40PID=$!
6028 wait_sock "$SOCK43" "$OUT.zh.da" "hidden-tile daemon A never bound"
6029 "$MUXD" run --sock "$SOCK44" --shell "$ZHBSH" > "$OUT.zh.db" 2>&1 &
6030 D41PID=$!
6031 wait_sock "$SOCK44" "$OUT.zh.db" "hidden-tile daemon B never bound"
6032 # ONE anchor for the wall, scenario 33's lesson: tile threads race and
6033 # `expect` is a stream cursor, so the second of two markers is buried for
6034 # good whenever it painted first. The settle after it is what says both
6035 # stripes have finished.
6036 #
6037 # The ptyclient script is a FILE rather than a heredoc for the same reason
6038 # the B shell is unquoted: `$ZHFLAG` has to expand, and this suite's
6039 # scenario heredocs are quoted precisely so that backslash escapes reach
6040 # ptyclient untouched. Nothing here needs one that bash would eat.
6041 cat > "$ZHSCRIPT" <<EOF
6042 expect zhb-pin 20000
6043 settle 900 20000
6044 send \r
6045 settle 900 20000
6046 send : > "$ZHFLAG"\n
6047 settle 1500 20000
6048 send echo zhzoom-ok\n
6049 expect zhzoom-ok 15000
6050 settle 700 20000
6051 send \x1cw
6052 expect bhidden-pin 20000
6053 settle 900 20000
6054 send q
6055 waitexit 10000
6056 EOF
6057 set +e
6058 timeout 90 "$PTYCLIENT" --cols 96 --rows 30 --out "$OUT.zhcap" --err "$OUT.zhcap.err" -- \
6059 "$MUX" wall "--sock $SOCK43" "--sock $SOCK44" > "$OUT.zhpc" 2>&1 < "$ZHSCRIPT"
6060 RC=$?
6061 set -e
6062 [ "$RC" -eq 0 ] || {
6063 echo "e2e FAIL: hidden tile: ptyclient leg exited $RC (did the zoom take, did the flag land?):"
6064 cat "$OUT.zhpc"; exit 1; }
6065 # The flag really was created by the ZOOMED session's shell, so the hidden
6066 # session really did have something to say while it was hidden. Without this
6067 # the count below would pass on a session that simply never printed.
6068 [ -f "$ZHFLAG" ] || {
6069 echo "e2e FAIL: hidden tile: the zoom never created the flag, so the hidden"
6070 echo " session never printed and the count proves nothing:"
6071 cat "$OUT.zhpc"; exit 1; }
6072 # The money assertion. `grep -o`, not `grep -c`: a stripe paint is escape
6073 # sequences with no newlines in them, so a line count reads 1 either way.
6074 _zh_paints=$(grep -oa "bhidden-pin" "$OUT.zhcap" | wc -l)
6075 [ "$_zh_paints" -eq 1 ] || {
6076 echo "e2e FAIL: hidden tile: the hidden session's marker reached this terminal"
6077 echo " $_zh_paints time(s); exactly one — the stripe repaint after the"
6078 echo " unzoom — is the whole budget, and the rest is a tile painting"
6079 echo " over the session the user was typing at:"
6080 cat "$OUT.zhpc"; exit 1; }
6081 assert_stopped "$SOCK43" "$D40PID" "hidden tile A" "$OUT.zhstopa"
6082 D40PID=""
6083 assert_stopped "$SOCK44" "$D41PID" "hidden tile B" "$OUT.zhstopb"
6084 D41PID=""
6085 ok "a tile the zoom is not on paints nothing at all, however loud its session gets"
6086
5957 # ---- a tile whose pump has died still says something --------------------- 6087 # ---- a tile whose pump has died still says something ---------------------
5958 # 6088 #
5959 # Only pumps answer `repaint_gen`, so a tile whose pump has ENDED has 6089 # Only pumps answer `repaint_gen`, so a tile whose pump has ENDED has
@@ -6386,8 +6516,8 @@ DPID=""
6386 # convergence point either: what it asserts on is that file shrinking by 6516 # convergence point either: what it asserts on is that file shrinking by
6387 # one line while the session it named goes on answering, which is two 6517 # one line while the session it named goes on answering, which is two
6388 # facts a grid comparison speaks to neither of. 6518 # facts a grid comparison speaks to neither of.
6389 [ "$OK_COUNT" = "51" ] || { 6519 [ "$OK_COUNT" = "52" ] || {
6390 echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 51 —" 6520 echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 52 —"
6391 echo " a scenario was added (update the pin) or silently lost" 6521 echo " a scenario was added (update the pin) or silently lost"
6392 exit 1 6522 exit 1
6393 } 6523 }
@@ -6395,4 +6525,4 @@ DPID=""
6395 echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 35" 6525 echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 35"
6396 exit 1 6526 exit 1
6397 } 6527 }
6398 echo "e2e OK (51 scenarios, 35 convergence points)" 6528 echo "e2e OK (52 scenarios, 35 convergence points)"