Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .codecov.yml
Original file line number Diff line number Diff line change
@@ -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%
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
<exclude>target/**</exclude>
<exclude>.vscode/**</exclude>
<exclude>.editorconfig</exclude>
<exclude>.codecov.yml</exclude>
</excludes>
</licenseSet>
</licenseSets>
Expand Down
37 changes: 37 additions & 0 deletions src/main/java/engineering/swat/watch/WatchEventListener.java
Original file line number Diff line number Diff line change
@@ -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) { }

Check warning on line 33 in src/main/java/engineering/swat/watch/WatchEventListener.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/engineering/swat/watch/WatchEventListener.java#L33

Added line #L33 was not covered by tests
default void onModified(WatchEvent ev) { }
default void onDeleted(WatchEvent ev) { }
default void onOverflow(WatchEvent ev) { }

Check warning on line 36 in src/main/java/engineering/swat/watch/WatchEventListener.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/engineering/swat/watch/WatchEventListener.java#L35-L36

Added lines #L35 - L36 were not covered by tests
}
43 changes: 37 additions & 6 deletions src/main/java/engineering/swat/watch/Watcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@
private final Path path;
private volatile Executor executor = CompletableFuture::runAsync;

private static final Consumer<WatchEvent> NULL_HANDLER = p -> {};
private volatile Consumer<WatchEvent> eventHandler = NULL_HANDLER;
private static final Consumer<WatchEvent> EMPTY_HANDLER = p -> {};
private volatile Consumer<WatchEvent> eventHandler = EMPTY_HANDLER;


private Watcher(WatchScope scope, Path path) {
Expand Down Expand Up @@ -99,13 +99,44 @@
* @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<WatchEvent> eventHandler) {
public Watcher on(Consumer<WatchEvent> 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");

Check warning on line 115 in src/main/java/engineering/swat/watch/Watcher.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/engineering/swat/watch/Watcher.java#L115

Added line #L115 was not covered by tests
}
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;

Check warning on line 130 in src/main/java/engineering/swat/watch/Watcher.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/engineering/swat/watch/Watcher.java#L129-L130

Added lines #L129 - L130 were not covered by tests
default:
throw new IllegalArgumentException("Unexpected kind: " + ev.getKind());

Check warning on line 132 in src/main/java/engineering/swat/watch/Watcher.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/engineering/swat/watch/Watcher.java#L132

Added line #L132 was not covered by tests
}
};
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
Expand All @@ -119,10 +150,10 @@
* 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) {
Expand Down
109 changes: 109 additions & 0 deletions src/test/java/engineering/swat/watch/APIErrorsTests.java
Original file line number Diff line number Diff line change
@@ -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);
}


}
2 changes: 1 addition & 1 deletion src/test/java/engineering/swat/watch/DeleteLockTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down
6 changes: 3 additions & 3 deletions src/test/java/engineering/swat/watch/RecursiveWatchTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}
Expand Down
33 changes: 32 additions & 1 deletion src/test/java/engineering/swat/watch/SingleDirectoryTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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);
}
}
}
4 changes: 2 additions & 2 deletions src/test/java/engineering/swat/watch/SingleFileTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand Down
6 changes: 3 additions & 3 deletions src/test/java/engineering/swat/watch/SmokeTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -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() ) {
Expand All @@ -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() ) {
Expand All @@ -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);
}
Expand Down
Loading
Loading