diff --git a/src/backend/FSEvents.zig b/src/backend/FSEvents.zig index 3726d97..4624f2c 100644 --- a/src/backend/FSEvents.zig +++ b/src/backend/FSEvents.zig @@ -7,6 +7,8 @@ const ObjectType = types.ObjectType; pub const watches_recursively = true; // FSEventStreamCreate watches the entire subtree pub const detects_file_modifications = true; pub const emits_close_events = false; +pub const emits_rename_for_files = true; +pub const emits_rename_for_dirs = true; handler: *Handler, stream: ?*anyopaque, // FSEventStreamRef diff --git a/src/backend/INotify.zig b/src/backend/INotify.zig index 67d7242..7ec8050 100644 --- a/src/backend/INotify.zig +++ b/src/backend/INotify.zig @@ -36,6 +36,8 @@ pub fn Create(comptime variant: InterfaceType) type { pub const watches_recursively = false; pub const detects_file_modifications = true; pub const emits_close_events = true; + pub const emits_rename_for_files = true; + pub const emits_rename_for_dirs = true; pub const polling = variant == .polling; const WatchEntry = struct { path: []u8, is_dir: bool }; diff --git a/src/backend/KQueue.zig b/src/backend/KQueue.zig index 1bf325d..aeee5fd 100644 --- a/src/backend/KQueue.zig +++ b/src/backend/KQueue.zig @@ -7,6 +7,8 @@ const ObjectType = types.ObjectType; pub const watches_recursively = false; pub const detects_file_modifications = true; pub const emits_close_events = false; +pub const emits_rename_for_files = false; +pub const emits_rename_for_dirs = true; handler: *Handler, kq: std.posix.fd_t, diff --git a/src/backend/KQueueDir.zig b/src/backend/KQueueDir.zig index fd27548..0cf816f 100644 --- a/src/backend/KQueueDir.zig +++ b/src/backend/KQueueDir.zig @@ -7,6 +7,8 @@ const ObjectType = types.ObjectType; pub const watches_recursively = false; pub const detects_file_modifications = false; pub const emits_close_events = false; +pub const emits_rename_for_files = false; +pub const emits_rename_for_dirs = true; pub const WatchEntry = struct { fd: std.posix.fd_t, is_file: bool }; handler: *Handler, diff --git a/src/backend/Windows.zig b/src/backend/Windows.zig index ad477b9..050bf9c 100644 --- a/src/backend/Windows.zig +++ b/src/backend/Windows.zig @@ -7,6 +7,8 @@ const ObjectType = types.ObjectType; pub const watches_recursively = true; // ReadDirectoryChangesW with bWatchSubtree=1 pub const detects_file_modifications = true; pub const emits_close_events = false; +pub const emits_rename_for_files = true; +pub const emits_rename_for_dirs = true; const windows = std.os.windows; diff --git a/src/nightwatch.zig b/src/nightwatch.zig index 4b891a8..7fa107a 100644 --- a/src/nightwatch.zig +++ b/src/nightwatch.zig @@ -115,6 +115,8 @@ pub fn Create(comptime variant: Variant) type { /// modifications. pub const detects_file_modifications = Backend.detects_file_modifications; pub const emits_close_events = Backend.emits_close_events; + pub const emits_rename_for_files = Backend.emits_rename_for_files; + pub const emits_rename_for_dirs = Backend.emits_rename_for_dirs; /// Create a new watcher. /// diff --git a/src/nightwatch_test.zig b/src/nightwatch_test.zig index 323c56c..8230599 100644 --- a/src/nightwatch_test.zig +++ b/src/nightwatch_test.zig @@ -394,15 +394,56 @@ fn testRenameFile(comptime Watcher: type, allocator: std.mem.Allocator) !void { try std.fs.renameAbsolute(src_path, dst_path); try drainEvents(Watcher, &watcher); - if (builtin.os.tag == .linux) { - try std.testing.expect(th.hasRename(src_path, dst_path)); + if (comptime Watcher.emits_rename_for_files) { + // INotify delivers a paired atomic rename callback; FSEvents/Windows + // deliver individual .renamed change events per path. + const has_rename = th.hasRename(src_path, dst_path) or + th.hasChange(src_path, .renamed, .file); + try std.testing.expect(has_rename); } else { - const has_old = th.hasChange(src_path, .renamed, .file) or th.hasChange(src_path, .deleted, .file); - const has_new = th.hasChange(dst_path, .renamed, .file) or th.hasChange(dst_path, .created, .file); - try std.testing.expect(has_old or has_new); + // KQueue/KQueueDir: file rename appears as delete + create. + try std.testing.expect(th.hasChange(src_path, .deleted, .file)); + try std.testing.expect(th.hasChange(dst_path, .created, .file)); } } +fn testRenameDir(comptime Watcher: type, allocator: std.mem.Allocator) !void { + if (comptime !Watcher.emits_rename_for_dirs) return error.SkipZigTest; + + const TH = MakeTestHandler(Watcher); + + const tmp = try makeTempDir(allocator); + defer { + removeTempDir(tmp); + allocator.free(tmp); + } + + const th = try TH.init(allocator); + defer th.deinit(); + + const src_path = try std.fs.path.join(allocator, &.{ tmp, "before" }); + defer allocator.free(src_path); + const dst_path = try std.fs.path.join(allocator, &.{ tmp, "after" }); + defer allocator.free(dst_path); + + try std.fs.makeDirAbsolute(src_path); + + var watcher = try Watcher.init(allocator, &th.handler); + defer watcher.deinit(); + try watcher.watch(tmp); + + try std.fs.renameAbsolute(src_path, dst_path); + try drainEvents(Watcher, &watcher); + + // All backends with emits_rename_for_dirs=true deliver at least a rename + // event for the source path. INotify delivers a paired rename callback; + // KQueue/KQueueDir deliver change(.renamed, .dir) for the old path only; + // FSEvents/Windows deliver change(.renamed, .dir) for both paths. + const has_rename = th.hasRename(src_path, dst_path) or + th.hasChange(src_path, .renamed, .dir); + try std.testing.expect(has_rename); +} + fn testUnwatchedDir(comptime Watcher: type, allocator: std.mem.Allocator) !void { const TH = MakeTestHandler(Watcher); @@ -644,6 +685,12 @@ test "renaming a file is reported correctly per-platform" { } } +test "renaming a directory emits a rename event" { + inline for (comptime std.enums.values(nw.Variant)) |variant| { + try testRenameDir(nw.Create(variant), std.testing.allocator); + } +} + test "an unwatched directory produces no events" { inline for (comptime std.enums.values(nw.Variant)) |variant| { try testUnwatchedDir(nw.Create(variant), std.testing.allocator);