summaryrefslogtreecommitdiff
path: root/src/ninep/testing.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-20 00:36:50 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commitc1990b3e6e196ad41379aa432bf3ccca8a65d9f0 (patch)
treed7ad5afe559f7f56464b14c3122242fa3f16399b /src/ninep/testing.zig
parent717afaf3177a2e0925b18ae3445119efe808b129 (diff)
downloadpardes-c1990b3e6e196ad41379aa432bf3ccca8a65d9f0.tar.gz
pardes-c1990b3e6e196ad41379aa432bf3ccca8a65d9f0.zip
Flatten the 9P control tree and move it out of fs.zig
The served tree loses the self/ level: /index /ctl /new /log /screen /listeners /pane/<n>/... /os, with /src only in -Dembed-sources=true builds (default off, on for esp32p4). ctl speaks the editor's own language with two lowercase verbs, look TEXT and exec TEXT, plus acme's addr verbs; the new/ factory directory becomes one clone file; cons is gone (exec Msg); name and sel are files; stats report real lengths, modes and mtimes; /log streams pane new/del/rename/save events. The tree code lives in src/ninep/ (tree, pane, ctl, addr, pty, events, screen, sources); fs.zig keeps host access, mounts, resolution and find/grep. Same engine and transports. README (fs-help.txt) and docs rewritten; tests updated and extended. Co-Authored-By: Claude Fable 5.1 <[email protected]>
Diffstat (limited to 'src/ninep/testing.zig')
-rw-r--r--src/ninep/testing.zig135
1 files changed, 135 insertions, 0 deletions
diff --git a/src/ninep/testing.zig b/src/ninep/testing.zig
new file mode 100644
index 00000000..ba26cb38
--- /dev/null
+++ b/src/ninep/testing.zig
@@ -0,0 +1,135 @@
+//! 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,
+
+ 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 {
+ p.update(.{ .fs_req = req });
+ var ans: Answer = .{};
+ while (p.nextEffect()) |e| switch (e) {
+ .fs_reply => |r| {
+ ans.reply = r;
+ ans.bytes = p.fsPayload(r);
+ },
+ .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,
+ .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 });
+}
+
+/// Opens /new once, as a client would, and returns the pane it made.
+pub fn newPane(p: *Pardes) !u32 {
+ const new = @intFromEnum(tree.TopFile.new);
+ const opened = call(p, .{ .tag = 9, .op = .open, .node = new });
+ if (opened.reply.status != .ok or opened.reply.handle == 0) return error.NoPane;
+ _ = call(p, .{ .tag = 9, .op = .release, .node = new, .handle = opened.reply.handle });
+ return opened.reply.handle;
+}
+
+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;
+ }
+};