diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-06 18:11:36 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-07 13:59:12 -0300 |
| commit | 60367d8fe23f6af98ec28e3cf6c2094dfe332df0 (patch) | |
| tree | 310fc734173cf771881f4691c71909135fadde97 /src/esp32p4_gpio.zig | |
| parent | fa82cac885cb4738fe36d1e49b4749b5a3e31a4a (diff) | |
| download | pardes-60367d8fe23f6af98ec28e3cf6c2094dfe332df0.tar.gz pardes-60367d8fe23f6af98ec28e3cf6c2094dfe332df0.zip | |
Refactor panes and filesystem; replace FUSE with 9P
Consolidate pane, layout, memory and host code. Serve 9P by default over Unix sockets, with runtime mounts and optional TCP/QUIC transports. Remove FUSE and obsolete proof-of-concept examples.
Fix highlighting and terminal-history performance, expand differential and stress-test infrastructure, sort navigation results while preserving the next occurrence, add syntax-colored Braille minimaps, remove SPC-k, and document 9P interaction as a repository skill.
Diffstat (limited to 'src/esp32p4_gpio.zig')
| -rw-r--r-- | src/esp32p4_gpio.zig | 576 |
1 files changed, 576 insertions, 0 deletions
diff --git a/src/esp32p4_gpio.zig b/src/esp32p4_gpio.zig new file mode 100644 index 00000000..92468581 --- /dev/null +++ b/src/esp32p4_gpio.zig @@ -0,0 +1,576 @@ +//! ESP32-P4 JP1 GPIO data and its freestanding filesystem. +const std = @import("std"); + +// JP1: JC-ESP32P4-M3-DEV schematic, sheet 2 "Expand IO": +// 01-esp32p4-m3/docs/schematics/2_EXPAND_IO&BAT.png +pub const Header = struct { + pub const Pad = union(enum) { + none, + gpio: u8, + // C6 and power nets are not P4 GPIOs; the schematic gives no GPIO numbers for ES_I2C. + net: []const u8, + + fn label(comptime p: Pad) []const u8 { + return switch (p) { + .none => "--", + .gpio => |n| std.fmt.comptimePrint("GPIO {d}", .{n}), + .net => |s| s, + }; + } + }; + + pub const Row = struct { left: Pad, right: Pad }; + + pub const rows = [13]Row{ + .{ .left = .{ .net = "3V3" }, .right = .{ .net = "5V" } }, + .{ .left = .{ .net = "3V3" }, .right = .{ .net = "5V" } }, + .{ .left = .{ .net = "GND" }, .right = .{ .net = "GND" } }, + .{ .left = .{ .gpio = 1 }, .right = .none }, + .{ .left = .{ .gpio = 2 }, .right = .{ .gpio = 47 } }, + .{ .left = .{ .gpio = 3 }, .right = .{ .gpio = 46 } }, + .{ .left = .{ .gpio = 4 }, .right = .{ .gpio = 45 } }, + .{ .left = .{ .gpio = 5 }, .right = .{ .net = "GND" } }, + .{ .left = .{ .gpio = 20 }, .right = .{ .net = "3V3" } }, + .{ .left = .{ .gpio = 32 }, .right = .{ .net = "C6_U0RXD" } }, + .{ .left = .{ .gpio = 33 }, .right = .{ .net = "C6_U0TXD" } }, + .{ .left = .{ .net = "ES_I2C_SDA" }, .right = .{ .net = "C6_IO9" } }, + .{ .left = .{ .net = "ES_I2C_SCL" }, .right = .{ .net = "C6_CHIP_PU" } }, + }; + + const row_format = "{s:>10} | {d:>2} | {d:>2} | {s}\n"; + + const border = " +---------+\n"; + + pub const text = rendered: { + var out: []const u8 = + \\JP1 header - 26 pins, pin 1 top left. + \\Every number here is DECIMAL. + \\ + \\ + ; + out = out ++ border; + for (rows, 0..) |row, i| out = out ++ std.fmt.comptimePrint( + row_format, + .{ row.left.label(), 2 * i + 1, 2 * i + 2, row.right.label() }, + ); + break :rendered out ++ border ++ + \\ + \\Gpio <pin> flips one: 0->1 or 1->0. + \\ + ; + }; + + pub const gpio_pins = pins: { + var found: [2 * rows.len]u8 = undefined; + var n: usize = 0; + for (rows) |row| for ([2]Pad{ row.left, row.right }) |p| switch (p) { + .gpio => |g| { + found[n] = g; + n += 1; + }, + else => {}, + }; + std.mem.sort(u8, found[0..n], {}, std.sort.asc(u8)); + break :pins found[0..n].*; + }; + + test "the rendered header is the drawing the console has always printed" { + try std.testing.expectEqualStrings( + \\JP1 header - 26 pins, pin 1 top left. + \\Every number here is DECIMAL. + \\ + \\ +---------+ + \\ 3V3 | 1 | 2 | 5V + \\ 3V3 | 3 | 4 | 5V + \\ GND | 5 | 6 | GND + \\ GPIO 1 | 7 | 8 | -- + \\ GPIO 2 | 9 | 10 | GPIO 47 + \\ GPIO 3 | 11 | 12 | GPIO 46 + \\ GPIO 4 | 13 | 14 | GPIO 45 + \\ GPIO 5 | 15 | 16 | GND + \\ GPIO 20 | 17 | 18 | 3V3 + \\ GPIO 32 | 19 | 20 | C6_U0RXD + \\ GPIO 33 | 21 | 22 | C6_U0TXD + \\ES_I2C_SDA | 23 | 24 | C6_IO9 + \\ES_I2C_SCL | 25 | 26 | C6_CHIP_PU + \\ +---------+ + \\ + \\Gpio <pin> flips one: 0->1 or 1->0. + \\ + , text); + } + + test "the header's own GPIOs, and only those" { + try std.testing.expectEqualSlices(u8, &.{ 1, 2, 3, 4, 5, 20, 32, 33, 45, 46, 47 }, &gpio_pins); + try std.testing.expectEqual(Pad.none, rows[3].right); + try std.testing.expectEqualStrings("C6_IO9", rows[11].right.net); + } +}; + +pub const E = struct { + pub const NOENT: u16 = 2; + pub const NOTDIR: u16 = 20; + pub const INVAL: u16 = 22; +}; + +pub const root: u64 = 1; +const gpio_node: u64 = 16; +const pinout_node: u64 = 17; + +const Entry = struct { + node: u64, + parent: u64, + name: []const u8, + kind: enum { directory, pinout, value }, + pin: u8 = 0, +}; + +const entries = table: { + var result: [3 + 2 * Header.gpio_pins.len]Entry = undefined; + result[0] = .{ .node = root, .parent = root, .name = "/", .kind = .directory }; + result[1] = .{ .node = gpio_node, .parent = root, .name = "gpio", .kind = .directory }; + result[2] = .{ .node = pinout_node, .parent = gpio_node, .name = "pinout", .kind = .pinout }; + for (Header.gpio_pins, 0..) |pin, i| { + const directory = 18 + i; + result[3 + 2 * i] = .{ + .node = directory, + .parent = gpio_node, + .name = std.fmt.comptimePrint("{d}", .{pin}), + .kind = .directory, + }; + result[4 + 2 * i] = .{ + .node = (2 + i) * 16 + 1, + .parent = directory, + .name = "value", + .kind = .value, + .pin = pin, + }; + } + break :table result; +}; + +const dirent_fixed = 10; +const out_capacity = size: { + var result: usize = 2; + for (entries) |directory| { + if (directory.kind != .directory) continue; + var bytes: usize = 0; + for (entries) |entry| { + if (entry.parent == directory.node and entry.node != directory.node) + bytes += dirent_fixed + entry.name.len; + } + result = @max(result, bytes); + } + break :size result; +}; + +pub fn Fs(comptime Pads: type) type { + return struct { + const Self = @This(); + pub const name_capacity = 28; + + pub const Op = enum(u8) { lookup, getattr, setattr, open, read, write, release, readdir }; + + pub const Status = enum(u8) { ok, again, err }; + + pub const Req = struct { + tag: u64, + op: Op, + node: u64, + handle: u32 = 0, + off: u64 = 0, + size: u32 = 0, + data: []const u8 = &.{}, + truncate: bool = false, + }; + + pub const Reply = struct { + tag: u64, + status: Status = .ok, + errno: u16 = 0, + attr: Attr = .{}, + handle: u32 = 0, + written: u32 = 0, + + pub const Attr = struct { + name: []const u8 = "", + node: u64 = 0, + dir: bool = false, + size: u64 = 0, + mode: u16 = 0o600, + }; + + pub fn fail(tag: u64, e: u16) Reply { + return .{ .tag = tag, .status = .err, .errno = e }; + } + }; + + // Reply bytes are borrowed until the next handle call. + pub const Answer = struct { reply: Reply, bytes: []const u8 = "" }; + + out: [out_capacity]u8 = undefined, + + fn find(node: u64) ?*const Entry { + for (&entries) |*entry| if (entry.node == node) return entry; + return null; + } + + fn attributes(entry: *const Entry) Reply.Attr { + return .{ + .name = entry.name, + .node = entry.node, + .dir = entry.kind == .directory, + .mode = switch (entry.kind) { + .directory => 0o500, + .pinout => 0o400, + .value => 0o600, + }, + .size = switch (entry.kind) { + .directory => 0, + .pinout => Header.text.len, + .value => 2, + }, + }; + } + + pub fn handle(fs: *Self, req: Req) Answer { + const entry = find(req.node) orelse return .{ .reply = .fail(req.tag, E.NOENT) }; + switch (req.op) { + .lookup => { + if (entry.kind != .directory) return .{ .reply = .fail(req.tag, E.NOTDIR) }; + if (std.mem.eql(u8, req.data, "..")) + return .{ .reply = .{ .tag = req.tag, .attr = attributes(find(entry.parent).?) } }; + for (&entries) |*child| { + if (child.parent != req.node or child.node == req.node) continue; + if (!std.mem.eql(u8, child.name, req.data)) continue; + return .{ .reply = .{ .tag = req.tag, .attr = attributes(child) } }; + } + return .{ .reply = .fail(req.tag, E.NOENT) }; + }, + .getattr => return .{ .reply = .{ .tag = req.tag, .attr = attributes(entry) } }, + .setattr => { + // GPIO register views have no storage to truncate. + if (entry.kind == .directory) return .{ .reply = .fail(req.tag, E.INVAL) }; + return .{ .reply = .{ .tag = req.tag, .attr = attributes(entry) } }; + }, + .open => return .{ .reply = .{ .tag = req.tag, .handle = 1 } }, + .release => return .{ .reply = .{ .tag = req.tag } }, + .read => { + const bytes = switch (entry.kind) { + .directory => return .{ .reply = .fail(req.tag, E.INVAL) }, + .pinout => Header.text, + .value => value: { + fs.out[0] = '0' + @as(u8, Pads.level(entry.pin)); + fs.out[1] = '\n'; + break :value fs.out[0..2]; + }, + }; + const off: usize = @intCast(@min(req.off, bytes.len)); + return .{ .reply = .{ .tag = req.tag }, .bytes = bytes[off..][0..@min(bytes.len - off, req.size)] }; + }, + .write => { + if (entry.kind != .value or req.off != 0) return .{ .reply = .fail(req.tag, E.INVAL) }; + const value = std.mem.trimEnd(u8, req.data, "\r\n"); + if (value.len != 1 or (value[0] != '0' and value[0] != '1')) + return .{ .reply = .fail(req.tag, E.INVAL) }; + Pads.drive(entry.pin, @intCast(value[0] - '0')); + return .{ .reply = .{ .tag = req.tag, .written = @intCast(req.data.len) } }; + }, + .readdir => { + if (entry.kind != .directory) return .{ .reply = .fail(req.tag, E.NOTDIR) }; + var len: usize = 0; + var skip = req.off; + for (entries) |child| { + if (child.parent != req.node or child.node == req.node) continue; + if (skip != 0) { + skip -= 1; + continue; + } + std.mem.writeInt(u64, fs.out[len..][0..8], child.node, .little); + fs.out[len + 8] = @intFromBool(child.kind == .directory); + fs.out[len + 9] = @intCast(child.name.len); + @memcpy(fs.out[len + dirent_fixed ..][0..child.name.len], child.name); + len += dirent_fixed + child.name.len; + } + return .{ .reply = .{ .tag = req.tag }, .bytes = fs.out[0..len] }; + }, + } + } + }; +} + +const testing = std.testing; + +const StubPads = struct { + var driven: [64]u1 = @splat(0); + var log: [16]Call = undefined; + var log_len: usize = 0; + + const Call = struct { pin: u8, level: u1 }; + + fn reset() void { + driven = @splat(0); + log_len = 0; + } + + fn level(pin: u8) u1 { + return driven[pin]; + } + + fn drive(pin: u8, want: u1) void { + driven[pin] = want; + log[log_len] = .{ .pin = pin, .level = want }; + log_len += 1; + } +}; + +const Board = Fs(StubPads); + +fn walk(t: *Board, path: []const []const u8) !Board.Reply.Attr { + var at: u64 = root; + var attr: Board.Reply.Attr = .{ .node = root, .dir = true, .mode = 0o500 }; + for (path) |name| { + const a = t.handle(.{ .tag = 1, .op = .lookup, .node = at, .data = name }); + if (a.reply.status == .err) return switch (a.reply.errno) { + E.NOENT => error.NoEntry, + E.NOTDIR => error.NotDirectory, + else => error.Refused, + }; + attr = a.reply.attr; + at = attr.node; + } + return attr; +} + +fn readAll(t: *Board, node: u64) !Board.Answer { + const open = t.handle(.{ .tag = 1, .op = .open, .node = node }); + try testing.expectEqual(Board.Status.ok, open.reply.status); + return t.handle(.{ .tag = 2, .op = .read, .node = node, .handle = open.reply.handle, .size = 65535 }); +} + +test "GPIO: the generated tree has exactly the header's pins, and nothing else" { + var t: Board = .{}; + + try testing.expect((try walk(&t, &.{"gpio"})).dir); + try testing.expect(!(try walk(&t, &.{ "gpio", "pinout" })).dir); + + for (Header.gpio_pins, 0..) |pin, i| { + var name: [4]u8 = undefined; + const dir = try std.fmt.bufPrint(&name, "{d}", .{pin}); + const directory = try walk(&t, &.{ "gpio", dir }); + try testing.expect(directory.dir); + try testing.expectEqual(@as(u64, 18 + i), directory.node); + const value = try walk(&t, &.{ "gpio", dir, "value" }); + try testing.expect(!value.dir); + try testing.expectEqual(@as(u64, (2 + i) * 16 + 1), value.node); + try testing.expectEqual(@as(u16, 0o600), value.mode); + } + + try testing.expectError(error.NoEntry, walk(&t, &.{ "gpio", "6" })); + try testing.expectError(error.NoEntry, walk(&t, &.{ "gpio", "21" })); + try testing.expectError(error.NoEntry, walk(&t, &.{ "gpio", "20", "level" })); + try testing.expectError(error.NoEntry, walk(&t, &.{"mem"})); +} + +test "GPIO: a read of gpio/pinout is the bytes the Gpio word draws" { + var t: Board = .{}; + const at = try walk(&t, &.{ "gpio", "pinout" }); + const a = try readAll(&t, at.node); + try testing.expectEqualStrings(Header.text, a.bytes); + try testing.expectEqual(Header.text.len, at.size); + try testing.expectEqual(@as(u16, 0o400), at.mode); + const w = t.handle(.{ .tag = 3, .op = .write, .node = at.node, .data = "x" }); + try testing.expectEqual(E.INVAL, w.reply.errno); +} + +test "GPIO: writing 1 then 0 drives the pad twice, through the seam" { + StubPads.reset(); + var t: Board = .{}; + const at = try walk(&t, &.{ "gpio", "20", "value" }); + + const before = try readAll(&t, at.node); + try testing.expectEqualStrings("0\n", before.bytes); + + const one = t.handle(.{ .tag = 4, .op = .write, .node = at.node, .data = "1" }); + try testing.expectEqual(Board.Status.ok, one.reply.status); + try testing.expectEqual(@as(u32, 1), one.reply.written); + try testing.expectEqualStrings("1\n", (try readAll(&t, at.node)).bytes); + + const zero = t.handle(.{ .tag = 5, .op = .write, .node = at.node, .data = "0\n" }); + try testing.expectEqual(@as(u32, 2), zero.reply.written); + try testing.expectEqualStrings("0\n", (try readAll(&t, at.node)).bytes); + + try testing.expectEqual(@as(usize, 2), StubPads.log_len); + try testing.expectEqual(StubPads.Call{ .pin = 20, .level = 1 }, StubPads.log[0]); + try testing.expectEqual(StubPads.Call{ .pin = 20, .level = 0 }, StubPads.log[1]); +} + +test "GPIO: a pad takes 0 and 1 and refuses everything else, without touching the pads" { + StubPads.reset(); + var t: Board = .{}; + const at = try walk(&t, &.{ "gpio", "45", "value" }); + + for ([_][]const u8{ "2", "", "01", "x", "true", "high", "\n", "1 ", " 1", "10" }) |bad| { + const a = t.handle(.{ .tag = 6, .op = .write, .node = at.node, .data = bad }); + try testing.expectEqual(Board.Status.err, a.reply.status); + try testing.expectEqual(E.INVAL, a.reply.errno); + } + try testing.expectEqual(@as(usize, 0), StubPads.log_len); + + const off = t.handle(.{ .tag = 7, .op = .write, .node = at.node, .off = 1, .data = "1" }); + try testing.expectEqual(E.INVAL, off.reply.errno); + try testing.expectEqual(@as(usize, 0), StubPads.log_len); +} + +test "GPIO: node parents are explicit, not derived from node bits" { + for (&entries) |*e| { + const serial = e.node >> 4; + const file = e.node & 0xF; + const derived: u64 = if (e.node == root or serial == 0 or file == 0) root else serial << 4; + if (derived == e.parent) continue; + try testing.expectEqualStrings("value", e.name); + var t: Board = .{}; + const a = t.handle(.{ .tag = 8, .op = .getattr, .node = derived }); + try testing.expectEqual(E.NOENT, a.reply.errno); + } +} + +test "GPIO: parent lookup follows every generated directory" { + var t: Board = .{}; + for (&entries) |*entry| { + if (entry.kind != .directory) continue; + const answer = t.handle(.{ .tag = 1, .op = .lookup, .node = entry.node, .data = ".." }); + try testing.expectEqual(Board.Status.ok, answer.reply.status); + try testing.expectEqual(entry.parent, answer.reply.attr.node); + try testing.expect(answer.reply.attr.dir); + } +} + +test "GPIO: a directory read lists what the table generated, in table order" { + var t: Board = .{}; + + try testing.expectEqualStrings("gpio", (try names(&t, root, 0))[0]); + try testing.expectEqual(@as(usize, 1), (try names(&t, root, 0)).len); + + const gpio = (try walk(&t, &.{"gpio"})).node; + const listing = try names(&t, gpio, 0); + try testing.expectEqual(Header.gpio_pins.len + 1, listing.len); + try testing.expectEqualStrings("pinout", listing[0]); + for (Header.gpio_pins, 0..) |pin, i| { + var buf: [4]u8 = undefined; + try testing.expectEqualStrings(try std.fmt.bufPrint(&buf, "{d}", .{pin}), listing[i + 1]); + } + + const rest = try names(&t, gpio, 5); + try testing.expectEqual(Header.gpio_pins.len + 1 - 5, rest.len); + try testing.expectEqualStrings("5", rest[0]); +} + +var name_slots: [32][]const u8 = undefined; +fn names(t: *Board, node: u64, skip: u64) ![][]const u8 { + const a = t.handle(.{ .tag = 9, .op = .readdir, .node = node, .off = skip, .size = 65535 }); + try testing.expectEqual(Board.Status.ok, a.reply.status); + var n: usize = 0; + var i: usize = 0; + while (i < a.bytes.len) { + const len = a.bytes[i + 9]; + name_slots[n] = a.bytes[i + 10 ..][0..len]; + n += 1; + i += 10 + len; + } + return name_slots[0..n]; +} + +test "GPIO: the whole tree costs one buffer, and the table says how big" { + try testing.expectEqual(@as(usize, 143), out_capacity); + try testing.expectEqual(@as(usize, 143), @sizeOf(Board)); + try testing.expectEqual(@as(usize, 28), Board.name_capacity); + try testing.expect(Header.text.len > out_capacity); +} + +test "GPIO: stat and truncation preserve the register value and reads respect offsets" { + StubPads.reset(); + var fs: Board = .{}; + const value = try walk(&fs, &.{ "gpio", "20", "value" }); + for (0..2) |_| { + const written = fs.handle(.{ .tag = 1, .op = .write, .node = value.node, .data = "1\r\n" }); + try testing.expectEqual(@as(u32, 3), written.reply.written); + } + try testing.expectEqual(@as(usize, 2), StubPads.log_len); + const stat = fs.handle(.{ .tag = 2, .op = .getattr, .node = value.node }); + try testing.expectEqual(@as(u64, 2), stat.reply.attr.size); + const truncated = fs.handle(.{ .tag = 3, .op = .setattr, .node = value.node, .truncate = true }); + try testing.expectEqual(Board.Status.ok, truncated.reply.status); + try testing.expectEqual(@as(u1, 1), StubPads.driven[20]); + const part = fs.handle(.{ .tag = 4, .op = .read, .node = value.node, .off = 1, .size = 1 }); + try testing.expectEqualStrings("\n", part.bytes); + const eof = fs.handle(.{ .tag = 5, .op = .read, .node = value.node, .off = 2, .size = 1 }); + try testing.expectEqual(@as(usize, 0), eof.bytes.len); + const pinout = try walk(&fs, &.{ "gpio", "pinout" }); + const text = fs.handle(.{ .tag = 6, .op = .read, .node = pinout.node, .off = 10, .size = 12 }); + try testing.expectEqualStrings(Header.text[10..22], text.bytes); +} + +const ninep = @import("9p.zig"); + +test "GPIO: a real 9P client reads a pin's value off this tree" { + StubPads.reset(); + const Server = ninep.Server(Board, ninep.board_fids); + var in: [1024]u8 = undefined; + var out: [2048]u8 = undefined; + var fsys: Board = .{}; + var srv = Server.init(.{ .in = &in, .out = &out, .root = root }); + + var scratch: [256]u8 = undefined; + const send = struct { + fn call(s: *Server, f: *Board, buf: []u8, tag: u16, msg: ninep.Msg) !void { + const bytes = try ninep.encode(msg, tag, buf); + try testing.expectEqual(bytes.len, s.push(bytes)); + while (s.retry()) |req| { + const a = f.handle(req); + s.reply(&a.reply, a.bytes); + } + while (s.next()) |req| { + const a = f.handle(req); + s.reply(&a.reply, a.bytes); + } + } + }.call; + const reap = struct { + fn call(s: *Server) !ninep.Decoded { + const queued = s.output(); + const len = ninep.frameLen(queued) orelse return error.NoReply; + const got = try ninep.decode(queued[0..len]); + s.wrote(len); + return got; + } + }.call; + + try send(&srv, &fsys, &scratch, ninep.notag, .{ .tversion = .{ .msize = 8192, .version = "9P2000" } }); + const v = try reap(&srv); + try testing.expectEqual(@as(u32, 1024), v.msg.rversion.msize); + + try send(&srv, &fsys, &scratch, 1, .{ .tattach = .{ .fid = 0, .afid = ninep.nofid, .uname = "goblin", .aname = "" } }); + try testing.expectEqual(root, (try reap(&srv)).msg.rattach.qid.path); + + var wname: [ninep.max_welem][]const u8 = @splat(""); + wname[0] = "gpio"; + wname[1] = "20"; + wname[2] = "value"; + try send(&srv, &fsys, &scratch, 2, .{ .twalk = .{ .fid = 0, .newfid = 1, .nwname = 3, .wname = wname } }); + try testing.expectEqual(@as(u16, 3), (try reap(&srv)).msg.rwalk.nwqid); + + try send(&srv, &fsys, &scratch, 3, .{ .topen = .{ .fid = 1, .mode = ninep.ordwr } }); + _ = try reap(&srv); + + try send(&srv, &fsys, &scratch, 4, .{ .twrite = .{ .fid = 1, .offset = 0, .data = "1\n" } }); + try testing.expectEqual(@as(u32, 2), (try reap(&srv)).msg.rwrite.count); + try testing.expectEqual(@as(u1, 1), StubPads.driven[20]); + + try send(&srv, &fsys, &scratch, 5, .{ .tread = .{ .fid = 1, .offset = 0, .count = 512 } }); + try testing.expectEqualStrings("1\n", (try reap(&srv)).msg.rread.data); + + try send(&srv, &fsys, &scratch, 6, .{ .twrite = .{ .fid = 1, .offset = 0, .data = "on" } }); + try testing.expectEqualStrings(ninep.errString(E.INVAL), (try reap(&srv)).msg.rerror.ename); + try testing.expectEqual(@as(usize, 1), StubPads.log_len); +} |
