diff --git a/.codecov.yml b/.codecov.yml
new file mode 100644
index 00000000..ce294a3a
--- /dev/null
+++ b/.codecov.yml
@@ -0,0 +1,13 @@
+coverage:
+ range: "80..100"
+ precision: 1
+ status:
+ project:
+ default:
+ target: auto
+ threshold: 5% # allow a bit of coverage drop
+ base: auto
+ patch:
+ default:
+ target: 50% # have at least 50% of test coverage
+ threshold: 10%
diff --git a/README.md b/README.md
index a2145a2c..f8dd168b 100644
--- a/README.md
+++ b/README.md
@@ -37,7 +37,7 @@ Start using java-watch:
var directory = Path.of("tmp", "test-dir");
var watcherSetup = Watcher.watch(directory, WatchScope.PATH_AND_CHILDREN)
.withExecutor(Executors.newCachedThreadPool()) // optionally configure a custom thread pool
- .onEvent(watchEvent -> {
+ .on(watchEvent -> {
System.err.println(watchEvent);
});
diff --git a/pom.xml b/pom.xml
index 9c935e0b..1d1df8fd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -129,6 +129,7 @@
target/**
.vscode/**
.editorconfig
+ .codecov.yml
diff --git a/src/main/java/engineering/swat/watch/WatchEventListener.java b/src/main/java/engineering/swat/watch/WatchEventListener.java
new file mode 100644
index 00000000..5fa897d0
--- /dev/null
+++ b/src/main/java/engineering/swat/watch/WatchEventListener.java
@@ -0,0 +1,37 @@
+/*
+ * BSD 2-Clause License
+ *
+ * Copyright (c) 2023, Swat.engineering
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package engineering.swat.watch;
+
+/**
+ * A visit like interface that allows you to only override the functions you are interested in
+ */
+public interface WatchEventListener {
+ default void onCreated(WatchEvent ev) { }
+ default void onModified(WatchEvent ev) { }
+ default void onDeleted(WatchEvent ev) { }
+ default void onOverflow(WatchEvent ev) { }
+}
diff --git a/src/main/java/engineering/swat/watch/Watcher.java b/src/main/java/engineering/swat/watch/Watcher.java
index ab762711..303bfefc 100644
--- a/src/main/java/engineering/swat/watch/Watcher.java
+++ b/src/main/java/engineering/swat/watch/Watcher.java
@@ -54,8 +54,8 @@ public class Watcher {
private final Path path;
private volatile Executor executor = CompletableFuture::runAsync;
- private static final Consumer NULL_HANDLER = p -> {};
- private volatile Consumer eventHandler = NULL_HANDLER;
+ private static final Consumer EMPTY_HANDLER = p -> {};
+ private volatile Consumer eventHandler = EMPTY_HANDLER;
private Watcher(WatchScope scope, Path path) {
@@ -99,13 +99,44 @@ public static Watcher watch(Path path, WatchScope scope) {
* @param eventHandler a callback that handles the watch event, will be called once per event.
* @return this for optional method chaining
*/
- public Watcher onEvent(Consumer eventHandler) {
+ public Watcher on(Consumer eventHandler) {
+ if (this.eventHandler != EMPTY_HANDLER) {
+ throw new IllegalArgumentException("on handler cannot be set more than once");
+ }
this.eventHandler = eventHandler;
return this;
}
/**
- * Optionally configure the executor in which the {@link #onEvent(Consumer)} callbacks are scheduled.
+ * Convenience variant of {@link #on(Consumer)}, which allows you to only respond to certain events
+ */
+ public Watcher on(WatchEventListener listener) {
+ if (this.eventHandler != EMPTY_HANDLER) {
+ throw new IllegalArgumentException("on handler cannot be set more than once");
+ }
+ this.eventHandler = ev -> {
+ switch (ev.getKind()) {
+ case CREATED:
+ listener.onCreated(ev);
+ break;
+ case DELETED:
+ listener.onDeleted(ev);
+ break;
+ case MODIFIED:
+ listener.onModified(ev);
+ break;
+ case OVERFLOW:
+ listener.onOverflow(ev);
+ break;
+ default:
+ throw new IllegalArgumentException("Unexpected kind: " + ev.getKind());
+ }
+ };
+ return this;
+ }
+
+ /**
+ * Optionally configure the executor in which the {@link #on(Consumer)} callbacks are scheduled.
* If not defined, every task will be scheduled on the {@link java.util.concurrent.ForkJoinPool#commonPool()}.
* @param callbackHandler worker pool to use
* @return this for optional method chaining
@@ -119,10 +150,10 @@ public Watcher withExecutor(Executor callbackHandler) {
* Start watch the path for events.
* @return a subscription for the watch, when closed, new events will stop being registered to the worker pool.
* @throws IOException in case the starting of the watcher caused an underlying IO exception
- * @throws IllegalStateException the watchers is not configured correctly (for example, missing {@link #onEvent(Consumer)}, or a watcher is started twice)
+ * @throws IllegalStateException the watchers is not configured correctly (for example, missing {@link #on(Consumer)}, or a watcher is started twice)
*/
public ActiveWatch start() throws IOException {
- if (this.eventHandler == NULL_HANDLER) {
+ if (this.eventHandler == EMPTY_HANDLER) {
throw new IllegalStateException("There is no onEvent handler defined");
}
switch (scope) {
diff --git a/src/test/java/engineering/swat/watch/APIErrorsTests.java b/src/test/java/engineering/swat/watch/APIErrorsTests.java
new file mode 100644
index 00000000..ffa5b44c
--- /dev/null
+++ b/src/test/java/engineering/swat/watch/APIErrorsTests.java
@@ -0,0 +1,109 @@
+/*
+ * BSD 2-Clause License
+ *
+ * Copyright (c) 2023, Swat.engineering
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package engineering.swat.watch;
+
+import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
+
+import java.io.IOException;
+import java.nio.file.Files;
+
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class APIErrorsTests {
+
+ private TestDirectory testDir;
+
+ @BeforeEach
+ void setup() throws IOException {
+ testDir = new TestDirectory();
+ }
+
+ @AfterEach
+ void cleanup() {
+ if (testDir != null) {
+ testDir.close();
+ }
+ }
+
+ @BeforeAll
+ static void setupEverything() {
+ Awaitility.setDefaultTimeout(TestHelper.NORMAL_WAIT);
+ }
+
+ @Test
+ void noDuplicateEvents() {
+ assertThrowsExactly(IllegalArgumentException.class, () ->
+ Watcher
+ .watch(testDir.getTestDirectory(), WatchScope.PATH_AND_CHILDREN)
+ .on(System.out::println)
+ .on(System.err::println)
+ );
+ }
+
+ @Test
+ void onlyDirectoryWatchingOnDirectories() {
+ assertThrowsExactly(IllegalArgumentException.class, () ->
+ Watcher
+ .watch(testDir.getTestFiles().get(0), WatchScope.PATH_AND_CHILDREN)
+ );
+ }
+
+ @Test
+ void doNotStartWithoutEventHandler() {
+ assertThrowsExactly(IllegalStateException.class, () ->
+ Watcher
+ .watch(testDir.getTestDirectory(), WatchScope.PATH_AND_CHILDREN)
+ .start()
+ );
+ }
+
+ @Test
+ void noRelativePaths() {
+ var relativePath = testDir.getTestDirectory().resolve("d1").relativize(testDir.getTestDirectory());
+
+ assertThrowsExactly(IllegalArgumentException.class, () ->
+ Watcher
+ .watch(relativePath, WatchScope.PATH_AND_CHILDREN)
+ .start()
+ );
+ }
+
+ @Test
+ void nonExistingDirectory() throws IOException {
+ var nonExistingDir = testDir.getTestDirectory().resolve("testd1");
+ Files.createDirectory(nonExistingDir);
+ var w = Watcher.watch(nonExistingDir, WatchScope.PATH_AND_CHILDREN);
+ Files.delete(nonExistingDir);
+ assertThrowsExactly(IllegalStateException.class, w::start);
+ }
+
+
+}
diff --git a/src/test/java/engineering/swat/watch/DeleteLockTests.java b/src/test/java/engineering/swat/watch/DeleteLockTests.java
index 08ed96f6..1996515c 100644
--- a/src/test/java/engineering/swat/watch/DeleteLockTests.java
+++ b/src/test/java/engineering/swat/watch/DeleteLockTests.java
@@ -77,7 +77,7 @@ private static void recursiveDelete(Path target) throws IOException {
}
private void deleteAndVerify(Path target, WatchScope scope) throws IOException {
- try (var watch = Watcher.watch(target, scope).onEvent(ev -> {}).start()) {
+ try (var watch = Watcher.watch(target, scope).on(ev -> {}).start()) {
recursiveDelete(target);
assertFalse(Files.exists(target), "The file/directory shouldn't exist anymore");
}
diff --git a/src/test/java/engineering/swat/watch/RecursiveWatchTests.java b/src/test/java/engineering/swat/watch/RecursiveWatchTests.java
index bcc2a465..655a2b8d 100644
--- a/src/test/java/engineering/swat/watch/RecursiveWatchTests.java
+++ b/src/test/java/engineering/swat/watch/RecursiveWatchTests.java
@@ -74,7 +74,7 @@ void newDirectoryWithFilesChangesDetected() throws IOException {
var created = new AtomicBoolean(false);
var changed = new AtomicBoolean(false);
var watchConfig = Watcher.watch(testDir.getTestDirectory(), WatchScope.PATH_AND_ALL_DESCENDANTS)
- .onEvent(ev -> {
+ .on(ev -> {
logger.debug("Event received: {}", ev);
if (ev.calculateFullPath().equals(target.get())) {
switch (ev.getKind()) {
@@ -106,7 +106,7 @@ void correctRelativePathIsReported() throws IOException {
Path relative = Path.of("a","b", "c", "d.txt");
var seen = new AtomicBoolean(false);
var watcher = Watcher.watch(testDir.getTestDirectory(), WatchScope.PATH_AND_ALL_DESCENDANTS)
- .onEvent(ev -> {
+ .on(ev -> {
logger.debug("Seen event: {}", ev);
if (ev.getRelativePath().equals(relative)) {
seen.set(true);
@@ -131,7 +131,7 @@ void deleteOfFileInDirectoryShouldBeVisible() throws IOException, InterruptedExc
.orElseThrow();
var seen = new AtomicBoolean(false);
var watchConfig = Watcher.watch(target.getParent(), WatchScope.PATH_AND_CHILDREN)
- .onEvent(ev -> {
+ .on(ev -> {
if (ev.getKind() == Kind.DELETED && ev.calculateFullPath().equals(target)) {
seen.set(true);
}
diff --git a/src/test/java/engineering/swat/watch/SingleDirectoryTests.java b/src/test/java/engineering/swat/watch/SingleDirectoryTests.java
index 4337c47d..63ec141b 100644
--- a/src/test/java/engineering/swat/watch/SingleDirectoryTests.java
+++ b/src/test/java/engineering/swat/watch/SingleDirectoryTests.java
@@ -68,7 +68,7 @@ void deleteOfFileInDirectoryShouldBeVisible() throws IOException, InterruptedExc
var seenDelete = new AtomicBoolean(false);
var seenCreate = new AtomicBoolean(false);
var watchConfig = Watcher.watch(target.getParent(), WatchScope.PATH_AND_CHILDREN)
- .onEvent(ev -> {
+ .on(ev -> {
if (ev.getKind() == Kind.DELETED && ev.calculateFullPath().equals(target)) {
seenDelete.set(true);
}
@@ -89,4 +89,35 @@ void deleteOfFileInDirectoryShouldBeVisible() throws IOException, InterruptedExc
.untilTrue(seenCreate);
}
}
+
+ @Test
+ void alternativeAPITest() throws IOException, InterruptedException {
+ var target = testDir.getTestFiles().get(0);
+ var seenDelete = new AtomicBoolean(false);
+ var seenCreate = new AtomicBoolean(false);
+ var watchConfig = Watcher.watch(target.getParent(), WatchScope.PATH_AND_CHILDREN)
+ .on(new WatchEventListener() {
+ @Override
+ public void onCreated(WatchEvent ev) {
+ seenCreate.set(true);
+ }
+
+ @Override
+ public void onDeleted(WatchEvent ev) {
+ seenDelete.set(true);
+ }
+ });
+ try (var watch = watchConfig.start()) {
+
+ // Delete the file
+ Files.delete(target);
+ await("File deletion should generate delete event")
+ .untilTrue(seenDelete);
+
+ // Re-create it again
+ Files.writeString(target, "Hello World");
+ await("File creation should generate create event")
+ .untilTrue(seenCreate);
+ }
+ }
}
diff --git a/src/test/java/engineering/swat/watch/SingleFileTests.java b/src/test/java/engineering/swat/watch/SingleFileTests.java
index f176df09..71a2ab93 100644
--- a/src/test/java/engineering/swat/watch/SingleFileTests.java
+++ b/src/test/java/engineering/swat/watch/SingleFileTests.java
@@ -66,7 +66,7 @@ void singleFileShouldNotTriggerOnOtherFilesInSameDir() throws IOException, Inter
var seen = new AtomicBoolean(false);
var others = new AtomicBoolean(false);
var watchConfig = Watcher.watch(target, WatchScope.PATH_ONLY)
- .onEvent(ev -> {
+ .on(ev -> {
if (ev.calculateFullPath().equals(target)) {
seen.set(true);
}
@@ -95,7 +95,7 @@ void singleFileThatMonitorsOnlyADirectory() throws IOException, InterruptedExcep
var seen = new AtomicBoolean(false);
var others = new AtomicBoolean(false);
var watchConfig = Watcher.watch(target, WatchScope.PATH_ONLY)
- .onEvent(ev -> {
+ .on(ev -> {
if (ev.calculateFullPath().equals(target)) {
seen.set(true);
}
diff --git a/src/test/java/engineering/swat/watch/SmokeTests.java b/src/test/java/engineering/swat/watch/SmokeTests.java
index 2325e416..29cfe506 100644
--- a/src/test/java/engineering/swat/watch/SmokeTests.java
+++ b/src/test/java/engineering/swat/watch/SmokeTests.java
@@ -67,7 +67,7 @@ void watchDirectory() throws IOException, InterruptedException {
var changed = new AtomicBoolean(false);
var target = testDir.getTestFiles().get(0);
var watchConfig = Watcher.watch(testDir.getTestDirectory(), WatchScope.PATH_AND_CHILDREN)
- .onEvent(ev -> {if (ev.getKind() == MODIFIED && ev.calculateFullPath().equals(target)) { changed.set(true); }})
+ .on(ev -> {if (ev.getKind() == MODIFIED && ev.calculateFullPath().equals(target)) { changed.set(true); }})
;
try (var activeWatch = watchConfig.start() ) {
@@ -84,7 +84,7 @@ void watchRecursiveDirectory() throws IOException, InterruptedException {
.findFirst()
.orElseThrow();
var watchConfig = Watcher.watch(testDir.getTestDirectory(), WatchScope.PATH_AND_ALL_DESCENDANTS)
- .onEvent(ev -> { if (ev.getKind() == MODIFIED && ev.calculateFullPath().equals(target)) { changed.set(true);}})
+ .on(ev -> { if (ev.getKind() == MODIFIED && ev.calculateFullPath().equals(target)) { changed.set(true);}})
;
try (var activeWatch = watchConfig.start() ) {
@@ -102,7 +102,7 @@ void watchSingleFile() throws IOException {
.orElseThrow();
var watchConfig = Watcher.watch(target, WatchScope.PATH_ONLY)
- .onEvent(ev -> {
+ .on(ev -> {
if (ev.calculateFullPath().equals(target)) {
changed.set(true);
}
diff --git a/src/test/java/engineering/swat/watch/TortureTests.java b/src/test/java/engineering/swat/watch/TortureTests.java
index 2a87b930..5704d864 100644
--- a/src/test/java/engineering/swat/watch/TortureTests.java
+++ b/src/test/java/engineering/swat/watch/TortureTests.java
@@ -148,7 +148,7 @@ void pressureOnFSShouldNotMissNewFilesAnything() throws InterruptedException, IO
var seenCreates = ConcurrentHashMap.newKeySet();
var watchConfig = Watcher.watch(testDir.getTestDirectory(), WatchScope.PATH_AND_ALL_DESCENDANTS)
.withExecutor(pool)
- .onEvent(ev -> {
+ .on(ev -> {
var fullPath = ev.calculateFullPath();
switch (ev.getKind()) {
case CREATED:
@@ -212,7 +212,7 @@ void manyRegistrationsForSamePath() throws InterruptedException, IOException {
try {
var watcher = Watcher
.watch(testDir.getTestDirectory(), WatchScope.PATH_AND_CHILDREN)
- .onEvent(e -> seen.add(e.calculateFullPath()));
+ .on(e -> seen.add(e.calculateFullPath()));
startRegistering.acquire();
try (var c = watcher.start()) {
startedWatching.release();
@@ -279,7 +279,7 @@ void manyRegisterAndUnregisterSameTime() throws InterruptedException, IOExceptio
for (int k = 0; k < 1000; k++) {
var watcher = Watcher
.watch(testDir.getTestDirectory(), WatchScope.PATH_AND_CHILDREN)
- .onEvent(e -> {
+ .on(e -> {
if (e.calculateFullPath().equals(target)) {
seen.add(id);
}
@@ -345,7 +345,7 @@ void pressureOnFSShouldNotMissDeletes() throws InterruptedException, IOException
final var happened = new Semaphore(0);
var watchConfig = Watcher.watch(testDir.getTestDirectory(), WatchScope.PATH_AND_ALL_DESCENDANTS)
.withExecutor(pool)
- .onEvent(ev -> {
+ .on(ev -> {
events.getAndIncrement();
happened.release();
var fullPath = ev.calculateFullPath();