762d350f
test: the stubborn-shell wait names the session the test ends
a73x 2026-09-04 10:16
Commit message
src/server/server_test_attach.zig
| Old | New | ||
|---|---|---|---|
| @@ -1500,8 +1500,9 @@ test "Server: the two bounded deadlines read a monotonic clock, not the calendar | |||
| 1500 | try td.start(.{ .shell = script }); | 1500 | try td.start(.{ .shell = script }); |
| 1501 | // The shell has to be STUBBORN before anything ends it: until its `trap` | 1501 | // The shell has to be STUBBORN before anything ends it: until its `trap` |
| 1502 | // line runs, the SIGTERM an accepted end sends is fatal, and a session | 1502 | // line runs, the SIGTERM an accepted end sends is fatal, and a session |
| 1503 | // that died there is a session no assertion below is about. | 1503 | // that died there is a session no assertion below is about. Named for the |
| 1504 | try h.awaitStubbornArmed(&td.srv, &td.tmp, 0, 5000); | 1504 | // session this test ENDS, which is the shell whose traps matter. |
| 1505 | try h.awaitStubbornArmed(&td.srv, &td.tmp, "", 5000); | ||
| 1505 | 1506 | ||
| 1506 | // CLOCK_MONOTONIC counts from boot and CLOCK_REALTIME from 1970, so any | 1507 | // CLOCK_MONOTONIC counts from boot and CLOCK_REALTIME from 1970, so any |
| 1507 | // machine that has been up less than a decade separates them by a | 1508 | // machine that has been up less than a decade separates them by a |
src/server/server_test_harness.zig
| Old | New | ||
|---|---|---|---|
| @@ -615,8 +615,18 @@ pub fn writeStubbornShell(alloc: std.mem.Allocator, tmp: *TmpDir) ![:0]u8 { | |||
| 615 | return std.fmt.allocPrintSentinel(alloc, "{s}/stubborn.sh", .{tmp.path()}, 0); | 615 | return std.fmt.allocPrintSentinel(alloc, "{s}/stubborn.sh", .{tmp.path()}, 0); |
| 616 | } | 616 | } |
| 617 | 617 | ||
| 618 | /// Wait until the stubborn shell behind session `si` has installed the traps | 618 | /// Wait until the stubborn shell behind the session `wire_name` spells has |
| 619 | /// that make it stubborn, or fail saying it never did. | 619 | /// installed the traps that make it stubborn, or fail saying it never did. |
| 620 | /// | ||
| 621 | /// By NAME, and the name must be the one the caller is about to END. Three of | ||
| 622 | /// these tests end a session their own `dialAttachNamed` created, which is a | ||
| 623 | /// different shell with a different pid from the default session `Server.init` | ||
| 624 | /// spawned; a wait that named the default proved nothing about the shell being | ||
| 625 | /// ended and left the race exactly where it was. Worse in the repeated-end | ||
| 626 | /// test, where a "nag" that died to the first pre-trap SIGTERM passes | ||
| 627 | /// `expect(!alive(pid))` without the SIGKILL deadline it is named for ever | ||
| 628 | /// being reached. `""` resolves to the default session, so one spelling | ||
| 629 | /// covers every site. | ||
| 620 | /// | 630 | /// |
| 621 | /// Every caller of `writeStubbornShell` needs this before it asks for an end, | 631 | /// Every caller of `writeStubbornShell` needs this before it asks for an end, |
| 622 | /// and none of them had it. `Pty.requestExit` closes the master (SIGHUP) and | 632 | /// and none of them had it. `Pty.requestExit` closes the master (SIGHUP) and |
| @@ -636,7 +646,8 @@ pub fn writeStubbornShell(alloc: std.mem.Allocator, tmp: *TmpDir) ![:0]u8 { | |||
| 636 | /// | 646 | /// |
| 637 | /// It polls a file rather than the pump, because the shell writes that file | 647 | /// It polls a file rather than the pump, because the shell writes that file |
| 638 | /// on its own and a daemon on a thread would make pumping here a race. | 648 | /// on its own and a daemon on a thread would make pumping here a race. |
| 639 | pub fn awaitStubbornArmed(srv: *Server, tmp: *TmpDir, si: usize, budget_ms: i64) !void { | 649 | pub fn awaitStubbornArmed(srv: *Server, tmp: *TmpDir, wire_name: []const u8, budget_ms: i64) !void { |
| 650 | const si = srv.sessions.find(wire_name) orelse return error.NoSuchSessionToArm; | ||
| 640 | const s = srv.sessions.table[si] orelse return error.NoSuchSessionToArm; | 651 | const s = srv.sessions.table[si] orelse return error.NoSuchSessionToArm; |
| 641 | var name_buf: [64]u8 = undefined; | 652 | var name_buf: [64]u8 = undefined; |
| 642 | const name = try std.fmt.bufPrint(&name_buf, "stubborn-{d}.armed", .{s.pty.child}); | 653 | const name = try std.fmt.bufPrint(&name_buf, "stubborn-{d}.armed", .{s.pty.child}); |
src/server/server_test_session.zig
| Old | New | ||
|---|---|---|---|
| @@ -1634,14 +1634,15 @@ test "Server: a keystroke into an ending session does not cost that client its e | |||
| 1634 | defer alloc.free(script); | 1634 | defer alloc.free(script); |
| 1635 | 1635 | ||
| 1636 | try td.start(.{ .shell = script }); | 1636 | try td.start(.{ .shell = script }); |
| 1637 | // The shell has to be STUBBORN before anything ends it: until its `trap` | ||
| 1638 | // line runs, the SIGTERM an accepted end sends is fatal, and a session | ||
| 1639 | // that died there is a session no assertion below is about. | ||
| 1640 | try h.awaitStubbornArmed(&td.srv, &td.tmp, 0, 5000); | ||
| 1641 | 1637 | ||
| 1642 | const c = try dial.dialAttachNamed(td.sock_path, 80, 24, "typing"); | 1638 | const c = try dial.dialAttachNamed(td.sock_path, 80, 24, "typing"); |
| 1643 | defer c.close(); | 1639 | defer c.close(); |
| 1644 | (try awaitFrame(alloc, &td.srv, c.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc); | 1640 | (try awaitFrame(alloc, &td.srv, c.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc); |
| 1641 | // The shell has to be STUBBORN before anything ends it: until its `trap` | ||
| 1642 | // line runs, the SIGTERM an accepted end sends is fatal, and a session | ||
| 1643 | // that died there is a session no assertion below is about. Named for the | ||
| 1644 | // session this test ENDS, which is the shell whose traps matter. | ||
| 1645 | try h.awaitStubbornArmed(&td.srv, &td.tmp, "typing", 5000); | ||
| 1645 | 1646 | ||
| 1646 | var rq: [proto.end_req_max_len]u8 = undefined; | 1647 | var rq: [proto.end_req_max_len]u8 = undefined; |
| 1647 | try proto.writeFrame(c.handle, .end_req, proto.encodeEndReq(&rq, false, "typing")); | 1648 | try proto.writeFrame(c.handle, .end_req, proto.encodeEndReq(&rq, false, "typing")); |
| @@ -1666,14 +1667,15 @@ test "Server: a repeated end_req does not push the SIGKILL deadline out" { | |||
| 1666 | defer alloc.free(script); | 1667 | defer alloc.free(script); |
| 1667 | 1668 | ||
| 1668 | try td.start(.{ .shell = script }); | 1669 | try td.start(.{ .shell = script }); |
| 1669 | // The shell has to be STUBBORN before anything ends it: until its `trap` | ||
| 1670 | // line runs, the SIGTERM an accepted end sends is fatal, and a session | ||
| 1671 | // that died there is a session no assertion below is about. | ||
| 1672 | try h.awaitStubbornArmed(&td.srv, &td.tmp, 0, 5000); | ||
| 1673 | 1670 | ||
| 1674 | const c = try dial.dialAttachNamed(td.sock_path, 80, 24, "nag"); | 1671 | const c = try dial.dialAttachNamed(td.sock_path, 80, 24, "nag"); |
| 1675 | defer c.close(); | 1672 | defer c.close(); |
| 1676 | (try awaitFrame(alloc, &td.srv, c.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc); | 1673 | (try awaitFrame(alloc, &td.srv, c.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc); |
| 1674 | // The shell has to be STUBBORN before anything ends it: until its `trap` | ||
| 1675 | // line runs, the SIGTERM an accepted end sends is fatal, and a session | ||
| 1676 | // that died there is a session no assertion below is about. Named for the | ||
| 1677 | // session this test ENDS, which is the shell whose traps matter. | ||
| 1678 | try h.awaitStubbornArmed(&td.srv, &td.tmp, "nag", 5000); | ||
| 1677 | 1679 | ||
| 1678 | const pid = shellPidOf(&td.srv, "nag"); | 1680 | const pid = shellPidOf(&td.srv, "nag"); |
| 1679 | var rq: [proto.end_req_max_len]u8 = undefined; | 1681 | var rq: [proto.end_req_max_len]u8 = undefined; |
src/server/server_test_upgrade.zig
| Old | New | ||
|---|---|---|---|
| @@ -452,8 +452,9 @@ test "Server: an upgrade asked for during a session's hangup is refused, not att | |||
| 452 | try td.start(.{ .shell = script, .version = "0.0.1-1" }); | 452 | try td.start(.{ .shell = script, .version = "0.0.1-1" }); |
| 453 | // The shell has to be STUBBORN before anything ends it: until its `trap` | 453 | // The shell has to be STUBBORN before anything ends it: until its `trap` |
| 454 | // line runs, the SIGTERM an accepted end sends is fatal, and a session | 454 | // line runs, the SIGTERM an accepted end sends is fatal, and a session |
| 455 | // that died there is a session no assertion below is about. | 455 | // that died there is a session no assertion below is about. Named for the |
| 456 | try h.awaitStubbornArmed(&td.srv, &td.tmp, 0, 5000); | 456 | // session this test ENDS, which is the shell whose traps matter. |
| 457 | try h.awaitStubbornArmed(&td.srv, &td.tmp, "", 5000); | ||
| 457 | 458 | ||
| 458 | // The shell outlives the hangup by the whole grace, so the exec's | 459 | // The shell outlives the hangup by the whole grace, so the exec's |
| 459 | // clearCloexec and the manifest's pty_fd would both see -1. | 460 | // clearCloexec and the manifest's pty_fd would both see -1. |
| @@ -606,10 +607,6 @@ test "Server: an accepted end cancels a pending upgrade — the exec never sees | |||
| 606 | defer alloc.free(script); | 607 | defer alloc.free(script); |
| 607 | 608 | ||
| 608 | try td.start(.{ .shell = script, .version = "0.0.1-1" }); | 609 | try td.start(.{ .shell = script, .version = "0.0.1-1" }); |
| 609 | // The shell has to be STUBBORN before anything ends it: until its `trap` | ||
| 610 | // line runs, the SIGTERM an accepted end sends is fatal, and a session | ||
| 611 | // that died there is a session no assertion below is about. | ||
| 612 | try h.awaitStubbornArmed(&td.srv, &td.tmp, 0, 5000); | ||
| 613 | 610 | ||
| 614 | // Plural: the end takes ONE session and the upgrade would have carried | 611 | // Plural: the end takes ONE session and the upgrade would have carried |
| 615 | // the others, so a guard that only looked at the ending slot's own | 612 | // the others, so a guard that only looked at the ending slot's own |
| @@ -617,6 +614,11 @@ test "Server: an accepted end cancels a pending upgrade — the exec never sees | |||
| 617 | const ca = try dial.dialAttachNamed(td.sock_path, 80, 24, "a"); | 614 | const ca = try dial.dialAttachNamed(td.sock_path, 80, 24, "a"); |
| 618 | defer ca.close(); | 615 | defer ca.close(); |
| 619 | (try awaitFrame(alloc, &td.srv, ca.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc); | 616 | (try awaitFrame(alloc, &td.srv, ca.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc); |
| 617 | // The shell has to be STUBBORN before anything ends it: until its `trap` | ||
| 618 | // line runs, the SIGTERM an accepted end sends is fatal, and a session | ||
| 619 | // that died there is a session no assertion below is about. Named for the | ||
| 620 | // session this test ENDS, which is the shell whose traps matter. | ||
| 621 | try h.awaitStubbornArmed(&td.srv, &td.tmp, "a", 5000); | ||
| 620 | 622 | ||
| 621 | try td.tmp.dir.writeFile(.{ | 623 | try td.tmp.dir.writeFile(.{ |
| 622 | .sub_path = "fakemux.sh", | 624 | .sub_path = "fakemux.sh", |