|
26 | 26 | */ |
27 | 27 | package engineering.swat.watch; |
28 | 28 |
|
| 29 | +import static engineering.swat.watch.WatchEvent.Kind.OVERFLOW; |
29 | 30 | import static org.awaitility.Awaitility.await; |
30 | 31 |
|
31 | 32 | import java.io.IOException; |
32 | 33 | import java.nio.file.Files; |
| 34 | +import java.util.concurrent.Semaphore; |
33 | 35 | import java.util.concurrent.atomic.AtomicBoolean; |
34 | 36 | import java.util.concurrent.atomic.AtomicInteger; |
35 | 37 | import java.util.function.Predicate; |
@@ -165,47 +167,106 @@ void memorylessRescanOnOverflow() throws IOException, InterruptedException { |
165 | 167 |
|
166 | 168 | @Test |
167 | 169 | 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 | + |
168 | 174 | var directory = testDir.getTestDirectory(); |
169 | | - Files.writeString(directory.resolve("a.txt"), "foo"); |
170 | | - Files.writeString(directory.resolve("b.txt"), "bar"); |
171 | 175 |
|
| 176 | + var semaphore = new Semaphore(0); |
172 | 177 | var nCreated = new AtomicInteger(); |
173 | 178 | var nModified = new AtomicInteger(); |
| 179 | + var nDeleted = new AtomicInteger(); |
| 180 | + |
174 | 181 | var watchConfig = Watcher.watch(directory, WatchScope.PATH_AND_CHILDREN) |
175 | 182 | .approximate(OnOverflow.DIRTY) |
176 | 183 | .on(e -> { |
177 | | - switch (e.getKind()) { |
178 | | - case CREATED: |
179 | | - nCreated.incrementAndGet(); |
180 | | - break; |
181 | | - case MODIFIED: |
182 | | - nModified.incrementAndGet(); |
183 | | - break; |
184 | | - default: |
185 | | - break; |
| 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(); |
186 | 205 | } |
187 | 206 | }); |
188 | 207 |
|
189 | 208 | try (var watch = watchConfig.start()) { |
| 209 | + // At this point, the index of last-modified-times inside `watch` is |
| 210 | + // populated with initial values. |
| 211 | + |
| 212 | + Files.writeString(directory.resolve("a.txt"), "foo"); |
| 213 | + Files.writeString(directory.resolve("b.txt"), "bar"); |
| 214 | + Files.delete(directory.resolve("c.txt")); |
| 215 | + Files.createFile(directory.resolve("d.txt")); |
| 216 | + // At this point, regular events have been generated for a.txt, |
| 217 | + // b.txt, c.txt, and d.txt by the file system. These events won't be |
| 218 | + // handled by `watch` just yet, though, because the semaphore is |
| 219 | + // still empty (i.e., event-handling threads are blocked from making |
| 220 | + // progress). Thus, the index inside `watch` still contains the |
| 221 | + // initial last-modified-times. (Warning: The blockade works only |
| 222 | + // when the rescanner runs after the user-defined event-handler. |
| 223 | + // Currently, this is the case, but changing their order probably |
| 224 | + // breaks this test.) |
| 225 | + |
190 | 226 | var overflow = new WatchEvent(WatchEvent.Kind.OVERFLOW, directory); |
191 | 227 | ((EventHandlingWatch) watch).handleEvent(overflow); |
192 | | - |
193 | 228 | Thread.sleep(TestHelper.NORMAL_WAIT.toMillis()); |
194 | | - await("Overflow shouldn't trigger created events") |
195 | | - .until(nCreated::get, Predicate.isEqual(0)); |
196 | | - await("Overflow shouldn't trigger modified events") |
197 | | - .until(nModified::get, Predicate.isEqual(0)); |
| 229 | + // At this point, the current thread has presumably slept long |
| 230 | + // enough for the `OVERFLOW` event to have been handled by the |
| 231 | + // rescanner. This means that synthetic events must have been issued |
| 232 | + // (because the index still contained the initial last-modified |
| 233 | + // times). |
| 234 | + |
| 235 | + // Readiness achieved: Threads can now start handling non-`OVERFLOW` |
| 236 | + // events. |
| 237 | + semaphore.release(); |
198 | 238 |
|
| 239 | + await("Overflow should trigger created events") |
| 240 | + .until(nCreated::get, Predicate.isEqual(2)); // 1 synthetic event + 1 regular event |
| 241 | + await("Overflow should trigger modified events") |
| 242 | + .until(nModified::get, Predicate.isEqual(4)); // 2 synthetic events + 2 regular events |
| 243 | + await("Overflow should trigger deleted events") |
| 244 | + .until(nDeleted::get, Predicate.isEqual(2)); // 1 synthetic event + 1 regular event |
| 245 | + |
| 246 | + // Let's do some more file operations, trigger another `OVERFLOW` |
| 247 | + // event, and observe that synthetic events *aren't* issued this |
| 248 | + // time (because the index was already updated when the regular |
| 249 | + // events were handled). |
199 | 250 | Files.writeString(directory.resolve("b.txt"), "baz"); |
200 | | - await("File write should trigger modified event") |
201 | | - .until(nModified::get, Predicate.isEqual(1)); |
| 251 | + Files.createFile(directory.resolve("c.txt")); |
| 252 | + Files.delete(directory.resolve("d.txt")); |
| 253 | + |
| 254 | + await("File create should trigger regular created event") |
| 255 | + .until(nCreated::get, Predicate.isEqual(3)); |
| 256 | + await("File write should trigger regular modified event") |
| 257 | + .until(nModified::get, Predicate.isEqual(5)); |
| 258 | + await("File delete should trigger regular deleted event") |
| 259 | + .until(nDeleted::get, Predicate.isEqual(3)); |
202 | 260 |
|
203 | 261 | ((EventHandlingWatch) watch).handleEvent(overflow); |
204 | 262 | Thread.sleep(TestHelper.NORMAL_WAIT.toMillis()); |
205 | | - await("Overflow shouldn't trigger created event after file write (and index updated)") |
206 | | - .until(nCreated::get, Predicate.isEqual(0)); |
207 | | - await("Overflow shouldn't trigger modified event after file write (and index updated)") |
208 | | - .until(nModified::get, Predicate.isEqual(1)); // Still 1 (because of the real MODIFIED) |
| 263 | + |
| 264 | + await("Overflow shouldn't trigger synthetic created event after file create (and index updated)") |
| 265 | + .until(nCreated::get, Predicate.isEqual(3)); |
| 266 | + await("Overflow shouldn't trigger synthetic modified event after file write (and index updated)") |
| 267 | + .until(nModified::get, Predicate.isEqual(5)); |
| 268 | + await("Overflow shouldn't trigger synthetic deleted event after file delete (and index updated)") |
| 269 | + .until(nDeleted::get, Predicate.isEqual(3)); |
209 | 270 | } |
210 | 271 | } |
211 | 272 | } |
0 commit comments