5d5b6fa5
perf: one bell per drain, and the count pinned on a real terminal
a73x 2026-08-15 20:53
Commit message
src/server.zig
| Old | New | ||
|---|---|---|---|
| @@ -2238,7 +2238,33 @@ pub const Server = struct { | |||
| 2238 | // of bells is a single allocation that the first event sizes. | 2238 | // of bells is a single allocation that the first event sizes. |
| 2239 | var payload: std.ArrayList(u8) = .empty; | 2239 | var payload: std.ArrayList(u8) = .empty; |
| 2240 | defer payload.deinit(self.alloc); | 2240 | defer payload.deinit(self.alloc); |
| 2241 | // At most one bell per drain. N rings inside one 64 KiB chunk are one | ||
| 2242 | // ring to a human, and the bell is the cheapest event a session can | ||
| 2243 | // emit in bulk: 0x07 is ~1/256 of random bytes, so `cat` on a binary | ||
| 2244 | // produces ~256 frames per chunk — 6 bytes on the wire and a | ||
| 2245 | // writeAllFd on every client's stdout apiece, for a sound that cannot | ||
| 2246 | // ring 256 times. sampleTermModes states the discipline this restores: | ||
| 2247 | // "bytes proportional to what changed". Modes dedup, titles are | ||
| 2248 | // sampled, the grid folds into one bounded delta; the bell was the | ||
| 2249 | // only client-bound stream left that did not. | ||
| 2250 | // | ||
| 2251 | // Per DRAIN, not per session. A bell a second later is a separate ring | ||
| 2252 | // and gets its own frame — only a burst the user could not have heard | ||
| 2253 | // apart folds. A session-lifetime flag would silence every bell after | ||
| 2254 | // the first, which is a different feature and a broken one. | ||
| 2255 | // | ||
| 2256 | // The pending slot is unaffected, which is worth stating because it is | ||
| 2257 | // the half that could have gone wrong: it is one slot per kind stamped | ||
| 2258 | // with tracker.seq, and tracker.seq does not move inside this loop. So | ||
| 2259 | // 256 rings recorded the old way left exactly what one ring leaves — | ||
| 2260 | // the same payload at the same seq, after 255 pointless dupe-and-free | ||
| 2261 | // pairs. Coalescing removes the churn and changes no recorded state. | ||
| 2262 | var rang = false; | ||
| 2241 | for (s.eng.sideEvents()) |ev| { | 2263 | for (s.eng.sideEvents()) |ev| { |
| 2264 | if (ev.kind == .bell) { | ||
| 2265 | if (rang) continue; | ||
| 2266 | rang = true; | ||
| 2267 | } | ||
| 2242 | payload.clearRetainingCapacity(); | 2268 | payload.clearRetainingCapacity(); |
| 2243 | switch (ev.kind) { | 2269 | switch (ev.kind) { |
| 2244 | // What a failed encode gives up is this one event: a | 2270 | // What a failed encode gives up is this one event: a |
| @@ -7846,6 +7872,141 @@ test "Server: a BEL from the session reaches its client as a bell term_event" { | |||
| 7846 | } | 7872 | } |
| 7847 | } | 7873 | } |
| 7848 | 7874 | ||
| 7875 | test "Server: a burst of bells in one chunk is coalesced into one frame" { | ||
| 7876 | const alloc = std.testing.allocator; | ||
| 7877 | |||
| 7878 | var tmp = try TmpDir.make(); | ||
| 7879 | defer tmp.cleanup(); | ||
| 7880 | const sock_path = try std.fmt.allocPrint(alloc, "{s}/bellburst.sock", .{tmp.path()}); | ||
| 7881 | defer alloc.free(sock_path); | ||
| 7882 | |||
| 7883 | // A script rather than `cat`, unlike the test above, and the count is the | ||
| 7884 | // reason. Typed input is echoed by the tty, so five typed BELs can reach | ||
| 7885 | // the engine TWICE — once as the echo and once written back — in two | ||
| 7886 | // chunks and therefore two honest drains. A file writes them once, in one | ||
| 7887 | // write, so "one frame" here is a statement about coalescing rather than | ||
| 7888 | // about what ECHOCTL happened to do. | ||
| 7889 | // | ||
| 7890 | // The `read` is not decoration, and this test failed both ways without it | ||
| 7891 | // before it was added. A script that rings at once races the attach: the | ||
| 7892 | // shell can reach printf before the client is in the session, and then the | ||
| 7893 | // drain has nobody to queue to. The event IS recorded pending — but a | ||
| 7894 | // fresh attach takes the snapshot branch, which replays nothing (a | ||
| 7895 | // reattach quoting a seq is what collects a gap), so the ring is simply | ||
| 7896 | // gone and the test fails claiming no bell rather than too many. Ringing | ||
| 7897 | // only once released proves the client was there to miss it. | ||
| 7898 | try tmp.dir.writeFile(.{ | ||
| 7899 | .sub_path = "bellburst.sh", | ||
| 7900 | .data = | ||
| 7901 | \\#!/bin/sh | ||
| 7902 | \\read -r go | ||
| 7903 | \\printf '\007\007\007\007\007' | ||
| 7904 | \\exec sleep 30 | ||
| 7905 | \\ | ||
| 7906 | , | ||
| 7907 | .flags = .{ .mode = 0o755 }, | ||
| 7908 | }); | ||
| 7909 | const script = try std.fmt.allocPrintSentinel(alloc, "{s}/bellburst.sh", .{tmp.path()}, 0); | ||
| 7910 | defer alloc.free(script); | ||
| 7911 | |||
| 7912 | var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); | ||
| 7913 | defer srv.deinit(); | ||
| 7914 | |||
| 7915 | const c = try std.net.connectUnixSocket(sock_path); | ||
| 7916 | defer c.close(); | ||
| 7917 | try proto.writeFrame(c.handle, .attach, &proto.encodeAttach(80, 24, 0, 0)); | ||
| 7918 | // Awaited, not assumed: this is what makes the ring below strictly later | ||
| 7919 | // than the attach, and so what removes the race described above. | ||
| 7920 | (try awaitFrame(alloc, &srv, c.handle, .snapshot, 400) orelse | ||
| 7921 | return error.NoSnapshot).deinit(alloc); | ||
| 7922 | |||
| 7923 | // `go`, never a BEL: with a bell typed here the tty's own echo could | ||
| 7924 | // arrive as a separate event in a separate chunk, and this test would be | ||
| 7925 | // counting ECHOCTL rather than coalescing. | ||
| 7926 | try proto.writeFrame(c.handle, .input, "go\n"); | ||
| 7927 | |||
| 7928 | const ev = (try awaitFrame(alloc, &srv, c.handle, .term_event, 400)) orelse | ||
| 7929 | return error.NoTermEvent; | ||
| 7930 | defer ev.deinit(alloc); | ||
| 7931 | switch (try proto.decodeTermEvent(ev.payload)) { | ||
| 7932 | .bell => {}, | ||
| 7933 | .clipboard => return error.ExpectedBellGotClipboard, | ||
| 7934 | } | ||
| 7935 | |||
| 7936 | // The boundary, with the positive already in hand so it cannot pass | ||
| 7937 | // vacuously: the other four rings must not have become four more frames. | ||
| 7938 | // The budget keeps pumping long past the drain that produced the first, | ||
| 7939 | // which is the very pump the rest would have been queued in. | ||
| 7940 | if (try awaitFrame(alloc, &srv, c.handle, .term_event, 60)) |extra| { | ||
| 7941 | extra.deinit(alloc); | ||
| 7942 | return error.BellsNotCoalesced; | ||
| 7943 | } | ||
| 7944 | } | ||
| 7945 | |||
| 7946 | test "Server: a bell in a later chunk is its own frame, not folded into the first" { | ||
| 7947 | const alloc = std.testing.allocator; | ||
| 7948 | |||
| 7949 | var tmp = try TmpDir.make(); | ||
| 7950 | defer tmp.cleanup(); | ||
| 7951 | const sock_path = try std.fmt.allocPrint(alloc, "{s}/belltwice.sock", .{tmp.path()}); | ||
| 7952 | defer alloc.free(sock_path); | ||
| 7953 | |||
| 7954 | // Coalescing is PER DRAIN, and this is the half of that choice the burst | ||
| 7955 | // test above cannot see: hoisting the flag to session state would pass it | ||
| 7956 | // and silence every ring after the first for the session's whole life. | ||
| 7957 | // | ||
| 7958 | // Each ring is released by its own `read`, which buys two things. It puts | ||
| 7959 | // the two rings in separate chunks — so the second is a drain of its own | ||
| 7960 | // rather than a hoped-for split of one write — and it puts BOTH strictly | ||
| 7961 | // after the attach, which is the race the burst test above documents: a | ||
| 7962 | // shell that rings before the client is in the session has nobody to queue | ||
| 7963 | // to, and a fresh attach replays no pending event. | ||
| 7964 | // | ||
| 7965 | // What gets typed to release them is `go`, never a BEL. With `cat` the | ||
| 7966 | // echo of a typed bell could itself arrive as a second event, and this | ||
| 7967 | // test would pass on the leftovers of the first ring while the second was | ||
| 7968 | // being swallowed — green for precisely the bug it exists to catch. | ||
| 7969 | try tmp.dir.writeFile(.{ | ||
| 7970 | .sub_path = "belltwice.sh", | ||
| 7971 | .data = | ||
| 7972 | \\#!/bin/sh | ||
| 7973 | \\read -r one | ||
| 7974 | \\printf '\007' | ||
| 7975 | \\read -r two | ||
| 7976 | \\printf '\007' | ||
| 7977 | \\exec sleep 30 | ||
| 7978 | \\ | ||
| 7979 | , | ||
| 7980 | .flags = .{ .mode = 0o755 }, | ||
| 7981 | }); | ||
| 7982 | const script = try std.fmt.allocPrintSentinel(alloc, "{s}/belltwice.sh", .{tmp.path()}, 0); | ||
| 7983 | defer alloc.free(script); | ||
| 7984 | |||
| 7985 | var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = script }); | ||
| 7986 | defer srv.deinit(); | ||
| 7987 | |||
| 7988 | const c = try std.net.connectUnixSocket(sock_path); | ||
| 7989 | defer c.close(); | ||
| 7990 | try proto.writeFrame(c.handle, .attach, &proto.encodeAttach(80, 24, 0, 0)); | ||
| 7991 | (try awaitFrame(alloc, &srv, c.handle, .snapshot, 400) orelse | ||
| 7992 | return error.NoSnapshot).deinit(alloc); | ||
| 7993 | |||
| 7994 | try proto.writeFrame(c.handle, .input, "one\n"); | ||
| 7995 | const first = (try awaitFrame(alloc, &srv, c.handle, .term_event, 400)) orelse | ||
| 7996 | return error.NoFirstBell; | ||
| 7997 | first.deinit(alloc); | ||
| 7998 | |||
| 7999 | try proto.writeFrame(c.handle, .input, "two\n"); | ||
| 8000 | |||
| 8001 | const second = (try awaitFrame(alloc, &srv, c.handle, .term_event, 400)) orelse | ||
| 8002 | return error.SecondBellSwallowed; | ||
| 8003 | defer second.deinit(alloc); | ||
| 8004 | switch (try proto.decodeTermEvent(second.payload)) { | ||
| 8005 | .bell => {}, | ||
| 8006 | .clipboard => return error.ExpectedBellGotClipboard, | ||
| 8007 | } | ||
| 8008 | } | ||
| 8009 | |||
| 7849 | // --------------------------------------------------------------------------- | 8010 | // --------------------------------------------------------------------------- |
| 7850 | // The gap: a side-channel event produced while nobody was attached. | 8011 | // The gap: a side-channel event produced while nobody was attached. |
| 7851 | // | 8012 | // |
test/e2e.sh
| Old | New | ||
|---|---|---|---|
| @@ -209,6 +209,15 @@ QPORT5=$(( 56000 + ($$ % 4000) )) | |||
| 209 | M18KEY="${TMPDIR:-/tmp}/mux-e2e-m18key-$$" | 209 | M18KEY="${TMPDIR:-/tmp}/mux-e2e-m18key-$$" |
| 210 | D20PID="" | 210 | D20PID="" |
| 211 | 211 | ||
| 212 | # The bell leg's own daemon. A socket of its own is load-bearing rather than | ||
| 213 | # tidiness, and the argument is at the leg itself: that scenario counts BEL | ||
| 214 | # BYTES in a host capture, and a BEL is also what terminates an OSC. $SOCK's | ||
| 215 | # session has a window title set on it by the title leg, and every client | ||
| 216 | # attaching there is told so as `ESC]0;...BEL` — which would land in the | ||
| 217 | # capture and make the count assert something other than what it says. | ||
| 218 | SOCK24="${TMPDIR:-/tmp}/muxd-e2e-bell-$$.sock" | ||
| 219 | D21PID="" | ||
| 220 | |||
| 212 | # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one | 221 | # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one |
| 213 | # such line on exit; every field is a key=value pair, so a rename or reorder | 222 | # such line on exit; every field is a key=value pair, so a rename or reorder |
| 214 | # in the client shows up here as an empty read rather than a wrong number. | 223 | # in the client shows up here as an empty read rather than a wrong number. |
| @@ -722,6 +731,7 @@ cleanup() { | |||
| 722 | [ -n "$D18PID" ] && kill "$D18PID" 2>/dev/null || true | 731 | [ -n "$D18PID" ] && kill "$D18PID" 2>/dev/null || true |
| 723 | [ -n "$D19PID" ] && kill "$D19PID" 2>/dev/null || true | 732 | [ -n "$D19PID" ] && kill "$D19PID" 2>/dev/null || true |
| 724 | [ -n "$D20PID" ] && kill "$D20PID" 2>/dev/null || true | 733 | [ -n "$D20PID" ] && kill "$D20PID" 2>/dev/null || true |
| 734 | [ -n "$D21PID" ] && kill "$D21PID" 2>/dev/null || true | ||
| 725 | # The stops still precede the socket rm below, like SOCK14-17 above: | 735 | # The stops still precede the socket rm below, like SOCK14-17 above: |
| 726 | # unlinking a socket first would leave a live daemon nothing could reach | 736 | # unlinking a socket first would leave a live daemon nothing could reach |
| 727 | # by path. | 737 | # by path. |
| @@ -739,7 +749,7 @@ cleanup() { | |||
| 739 | "$D7PID" "$D9PID" "$D10PID" "$D12PID" "$D13PID" "$SPID" "$TPID" \ | 749 | "$D7PID" "$D9PID" "$D10PID" "$D12PID" "$D13PID" "$SPID" "$TPID" \ |
| 740 | "$GPID" "$APID" "$PAPID" "$HAPID" "$HDPID" \ | 750 | "$GPID" "$APID" "$PAPID" "$HAPID" "$HDPID" \ |
| 741 | "$D14PID" "$D15PID" "$D16PID" "$D17PID" "$D18PID" "$D19PID" \ | 751 | "$D14PID" "$D15PID" "$D16PID" "$D17PID" "$D18PID" "$D19PID" \ |
| 742 | "$D20PID" | 752 | "$D20PID" "$D21PID" |
| 743 | _leak=0 | 753 | _leak=0 |
| 744 | leak_sweep "$_rc" || _leak=1 | 754 | leak_sweep "$_rc" || _leak=1 |
| 745 | 755 | ||
| @@ -796,6 +806,12 @@ cleanup() { | |||
| 796 | # ...and its other half: the paste capture and the file nvim wrote, which | 806 | # ...and its other half: the paste capture and the file nvim wrote, which |
| 797 | # IS that scenario's assertion rather than a log beside it. | 807 | # IS that scenario's assertion rather than a log beside it. |
| 798 | rm -f "$OUT.paste" "$OUT.paste.err" "$OUT.paste.log" "$OUT.pasted.txt" | 808 | rm -f "$OUT.paste" "$OUT.paste.err" "$OUT.paste.log" "$OUT.pasted.txt" |
| 809 | # ...and the bell leg's five: the four the other side-channel legs have, | ||
| 810 | # plus the daemon capture, since this leg runs a daemon of its own. The | ||
| 811 | # daemon capture is rm'd here and never mid-suite, so the leak sweep above | ||
| 812 | # still has it to read. | ||
| 813 | rm -f "$SOCK24" "$OUT.d21.d" \ | ||
| 814 | "$OUT.bell" "$OUT.bell.err" "$OUT.bell.log" "$OUT.bell.sh" | ||
| 799 | # M14 handoff. The shim, both runtime dirs (each holding its daemon's | 815 | # M14 handoff. The shim, both runtime dirs (each holding its daemon's |
| 800 | # socket), the second key, the unusable config home, and the five | 816 | # socket), the second key, the unusable config home, and the five |
| 801 | # captures. | 817 | # captures. |
| @@ -3007,6 +3023,93 @@ grep -q '^ b = 2$' "$OUT.pasted.txt" || { | |||
| 3007 | rm_swept "$OUT.paste" "$OUT.paste.err" "$OUT.paste.log" "$OUT.pasted.txt" | 3023 | rm_swept "$OUT.paste" "$OUT.paste.err" "$OUT.paste.log" "$OUT.pasted.txt" |
| 3008 | ok "a paste into nvim keeps its indentation" | 3024 | ok "a paste into nvim keeps its indentation" |
| 3009 | 3025 | ||
| 3026 | # --- side channel: a burst of bells reaches the host as exactly one ------ | ||
| 3027 | # | ||
| 3028 | # This leg exists for the COALESCING, not for the bell. The client's | ||
| 3029 | # term_event dispatch is kind-agnostic — every event goes through one | ||
| 3030 | # writeSideChannel call with no per-kind branch — so the clipboard leg above | ||
| 3031 | # already proves frame → decode → builder → host tty for the whole frame | ||
| 3032 | # type, and a bell that merely ARRIVED would add no coverage: the one | ||
| 3033 | # bell-specific line past the wire is pinned directly in client.zig. What | ||
| 3034 | # only e2e can see is the NUMBER. Five rings inside one pty chunk must reach | ||
| 3035 | # the host as one (server.zig, drainSideEvents), and a regression there is | ||
| 3036 | # silent — every assertion anyone would think to write about a bell still | ||
| 3037 | # passes while 256 frames per chunk go out. | ||
| 3038 | # | ||
| 3039 | # So the assertion is a count, and a count of BEL BYTES is only unambiguous | ||
| 3040 | # if nothing else in the capture can contribute one. Two things make that | ||
| 3041 | # true here, and both are asserted rather than assumed: | ||
| 3042 | # | ||
| 3043 | # 1. A daemon of its own. The client writes exactly two OSCs, ever — | ||
| 3044 | # `ESC]52;` and `ESC]0;` (client.zig), each BEL-terminated — and $SOCK's | ||
| 3045 | # session has had a title set on it by the leg above, which every client | ||
| 3046 | # attaching there is then told about. On a fresh daemon nothing sets a | ||
| 3047 | # title and nothing copies, so neither OSC has an occasion to be built. | ||
| 3048 | # 2. The zero-OSC check below, which is what turns that argument into a | ||
| 3049 | # measurement. With no `ESC]` anywhere in the capture, every BEL in it | ||
| 3050 | # is a bare bell — there is no sequence left for one to be terminating. | ||
| 3051 | # | ||
| 3052 | # The session's own OSC 133 marks are BEL-terminated too and do not reach | ||
| 3053 | # here, because the engine consumes them into cmd_state rather than | ||
| 3054 | # forwarding bytes. That is not an assumption either: check 2 would see them. | ||
| 3055 | "$MUXD" run --sock "$SOCK24" --shell /bin/sh > "$OUT.d21.d" 2>&1 & | ||
| 3056 | D21PID=$! | ||
| 3057 | wait_sock "$SOCK24" "$OUT.d21.d" "bell daemon socket never appeared" | ||
| 3058 | |||
| 3059 | # Five rings in ONE printf, which is one write and therefore one pty read and | ||
| 3060 | # one drain. Split across two writes they would be two chunks and two frames | ||
| 3061 | # — correctly, since coalescing is per drain — so the single printf is what | ||
| 3062 | # makes "one" the right answer rather than a number that depends on timing. | ||
| 3063 | # | ||
| 3064 | # Emitted by a FILE, never typed, for the M12 reason spelled out on the | ||
| 3065 | # clipboard leg: the shell echoes what is typed, so a needle that could | ||
| 3066 | # arrive as an echo would pass on a client that forwards nothing at all. | ||
| 3067 | cat > "$OUT.bell.sh" <<'BELLSH' | ||
| 3068 | printf '\007\007\007\007\007' | ||
| 3069 | printf 'BELLDONE\n' | ||
| 3070 | BELLSH | ||
| 3071 | set +e | ||
| 3072 | timeout 40 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.bell" --err "$OUT.bell.err" \ | ||
| 3073 | -- "$MUX" --sock "$SOCK24" > "$OUT.bell.log" 2>&1 <<EOF | ||
| 3074 | expect \x1b[?1049h 15000 | ||
| 3075 | settle 400 15000 | ||
| 3076 | send sh $OUT.bell.sh\n | ||
| 3077 | expect BELLDONE 15000 | ||
| 3078 | settle 400 15000 | ||
| 3079 | send \x1c | ||
| 3080 | waitexit 10000 | ||
| 3081 | EOF | ||
| 3082 | RC=$? | ||
| 3083 | set -e | ||
| 3084 | [ "$RC" -eq 0 ] || { | ||
| 3085 | echo "e2e FAIL: bell scenario did not run: ptyclient exited $RC" | ||
| 3086 | cat "$OUT.bell.log"; cat -v "$OUT.bell.err" 2>/dev/null; exit 1; } | ||
| 3087 | # The positive control, as on every leg here: the marker travels the ordinary | ||
| 3088 | # grid path, so without it "one bell" and "no session" are the same reading — | ||
| 3089 | # and "zero bells" would pass the zero-OSC check below just as well. | ||
| 3090 | grep -qa 'BELLDONE' "$OUT.bell" || { | ||
| 3091 | echo "e2e FAIL: the session never ran (no marker on the host)"; exit 1; } | ||
| 3092 | # -F, and it is not decoration: the needle is `ESC]`, and `]` unescaped makes | ||
| 3093 | # this a malformed bracket expression — grep exits 2 and `grep && { fail }` | ||
| 3094 | # reads the error as "not found". That exact defect has already shipped in | ||
| 3095 | # this file once (see the dead-transport leg). | ||
| 3096 | OSCINTRO=$(printf '\033]') | ||
| 3097 | grep -qaF "$OSCINTRO" "$OUT.bell" && { | ||
| 3098 | echo "e2e FAIL: an OSC reached the bell capture, so its BELs cannot be counted" | ||
| 3099 | cat -v "$OUT.bell"; exit 1; } | ||
| 3100 | # ...and now the count means what it says. `occurrences` is the title leg's | ||
| 3101 | # helper, defined above and reused here on the same kind of byte needle. | ||
| 3102 | BELBYTE=$(printf '\007') | ||
| 3103 | BELLN=$(occurrences "$OUT.bell" "$BELBYTE") | ||
| 3104 | [ "$BELLN" -eq 1 ] || { | ||
| 3105 | echo "e2e FAIL: $BELLN bells on the host tty for one burst of five, want exactly 1" | ||
| 3106 | cat -v "$OUT.bell"; exit 1; } | ||
| 3107 | "$MUXD" stop --sock "$SOCK24" > /dev/null 2>&1 || true | ||
| 3108 | wait_pid_gone "$D21PID" "bell leg: stop reported stopped" | ||
| 3109 | D21PID="" | ||
| 3110 | rm_swept "$OUT.bell" "$OUT.bell.err" "$OUT.bell.log" "$OUT.bell.sh" "$OUT.d21.d" | ||
| 3111 | ok "a burst of bells in one chunk reaches the host as exactly one" | ||
| 3112 | |||
| 3010 | # --- M14: the ssh→QUIC handoff ----------------------------------------- | 3113 | # --- M14: the ssh→QUIC handoff ----------------------------------------- |
| 3011 | # | 3114 | # |
| 3012 | # `mux HOST` fetches QUIC coordinates over ssh once, caches them, and | 3115 | # `mux HOST` fetches QUIC coordinates over ssh once, caches them, and |
| @@ -4012,7 +4115,7 @@ DPID="" | |||
| 4012 | 4115 | ||
| 4013 | # The pins. Literals, not variables set from counting something else — | 4116 | # The pins. Literals, not variables set from counting something else — |
| 4014 | # "assert the literal, never the constant the code under test reads" | 4117 | # "assert the literal, never the constant the code under test reads" |
| 4015 | # (decisions.md, M10). 30 scenario checkpoints; 35 convergence points. | 4118 | # (decisions.md, M10). 31 scenario checkpoints; 35 convergence points. |
| 4016 | # Anyone adding a scenario updates these by hand, on purpose. | 4119 | # Anyone adding a scenario updates these by hand, on purpose. |
| 4017 | # | 4120 | # |
| 4018 | # M18 added three checkpoints and no convergence points: its wall block | 4121 | # M18 added three checkpoints and no convergence points: its wall block |
| @@ -4026,9 +4129,12 @@ DPID="" | |||
| 4026 | # added the 29th, and no convergence point for the side-channel reason | 4129 | # added the 29th, and no convergence point for the side-channel reason |
| 4027 | # again: it asserts on a capture, for bytes no grid carries. The 30th is | 4130 | # again: it asserts on a capture, for bytes no grid carries. The 30th is |
| 4028 | # that leg's negative twin — the same bytes, asserted ABSENT from a client | 4131 | # that leg's negative twin — the same bytes, asserted ABSENT from a client |
| 4029 | # that took no terminal over — and carries no convergence point either. | 4132 | # that took no terminal over — and carries no convergence point either. The |
| 4030 | [ "$OK_COUNT" = "30" ] || { | 4133 | # 31st is the bell burst, and no convergence point for the side-channel |
| 4031 | echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 30 —" | 4134 | # reason a third time: what it asserts on is a COUNT of bytes in a capture, |
| 4135 | # and a grid carries neither the bytes nor the number of them. | ||
| 4136 | [ "$OK_COUNT" = "31" ] || { | ||
| 4137 | echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 31 —" | ||
| 4032 | echo " a scenario was added (update the pin) or silently lost" | 4138 | echo " a scenario was added (update the pin) or silently lost" |
| 4033 | exit 1 | 4139 | exit 1 |
| 4034 | } | 4140 | } |
| @@ -4036,4 +4142,4 @@ DPID="" | |||
| 4036 | echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 35" | 4142 | echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 35" |
| 4037 | exit 1 | 4143 | exit 1 |
| 4038 | } | 4144 | } |
| 4039 | echo "e2e OK (30 scenarios, 35 convergence points)" | 4145 | echo "e2e OK (31 scenarios, 35 convergence points)" |