85da1e61
test: tp2 — a pty client attaches at 100x30 and resizes to 90x28
a73x 2026-08-10 09:02
Commit message
test/e2e.sh
| Old | New | ||
|---|---|---|---|
| @@ -63,6 +63,11 @@ QPORT4=$(( 36000 + ($$ % 4000) )) | |||
| 63 | SPID="" | 63 | SPID="" |
| 64 | TPID="" | 64 | TPID="" |
| 65 | GPID="" | 65 | GPID="" |
| 66 | # M12 pty scenarios. Each needs a daemon whose grid size it owns: tp2 | ||
| 67 | # resizes the grid twice and tp1's session is a scrollback-generating | ||
| 68 | # wrapper, so neither can share the long-lived /bin/sh daemon. | ||
| 69 | SOCK12="${TMPDIR:-/tmp}/muxd-e2e-tp2-$$.sock" | ||
| 70 | D12PID="" | ||
| 66 | 71 | ||
| 67 | # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one | 72 | # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one |
| 68 | # such line on exit; every field is a key=value pair, so a rename or reorder | 73 | # such line on exit; every field is a key=value pair, so a rename or reorder |
| @@ -134,14 +139,27 @@ proxy_pid() { | |||
| 134 | } | 139 | } |
| 135 | 140 | ||
| 136 | # --- M11: render-vs-dump convergence ----------------------------------- | 141 | # --- M11: render-vs-dump convergence ----------------------------------- |
| 137 | # converged_quiet CLIENT_OUT SOCK — render the captured client stream and | 142 | # converged_quiet CLIENT_OUT SOCK [COLS ROWS] — render the captured client |
| 138 | # diff it against the daemon's grid, plain and styled. Nonzero on | 143 | # stream and diff it against the daemon's grid, plain and styled. Nonzero on |
| 139 | # divergence, leaving CLIENT_OUT.{render,dump,diff,rvt,dvt} behind for | 144 | # divergence, leaving CLIENT_OUT.{render,dump,diff,rvt,dvt} behind for |
| 140 | # inspection. CLIENT_OUT must be PURE client stdout: a capture taken with | 145 | # inspection. CLIENT_OUT must be PURE client stdout: a capture taken with |
| 141 | # 2>&1 has exit messages and predict stats mixed into the escape stream. | 146 | # 2>&1 has exit messages and predict stats mixed into the escape stream. |
| 147 | # The optional size is for pty scenarios whose grids are not the non-tty | ||
| 148 | # 80x24 default; render must replay into the same dimensions the daemon | ||
| 149 | # holds or the diff compares two honest grids of different shapes. | ||
| 150 | # It is an INPUT, not something this check validates, and the error is | ||
| 151 | # one-sided: too small re-wraps or clips a row and diverges loudly (the | ||
| 152 | # 80x24 default against tp2b's 90-wide grid does exactly that), while too | ||
| 153 | # large only adds trailing blanks that the normalization strips, and | ||
| 154 | # passes. Pass the size the scenario actually ran at. | ||
| 142 | converged_quiet() { | 155 | converged_quiet() { |
| 143 | _co="$1"; _cs="$2" | 156 | _co="$1"; _cs="$2"; _sz="" |
| 144 | "$RENDER" < "$_co" > "$_co.render" || return 1 | 157 | # Not `[ $# -ge 4 ] && _sz=...`: as the last command of a function under |
| 158 | # `set -e`, a false guard would return nonzero — a 2-argument call would | ||
| 159 | # report divergence it never looked for. | ||
| 160 | if [ $# -ge 4 ]; then _sz="--cols $3 --rows $4"; fi | ||
| 161 | # shellcheck disable=SC2086 — $_sz is two flags or nothing, never data | ||
| 162 | "$RENDER" $_sz < "$_co" > "$_co.render" || return 1 | ||
| 145 | "$MUXD" dump --sock "$_cs" > "$_co.dump" || return 1 | 163 | "$MUXD" dump --sock "$_cs" > "$_co.dump" || return 1 |
| 146 | # Trailing whitespace is a formatting difference between two correct | 164 | # Trailing whitespace is a formatting difference between two correct |
| 147 | # grids (padded vs unpadded row ends), not a divergence. | 165 | # grids (padded vs unpadded row ends), not a divergence. |
| @@ -153,7 +171,8 @@ converged_quiet() { | |||
| 153 | # or a leftover prediction underline — plain text dumps the same glyph | 171 | # or a leftover prediction underline — plain text dumps the same glyph |
| 154 | # either way, which is exactly why plain alone cannot carry M9's | 172 | # either way, which is exactly why plain alone cannot carry M9's |
| 155 | # overlay-never-becomes-state invariant. | 173 | # overlay-never-becomes-state invariant. |
| 156 | "$RENDER" --vt < "$_co" > "$_co.rvt" || return 1 | 174 | # shellcheck disable=SC2086 — same as above: flags or nothing |
| 175 | "$RENDER" --vt $_sz < "$_co" > "$_co.rvt" || return 1 | ||
| 157 | "$MUXD" dump --vt --sock "$_cs" > "$_co.dvt" || return 1 | 176 | "$MUXD" dump --vt --sock "$_cs" > "$_co.dvt" || return 1 |
| 158 | cmp -s "$_co.dvt" "$_co.rvt" || return 1 | 177 | cmp -s "$_co.dvt" "$_co.rvt" || return 1 |
| 159 | rm -f "$_co.render" "$_co.dump" "$_co.render.n" "$_co.dump.n" \ | 178 | rm -f "$_co.render" "$_co.dump" "$_co.render.n" "$_co.dump.n" \ |
| @@ -161,13 +180,17 @@ converged_quiet() { | |||
| 161 | return 0 | 180 | return 0 |
| 162 | } | 181 | } |
| 163 | 182 | ||
| 164 | # assert_converged CLIENT_OUT SOCK NAME — a scenario's LAST act, after | 183 | # assert_converged CLIENT_OUT SOCK NAME [COLS ROWS] — a scenario's LAST act, |
| 165 | # quiesce and after the client detached: mid-scenario the dump is still | 184 | # after quiesce and after the client detached: mid-scenario the dump is still |
| 166 | # moving, and another attach would claim the grid. | 185 | # moving, and another attach would claim the grid. |
| 167 | CONV_COUNT=0 | 186 | CONV_COUNT=0 |
| 168 | assert_converged() { | 187 | assert_converged() { |
| 169 | CONV_COUNT=$((CONV_COUNT + 1)) | 188 | CONV_COUNT=$((CONV_COUNT + 1)) |
| 170 | converged_quiet "$1" "$2" || { | 189 | if [ $# -ge 5 ]; then |
| 190 | converged_quiet "$1" "$2" "$4" "$5" | ||
| 191 | else | ||
| 192 | converged_quiet "$1" "$2" | ||
| 193 | fi || { | ||
| 171 | echo "e2e FAIL: $3: client render diverges from daemon grid (-daemon +client):" | 194 | echo "e2e FAIL: $3: client render diverges from daemon grid (-daemon +client):" |
| 172 | # An empty diff file is the styled-only case, not a passing one — | 195 | # An empty diff file is the styled-only case, not a passing one — |
| 173 | # the glyphs agree and the pens do not, which is precisely what the | 196 | # the glyphs agree and the pens do not, which is precisely what the |
| @@ -212,6 +235,7 @@ cleanup() { | |||
| 212 | [ -n "$D7PID" ] && kill "$D7PID" 2>/dev/null || true | 235 | [ -n "$D7PID" ] && kill "$D7PID" 2>/dev/null || true |
| 213 | [ -n "$D9PID" ] && kill "$D9PID" 2>/dev/null || true | 236 | [ -n "$D9PID" ] && kill "$D9PID" 2>/dev/null || true |
| 214 | [ -n "$D10PID" ] && kill "$D10PID" 2>/dev/null || true | 237 | [ -n "$D10PID" ] && kill "$D10PID" 2>/dev/null || true |
| 238 | [ -n "$D12PID" ] && kill "$D12PID" 2>/dev/null || true | ||
| 215 | # Detached daemons: killed by the pid their own up-line reported. | 239 | # Detached daemons: killed by the pid their own up-line reported. |
| 216 | [ -n "$SPID" ] && kill "$SPID" 2>/dev/null || true | 240 | [ -n "$SPID" ] && kill "$SPID" 2>/dev/null || true |
| 217 | [ -n "$TPID" ] && kill "$TPID" 2>/dev/null || true | 241 | [ -n "$TPID" ] && kill "$TPID" 2>/dev/null || true |
| @@ -238,6 +262,9 @@ cleanup() { | |||
| 238 | # would expand to a bare ".2" — an rm target in the CWD, not a temp file. | 262 | # would expand to a bare ".2" — an rm target in the CWD, not a temp file. |
| 239 | rm -f "$OUT.pc" "$OUT.pc.err" "$OUT.pc2" "$OUT.pc2.err" "$OUT.pc3" "$OUT.pc3.err" \ | 263 | rm -f "$OUT.pc" "$OUT.pc.err" "$OUT.pc2" "$OUT.pc2.err" "$OUT.pc3" "$OUT.pc3.err" \ |
| 240 | "$OUT.pc.log" "$OUT.pc.log.2" "$OUT.pc.log.3" | 264 | "$OUT.pc.log" "$OUT.pc.log.2" "$OUT.pc.log.3" |
| 265 | rm -f "$SOCK12" "$OUT.tp2.d" \ | ||
| 266 | "$OUT.tp2a" "$OUT.tp2a.err" "$OUT.tp2a.log" \ | ||
| 267 | "$OUT.tp2b" "$OUT.tp2b.err" "$OUT.tp2b.log" | ||
| 241 | # The convergence files a FAILING assert_converged leaves behind | 268 | # The convergence files a FAILING assert_converged leaves behind |
| 242 | # (.render/.dump/.rvt/.dvt/.diff for that capture) are deliberately not | 269 | # (.render/.dump/.rvt/.dvt/.diff for that capture) are deliberately not |
| 243 | # chased here: on a failing run they are the evidence. | 270 | # chased here: on a failing run they are the evidence. |
| @@ -1520,7 +1547,7 @@ D5PID="" | |||
| 1520 | # what it did see. A check that cannot fail proves nothing. | 1547 | # what it did see. A check that cannot fail proves nothing. |
| 1521 | PCLOG="$OUT.pc.log" | 1548 | PCLOG="$OUT.pc.log" |
| 1522 | set +e | 1549 | set +e |
| 1523 | "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.pc" --err "$OUT.pc.err" -- /bin/cat > "$PCLOG" 2>&1 <<'EOF' | 1550 | timeout 30 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.pc" --err "$OUT.pc.err" -- /bin/cat > "$PCLOG" 2>&1 <<'EOF' |
| 1524 | send hello\n | 1551 | send hello\n |
| 1525 | expect hello 10000 | 1552 | expect hello 10000 |
| 1526 | send \x04 | 1553 | send \x04 |
| @@ -1535,7 +1562,7 @@ grep -q "hello" "$OUT.pc" || { | |||
| 1535 | # The must-fail leg. 500ms: nothing is being waited FOR — the needle never | 1562 | # The must-fail leg. 500ms: nothing is being waited FOR — the needle never |
| 1536 | # arrives by construction — so the deadline only bounds the control's cost. | 1563 | # arrives by construction — so the deadline only bounds the control's cost. |
| 1537 | set +e | 1564 | set +e |
| 1538 | "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.pc2" --err "$OUT.pc2.err" -- /bin/cat > "$PCLOG.2" 2>&1 <<'EOF' | 1565 | timeout 30 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.pc2" --err "$OUT.pc2.err" -- /bin/cat > "$PCLOG.2" 2>&1 <<'EOF' |
| 1539 | expect never-going-to-match 500 | 1566 | expect never-going-to-match 500 |
| 1540 | EOF | 1567 | EOF |
| 1541 | RC=$? | 1568 | RC=$? |
| @@ -1553,7 +1580,7 @@ grep -q "did not arrive" "$PCLOG.2" || { | |||
| 1553 | # a child that writes to BOTH streams can tell a working split from a child | 1580 | # a child that writes to BOTH streams can tell a working split from a child |
| 1554 | # that simply never wrote to stderr. | 1581 | # that simply never wrote to stderr. |
| 1555 | set +e | 1582 | set +e |
| 1556 | "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.pc3" --err "$OUT.pc3.err" -- \ | 1583 | timeout 30 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.pc3" --err "$OUT.pc3.err" -- \ |
| 1557 | /bin/sh -c 'printf pc-stdout; printf pc-stderr 1>&2' > "$PCLOG.3" 2>&1 <<'EOF' | 1584 | /bin/sh -c 'printf pc-stdout; printf pc-stderr 1>&2' > "$PCLOG.3" 2>&1 <<'EOF' |
| 1558 | expect pc-stdout 10000 | 1585 | expect pc-stdout 10000 |
| 1559 | waitexit 10000 | 1586 | waitexit 10000 |
| @@ -1573,17 +1600,107 @@ rm -f "$OUT.pc" "$OUT.pc.err" "$OUT.pc2" "$OUT.pc2.err" "$OUT.pc3" "$OUT.pc3.err | |||
| 1573 | "$PCLOG" "$PCLOG.2" "$PCLOG.3" | 1600 | "$PCLOG" "$PCLOG.2" "$PCLOG.3" |
| 1574 | ok "ptyclient controls: pty echo roundtrips, impossible expect fails loudly, stderr stays off the capture" | 1601 | ok "ptyclient controls: pty echo roundtrips, impossible expect fails loudly, stderr stays off the capture" |
| 1575 | 1602 | ||
| 1603 | # ---- M12 tp2: resize under a real tty (campaign row 7's branch) -------- | ||
| 1604 | # Two clients, two convergence points, because convergence is a scenario's | ||
| 1605 | # LAST act after detach: tp2a exercises the snapshot resize prefix (a | ||
| 1606 | # 100x30 tty attaching to an 80x24 grid resizes the replica DOWN, then the | ||
| 1607 | # claim's answering snapshot resizes it back UP); tp2b exercises the winch | ||
| 1608 | # path mid-session and is the leg that catches row 7 — its 95-wide row is | ||
| 1609 | # two rows at 90 cols and one row at 100, so a replica whose width is | ||
| 1610 | # stale settles into a different SHAPE than the grid it is replicating. | ||
| 1611 | "$MUXD" run --sock "$SOCK12" --shell /bin/sh > "$OUT.tp2.d" 2>&1 & | ||
| 1612 | D12PID=$! | ||
| 1613 | i=0 | ||
| 1614 | while [ ! -S "$SOCK12" ] && [ "$i" -lt 50 ]; do sleep 0.1; i=$((i+1)); done | ||
| 1615 | [ -S "$SOCK12" ] || { echo "e2e FAIL: tp2 daemon never bound"; cat "$OUT.tp2.d"; exit 1; } | ||
| 1616 | |||
| 1617 | # Two rules govern every script below, and breaking either one produces a | ||
| 1618 | # capture that is honestly SHORT rather than wrong — a divergence whose | ||
| 1619 | # diff blames the product for a scenario's mistake: | ||
| 1620 | # | ||
| 1621 | # A marker must not appear in the command line that produces it. The | ||
| 1622 | # shell echoes what is typed, so `echo tp2-claim` paints the needle | ||
| 1623 | # before it has run anything; the expect then matches the echo and the | ||
| 1624 | # script detaches while the OUTPUT is still in flight. `printf 'tp2-%s' | ||
| 1625 | # claim` keeps the literal off the typed line, so a hit is proof the | ||
| 1626 | # shell ran it — the same argument-splitting trick the M5 scenario uses. | ||
| 1627 | # | ||
| 1628 | # The LAST thing expected must be the prompt, not the output. A shell | ||
| 1629 | # writes its result and its next prompt as two separate writes, so a | ||
| 1630 | # script that detaches on the result can stop one frame short of the | ||
| 1631 | # grid the daemon holds. PS1 is set to a marker here purely so that | ||
| 1632 | # prompt is assertable: it is the quiesce signal that would otherwise | ||
| 1633 | # have to be a sleep. | ||
| 1634 | set +e | ||
| 1635 | timeout 40 "$PTYCLIENT" --cols 100 --rows 30 --out "$OUT.tp2a" --err "$OUT.tp2a.err" -- \ | ||
| 1636 | "$MUX" --sock "$SOCK12" > "$OUT.tp2a.log" 2>&1 <<'EOF' | ||
| 1637 | expect \x1b[?1049h 10000 | ||
| 1638 | send PS1=tp2rdy@\n | ||
| 1639 | # Twice, and the fixture's cursor is what makes that meaningful: the first | ||
| 1640 | # hit is the ECHO of the assignment line, the second is the prompt the | ||
| 1641 | # shell painted from it. Only the second proves PS1 took effect. | ||
| 1642 | expect tp2rdy@ 10000 | ||
| 1643 | expect tp2rdy@ 10000 | ||
| 1644 | send printf 'tp2-%s\\n' claim\n | ||
| 1645 | expect tp2-claim 10000 | ||
| 1646 | expect tp2rdy@ 10000 | ||
| 1647 | send \x1c | ||
| 1648 | waitexit 10000 | ||
| 1649 | EOF | ||
| 1650 | RC=$? | ||
| 1651 | set -e | ||
| 1652 | [ "$RC" -eq 0 ] || { | ||
| 1653 | echo "e2e FAIL: tp2a ptyclient exited $RC:"; cat "$OUT.tp2a.log"; exit 1; } | ||
| 1654 | assert_converged "$OUT.tp2a" "$SOCK12" "pty attach at 100x30" 100 30 | ||
| 1655 | |||
| 1656 | set +e | ||
| 1657 | timeout 40 "$PTYCLIENT" --cols 100 --rows 30 --out "$OUT.tp2b" --err "$OUT.tp2b.err" -- \ | ||
| 1658 | "$MUX" --sock "$SOCK12" > "$OUT.tp2b.log" 2>&1 <<'EOF' | ||
| 1659 | expect \x1b[?1049h 10000 | ||
| 1660 | send printf 'tp2-%s\\n' live\n | ||
| 1661 | expect tp2-live 10000 | ||
| 1662 | resize 90 28 | ||
| 1663 | expect tp2-live 10000 | ||
| 1664 | send printf '%095d\\n' 7\n | ||
| 1665 | expect 00007 10000 | ||
| 1666 | expect tp2rdy@ 10000 | ||
| 1667 | send \x1c | ||
| 1668 | waitexit 10000 | ||
| 1669 | EOF | ||
| 1670 | RC=$? | ||
| 1671 | set -e | ||
| 1672 | [ "$RC" -eq 0 ] || { | ||
| 1673 | echo "e2e FAIL: tp2b ptyclient exited $RC:"; cat "$OUT.tp2b.log"; exit 1; } | ||
| 1674 | # The second `expect tp2-live` is the resize's answering snapshot, and it | ||
| 1675 | # is the leg's real assertion: `tp2-live` reaches the stream exactly once | ||
| 1676 | # per full paint (the typed line says `tp2-%s`), the cursor consumed that | ||
| 1677 | # one, and nothing in the session wrote anything since — so only a REPAINT | ||
| 1678 | # can satisfy it, and the resize is the only thing that could have asked | ||
| 1679 | # for one. | ||
| 1680 | # | ||
| 1681 | # `00007` is a synchronization point and NOT a discriminator, which is | ||
| 1682 | # worth saying plainly: the 95-wide row wraps into 90 zeros and "00007" at | ||
| 1683 | # 90 cols, but an unwrapped 95-char row ends in those same five bytes, so | ||
| 1684 | # the needle arrives either way. What the stale width would change is the | ||
| 1685 | # SHAPE of the grid — one row or two — and only the convergence check | ||
| 1686 | # below can see that. PS1 is still tp2rdy@ from tp2a: same daemon, same | ||
| 1687 | # shell, and the assignment outlived the client that typed it. | ||
| 1688 | assert_converged "$OUT.tp2b" "$SOCK12" "pty resize mid-session" 90 28 | ||
| 1689 | rm -f "$OUT.tp2a" "$OUT.tp2a.err" "$OUT.tp2a.log" \ | ||
| 1690 | "$OUT.tp2b" "$OUT.tp2b.err" "$OUT.tp2b.log" "$OUT.tp2.d" | ||
| 1691 | ok "a pty client resizes: snapshot prefix applied, winch follows the tty" | ||
| 1692 | |||
| 1576 | # The pins. Literals, not variables set from counting something else — | 1693 | # The pins. Literals, not variables set from counting something else — |
| 1577 | # "assert the literal, never the constant the code under test reads" | 1694 | # "assert the literal, never the constant the code under test reads" |
| 1578 | # (decisions.md, M10). 11 scenario checkpoints; 22 convergence points. | 1695 | # (decisions.md, M10). 12 scenario checkpoints; 24 convergence points. |
| 1579 | # Anyone adding a scenario updates these by hand, on purpose. | 1696 | # Anyone adding a scenario updates these by hand, on purpose. |
| 1580 | [ "$OK_COUNT" = "11" ] || { | 1697 | [ "$OK_COUNT" = "12" ] || { |
| 1581 | echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 11 —" | 1698 | echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 12 —" |
| 1582 | echo " a scenario was added (update the pin) or silently lost" | 1699 | echo " a scenario was added (update the pin) or silently lost" |
| 1583 | exit 1 | 1700 | exit 1 |
| 1584 | } | 1701 | } |
| 1585 | [ "$CONV_COUNT" = "22" ] || { | 1702 | [ "$CONV_COUNT" = "24" ] || { |
| 1586 | echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 22" | 1703 | echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 24" |
| 1587 | exit 1 | 1704 | exit 1 |
| 1588 | } | 1705 | } |
| 1589 | echo "e2e OK (11 scenarios, 22 convergence points)" | 1706 | echo "e2e OK (12 scenarios, 24 convergence points)" |
test/ptyclient.zig
| Old | New | ||
|---|---|---|---|
| @@ -306,8 +306,14 @@ pub fn main() !void { | |||
| 306 | // reporting success. Read to EIO before believing the | 306 | // reporting success. Read to EIO before believing the |
| 307 | // exit. Bounded, because anything OTHER than the client | 307 | // exit. Bounded, because anything OTHER than the client |
| 308 | // still holding the slave keeps the fd open forever, | 308 | // still holding the slave keeps the fd open forever, |
| 309 | // and a hung suite is worse than a short capture. | 309 | // and a hung suite is worse than a short capture. Two |
| 310 | // bounds: a quiet poll ends the drain normally, and the | ||
| 311 | // verb's own deadline ends it when the holder is never | ||
| 312 | // quiet — a process that ignores the HUP and keeps | ||
| 313 | // writing satisfies every poll and would otherwise spin | ||
| 314 | // here forever, filling --out as it went. | ||
| 310 | while (try drain(alloc, &pty, out, &exp)) { | 315 | while (try drain(alloc, &pty, out, &exp)) { |
| 316 | if (std.time.milliTimestamp() - start > deadline_ms) break; | ||
| 311 | var fds = [_]std.posix.pollfd{ | 317 | var fds = [_]std.posix.pollfd{ |
| 312 | .{ .fd = pty.master, .events = std.posix.POLL.IN, .revents = 0 }, | 318 | .{ .fd = pty.master, .events = std.posix.POLL.IN, .revents = 0 }, |
| 313 | }; | 319 | }; |