Skip to content

Commit d97a184

Browse files
authored
Merge pull request #35 from SWAT-engineering/refactor-tests
Refactor tests
2 parents 6d257fa + c201bb7 commit d97a184

File tree

5 files changed

+211
-182
lines changed

5 files changed

+211
-182
lines changed

src/main/java/engineering/swat/watch/WatchEvent.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
package engineering.swat.watch;
2828

2929
import java.nio.file.Path;
30+
import java.util.Objects;
3031

3132
import org.checkerframework.checker.nullness.qual.Nullable;
3233

@@ -126,4 +127,24 @@ public String toString() {
126127
return String.format("WatchEvent[%s, %s, %s]", this.rootPath, this.kind, this.relativePath);
127128
}
128129

130+
/**
131+
* <p>
132+
* Tests the equivalence of two events. Two events are equivalent when they
133+
* have equal kinds, equal root paths, and equal relative paths.
134+
* </p>
135+
*
136+
* <p>
137+
* Note: This method applies different logic to compare events than (the
138+
* default implementation of) method {@link #equals(Object)}, which
139+
* shouldn't be overridden. This is because events should normally be
140+
* compared in terms of their identities (e.g., two successive modifications
141+
* of the same file result in events that are equivalent, but not equal;
142+
* they need to be distinguishable in collections).
143+
* </p>
144+
*/
145+
public static boolean areEquivalent(WatchEvent e1, WatchEvent e2) {
146+
return Objects.equals(e1.getKind(), e2.getKind()) &&
147+
Objects.equals(e1.getRootPath(), e2.getRootPath()) &&
148+
Objects.equals(e1.getRelativePath(), e2.getRelativePath());
149+
}
129150
}

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: 75 additions & 119 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,109 @@ 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.
178+
// Begin overflow simulation
179+
dropEvents.set(true);
212180

181+
// Perform some file operations (after a short wait to ensure a new
182+
// last-modified-time). No events should be observed (because the
183+
// overflow simulation is running).
184+
Thread.sleep(TestHelper.SHORT_WAIT.toMillis());
213185
Files.writeString(directory.resolve("a.txt"), "foo");
214186
Files.writeString(directory.resolve("b.txt"), "bar");
215187
Files.delete(directory.resolve("c.txt"));
216188
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.)
227189

190+
await("No events should have been triggered")
191+
.pollDelay(TestHelper.SHORT_WAIT)
192+
.until(() -> bookkeeper.events().none());
193+
194+
// End overflow simulation, and generate an `OVERFLOW` event.
195+
// Synthetic events should now be issued and observed.
196+
dropEvents.set(false);
228197
var overflow = new WatchEvent(WatchEvent.Kind.OVERFLOW, directory);
229198
((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).
199+
200+
for (var e : new WatchEvent[] {
201+
new WatchEvent(MODIFIED, directory, Path.of("a.txt")),
202+
new WatchEvent(MODIFIED, directory, Path.of("b.txt")),
203+
new WatchEvent(DELETED, directory, Path.of("c.txt")),
204+
new WatchEvent(CREATED, directory, Path.of("d.txt"))
205+
}) {
206+
await("Overflow should trigger event: " + e)
207+
.until(() -> bookkeeper.events().any(e));
208+
}
209+
210+
bookkeeper.reset();
211+
212+
// Perform some more file operations. All events should be observed
213+
// (because the overflow simulation is no longer running).
214+
Thread.sleep(TestHelper.SHORT_WAIT.toMillis());
215+
Files.delete(directory.resolve("a.txt"));
257216
Files.writeString(directory.resolve("b.txt"), "baz");
258217
Files.createFile(directory.resolve("c.txt"));
259-
Files.delete(directory.resolve("d.txt"));
260218

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);
219+
for (var e : new WatchEvent[] {
220+
new WatchEvent(DELETED, directory, Path.of("a.txt")),
221+
new WatchEvent(MODIFIED, directory, Path.of("b.txt")),
222+
new WatchEvent(CREATED, directory, Path.of("c.txt"))
223+
}) {
224+
await("File operation should trigger event: " + e)
225+
.until(() -> bookkeeper.events().any(e));
226+
}
267227

268-
var nCreatedBeforeOverflow = nCreated.get();
269-
var nModifiedBeforeOverflow = nModified.get();
270-
var nDeletedBeforeOverflow = nDeleted.get();
228+
bookkeeper.reset();
271229

230+
// Generate another `OVERFLOW` event. Synthetic events shouldn't be
231+
// issued and observed (because the index should have been updated).
272232
((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));
233+
234+
await("No events should have been triggered")
235+
.pollDelay(TestHelper.SHORT_WAIT)
236+
.until(() -> bookkeeper.events().kindNot(OVERFLOW).none());
281237
}
282238
}
283239
}

0 commit comments

Comments
 (0)