commit 746932d8a6954fd27d99593b7d4b51774cf95a1b
parent 6d4e9c975b027c7846a7133974c2ad8e8ccb4c00
Author: mtmn <miro@haravara.org>
Date: Wed, 10 Jun 2026 00:50:39 +0200
feat: test: add `testing` and fix findings
Diffstat:
| M | build.zig | | | 15 | +++++++++++++++ |
| M | justfile | | | 7 | +++++-- |
| M | src/cfg.zig | | | 50 | ++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | src/cli.zig | | | 180 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- |
| M | src/db.zig | | | 186 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------- |
| M | src/fzf.zig | | | 44 | ++++++++++++++++++++++++++++---------------- |
| M | src/main.zig | | | 7 | +++++++ |
7 files changed, 410 insertions(+), 79 deletions(-)
diff --git a/build.zig b/build.zig
@@ -31,4 +31,19 @@ pub fn build(b: *std.Build) void {
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
+
+ const tests = b.addTest(.{
+ .root_module = b.createModule(.{
+ .root_source_file = b.path("src/main.zig"),
+ .target = target,
+ .optimize = optimize,
+ .link_libc = true,
+ .imports = &.{
+ .{ .name = "zul", .module = zul.module("zul") },
+ },
+ }),
+ });
+ const run_tests = b.addRunArtifact(tests);
+ const test_step = b.step("test", "Run unit tests");
+ test_step.dependOn(&run_tests.step);
}
diff --git a/justfile b/justfile
@@ -1,8 +1,11 @@
run opt="Debug":
zig build run -Doptimize={{ opt }}
+test:
+ zig build test --summary all
+
build opt="Debug":
zig build -Doptimize={{ opt }} -p build/{{ opt }}
-install opt="ReleaseSafe" prefix="~/.local":
- zig build -Doptimize={{ opt }} --prefix {{ prefix }} install
+install opt="ReleaseSafe" path="~/.local":
+ zig build -Doptimize={{ opt }} --prefix {{ path }} install
diff --git a/src/cfg.zig b/src/cfg.zig
@@ -62,3 +62,53 @@ pub fn loadConfig(allocator: std.mem.Allocator) ?std.json.Parsed(Config) {
return null;
};
}
+
+const testing = std.testing;
+
+test "applyFolderOverride: no overrides returns base" {
+ const base = Config{ .max_depth = 3, .editor = "vim" };
+ const got = applyFolderOverride(base, "/anywhere");
+ try testing.expectEqual(@as(usize, 3), got.max_depth);
+ try testing.expectEqualStrings("vim", got.editor.?);
+}
+
+test "applyFolderOverride: matching path overrides only provided fields" {
+ const ignored = [_][]const u8{ "node_modules", ".git" };
+ var overrides = [_]FolderOverride{
+ .{ .path = "/work", .max_depth = 9, .editor = "nvim" },
+ };
+ const base = Config{
+ .max_depth = 3,
+ .editor = "vim",
+ .ignored_patterns = &ignored,
+ .overrides = &overrides,
+ };
+
+ const got = applyFolderOverride(base, "/work");
+ try testing.expectEqual(@as(usize, 9), got.max_depth);
+ try testing.expectEqualStrings("nvim", got.editor.?);
+ // A field left null on the override keeps the base value.
+ try testing.expectEqual(base.ignored_patterns.ptr, got.ignored_patterns.ptr);
+}
+
+test "applyFolderOverride: non-matching path leaves base untouched" {
+ var overrides = [_]FolderOverride{
+ .{ .path = "/work", .max_depth = 9 },
+ };
+ const base = Config{ .max_depth = 3, .editor = "vim", .overrides = &overrides };
+
+ const got = applyFolderOverride(base, "/elsewhere");
+ try testing.expectEqual(@as(usize, 3), got.max_depth);
+ try testing.expectEqualStrings("vim", got.editor.?);
+}
+
+test "applyFolderOverride: first matching override wins" {
+ var overrides = [_]FolderOverride{
+ .{ .path = "/work", .max_depth = 5 },
+ .{ .path = "/work", .max_depth = 8 },
+ };
+ const base = Config{ .max_depth = 3, .overrides = &overrides };
+
+ const got = applyFolderOverride(base, "/work");
+ try testing.expectEqual(@as(usize, 5), got.max_depth);
+}
diff --git a/src/cli.zig b/src/cli.zig
@@ -50,8 +50,14 @@ pub const CommandLineArgs = struct {
arena.* = std.heap.ArenaAllocator.init(parent);
errdefer arena.deinit();
+ const items = try source.toSlice(arena.allocator());
+ return finishParse(arena, items);
+ }
+
+ // Parses an already-collected argv. Split out from `parse` so tests can
+ // feed a literal slice without constructing a `std.process.Args`.
+ fn finishParse(arena: *std.heap.ArenaAllocator, items: []const [:0]const u8) !CommandLineArgs {
const allocator = arena.allocator();
- const items = try source.toSlice(allocator);
var lookup: std.StringHashMapUnmanaged([]const u8) = .{};
@@ -61,7 +67,6 @@ pub const CommandLineArgs = struct {
const exe = items[0];
- // 1, skip the exe
var i: usize = 1;
var tail_start: usize = 1;
@@ -140,7 +145,14 @@ const KeyValue = struct {
pub fn parseArgs(allocator: std.mem.Allocator, source: std.process.Args) !Args {
const cla = try CommandLineArgs.parse(allocator, source);
+ errdefer cla.deinit();
+ return resolveArgs(cla);
+}
+// Maps a parsed command line onto an `Args`. Split from `parseArgs` so tests
+// can drive it from a `CommandLineArgs` built off a literal slice. On success
+// the returned `Args` owns `cla` (freed via `Args.deinit`).
+fn resolveArgs(cla: CommandLineArgs) !Args {
var res = Args{
.action = .help,
._cla = cla,
@@ -247,3 +259,167 @@ pub fn printUsage() !void {
, .{});
try writer.end();
}
+
+const testing = std.testing;
+
+// Builds a CommandLineArgs straight from a literal argv, bypassing
+// std.process.Args. Mirrors `parse`'s ownership shape exactly (errdefer +
+// error-union return). Caller must `defer cla.deinit()`.
+fn testCla(items: []const [:0]const u8) !CommandLineArgs {
+ const arena = try testing.allocator.create(std.heap.ArenaAllocator);
+ errdefer testing.allocator.destroy(arena);
+ arena.* = std.heap.ArenaAllocator.init(testing.allocator);
+ errdefer arena.deinit();
+ return CommandLineArgs.finishParse(arena, items);
+}
+
+// Builds an Args from a literal argv, mirroring `parseArgs`. Caller must
+// `defer args.deinit()`.
+fn testArgs(items: []const [:0]const u8) !Args {
+ const cla = try testCla(items);
+ errdefer cla.deinit();
+ return resolveArgs(cla);
+}
+
+test "CommandLineArgs: empty argv" {
+ var cla = try testCla(&.{});
+ defer cla.deinit();
+ try testing.expectEqualStrings("", cla.exe);
+ try testing.expectEqual(@as(usize, 0), cla.tail.len);
+}
+
+test "CommandLineArgs: exe only" {
+ var cla = try testCla(&[_][:0]const u8{"magdalena"});
+ defer cla.deinit();
+ try testing.expectEqualStrings("magdalena", cla.exe);
+ try testing.expectEqual(@as(usize, 0), cla.tail.len);
+}
+
+test "CommandLineArgs: flags with and without values" {
+ var cla = try testCla(&[_][:0]const u8{ "bin", "--level", "info", "--silent", "-p", "5432", "-x" });
+ defer cla.deinit();
+ try testing.expectEqualStrings("bin", cla.exe);
+ try testing.expectEqual(@as(usize, 0), cla.tail.len);
+ try testing.expectEqualStrings("info", cla.get("level").?);
+ try testing.expect(cla.contains("silent"));
+ try testing.expectEqualStrings("", cla.get("silent").?);
+ try testing.expectEqualStrings("5432", cla.get("p").?);
+ try testing.expect(cla.contains("x"));
+}
+
+test "CommandLineArgs: key=value form" {
+ var cla = try testCla(&[_][:0]const u8{ "bin", "--level=error", "-p=6669" });
+ defer cla.deinit();
+ try testing.expectEqualStrings("error", cla.get("level").?);
+ try testing.expectEqualStrings("6669", cla.get("p").?);
+}
+
+test "CommandLineArgs: bundled single-char flags" {
+ var cla = try testCla(&[_][:0]const u8{ "bin", "-xvf", "file.tar.gz" });
+ defer cla.deinit();
+ try testing.expect(cla.contains("x"));
+ try testing.expectEqualStrings("", cla.get("x").?);
+ try testing.expect(cla.contains("v"));
+ try testing.expectEqualStrings("file.tar.gz", cla.get("f").?);
+}
+
+test "CommandLineArgs: tail begins at first positional" {
+ // `-l` is followed by `--k`, which starts with '-', so `-l` takes no value;
+ // `--k` consumes the following `x`; the tail then starts at the first
+ // non-flag token.
+ var cla = try testCla(&[_][:0]const u8{ "bin", "-l", "--k", "x", "ts", "-p=6669", "hello" });
+ defer cla.deinit();
+ try testing.expect(cla.contains("l"));
+ try testing.expectEqualStrings("x", cla.get("k").?);
+ try testing.expectEqual(@as(usize, 3), cla.tail.len);
+ try testing.expectEqualStrings("ts", cla.tail[0]);
+ try testing.expectEqualStrings("-p=6669", cla.tail[1]);
+ try testing.expectEqualStrings("hello", cla.tail[2]);
+}
+
+test "resolveArgs: no args is help" {
+ var args = try testArgs(&[_][:0]const u8{"magdalena"});
+ defer args.deinit();
+ try testing.expectEqual(Action.help, args.action);
+}
+
+test "resolveArgs: --help and -h" {
+ for ([_][:0]const u8{ "--help", "-h", "help" }) |flag| {
+ var args = try testArgs(&[_][:0]const u8{ "magdalena", flag });
+ defer args.deinit();
+ try testing.expectEqual(Action.help, args.action);
+ }
+}
+
+test "resolveArgs: unknown command falls back to help" {
+ var args = try testArgs(&[_][:0]const u8{ "magdalena", "bogus" });
+ defer args.deinit();
+ try testing.expectEqual(Action.help, args.action);
+}
+
+test "resolveArgs: cleanup is triggered by -c and --clean flags" {
+ // Cleanup is a flag, not a subcommand: a bare `clean` positional is not
+ // recognized and falls through to help.
+ for ([_][:0]const u8{ "-c", "--clean" }) |flag| {
+ var args = try testArgs(&[_][:0]const u8{ "magdalena", flag });
+ defer args.deinit();
+ try testing.expectEqual(Action.cleanup, args.action);
+ }
+
+ var bare = try testArgs(&[_][:0]const u8{ "magdalena", "clean" });
+ defer bare.deinit();
+ try testing.expectEqual(Action.help, bare.action);
+}
+
+test "resolveArgs: simple subcommands" {
+ const cases = [_]struct { cmd: [:0]const u8, action: Action }{
+ .{ .cmd = "recent-dirs", .action = .recent_dirs },
+ .{ .cmd = "recent-files", .action = .recent_files },
+ .{ .cmd = "favorites", .action = .favorites },
+ .{ .cmd = "goto-dir", .action = .goto_dir },
+ .{ .cmd = "goto-file", .action = .goto_file },
+ .{ .cmd = "grep", .action = .grep },
+ };
+ for (cases) |c| {
+ var args = try testArgs(&[_][:0]const u8{ "magdalena", c.cmd });
+ defer args.deinit();
+ try testing.expectEqual(c.action, args.action);
+ }
+}
+
+test "resolveArgs: search carries query" {
+ var args = try testArgs(&[_][:0]const u8{ "magdalena", "search", "needle" });
+ defer args.deinit();
+ try testing.expectEqual(Action.search, args.action);
+ try testing.expectEqualStrings("needle", args.query.?);
+}
+
+test "resolveArgs: log-file carries path, type and action" {
+ var args = try testArgs(&[_][:0]const u8{ "magdalena", "log-file", "/tmp/x.zig", "zig", "open" });
+ defer args.deinit();
+ try testing.expectEqual(Action.log_file, args.action);
+ try testing.expectEqualStrings("/tmp/x.zig", args.log_path.?);
+ try testing.expectEqualStrings("zig", args.file_type.?);
+ try testing.expectEqualStrings("open", args.file_action.?);
+}
+
+test "resolveArgs: depth before command via --depth, --depth= and -d" {
+ const cases = [_][:0]const u8{ "--depth", "--depth=4", "-d" };
+ inline for (cases) |form| {
+ const items = if (std.mem.eql(u8, form, "--depth=4"))
+ &[_][:0]const u8{ "magdalena", "--depth=4", "look-dir" }
+ else
+ &[_][:0]const u8{ "magdalena", form, "4", "look-dir" };
+ var args = try testArgs(items);
+ defer args.deinit();
+ try testing.expectEqual(Action.look_dir, args.action);
+ try testing.expectEqual(@as(usize, 4), args.depth.?);
+ }
+}
+
+test "resolveArgs: depth after command via tail scan" {
+ var args = try testArgs(&[_][:0]const u8{ "magdalena", "look-file", "--depth", "7" });
+ defer args.deinit();
+ try testing.expectEqual(Action.look_file, args.action);
+ try testing.expectEqual(@as(usize, 7), args.depth.?);
+}
diff --git a/src/db.zig b/src/db.zig
@@ -42,12 +42,10 @@ pub const Db = struct {
.files_path = files_path,
};
- // Ensure directory exists
std.Io.Dir.cwd().createDirPath(io.rt, base_path) catch |err| {
if (err != error.PathAlreadyExists) return err;
};
- // Ensure files exist
for (&[_][]const u8{ dirs_path, files_path }) |p| {
var f = std.Io.Dir.cwd().openFile(io.rt, p, .{ .mode = .read_write }) catch |err| {
if (err == error.FileNotFound) {
@@ -74,7 +72,7 @@ pub const Db = struct {
if (err == error.FileNotFound) return zul.Managed([]DirectoryEntry).fromJson(try std.json.parseFromSlice([]DirectoryEntry, self.allocator, "[]", .{}));
return err;
};
- errdefer self.allocator.free(content);
+ defer self.allocator.free(content);
var arena = try self.allocator.create(std.heap.ArenaAllocator);
errdefer {
@@ -82,35 +80,10 @@ pub const Db = struct {
self.allocator.destroy(arena);
}
arena.* = std.heap.ArenaAllocator.init(self.allocator);
- const allocator = arena.allocator();
-
- var list = std.ArrayListUnmanaged(DirectoryEntry).empty;
-
- var seen = std.StringHashMap(void).init(allocator);
- // Use splitBackwardsScalar to read the newest entries first
- var it = std.mem.splitBackwardsScalar(u8, content, '\n');
- while (it.next()) |line| {
- const trimmed = std.mem.trim(u8, line, " \r\t");
- if (trimmed.len == 0) continue;
-
- var line_it = std.mem.splitScalar(u8, trimmed, '|');
- const ts = line_it.next() orelse continue;
- const dir_path = line_it.next() orelse continue;
-
- if (seen.contains(dir_path)) continue;
- try seen.put(dir_path, {});
-
- try list.append(allocator, .{
- .path = try allocator.dupe(u8, dir_path),
- .timestamp = try allocator.dupe(u8, ts),
- });
- }
-
- self.allocator.free(content);
return .{
.arena = arena,
- .value = try list.toOwnedSlice(allocator),
+ .value = try parseDirLog(arena.allocator(), content),
};
}
@@ -119,7 +92,7 @@ pub const Db = struct {
if (err == error.FileNotFound) return zul.Managed([]FileEntry).fromJson(try std.json.parseFromSlice([]FileEntry, self.allocator, "[]", .{}));
return err;
};
- errdefer self.allocator.free(content);
+ defer self.allocator.free(content);
var arena = try self.allocator.create(std.heap.ArenaAllocator);
errdefer {
@@ -127,38 +100,10 @@ pub const Db = struct {
self.allocator.destroy(arena);
}
arena.* = std.heap.ArenaAllocator.init(self.allocator);
- const allocator = arena.allocator();
-
- var list = std.ArrayListUnmanaged(FileEntry).empty;
-
- var seen = std.StringHashMap(void).init(allocator);
-
- var it = std.mem.splitBackwardsScalar(u8, content, '\n');
- while (it.next()) |line| {
- const trimmed = std.mem.trim(u8, line, " \r\t");
- if (trimmed.len == 0) continue;
- var line_it = std.mem.splitScalar(u8, trimmed, '|');
- const ts = line_it.next() orelse continue;
- const ftype = line_it.next() orelse continue;
- const action = line_it.next() orelse continue;
- const fpath = line_it.next() orelse continue;
-
- if (seen.contains(fpath)) continue;
- try seen.put(fpath, {});
-
- try list.append(allocator, .{
- .path = try allocator.dupe(u8, fpath),
- .file_type = try allocator.dupe(u8, ftype),
- .action = try allocator.dupe(u8, action),
- .timestamp = try allocator.dupe(u8, ts),
- });
- }
-
- self.allocator.free(content);
return .{
.arena = arena,
- .value = try list.toOwnedSlice(allocator),
+ .value = try parseFileLog(arena.allocator(), content),
};
}
@@ -334,3 +279,126 @@ pub fn getDefaultDbPath(allocator: std.mem.Allocator) ![]const u8 {
}
return error.HomeNotFound;
}
+
+// Parses a `dirs.log` body (`timestamp|path` lines) newest-first, keeping only
+// the first occurrence of each path. Pure logic, no IO — split out for testing.
+fn parseDirLog(allocator: std.mem.Allocator, content: []const u8) ![]DirectoryEntry {
+ var list = std.ArrayListUnmanaged(DirectoryEntry).empty;
+ var seen = std.StringHashMap(void).init(allocator);
+ defer seen.deinit();
+
+ var it = std.mem.splitBackwardsScalar(u8, content, '\n');
+ while (it.next()) |line| {
+ const trimmed = std.mem.trim(u8, line, " \r\t");
+ if (trimmed.len == 0) continue;
+
+ var line_it = std.mem.splitScalar(u8, trimmed, '|');
+ const ts = line_it.next() orelse continue;
+ const dir_path = line_it.next() orelse continue;
+
+ if (seen.contains(dir_path)) continue;
+ try seen.put(dir_path, {});
+
+ try list.append(allocator, .{
+ .path = try allocator.dupe(u8, dir_path),
+ .timestamp = try allocator.dupe(u8, ts),
+ });
+ }
+
+ return list.toOwnedSlice(allocator);
+}
+
+// Parses a `files.log` body (`timestamp|type|action|path` lines) newest-first,
+// keeping only the first occurrence of each path.
+fn parseFileLog(allocator: std.mem.Allocator, content: []const u8) ![]FileEntry {
+ var list = std.ArrayListUnmanaged(FileEntry).empty;
+ var seen = std.StringHashMap(void).init(allocator);
+ defer seen.deinit();
+
+ var it = std.mem.splitBackwardsScalar(u8, content, '\n');
+ while (it.next()) |line| {
+ const trimmed = std.mem.trim(u8, line, " \r\t");
+ if (trimmed.len == 0) continue;
+
+ var line_it = std.mem.splitScalar(u8, trimmed, '|');
+ const ts = line_it.next() orelse continue;
+ const ftype = line_it.next() orelse continue;
+ const action = line_it.next() orelse continue;
+ const fpath = line_it.next() orelse continue;
+
+ if (seen.contains(fpath)) continue;
+ try seen.put(fpath, {});
+
+ try list.append(allocator, .{
+ .path = try allocator.dupe(u8, fpath),
+ .file_type = try allocator.dupe(u8, ftype),
+ .action = try allocator.dupe(u8, action),
+ .timestamp = try allocator.dupe(u8, ts),
+ });
+ }
+
+ return list.toOwnedSlice(allocator);
+}
+
+const testing = std.testing;
+
+test "parseDirLog: newest-first, dedup by path" {
+ var arena = std.heap.ArenaAllocator.init(testing.allocator);
+ defer arena.deinit();
+
+ const content =
+ "2024-01-01T00:00:00Z|/a\n" ++
+ "2024-01-02T00:00:00Z|/b\n" ++
+ "2024-01-03T00:00:00Z|/a\n";
+
+ const entries = try parseDirLog(arena.allocator(), content);
+ try testing.expectEqual(@as(usize, 2), entries.len);
+ try testing.expectEqualStrings("/a", entries[0].path);
+ try testing.expectEqualStrings("2024-01-03T00:00:00Z", entries[0].timestamp.?);
+ try testing.expectEqualStrings("/b", entries[1].path);
+}
+
+test "parseDirLog: skips blank and malformed lines" {
+ var arena = std.heap.ArenaAllocator.init(testing.allocator);
+ defer arena.deinit();
+
+ const content = "\n \n2024|/only\nno-pipe-here\n";
+ const entries = try parseDirLog(arena.allocator(), content);
+ try testing.expectEqual(@as(usize, 1), entries.len);
+ try testing.expectEqualStrings("/only", entries[0].path);
+}
+
+test "parseDirLog: empty input yields no entries" {
+ var arena = std.heap.ArenaAllocator.init(testing.allocator);
+ defer arena.deinit();
+ const entries = try parseDirLog(arena.allocator(), "");
+ try testing.expectEqual(@as(usize, 0), entries.len);
+}
+
+test "parseFileLog: newest-first dedup keeps type and action" {
+ var arena = std.heap.ArenaAllocator.init(testing.allocator);
+ defer arena.deinit();
+
+ const content =
+ "t1|zig|open|/x\n" ++
+ "t2|md|edit|/y\n" ++
+ "t3|rs|run|/x\n";
+
+ const entries = try parseFileLog(arena.allocator(), content);
+ try testing.expectEqual(@as(usize, 2), entries.len);
+ try testing.expectEqualStrings("/x", entries[0].path);
+ try testing.expectEqualStrings("rs", entries[0].file_type);
+ try testing.expectEqualStrings("run", entries[0].action);
+ try testing.expectEqualStrings("t3", entries[0].timestamp.?);
+ try testing.expectEqualStrings("/y", entries[1].path);
+}
+
+test "parseFileLog: ignores lines with too few columns" {
+ var arena = std.heap.ArenaAllocator.init(testing.allocator);
+ defer arena.deinit();
+
+ const content = "t1|zig|open\nt2|md|edit|/y\n";
+ const entries = try parseFileLog(arena.allocator(), content);
+ try testing.expectEqual(@as(usize, 1), entries.len);
+ try testing.expectEqualStrings("/y", entries[0].path);
+}
diff --git a/src/fzf.zig b/src/fzf.zig
@@ -114,22 +114,15 @@ fn runFzfPicker(allocator: std.mem.Allocator, fzf_opts: []const []const u8, item
const child = &fzf.child;
if (child.stdin) |stdin| {
- // Write items in batches to prevent excessive buffer usage
- const batch_size = 1000;
- for (0..((items.len + batch_size - 1) / batch_size)) |batch_idx| {
- const start = batch_idx * batch_size;
- const end = @min(start + batch_size, items.len);
-
- for (start..end) |i| {
- stdin.writeStreamingAll(io.rt, items[i]) catch |err| {
- if (err == error.BrokenPipe) break;
- return err;
- };
- stdin.writeStreamingAll(io.rt, "\n") catch |err| {
- if (err == error.BrokenPipe) break;
- return err;
- };
- }
+ for (items) |item| {
+ stdin.writeStreamingAll(io.rt, item) catch |err| {
+ if (err == error.BrokenPipe) break;
+ return err;
+ };
+ stdin.writeStreamingAll(io.rt, "\n") catch |err| {
+ if (err == error.BrokenPipe) break;
+ return err;
+ };
}
stdin.close(io.rt);
child.stdin = null;
@@ -540,3 +533,22 @@ fn runFzfWithWalker(allocator: std.mem.Allocator, db: *hist.Db, kind: std.Io.Fil
}
}
}
+
+const testing = std.testing;
+
+test "shouldSkip: matches an exact path component" {
+ const ignored = [_][]const u8{ "node_modules", ".git" };
+ try testing.expect(shouldSkip("src/node_modules/pkg/x.js", &ignored));
+ try testing.expect(shouldSkip(".git/config", &ignored));
+ try testing.expect(shouldSkip("a/b/.git", &ignored));
+}
+
+test "shouldSkip: partial component is not a match" {
+ const ignored = [_][]const u8{"node_modules"};
+ try testing.expect(!shouldSkip("src/lib/main.zig", &ignored));
+ try testing.expect(!shouldSkip("src/node_modules_old/x", &ignored));
+}
+
+test "shouldSkip: empty pattern set never skips" {
+ try testing.expect(!shouldSkip("a/b/c", &.{}));
+}
diff --git a/src/main.zig b/src/main.zig
@@ -5,6 +5,13 @@ const fzf = @import("fzf.zig");
const hist = @import("db.zig");
const io = @import("io.zig");
+test {
+ _ = cli;
+ _ = cfg;
+ _ = fzf;
+ _ = hist;
+}
+
pub fn main(init: std.process.Init) void {
io.rt = init.io;
io.env = init.environ_map;