summaryrefslogtreecommitdiff
path: root/src/ninep/testing.zig
blob: b8eaa02bf02b41b3e2de195564f8fb2c67100998 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
//! Test helpers shared by the control-tree modules: drive one request
//! through the core and collect the reply and the effects it raised.
const std = @import("std");
const pardes = @import("../pardes.zig");
const tree = @import("tree.zig");

const Pardes = pardes.Pardes;
pub const Req = tree.Req;
pub const Reply = tree.Reply;
pub const E = tree.E;

pub const Answer = struct {
    reply: Reply = .{ .tag = 0, .status = .err, .errno = E.IO },
    bytes: []const u8 = "",
    saved: bool = false,
    watch: ?bool = null,
    pty_buf: [256]u8 = undefined,
    pty_len: usize = 0,
    winsize: ?struct { cols: u16, rows: u16 } = null,
    signal: ?pardes.PtySignal = null,
    spawned: bool = false,
    closed: bool = false,

    pub fn pty(a: *const Answer) []const u8 {
        return a.pty_buf[0..a.pty_len];
    }

    pub fn errno(a: Answer) u16 {
        return if (a.reply.status == .err) a.reply.errno else 0;
    }
};

pub fn call(p: *Pardes, req: Req) Answer {
    var ans: Answer = .{};
    ans.reply = p.serveFs(req);
    ans.bytes = p.fsPayload(ans.reply);
    while (p.nextEffect()) |e| switch (e) {
        .save_file, .save_text => ans.saved = true,
        .watch => |w| ans.watch = w.on,
        .write => |w| {
            const b = w.bytes.slice();
            const n = @min(b.len, ans.pty_buf.len - ans.pty_len);
            @memcpy(ans.pty_buf[ans.pty_len..][0..n], b[0..n]);
            ans.pty_len += n;
        },
        .resize_pty => |r| ans.winsize = .{ .cols = r.cols, .rows = r.rows },
        .signal_pty => |s| ans.signal = s.sig,
        .close_pty => ans.closed = true,
        .spawn => ans.spawned = true,
        else => {},
    };
    return ans;
}

pub fn rd(p: *Pardes, node: u64, off: u64, size: u32) Answer {
    return call(p, .{ .tag = 1, .op = .read, .node = node, .off = off, .size = size });
}

/// Whether the log, read whole from a fresh open, holds `needle`.
pub fn logHas(p: *Pardes, needle: []const u8) bool {
    return logCount(p, needle) > 0;
}

/// How many times the log, read whole from a fresh open, holds `needle`.
pub fn logCount(p: *Pardes, needle: []const u8) usize {
    const log = @intFromEnum(tree.TopFile.log);
    const h = call(p, .{ .tag = 1, .op = .open, .node = log }).reply.handle;
    defer _ = call(p, .{ .tag = 3, .op = .release, .node = log, .handle = h });
    const text = call(p, .{ .tag = 2, .op = .read, .node = log, .handle = h, .size = 1 << 16 }).bytes;
    return std.mem.count(u8, text, needle);
}

pub fn wr(p: *Pardes, node: u64, data: []const u8) Answer {
    return call(p, .{ .tag = 2, .op = .write, .node = node, .data = data });
}

pub fn rdir(p: *Pardes, node: u64, skip: u64) Answer {
    return call(p, .{ .tag = 4, .op = .readdir, .node = node, .off = skip, .size = 4096 });
}

pub fn look_up(p: *Pardes, dir: u64, name: []const u8) Answer {
    return call(p, .{ .tag = 3, .op = .lookup, .node = dir, .data = name });
}

/// Makes a pane the way a client does, by walking to /pane/new, and answers
/// the serial of the pane the walk landed on.
/// The open is what makes the pane; the handle it answers is the serial, which
/// is also what a read of that fid reports.
pub fn newPane(p: *Pardes) !u32 {
    const made = call(p, .{ .tag = 8, .op = .open, .node = @intFromEnum(tree.TopFile.new) });
    if (made.reply.status != .ok or made.reply.handle == 0) return error.NoPane;
    return made.reply.handle;
}

pub fn rmdir(p: *Pardes, node: u64) Answer {
    return call(p, .{ .tag = 9, .op = .release, .node = node, .remove = true });
}

pub fn withFile(gpa: std.mem.Allocator, text: []const u8) !*Pardes {
    const p = try Pardes.init(gpa, .{ .tty_only = true, .cols = 80, .rows = 24 });
    errdefer p.deinit();
    while (p.nextEffect()) |_| {}
    _ = try p.setTestFile(text);
    while (p.nextEffect()) |_| {}
    return p;
}

pub fn withTerm(gpa: std.mem.Allocator) !*Pardes {
    const p = try Pardes.init(gpa, .{ .tty_only = true, .cols = 80, .rows = 24 });
    errdefer p.deinit();
    while (p.nextEffect()) |_| {}
    std.debug.assert(p.panes[0].?.isTerminal());
    return p;
}

pub fn serialOf(p: *Pardes) u32 {
    return p.panes[0].?.serial;
}

pub const Dirent = struct { node: u64, dir: bool, name: []const u8 };

pub fn dirents(bytes: []const u8, out: []Dirent) []Dirent {
    var n: usize = 0;
    var i: usize = 0;
    while (i + 10 <= bytes.len and n < out.len) {
        const node = std.mem.readInt(u64, bytes[i..][0..8], .little);
        const kind = bytes[i + 8];
        const len = bytes[i + 9];
        i += 10;
        if (i + len > bytes.len) break;
        out[n] = .{ .node = node, .dir = kind == 1, .name = bytes[i .. i + len] };
        i += len;
        n += 1;
    }
    return out[0..n];
}

pub fn nameAt(list: []const Dirent, want: []const u8) ?Dirent {
    for (list) |d| if (std.mem.eql(u8, d.name, want)) return d;
    return null;
}

pub const FakeTty = struct {
    taken: bool,

    pub const vtable: pardes.Host.VTable = .{ .tty_taken = answer };

    fn answer(ctx: ?*anyopaque, pane: u8) bool {
        _ = pane;
        const f: *FakeTty = @ptrCast(@alignCast(ctx.?));
        return f.taken;
    }
};