From c1990b3e6e196ad41379aa432bf3ccca8a65d9f0 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sun, 20 Sep 2026 00:36:50 -0300 Subject: 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//... /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 --- src/ninep/testing.zig | 135 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 src/ninep/testing.zig (limited to 'src/ninep/testing.zig') 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; + } +}; -- cgit v1.3