From bfd1125449bc78532d231b6f75e3dd5e0d2c031a Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Sun, 29 Mar 2026 19:44:45 +0200 Subject: [PATCH] refactor: add move in/out tests --- src/nightwatch_test.zig | 158 ++++++++++++++++++++++++++++++++++++++++ test_manual.ps1 | 50 +++++++++++-- test_manual.sh | 42 ++++++++++- 3 files changed, 243 insertions(+), 7 deletions(-) diff --git a/src/nightwatch_test.zig b/src/nightwatch_test.zig index e94e7af..d2e83dd 100644 --- a/src/nightwatch_test.zig +++ b/src/nightwatch_test.zig @@ -643,6 +643,146 @@ fn testRenameThenModify(comptime Watcher: type, allocator: std.mem.Allocator) !v try std.testing.expect(rename_idx < modify_idx); } +fn testMoveOutFile(comptime Watcher: type, allocator: std.mem.Allocator) !void { + const TH = MakeTestHandler(Watcher); + + const watched = try makeTempDir(allocator); + defer { + removeTempDir(watched); + allocator.free(watched); + } + const other = try makeTempDir(allocator); + defer { + removeTempDir(other); + allocator.free(other); + } + + const th = try TH.init(allocator); + defer th.deinit(); + + var watcher = try Watcher.init(allocator, &th.handler); + defer watcher.deinit(); + try watcher.watch(watched); + + const src_path = try std.fs.path.join(allocator, &.{ watched, "moveme.txt" }); + defer allocator.free(src_path); + const dst_path = try std.fs.path.join(allocator, &.{ other, "moveme.txt" }); + defer allocator.free(dst_path); + + { + const f = try std.fs.createFileAbsolute(src_path, .{}); + f.close(); + } + try drainEvents(Watcher, &watcher); + + try std.fs.renameAbsolute(src_path, dst_path); + try drainEvents(Watcher, &watcher); + + // File moved out of the watched tree: appears as deleted (INotify, Windows) + // or renamed (kqueue, which holds a vnode fd and sees NOTE_RENAME). + const src_gone = th.hasChange(src_path, .deleted, .file) or + th.hasChange(src_path, .renamed, .file); + try std.testing.expect(src_gone); + // No event for the destination - it is in an unwatched directory. + try std.testing.expect(!th.hasChange(dst_path, .created, .file)); +} + +fn testMoveInFile(comptime Watcher: type, allocator: std.mem.Allocator) !void { + const TH = MakeTestHandler(Watcher); + + const watched = try makeTempDir(allocator); + defer { + removeTempDir(watched); + allocator.free(watched); + } + const other = try makeTempDir(allocator); + defer { + removeTempDir(other); + allocator.free(other); + } + + const th = try TH.init(allocator); + defer th.deinit(); + + var watcher = try Watcher.init(allocator, &th.handler); + defer watcher.deinit(); + try watcher.watch(watched); + + const src_path = try std.fs.path.join(allocator, &.{ other, "moveme.txt" }); + defer allocator.free(src_path); + const dst_path = try std.fs.path.join(allocator, &.{ watched, "moveme.txt" }); + defer allocator.free(dst_path); + + { + const f = try std.fs.createFileAbsolute(src_path, .{}); + f.close(); + } + + try std.fs.renameAbsolute(src_path, dst_path); + try drainEvents(Watcher, &watcher); + + // File moved into the watched tree: appears as created. + try std.testing.expect(th.hasChange(dst_path, .created, .file)); + // No event for the source - it was in an unwatched directory. + try std.testing.expect(!th.hasChange(src_path, .deleted, .file)); +} + +fn testMoveInSubdir(comptime Watcher: type, allocator: std.mem.Allocator) !void { + const TH = MakeTestHandler(Watcher); + + const watched = try makeTempDir(allocator); + defer { + removeTempDir(watched); + allocator.free(watched); + } + const other = try makeTempDir(allocator); + defer { + removeTempDir(other); + allocator.free(other); + } + + const th = try TH.init(allocator); + defer th.deinit(); + + var watcher = try Watcher.init(allocator, &th.handler); + defer watcher.deinit(); + try watcher.watch(watched); + + const src_sub = try std.fs.path.join(allocator, &.{ other, "sub" }); + defer allocator.free(src_sub); + const dst_sub = try std.fs.path.join(allocator, &.{ watched, "sub" }); + defer allocator.free(dst_sub); + const src_file = try std.fs.path.join(allocator, &.{ src_sub, "f.txt" }); + defer allocator.free(src_file); + const dst_file = try std.fs.path.join(allocator, &.{ dst_sub, "f.txt" }); + defer allocator.free(dst_file); + + // Create subdir with a file in the unwatched root, then move it in. + try std.fs.makeDirAbsolute(src_sub); + { + const f = try std.fs.createFileAbsolute(src_file, .{}); + f.close(); + } + try std.fs.renameAbsolute(src_sub, dst_sub); + try drainEvents(Watcher, &watcher); + + try std.testing.expect(th.hasChange(dst_sub, .created, .dir)); + + // Delete the file inside the moved-in subdir. + try std.fs.deleteFileAbsolute(dst_file); + try drainEvents(Watcher, &watcher); + + // Object type must be .file, not .unknown - backend must have seeded + // the path_types cache when the subdir was moved in. + try std.testing.expect(th.hasChange(dst_file, .deleted, .file)); + + // Delete the now-empty subdir. + try std.fs.deleteDirAbsolute(dst_sub); + try drainEvents(Watcher, &watcher); + + try std.testing.expect(th.hasChange(dst_sub, .deleted, .dir)); +} + // --------------------------------------------------------------------------- // Test blocks - each runs its case across all available variants. // --------------------------------------------------------------------------- @@ -724,3 +864,21 @@ test "rename-then-modify: rename event precedes the subsequent modify event" { try testRenameThenModify(nw.Create(variant), std.testing.allocator); } } + +test "moving a file out of the watched tree appears as deleted or renamed" { + inline for (comptime std.enums.values(nw.Variant)) |variant| { + try testMoveOutFile(nw.Create(variant), std.testing.allocator); + } +} + +test "moving a file into the watched tree appears as created" { + inline for (comptime std.enums.values(nw.Variant)) |variant| { + try testMoveInFile(nw.Create(variant), std.testing.allocator); + } +} + +test "moving a subdir into the watched tree: contents can be deleted with correct types" { + inline for (comptime std.enums.values(nw.Variant)) |variant| { + try testMoveInSubdir(nw.Create(variant), std.testing.allocator); + } +} diff --git a/test_manual.ps1 b/test_manual.ps1 index 672bc6b..8e8ced7 100644 --- a/test_manual.ps1 +++ b/test_manual.ps1 @@ -14,12 +14,14 @@ if (-not (Test-Path $NW)) { exit 1 } -$TESTDIR = Join-Path $env:TEMP "nightwatch_manual_$PID" -$TESTDIR2 = Join-Path $env:TEMP "nightwatch_manual2_$PID" -New-Item -ItemType Directory -Path $TESTDIR | Out-Null -New-Item -ItemType Directory -Path $TESTDIR2 | Out-Null +$TESTDIR = Join-Path $env:TEMP "nightwatch_manual_$PID" +$TESTDIR2 = Join-Path $env:TEMP "nightwatch_manual2_$PID" +$UNWATCHED = Join-Path $env:TEMP "nightwatch_unwatched_$PID" +New-Item -ItemType Directory -Path $TESTDIR | Out-Null +New-Item -ItemType Directory -Path $TESTDIR2 | Out-Null +New-Item -ItemType Directory -Path $UNWATCHED | Out-Null -Write-Host "--- watching $TESTDIR and $TESTDIR2 ---" +Write-Host "--- watching $TESTDIR and $TESTDIR2 (unwatched: $UNWATCHED) ---" Write-Host "--- starting nightwatch (Ctrl-C to stop early) ---" Write-Host "" @@ -104,9 +106,47 @@ Write-Host "[op] rename subA: dir1 -> dir2 (subdir across roots)" Move-Item -Path "$TESTDIR\subA" -Destination "$TESTDIR2\subA2" Start-Sleep -Milliseconds 500 +Write-Host "" +Write-Host "# move in/out (one side unwatched)" +Write-Host "" + +Write-Host "[op] touch outfile.txt in dir1" +New-Item -ItemType File -Path "$TESTDIR\outfile.txt" | Out-Null +Start-Sleep -Milliseconds 400 + +Write-Host "[op] move outfile.txt: dir1 -> unwatched (move out)" +Move-Item -Path "$TESTDIR\outfile.txt" -Destination "$UNWATCHED\outfile.txt" +Start-Sleep -Milliseconds 400 + +Write-Host "[op] move outfile.txt: unwatched -> dir1 (move in)" +Move-Item -Path "$UNWATCHED\outfile.txt" -Destination "$TESTDIR\outfile.txt" +Start-Sleep -Milliseconds 400 + +Write-Host "[op] delete outfile.txt" +Remove-Item -Path "$TESTDIR\outfile.txt" +Start-Sleep -Milliseconds 400 + +Write-Host "[op] mkdir unwatched\subdir with a file" +New-Item -ItemType Directory -Path "$UNWATCHED\subdir" | Out-Null +New-Item -ItemType File -Path "$UNWATCHED\subdir\inside.txt" | Out-Null +Start-Sleep -Milliseconds 400 + +Write-Host "[op] move unwatched\subdir -> dir1\subdir (move subdir in)" +Move-Item -Path "$UNWATCHED\subdir" -Destination "$TESTDIR\subdir" +Start-Sleep -Milliseconds 400 + +Write-Host "[op] delete dir1\subdir\inside.txt" +Remove-Item -Path "$TESTDIR\subdir\inside.txt" +Start-Sleep -Milliseconds 400 + +Write-Host "[op] rmdir dir1\subdir" +Remove-Item -Path "$TESTDIR\subdir" +Start-Sleep -Milliseconds 500 + Write-Host "" Write-Host "--- done, stopping nightwatch ---" Stop-Process -Id $proc.Id -ErrorAction SilentlyContinue $proc.WaitForExit() Remove-Item -Recurse -Force -Path $TESTDIR Remove-Item -Recurse -Force -Path $TESTDIR2 +Remove-Item -Recurse -Force -Path $UNWATCHED diff --git a/test_manual.sh b/test_manual.sh index 3680d14..de90ef8 100755 --- a/test_manual.sh +++ b/test_manual.sh @@ -15,7 +15,8 @@ fi TESTDIR=$(mktemp -d) TESTDIR2=$(mktemp -d) -echo "--- watching $TESTDIR and $TESTDIR2 ---" +UNWATCHED=$(mktemp -d) +echo "--- watching $TESTDIR and $TESTDIR2 (unwatched: $UNWATCHED) ---" echo "--- starting nightwatch (Ctrl-C to stop early) ---" echo "" @@ -101,8 +102,45 @@ echo "[op] rename subA: dir1 -> dir2 (subdir across roots)" mv "$TESTDIR/subA" "$TESTDIR2/subA2" sleep 0.5 +echo "" +echo "# move in/out (one side unwatched)" +echo "" + +echo "[op] touch outfile.txt in dir1" +touch "$TESTDIR/outfile.txt" +sleep 0.4 + +echo "[op] move outfile.txt: dir1 -> unwatched (move out)" +mv "$TESTDIR/outfile.txt" "$UNWATCHED/outfile.txt" +sleep 0.4 + +echo "[op] move outfile.txt: unwatched -> dir1 (move in)" +mv "$UNWATCHED/outfile.txt" "$TESTDIR/outfile.txt" +sleep 0.4 + +echo "[op] delete outfile.txt" +rm "$TESTDIR/outfile.txt" +sleep 0.4 + +echo "[op] mkdir unwatched/subdir with a file" +mkdir "$UNWATCHED/subdir" +touch "$UNWATCHED/subdir/inside.txt" +sleep 0.4 + +echo "[op] move unwatched/subdir -> dir1/subdir (move subdir in)" +mv "$UNWATCHED/subdir" "$TESTDIR/subdir" +sleep 0.4 + +echo "[op] delete dir1/subdir/inside.txt" +rm "$TESTDIR/subdir/inside.txt" +sleep 0.4 + +echo "[op] rmdir dir1/subdir" +rmdir "$TESTDIR/subdir" +sleep 0.5 + echo "" echo "--- done, stopping nightwatch ---" kill $NW_PID 2>/dev/null wait $NW_PID 2>/dev/null -rm -rf "$TESTDIR" "$TESTDIR2" +rm -rf "$TESTDIR" "$TESTDIR2" "$UNWATCHED"