Skip to content

Commit 9eba969

Browse files
committed
Use Bookkeeper in overflow-related tests
1 parent 2e67f21 commit 9eba969

File tree

3 files changed

+91
-142
lines changed

3 files changed

+91
-142
lines changed

src/test/java/engineering/swat/watch/RecursiveWatchTests.java

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,15 @@
2626
*/
2727
package engineering.swat.watch;
2828

29+
import static engineering.swat.watch.WatchEvent.Kind.CREATED;
2930
import static org.awaitility.Awaitility.await;
3031

3132
import java.io.IOException;
3233
import java.nio.file.Files;
3334
import java.nio.file.Path;
34-
import java.util.concurrent.ConcurrentHashMap;
3535
import java.util.concurrent.ForkJoinPool;
3636
import java.util.concurrent.atomic.AtomicBoolean;
3737
import java.util.concurrent.atomic.AtomicReference;
38-
import java.util.function.BiPredicate;
3938
import java.util.function.Consumer;
4039

4140
import org.apache.logging.log4j.LogManager;
@@ -158,33 +157,27 @@ void overflowsAreRecoveredFrom(Approximation whichFiles) throws IOException, Int
158157
Path.of("bar", "x", "y", "z")
159158
};
160159

161-
// Define a bunch of helper functions to test which events have happened
162-
var events = ConcurrentHashMap.<WatchEvent> newKeySet(); // Stores all incoming events
163-
164-
BiPredicate<WatchEvent.Kind, Path> eventsContains = (kind, descendant) ->
165-
events.stream().anyMatch(e ->
166-
e.getKind().equals(kind) &&
167-
e.getRootPath().equals(parent) &&
168-
e.getRelativePath().equals(descendant));
169-
170-
Consumer<Path> awaitCreation = p ->
171-
await("Creation of `" + p + "` should be observed").until(
172-
() -> eventsContains.test(Kind.CREATED, p));
173-
174-
Consumer<Path> awaitNotCreation = p ->
175-
await("Creation of `" + p + "` shouldn't be observed: " + events)
176-
.pollDelay(TestHelper.TINY_WAIT)
177-
.until(() -> !eventsContains.test(Kind.CREATED, p));
178-
179160
// Configure and start watch
180161
var dropEvents = new AtomicBoolean(false); // Toggles overflow simulation
162+
var bookkeeper = new TestHelper.Bookkeeper();
181163
var watchConfig = Watcher.watch(parent, WatchScope.PATH_AND_ALL_DESCENDANTS)
182164
.withExecutor(ForkJoinPool.commonPool())
183-
.onOverflow(whichFiles)
184165
.filter(e -> !dropEvents.get())
185-
.on(events::add);
166+
.onOverflow(whichFiles)
167+
.on(bookkeeper);
186168

