From 091744cdaab1fc7bb36cc37929b0fe2c76386c4b Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Wed, 13 Aug 2025 17:48:51 +0200 Subject: [PATCH 1/5] Created own await framework to get faster tests and better messages --- .../swat/watch/RecursiveWatchTests.java | 80 ++++---- .../engineering/swat/watch/TestHelper.java | 28 ++- .../engineering/swat/watch/util/WaitFor.java | 181 ++++++++++++++++++ 3 files changed, 256 insertions(+), 33 deletions(-) create mode 100644 src/test/java/engineering/swat/watch/util/WaitFor.java diff --git a/src/test/java/engineering/swat/watch/RecursiveWatchTests.java b/src/test/java/engineering/swat/watch/RecursiveWatchTests.java index c6d7bbb3..3b99d04a 100644 --- a/src/test/java/engineering/swat/watch/RecursiveWatchTests.java +++ b/src/test/java/engineering/swat/watch/RecursiveWatchTests.java @@ -27,15 +27,22 @@ package engineering.swat.watch; import static engineering.swat.watch.WatchEvent.Kind.CREATED; -import static org.awaitility.Awaitility.await; +import static engineering.swat.watch.util.WaitFor.await; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; +import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -49,6 +56,7 @@ import engineering.swat.watch.WatchEvent.Kind; import engineering.swat.watch.impl.EventHandlingWatch; +import engineering.swat.watch.util.WaitFor; class RecursiveWatchTests { private final Logger logger = LogManager.getLogger(); @@ -69,7 +77,7 @@ void cleanup() { @BeforeAll static void setupEverything() { - Awaitility.setDefaultTimeout(TestHelper.NORMAL_WAIT); + WaitFor.setDefaultTimeout(TestHelper.NORMAL_WAIT); } @Test @@ -99,9 +107,9 @@ void newDirectoryWithFilesChangesDetected() throws IOException { target.set(freshFile); logger.debug("Interested in: {}", freshFile); Files.writeString(freshFile, "Hello world"); - await("New files should have been seen").untilTrue(created); + await("New files should have been seen").until(created); Files.writeString(freshFile, "Hello world 2"); - await("Fresh file change have been detected").untilTrue(changed); + await("Fresh file change have been detected").until(changed); } } @@ -121,7 +129,7 @@ void correctRelativePathIsReported() throws IOException { var targetFile = testDir.getTestDirectory().resolve(relative); Files.createDirectories(targetFile.getParent()); Files.writeString(targetFile, "Hello World"); - await("Nested path is seen").untilTrue(seen); + await("Nested path is seen").until(seen); } } @@ -143,7 +151,7 @@ void deleteOfFileInDirectoryShouldBeVisible() throws IOException { try (var watch = watchConfig.start()) { Files.delete(target); await("File deletion should generate delete event") - .untilTrue(seen); + .until(seen); } } @@ -151,11 +159,11 @@ void deleteOfFileInDirectoryShouldBeVisible() throws IOException { @EnumSource // Repeat test for each `Approximation` value void overflowsAreRecoveredFrom(Approximation whichFiles) throws IOException, InterruptedException { var parent = testDir.getTestDirectory(); - var descendants = new Path[] { + var descendants = List.of( Path.of("foo"), Path.of("bar"), Path.of("bar", "x", "y", "z") - }; + ); // Configure and start watch var dropEvents = new AtomicBoolean(false); // Toggles overflow simulation @@ -169,28 +177,38 @@ void overflowsAreRecoveredFrom(Approximation whichFiles) throws IOException, Int try (var watch = (EventHandlingWatch) watchConfig.start()) { // Define helper functions to test which events have happened - Consumer awaitCreation = p -> - await("Creation of `" + p + "` should be observed") - .until(() -> bookkeeper.events().kind(CREATED).rootPath(parent).relativePath(p).any()); - - Consumer awaitNotCreation = p -> - await("Creation of `" + p + "` shouldn't be observed: " + bookkeeper) - .pollDelay(TestHelper.TINY_WAIT) - .until(() -> bookkeeper.events().kind(CREATED).rootPath(parent).relativePath(p).none()); + Consumer> awaitCreation = paths -> + WaitFor.await("Creation should be observed") + .untilContainsAll(() -> + bookkeeper.events() + .kind(CREATED) + .rootPath(parent) + .relativePath(paths) + .events() + .map(WatchEvent::getRelativePath) + , paths); // Begin overflow simulation dropEvents.set(true); // Create descendants and files. They *shouldn't* be observed yet. + var missedCreates = new ArrayList(); var file1 = Path.of("file1.txt"); for (var descendant : descendants) { - Files.createDirectories(parent.resolve(descendant)); - Files.createFile(parent.resolve(descendant).resolve(file1)); - } - for (var descendant : descendants) { - awaitNotCreation.accept(descendant); - awaitNotCreation.accept(descendant.resolve(file1)); + var d = parent.resolve(descendant); + var f = d.resolve(file1); + Files.createDirectories(d); + Files.createFile(f); + missedCreates.add(descendant); + missedCreates.add(descendant.resolve(file1)); } + WaitFor.await(() -> "We should not have seen any events") + .time(TestHelper.TINY_WAIT) + .holdsEmpty(() -> bookkeeper.events() + .kind(CREATED) + .rootPath(parent) + .relativePath(missedCreates) + .events()); // End overflow simulation, and generate the `OVERFLOW` event. The // previous creation of descendants and files *should* now be @@ -201,10 +219,7 @@ void overflowsAreRecoveredFrom(Approximation whichFiles) throws IOException, Int watch.handleEvent(overflow); if (whichFiles != Approximation.NONE) { // Auto-handler is configured - for (var descendant : descendants) { - awaitCreation.accept(descendant); - awaitCreation.accept(descendant.resolve(file1)); - } + awaitCreation.accept(missedCreates); } else { // Give the watch some time to process the `OVERFLOW` event and // do internal bookkeeping @@ -213,13 +228,14 @@ void overflowsAreRecoveredFrom(Approximation whichFiles) throws IOException, Int // Create more files. They *should* be observed (regardless of // whether an auto-handler for `OVERFLOW` events is configured). - var file2 = Path.of("file2.txt"); - for (var descendant : descendants) { - Files.createFile(parent.resolve(descendant).resolve(file2)); - } - for (var descendant : descendants) { - awaitCreation.accept(descendant.resolve(file2)); + var moreFiles = descendants.stream() + .map(d -> d.resolve(Path.of("file2.txt"))) + .collect(Collectors.toList()); + + for (var f : moreFiles) { + Files.createFile(parent.resolve(f)); } + awaitCreation.accept(moreFiles); } } } diff --git a/src/test/java/engineering/swat/watch/TestHelper.java b/src/test/java/engineering/swat/watch/TestHelper.java index 478d8770..88275b39 100644 --- a/src/test/java/engineering/swat/watch/TestHelper.java +++ b/src/test/java/engineering/swat/watch/TestHelper.java @@ -29,6 +29,8 @@ import java.nio.file.Path; import java.time.Duration; import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.function.Consumer; @@ -113,35 +115,59 @@ public boolean none() { return !any(); } + public Stream events() { + return stream; + } + public boolean none(WatchEvent event) { return !any(event); } public Events kind(WatchEvent.Kind... kinds) { + return kind(Arrays.asList(kinds)); + } + public Events kind(Collection kinds) { return new Events(stream.filter(e -> contains(kinds, e.getKind()))); } public Events kindNot(WatchEvent.Kind... kinds) { + return kindNot(Arrays.asList(kinds)); + } + public Events kindNot(Collection kinds) { return new Events(stream.filter(e -> !contains(kinds, e.getKind()))); } public Events rootPath(Path... rootPaths) { + return rootPath(Arrays.asList(rootPaths)); + } + public Events rootPath(Collection rootPaths) { return new Events(stream.filter(e -> contains(rootPaths, e.getRootPath()))); } public Events rootPathNot(Path... rootPaths) { + return rootPathNot(Arrays.asList(rootPaths)); + } + public Events rootPathNot(Collection rootPaths) { return new Events(stream.filter(e -> !contains(rootPaths, e.getRootPath()))); } public Events relativePath(Path... relativePaths) { + return relativePath(Arrays.asList(relativePaths)); + } + + public Events relativePath(Collection relativePaths) { return new Events(stream.filter(e -> contains(relativePaths, e.getRelativePath()))); } public Events relativePathNot(Path... relativePaths) { + return relativePathNot(Arrays.asList(relativePaths)); + } + + public Events relativePathNot(Collection relativePaths) { return new Events(stream.filter(e -> !contains(relativePaths, e.getRelativePath()))); } - private boolean contains(Object[] a, Object key) { + private boolean contains(Collection a, T key) { for (var elem : a) { if (elem.equals(key)) { return true; diff --git a/src/test/java/engineering/swat/watch/util/WaitFor.java b/src/test/java/engineering/swat/watch/util/WaitFor.java new file mode 100644 index 00000000..bbf6d87f --- /dev/null +++ b/src/test/java/engineering/swat/watch/util/WaitFor.java @@ -0,0 +1,181 @@ +package engineering.swat.watch.util; + +import static java.lang.System.currentTimeMillis; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.time.Duration; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BooleanSupplier; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.checkerframework.checker.nullness.qual.Nullable; + +public class WaitFor { + + private static Duration defaultWaitTime = Duration.ofSeconds(2); + + public static void setDefaultTimeout(Duration newDefaultTime) { + defaultWaitTime = newDefaultTime; + } + + private final Supplier message; + private @Nullable BooleanSupplier failFast; + private @Nullable Duration time; + private @Nullable Duration poll; + + + private WaitFor(Supplier message) { + this.message = message; + } + + public static WaitFor await(String message) { + return await(() -> message); + } + + public static WaitFor await(Supplier message) { + return new WaitFor(message); + } + + public WaitFor time(Duration d) { + this.time = d; + return this; + } + + public WaitFor pollInterval(Duration d) { + this.poll = d; + return this; + } + + public WaitFor failFast(BooleanSupplier b) { + this.failFast = b; + return this; + } + + public WaitFor failFast(AtomicBoolean b) { + return failFast(b::get); + } + + + + private boolean alreadyFailed() { + if (failFast != null) { + try { + return failFast.getAsBoolean(); + } catch (RuntimeException ex) { + return false; + } + } + return false; + } + + private Duration calculatePoll(Duration time) { + var poll = this.poll; + if (poll == null || poll.toMillis() < 1) { + poll = derivePoll(time); + } + return poll; + } + + private Duration calculateTime() { + var time = this.time; + if (time == null) { + time = defaultWaitTime; + } + return time; + } + + private static Duration derivePoll(Duration time) { + var result = time.dividedBy(100); + if (result.toMillis() > 1) { + return result; + } + result = time.dividedBy(10); + if (result.toMillis() > 1) { + return result; + } + return Duration.ofMillis(1); + } + + + private boolean block(BooleanSupplier action) { + var time = calculateTime(); + var poll = calculatePoll(time); + + var end = currentTimeMillis() + time.toMillis(); + while (end > currentTimeMillis()) { + var start = currentTimeMillis(); + if (action.getAsBoolean()) { + return true; + } + if (alreadyFailed()) { + fail(message.get() + " was terminated by failFast predicate"); + } + var stop = currentTimeMillis(); + var remaining = poll.toMillis() - (stop - start); + if (remaining > 0) { + try { + Thread.sleep(remaining); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + return false; + } + + + + private static Set notSeen(Stream stream, Collection needles) { + var seen = new HashSet<>(needles); + stream.anyMatch(e -> { + seen.remove(e); + return seen.isEmpty(); // fast exit the stream + }); + return seen; + } + + public void untilContains(Supplier> stream, T needle) { + assertTrue(block(() -> stream.get().anyMatch(needle::equals)), () -> message.get() + " due to missing: " + needle); + } + + public void untilContainsAll(Supplier> stream, Collection needles) { + if (!block(() -> notSeen(stream.get(), needles).isEmpty())) { + assertEquals(Collections.emptySet(), notSeen(stream.get(), needles), () -> message.get() + " due to not all entries present"); + } + } + + public void until(AtomicBoolean b) { + until(b::get); + } + + public void until(BooleanSupplier p) { + assertTrue(block(p), message); + } + + private void holdBlock(BooleanSupplier p) { + // we keep going untill a false result + block(() -> !p.getAsBoolean()); + assertTrue(p, message); + } + + public void holds(BooleanSupplier p) { + holdBlock(p); + } + + public void holdsEmpty(Supplier> stream) { + holdBlock(() -> { + assertEquals(Collections.emptyList(), stream.get().collect(Collectors.toList()), message); + return true; + }); + } + + +} From 39e1f4359326769d8b1ca6a6207f17d8182e917b Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Wed, 13 Aug 2025 20:48:14 +0200 Subject: [PATCH 2/5] More slow test converted to different framework --- .../swat/watch/ParallelWatches.java | 32 +++++++++---------- .../engineering/swat/watch/util/WaitFor.java | 10 ++++++ 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/test/java/engineering/swat/watch/ParallelWatches.java b/src/test/java/engineering/swat/watch/ParallelWatches.java index 09de0b07..8d5ff8f8 100644 --- a/src/test/java/engineering/swat/watch/ParallelWatches.java +++ b/src/test/java/engineering/swat/watch/ParallelWatches.java @@ -28,21 +28,21 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.awaitility.Awaitility.await; +import static engineering.swat.watch.util.WaitFor.await; import java.io.Closeable; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.time.Duration; import java.util.concurrent.atomic.AtomicBoolean; -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; +import engineering.swat.watch.util.WaitFor; + class ParallelWatches { private TestDirectory testDir; @@ -60,7 +60,7 @@ void cleanup() { @BeforeAll static void setupEverything() { - Awaitility.setDefaultTimeout(TestHelper.NORMAL_WAIT); + WaitFor.setDefaultTimeout(TestHelper.NORMAL_WAIT); } @Test @@ -73,9 +73,9 @@ void directoryAndFileBothTrigger() throws IOException { try (var fileWatch = watch(file, WatchScope.PATH_ONLY, fileTriggered)) { Files.write(file, "test".getBytes()); await("Directory should have picked up the file") - .untilTrue(dirTriggered); + .until(dirTriggered); await("File should have picked up the file") - .untilTrue(fileTriggered); + .until(fileTriggered); } } } @@ -92,10 +92,10 @@ void fileShouldNotTrigger() throws IOException { try (var fileWatch = watch(file, WatchScope.PATH_ONLY, fileTriggered)) { Files.write(file2, "test2".getBytes()); await("Directory should have picked up the file") - .untilTrue(dirTriggered); + .until(dirTriggered); await("File should not have picked up the file") - .pollDelay(TestHelper.NORMAL_WAIT.minus(Duration.ofMillis(100))) - .untilFalse(fileTriggered); + .time(TestHelper.SHORT_WAIT) + .holdsFalse(fileTriggered); } } } @@ -111,9 +111,9 @@ void nestedDirectory() throws IOException { try (var nestedDirWatch = watch(dir2, WatchScope.PATH_AND_CHILDREN, nestedDirTriggered)) { Files.write(dir2.resolve("a2.txt"), "test2".getBytes()); await("Directory should have picked up nested file") - .untilTrue(dirTriggered); + .until(dirTriggered); await("Nested directory should have picked up the file") - .untilTrue(nestedDirTriggered); + .until(nestedDirTriggered); } } } @@ -129,10 +129,10 @@ void nestedDirectoryNotTrigger() throws IOException { try (var nestedDirWatch = watch(dir2, WatchScope.PATH_AND_CHILDREN, nestedDirTriggered)) { Files.write(dir1.resolve("a1.txt"), "1".getBytes()); await("Directory should have picked up the file") - .untilTrue(dirTriggered); + .until(dirTriggered); await("Nested dir should not have picked up the file") - .pollDelay(TestHelper.NORMAL_WAIT.minus(Duration.ofMillis(100))) - .untilFalse(nestedDirTriggered); + .time(TestHelper.SHORT_WAIT) + .holdsFalse(nestedDirTriggered); } } } @@ -153,9 +153,9 @@ void watchingSameDirectory() throws IOException { Files.write(dir.resolve("a1.txt"), "1".getBytes()); await("Watch 1 should have triggered") - .untilTrue(trig1); + .until(trig1); await("Directory should have picked up the file") - .untilTrue(trig2); + .until(trig2); } } } diff --git a/src/test/java/engineering/swat/watch/util/WaitFor.java b/src/test/java/engineering/swat/watch/util/WaitFor.java index bbf6d87f..8793817f 100644 --- a/src/test/java/engineering/swat/watch/util/WaitFor.java +++ b/src/test/java/engineering/swat/watch/util/WaitFor.java @@ -166,10 +166,20 @@ private void holdBlock(BooleanSupplier p) { assertTrue(p, message); } + public void holds(AtomicBoolean b) { + holds(b::get); + } public void holds(BooleanSupplier p) { holdBlock(p); } + public void holdsFalse(AtomicBoolean p) { + holdsFalse(p::get); + } + public void holdsFalse(BooleanSupplier p) { + holds(() -> !p.getAsBoolean()); + } + public void holdsEmpty(Supplier> stream) { holdBlock(() -> { assertEquals(Collections.emptyList(), stream.get().collect(Collectors.toList()), message); From 5663257cd8c47fdf5b780f1c54364748736b0e44 Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Wed, 13 Aug 2025 21:02:17 +0200 Subject: [PATCH 3/5] Another test made 3x faster --- .../swat/watch/SingleDirectoryTests.java | 26 ++++++++++--------- .../engineering/swat/watch/TestHelper.java | 2 ++ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/test/java/engineering/swat/watch/SingleDirectoryTests.java b/src/test/java/engineering/swat/watch/SingleDirectoryTests.java index 787e47d9..1f347036 100644 --- a/src/test/java/engineering/swat/watch/SingleDirectoryTests.java +++ b/src/test/java/engineering/swat/watch/SingleDirectoryTests.java @@ -30,7 +30,7 @@ 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 static engineering.swat.watch.util.WaitFor.await; import java.io.IOException; import java.nio.file.Files; @@ -45,6 +45,7 @@ import engineering.swat.watch.WatchEvent.Kind; import engineering.swat.watch.impl.EventHandlingWatch; +import engineering.swat.watch.util.WaitFor; class SingleDirectoryTests { private TestDirectory testDir; @@ -63,7 +64,7 @@ void cleanup() { @BeforeAll static void setupEverything() { - Awaitility.setDefaultTimeout(TestHelper.NORMAL_WAIT); + WaitFor.setDefaultTimeout(TestHelper.NORMAL_WAIT); } @Test @@ -85,12 +86,12 @@ void deleteOfFileInDirectoryShouldBeVisible() throws IOException { // Delete the file Files.delete(target); await("File deletion should generate delete event") - .untilTrue(seenDelete); + .until(seenDelete); // Re-create it again Files.writeString(target, "Hello World"); await("File creation should generate create event") - .untilTrue(seenCreate); + .until(seenCreate); } } @@ -116,12 +117,12 @@ public void onDeleted(WatchEvent ev) { // Delete the file Files.delete(target); await("File deletion should generate delete event") - .untilTrue(seenDelete); + .until(seenDelete); // Re-create it again Files.writeString(target, "Hello World"); await("File creation should generate create event") - .untilTrue(seenCreate); + .until(seenCreate); } } @@ -181,15 +182,15 @@ void indexingRescanOnOverflow() throws IOException, InterruptedException { // Perform some file operations (after a short wait to ensure a new // last-modified-time). No events should be observed (because the // overflow simulation is running). - Thread.sleep(TestHelper.SHORT_WAIT.toMillis()); + Thread.sleep(100); Files.writeString(directory.resolve("a.txt"), "foo"); Files.writeString(directory.resolve("b.txt"), "bar"); Files.delete(directory.resolve("c.txt")); Files.createFile(directory.resolve("d.txt")); await("No events should have been triggered") - .pollDelay(TestHelper.SHORT_WAIT) - .until(() -> bookkeeper.events().none()); + .time(TestHelper.SMALL_WAIT) + .holds(() -> bookkeeper.events().none()); // End overflow simulation, and generate an `OVERFLOW` event. // Synthetic events should now be issued and observed. @@ -197,6 +198,7 @@ void indexingRescanOnOverflow() throws IOException, InterruptedException { var overflow = new WatchEvent(WatchEvent.Kind.OVERFLOW, directory); ((EventHandlingWatch) watch).handleEvent(overflow); + for (var e : new WatchEvent[] { new WatchEvent(MODIFIED, directory, Path.of("a.txt")), new WatchEvent(MODIFIED, directory, Path.of("b.txt")), @@ -211,7 +213,7 @@ void indexingRescanOnOverflow() throws IOException, InterruptedException { // Perform some more file operations. All events should be observed // (because the overflow simulation is no longer running). - Thread.sleep(TestHelper.SHORT_WAIT.toMillis()); + Thread.sleep(500); Files.delete(directory.resolve("a.txt")); Files.writeString(directory.resolve("b.txt"), "baz"); Files.createFile(directory.resolve("c.txt")); @@ -232,8 +234,8 @@ void indexingRescanOnOverflow() throws IOException, InterruptedException { ((EventHandlingWatch) watch).handleEvent(overflow); await("No events should have been triggered") - .pollDelay(TestHelper.SHORT_WAIT) - .until(() -> bookkeeper.events().kindNot(OVERFLOW).none()); + .time(TestHelper.SMALL_WAIT) + .holds(() -> bookkeeper.events().kindNot(OVERFLOW).none()); } } } diff --git a/src/test/java/engineering/swat/watch/TestHelper.java b/src/test/java/engineering/swat/watch/TestHelper.java index 88275b39..71e4c5a3 100644 --- a/src/test/java/engineering/swat/watch/TestHelper.java +++ b/src/test/java/engineering/swat/watch/TestHelper.java @@ -40,6 +40,7 @@ public class TestHelper { public static final Duration TINY_WAIT; + public static final Duration SMALL_WAIT; public static final Duration SHORT_WAIT; public static final Duration NORMAL_WAIT; public static final Duration LONG_WAIT; @@ -59,6 +60,7 @@ else if (os.contains("win")) { delayFactor *= 4; } TINY_WAIT = Duration.ofMillis(250 * delayFactor); + SMALL_WAIT = TINY_WAIT.multipliedBy(2); SHORT_WAIT = Duration.ofSeconds(1 * delayFactor); NORMAL_WAIT = Duration.ofSeconds(4 * delayFactor); LONG_WAIT = Duration.ofSeconds(8 * delayFactor); From 28b9cdd6481dc74a84499820a62d9d7e294a4d91 Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Wed, 13 Aug 2025 21:18:13 +0200 Subject: [PATCH 4/5] Rewrote more test to new framework and joined waits together --- .../swat/watch/SingleFileTests.java | 45 ++++++++++++------- .../engineering/swat/watch/util/WaitFor.java | 32 ++++++++----- 2 files changed, 50 insertions(+), 27 deletions(-) diff --git a/src/test/java/engineering/swat/watch/SingleFileTests.java b/src/test/java/engineering/swat/watch/SingleFileTests.java index 26c0f015..f5aa218f 100644 --- a/src/test/java/engineering/swat/watch/SingleFileTests.java +++ b/src/test/java/engineering/swat/watch/SingleFileTests.java @@ -30,13 +30,15 @@ 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 static engineering.swat.watch.util.WaitFor.await; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; import java.nio.file.Files; import java.nio.file.attribute.FileTime; import java.time.Instant; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BooleanSupplier; import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterEach; @@ -45,6 +47,7 @@ import org.junit.jupiter.api.Test; import engineering.swat.watch.impl.EventHandlingWatch; +import engineering.swat.watch.util.WaitFor; class SingleFileTests { private TestDirectory testDir; @@ -63,7 +66,7 @@ void cleanup() { @BeforeAll static void setupEverything() { - Awaitility.setDefaultTimeout(TestHelper.NORMAL_WAIT); + WaitFor.setDefaultTimeout(TestHelper.NORMAL_WAIT); } @Test @@ -86,12 +89,11 @@ void singleFileShouldNotTriggerOnOtherFilesInSameDir() throws IOException, Inter Files.writeString(f, "Hello"); } } - Thread.sleep(TestHelper.SHORT_WAIT.toMillis()); + Thread.sleep(TestHelper.SMALL_WAIT.toMillis()); Files.writeString(target, "Hello world"); await("Single file does trigger") - .pollDelay(TestHelper.NORMAL_WAIT.minusMillis(10)) - .failFast("No others should be notified", others::get) - .untilTrue(seen); + .failFast("Other files should not trigger", others::get) + .until(seen); } } @@ -115,12 +117,11 @@ void singleFileThatMonitorsOnlyADirectory() throws IOException, InterruptedExcep Files.writeString(f, "Hello"); } } - Thread.sleep(TestHelper.SHORT_WAIT.toMillis()); + Thread.sleep(TestHelper.SMALL_WAIT.toMillis()); Files.setLastModifiedTime(target, FileTime.from(Instant.now())); await("Single directory does trigger") - .pollDelay(TestHelper.NORMAL_WAIT.minusMillis(10)) .failFast("No others should be notified", others::get) - .untilTrue(seen); + .until(seen); } } @@ -128,12 +129,13 @@ void singleFileThatMonitorsOnlyADirectory() throws IOException, InterruptedExcep void noRescanOnOverflow() throws IOException, InterruptedException { var bookkeeper = new TestHelper.Bookkeeper(); try (var watch = startWatchAndTriggerOverflow(Approximation.NONE, bookkeeper)) { - Thread.sleep(TestHelper.SHORT_WAIT.toMillis()); + //Thread.sleep(TestHelper.SMALL_WAIT.toMillis()); - await("Overflow shouldn't trigger created, modified, or deleted events: " + bookkeeper) - .until(() -> bookkeeper.events().kind(CREATED, MODIFIED, DELETED).none()); await("Overflow should be visible to user-defined event handler") .until(() -> bookkeeper.events().kind(OVERFLOW).any()); + await("Overflow shouldn't trigger created, modified, or deleted events: " + bookkeeper) + .time(TestHelper.SMALL_WAIT) + .holds(() -> bookkeeper.events().kind(CREATED, MODIFIED, DELETED).none()); } } @@ -141,15 +143,24 @@ void noRescanOnOverflow() throws IOException, InterruptedException { void memorylessRescanOnOverflow() throws IOException, InterruptedException { var bookkeeper = new TestHelper.Bookkeeper(); try (var watch = startWatchAndTriggerOverflow(Approximation.ALL, bookkeeper)) { - Thread.sleep(TestHelper.SHORT_WAIT.toMillis()); + //Thread.sleep(TestHelper.SMALL_WAIT.toMillis()); var path = watch.getPath(); await("Overflow should trigger created event for `" + path + "`") .until(() -> bookkeeper.events().kind(CREATED).rootPath(path).any()); - await("Overflow shouldn't trigger created events for other files") - .until(() -> bookkeeper.events().kind(CREATED).rootPathNot(path).none()); - await("Overflow shouldn't trigger modified or deleted events") - .until(() -> bookkeeper.events().kind(MODIFIED, DELETED).none()); + await("No extra events") + .time(TestHelper.SMALL_WAIT) + .holds(() -> { + assertTrue( + bookkeeper.events().kind(CREATED).rootPathNot(path).none(), + "Overflow shouldn't trigger created events for other files" + ); + assertTrue( + bookkeeper.events().kind(MODIFIED, DELETED).none() + ,"Overflow shouldn't trigger modified or deleted events" + ); + return true; + }); await("Overflow should be visible to user-defined event handler") .until(() -> bookkeeper.events().kind(OVERFLOW).any()); } diff --git a/src/test/java/engineering/swat/watch/util/WaitFor.java b/src/test/java/engineering/swat/watch/util/WaitFor.java index 8793817f..6b7bd61c 100644 --- a/src/test/java/engineering/swat/watch/util/WaitFor.java +++ b/src/test/java/engineering/swat/watch/util/WaitFor.java @@ -30,6 +30,7 @@ public static void setDefaultTimeout(Duration newDefaultTime) { private @Nullable BooleanSupplier failFast; private @Nullable Duration time; private @Nullable Duration poll; + private @Nullable Supplier failMessage; private WaitFor(Supplier message) { @@ -54,26 +55,38 @@ public WaitFor pollInterval(Duration d) { return this; } - public WaitFor failFast(BooleanSupplier b) { + public WaitFor failFast(String message, BooleanSupplier b) { + return failFast(() -> message, b); + } + + public WaitFor failFast(Supplier message, BooleanSupplier b) { this.failFast = b; + this.failMessage = message; return this; } - public WaitFor failFast(AtomicBoolean b) { - return failFast(b::get); + public WaitFor failFast(Supplier message, AtomicBoolean b) { + return failFast(message, b::get); + } + public WaitFor failFast(String message, AtomicBoolean b) { + return failFast(message, b::get); } - private boolean alreadyFailed() { + private void checkFailed() { if (failFast != null) { try { - return failFast.getAsBoolean(); + if (failFast.getAsBoolean()) { + var actualFailMessage = failMessage; + if (actualFailMessage == null) { + actualFailMessage = () -> this.message.get() + " was terminated earlier due to fail fast"; + } + fail(actualFailMessage); + } } catch (RuntimeException ex) { - return false; } } - return false; } private Duration calculatePoll(Duration time) { @@ -112,12 +125,11 @@ private boolean block(BooleanSupplier action) { var end = currentTimeMillis() + time.toMillis(); while (end > currentTimeMillis()) { var start = currentTimeMillis(); + checkFailed(); if (action.getAsBoolean()) { return true; } - if (alreadyFailed()) { - fail(message.get() + " was terminated by failFast predicate"); - } + checkFailed(); var stop = currentTimeMillis(); var remaining = poll.toMillis() - (stop - start); if (remaining > 0) { From d01f47c3fec6fb1d643aff3eac37d36de5703871 Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Wed, 13 Aug 2025 21:55:57 +0200 Subject: [PATCH 5/5] Also made the torture test a bit faster to run --- .../engineering/swat/watch/TortureTests.java | 26 ++++----- .../engineering/swat/watch/util/WaitFor.java | 56 +++++++++++++++++++ 2 files changed, 69 insertions(+), 13 deletions(-) diff --git a/src/test/java/engineering/swat/watch/TortureTests.java b/src/test/java/engineering/swat/watch/TortureTests.java index eb159b49..fd7000c6 100644 --- a/src/test/java/engineering/swat/watch/TortureTests.java +++ b/src/test/java/engineering/swat/watch/TortureTests.java @@ -26,7 +26,7 @@ */ package engineering.swat.watch; -import static org.awaitility.Awaitility.await; +import static engineering.swat.watch.util.WaitFor.await; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -45,12 +45,10 @@ import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Predicate; import java.util.stream.Stream; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -60,6 +58,8 @@ import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.MethodSource; +import engineering.swat.watch.util.WaitFor; + class TortureTests { private final Logger logger = LogManager.getLogger(); @@ -68,7 +68,7 @@ class TortureTests { @BeforeAll static void setupEverything() { - Awaitility.setDefaultTimeout(TestHelper.LONG_WAIT.getSeconds(), TimeUnit.SECONDS); + WaitFor.setDefaultTimeout(TestHelper.LONG_WAIT); } @BeforeEach @@ -181,13 +181,13 @@ void pressureOnFSShouldNotMissNewFilesAnything(Approximation whichFiles) throws logger.info("Starting {} jobs", THREADS); io.start(); // now we generate a whole bunch of events - Thread.sleep(TestHelper.NORMAL_WAIT.toMillis()); + Thread.sleep(Math.min(TestHelper.NORMAL_WAIT.toMillis(), Duration.ofSeconds(30).toMillis())); logger.info("Stopping jobs"); pathsWritten = io.stop(); logger.info("Generated: {} files", pathsWritten.size()); await("After a while we should have seen all the create events") - .timeout(TestHelper.LONG_WAIT.multipliedBy(50)) + .time(TestHelper.LONG_WAIT.multipliedBy(50)) .pollInterval(Duration.ofMillis(500)) .until(() -> seenCreates.containsAll(pathsWritten)); } @@ -254,10 +254,10 @@ void manyRegistrationsForSamePath() throws InterruptedException, IOException { Files.writeString(target, "Hello World"); var expected = Collections.singleton(target); await("We should see only one event") - .failFast(() -> !exceptions.isEmpty()) - .timeout(TestHelper.LONG_WAIT) + .failFast(() -> "There was an exception: "+ exceptions, () -> !exceptions.isEmpty()) + .time(TestHelper.NORMAL_WAIT) .pollInterval(Duration.ofMillis(10)) - .until(() -> seen, expected::equals); + .untilEquals(() -> seen, expected); if (!exceptions.isEmpty()) { fail(exceptions.pop()); } @@ -269,7 +269,7 @@ void manyRegistrationsForSamePath() throws InterruptedException, IOException { static Stream manyRegisterAndUnregisterSameTimeSource() { Approximation[] values = { Approximation.ALL, Approximation.DIFF }; - return TestHelper.streamOf(values, 5); + return TestHelper.streamOf(values, 3); } @ParameterizedTest @@ -328,9 +328,9 @@ void manyRegisterAndUnregisterSameTime(Approximation whichFiles) throws Interrup assertTrue(seen.isEmpty(), "No events should have been sent"); Files.writeString(target, "Hello World"); await("We should see only exactly the " + amountOfWatchersActive + " events we expect") - .failFast(() -> !exceptions.isEmpty()) - .pollDelay(TestHelper.NORMAL_WAIT.minusMillis(100)) - .until(seen::size, Predicate.isEqual(amountOfWatchersActive)) + .time(TestHelper.SHORT_WAIT) + .failFast(() -> "There was an exception: "+ exceptions, () -> !exceptions.isEmpty()) + .delayedHoldsEquals(seen::size, amountOfWatchersActive) ; if (!exceptions.isEmpty()) { fail(exceptions.pop()); diff --git a/src/test/java/engineering/swat/watch/util/WaitFor.java b/src/test/java/engineering/swat/watch/util/WaitFor.java index 6b7bd61c..6c0817c9 100644 --- a/src/test/java/engineering/swat/watch/util/WaitFor.java +++ b/src/test/java/engineering/swat/watch/util/WaitFor.java @@ -1,3 +1,29 @@ +/* + * 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.util; import static java.lang.System.currentTimeMillis; @@ -172,6 +198,11 @@ public void until(BooleanSupplier p) { assertTrue(block(p), message); } + public void untilEquals(Supplier val, T expected) { + assertTrue(block(() -> expected.equals(val.get())), message); + assertEquals(expected, val.get(), message); + } + private void holdBlock(BooleanSupplier p) { // we keep going untill a false result block(() -> !p.getAsBoolean()); @@ -199,5 +230,30 @@ public void holdsEmpty(Supplier> stream) { }); } + private void delayedHoldsBlock(BooleanSupplier p, Runnable finalCheck) { + AtomicBoolean turnedTrue = new AtomicBoolean(false); + block(() -> { + var result = p.getAsBoolean(); + if (result) { + turnedTrue.set(true); + // keep running, all good + return false; + } + // now if false, that could be fine, if we're never turned true yet + return turnedTrue.get(); + }); + finalCheck.run(); + } + + public void delayedHolds(BooleanSupplier p) { + delayedHoldsBlock(p, () -> assertTrue(p, message)); + } + + public void delayedHoldsEquals(Supplier sup, T expected) { + delayedHoldsBlock( + () -> expected.equals(sup.get()), + () -> assertEquals(expected, sup.get(), message) + ); + } }