a73x

src/tui/wall_test_harness.zig

Ref:   Size: 10.7 KiB   History

//! Fixtures the wall's test files share: benches that stand a wall up
//! without a terminal, and the transports and screens they read back.
const std = @import("std");
const proto = @import("term").protocol;
const client = @import("client");
const Engine = @import("engine").Engine;
const layout = @import("client").layout;
const wall_host = @import("wall_host.zig");
const wall_picker = @import("wall_picker.zig");
const wall_pump = @import("wall_pump.zig");
const wv = @import("wallview.zig");
const EndReason = wv.EndReason;
const Host = wall_host.Host;
const Shared = wv.Shared;
const Tile = wv.Tile;

/// The `Wall` the product passes around, from a test's own arrays.
pub fn wallOf(
    alloc: std.mem.Allocator,
    tiles: []Tile,
    present: []bool,
    live: *usize,
    shared: *Shared,
    hosts: []Host,
) wv.Wall {
    return .{ .alloc = alloc, .tiles = tiles, .present = present, .live = live, .shared = shared, .hosts = hosts };
}

/// The cell `wallLive` points at. File-scope because a layout test builds
/// its wall per call and Zig runs the tests in one process, one at a time;
/// nothing here births, so nothing writes it but the call being made. The
/// nearer trap is intra-test: two `wallLive` calls with different `live`
/// share this cell, and the first wall then reads the second's count.
var standing_live: usize = 0;

/// A wall of `live` slots over arrays a test declared, with no hosts: what
/// the layout and focus calls take, none of which grows the wall.
fn wallLive(alloc: std.mem.Allocator, tiles: []Tile, present: []bool, live: usize, shared: *Shared) wv.Wall {
    standing_live = live;
    return wallOf(alloc, tiles, present, &standing_live, shared, &.{});
}

/// `wallLive` where every slot the test declared is on the wall.
pub fn wallAll(alloc: std.mem.Allocator, tiles: []Tile, present: []bool, shared: *Shared) wv.Wall {
    return wallLive(alloc, tiles, present, tiles.len, shared);
}

/// Three tiles: two sessions on host 0 and a same-named one on host 1, so a
/// diff that aliased by NAME alone would be caught.
pub fn diffFixture(shared: *Shared) [3]Tile {
    const t0: client.Target = .{ .sock = "/tmp/h0.sock" };
    const t1: client.Target = .{ .sock = "/tmp/h1.sock" };
    const rect: layout.Rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 };
    return .{
        .{
            .r = .{ .target = t0, .label = "--sock /tmp/h0.sock#a", .session = "a" },
            .rect = rect,
            .shared = shared,
            .idx = 0,
            .wake_r = -1,
            .wake_w = -1,
            .host = 0,
        },
        .{
            .r = .{ .target = t0, .label = "--sock /tmp/h0.sock#b", .session = "b" },
            .rect = rect,
            .shared = shared,
            .idx = 1,
            .wake_r = -1,
            .wake_w = -1,
            .host = 0,
        },
        .{
            .r = .{ .target = t1, .label = "--sock /tmp/h1.sock#a", .session = "a" },
            .rect = rect,
            .shared = shared,
            .idx = 2,
            .wake_r = -1,
            .wake_w = -1,
            .host = 1,
        },
    };
}

/// The keyboard-thread fixture: `running` stopped, so a pump a diff spawns
/// returns at its first check rather than dialling a socket no test is
/// listening on.
pub fn stoppedWall(alloc: std.mem.Allocator, shared: *Shared) void {
    shared.* = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
    shared.tree = layout.Tree.init(alloc);
    shared.flat_alloc = alloc;
    shared.running.store(false, .release);
}

/// Detached pumps hold `*Tile` into the test's own frame; nothing may return
/// while one is still running. The wake pipes go here too: the WALL never
/// closes one (see `ring`), and a test binary would run out of fds.
pub fn endPumps(tiles: []Tile) void {
    for (tiles) |*t| {
        while (t.alive.load(.acquire)) std.Thread.sleep(std.time.ns_per_ms);
    }
    for (tiles) |*t| {
        if (t.wake_r >= 0) std.posix.close(t.wake_r);
        if (t.wake_w >= 0) std.posix.close(t.wake_w);
        t.wake_r = -1;
        t.wake_w = -1;
    }
}

pub fn testHost(shared: *Shared, spelling: []const u8, sock: []const u8) Host {
    return .{
        .spec = .{ .spelling = spelling, .target = .{ .sock = sock }, .poll_target = .{ .sock = sock } },
        .shared = shared,
    };
}

pub fn setList(h: *Host, list: []const u8) void {
    @memcpy(h.poll.list[0..list.len], list);
    h.poll.list_len = list.len;
    h.poll.reachable.store(true, .release);
}

/// A wall of hosts and no tiles: what a test about the BOX rather than the
/// panes under it paints from. `hostWall`'s own cell, not `standing_live`,
/// so a test that already stood a wall up does not have its count zeroed by
/// painting a popup over it.
var host_wall_live: usize = 0;
var no_tiles: [0]Tile = .{};
var no_present: [0]bool = .{};

pub fn hostWall(shared: *Shared, host_table: []Host) wv.Wall {
    host_wall_live = 0;
    return wallOf(std.testing.allocator, &no_tiles, &no_present, &host_wall_live, shared, host_table);
}

// A picker painted into a pipe, drained. Non-blocking on both ends so a
// frame that outgrew the pipe FAILS here rather than parking the suite.
pub fn pickerFrame(shared: *Shared, r: std.posix.fd_t, host_table: []Host, sel: usize, out: []u8) []const u8 {
    return pickerFrameAt(shared, r, host_table, sel, .{}, out);
}

