diff --git a/src/main/java/engineering/swat/watch/Watcher.java b/src/main/java/engineering/swat/watch/Watcher.java index d639fa19..ff84df84 100644 --- a/src/main/java/engineering/swat/watch/Watcher.java +++ b/src/main/java/engineering/swat/watch/Watcher.java @@ -198,7 +198,7 @@ public ActiveWatch start() throws IOException { } } case PATH_ONLY: { - var result = new JDKFileWatch(path, executor, eventHandler); + var result = new JDKFileWatch(path, executor, h); result.open(); return result; } diff --git a/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java b/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java index 520d4a16..852cefea 100644 --- a/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java +++ b/src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java @@ -57,6 +57,10 @@ public JDKFileWatch(Path file, Executor exec, BiConsumer { + if (e.getKind() == WatchEvent.Kind.OVERFLOW) { + var overflow = new WatchEvent(WatchEvent.Kind.OVERFLOW, file); + eventHandler.accept(w, overflow); + } if (fileName.equals(e.getRelativePath())) { eventHandler.accept(w, e); } diff --git a/src/test/java/engineering/swat/watch/SingleFileTests.java b/src/test/java/engineering/swat/watch/SingleFileTests.java index 206bc9bf..19def650 100644 --- a/src/test/java/engineering/swat/watch/SingleFileTests.java +++ b/src/test/java/engineering/swat/watch/SingleFileTests.java @@ -26,13 +26,25 @@ */ package engineering.swat.watch; +import static engineering.swat.watch.WatchEvent.Kind.CREATED; +import static engineering.swat.watch.WatchEvent.Kind.DELETED; +import static engineering.swat.watch.WatchEvent.Kind.MODIFIED; +import static engineering.swat.watch.WatchEvent.Kind.OVERFLOW; import static org.awaitility.Awaitility.await; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.attribute.FileTime; import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Stream; import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterEach; @@ -40,6 +52,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import engineering.swat.watch.impl.EventHandlingWatch; + class SingleFileTests { private TestDirectory testDir; @@ -117,4 +131,72 @@ void singleFileThatMonitorsOnlyADirectory() throws IOException, InterruptedExcep .untilTrue(seen); } } + + @Test + void noRescanOnOverflow() throws IOException, InterruptedException { + var bookkeeper = new Bookkeeper(); + try (var watch = startWatchAndTriggerOverflow(OnOverflow.NONE, bookkeeper)) { + Thread.sleep(TestHelper.SHORT_WAIT.toMillis()); + + await("Overflow shouldn't trigger created, modified, or deleted events") + .until(() -> bookkeeper.fullPaths(CREATED, MODIFIED, DELETED).count() == 0); + await("Overflow should be visible to user-defined event handler") + .until(() -> bookkeeper.fullPaths(OVERFLOW).count() == 1); + } + } + + @Test + void memorylessRescanOnOverflow() throws IOException, InterruptedException { + var bookkeeper = new Bookkeeper(); + try (var watch = startWatchAndTriggerOverflow(OnOverflow.ALL, bookkeeper)) { + Thread.sleep(TestHelper.SHORT_WAIT.toMillis()); + + var isFile = Predicate.isEqual(watch.getPath()); + var isNotFile = Predicate.not(isFile); + + await("Overflow should trigger created event for `file`") + .until(() -> bookkeeper.fullPaths(CREATED).filter(isFile).count() == 1); + await("Overflow shouldn't trigger created events for other files") + .until(() -> bookkeeper.fullPaths(CREATED).filter(isNotFile).count() == 0); + await("Overflow shouldn't trigger modified events (`file` is empty)") + .until(() -> bookkeeper.fullPaths(MODIFIED).count() == 0); + await("Overflow shouldn't trigger deleted events") + .until(() -> bookkeeper.fullPaths(DELETED).count() == 0); + await("Overflow should be visible to user-defined event handler") + .until(() -> bookkeeper.fullPaths(OVERFLOW).count() == 1); + } + } + + private ActiveWatch startWatchAndTriggerOverflow(OnOverflow whichFiles, Bookkeeper bookkeeper) throws IOException { + var parent = testDir.getTestDirectory(); + var file = parent.resolve("a.txt"); + + var watch = Watcher + .watch(file, WatchScope.PATH_ONLY) + .approximate(whichFiles) + .on(bookkeeper) + .start(); + + var overflow = new WatchEvent(WatchEvent.Kind.OVERFLOW, parent); + ((EventHandlingWatch) watch).handleEvent(overflow); + return watch; + } + + private static class Bookkeeper implements Consumer { + private final List events = Collections.synchronizedList(new ArrayList<>()); + + public Stream events(WatchEvent.Kind... kinds) { + var list = Arrays.asList(kinds.length == 0 ? WatchEvent.Kind.values() : kinds); + return events.stream().filter(e -> list.contains(e.getKind())); + } + + public Stream fullPaths(WatchEvent.Kind... kinds) { + return events(kinds).map(WatchEvent::calculateFullPath); + } + + @Override + public void accept(WatchEvent e) { + events.add(e); + } + } }