地圖 (Jido) is a lightweight Unix TUI file explorer designed for speed and simplicity.

feat: add tests

+552 -2
+44 -1
build.zig
··· 50 50 build_options.addOption(std.SemanticVersion, "version", version); 51 51 const build_options_module = build_options.createModule(); 52 52 53 - // Building targets for release. 54 53 const build_all = b.option(bool, "all-targets", "Build all targets in ReleaseSafe mode.") orelse false; 55 54 if (build_all) { 56 55 try buildTargets(b, build_options_module); ··· 67 66 } 68 67 const run_step = b.step("run", "Run the app"); 69 68 run_step.dependOn(&run_cmd.step); 69 + 70 + const libvaxis = b.dependency("vaxis", .{ .target = target, .optimize = optimize }).module("vaxis"); 71 + const fuzzig = b.dependency("fuzzig", .{ .target = target, .optimize = optimize }).module("fuzzig"); 72 + const zuid = b.dependency("zuid", .{ .target = target, .optimize = optimize }).module("zuid"); 73 + const zeit = b.dependency("zeit", .{ .target = target, .optimize = optimize }).module("zeit"); 74 + const test_step = b.step("test", "Run unit tests"); 75 + const unit_tests = b.addTest(.{ 76 + .root_module = b.createModule(.{ 77 + .root_source_file = b.path("src/main.zig"), 78 + .target = target, 79 + .optimize = optimize, 80 + }), 81 + }); 82 + unit_tests.root_module.addImport("options", build_options_module); 83 + unit_tests.root_module.addImport("vaxis", libvaxis); 84 + unit_tests.root_module.addImport("fuzzig", fuzzig); 85 + unit_tests.root_module.addImport("zeit", zeit); 86 + unit_tests.root_module.addImport("zuid", zuid); 87 + 88 + const run_unit_tests = b.addRunArtifact(unit_tests); 89 + test_step.dependOn(&run_unit_tests.step); 90 + 91 + const integration_tests = &[_][]const u8{ 92 + "src/test_navigation.zig", 93 + "src/test_file_operations.zig", 94 + }; 95 + 96 + for (integration_tests) |test_file| { 97 + const test_exe = b.addTest(.{ 98 + .root_module = b.createModule(.{ 99 + .root_source_file = b.path(test_file), 100 + .target = target, 101 + .optimize = optimize, 102 + }), 103 + }); 104 + test_exe.root_module.addImport("vaxis", libvaxis); 105 + test_exe.root_module.addImport("fuzzig", fuzzig); 106 + test_exe.root_module.addImport("zuid", zuid); 107 + test_exe.root_module.addImport("zeit", zeit); 108 + test_exe.root_module.addImport("options", build_options_module); 109 + 110 + const run_test = b.addRunArtifact(test_exe); 111 + test_step.dependOn(&run_test.step); 112 + } 70 113 } 71 114 72 115 fn buildTargets(b: *std.Build, build_options: *std.Build.Module) !void {
+44 -1
src/circ_stack.zig
··· 30 30 pub fn pop(self: *Self) ?T { 31 31 if (self.count == 0) return null; 32 32 33 - self.head = (self.head - 1) % capacity; 33 + self.head = if (self.head == 0) capacity - 1 else self.head - 1; 34 34 const value = self.buf[self.head]; 35 35 self.count -= 1; 36 36 return value; 37 37 } 38 38 }; 39 39 } 40 + 41 + const testing = std.testing; 42 + 43 + test "CircularStack: push and pop basic operations" { 44 + var stack = CircularStack(u32, 5).init(); 45 + 46 + _ = stack.push(1); 47 + _ = stack.push(2); 48 + _ = stack.push(3); 49 + 50 + try testing.expectEqual(@as(?u32, 3), stack.pop()); 51 + try testing.expectEqual(@as(?u32, 2), stack.pop()); 52 + try testing.expectEqual(@as(?u32, 1), stack.pop()); 53 + try testing.expectEqual(@as(?u32, null), stack.pop()); 54 + } 55 + 56 + test "CircularStack: wraparound behavior at capacity" { 57 + var stack = CircularStack(u32, 3).init(); 58 + 59 + _ = stack.push(1); 60 + _ = stack.push(2); 61 + _ = stack.push(3); 62 + 63 + const evicted = stack.push(4); 64 + try testing.expectEqual(@as(?u32, 1), evicted); 65 + 66 + try testing.expectEqual(@as(?u32, 4), stack.pop()); 67 + try testing.expectEqual(@as(?u32, 3), stack.pop()); 68 + try testing.expectEqual(@as(?u32, 2), stack.pop()); 69 + } 70 + 71 + test "CircularStack: reset clears all entries" { 72 + var stack = CircularStack(u32, 5).init(); 73 + 74 + _ = stack.push(1); 75 + _ = stack.push(2); 76 + _ = stack.push(3); 77 + 78 + stack.reset(); 79 + 80 + try testing.expectEqual(@as(?u32, null), stack.pop()); 81 + try testing.expectEqual(@as(usize, 0), stack.count); 82 + }
+50
src/commands.zig
··· 172 172 try app.repopulateDirectory(""); 173 173 app.directories.history.reset(); 174 174 } 175 + 176 + const testing = std.testing; 177 + 178 + test "CommandHistory: add and retrieve commands" { 179 + var history = CommandHistory{}; 180 + defer history.deinit(testing.allocator); 181 + 182 + try history.add(":cd /tmp", testing.allocator); 183 + try history.add(":config", testing.allocator); 184 + 185 + try testing.expectEqual(@as(usize, 2), history.count); 186 + } 187 + 188 + test "CommandHistory: previous/next navigation" { 189 + var history = CommandHistory{}; 190 + defer history.deinit(testing.allocator); 191 + 192 + try history.add(":cmd1", testing.allocator); 193 + try history.add(":cmd2", testing.allocator); 194 + try history.add(":cmd3", testing.allocator); 195 + 196 + const cmd3 = history.previous(); 197 + try testing.expectEqualStrings(":cmd3", cmd3.?); 198 + 199 + const cmd2 = history.previous(); 200 + try testing.expectEqualStrings(":cmd2", cmd2.?); 201 + 202 + const cmd3_again = history.next(); 203 + try testing.expectEqualStrings(":cmd3", cmd3_again.?); 204 + 205 + const at_end = history.next(); 206 + try testing.expect(at_end == null); 207 + } 208 + 209 + test "CommandHistory: wraparound at capacity" { 210 + var history = CommandHistory{}; 211 + defer history.deinit(testing.allocator); 212 + 213 + var i: u32 = 0; 214 + while (i < 15) : (i += 1) { 215 + const cmd = try std.fmt.allocPrint(testing.allocator, ":cmd{}", .{i}); 216 + defer testing.allocator.free(cmd); 217 + try history.add(cmd, testing.allocator); 218 + } 219 + 220 + try testing.expectEqual(@as(usize, 10), history.count); 221 + 222 + const recent = history.previous(); 223 + try testing.expectEqualStrings(":cmd14", recent.?); 224 + }
+88
src/directories.zig
··· 167 167 } 168 168 self.child_entries.clear(); 169 169 } 170 + 171 + const testing = std.testing; 172 + 173 + test "Directories: populateEntries respects show_hidden config" { 174 + const local_config = &@import("./config.zig").config; 175 + 176 + var tmp = testing.tmpDir(.{}); 177 + defer tmp.cleanup(); 178 + 179 + { 180 + const visible = try tmp.dir.createFile("visible.txt", .{}); 181 + visible.close(); 182 + const hidden = try tmp.dir.createFile(".hidden.txt", .{}); 183 + hidden.close(); 184 + } 185 + 186 + var path_buf: [std.fs.max_path_bytes]u8 = undefined; 187 + const tmp_path = try tmp.dir.realpath(".", &path_buf); 188 + const iter_dir = try std.fs.openDirAbsolute(tmp_path, .{ .iterate = true }); 189 + 190 + var dirs = try Self.init(testing.allocator, null); 191 + defer { 192 + dirs.clearEntries(); 193 + dirs.clearChildEntries(); 194 + dirs.entries.deinit(); 195 + dirs.child_entries.deinit(); 196 + dirs.searcher.deinit(); 197 + } 198 + dirs.dir.close(); 199 + dirs.dir = iter_dir; 200 + 201 + local_config.show_hidden = false; 202 + try dirs.populateEntries(""); 203 + try testing.expectEqual(@as(usize, 1), dirs.entries.len()); 204 + 205 + dirs.clearEntries(); 206 + local_config.show_hidden = true; 207 + try dirs.populateEntries(""); 208 + try testing.expectEqual(@as(usize, 2), dirs.entries.len()); 209 + } 210 + 211 + test "Directories: fuzzy search filters entries" { 212 + var tmp = testing.tmpDir(.{}); 213 + defer tmp.cleanup(); 214 + 215 + { 216 + const f1 = try tmp.dir.createFile("test_file.txt", .{}); 217 + f1.close(); 218 + const f2 = try tmp.dir.createFile("other.txt", .{}); 219 + f2.close(); 220 + const f3 = try tmp.dir.createFile("test_another.txt", .{}); 221 + f3.close(); 222 + } 223 + 224 + var path_buf: [std.fs.max_path_bytes]u8 = undefined; 225 + const tmp_path = try tmp.dir.realpath(".", &path_buf); 226 + const iter_dir = try std.fs.openDirAbsolute(tmp_path, .{ .iterate = true }); 227 + 228 + var dirs = try Self.init(testing.allocator, null); 229 + defer { 230 + dirs.clearEntries(); 231 + dirs.clearChildEntries(); 232 + dirs.entries.deinit(); 233 + dirs.child_entries.deinit(); 234 + dirs.searcher.deinit(); 235 + } 236 + dirs.dir.close(); 237 + dirs.dir = iter_dir; 238 + 239 + try dirs.populateEntries("test"); 240 + // Should match test_* 241 + try testing.expect(dirs.entries.len() >= 2); 242 + 243 + // Verify all entries contain "test" 244 + for (dirs.entries.all()) |entry| { 245 + try testing.expect(std.mem.indexOf(u8, entry.name, "test") != null); 246 + } 247 + } 248 + 249 + test "Directories: fullPath resolves relative paths" { 250 + var dirs = try Self.init(testing.allocator, "."); 251 + defer dirs.deinit(); 252 + 253 + const path = try dirs.fullPath("."); 254 + try testing.expect(path.len > 0); 255 + // Should be absolute 256 + try testing.expect(std.mem.indexOf(u8, path, "/") != null); 257 + }
+67
src/list.zig
··· 85 85 } 86 86 }; 87 87 } 88 + 89 + const testing = std.testing; 90 + 91 + test "List: navigation respects bounds" { 92 + var list = List(u32).init(testing.allocator); 93 + defer list.deinit(); 94 + 95 + try list.append(1); 96 + try list.append(2); 97 + try list.append(3); 98 + 99 + try testing.expectEqual(@as(usize, 0), list.selected); 100 + 101 + list.next(); 102 + try testing.expectEqual(@as(usize, 1), list.selected); 103 + 104 + list.next(); 105 + list.next(); 106 + // Try to go past end 107 + list.next(); 108 + // Should stay at last 109 + try testing.expectEqual(@as(usize, 2), list.selected); 110 + 111 + list.previous(); 112 + try testing.expectEqual(@as(usize, 1), list.selected); 113 + 114 + list.previous(); 115 + // Try to go before start 116 + list.previous(); 117 + // Should stay at first 118 + try testing.expectEqual(@as(usize, 0), list.selected); 119 + } 120 + 121 + test "List: getSelected handles empty list" { 122 + var list = List(u32).init(testing.allocator); 123 + defer list.deinit(); 124 + 125 + const result = try list.getSelected(); 126 + try testing.expect(result == null); 127 + } 128 + 129 + test "List: append and get operations" { 130 + var list = List(u32).init(testing.allocator); 131 + defer list.deinit(); 132 + 133 + try list.append(42); 134 + try list.append(84); 135 + 136 + try testing.expectEqual(@as(usize, 2), list.len()); 137 + try testing.expectEqual(@as(u32, 42), try list.get(0)); 138 + try testing.expectEqual(@as(u32, 84), try list.get(1)); 139 + } 140 + 141 + test "List: selectFirst and selectLast" { 142 + var list = List(u32).init(testing.allocator); 143 + defer list.deinit(); 144 + 145 + try list.append(1); 146 + try list.append(2); 147 + try list.append(3); 148 + 149 + list.selectLast(); 150 + try testing.expectEqual(@as(usize, 2), list.selected); 151 + 152 + list.selectFirst(); 153 + try testing.expectEqual(@as(usize, 0), list.selected); 154 + }
+84
src/test_file_operations.zig
··· 1 + const std = @import("std"); 2 + const testing = std.testing; 3 + const TestEnv = @import("test_helpers.zig").TestEnv; 4 + const Directories = @import("directories.zig"); 5 + const environment = @import("environment.zig"); 6 + 7 + test "FileOps: create new directory" { 8 + var env = try TestEnv.init(testing.allocator); 9 + defer env.deinit(); 10 + 11 + var dirs = try Directories.init(testing.allocator, env.tmp_path); 12 + defer dirs.deinit(); 13 + 14 + try dirs.dir.makeDir("testdir"); 15 + 16 + var test_dir = dirs.dir.openDir("testdir", .{}) catch |err| { 17 + std.debug.print("Failed to open created directory: {}\n", .{err}); 18 + return err; 19 + }; 20 + test_dir.close(); 21 + 22 + try dirs.populateEntries(""); 23 + var found = false; 24 + for (dirs.entries.all()) |entry| { 25 + if (std.mem.eql(u8, entry.name, "testdir")) { 26 + found = true; 27 + try testing.expectEqual(std.fs.Dir.Entry.Kind.directory, entry.kind); 28 + } 29 + } 30 + try testing.expect(found); 31 + } 32 + 33 + test "FileOps: create new file" { 34 + var env = try TestEnv.init(testing.allocator); 35 + defer env.deinit(); 36 + 37 + var dirs = try Directories.init(testing.allocator, env.tmp_path); 38 + defer dirs.deinit(); 39 + 40 + const file = try dirs.dir.createFile("testfile.txt", .{}); 41 + file.close(); 42 + 43 + try testing.expect(environment.fileExists(dirs.dir, "testfile.txt")); 44 + 45 + try dirs.populateEntries(""); 46 + var found = false; 47 + for (dirs.entries.all()) |entry| { 48 + if (std.mem.eql(u8, entry.name, "testfile.txt")) { 49 + found = true; 50 + try testing.expectEqual(std.fs.Dir.Entry.Kind.file, entry.kind); 51 + } 52 + } 53 + try testing.expect(found); 54 + } 55 + 56 + test "FileOps: rename file" { 57 + var env = try TestEnv.init(testing.allocator); 58 + defer env.deinit(); 59 + 60 + try env.createFiles(&.{"oldname.txt"}); 61 + 62 + var dirs = try Directories.init(testing.allocator, env.tmp_path); 63 + defer dirs.deinit(); 64 + 65 + try dirs.populateEntries(""); 66 + 67 + try testing.expect(environment.fileExists(dirs.dir, "oldname.txt")); 68 + try dirs.dir.rename("oldname.txt", "newname.txt"); 69 + try testing.expect(!environment.fileExists(dirs.dir, "oldname.txt")); 70 + try testing.expect(environment.fileExists(dirs.dir, "newname.txt")); 71 + 72 + dirs.clearEntries(); 73 + try dirs.populateEntries(""); 74 + 75 + var found_old = false; 76 + var found_new = false; 77 + for (dirs.entries.all()) |entry| { 78 + if (std.mem.eql(u8, entry.name, "oldname.txt")) found_old = true; 79 + if (std.mem.eql(u8, entry.name, "newname.txt")) found_new = true; 80 + } 81 + 82 + try testing.expect(!found_old); 83 + try testing.expect(found_new); 84 + }
+61
src/test_helpers.zig
··· 1 + const std = @import("std"); 2 + 3 + pub const TestEnv = struct { 4 + allocator: std.mem.Allocator, 5 + tmp_dir: std.testing.TmpDir, 6 + tmp_path: []const u8, 7 + 8 + pub fn init(allocator: std.mem.Allocator) !TestEnv { 9 + var tmp_dir = std.testing.tmpDir(.{}); 10 + const real_path = try tmp_dir.dir.realpathAlloc(allocator, "."); 11 + 12 + return TestEnv{ 13 + .allocator = allocator, 14 + .tmp_dir = tmp_dir, 15 + .tmp_path = real_path, 16 + }; 17 + } 18 + 19 + pub fn deinit(self: *TestEnv) void { 20 + self.allocator.free(self.tmp_path); 21 + self.tmp_dir.cleanup(); 22 + } 23 + 24 + pub fn createFiles(self: *TestEnv, names: []const []const u8) !void { 25 + for (names) |name| { 26 + const file = try self.tmp_dir.dir.createFile(name, .{}); 27 + file.close(); 28 + } 29 + } 30 + 31 + pub const DirNode = struct { 32 + name: []const u8, 33 + children: ?[]const DirNode, 34 + }; 35 + 36 + pub fn createDirStructure(self: *TestEnv, nodes: []const DirNode) !void { 37 + for (nodes) |node| { 38 + if (node.children) |children| { 39 + try self.tmp_dir.dir.makeDir(node.name); 40 + var subdir = try self.tmp_dir.dir.openDir(node.name, .{}); 41 + defer subdir.close(); 42 + 43 + for (children) |child| { 44 + if (child.children) |_| { 45 + try subdir.makeDir(child.name); 46 + } else { 47 + const file = try subdir.createFile(child.name, .{}); 48 + file.close(); 49 + } 50 + } 51 + } else { 52 + const file = try self.tmp_dir.dir.createFile(node.name, .{}); 53 + file.close(); 54 + } 55 + } 56 + } 57 + 58 + pub fn path(self: *TestEnv, relative: []const u8) ![]const u8 { 59 + return try std.fs.path.join(self.allocator, &.{ self.tmp_path, relative }); 60 + } 61 + };
+114
src/test_navigation.zig
··· 1 + const std = @import("std"); 2 + const testing = std.testing; 3 + const TestEnv = @import("test_helpers.zig").TestEnv; 4 + const Directories = @import("directories.zig"); 5 + const events = @import("events.zig"); 6 + const App = @import("app.zig"); 7 + 8 + test "Navigation: traverse left to parent directory" { 9 + var env = try TestEnv.init(testing.allocator); 10 + defer env.deinit(); 11 + 12 + try env.createDirStructure(&.{ 13 + .{ .name = "parent", .children = &.{ 14 + .{ .name = "child", .children = &.{} }, 15 + .{ .name = "sibling.txt", .children = null }, 16 + } }, 17 + }); 18 + 19 + const child_path = try env.path("parent/child"); 20 + defer testing.allocator.free(child_path); 21 + 22 + var dirs = try Directories.init(testing.allocator, child_path); 23 + defer dirs.deinit(); 24 + 25 + const before_path = try dirs.fullPath("."); 26 + try testing.expect(std.mem.endsWith(u8, before_path, "child")); 27 + 28 + const parent_dir = try dirs.dir.openDir("../", .{ .iterate = true }); 29 + dirs.dir.close(); 30 + dirs.dir = parent_dir; 31 + 32 + const after_path = try dirs.fullPath("."); 33 + try testing.expect(std.mem.endsWith(u8, after_path, "parent")); 34 + 35 + try dirs.populateEntries(""); 36 + var found_child = false; 37 + for (dirs.entries.all()) |entry| { 38 + if (std.mem.eql(u8, entry.name, "child")) { 39 + found_child = true; 40 + try testing.expectEqual(std.fs.Dir.Entry.Kind.directory, entry.kind); 41 + } 42 + } 43 + try testing.expect(found_child); 44 + } 45 + 46 + test "Navigation: traverse right into directory" { 47 + var env = try TestEnv.init(testing.allocator); 48 + defer env.deinit(); 49 + 50 + try env.createDirStructure(&.{ 51 + .{ .name = "subdir", .children = &.{ 52 + .{ .name = "inner.txt", .children = null }, 53 + } }, 54 + .{ .name = "file.txt", .children = null }, 55 + }); 56 + 57 + var dirs = try Directories.init(testing.allocator, env.tmp_path); 58 + defer dirs.deinit(); 59 + 60 + try dirs.populateEntries(""); 61 + 62 + for (dirs.entries.all(), 0..) |entry, i| { 63 + if (std.mem.eql(u8, entry.name, "subdir")) { 64 + dirs.entries.selected = i; 65 + break; 66 + } 67 + } 68 + 69 + const selected = try dirs.getSelected(); 70 + try testing.expect(selected != null); 71 + try testing.expectEqualStrings("subdir", selected.?.name); 72 + 73 + const subdir = try dirs.dir.openDir("subdir", .{ .iterate = true }); 74 + dirs.dir.close(); 75 + dirs.dir = subdir; 76 + 77 + const current_path = try dirs.fullPath("."); 78 + try testing.expect(std.mem.endsWith(u8, current_path, "subdir")); 79 + 80 + dirs.clearEntries(); 81 + try dirs.populateEntries(""); 82 + try testing.expectEqual(@as(usize, 1), dirs.entries.len()); 83 + 84 + const inner = try dirs.entries.get(0); 85 + try testing.expectEqualStrings("inner.txt", inner.name); 86 + } 87 + 88 + test "Navigation: move selection with next and previous" { 89 + var env = try TestEnv.init(testing.allocator); 90 + defer env.deinit(); 91 + 92 + try env.createFiles(&.{ "file1.txt", "file2.txt", "file3.txt", "file4.txt", "file5.txt" }); 93 + 94 + var dirs = try Directories.init(testing.allocator, env.tmp_path); 95 + defer dirs.deinit(); 96 + 97 + try dirs.populateEntries(""); 98 + try testing.expectEqual(@as(usize, 5), dirs.entries.len()); 99 + try testing.expectEqual(@as(usize, 0), dirs.entries.selected); 100 + 101 + dirs.entries.next(); 102 + dirs.entries.next(); 103 + dirs.entries.next(); 104 + try testing.expectEqual(@as(usize, 3), dirs.entries.selected); 105 + 106 + dirs.entries.previous(); 107 + try testing.expectEqual(@as(usize, 2), dirs.entries.selected); 108 + 109 + dirs.entries.selectLast(); 110 + try testing.expectEqual(@as(usize, 4), dirs.entries.selected); 111 + 112 + dirs.entries.selectFirst(); 113 + try testing.expectEqual(@as(usize, 0), dirs.entries.selected); 114 + }