Skip to content

Commit 2e67f21

Browse files
committed
Generalize auxiliary class Bookkeeper and move it from SingleFileTests to TestHelper to enable reuse across test classes
1 parent 5e7da8e commit 2e67f21

File tree

3 files changed

+111
-41
lines changed

3 files changed

+111
-41
lines changed

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

Lines changed: 19 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,22 @@ 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+
*
135+
* <p>
136+
* Note: This method applies different logic to compare events than (the
137+
* default implementation of) method {@link #equals(Object)}, which
138+
* shouldn't be overridden. This is because events should norminally be
139+
* compared in terms of their identities (e.g., two successive modifications
140+
* of the same file result in events that are equivalent, but not equal;
141+
* they need to be distinguishable in collections).
142+
*/
143+
public static boolean areEquivalent(WatchEvent e1, WatchEvent e2) {
144+
return Objects.equals(e1.getKind(), e2.getKind()) &&
145+
Objects.equals(e1.getRootPath(), e2.getRootPath()) &&
146+
Objects.equals(e1.getRelativePath(), e2.getRelativePath());
147+
}
129148
}

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

Lines changed: 13 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,9 @@
3434

3535
import java.io.IOException;
3636
import java.nio.file.Files;
37-
import java.nio.file.Path;
3837
import java.nio.file.attribute.FileTime;
3938
import java.time.Instant;
40-
import java.util.ArrayList;
41-
import java.util.Arrays;
42-
import java.util.Collections;
43-
import java.util.List;
4439
import java.util.concurrent.atomic.AtomicBoolean;
45-
import java.util.function.Consumer;
46-
import java.util.function.Predicate;
47-
import java.util.stream.Stream;
4840

4941
import org.awaitility.Awaitility;
5042
import org.junit.jupiter.api.AfterEach;
@@ -134,40 +126,38 @@ void singleFileThatMonitorsOnlyADirectory() throws IOException, InterruptedExcep
134126

135127
@Test
136128
void noRescanOnOverflow() throws IOException, InterruptedException {
137-
var bookkeeper = new Bookkeeper();
129+
var bookkeeper = new TestHelper.Bookkeeper();
138130
try (var watch = startWatchAndTriggerOverflow(Approximation.NONE, bookkeeper)) {
139131
Thread.sleep(TestHelper.SHORT_WAIT.toMillis());
140132

141133
await("Overflow shouldn't trigger created, modified, or deleted events")
142-
.until(() -> bookkeeper.fullPaths(CREATED, MODIFIED, DELETED).count() == 0);
134+
.until(() -> bookkeeper.events().kind(CREATED, MODIFIED, DELETED).none());
143135
await("Overflow should be visible to user-defined event handler")
144-
.until(() -> bookkeeper.fullPaths(OVERFLOW).count() == 1);
136+
.until(() -> bookkeeper.events().kind(OVERFLOW).any());
145137
}
146138
}
147139

148140
@Test
149141
void memorylessRescanOnOverflow() throws IOException, InterruptedException {
150-
var bookkeeper = new Bookkeeper();
142+
var bookkeeper = new TestHelper.Bookkeeper();
151143
try (var watch = startWatchAndTriggerOverflow(Approximation.ALL, bookkeeper)) {
152144
Thread.sleep(TestHelper.SHORT_WAIT.toMillis());
153145

154-
var isFile = Predicate.isEqual(watch.getPath());
155-
var isNotFile = Predicate.not(isFile);
146+
var fileName = watch.getPath().getFileName();
147+
var parent = watch.getPath().getParent();
156148

157-
await("Overflow should trigger created event for `file`")
158-
.until(() -> bookkeeper.fullPaths(CREATED).filter(isFile).count() == 1);
149+
await("Overflow should trigger created event for `" + fileName + "`")
150+
.until(() -> bookkeeper.events().kind(CREATED).rootPath(parent).relativePath(fileName).any());
159151
await("Overflow shouldn't trigger created events for other files")
160-
.until(() -> bookkeeper.fullPaths(CREATED).filter(isNotFile).count() == 0);
161-
await("Overflow shouldn't trigger modified events (`file` is empty)")
162-
.until(() -> bookkeeper.fullPaths(MODIFIED).count() == 0);
163-
await("Overflow shouldn't trigger deleted events")
164-
.until(() -> bookkeeper.fullPaths(DELETED).count() == 0);
152+
.until(() -> bookkeeper.events().kind(CREATED).rootPath(parent).relativePathNot(fileName).none());
153+
await("Overflow shouldn't trigger modified or deleted events")
154+
.until(() -> bookkeeper.events().kind(MODIFIED, DELETED).none());
165155
await("Overflow should be visible to user-defined event handler")
166-
.until(() -> bookkeeper.fullPaths(OVERFLOW).count() == 1);
156+
.until(() -> bookkeeper.events().kind(OVERFLOW).any());
167157
}
168158
}
169159