/// `pickerFrame` at a stated level and row: the session list is a second
/// body out of the same box, and the paint is what a test can judge.
pub fn pickerFrameAt(
    shared: *Shared,
    r: std.posix.fd_t,
    host_table: []Host,
    sel: usize,
    view: wall_picker.PickerView,
    out: []u8,
) []const u8 {
    wall_picker.paintPicker(hostWall(shared, host_table), sel, view, null);
    const n = std.posix.read(r, out) catch 0;
    return out[0..n];
}

pub fn claimBench(shared: *Shared, idx: usize) Tile {
    return .{
        .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "0" },
        .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 },
        .shared = shared,
        .idx = idx,
        .wake_r = -1,
        .wake_w = -1,
    };
}

/// Only the fields `endAction` reads: a whole Tile describes the pump, not
/// the rule.
pub fn endBench(
    tiles: []Tile,
    shared: *Shared,
    ended: usize,
    reason: EndReason,
    code: u8,
    born_from: ?usize,
) void {
    for (tiles, 0..) |*t, i| {
        t.* = .{
            .r = .{ .target = .{ .sock = "/s" }, .label = "t", .session = "" },
            .rect = .{ .top = 0, .left = 0, .rows = 2, .cols = 80 },
            .shared = shared,
            .idx = i,
        };
    }
    tiles[ended].end.store(@intFromEnum(reason), .release);
    tiles[ended].code.store(code, .release);
    tiles[ended].born_from = born_from;
}

// A pump, reduced to the one thing this test judges: it takes passes, and
// remembers the content size the pass told it to send.
pub const ResizeWitness = struct {
    t: *Tile,
    stop: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
    sent: std.atomic.Value(u32) = std.atomic.Value(u32).init(0),
    passes: std.atomic.Value(u64) = std.atomic.Value(u64).init(0),

    pub fn run(w: *ResizeWitness) void {
        while (!w.stop.load(.acquire)) {
            const p = wall_pump.takePass(w.t);
            if (p.resize) w.sent.store(pack(p.cols, p.rows -| p.label_rows), .release);
            _ = w.passes.fetchAdd(1, .release);
        }
    }

    pub fn pack(cols: u16, rows: u16) u32 {
        return (@as(u32, cols) << 16) | @as(u32, rows);
    }
};

// The terminal, as an oracle. The wall's bytes are replayed into an engine
// and the GRID is judged: a bar painted at a row the terminal no longer has
// looks fine in the bytes — the terminal clamps the CUP onto its last row —
// and lands on top of somebody else's content in the grid.
pub const WallScreen = struct {
    eng: *Engine,
    r: std.posix.fd_t,
    w: std.posix.fd_t,

    pub fn init(alloc: std.mem.Allocator, cols: u16, rows: u16) !WallScreen {
        const p = try std.posix.pipe2(.{ .NONBLOCK = true });
        return .{
            .eng = try Engine.init(alloc, .{ .cols = cols, .rows = rows }),
            .r = p[0],
            .w = p[1],
        };
    }

    pub fn deinit(self: *WallScreen) void {
        self.eng.deinit();
        std.posix.close(self.r);
        std.posix.close(self.w);
    }

    pub fn drain(self: *WallScreen) void {
        var buf: [4096]u8 = undefined;
        while (std.posix.read(self.r, &buf)) |n| {
            if (n == 0) break;
            self.eng.feed(buf[0..n]);
        } else |_| {}
    }

    pub fn line(text: []const u8, n: u16) ?[]const u8 {
        var it = std.mem.splitScalar(u8, text, '\n');
        var i: u16 = 0;
        while (it.next()) |l| : (i += 1) if (i == n) return l;
        return null;
    }
};

pub fn noticeBench(shared: *Shared, tiles: []Tile) void {
    for (tiles, 0..) |*t, i| {
        t.* = Tile{
            .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" },
            .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 },
            .shared = shared,
            .idx = i,
            .wake_r = -1,
            .wake_w = -1,
        };
    }
}

// A transport that swallows every frame: a Core's selection request goes
// nowhere, which is what lets a copy test stand up without a daemon.
pub const SwallowTransport = struct {
    pub fn writeFrame(_: *SwallowTransport, _: proto.MsgType, _: []const u8) !void {}
};

// One selection_reply payload — id, status, watermark, text — in the wire
// form encodeSelectionReply writes, laid out in a caller buffer so a test
// does not allocate the prefix separately.
pub fn replyBytes(
    buf: []u8,
    id: u32,
    status: proto.SelectionStatus,
    history_rows: u32,
    text: []const u8,
) []const u8 {
    std.mem.writeInt(u32, buf[0..4], id, .little);
    buf[4] = @intFromEnum(status);
    std.mem.writeInt(u32, buf[5..9], history_rows, .little);
    @memset(buf[9..proto.selection_reply_prefix_len], 0);
    @memcpy(buf[proto.selection_reply_prefix_len..][0..text.len], text);
    return buf[0 .. proto.selection_reply_prefix_len + text.len];
}

/// Whether the other end of a non-blocking pipe has been closed: EOF says
/// yes, EAGAIN says the writer is still holding it.
pub fn peerClosed(fd: std.posix.fd_t) bool {
    var b: [1]u8 = undefined;
    const n = std.posix.read(fd, &b) catch return false;
    return n == 0;
}

/// Everything waiting on a non-blocking pipe. Non-blocking so a half that
/// stopped writing fails the assertion rather than parking the test on a
/// read that will never return.
pub fn readAvail(fd: std.posix.fd_t, buf: []u8) []const u8 {
    var n: usize = 0;
    while (std.posix.read(fd, buf[n..])) |got| {
        if (got == 0) break;
        n += got;
    } else |_| {}
    return buf[0..n];
}