|
26 | 26 | */ |
27 | 27 | package engineering.swat.watch; |
28 | 28 |
|
| 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; |
29 | 32 | import static engineering.swat.watch.WatchEvent.Kind.OVERFLOW; |
30 | 33 | import static org.awaitility.Awaitility.await; |
31 | 34 |
|
32 | 35 | import java.io.IOException; |
33 | 36 | import java.nio.file.Files; |
34 | | -import java.util.concurrent.Semaphore; |
| 37 | +import java.nio.file.Path; |
35 | 38 | import java.util.concurrent.atomic.AtomicBoolean; |
36 | | -import java.util.concurrent.atomic.AtomicInteger; |
37 | | -import java.util.function.Predicate; |
38 | 39 |
|
39 | 40 | import org.awaitility.Awaitility; |
40 | 41 | import org.junit.jupiter.api.AfterEach; |
@@ -130,154 +131,105 @@ void memorylessRescanOnOverflow() throws IOException, InterruptedException { |
130 | 131 | Files.writeString(directory.resolve("a.txt"), "foo"); |
131 | 132 | Files.writeString(directory.resolve("b.txt"), "bar"); |
132 | 133 |
|
133 | | - var nCreated = new AtomicInteger(); |
134 | | - var nModified = new AtomicInteger(); |
135 | | - var nOverflow = new AtomicInteger(); |
| 134 | + var bookkeeper = new TestHelper.Bookkeeper(); |
136 | 135 | var watchConfig = Watcher.watch(directory, WatchScope.PATH_AND_CHILDREN) |
137 | 136 | .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); |
153 | 138 |
|
154 | 139 | try (var watch = watchConfig.start()) { |
155 | | - var overflow = new WatchEvent(WatchEvent.Kind.OVERFLOW, directory); |
| 140 | + var overflow = new WatchEvent(OVERFLOW, directory); |
156 | 141 | ((EventHandlingWatch) watch).handleEvent(overflow); |
157 | | - Thread.sleep(TestHelper.SHORT_WAIT.toMillis()); |
158 | 142 |
|
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) |
163 | 143 | 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)); |
165 | 163 | } |
166 | 164 | } |
167 | 165 |
|
168 | 166 | @Test |
169 | 167 | 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 | | - |
174 | 168 | 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(); |
180 | 169 |
|
| 170 | + var bookkeeper = new TestHelper.Bookkeeper(); |
| 171 | + var dropEvents = new AtomicBoolean(false); // Toggles overflow simulation |
181 | 172 | var watchConfig = Watcher.watch(directory, WatchScope.PATH_AND_CHILDREN) |
| 173 | + .filter(e -> !dropEvents.get()) |
182 | 174 | .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); |
207 | 176 |
|
208 | 177 | 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. |
212 | 178 |
|
| 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). |
213 | 184 | Files.writeString(directory.resolve("a.txt"), "foo"); |
214 | 185 | Files.writeString(directory.resolve("b.txt"), "bar"); |
215 | 186 | Files.delete(directory.resolve("c.txt")); |
216 | 187 | 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.) |
227 | 188 |
|
| 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); |
228 | 196 | var overflow = new WatchEvent(WatchEvent.Kind.OVERFLOW, directory); |
229 | 197 | ((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(); |
257 | 212 | Files.writeString(directory.resolve("b.txt"), "baz"); |
258 | 213 | Files.createFile(directory.resolve("c.txt")); |
259 | 214 | Files.delete(directory.resolve("d.txt")); |
260 | 215 |
|
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(); |
272 | 228 | ((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()); |
281 | 233 | } |
282 | 234 | } |
283 | 235 | } |
0 commit comments