187169
try (var watch = (EventHandlingWatch) watchConfig.start()) {
170+
171+
// Define helper functions to test which events have happened
172+
Consumer<Path> awaitCreation = p ->
173+
await("Creation of `" + p + "` should be observed")
174+
.until(() -> bookkeeper.events().kind(CREATED).rootPath(parent).relativePath(p).any());
175+
176+
Consumer<Path> awaitNotCreation = p ->
177+
await("Creation of `" + p + "` shouldn't be observed: " + bookkeeper)
178+
.pollDelay(TestHelper.TINY_WAIT)
179+
.until(() -> bookkeeper.events().kind(CREATED).rootPath(parent).relativePath(p).none());
180+
188181
// Begin overflow simulation
189182
dropEvents.set(true);
190183

src/test/java/engineering/swat/watch/SingleDirectoryTests.java

Lines changed: 72 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,16 @@
2626
*/
2727
package engineering.swat.watch;
2828

29+
import static engineering.swat.watch.WatchEvent.Kind.CREATED;
30+
import static engineering.swat.watch.WatchEvent.Kind.DELETED;
31+
import static engineering.swat.watch.WatchEvent.Kind.MODIFIED;
2932
import static engineering.swat.watch.WatchEvent.Kind.OVERFLOW;
3033
import static org.awaitility.Awaitility.await;
3134

3235
import java.io.IOException;
3336
import java.nio.file.Files;
34-
import java.util.concurrent.Semaphore;
37+
import java.nio.file.Path;
3538
import java.util.concurrent.atomic.AtomicBoolean;
36-
import java.util.concurrent.atomic.AtomicInteger;
37-
import java.util.function.Predicate;
3839

3940
import org.awaitility.Awaitility;
4041
import org.junit.jupiter.api.AfterEach;
@@ -130,154 +131,105 @@ void memorylessRescanOnOverflow() throws IOException, InterruptedException {
130131
Files.writeString(directory.resolve("a.txt"), "foo");
131132
Files.writeString(directory.resolve("b.txt"), "bar");
132133

133-
var nCreated = new AtomicInteger();
134-
var nModified = new AtomicInteger();
135-
var nOverflow = new AtomicInteger();
134+
var bookkeeper = new TestHelper.Bookkeeper();
136135
var watchConfig = Watcher.watch(directory, WatchScope.PATH_AND_CHILDREN)
137136
.onOverflow(Approximation.ALL)
138-
.on(e -> {
139-
switch (e.getKind()) {
140-
case CREATED:
141-
nCreated.incrementAndGet();
142-
break;
143-
case MODIFIED:
144-
nModified.incrementAndGet();
145-
break;
146-
case OVERFLOW:
147-
nOverflow.incrementAndGet();
148-
break;
149-
default:
150-
break;
151-
}
152-
});
137+
.on(bookkeeper);
153138

154139
try (var watch = watchConfig.start()) {
155-
var overflow = new WatchEvent(WatchEvent.Kind.OVERFLOW, directory);
140+
var overflow = new WatchEvent(OVERFLOW, directory);
156141
((EventHandlingWatch) watch).handleEvent(overflow);
157-
Thread.sleep(TestHelper.SHORT_WAIT.toMillis());
158142

159-
await("Overflow should trigger created events")
160-
.until(nCreated::get, Predicate.isEqual(6)); // 3 directories + 3 files
161-
await("Overflow should trigger modified events")
162-
.until(nModified::get, Predicate.isEqual(2)); // 2 files (c.txt is still empty)
163143
await("Overflow should be visible to user-defined event handler")
164-
.until(nOverflow::get, Predicate.isEqual(1));
144+
.until(() -> bookkeeper.events().kind(OVERFLOW).any());
145+
146+
for (var e : new WatchEvent[] {
147+
new WatchEvent(CREATED, directory, Path.of("d1")),
148+
new WatchEvent(CREATED, directory, Path.of("d2")),
149+
new WatchEvent(CREATED, directory, Path.of("d3")),
150+
new WatchEvent(CREATED, directory, Path.of("a.txt")),
151+
new WatchEvent(CREATED, directory, Path.of("b.txt")),
152+
new WatchEvent(CREATED, directory, Path.of("c.txt")),
153+
new WatchEvent(MODIFIED, directory, Path.of("a.txt")),
154+
new WatchEvent(MODIFIED, directory, Path.of("b.txt"))
155+
}) {
156+
await("Overflow should trigger event: " + e)
157+
.until(() -> bookkeeper.events().any(e));
158+
}
159+
160+
var event = new WatchEvent(MODIFIED, directory, Path.of("c.txt"));
161+
await("Overflow shouldn't trigger event: " + event)
162+
.until(() -> bookkeeper.events().none(event));
165163
}
166164
}
167165

168166
@Test
169167
void indexingRescanOnOverflow() throws IOException, InterruptedException {
170-
// Preface: This test looks a bit hacky because there's no API to
171-
// directly manipulate, or prevent the auto-manipulation of, the index
172-
// inside a watch. I've added some comments below to make it make sense.
173-
174168
var directory = testDir.getTestDirectory();
175-
var semaphore = new Semaphore(0);
176-
177-
var nCreated = new AtomicInteger();
178-
var nModified = new AtomicInteger();
179-
var nDeleted = new AtomicInteger();
180169

170+
var bookkeeper = new TestHelper.Bookkeeper();
171+
var dropEvents = new AtomicBoolean(false); // Toggles overflow simulation
181172
var watchConfig = Watcher.watch(directory, WatchScope.PATH_AND_CHILDREN)
173+
.filter(e -> !dropEvents.get())
182174
.onOverflow(Approximation.DIFF)
183-
.on(e -> {
184-
var kind = e.getKind();
185-
if (kind != OVERFLOW) {
186-
// Threads can handle non-`OVERFLOW` events *only after*
187-
// everything is "ready" for that (in which case a token is
188-
// released to the semaphore, which is initially empty). See
189-
// below for an explanation of "readiness".
190-
semaphore.acquireUninterruptibly();
191-
switch (e.getKind()) {
192-
case CREATED:
193-
nCreated.incrementAndGet();
194-
break;
195-
case MODIFIED:
196-
nModified.incrementAndGet();
197-
break;
198-
case DELETED:
199-
nDeleted.incrementAndGet();
200-
break;
201-
default:
202-
break;
203-
}
204-
semaphore.release();
205-
}
206-
});
175+
.on(bookkeeper);
207176

208177
try (var watch = watchConfig.start()) {
209-
Thread.sleep(TestHelper.NORMAL_WAIT.toMillis());
210-
// At this point, the index of last-modified-times inside `watch` is
211-
// populated with initial values.
212178

179+
// Begin overflow simulation
180+
dropEvents.set(true);
181+
182+
// Do some file operations. No events should be observed (because
183+
// the overflow simulation is running).
213184
Files.writeString(directory.resolve("a.txt"), "foo");
214185
Files.writeString(directory.resolve("b.txt"), "bar");
215186
Files.delete(directory.resolve("c.txt"));
216187
Files.createFile(directory.resolve("d.txt"));
217-
Thread.sleep(TestHelper.NORMAL_WAIT.toMillis());
218-
// At this point, regular events have been generated for a.txt,
219-
// b.txt, c.txt, and d.txt by the file system. These events won't be
220-
// handled by `watch` just yet, though, because the semaphore is
221-
// still empty (i.e., event-handling threads are blocked from making
222-
// progress). Thus, the index inside `watch` still contains the
223-
// initial last-modified-times. (Warning: The blockade works only
224-
// when the rescanner runs after the user-defined event-handler.
225-
// Currently, this is the case, but changing their order probably
226-
// breaks this test.)
227188

189+
await("No events should have been triggered")
190+
.pollDelay(TestHelper.SHORT_WAIT)
191+
.until(() -> bookkeeper.events().none());
192+
193+
// End overflow simulation, and generate an `OVERFLOW` event.
194+
// Synthetic events should now be issued and observed.
195+
dropEvents.set(false);
228196
var overflow = new WatchEvent(WatchEvent.Kind.OVERFLOW, directory);
229197
((EventHandlingWatch) watch).handleEvent(overflow);
230-
Thread.sleep(TestHelper.NORMAL_WAIT.toMillis());
231-
// At this point, the current thread has presumably slept long
232-
// enough for the `OVERFLOW` event to have been handled by the
233-
// rescanner. This means that synthetic events must have been issued
234-
// (because the index still contained the initial last-modified
235-
// times).
236-
237-
// Readiness achieved: Threads can now start handling non-`OVERFLOW`
238-
// events.
239-
semaphore.release();
240-
241-
await("Overflow should trigger created events")
242-
.until(nCreated::get, n -> n >= 2); // 1 synthetic event + >=1 regular event
243-
await("Overflow should trigger modified events")
244-
.until(nModified::get, n -> n >= 4); // 2 synthetic events + >=2 regular events
245-
await("Overflow should trigger deleted events")
246-
.until(nDeleted::get, n -> n >= 2); // 1 synthetic event + >=1 regular event
247-
248-
// Reset counters for next phase of the test
249-
nCreated.set(0);
250-
nModified.set(0);
251-
nDeleted.set(0);
252-
253-
// Let's do some more file operations, trigger another `OVERFLOW`
254-
// event, and observe that synthetic events *aren't* issued this
255-
// time (because the index was already updated when the regular
256-
// events were handled).
198+
199+
for (var e : new WatchEvent[] {
200+
new WatchEvent(MODIFIED, directory, Path.of("a.txt")),
201+
new WatchEvent(MODIFIED, directory, Path.of("b.txt")),
202+
new WatchEvent(DELETED, directory, Path.of("c.txt")),
203+
new WatchEvent(CREATED, directory, Path.of("d.txt"))
204+
}) {
205+
await("Overflow should trigger event: " + e)
206+
.until(() -> bookkeeper.events().any(e));
207+
}
208+
209+
// Do some more file operations. All events should be observed
210+
// (because the overflow simulation is no longer running).
211+
bookkeeper.reset();
257212
Files.writeString(directory.resolve("b.txt"), "baz");
258213
Files.createFile(directory.resolve("c.txt"));
259214
Files.delete(directory.resolve("d.txt"));
260215

261-
await("File create should trigger regular created event")
262-
.until(nCreated::get, n -> n >= 1);
263-
await("File write should trigger regular modified event")
264-
.until(nModified::get, n -> n >= 1);
265-
await("File delete should trigger regular deleted event")
266-
.until(nDeleted::get, n -> n >= 1);
267-
268-
var nCreatedBeforeOverflow = nCreated.get();
269-
var nModifiedBeforeOverflow = nModified.get();
270-
var nDeletedBeforeOverflow = nDeleted.get();
271-
216+
for (var e : new WatchEvent[] {
217+
new WatchEvent(MODIFIED, directory, Path.of("b.txt")),
218+
new WatchEvent(CREATED, directory, Path.of("c.txt")),
219+
new WatchEvent(DELETED, directory, Path.of("d.txt"))
220+
}) {
221+
await("File operation should trigger event: " + e)
222+
.until(() -> bookkeeper.events().any(e));
223+
}
224+
225+
// Generate another `OVERFLOW` event. Synthetic events shouldn't be
226+
// issued and observed (because the index should have been updated).
227+
bookkeeper.reset();
272228
((EventHandlingWatch) watch).handleEvent(overflow);
273-
Thread.sleep(TestHelper.NORMAL_WAIT.toMillis());
274-
275-
await("Overflow shouldn't trigger synthetic created event after file create (and index updated)")
276-
.until(nCreated::get, Predicate.isEqual(nCreatedBeforeOverflow));
277-
await("Overflow shouldn't trigger synthetic modified event after file write (and index updated)")
278-
.until(nModified::get, Predicate.isEqual(nModifiedBeforeOverflow));
279-
await("Overflow shouldn't trigger synthetic deleted event after file delete (and index updated)")
280-
.until(nDeleted::get, Predicate.isEqual(nDeletedBeforeOverflow));
229+
230+
await("No events should have been triggered")
231+
.pollDelay(TestHelper.SHORT_WAIT)
232+
.until(() -> bookkeeper.events().kindNot(OVERFLOW).none());
281233
}
282234
}
283235
}

src/test/java/engineering/swat/watch/TestHelper.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ public static <T> Stream<T> streamOf(T[] values, int repetitions, boolean sortBy
8282
public static class Bookkeeper implements Consumer<WatchEvent> {
8383
private final Queue<WatchEvent> events = new ConcurrentLinkedQueue<>();
8484

85+
public void reset() {
86+
events.clear();
87+
}
88+
8589
public Events events() {
8690
return new Events(events.stream());
8791
}

0 commit comments

Comments
 (0)