//! 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 }); } 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; } };