1fc81c25
feat: the poll grades panes and adds none; the layout is the only source of tiles
a73x 2026-09-03 05:20
Commit message
src/tui/wall_host.zig
| Old | New | ||
|---|---|---|---|
| @@ -1,7 +1,9 @@ | |||
| 1 | //! The wall's daemons: the hosts file's lines resolved to targets, the table | 1 | //! The wall's daemons: the hosts file's lines resolved to targets, the table |
| 2 | //! that holds them, and the poller that asks each for its live sessions once a | 2 | //! that holds them, and the poller that asks each for its live sessions once a |
| 3 | //! second. A host contributes TILES, never a tile of its own — `applyHostList` | 3 | //! second. A host contributes nothing but the GRADE of the panes the layout |
| 4 | //! is where a poll's answer becomes births and vanishings. | 4 | //! already gave it — `applyHostList` binds a pending pane whose session the |
| 5 | //! list names, marks `gone` one it does not, and vanishes a live pane whose | ||
| 6 | //! shell has ended. The layout is the only source of tiles. | ||
| 5 | const std = @import("std"); | 7 | const std = @import("std"); |
| 6 | const proto = @import("term").protocol; | 8 | const proto = @import("term").protocol; |
| 7 | const client = @import("client"); | 9 | const client = @import("client"); |
| @@ -20,9 +22,9 @@ pub const Resolved = struct { | |||
| 20 | label: []const u8, | 22 | label: []const u8, |
| 21 | session: []const u8, | 23 | session: []const u8, |
| 22 | /// Whether this tile's attaches offer this client's ssh-agent (`mux | 24 | /// Whether this tile's attaches offer this client's ssh-agent (`mux |
| 23 | /// -A`). Per tile rather than per wall: a tile the user never named — | 25 | /// -A`). Per tile rather than per wall: a tile whose host the user did |
| 24 | /// one a host's poll turned up — must not hand a stranger's host the | 26 | /// not offer the agent to must not hand that stranger the keys, even |
| 25 | /// keys, even while an `-A` tile is on the same wall. | 27 | /// while an `-A` tile is on the same wall. |
| 26 | agent: bool = false, | 28 | agent: bool = false, |
| 27 | }; | 29 | }; |
| 28 | 30 | ||
| @@ -75,10 +77,10 @@ pub fn addHost( | |||
| 75 | idle_ms: u32, | 77 | idle_ms: u32, |
| 76 | path: ?[]const u8, | 78 | path: ?[]const u8, |
| 77 | ) AddHost { | 79 | ) AddHost { |
| 78 | // No tile is born here: the host's first poll is what turns its | 80 | // No tile is born here, and none comes later either: a host is a row, |
| 79 | // sessions into tiles, so the prompt cannot conjure a session that is | 81 | // and Enter on that row is what puts a pane on the wall. Twice is once |
| 80 | // not there. Twice is once — a second poller on the same daemon would | 82 | // — a second row for one daemon is a second poller grading the same |
| 81 | // tile every one of its sessions again. | 83 | // panes, and a picker naming one machine twice. |
| 82 | for (host_table[0..hosts_live.*], 0..) |*h, hi| { | 84 | for (host_table[0..hosts_live.*], 0..) |*h, hi| { |
| 83 | // A forgotten host is not on the wall, so its slot must not refuse | 85 | // A forgotten host is not on the wall, so its slot must not refuse |
| 84 | // the spelling back: `x` then `a` on the same host is one of the | 86 | // the spelling back: `x` then `a` on the same host is one of the |
| @@ -138,19 +140,17 @@ pub fn addHost( | |||
| 138 | return .{ .added = at }; | 140 | return .{ .added = at }; |
| 139 | } | 141 | } |
| 140 | 142 | ||
| 141 | /// A diff's plan, bounded by the wall's own capacity so a daemon with more | 143 | /// A diff's plan, bounded by the wall's own capacity so an append can never |
| 142 | /// sessions than the wall can hold is COUNTED rather than overrunning. | 144 | /// overrun. Every plan here is a list of TILE indices, so the bound cannot |
| 145 | /// bite — a wall holds at most `wv.max_tiles` of them — and the guard is | ||
| 146 | /// what keeps that true if a caller ever hands it something else. | ||
| 143 | fn Fixed(comptime T: type) type { | 147 | fn Fixed(comptime T: type) type { |
| 144 | return struct { | 148 | return struct { |
| 145 | items: [wv.max_tiles]T = undefined, | 149 | items: [wv.max_tiles]T = undefined, |
| 146 | len: usize = 0, | 150 | len: usize = 0, |
| 147 | dropped: usize = 0, | ||
| 148 | 151 | ||
| 149 | fn append(self: *@This(), v: T) void { | 152 | fn append(self: *@This(), v: T) void { |
| 150 | if (self.len == self.items.len) { | 153 | if (self.len == self.items.len) return; |
| 151 | self.dropped += 1; | ||
| 152 | return; | ||
| 153 | } | ||
| 154 | self.items[self.len] = v; | 154 | self.items[self.len] = v; |
| 155 | self.len += 1; | 155 | self.len += 1; |
| 156 | } | 156 | } |
| @@ -161,7 +161,6 @@ fn Fixed(comptime T: type) type { | |||
| 161 | }; | 161 | }; |
| 162 | } | 162 | } |
| 163 | 163 | ||
| 164 | pub const BirthNames = Fixed([]const u8); | ||
| 165 | pub const TileIdxs = Fixed(usize); | 164 | pub const TileIdxs = Fixed(usize); |
| 166 | 165 | ||
| 167 | /// Only a host's own sessions are its list's to keep or to drop. | 166 | /// Only a host's own sessions are its list's to keep or to drop. |
| @@ -170,9 +169,10 @@ pub fn ownedBy(t: *const Tile, host: usize) bool { | |||
| 170 | return h == host; | 169 | return h == host; |
| 171 | } | 170 | } |
| 172 | 171 | ||
| 173 | /// The whole of "a host's live sessions are its tiles", per HOST so that | 172 | /// The whole of "a host GRADES the panes the layout gave it", per HOST so |
| 174 | /// two daemons may share a session name. Pure: the rule is testable | 173 | /// that two daemons may share a session name. A name in the list that no |
| 175 | /// without a daemon. | 174 | /// pane spells is nobody's business: the layout is the only source of tiles. |
| 175 | /// Pure: the rule is testable without a daemon. | ||
| 176 | pub fn planHostDiff( | 176 | pub fn planHostDiff( |
| 177 | tiles: []Tile, | 177 | tiles: []Tile, |
| 178 | present: []const bool, | 178 | present: []const bool, |
| @@ -180,28 +180,27 @@ pub fn planHostDiff( | |||
| 180 | host: usize, | 180 | host: usize, |
| 181 | list: []const u8, | 181 | list: []const u8, |
| 182 | self_name: ?[]const u8, | 182 | self_name: ?[]const u8, |
| 183 | births: *BirthNames, | ||
| 184 | binds: *TileIdxs, | 183 | binds: *TileIdxs, |
| 185 | vanish: *TileIdxs, | 184 | vanish: *TileIdxs, |
| 186 | gones: *TileIdxs, | 185 | gones: *TileIdxs, |
| 187 | ) void { | 186 | ) void { |
| 188 | // `proto.sessionsIter` carries the trust policy: a peer's reply is bounded | 187 | // `proto.sessionsIter` carries the trust policy: a peer's reply is bounded |
| 189 | // only in total, so it yields only lines that are names — which is what | 188 | // only in total, so it yields only lines that are names — which is what |
| 190 | // makes a birth's name safe to hand to `encodeAttachNamed`. | 189 | // keeps a hostile line from matching a pane by accident. |
| 191 | var it = proto.sessionsIter(list); | 190 | var it = proto.sessionsIter(list); |
| 192 | while (it.next()) |name| { | 191 | while (it.next()) |name| { |
| 192 | // The shell mux is running inside cannot be a pane of the wall it is | ||
| 193 | // painting, so its name never wakes one — `seedLayout` refuses such | ||
| 194 | // a leaf, and this is the poll's own half of that refusal. | ||
| 193 | if (self_name) |self| if (std.mem.eql(u8, self, name)) continue; | 195 | if (self_name) |self| if (std.mem.eql(u8, self, name)) continue; |
| 194 | var found = false; | ||
| 195 | for (tiles[0..live], present[0..live], 0..) |*t, p, i| { | 196 | for (tiles[0..live], present[0..live], 0..) |*t, p, i| { |
| 196 | if (p and ownedBy(t, host) and std.mem.eql(u8, proto.resolveName(t.r.session), name)) { | 197 | if (p and ownedBy(t, host) and std.mem.eql(u8, proto.resolveName(t.r.session), name)) { |
| 197 | // The sidecar's guess, confirmed by the host's own list: | 198 | // The layout's guess, confirmed by the host's own list: |
| 198 | // wake the pending pane rather than birthing a twin. | 199 | // wake the pending pane where it already stands. |
| 199 | if (t.pending) binds.append(i); | 200 | if (t.pending) binds.append(i); |
| 200 | found = true; | ||
| 201 | break; | 201 | break; |
| 202 | } | 202 | } |
| 203 | } | 203 | } |
| 204 | if (!found) births.append(name); | ||
| 205 | } | 204 | } |
| 206 | for (tiles[0..live], present[0..live], 0..) |*t, p, i| { | 205 | for (tiles[0..live], present[0..live], 0..) |*t, p, i| { |
| 207 | if (!p or !ownedBy(t, host)) continue; | 206 | if (!p or !ownedBy(t, host)) continue; |
| @@ -286,8 +285,9 @@ pub fn pollHost(h: *Host) void { | |||
| 286 | h.poller_done.store(true, .release); | 285 | h.poller_done.store(true, .release); |
| 287 | } | 286 | } |
| 288 | 287 | ||
| 289 | /// A chord that births asks its host for a list NOW: a session made by | 288 | /// A chord that births asks its host for a list NOW: the tile is already on |
| 290 | /// `c` or `:` must not wait out the poll interval to become a tile. | 289 | /// the wall, and it is graded against a list that must name the session the |
| 290 | /// chord just made rather than the one from a second ago. | ||
| 291 | pub fn pokeHost(host_table: []Host, t: *const Tile) void { | 291 | pub fn pokeHost(host_table: []Host, t: *const Tile) void { |
| 292 | const hi = t.host orelse return; | 292 | const hi = t.host orelse return; |
| 293 | if (hi < host_table.len) host_table[hi].poll.poke.store(true, .release); | 293 | if (hi < host_table.len) host_table[hi].poll.poke.store(true, .release); |
| @@ -372,12 +372,11 @@ pub fn applyHostList(w: Wall, hi: usize) void { | |||
| 372 | // tiles, which reconnect on their own; vanishing them on a failed poll | 372 | // tiles, which reconnect on their own; vanishing them on a failed poll |
| 373 | // would tear a wall down over one dropped packet. | 373 | // would tear a wall down over one dropped packet. |
| 374 | if (reachable) { | 374 | if (reachable) { |
| 375 | var births = BirthNames{}; | ||
| 376 | var binds = TileIdxs{}; | 375 | var binds = TileIdxs{}; |
| 377 | var vanish = TileIdxs{}; | 376 | var vanish = TileIdxs{}; |
| 378 | // Where the seeded panes this list disowns land. | 377 | // Where the seeded panes this list disowns land. |
| 379 | var gones = TileIdxs{}; | 378 | var gones = TileIdxs{}; |
| 380 | planHostDiff(w.liveTiles(), w.livePresent(), w.live.*, hi, list, h.self_name, &births, &binds, &vanish, &gones); | 379 | planHostDiff(w.liveTiles(), w.livePresent(), w.live.*, hi, list, h.self_name, &binds, &vanish, &gones); |
| 381 | // Binds first, and they do not count as change: a pane whose rect | 380 | // Binds first, and they do not count as change: a pane whose rect |
| 382 | // was cut at seed time wakes in place, and re-cuts nothing. | 381 | // was cut at seed time wakes in place, and re-cuts nothing. |
| 383 | for (binds.items[0..binds.len]) |bi| wv.bindTile(w, bi); | 382 | for (binds.items[0..binds.len]) |bi| wv.bindTile(w, bi); |
| @@ -396,39 +395,10 @@ pub fn applyHostList(w: Wall, hi: usize) void { | |||
| 396 | wv.vanishTile(w.liveTiles(), w.livePresent(), w.shared, v, null); | 395 | wv.vanishTile(w.liveTiles(), w.livePresent(), w.shared, v, null); |
| 397 | changed = true; | 396 | changed = true; |
| 398 | } | 397 | } |
| 399 | var placed: usize = 0; | 398 | // A name the list carries that no pane spells ends here, unsaid: the |
| 400 | // The tile the NEXT birth sits beside: the focus first, then the one just | 399 | // user's layout says which sessions this wall shows, and a session |
| 401 | // born. `insert` puts a leaf immediately after its anchor, so anchoring | 400 | // somebody else started on the same daemon is not this wall's to add |
| 402 | // every birth at the focus lays {b, c} out as c, b. | 401 | // — nor is it something to apologise for in a notice. |
| 403 | var anchor = wall_layout.anchorTile(w.livePresent(), w.shared.sel); | ||
| 404 | // Stops at the FIRST refusal rather than retrying each name: the wall | ||
| 405 | // refuses for a reason that holds for the whole list, and this list comes | ||
| 406 | // back every second — a per-name retry is a flatten and an undo forever. | ||
| 407 | while (placed < births.len) : (placed += 1) { | ||
| 408 | const at = wv.birthTile(w, .{ | ||
| 409 | // Joins, never creates: the daemon already has this session, | ||
| 410 | // and a sized attach on a live one would resize somebody. | ||
| 411 | // The name is this poll's reply buffer until the wall takes | ||
| 412 | // the tile — see `Birth.borrowed`. | ||
| 413 | .r = .{ .target = h.spec.target, .label = "", .session = births.get(placed) }, | ||
| 414 | .from = anchor, | ||
| 415 | .place = .beside_focus, | ||
| 416 | .creates = false, | ||
| 417 | .born_from = null, | ||
| 418 | .host = hi, | ||
| 419 | .borrowed = true, | ||
| 420 | }) orelse break; | ||
| 421 | anchor = at; | ||
| 422 | wv.spawnPump(&w.tiles[at]); | ||
| 423 | changed = true; | ||
| 424 | } | ||
| 425 | const unplaced = births.dropped + (births.len - placed); | ||
| 426 | // Said out loud rather than dropped: a wall showing a PREFIX of a | ||
| 427 | // daemon's sessions is a wall lying about what it is. | ||
| 428 | if (unplaced > 0) { | ||
| 429 | var buf: [48]u8 = undefined; | ||
| 430 | wv.setNoticeIdle(w.shared, std.fmt.bufPrint(&buf, "[+{d} not shown]", .{unplaced}) catch "[not shown]"); | ||
| 431 | } | ||
| 432 | applyDrift(w, hi, list); | 402 | applyDrift(w, hi, list); |
| 433 | } else dressSilent(w, hi); | 403 | } else dressSilent(w, hi); |
| 434 | if ((!had_focus or w.shared.sel >= w.live.* or !w.present[w.shared.sel]) and | 404 | if ((!had_focus or w.shared.sel >= w.live.* or !w.present[w.shared.sel]) and |
src/tui/wall_test_host.zig
| Old | New | ||
|---|---|---|---|
| @@ -10,45 +10,73 @@ const wall_layout = @import("wall_layout.zig"); | |||
| 10 | const wall_pump = @import("wall_pump.zig"); | 10 | const wall_pump = @import("wall_pump.zig"); |
| 11 | const wv = @import("wallview.zig"); | 11 | const wv = @import("wallview.zig"); |
| 12 | const AddHost = wall_host.AddHost; | 12 | const AddHost = wall_host.AddHost; |
| 13 | const BirthNames = wall_host.BirthNames; | ||
| 14 | const Host = wall_host.Host; | 13 | const Host = wall_host.Host; |
| 15 | const HostSpec = client.HostSpec; | 14 | const HostSpec = client.HostSpec; |
| 16 | const Shared = wv.Shared; | 15 | const Shared = wv.Shared; |
| 17 | const Tile = wv.Tile; | 16 | const Tile = wv.Tile; |
| 18 | const TileIdxs = wall_host.TileIdxs; | 17 | const TileIdxs = wall_host.TileIdxs; |
| 19 | 18 | ||
| 20 | test "planHostDiff: names the daemon has and the wall does not are births, tiles it dropped vanish, and another host's same name is untouched" { | 19 | /// Panes the layout seated and a poll already bound: live tiles, in the |
| 20 | /// tree, whose pump never ran — so `alive` is whatever the test states and | ||
| 21 | /// there is no thread to join. The wall's tiles come from the layout now, | ||
| 22 | /// so a poll test that wants tiles has to seat them itself. | ||
| 23 | fn seatBound( | ||
| 24 | tiles: []Tile, | ||
| 25 | present: []bool, | ||
| 26 | live: *usize, | ||
| 27 | shared: *Shared, | ||
| 28 | rs: []const wall_host.Resolved, | ||
| 29 | hostof: []const usize, | ||
| 30 | ) !void { | ||
| 31 | for (rs, hostof, 0..) |r, h, i| { | ||
| 32 | tiles[i] = .{ | ||
| 33 | .r = r, | ||
| 34 | .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 }, | ||
| 35 | .shared = shared, | ||
| 36 | .idx = i, | ||
| 37 | .wake_r = -1, | ||
| 38 | .wake_w = -1, | ||
| 39 | .host = h, | ||
| 40 | .alive = std.atomic.Value(bool).init(false), | ||
| 41 | }; | ||
| 42 | present[i] = true; | ||
| 43 | if (i == 0) { | ||
| 44 | try shared.tree.addFirst(0); | ||
| 45 | } else { | ||
| 46 | try shared.tree.insert(@intCast(i - 1), @intCast(i)); | ||
| 47 | } | ||
| 48 | } | ||
| 49 | live.* = rs.len; | ||
| 50 | } | ||
| 51 | |||
| 52 | test "planHostDiff: tiles the daemon dropped vanish, and another host's same name is untouched" { | ||
| 21 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false }; | 53 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false }; |
| 22 | var tiles = fixture.diffFixture(&shared); | 54 | var tiles = fixture.diffFixture(&shared); |
| 23 | var present = [_]bool{ true, true, true }; | 55 | var present = [_]bool{ true, true, true }; |
| 24 | 56 | ||
| 25 | var births = BirthNames{}; | ||
| 26 | var binds = TileIdxs{}; | 57 | var binds = TileIdxs{}; |
| 27 | var vanish = TileIdxs{}; | 58 | var vanish = TileIdxs{}; |
| 28 | var gones = TileIdxs{}; | 59 | var gones = TileIdxs{}; |
| 29 | // The birth is immediate; the vanish waits for a second list to agree | 60 | // The vanish waits for a second list to agree (see the grace test |
| 30 | // (see the grace test below), so this list is asked twice. | 61 | // below), so this list is asked twice. "c" is a session of host 0's |
| 31 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nc\n", null, &births, &binds, &vanish, &gones); | 62 | // that no pane spells, and it stays nothing to this wall. |
| 32 | try std.testing.expectEqual(@as(usize, 1), births.len); | 63 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nc\n", null, &binds, &vanish, &gones); |
| 33 | try std.testing.expectEqualStrings("c", births.get(0)); | 64 | try std.testing.expectEqual(@as(usize, 0), vanish.len); |
| 34 | births.len = 0; | 65 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nc\n", null, &binds, &vanish, &gones); |
| 35 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nc\n", null, &births, &binds, &vanish, &gones); | ||
| 36 | try std.testing.expectEqual(@as(usize, 1), vanish.len); | 66 | try std.testing.expectEqual(@as(usize, 1), vanish.len); |
| 37 | try std.testing.expectEqual(@as(usize, 1), vanish.get(0)); | 67 | try std.testing.expectEqual(@as(usize, 1), vanish.get(0)); |
| 38 | 68 | ||
| 39 | // Host 1's "a" survives host 0's list saying nothing about it. | 69 | // Host 1's "a" survives host 0's list saying nothing about it. |
| 40 | births.len = 0; | ||
| 41 | vanish.len = 0; | 70 | vanish.len = 0; |
| 42 | wall_host.planHostDiff(&tiles, &present, 3, 1, "a\n", null, &births, &binds, &vanish, &gones); | 71 | wall_host.planHostDiff(&tiles, &present, 3, 1, "a\n", null, &binds, &vanish, &gones); |
| 43 | try std.testing.expectEqual(@as(usize, 0), births.len + vanish.len); | 72 | try std.testing.expectEqual(@as(usize, 0), vanish.len); |
| 44 | 73 | ||
| 45 | // An empty list vanishes everything the host had, once every tile has | 74 | // An empty list vanishes everything the host had, once every tile has |
| 46 | // spent the grace. | 75 | // spent the grace. |
| 47 | wall_host.planHostDiff(&tiles, &present, 3, 0, "", null, &births, &binds, &vanish, &gones); | 76 | wall_host.planHostDiff(&tiles, &present, 3, 0, "", null, &binds, &vanish, &gones); |
| 48 | vanish.len = 0; | 77 | vanish.len = 0; |
| 49 | wall_host.planHostDiff(&tiles, &present, 3, 0, "", null, &births, &binds, &vanish, &gones); | 78 | wall_host.planHostDiff(&tiles, &present, 3, 0, "", null, &binds, &vanish, &gones); |
| 50 | try std.testing.expectEqual(@as(usize, 2), vanish.len); | 79 | try std.testing.expectEqual(@as(usize, 2), vanish.len); |
| 51 | try std.testing.expectEqual(@as(usize, 0), births.len); | ||
| 52 | } | 80 | } |
| 53 | 81 | ||
| 54 | test "planHostDiff: a live pump's tile is not vanished by ONE list that lacks it" { | 82 | test "planHostDiff: a live pump's tile is not vanished by ONE list that lacks it" { |
| @@ -56,7 +84,6 @@ test "planHostDiff: a live pump's tile is not vanished by ONE list that lacks it | |||
| 56 | var tiles = fixture.diffFixture(&shared); | 84 | var tiles = fixture.diffFixture(&shared); |
| 57 | var present = [_]bool{ true, true, true }; | 85 | var present = [_]bool{ true, true, true }; |
| 58 | 86 | ||
| 59 | var births = BirthNames{}; | ||
| 60 | var binds = TileIdxs{}; | 87 | var binds = TileIdxs{}; |
| 61 | var vanish = TileIdxs{}; | 88 | var vanish = TileIdxs{}; |
| 62 | var gones = TileIdxs{}; | 89 | var gones = TileIdxs{}; |
| @@ -64,19 +91,19 @@ test "planHostDiff: a live pump's tile is not vanished by ONE list that lacks it | |||
| 64 | // the name while the pump has not yet read its `exit_status`. Vanishing | 91 | // the name while the pump has not yet read its `exit_status`. Vanishing |
| 65 | // on that list loses the exit code — on a wall of one, `endedTile` skips | 92 | // on that list loses the exit code — on a wall of one, `endedTile` skips |
| 66 | // a tile that is no longer present and mux never leaves. | 93 | // a tile that is no longer present and mux never leaves. |
| 67 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish, &gones); | 94 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &binds, &vanish, &gones); |
| 68 | try std.testing.expectEqual(@as(usize, 0), vanish.len); | 95 | try std.testing.expectEqual(@as(usize, 0), vanish.len); |
| 69 | try std.testing.expect(tiles[1].missed_once); | 96 | try std.testing.expect(tiles[1].missed_once); |
| 70 | 97 | ||
| 71 | // A list that names it again forgives it: a tile does not accumulate | 98 | // A list that names it again forgives it: a tile does not accumulate |
| 72 | // misses across the seconds it is legitimately live. | 99 | // misses across the seconds it is legitimately live. |
| 73 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\n", null, &births, &binds, &vanish, &gones); | 100 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\n", null, &binds, &vanish, &gones); |
| 74 | try std.testing.expectEqual(@as(usize, 0), vanish.len); | 101 | try std.testing.expectEqual(@as(usize, 0), vanish.len); |
| 75 | try std.testing.expect(!tiles[1].missed_once); | 102 | try std.testing.expect(!tiles[1].missed_once); |
| 76 | 103 | ||
| 77 | // Two consecutive lists without it, and it goes. | 104 | // Two consecutive lists without it, and it goes. |
| 78 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish, &gones); | 105 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &binds, &vanish, &gones); |
| 79 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish, &gones); | 106 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &binds, &vanish, &gones); |
| 80 | try std.testing.expectEqual(@as(usize, 1), vanish.len); | 107 | try std.testing.expectEqual(@as(usize, 1), vanish.len); |
| 81 | try std.testing.expectEqual(@as(usize, 1), vanish.get(0)); | 108 | try std.testing.expectEqual(@as(usize, 1), vanish.get(0)); |
| 82 | 109 | ||
| @@ -85,7 +112,7 @@ test "planHostDiff: a live pump's tile is not vanished by ONE list that lacks it | |||
| 85 | tiles[0].alive.store(false, .release); | 112 | tiles[0].alive.store(false, .release); |
| 86 | vanish.len = 0; | 113 | vanish.len = 0; |
| 87 | const only0 = [_]bool{ true, false, false }; | 114 | const only0 = [_]bool{ true, false, false }; |
| 88 | wall_host.planHostDiff(&tiles, &only0, 3, 0, "", null, &births, &binds, &vanish, &gones); | 115 | wall_host.planHostDiff(&tiles, &only0, 3, 0, "", null, &binds, &vanish, &gones); |
| 89 | try std.testing.expectEqual(@as(usize, 1), vanish.len); | 116 | try std.testing.expectEqual(@as(usize, 1), vanish.len); |
| 90 | try std.testing.expectEqual(@as(usize, 0), vanish.get(0)); | 117 | try std.testing.expectEqual(@as(usize, 0), vanish.get(0)); |
| 91 | } | 118 | } |
| @@ -101,13 +128,12 @@ test "planHostDiff: a pending pane the reachable list disowns is gone, not vanis | |||
| 101 | t.alive.store(false, .release); | 128 | t.alive.store(false, .release); |
| 102 | } | 129 | } |
| 103 | 130 | ||
| 104 | var births = BirthNames{}; | ||
| 105 | var binds = TileIdxs{}; | 131 | var binds = TileIdxs{}; |
| 106 | var vanish = TileIdxs{}; | 132 | var vanish = TileIdxs{}; |
| 107 | var gones = TileIdxs{}; | 133 | var gones = TileIdxs{}; |
| 108 | // The reachable host answers with NEITHER saved session: both panes are | 134 | // The reachable host answers with NEITHER saved session: both panes are |
| 109 | // gone-fodder, neither is vanish-fodder, and host 1's tile is untouched. | 135 | // gone-fodder, neither is vanish-fodder, and host 1's tile is untouched. |
| 110 | wall_host.planHostDiff(&tiles, &present, 3, 0, "", null, &births, &binds, &vanish, &gones); | 136 | wall_host.planHostDiff(&tiles, &present, 3, 0, "", null, &binds, &vanish, &gones); |
| 111 | try std.testing.expectEqual(@as(usize, 0), vanish.len); | 137 | try std.testing.expectEqual(@as(usize, 0), vanish.len); |
| 112 | try std.testing.expectEqual(@as(usize, 2), gones.len); | 138 | try std.testing.expectEqual(@as(usize, 2), gones.len); |
| 113 | try std.testing.expectEqual(@as(usize, 0), gones.get(0)); | 139 | try std.testing.expectEqual(@as(usize, 0), gones.get(0)); |
| @@ -118,16 +144,15 @@ test "planHostDiff: a pending pane the reachable list disowns is gone, not vanis | |||
| 118 | tiles[0].state = .gone; | 144 | tiles[0].state = .gone; |
| 119 | tiles[1].state = .gone; | 145 | tiles[1].state = .gone; |
| 120 | gones.len = 0; | 146 | gones.len = 0; |
| 121 | wall_host.planHostDiff(&tiles, &present, 3, 0, "", null, &births, &binds, &vanish, &gones); | 147 | wall_host.planHostDiff(&tiles, &present, 3, 0, "", null, &binds, &vanish, &gones); |
| 122 | try std.testing.expectEqual(@as(usize, 0), gones.len); | 148 | try std.testing.expectEqual(@as(usize, 0), gones.len); |
| 123 | try std.testing.expectEqual(@as(usize, 0), vanish.len); | 149 | try std.testing.expectEqual(@as(usize, 0), vanish.len); |
| 124 | 150 | ||
| 125 | // The dressing is reversible: a list that names session "a" binds pane 0 | 151 | // The dressing is reversible: a list that names session "a" binds pane 0 |
| 126 | // (it stayed pending), and does not birth a twin. | 152 | // (it stayed pending), in the rect it never left. |
| 127 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish, &gones); | 153 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &binds, &vanish, &gones); |
| 128 | try std.testing.expectEqual(@as(usize, 1), binds.len); | 154 | try std.testing.expectEqual(@as(usize, 1), binds.len); |
| 129 | try std.testing.expectEqual(@as(usize, 0), binds.get(0)); | 155 | try std.testing.expectEqual(@as(usize, 0), binds.get(0)); |
| 130 | try std.testing.expectEqual(@as(usize, 0), births.len); | ||
| 131 | } | 156 | } |
| 132 | 157 | ||
| 133 | test "planHostDiff: a vanished tile is not present, so the next list does not vanish it twice" { | 158 | test "planHostDiff: a vanished tile is not present, so the next list does not vanish it twice" { |
| @@ -135,54 +160,41 @@ test "planHostDiff: a vanished tile is not present, so the next list does not va | |||
| 135 | var tiles = fixture.diffFixture(&shared); | 160 | var tiles = fixture.diffFixture(&shared); |
| 136 | var present = [_]bool{ true, false, true }; | 161 | var present = [_]bool{ true, false, true }; |
| 137 | 162 | ||
| 138 | var births = BirthNames{}; | ||
| 139 | var binds = TileIdxs{}; | 163 | var binds = TileIdxs{}; |
| 140 | var vanish = TileIdxs{}; | 164 | var vanish = TileIdxs{}; |
| 141 | var gones = TileIdxs{}; | 165 | var gones = TileIdxs{}; |
| 142 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish, &gones); | 166 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &binds, &vanish, &gones); |
| 143 | try std.testing.expectEqual(@as(usize, 0), vanish.len); | 167 | try std.testing.expectEqual(@as(usize, 0), vanish.len); |
| 144 | // ...and the name is not reborn either: the tile is gone, but the | ||
| 145 | // daemon no longer lists it, so there is nothing to bring back. | ||
| 146 | try std.testing.expectEqual(@as(usize, 0), births.len); | ||
| 147 | } | 168 | } |
| 148 | 169 | ||
| 149 | test "planHostDiff: the session this shell is inside is never born as a tile" { | 170 | test "planHostDiff: the session this shell is inside never wakes a pane" { |
| 150 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false }; | 171 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false }; |
| 151 | var tiles = fixture.diffFixture(&shared); | 172 | var tiles = fixture.diffFixture(&shared); |
| 152 | var present = [_]bool{ true, true, true }; | 173 | var present = [_]bool{ true, true, true }; |
| 174 | // Two pending panes, one of them spelling the session mux is running | ||
| 175 | // inside. `seedLayout` refuses such a leaf, so this is the poll's own | ||
| 176 | // half of the same refusal: a pane it woke would attach a replica to | ||
| 177 | // the grid it is painting into. | ||
| 178 | for (tiles[0..2]) |*t| { | ||
| 179 | t.pending = true; | ||
| 180 | t.state = .waiting; | ||
| 181 | t.alive.store(false, .release); | ||
| 182 | } | ||
| 153 | 183 | ||
| 154 | var births = BirthNames{}; | ||
| 155 | var binds = TileIdxs{}; | 184 | var binds = TileIdxs{}; |
| 156 | var vanish = TileIdxs{}; | 185 | var vanish = TileIdxs{}; |
| 157 | var gones = TileIdxs{}; | 186 | var gones = TileIdxs{}; |
| 158 | // The daemon has "a", "b" and "self"; "self" is the shell mux runs in, | 187 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\n", "a", &binds, &vanish, &gones); |
| 159 | // so a tile of it would paint into the grid it is reading. | 188 | // Only "b" is woken; the pane named for this shell stays as it was, |
| 160 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\nself\n", "self", &births, &binds, &vanish, &gones); | 189 | // and is not dressed gone either — the daemon does have that session. |
| 161 | try std.testing.expectEqual(@as(usize, 0), births.len); | 190 | try std.testing.expectEqual(@as(usize, 1), binds.len); |
| 191 | try std.testing.expectEqual(@as(usize, 1), binds.get(0)); | ||
| 192 | try std.testing.expect(tiles[0].pending); | ||
| 193 | try std.testing.expectEqual(@as(usize, 0), gones.len); | ||
| 162 | try std.testing.expectEqual(@as(usize, 0), vanish.len); | 194 | try std.testing.expectEqual(@as(usize, 0), vanish.len); |
| 163 | } | 195 | } |
| 164 | 196 | ||
| 165 | test "planHostDiff: a name the wire grammar refuses never becomes a tile, however the peer spells it" { | 197 | test "applyReadyLists: only the host that reported is marked, and its sessions are not tiles" { |
| 166 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false }; | ||
| 167 | var tiles = fixture.diffFixture(&shared); | ||
| 168 | var present = [_]bool{ true, true, true }; | ||
| 169 | |||
| 170 | var births = BirthNames{}; | ||
| 171 | var binds = TileIdxs{}; | ||
| 172 | var vanish = TileIdxs{}; | ||
| 173 | var gones = TileIdxs{}; | ||
| 174 | // A whole reply may be `sessions_reply_max`, so ONE "name" in it can be | ||
| 175 | // 1056 bytes; `encodeAttachNamed` memcpys the birth's name into a | ||
| 176 | // 32-byte tail behind nothing but an assert. Peer bytes are filtered | ||
| 177 | // where they become a tile, not asserted about at the wire. | ||
| 178 | const over_long = "x" ** (proto.session_name_max + 1); | ||
| 179 | const list = "a\n" ++ over_long ++ "\nhas space\n\x1b[2J\nc\n"; | ||
| 180 | wall_host.planHostDiff(&tiles, &present, 3, 0, list, null, &births, &binds, &vanish, &gones); | ||
| 181 | try std.testing.expectEqual(@as(usize, 1), births.len); | ||
| 182 | try std.testing.expectEqualStrings("c", births.get(0)); | ||
| 183 | } | ||
| 184 | |||
| 185 | test "applyReadyLists: a host added after the wall opened gets its sessions, and reaches past nothing" { | ||
| 186 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); | 198 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); |
| 187 | defer arena.deinit(); | 199 | defer arena.deinit(); |
| 188 | const alloc = arena.allocator(); | 200 | const alloc = arena.allocator(); |
| @@ -205,13 +217,15 @@ test "applyReadyLists: a host added after the wall opened gets its sessions, and | |||
| 205 | 217 | ||
| 206 | _ = wall_host.applyReadyLists(fixture.wallOf(alloc, &tiles, &present, &live, &shared, table[0..3])); | 218 | _ = wall_host.applyReadyLists(fixture.wallOf(alloc, &tiles, &present, &live, &shared, table[0..3])); |
| 207 | 219 | ||
| 208 | // The added host's session is a tile, and only the host that reported is | 220 | // Only the host that reported is marked: the two that opened the wall |
| 209 | // marked: the two that opened the wall are still owed a first list. | 221 | // are still owed a first list. |
| 210 | try std.testing.expect(table[2].applied); | 222 | try std.testing.expect(table[2].applied); |
| 211 | try std.testing.expect(!table[0].applied); | 223 | try std.testing.expect(!table[0].applied); |
| 212 | try std.testing.expect(!table[1].applied); | 224 | try std.testing.expect(!table[1].applied); |
| 213 | try std.testing.expectEqual(@as(usize, 1), live); | 225 | // ...and the session it reported is on nobody's wall: this one was |
| 214 | try std.testing.expectEqualStrings("late", tiles[0].r.session); | 226 | // authored with no panes, and a poll does not author one. |
| 227 | try std.testing.expectEqual(@as(usize, 0), live); | ||
| 228 | try std.testing.expectEqual(@as(usize, 0), wv.presentCount(present[0..])); | ||
| 215 | } | 229 | } |
| 216 | 230 | ||
| 217 | test "recordHost: a file it cannot write comes BACK — the wall may be on the alternate screen" { | 231 | test "recordHost: a file it cannot write comes BACK — the wall may be on the alternate screen" { |
| @@ -256,6 +270,11 @@ test "applyHostList: drift dresses every tile of its host, and repaints only on | |||
| 256 | }; | 270 | }; |
| 257 | fixture.setList(&table[0], "a\n# mux 0.9"); | 271 | fixture.setList(&table[0], "a\n# mux 0.9"); |
| 258 | fixture.setList(&table[1], "b\nc\n# mux 1.0 stale"); | 272 | fixture.setList(&table[1], "b\nc\n# mux 1.0 stale"); |
| 273 | try seatBound(&tiles, &present, &live, &shared, &.{ | ||
| 274 | .{ .target = .{ .sock = "/tmp/behind.sock" }, .label = "behind#a", .session = "a" }, | ||
| 275 | .{ .target = .{ .sock = "/tmp/stale.sock" }, .label = "stale#b", .session = "b" }, | ||
| 276 | .{ .target = .{ .sock = "/tmp/stale.sock" }, .label = "stale#c", .session = "c" }, | ||
| 277 | }, &.{ 0, 1, 1 }); | ||
| 259 | 278 | ||
| 260 | wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0); | 279 | wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0); |
| 261 | wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 1); | 280 | wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 1); |
| @@ -283,7 +302,7 @@ test "applyHostList: drift dresses every tile of its host, and repaints only on | |||
| 283 | try std.testing.expect(shared.repaint_gen.load(.acquire) != gen); | 302 | try std.testing.expect(shared.repaint_gen.load(.acquire) != gen); |
| 284 | } | 303 | } |
| 285 | 304 | ||
| 286 | test "applyHostList: a host with no live session gets no tile — the wall shows sessions only" { | 305 | test "applyHostList: a host makes no tile — not when it is silent, not when it lists a session" { |
| 287 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); | 306 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); |
| 288 | defer arena.deinit(); | 307 | defer arena.deinit(); |
| 289 | const alloc = arena.allocator(); | 308 | const alloc = arena.allocator(); |
| @@ -311,13 +330,15 @@ test "applyHostList: a host with no live session gets no tile — the wall shows | |||
| 311 | try std.testing.expectEqual(@as(usize, 0), live); | 330 | try std.testing.expectEqual(@as(usize, 0), live); |
| 312 | try std.testing.expectEqual(@as(usize, 0), wv.presentCount(present[0..])); | 331 | try std.testing.expectEqual(@as(usize, 0), wv.presentCount(present[0..])); |
| 313 | 332 | ||
| 314 | // A session on the empty host, and the wall has exactly the one tile. | 333 | // The third way, and the one the layout changed: the host answers with |
| 334 | // a session, and this wall — which names no pane — still shows nothing. | ||
| 335 | // A session somebody else started is not this user's authored wall. | ||
| 315 | fixture.setList(&table[1], "a\n"); | 336 | fixture.setList(&table[1], "a\n"); |
| 316 | wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 1); | 337 | for (0..2) |_| |
| 338 | wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 1); | ||
| 317 | 339 | ||
| 318 | try std.testing.expectEqual(@as(usize, 1), wv.presentCount(present[0..live])); | 340 | try std.testing.expectEqual(@as(usize, 0), live); |
| 319 | try std.testing.expectEqualStrings("a", tiles[0].r.session); | 341 | try std.testing.expectEqual(@as(usize, 0), wv.presentCount(present[0..])); |
| 320 | try std.testing.expectEqual(@as(?usize, 1), tiles[0].host); | ||
| 321 | } | 342 | } |
| 322 | 343 | ||
| 323 | test "applyHostList: a live tile survives one list that lost its session, and goes on the next" { | 344 | test "applyHostList: a live tile survives one list that lost its session, and goes on the next" { |
| @@ -329,18 +350,18 @@ test "applyHostList: a live tile survives one list that lost its session, and go | |||
| 329 | var tiles: [4]Tile = undefined; | 350 | var tiles: [4]Tile = undefined; |
| 330 | var present = [_]bool{false} ** 4; | 351 | var present = [_]bool{false} ** 4; |
| 331 | var live: usize = 0; | 352 | var live: usize = 0; |
| 332 | defer fixture.endPumps(tiles[0..live]); | 353 | // Two panes, because the tile that keeps its name is what shows the |
| 333 | // Two sessions, because the tile that keeps its name is what shows the | 354 | // grace is per tile and not a wall-wide pause. No pump ever runs here, |
| 334 | // grace is per tile and not a wall-wide pause. | 355 | // so `endPumps` has nothing to wait for and the liveness the rule turns |
| 356 | // on is stated rather than raced for. | ||
| 335 | var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")}; | 357 | var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")}; |
| 336 | fixture.setList(&table[0], "a\nb\n"); | 358 | try seatBound(&tiles, &present, &live, &shared, &.{ |
| 337 | wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0); | 359 | .{ .target = .{ .sock = "/tmp/box.sock" }, .label = "box#a", .session = "a" }, |
| 338 | try std.testing.expectEqual(@as(usize, 2), live); | 360 | .{ .target = .{ .sock = "/tmp/box.sock" }, .label = "box#b", .session = "b" }, |
| 339 | 361 | }, &.{ 0, 0 }); | |
| 340 | // The wall's pumps are stopped in this fixture, so the liveness the rule | ||
| 341 | // turns on is stated rather than raced for. | ||
| 342 | tiles[0].alive.store(true, .release); | 362 | tiles[0].alive.store(true, .release); |
| 343 | tiles[1].alive.store(true, .release); | 363 | tiles[1].alive.store(true, .release); |
| 364 | |||
| 344 | fixture.setList(&table[0], "a\n"); | 365 | fixture.setList(&table[0], "a\n"); |
| 345 | wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0); | 366 | wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0); |
| 346 | // Still there: this list may have overtaken an `exit_status` the pump | 367 | // Still there: this list may have overtaken an `exit_status` the pump |
| @@ -353,139 +374,32 @@ test "applyHostList: a live tile survives one list that lost its session, and go | |||
| 353 | try std.testing.expect(present[0]); | 374 | try std.testing.expect(present[0]); |
| 354 | } | 375 | } |
| 355 | 376 | ||
| 356 | test "applyHostList: two sessions on an empty wall are two tiles, the first focused" { | 377 | test "applyHostList: a busy daemon leaves an empty wall empty — a list is not a source of tiles" { |
| 357 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); | ||
| 358 | defer arena.deinit(); | ||
| 359 | const alloc = arena.allocator(); | ||
| 360 | var shared: Shared = undefined; | ||
| 361 | fixture.stoppedWall(alloc, &shared); | ||
| 362 | var tiles: [4]Tile = undefined; | ||
| 363 | var present = [_]bool{false} ** 4; | ||
| 364 | var live: usize = 0; | ||
| 365 | defer fixture.endPumps(tiles[0..live]); | ||
| 366 | var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")}; | ||
| 367 | fixture.setList(&table[0], "a\nb\n"); | ||
| 368 | |||
| 369 | wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0); | ||
| 370 | |||
| 371 | // The first birth has no leaf to sit beside — an empty tree takes it as | ||
| 372 | // its root, and the second inserts against it. | ||
| 373 | try std.testing.expectEqual(@as(usize, 2), live); | ||
| 374 | try std.testing.expectEqual(@as(usize, 2), shared.tree.count()); | ||
| 375 | try std.testing.expectEqualStrings("a", tiles[0].r.session); | ||
| 376 | try std.testing.expectEqualStrings("b", tiles[1].r.session); | ||
| 377 | try std.testing.expectEqual(@as(usize, 0), shared.sel); | ||
| 378 | try std.testing.expect(tiles[0].claim_pending.load(.acquire)); | ||
| 379 | } | ||
| 380 | |||
| 381 | test "applyHostList: one list's tiles are laid out in the order the daemon reported them" { | ||
| 382 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); | 378 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); |
| 383 | defer arena.deinit(); | 379 | defer arena.deinit(); |
| 384 | const alloc = arena.allocator(); | 380 | const alloc = arena.allocator(); |
| 385 | var shared: Shared = undefined; | 381 | var shared: Shared = undefined; |
| 386 | fixture.stoppedWall(alloc, &shared); | 382 | fixture.stoppedWall(alloc, &shared); |
| 387 | defer if (shared.last_flat) |*f| f.deinit(alloc); | ||
| 388 | var tiles: [4]Tile = undefined; | 383 | var tiles: [4]Tile = undefined; |
| 389 | var present = [_]bool{false} ** 4; | 384 | var present = [_]bool{false} ** 4; |
| 390 | var live: usize = 0; | 385 | var live: usize = 0; |
| 391 | defer fixture.endPumps(tiles[0..live]); | 386 | defer fixture.endPumps(tiles[0..live]); |
| 392 | var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")}; | 387 | var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")}; |
| 393 | // THREE, because two cannot fail: the first birth roots the tree and the | 388 | // THREE sessions, none of them a pane: a wall the user authored empty |
| 394 | // second lands after it whatever it anchored on. The third is the one | 389 | // stays empty however busy the box is, and the picker is the door onto |
| 395 | // that has a choice, and anchoring it back at the focus puts it BETWEEN | 390 | // it. Twice, because a tile born once is still a tile. |
| 396 | // its two siblings. | ||
| 397 | fixture.setList(&table[0], "a\nb\nc\n"); | 391 | fixture.setList(&table[0], "a\nb\nc\n"); |
| 398 | 392 | ||
| 399 | wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0); | 393 | for (0..2) |_| |
| 400 | 394 | wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0); | |
| 401 | try std.testing.expectEqual(@as(usize, 3), live); | ||
| 402 | // Down the screen in the daemon's own order. The tile ARRAY is in that | ||
| 403 | // order by construction; what this reads is the flatten, which is where | ||
| 404 | // an anchor that did not move shows up. | ||
| 405 | const flat = shared.last_flat orelse return error.NoFlat; | ||
| 406 | const top_a = (flat.rectOf(0) orelse return error.NoRect).top; | ||
| 407 | const top_b = (flat.rectOf(1) orelse return error.NoRect).top; | ||
| 408 | const top_c = (flat.rectOf(2) orelse return error.NoRect).top; | ||
| 409 | try std.testing.expectEqualStrings("a", tiles[0].r.session); | ||
| 410 | try std.testing.expectEqualStrings("b", tiles[1].r.session); | ||
| 411 | try std.testing.expectEqualStrings("c", tiles[2].r.session); | ||
| 412 | try std.testing.expect(top_a < top_b); | ||
| 413 | try std.testing.expect(top_b < top_c); | ||
| 414 | } | ||
| 415 | |||
| 416 | test "applyHostList: sessions past the wall's capacity are counted in the notice, not dropped in silence" { | ||
| 417 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); | ||
| 418 | defer arena.deinit(); | ||
| 419 | const alloc = arena.allocator(); | ||
| 420 | var shared: Shared = undefined; | ||
| 421 | fixture.stoppedWall(alloc, &shared); | ||
| 422 | var tiles: [wv.max_tiles]Tile = undefined; | ||
| 423 | var present = [_]bool{true} ** wv.max_tiles; | ||
| 424 | var live: usize = wv.max_tiles; | ||
| 425 | var names: [wv.max_tiles][2]u8 = undefined; | ||
| 426 | var text: std.ArrayList(u8) = .empty; | ||
| 427 | for (&tiles, 0..) |*t, i| { | ||
| 428 | const name = std.fmt.bufPrint(&names[i], "{d}", .{i}) catch unreachable; | ||
| 429 | t.* = .{ | ||
| 430 | .r = .{ .target = .{ .sock = "/tmp/box.sock" }, .label = "box", .session = name }, | ||
| 431 | .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 }, | ||
| 432 | .shared = &shared, | ||
| 433 | .idx = i, | ||
| 434 | .wake_r = -1, | ||
| 435 | .wake_w = -1, | ||
| 436 | .host = 0, | ||
| 437 | .alive = std.atomic.Value(bool).init(false), | ||
| 438 | }; | ||
| 439 | try text.appendSlice(alloc, name); | ||
| 440 | try text.append(alloc, '\n'); | ||
| 441 | } | ||
| 442 | // Two more than the wall can hold. | ||
| 443 | try text.appendSlice(alloc, "x\ny\n"); | ||
| 444 | var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")}; | ||
| 445 | fixture.setList(&table[0], text.items); | ||
| 446 | |||
| 447 | wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0); | ||
| 448 | |||
| 449 | try std.testing.expectEqual(@as(usize, wv.max_tiles), live); | ||
| 450 | var buf: [96]u8 = undefined; | ||
| 451 | try std.testing.expectEqualStrings("[+2 not shown]", wv.takeNotice(&shared, &buf)); | ||
| 452 | } | ||
| 453 | |||
| 454 | test "applyHostList: a wall too thin for a second pane takes what fits and retains nothing for the rest" { | ||
| 455 | // The TESTING allocator, not an arena: what this pins is that a birth | ||
| 456 | // the wall refuses leaves no allocation behind, and only leak detection | ||
| 457 | // can say so. The tile that DID fit owns its copies, so the test frees | ||
| 458 | // exactly those two. | ||
| 459 | const alloc = std.testing.allocator; | ||
| 460 | var shared: Shared = undefined; | ||
| 461 | fixture.stoppedWall(alloc, &shared); | ||
| 462 | // Four rows on a tty: one pane fits (`min_session_rows` plus its label | ||
| 463 | // bar), two never can — each would owe a bar on top of that floor. | ||
| 464 | shared.is_tty = true; | ||
| 465 | shared.size = .{ .cols = 80, .rows = 4 }; | ||
| 466 | defer shared.tree.deinit(); | ||
| 467 | defer if (shared.last_flat) |*f| f.deinit(alloc); | ||
| 468 | defer if (shared.base_flat) |*f| f.deinit(alloc); | ||
| 469 | var tiles: [4]Tile = undefined; | ||
| 470 | var present = [_]bool{false} ** 4; | ||
| 471 | var live: usize = 0; | ||
| 472 | // Freed only after `endPumps` (defers run last-declared-first): a | ||
| 473 | // pump's first act is to paint its `connecting` bar, and on a tty | ||
| 474 | // every wall has bars, so the label must outlive the thread. | ||
| 475 | defer for (tiles[0..live]) |*t| { | ||
| 476 | alloc.free(t.r.session); | ||
| 477 | alloc.free(t.r.label); | ||
| 478 | }; | ||
| 479 | defer fixture.endPumps(tiles[0..live]); | ||
| 480 | var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")}; | ||
| 481 | fixture.setList(&table[0], "a\nb\n"); | ||
| 482 | |||
| 483 | wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0); | ||
| 484 | 395 | ||
| 485 | try std.testing.expectEqual(@as(usize, 1), live); | 396 | try std.testing.expectEqual(@as(usize, 0), live); |
| 486 | try std.testing.expectEqualStrings("a", tiles[0].r.session); | 397 | try std.testing.expectEqual(@as(usize, 0), shared.tree.count()); |
| 398 | try std.testing.expectEqual(@as(usize, 0), wv.presentCount(present[0..])); | ||
| 399 | // Not even a word about them: a wall that shows the panes it was given | ||
| 400 | // is not a wall hiding the rest of the daemon. | ||
| 487 | var buf: [96]u8 = undefined; | 401 | var buf: [96]u8 = undefined; |
| 488 | try std.testing.expectEqualStrings("[+1 not shown]", wv.takeNotice(&shared, &buf)); | 402 | try std.testing.expectEqualStrings("", wv.takeNotice(&shared, &buf)); |
| 489 | } | 403 | } |
| 490 | 404 | ||
| 491 | test "applyHostList: when a host's last session goes the wall empties, and nothing stands in for the host" { | 405 | test "applyHostList: when a host's last session goes the wall empties, and nothing stands in for the host" { |
| @@ -531,19 +445,18 @@ test "planHostDiff: a creating tile whose attach has not landed yet is not vanis | |||
| 531 | tiles[1].creates = true; | 445 | tiles[1].creates = true; |
| 532 | var present = [_]bool{ true, true, true }; | 446 | var present = [_]bool{ true, true, true }; |
| 533 | 447 | ||
| 534 | var births = BirthNames{}; | ||
| 535 | var binds = TileIdxs{}; | 448 | var binds = TileIdxs{}; |
| 536 | var vanish = TileIdxs{}; | 449 | var vanish = TileIdxs{}; |
| 537 | var gones = TileIdxs{}; | 450 | var gones = TileIdxs{}; |
| 538 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish, &gones); | 451 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &binds, &vanish, &gones); |
| 539 | try std.testing.expectEqual(@as(usize, 0), vanish.len); | 452 | try std.testing.expectEqual(@as(usize, 0), vanish.len); |
| 540 | 453 | ||
| 541 | // ...and once its session exists, the same list does drop it — after | 454 | // ...and once its session exists, the same list does drop it — after |
| 542 | // the one list of grace a live pump gets: a session the daemon HAD and | 455 | // the one list of grace a live pump gets: a session the daemon HAD and |
| 543 | // no longer lists is gone. | 456 | // no longer lists is gone. |
| 544 | tiles[1].ever_up.store(true, .release); | 457 | tiles[1].ever_up.store(true, .release); |
| 545 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish, &gones); | 458 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &binds, &vanish, &gones); |
| 546 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &births, &binds, &vanish, &gones); | 459 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &binds, &vanish, &gones); |
| 547 | try std.testing.expectEqual(@as(usize, 1), vanish.len); | 460 | try std.testing.expectEqual(@as(usize, 1), vanish.len); |
| 548 | try std.testing.expectEqual(@as(usize, 1), vanish.get(0)); | 461 | try std.testing.expectEqual(@as(usize, 1), vanish.get(0)); |
| 549 | } | 462 | } |
| @@ -756,7 +669,7 @@ fn seedBench(alloc: std.mem.Allocator, tiles: []Tile, present: []bool, live: *us | |||
| 756 | live.* = 3; | 669 | live.* = 3; |
| 757 | } | 670 | } |
| 758 | 671 | ||
| 759 | test "planHostDiff: a pending pane the list names is a bind, not a birth - and the same name on another host is still a birth" { | 672 | test "planHostDiff: a pending pane the list names is a bind, and only its own host's list may wake it" { |
| 760 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); | 673 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); |
| 761 | defer arena.deinit(); | 674 | defer arena.deinit(); |
| 762 | const alloc = arena.allocator(); | 675 | const alloc = arena.allocator(); |
| @@ -769,25 +682,23 @@ test "planHostDiff: a pending pane the list names is a bind, not a birth - and t | |||
| 769 | try seedBench(alloc, &tiles, &present, &live, &shared); | 682 | try seedBench(alloc, &tiles, &present, &live, &shared); |
| 770 | defer fixture.endPumps(tiles[0..live]); | 683 | defer fixture.endPumps(tiles[0..live]); |
| 771 | 684 | ||
| 772 | var births = BirthNames{}; | ||
| 773 | var binds = TileIdxs{}; | 685 | var binds = TileIdxs{}; |
| 774 | var vanish = TileIdxs{}; | 686 | var vanish = TileIdxs{}; |
| 775 | var gones = TileIdxs{}; | 687 | var gones = TileIdxs{}; |
| 776 | // Host 0's whole list is pending panes: two binds, no birth, no twin. | 688 | // Host 0's whole list is pending panes: two binds, and no pane of |
| 777 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\n", null, &births, &binds, &vanish, &gones); | 689 | // host 1's is touched by a name host 0 spelled. |
| 778 | try std.testing.expectEqual(@as(usize, 0), births.len); | 690 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\n", null, &binds, &vanish, &gones); |
| 779 | try std.testing.expectEqual(@as(usize, 0), vanish.len); | 691 | try std.testing.expectEqual(@as(usize, 0), vanish.len); |
| 780 | try std.testing.expectEqual(@as(usize, 2), binds.len); | 692 | try std.testing.expectEqual(@as(usize, 2), binds.len); |
| 781 | try std.testing.expectEqual(@as(usize, 0), binds.get(0)); | 693 | try std.testing.expectEqual(@as(usize, 0), binds.get(0)); |
| 782 | try std.testing.expectEqual(@as(usize, 1), binds.get(1)); | 694 | try std.testing.expectEqual(@as(usize, 1), binds.get(1)); |
| 783 | // Host 1's "a" is its own pane, not host 0's; its "x" is a stranger | 695 | // Host 1's "a" is its own pane, not host 0's; its "x" is a session no |
| 784 | // the sidecar never named, so it is still a birth. | 696 | // pane spells, and the wall has nothing to say about it. |
| 785 | binds.len = 0; | 697 | binds.len = 0; |
| 786 | wall_host.planHostDiff(&tiles, &present, 3, 1, "a\nx\n", null, &births, &binds, &vanish, &gones); | 698 | wall_host.planHostDiff(&tiles, &present, 3, 1, "a\nx\n", null, &binds, &vanish, &gones); |
| 787 | try std.testing.expectEqual(@as(usize, 1), binds.len); | 699 | try std.testing.expectEqual(@as(usize, 1), binds.len); |
| 788 | try std.testing.expectEqual(@as(usize, 2), binds.get(0)); | 700 | try std.testing.expectEqual(@as(usize, 2), binds.get(0)); |
| 789 | try std.testing.expectEqual(@as(usize, 1), births.len); | 701 | try std.testing.expectEqual(@as(usize, 3), live); |
| 790 | try std.testing.expectEqualStrings("x", births.get(0)); | ||
| 791 | } | 702 | } |
| 792 | 703 | ||
| 793 | test "applyHostList: binding a saved pane moves no rect and re-cuts nothing" { | 704 | test "applyHostList: binding a saved pane moves no rect and re-cuts nothing" { |
| @@ -895,7 +806,7 @@ test "bindTile: the pane under the saved focus claims the terminal when it binds | |||
| 895 | try std.testing.expect(!tiles[0].claim_pending.load(.acquire)); | 806 | try std.testing.expect(!tiles[0].claim_pending.load(.acquire)); |
| 896 | } | 807 | } |
| 897 | 808 | ||
| 898 | test "applyHostList: the list that disowns a seeded pane still births the newcomer beside it - one re-cut" { | 809 | test "applyHostList: one list binds a pane, dresses the pane it disowns gone, and adds nothing for the name it never saw" { |
| 899 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); | 810 | var arena = std.heap.ArenaAllocator.init(std.testing.allocator); |
| 900 | defer arena.deinit(); | 811 | defer arena.deinit(); |
| 901 | const alloc = arena.allocator(); | 812 | const alloc = arena.allocator(); |
| @@ -912,23 +823,30 @@ test "applyHostList: the list that disowns a seeded pane still births the newcom | |||
| 912 | fixture.testHost(&shared, "--sock /tmp/h1.sock", "/tmp/h1.sock"), | 823 | fixture.testHost(&shared, "--sock /tmp/h1.sock", "/tmp/h1.sock"), |
| 913 | }; | 824 | }; |
| 914 | const w = fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table); | 825 | const w = fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table); |
| 826 | // The seed's own cut, then quiet: what this list must not disturb. | ||
| 915 | wall_layout.relayout(w, 0); | 827 | wall_layout.relayout(w, 0); |
| 916 | const gen = shared.repaint_gen.load(.acquire); | 828 | const gen = shared.repaint_gen.load(.acquire); |
| 829 | var rects: [3]@TypeOf(tiles[0].rect) = undefined; | ||
| 830 | for (tiles[0..3], 0..) |t, i| rects[i] = t.rect; | ||
| 917 | 831 | ||
| 918 | // One list: "a" binds, "b" is disowned, "c" is new. One relayout serves | 832 | // One list, all three answers in it: "a" binds, "b" is disowned, and |
| 919 | // the whole list however many of the three it moved. | 833 | // "c" is a session of this daemon's that no pane spells. |
| 920 | fixture.setList(&table[0], "a\nc\n"); | 834 | fixture.setList(&table[0], "a\nc\n"); |
| 921 | wall_host.applyHostList(w, 0); | 835 | wall_host.applyHostList(w, 0); |
| 922 | 836 | ||
| 923 | try std.testing.expectEqual(gen + 1, shared.repaint_gen.load(.acquire)); | ||
| 924 | try std.testing.expect(!tiles[0].pending); | 837 | try std.testing.expect(!tiles[0].pending); |
| 925 | // The disowned pane keeps its digit and its rect: the host said no, and | 838 | // The disowned pane keeps its digit and its rect: the host said no, and |
| 926 | // a pane the user saved is not the daemon's to collapse. | 839 | // a pane the user authored is not the daemon's to collapse. |
| 927 | try std.testing.expect(present[1] and tiles[1].pending); | 840 | try std.testing.expect(present[1] and tiles[1].pending); |
| 928 | // So the newcomer takes the next FREE digit rather than a freed one. | 841 | try std.testing.expectEqual(wv.State.gone, tiles[1].state); |
| 929 | try std.testing.expectEqual(@as(usize, 4), live); | 842 | // "c" is nobody's business: the wall the user wrote down is three panes |
| 930 | try std.testing.expect(present[3]); | 843 | // wide before this list and three after it. |
| 931 | try std.testing.expectEqualStrings("c", tiles[3].r.session); | 844 | try std.testing.expectEqual(@as(usize, 3), live); |
| 845 | try std.testing.expectEqual(@as(usize, 3), wv.presentCount(present[0..live])); | ||
| 846 | // Nothing was re-cut: a bind wakes a pane in place and a gone pane is | ||
| 847 | // dressed where it stands, so no rect moved and no repaint was asked for. | ||
| 848 | try std.testing.expectEqual(gen, shared.repaint_gen.load(.acquire)); | ||
| 849 | for (tiles[0..3], 0..) |t, i| try std.testing.expectEqual(rects[i], t.rect); | ||
| 932 | } | 850 | } |
| 933 | 851 | ||
| 934 | test "sendKeys: the mailbox of a pending pane holds what was typed for the pump the bind will start" { | 852 | test "sendKeys: the mailbox of a pending pane holds what was typed for the pump the bind will start" { |
| @@ -1154,3 +1072,19 @@ test "endKey: x on a gone pane drops the tile locally" { | |||
| 1154 | // coincidence, so it is asserted rather than assumed. | 1072 | // coincidence, so it is asserted rather than assumed. |
| 1155 | try std.testing.expectEqual(wv.EndKey.drop, wv.endKey(&tiles[0], 0)); | 1073 | try std.testing.expectEqual(wv.EndKey.drop, wv.endKey(&tiles[0], 0)); |
| 1156 | } | 1074 | } |
| 1075 | |||
| 1076 | test "planHostDiff: a session the daemon has and the wall does not is nobody's business: no birth, no tile" { | ||
| 1077 | var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false }; | ||
| 1078 | var tiles = fixture.diffFixture(&shared); | ||
| 1079 | var present = [_]bool{ true, true, true }; | ||
| 1080 | var binds = TileIdxs{}; | ||
| 1081 | var vanish = TileIdxs{}; | ||
| 1082 | var gones = TileIdxs{}; | ||
| 1083 | // Host 0 answers with its two panes' sessions and three the wall never | ||
| 1084 | // asked for. Twice, so the grace has been spent and a vanish would show. | ||
| 1085 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\nx\ny\nz\n", null, &binds, &vanish, &gones); | ||
| 1086 | wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\nx\ny\nz\n", null, &binds, &vanish, &gones); | ||
| 1087 | try std.testing.expectEqual(@as(usize, 0), vanish.len); | ||
| 1088 | try std.testing.expectEqual(@as(usize, 0), gones.len); | ||
| 1089 | try std.testing.expectEqual(@as(usize, 3), wv.presentCount(&present)); | ||
| 1090 | } | ||