170-
private ActiveWatch startWatchAndTriggerOverflow(Approximation whichFiles, Bookkeeper bookkeeper) throws IOException {
160+
private ActiveWatch startWatchAndTriggerOverflow(Approximation whichFiles, TestHelper.Bookkeeper bookkeeper) throws IOException {
171161
var parent = testDir.getTestDirectory();
172162
var file = parent.resolve("a.txt");
173163

@@ -181,22 +171,4 @@ private ActiveWatch startWatchAndTriggerOverflow(Approximation whichFiles, Bookk
181171
((EventHandlingWatch) watch).handleEvent(overflow);
182172
return watch;
183173
}
184-
185-
private static class Bookkeeper implements Consumer<WatchEvent> {
186-
private final List<WatchEvent> events = Collections.synchronizedList(new ArrayList<>());
187-
188-
public Stream<WatchEvent> events(WatchEvent.Kind... kinds) {
189-
var list = Arrays.asList(kinds.length == 0 ? WatchEvent.Kind.values() : kinds);
190-
return events.stream().filter(e -> list.contains(e.getKind()));
191-
}
192-
193-
public Stream<Path> fullPaths(WatchEvent.Kind... kinds) {
194-
return events(kinds).map(WatchEvent::calculateFullPath);
195-
}
196-
197-
@Override
198-
public void accept(WatchEvent e) {
199-
events.add(e);
200-
}
201-
}
202174
}

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

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,12 @@
2626
*/
2727
package engineering.swat.watch;
2828

29+
import java.nio.file.Path;
2930
import java.time.Duration;
3031
import java.util.Arrays;
32+
import java.util.Queue;
33+
import java.util.concurrent.ConcurrentLinkedQueue;
34+
import java.util.function.Consumer;
3135
import java.util.stream.IntStream;
3236
import java.util.stream.Stream;
3337

@@ -74,4 +78,79 @@ public static <T> Stream<T> streamOf(T[] values, int repetitions, boolean sortBy
7478
IntStream.range(0, repetitions).mapToObj(i -> v));
7579
}
7680
}
81+
82+
public static class Bookkeeper implements Consumer<WatchEvent> {
83+
private final Queue<WatchEvent> events = new ConcurrentLinkedQueue<>();
84+
85+
public Events events() {
86+
return new Events(events.stream());
87+
}
88+
89+
public static class Events {
90+
private final Stream<WatchEvent> stream;
91+
92+
private Events(Stream<WatchEvent> stream) {
93+
this.stream = stream;
94+
}
95+
96+
public boolean any() {
97+
return stream.anyMatch(e -> true);
98+
}
99+
100+
public boolean any(WatchEvent event) {
101+
return stream.anyMatch(e -> WatchEvent.areEquivalent(e, event));
102+
}
103+
104+
public boolean none() {
105+
return stream.noneMatch(e -> true);
106+
}
107+
108+
public boolean none(WatchEvent event) {
109+
return stream.noneMatch(e -> WatchEvent.areEquivalent(e, event));
110+
}
111+
112+
public Events kind(WatchEvent.Kind... kinds) {
113+
return new Events(stream.filter(e -> contains(kinds, e.getKind())));
114+
}
115+
116+
public Events kindNot(WatchEvent.Kind... kinds) {
117+
return new Events(stream.filter(e -> !contains(kinds, e.getKind())));
118+
}
119+
120+
public Events rootPath(Path... rootPaths) {
121+
return new Events(stream.filter(e -> contains(rootPaths, e.getRootPath())));
122+
}
123+
124+
public Events rootPathNot(Path... rootPaths) {
125+
return new Events(stream.filter(e -> !contains(rootPaths, e.getRootPath())));
126+
}
127+
128+
public Events relativePath(Path... relativePaths) {
129+
return new Events(stream.filter(e -> contains(relativePaths, e.getRelativePath())));
130+
}
131+
132+
public Events relativePathNot(Path... relativePaths) {
133+
return new Events(stream.filter(e -> !contains(relativePaths, e.getRelativePath())));
134+
}
135+
136+
private boolean contains(Object[] a, Object key) {
137+
for (var elem : a) {
138+
if (elem.equals(key)) {
139+
return true;
140+
}
141+
}
142+
return false;
143+
}
144+
}
145+
146+
@Override
147+
public void accept(WatchEvent event) {
148+
events.offer(event);
149+
}
150+
151+
@Override
152+
public String toString() {
153+
return events.toString();
154+
}
155+
}
77156
}

0 commit comments

Comments
 (0)