summaryrefslogtreecommitdiff
path: root/src/session_test.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-14 14:10:28 -0300
committerGabriel Schneider <[email protected]>2026-09-14 14:20:25 -0300
commit5f24c4a2284a0af84fb9d116f7a39f6a58e42ae9 (patch)
tree8679763439492361fe99ea01f5190e5204fa590c /src/session_test.zig
downloadcloud9-5f24c4a2284a0af84fb9d116f7a39f6a58e42ae9.tar.gz
cloud9-5f24c4a2284a0af84fb9d116f7a39f6a58e42ae9.zip
Implement base 9P2000 sessions, shared transports, and conformance probes
Diffstat (limited to 'src/session_test.zig')
-rw-r--r--src/session_test.zig222
1 files changed, 222 insertions, 0 deletions
diff --git a/src/session_test.zig b/src/session_test.zig
new file mode 100644
index 0000000..8600d03
--- /dev/null
+++ b/src/session_test.zig
@@ -0,0 +1,222 @@
+const std = @import("std");
+const testing = std.testing;
+const c9 = @import("root.zig");
+const Pair = struct {
+ client_in: [4096]u8 = undefined,
+ client_out: [4096]u8 = undefined,
+ server_in: [4096]u8 = undefined,
+ server_out: [8192]u8 = undefined,
+ client: c9.Client = undefined,
+ server: c9.Server = undefined,
+
+ fn init(p: *Pair) !void {
+ p.client = .init(.{ .in = &p.client_in, .out = &p.client_out });
+ p.server = .init(.{ .in = &p.server_in, .out = &p.server_out });
+ _ = try p.client.submit(.{ .version = .{} });
+ const request = try p.nextRequest();
+ try p.server.negotiate(request.msg.tversion.msize, request.msg.tversion.version);
+ p.server.release();
+ try testing.expectEqual(c9.Client.Op.version, (try p.nextResult()).op);
+ }
+
+ fn nextRequest(p: *Pair) !c9.Decoded {
+ // Exercise every frame boundary through single-byte delivery.
+ while (p.client.output().len != 0) {
+ try testing.expectEqual(@as(usize, 1), p.server.push(p.client.output()[0..1]));
+ p.client.wrote(1);
+ if (try p.server.receive()) |request| return request;
+ }
+ return (try p.server.receive()) orelse error.NoRequest;
+ }
+
+ fn nextResult(p: *Pair) !c9.Client.Done {
+ while (p.server.output().len != 0) {
+ try testing.expectEqual(@as(usize, 1), p.client.push(p.server.output()[0..1]));
+ p.server.wrote(1);
+ if (p.client.take()) |result| return result;
+ }
+ return p.client.take() orelse error.NoResult;
+ }
+
+ fn exchange(p: *Pair, req: c9.Client.Request, response: c9.Msg) !void {
+ const tag = try p.client.submit(req);
+ const request = try p.nextRequest();
+ try testing.expectEqual(tag, request.tag);
+ try p.server.reply(tag, response);
+ p.server.release();
+ const result = try p.nextResult();
+ try testing.expectEqual(std.meta.activeTag(req), result.op);
+ try testing.expectEqualStrings(@tagName(req), @tagName(result.result));
+ }
+};
+
+const qid: c9.Qid = .{ .type = 0, .version = 1, .path = 2 };
+const stat: c9.Stat = .{
+ .type = 0,
+ .dev = 0,
+ .qid = qid,
+ .mode = 0o600,
+ .atime = 0,
+ .mtime = 0,
+ .length = 0,
+ .name = "file",
+ .uid = "user",
+ .gid = "group",
+ .muid = "user",
+};
+
+test "all base 9P2000 client operations through fragmented server transport" {
+ var p: Pair = .{};
+ try p.init();
+ try p.exchange(.{ .auth = .{ .afid = 1, .uname = "user" } }, .{ .rauth = .{ .aqid = qid } });
+ try p.exchange(.{ .attach = .{ .fid = 2, .afid = 1, .uname = "user" } }, .{ .rattach = .{ .qid = qid } });
+ try p.exchange(.{ .walk = .{ .fid = 2, .newfid = 3, .names = &.{"file"} } }, .{ .rwalk = .{ .nwqid = 1, .wqid = @splat(qid) } });
+ try p.exchange(.{ .open = .{ .fid = 3, .mode = 2 } }, .{ .ropen = .{ .qid = qid, .iounit = 0 } });
+ try p.exchange(.{ .create = .{ .fid = 2, .name = "new", .perm = 0o600, .mode = 2 } }, .{ .rcreate = .{ .qid = qid, .iounit = 0 } });
+ try p.exchange(.{ .read = .{ .fid = 3, .offset = 0, .count = 3 } }, .{ .rread = .{ .data = "abc" } });
+ try p.exchange(.{ .write = .{ .fid = 3, .offset = 0, .data = "abc" } }, .{ .rwrite = .{ .count = 2 } });
+ try p.exchange(.{ .stat = .{ .fid = 3 } }, .{ .rstat = .{ .stat = stat } });
+ try p.exchange(.{ .wstat = .{ .fid = 3, .stat = stat } }, .rwstat);
+ try p.exchange(.{ .clunk = .{ .fid = 3 } }, .rclunk);
+ try p.exchange(.{ .remove = .{ .fid = 2 } }, .rremove);
+ try p.exchange(.{ .flush = .{ .oldtag = 42 } }, .rflush);
+}
+
+test "flush holds oldtag until Rflush, including a completed original request" {
+ for ([_]bool{ false, true }) |complete| {
+ var p: Pair = .{};
+ try p.init();
+ const oldtag = try p.client.submit(.{ .read = .{ .fid = 1, .offset = 0, .count = 1 } });
+ _ = try p.nextRequest();
+ p.server.release();
+ const flush = try p.client.submit(.{ .flush = .{ .oldtag = oldtag } });
+ _ = try p.nextRequest();
+ p.server.release();
+ if (complete) {
+ try p.server.reply(oldtag, .{ .rread = .{ .data = "x" } });
+ try testing.expectEqual(oldtag, (try p.nextResult()).tag);
+ try testing.expectEqual(@as(usize, 2), p.client.pending());
+ }
+ const other = try p.client.submit(.{ .stat = .{ .fid = 1 } });
+ try testing.expect(other != oldtag and other != flush);
+ try p.server.reply(flush, .rflush);
+ try testing.expectEqual(flush, (try p.nextResult()).tag);
+ try testing.expectEqual(@as(usize, 1), p.client.pending());
+ try testing.expectError(error.UnknownTag, p.server.reply(oldtag, .{ .rread = .{ .data = "x" } }));
+ const reused = try p.client.submit(.{ .stat = .{ .fid = 1 } });
+ try testing.expectEqual(oldtag, reused);
+ }
+}
+
+test "response bounds and reply types are checked before output is changed" {
+ var p: Pair = .{};
+ try p.init();
+ const tag = try p.client.submit(.{ .write = .{ .fid = 1, .offset = 0, .data = "a" } });
+ _ = try p.nextRequest();
+ p.server.release();
+ try testing.expectError(error.WrongReply, p.server.reply(tag, .{ .rwrite = .{ .count = 2 } }));
+ try testing.expectError(error.WrongReply, p.server.reply(tag, .rclunk));
+ try testing.expectEqual(@as(usize, 0), p.server.output().len);
+ try p.server.reply(tag, .{ .rwrite = .{ .count = 1 } });
+ _ = try p.nextResult();
+}
+
+test "stat outer length overflow is rejected without writing output" {
+ var name: [65535 - 48]u8 = @splat('a');
+ var entry = stat;
+ entry.name = &name;
+ entry.uid = "";
+ entry.gid = "";
+ entry.muid = "";
+ var buffer: [65550]u8 = @splat(0xaa);
+ try testing.expectError(error.Overlong, c9.encode(.{ .rstat = .{ .stat = entry } }, 1, &buffer));
+ try testing.expect(std.mem.allEqual(u8, &buffer, 0xaa));
+}
+
+test "arbitrary input decoder round trip" {
+ try testing.fuzz({}, fuzzDecode, .{});
+}
+
+fn fuzzDecode(_: void, smith: *testing.Smith) !void {
+ var input_buffer: [65536]u8 = undefined;
+ const input = input_buffer[0..smith.slice(&input_buffer)];
+ const decoded = c9.decode(input) catch return;
+ var buffer: [65536]u8 = undefined;
+ const encoded = try c9.encode(decoded.msg, decoded.tag, &buffer);
+ try testing.expectEqualSlices(u8, input, encoded);
+}
+
+test "flush can cancel a full request window and reserves unused oldtags" {
+ var p: Pair = .{};
+ try p.init();
+ _ = try p.client.submit(.{ .flush = .{ .oldtag = 2 } });
+ const next = try p.client.submit(.{ .stat = .{ .fid = 0 } });
+ try testing.expect(next != 2);
+ const third = try p.client.submit(.{ .stat = .{ .fid = 0 } });
+ try testing.expect(third != 2);
+ p.client.hangup();
+ try p.init();
+ for (0..c9.max_tags) |_| _ = try p.client.submit(.{ .stat = .{ .fid = 0 } });
+ try testing.expectError(error.NoTags, p.client.submit(.{ .stat = .{ .fid = 0 } }));
+ const flush = try p.client.submit(.{ .flush = .{ .oldtag = 0 } });
+ try testing.expectEqual(@as(u16, c9.max_tags), flush);
+}
+
+test "multiple flushes retain reservations until each response and can themselves be flushed" {
+ var p: Pair = .{};
+ try p.init();
+ const oldtag = try p.client.submit(.{ .read = .{ .fid = 1, .offset = 0, .count = 1 } });
+ _ = try p.nextRequest();
+ p.server.release();
+ const first = try p.client.submit(.{ .flush = .{ .oldtag = oldtag } });
+ _ = try p.nextRequest();
+ p.server.release();
+ const second = try p.client.submit(.{ .flush = .{ .oldtag = oldtag } });
+ _ = try p.nextRequest();
+ p.server.release();
+ try p.server.reply(first, .rflush);
+ _ = try p.nextResult();
+ const held = try p.client.submit(.{ .stat = .{ .fid = 1 } });
+ try testing.expect(held != oldtag);
+ try p.server.reply(second, .rflush);
+ _ = try p.nextResult();
+ try testing.expectEqual(oldtag, try p.client.submit(.{ .stat = .{ .fid = 1 } }));
+
+ try p.init();
+ const read_tag = try p.client.submit(.{ .read = .{ .fid = 1, .offset = 0, .count = 1 } });
+ _ = try p.nextRequest();
+ p.server.release();
+ const flush_tag = try p.client.submit(.{ .flush = .{ .oldtag = read_tag } });
+ _ = try p.nextRequest();
+ p.server.release();
+ const cancel_flush = try p.client.submit(.{ .flush = .{ .oldtag = flush_tag } });
+ _ = try p.nextRequest();
+ p.server.release();
+ try p.server.reply(read_tag, .{ .rread = .{ .data = "x" } });
+ _ = try p.nextResult();
+ try p.server.reply(cancel_flush, .rflush);
+ _ = try p.nextResult();
+ try testing.expectEqual(@as(usize, 0), p.client.pending());
+}
+
+test "server reserves cancellation capacity and rejects duplicate pending tags" {
+ var input: [4096]u8 = undefined;
+ var output: [4096]u8 = undefined;
+ var server: c9.Server = .init(.{ .in = &input, .out = &output });
+ server.msize = 4096;
+ var frame: [64]u8 = undefined;
+ for (0..64) |i| {
+ const bytes = try c9.encode(.{ .tstat = .{ .fid = 1 } }, @intCast(i), &frame);
+ _ = server.push(bytes);
+ _ = (try server.receive()).?;
+ server.release();
+ }
+ _ = server.push(try c9.encode(.{ .tflush = .{ .oldtag = 0 } }, 100, &frame));
+ _ = (try server.receive()).?;
+ server.release();
+ try server.reply(100, .rflush);
+ server.wrote(server.output().len);
+ _ = server.push(try c9.encode(.{ .tstat = .{ .fid = 1 } }, 1, &frame));
+ try testing.expectError(error.Protocol, server.receive());
+ try testing.expect(server.dead);
+}