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(); // No peer answers at the dial: Mount mounts nothing (it dials first). try std.testing.expect(p.executeBuiltinLine(0, "Mount peer /tmp/a socket.sock")); try std.testing.expectEqual(@as(usize, 0), p.fs.mounts.items.len); // Mounted (as one that answered is), its arguments are copies. var name = "peer".*; var dial = "/tmp/a socket.sock".*; try p.fs.mount(gpa, &name, &dial); @memset(&name, 'x'); @memset(&dial, '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/index", filesystem.resolve(p, "/n/peer/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/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 pardes.dump.dumpState(p); const restored = try pardes.dump.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.dump.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.dump.initFromDump(gpa, .{ .mounts = &.{.{ .name = "peer", .dial = "/tmp/different.sock" }}, }, p.dump_out.?)); const added = try pardes.dump.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 pardes.dump.dumpState(p); const restored = try pardes.dump.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 pardes.dump.dumpState(p); const restored = try pardes.dump.restore(p, p.dump_out.?, "test"); 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.expect(p.panes[0] == pane); try std.testing.expectEqualStrings("before\n", pane.file.?.content); try std.testing.expectError(error.BadDumpMagic, pardes.dump.restore( p, ".{ .magic = \"wrong\", .screen = .{.cols = 80, .rows = 24} }", "test", )); 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 "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); if (!pardes.memory.limits.embedded_sources) return; 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" { if (!pardes.memory.limits.embedded_sources) return error.SkipZigTest; 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 a virtual file opens one pane and failed opens release their slot" { const p = try pardes.Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); pardes.look.lookAt(p, 0, "/virtual/index"); var opened: 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/index")) opened += 1; } try std.testing.expectEqual(@as(usize, 1), opened); 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); }