summaryrefslogtreecommitdiff
path: root/test/fs_namespace.zig
diff options
context:
space:
mode:
Diffstat (limited to 'test/fs_namespace.zig')
-rw-r--r--test/fs_namespace.zig333
1 files changed, 333 insertions, 0 deletions
diff --git a/test/fs_namespace.zig b/test/fs_namespace.zig
new file mode 100644
index 00000000..c856148d
--- /dev/null
+++ b/test/fs_namespace.zig
@@ -0,0 +1,333 @@
+const std = @import("std");
+const pardes = @import("pardes");
+const filesystem = pardes.filesystem;
+
+test "runtime Mount and Unmount copy command arguments and change Look resolution" {
+ const gpa = std.testing.allocator;
+ const p = try pardes.Pardes.init(gpa, .{ .tty_only = true });
+ defer p.deinit();
+ var command = "Mount peer /tmp/a socket.sock".*;
+ try std.testing.expect(p.executeBuiltinLine(0, &command));
+ @memset(&command, 'x');
+ try std.testing.expectEqual(@as(usize, 1), p.fs.mounts.items.len);
+ try std.testing.expectEqualStrings("peer", p.fs.mounts.items[0].name);
+ try std.testing.expectEqualStrings("/tmp/a socket.sock", p.fs.mounts.items[0].dial);
+ var path: [4096]u8 = undefined;
+ try std.testing.expectEqualStrings("/n/peer/self/index", filesystem.resolve(p, "/n/peer/self/index", "/", &path).?.path);
+ try std.testing.expect(p.executeBuiltinLine(0, "Mount peer /tmp/other.sock"));
+ try std.testing.expectEqual(@as(usize, 1), p.fs.mounts.items.len);
+ try std.testing.expectEqualStrings("/tmp/a socket.sock", p.fs.mounts.items[0].dial);
+ try std.testing.expect(p.executeBuiltinLine(0, "Unmount peer"));
+ try std.testing.expectEqual(@as(usize, 0), p.fs.mounts.items.len);
+ try std.testing.expect(filesystem.resolve(p, "/n/peer/self/index", "/", &path) == null);
+ for ([_][]const u8{ "Mount", "Mount name", "Mount os /tmp/peer.sock", "Mount peer tcp!127.0.0.1!0", "Unmount", "Unmount name extra" }) |invalid| {
+ try std.testing.expect(p.executeBuiltinLine(0, invalid));
+ try std.testing.expectEqual(@as(usize, 0), p.fs.mounts.items.len);
+ }
+}
+
+test "mounts survive dumps without borrowing inputs or silently overriding a saved target" {
+ const gpa = std.testing.allocator;
+ var name = "peer".*;
+ var dial = "/tmp/peer.sock".*;
+ const startup = [_]filesystem.Mount{.{ .name = &name, .dial = &dial }};
+ const p = try pardes.Pardes.init(gpa, .{ .tty_only = true, .mounts = &startup });
+ defer p.deinit();
+ @memset(&name, 'x');
+ @memset(&dial, 'x');
+ try std.testing.expectEqual(@as(usize, 0), p.opts.mounts.len);
+ const file = try p.setTestFile("saved remote body\n");
+ file.file.?.revision = 3;
+ gpa.free(file.file.?.path);
+ file.file.?.path = try gpa.dupe(u8, "/n/peer/os/file.txt");
+ try p.dumpState();
+ const restored = try pardes.Pardes.initFromDump(gpa, .{}, p.dump_out.?);
+ defer restored.deinit();
+ try std.testing.expectEqual(@as(usize, 1), restored.fs.mounts.items.len);
+ try std.testing.expectEqualStrings("peer", restored.fs.mounts.items[0].name);
+ try std.testing.expectEqualStrings("/tmp/peer.sock", restored.fs.mounts.items[0].dial);
+ try std.testing.expectEqualStrings("/n/peer/os/file.txt", restored.panes[0].?.file.?.path);
+ try std.testing.expectEqualStrings("saved remote body\n", restored.panes[0].?.file.?.content);
+ try std.testing.expect(restored.panes[0].?.file.?.revision != restored.panes[0].?.file.?.saved_revision);
+ try std.testing.expectError(error.MountInUse, filesystem.unmount(restored, "peer"));
+ const same = try pardes.Pardes.initFromDump(gpa, .{ .mounts = &.{.{ .name = "peer", .dial = "/tmp/peer.sock" }} }, p.dump_out.?);
+ defer same.deinit();
+ try std.testing.expectEqual(@as(usize, 1), same.fs.mounts.items.len);
+ try std.testing.expectError(error.MountConflict, pardes.Pardes.initFromDump(gpa, .{
+ .mounts = &.{.{ .name = "peer", .dial = "/tmp/different.sock" }},
+ }, p.dump_out.?));
+ const added = try pardes.Pardes.initFromDump(gpa, .{ .mounts = &.{.{ .name = "other", .dial = "/tmp/other.sock" }} }, p.dump_out.?);
+ defer added.deinit();
+ try std.testing.expectEqual(@as(usize, 2), added.fs.mounts.items.len);
+}
+
+test "removed startup mounts are not resurrected by dump restore" {
+ const gpa = std.testing.allocator;
+ const p = try pardes.Pardes.init(gpa, .{
+ .tty_only = true,
+ .mounts = &.{.{ .name = "peer", .dial = "/tmp/peer.sock" }},
+ });
+ defer p.deinit();
+ try filesystem.unmount(p, "peer");
+ try p.dumpState();
+ const restored = try pardes.Pardes.initFromDump(gpa, p.opts, p.dump_out.?);
+ defer restored.deinit();
+ try std.testing.expectEqual(@as(usize, 0), restored.fs.mounts.items.len);
+}
+
+test "transactional restore preserves host capabilities and advances pane identities" {
+ const gpa = std.testing.allocator;
+ const p = try pardes.Pardes.init(gpa, .{ .tty_only = true, .cols = 91, .rows = 37 });
+ defer p.deinit();
+ const pane = try p.setTestFile("before\n");
+ p.lsp_seq = 41;
+ p.pipe_seq = 73;
+ if (@hasField(pardes.CellPixels, "w")) p.cell_pixels = .{ .w = 9, .h = 18 };
+ p.native_images = true;
+ p.fs.socket_path = "/tmp/session.sock";
+ p.fs.tcp_address = .{ .ip4 = .loopback(5640) };
+ p.fs.quic_address = .{ .ip6 = .loopback(5641) };
+ try p.dumpState();
+ const restored = try p.restore(p.dump_out.?);
+ defer restored.deinit();
+ try std.testing.expectEqual(@as(u32, 41), restored.lsp_seq);
+ try std.testing.expectEqual(@as(u32, 73), restored.pipe_seq);
+ try std.testing.expectEqual(p.cell_pixels, restored.cell_pixels);
+ try std.testing.expect(restored.native_images);
+ try std.testing.expect(restored.panes[0].?.serial > p.next_serial);
+ try std.testing.expect(restored.paneBySerial(pane.serial) == null);
+ try std.testing.expectEqual(p.screen_w, restored.screen_w);
+ try std.testing.expectEqual(p.screen_h, restored.screen_h);
+ try std.testing.expectEqualStrings(p.fs.socket_path, restored.fs.socket_path);
+ try std.testing.expectEqualDeep(p.fs.tcp_address, restored.fs.tcp_address);
+ try std.testing.expectEqualDeep(p.fs.quic_address, restored.fs.quic_address);
+ try std.testing.expect(p.panes[0] == pane);
+ try std.testing.expectEqualStrings("before\n", pane.file.?.content);
+ try std.testing.expectError(error.BadDumpMagic, p.restore(
+ ".{ .magic = \"wrong\", .screen = .{.cols = 80, .rows = 24} }",
+ ));
+ try std.testing.expect(p.panes[0] == pane);
+ try std.testing.expectEqualStrings("before\n", pane.file.?.content);
+ try std.testing.expectEqual(@as(u32, 41), p.lsp_seq);
+ try std.testing.expectEqualStrings("/tmp/session.sock", p.fs.socket_path);
+}
+
+test "same-session identity is available before an initial mounted file is opened" {
+ if (!filesystem.platform_has_fs) return error.SkipZigTest;
+ const gpa = std.testing.allocator;
+ var tmp = std.testing.tmpDir(.{});
+ defer tmp.cleanup();
+ try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "file.txt", .data = "no socket required\n" });
+ var buf: [4096]u8 = undefined;
+ const len = try tmp.dir.realPathFile(std.testing.io, "file.txt", &buf);
+ const path = try std.fmt.allocPrint(gpa, "/n/own/os{s}", .{buf[0..len]});
+ defer gpa.free(path);
+ const p = try pardes.Pardes.init(gpa, .{
+ .file = path,
+ .mounts = &.{.{ .name = "own", .dial = "/tmp/not-listening.sock" }},
+ .ninep_identity = .{ .socket_path = "/tmp/not-listening.sock" },
+ });
+ defer p.deinit();
+ try std.testing.expectEqualStrings(path, p.panes[0].?.file.?.path);
+ try std.testing.expectEqualStrings("no socket required\n", p.panes[0].?.file.?.content);
+ try std.testing.expectEqualStrings("", p.opts.ninep_identity.socket_path);
+}
+
+test "filesystem resolves OS first then virtual with explicit mounts and parent paths" {
+ if (!filesystem.platform_has_fs) return error.SkipZigTest;
+ const gpa = std.testing.allocator;
+ var tmp = std.testing.tmpDir(.{});
+ defer tmp.cleanup();
+ try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "index", .data = "OS INDEX\n" });
+ var directory_buffer: [4096]u8 = undefined;
+ const directory_len = try tmp.dir.realPath(std.testing.io, &directory_buffer);
+ const directory = directory_buffer[0..directory_len];
+ const os_index = try std.fmt.allocPrint(gpa, "{s}/index", .{directory});
+ defer gpa.free(os_index);
+ const explicit_os = try std.fmt.allocPrint(gpa, "/n/os{s}", .{os_index});
+ defer gpa.free(explicit_os);
+ const p = try pardes.Pardes.init(gpa, .{ .tty_only = true });
+ defer p.deinit();
+ const pane = try p.setTestFile("SELF BODY\n");
+ const explicit_self = try std.fmt.allocPrint(gpa, "/n/self/pane/{d}/body", .{pane.serial});
+ defer gpa.free(explicit_self);
+ const virtual_self = try std.fmt.allocPrint(gpa, "/virtual/pane/{d}/body", .{pane.serial});
+ defer gpa.free(virtual_self);
+ const parent_self = try std.fmt.allocPrint(gpa, "/n/self/pane/{d}/../{d}/body", .{ pane.serial, pane.serial });
+ defer gpa.free(parent_self);
+
+ for ([_]struct { word: []const u8, path: []const u8, bytes: []const u8 }{
+ .{ .word = "index", .path = os_index, .bytes = "OS INDEX\n" },
+ .{ .word = os_index, .path = os_index, .bytes = "OS INDEX\n" },
+ .{ .word = explicit_os, .path = explicit_os, .bytes = "OS INDEX\n" },
+ .{ .word = explicit_self, .path = virtual_self, .bytes = "SELF BODY\n" },
+ .{ .word = virtual_self, .path = virtual_self, .bytes = "SELF BODY\n" },
+ .{ .word = parent_self, .path = virtual_self, .bytes = "SELF BODY\n" },
+ }) |case| {
+ var resolved_buffer: [4096]u8 = undefined;
+ const found = filesystem.resolve(p, case.word, directory, &resolved_buffer) orelse return error.UnresolvedPath;
+ try std.testing.expectEqualStrings(case.path, found.path);
+ const bytes = try filesystem.read(p, found.path);
+ defer gpa.free(bytes);
+ try std.testing.expectEqualStrings(case.bytes, bytes);
+ }
+
+ for ([_][]const u8{ "/n/self/index", "/virtual/index" }) |word| {
+ var resolved_buffer: [4096]u8 = undefined;
+ const found = filesystem.resolve(p, word, directory, &resolved_buffer) orelse return error.UnresolvedPath;
+ try std.testing.expectEqualStrings("/virtual/index", found.path);
+ const bytes = try filesystem.read(p, found.path);
+ defer gpa.free(bytes);
+ try std.testing.expect(std.mem.indexOf(u8, bytes, "/test.txt") != null);
+ }
+ try tmp.dir.deleteFile(std.testing.io, "index");
+ var resolved_buffer: [4096]u8 = undefined;
+ const fallback = filesystem.resolve(p, "index", directory, &resolved_buffer) orelse return error.MissingVirtualFallback;
+ try std.testing.expectEqualStrings("/virtual/index", fallback.path);
+
+ for ([_][]const u8{ "src/pardes.zig", "/virtual/src/pardes.zig", "/virtual/src/../src/pardes.zig" }) |word| {
+ const found = filesystem.resolve(p, word, directory, &resolved_buffer) orelse return error.MissingEmbeddedSource;
+ const bytes = try filesystem.read(p, found.path);
+ defer gpa.free(bytes);
+ try std.testing.expect(std.mem.indexOf(u8, bytes, "pub const Pardes") != null);
+ }
+ for ([_][]const u8{ "/virtual/src", "/n/self/src" }) |word| {
+ const found = filesystem.resolve(p, word, directory, &resolved_buffer) orelse return error.MissingEmbeddedDirectory;
+ const bytes = try filesystem.read(p, found.path);
+ defer gpa.free(bytes);
+ try std.testing.expect(std.mem.indexOf(u8, bytes, "/virtual/src/pardes.zig\n") != null);
+ }
+}
+
+test "explicit OS file opens and restores keep their names and request watches" {
+ if (!filesystem.platform_has_fs) return error.SkipZigTest;
+ const gpa = std.testing.allocator;
+ var tmp = std.testing.tmpDir(.{});
+ defer tmp.cleanup();
+ try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "watched.zig", .data = "const disk = 1;\n" });
+ var directory_buffer: [4096]u8 = undefined;
+ const directory_len = try tmp.dir.realPath(std.testing.io, &directory_buffer);
+ const path = try std.fmt.allocPrint(gpa, "/n/os{s}/watched.zig", .{directory_buffer[0..directory_len]});
+ defer gpa.free(path);
+ const p = try pardes.Pardes.init(gpa, .{ .tty_only = true });
+ defer p.deinit();
+ while (p.nextEffect()) |_| {}
+ const opened = p.freeSlot().?;
+ const pane = try pardes.panes.File.open(p, opened, path, 0);
+ try std.testing.expectEqualStrings(path, pane.file.?.path);
+ try std.testing.expectEqualStrings("const disk = 1;\n", pane.file.?.content);
+ const restored = p.freeSlot().?;
+ const copy = try pardes.panes.File.restore(p, restored, .{
+ .kind = .file,
+ .tag = path,
+ .body = "",
+ .file = .{ .path = path, .content = "const unsaved = 2;\n" },
+ });
+ try std.testing.expectEqualStrings(path, copy.file.?.path);
+ try std.testing.expectEqualStrings("const unsaved = 2;\n", copy.file.?.content);
+ var watched_open = false;
+ var watched_restore = false;
+ while (p.nextEffect()) |effect| if (effect == .watch and effect.watch.on) {
+ if (effect.watch.pane == opened) watched_open = true;
+ if (effect.watch.pane == restored) watched_restore = true;
+ };
+ try std.testing.expect(watched_open and watched_restore);
+}
+
+test "virtual file opens do not request OS watches" {
+ const p = try pardes.Pardes.init(std.testing.allocator, .{ .tty_only = true });
+ defer p.deinit();
+ while (p.nextEffect()) |_| {}
+ const free = p.freeSlot().?;
+ _ = try pardes.panes.File.open(p, free, "/virtual/src/pardes.zig", 0);
+ while (p.nextEffect()) |effect| if (effect == .watch) return error.VirtualFileWatch;
+}
+
+test "Look at the self factory reserves its own pane and failed opens release it" {
+ const p = try pardes.Pardes.init(std.testing.allocator, .{ .tty_only = true });
+ defer p.deinit();
+ p.lookAt(0, "/virtual/new/ctl");
+ var control_count: usize = 0;
+ var scratch_count: usize = 0;
+ for (p.panes) |slot| {
+ const pane = slot orelse continue;
+ const file = pane.file orelse continue;
+ if (std.mem.eql(u8, file.path, "/virtual/new/ctl")) control_count += 1;
+ if (file.output) |output| if (output.from == .cmd and output.from.cmd == .New) {
+ scratch_count += 1;
+ };
+ }
+ try std.testing.expectEqual(@as(usize, 1), control_count);
+ try std.testing.expectEqual(@as(usize, 1), scratch_count);
+ const free = p.freeSlot().?;
+ try std.testing.expectError(error.FileNotFound, pardes.panes.File.open(p, free, "/n/self/does-not-exist", 0));
+ try std.testing.expect(p.panes[free] == null);
+ try std.testing.expect(!p.reserved_slots[free]);
+ _ = try p.newDocPane(free);
+}
+
+test "owned cwd failed file opens never publish a pane or retain a reservation" {
+ var succeeded = false;
+ for (0..16) |failure| {
+ var allocator = std.testing.FailingAllocator.init(std.testing.allocator, .{});
+ const p = try pardes.Pardes.init(allocator.allocator(), .{ .tty_only = true });
+ defer p.deinit();
+ const source = try p.setTestFile("source body\n");
+ p.setCwd(0, "/source/directory");
+ while (p.nextEffect()) |_| {}
+ var path: [128]u8 = undefined;
+ const name = try std.fmt.bufPrint(&path, "/virtual/pane/{d}/body", .{source.serial});
+ const slot = p.freeSlot().?;
+ const serial = p.next_serial;
+ const columns = p.col_panes;
+ const counts = p.col_n;
+ allocator.fail_index = allocator.alloc_index + failure;
+ const opened = pardes.panes.File.open(p, slot, name, 0) catch |err| {
+ try std.testing.expect(err == error.OutOfMemory or err == error.ReadFailed or err == error.WriteFailed);
+ try std.testing.expect(p.panes[slot] == null);
+ try std.testing.expect(!p.reserved_slots[slot]);
+ try std.testing.expectEqual(serial, p.next_serial);
+ try std.testing.expectEqual(slot, p.freeSlot().?);
+ try std.testing.expectEqualDeep(columns, p.col_panes);
+ try std.testing.expectEqualDeep(counts, p.col_n);
+ try std.testing.expectEqual(@as(usize, 0), p.effects_len);
+ try std.testing.expectEqualStrings("/source/directory", source.cwdSlice());
+ allocator.fail_index = std.math.maxInt(usize);
+ _ = try p.newDocPane(slot);
+ continue;
+ };
+ try std.testing.expect(!p.reserved_slots[slot]);
+ try std.testing.expectEqualStrings("source body\n", opened.file.?.content);
+ succeeded = true;
+ break;
+ }
+ try std.testing.expect(succeeded);
+}
+
+test "images decode and dump through explicit filesystem mounts" {
+ if (!filesystem.platform_has_fs) return error.SkipZigTest;
+ const gpa = std.testing.allocator;
+ pardes.image.start(std.testing.io, gpa);
+ defer pardes.image.stop();
+ var tmp = std.testing.tmpDir(.{});
+ defer tmp.cleanup();
+ const bytes = "P6\n1 1\n255\n\x0d\x4d\xe7";
+ try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "pixel.ppm", .data = bytes });
+ var directory_buffer: [4096]u8 = undefined;
+ const directory_len = try tmp.dir.realPath(std.testing.io, &directory_buffer);
+ const path = try std.fmt.allocPrint(gpa, "/n/os{s}/pixel.ppm", .{directory_buffer[0..directory_len]});
+ defer gpa.free(path);
+ const p = try pardes.Pardes.init(gpa, .{ .tty_only = true, .file = path });
+ defer p.deinit();
+ var arena = std.heap.ArenaAllocator.init(gpa);
+ defer arena.deinit();
+ _ = try p.render(arena.allocator());
+ const pane = p.panes[0].?;
+ const state = pane.image.?;
+ try std.testing.expectEqual(@as(usize, 1), state.iw);
+ try std.testing.expectEqual(@as(usize, 1), state.ih);
+ const saved = try pardes.panes.Image.dumpPane(p, arena.allocator(), pane, "", "", 0, path, &.{});
+ const decoded = try pardes.dump.decodeBytes(gpa, saved.image.?.bytes_b64);
+ defer gpa.free(decoded);
+ try std.testing.expectEqualStrings(bytes, decoded);
+}