diff options
Diffstat (limited to 'test/fs_namespace.zig')
| -rw-r--r-- | test/fs_namespace.zig | 333 |
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); +} |
