7e9a491b
test: server_test_modes rides the harness awaits
a73x 2026-08-31 21:58
Commit message
src/server/server_test_modes.zig
| Old | New | ||
|---|---|---|---|
| @@ -18,7 +18,8 @@ fn pumpAndCollectModes( | |||
| 18 | } | 18 | } |
| 19 | 19 | ||
| 20 | /// No pump: for paths that answer synchronously, where pumping would blur what | 20 | /// No pump: for paths that answer synchronously, where pumping would blur what |
| 21 | /// caused the frame. | 21 | /// caused the frame. Its own poll, and not a `Link.awaitFrame`, because it |
| 22 | /// does not WAIT at all — it takes what is already readable and returns. | ||
| 22 | fn drainModes( | 23 | fn drainModes( |
| 23 | alloc: std.mem.Allocator, | 24 | alloc: std.mem.Allocator, |
| 24 | fd: std.posix.fd_t, | 25 | fd: std.posix.fd_t, |
| @@ -354,6 +355,9 @@ test "Server: a session enabling bracketed paste tells its clients, and not agai | |||
| 354 | // vacuously on a session that simply never spoke again — and a second | 355 | // vacuously on a session that simply never spoke again — and a second |
| 355 | // term_modes would prove the send is per-pump rather than on change. | 356 | // term_modes would prove the send is per-pump rather than on change. |
| 356 | try proto.writeFrame(c.handle, .input, "go\n"); | 357 | try proto.writeFrame(c.handle, .input, "go\n"); |
| 358 | // A round-counting loop on purpose: the wait ends a fixed number of | ||
| 359 | // pumps AFTER the content frame, not when some frame arrives, and the | ||
| 360 | // verdict is what did NOT come in that window. | ||
| 357 | var saw_content = false; | 361 | var saw_content = false; |
| 358 | var resent: usize = 0; | 362 | var resent: usize = 0; |
| 359 | var after: usize = 0; | 363 | var after: usize = 0; |
| @@ -474,6 +478,9 @@ test "Server: a joiner that resizes the grid is still told the session's modes" | |||
| 474 | // resyncSnapshot broadcasts, and the other two arms send to the joiner | 478 | // resyncSnapshot broadcasts, and the other two arms send to the joiner |
| 475 | // alone. Without it the test could pass down the snapshot branch and | 479 | // alone. Without it the test could pass down the snapshot branch and |
| 476 | // pin nothing the resync-branch test does not already cover. | 480 | // pin nothing the resync-branch test does not already cover. |
| 481 | // Two connections at once, which is why this keeps its own poll: | ||
| 482 | // `Link.awaitFrame` waits on ONE link, and the verdict here is a fact | ||
| 483 | // about A and a fact about B arriving in the same window. | ||
| 477 | var a_resnapshotted = false; | 484 | var a_resnapshotted = false; |
| 478 | var b_modes: ?bool = null; | 485 | var b_modes: ?bool = null; |
| 479 | var i: usize = 0; | 486 | var i: usize = 0; |
| @@ -545,6 +552,9 @@ test "Server: a window title reaches clients on change, and only on change" { | |||
| 545 | // digested — without it the "no second frame" half would pass vacuously | 552 | // digested — without it the "no second frame" half would pass vacuously |
| 546 | // on a session that simply never spoke again. | 553 | // on a session that simply never spoke again. |
| 547 | try proto.writeFrame(c.handle, .input, "go\n"); | 554 | try proto.writeFrame(c.handle, .input, "go\n"); |
| 555 | // A round-counting loop on purpose: the wait ends a fixed number of | ||
| 556 | // pumps AFTER the content frame, not when some frame arrives, and the | ||
| 557 | // verdict is what did NOT come in that window. | ||
| 548 | var saw_content = false; | 558 | var saw_content = false; |
| 549 | var resent: usize = 0; | 559 | var resent: usize = 0; |
| 550 | var after: usize = 0; | 560 | var after: usize = 0; |
| @@ -609,6 +619,8 @@ test "Server: a session that never set a title has none sent for it" { | |||
| 609 | var joined = false; | 619 | var joined = false; |
| 610 | var b: ?std.net.Stream = null; | 620 | var b: ?std.net.Stream = null; |
| 611 | defer if (b) |s| s.close(); | 621 | defer if (b) |s| s.close(); |
| 622 | // Both reasons at once: two connections, and a round-counting settle | ||
| 623 | // whose verdict is `titles == 0`. | ||
| 612 | var i: usize = 0; | 624 | var i: usize = 0; |
| 613 | while (i < 500 and after < 80) : (i += 1) { | 625 | while (i < 500 and after < 80) : (i += 1) { |
| 614 | if (saw_content) { | 626 | if (saw_content) { |
| @@ -688,6 +700,8 @@ test "Server: a joiner that resizes the grid is still told the session's title" | |||
| 688 | // A's re-snapshot at B's size is the witness that this arm ran at all: | 700 | // A's re-snapshot at B's size is the witness that this arm ran at all: |
| 689 | // resyncSnapshot broadcasts, and the other two arms send to the joiner | 701 | // resyncSnapshot broadcasts, and the other two arms send to the joiner |
| 690 | // alone. | 702 | // alone. |
| 703 | // Two connections again; see the bracketed-paste test above for why the | ||
| 704 | // poll stays. | ||
| 691 | var a_resnapshotted = false; | 705 | var a_resnapshotted = false; |
| 692 | var b_title: ?[]const u8 = null; | 706 | var b_title: ?[]const u8 = null; |
| 693 | defer if (b_title) |t| alloc.free(t); | 707 | defer if (b_title) |t| alloc.free(t); |
| @@ -716,30 +730,32 @@ test "Server: a joiner that resizes the grid is still told the session's title" | |||
| 716 | try std.testing.expectEqualStrings("vim", b_title orelse return error.NoTitleOnResizingJoin); | 730 | try std.testing.expectEqualStrings("vim", b_title orelse return error.NoTitleOnResizingJoin); |
| 717 | } | 731 | } |
| 718 | 732 | ||
| 719 | /// awaitFrame drops every frame but the one asked for, including the branch's. | 733 | /// Which resync branch answered the join, and what it said about the modes. |
| 734 | /// A sink rather than a bare `awaitFrame`, because the branch is named by | ||
| 735 | /// the frame BEFORE the answer and awaitFrame drops those. | ||
| 720 | fn modesWithResync( | 736 | fn modesWithResync( |
| 721 | alloc: std.mem.Allocator, | 737 | alloc: std.mem.Allocator, |
| 722 | srv: *Server, | 738 | srv: *Server, |
| 723 | fd: std.posix.fd_t, | 739 | fd: std.posix.fd_t, |
| 724 | ) !struct { content: ?proto.MsgType, modes: ?bool } { | 740 | ) !struct { content: ?proto.MsgType, modes: ?bool } { |
| 725 | var content: ?proto.MsgType = null; | 741 | const Branch = struct { |
| 726 | var modes: ?bool = null; | 742 | content: ?proto.MsgType = null, |
| 727 | var i: usize = 0; | 743 | fn on(ctx: ?*anyopaque, frame: proto.Frame) anyerror!void { |
| 728 | while (i < 400 and modes == null) : (i += 1) { | 744 | if (frame.type != .snapshot and frame.type != .delta) return; |
| 729 | try srv.pumpOnce(5); | 745 | const self: *@This() = @ptrCast(@alignCast(ctx.?)); |
| 730 | var pfd = [_]std.posix.pollfd{ | 746 | self.content = frame.type; |
| 731 | .{ .fd = fd, .events = std.posix.POLL.IN, .revents = 0 }, | ||
| 732 | }; | ||
| 733 | if ((std.posix.poll(&pfd, 1) catch 0) == 0) continue; | ||
| 734 | const f = (try proto.readFrame(alloc, fd)) orelse break; | ||
| 735 | defer f.deinit(alloc); | ||
| 736 | switch (f.type) { | ||
| 737 | .snapshot, .delta => content = f.type, | ||
| 738 | .term_modes => modes = (try proto.decodeTermModes(f.payload)).bracketed_paste, | ||
| 739 | else => {}, | ||
| 740 | } | 747 | } |
| 748 | }; | ||
| 749 | var branch: Branch = .{}; | ||
| 750 | var modes: ?bool = null; | ||
| 751 | if (try h.awaitFrameSink(alloc, srv, fd, .term_modes, 400, .{ | ||
| 752 | .ctx = &branch, | ||
| 753 | .on = Branch.on, | ||
| 754 | })) |frame| { | ||
| 755 | defer frame.deinit(alloc); | ||
| 756 | modes = (try proto.decodeTermModes(frame.payload)).bracketed_paste; | ||
| 741 | } | 757 | } |
| 742 | return .{ .content = content, .modes = modes }; | 758 | return .{ .content = branch.content, .modes = modes }; |
| 743 | } | 759 | } |
| 744 | 760 | ||
| 745 | test "Server: a resize reaches the pty as an in-band size report when the app asked for one" { | 761 | test "Server: a resize reaches the pty as an in-band size report when the app asked for one" { |
| @@ -761,6 +777,8 @@ test "Server: a resize reaches the pty as an in-band size report when the app as | |||
| 761 | // The report is INPUT to the child, so the only place to see it from | 777 | // The report is INPUT to the child, so the only place to see it from |
| 762 | // here is the tty's own echo of it back on the master. Canonical mode | 778 | // here is the tty's own echo of it back on the master. Canonical mode |
| 763 | // may spell the ESC as ^[, so the match starts after it. | 779 | // may spell the ESC as ^[, so the match starts after it. |
| 780 | // Raw bytes off the pty master, not frames on a connection, so there is | ||
| 781 | // no Link here to await on. | ||
| 764 | var got: std.ArrayList(u8) = .empty; | 782 | var got: std.ArrayList(u8) = .empty; |
| 765 | defer got.deinit(alloc); | 783 | defer got.deinit(alloc); |
| 766 | var tries: usize = 0; | 784 | var tries: usize = 0; |