package io.methvin.watchservice; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ListMultimap; import io.methvin.watcher.DirectoryChangeEvent; import io.methvin.watcher.DirectoryChangeListener; import io.methvin.watcher.DirectoryWatcher; import io.methvin.watcher.hashing.FileHasher; import io.methvin.watchservice.FileSystem.FileSystemAction; import org.codehaus.plexus.util.FileUtils; import org.junit.Assume; import org.junit.Test; import java.io.File; import java.io.IOException; import java.nio.file.*; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import static java.nio.file.StandardWatchEventKinds.*; import static org.junit.Assert.*; public class DirectoryWatcherTest { @Test public void validateOsxWatchKeyOverflow() throws Exception { Assume.assumeTrue(System.getProperty("os.name").toLowerCase().contains("mac")); File directory = new File(new File("").getAbsolutePath(), "target/directory"); FileUtils.deleteDirectory(directory); directory.mkdirs(); MacOSXListeningWatchService service = new MacOSXListeningWatchService(); MacOSXWatchKey key = new MacOSXWatchKey(service, ImmutableList.of(ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY), 16); int totalEvents = 0; for (int i = 0; i < 10; i++) { Path toSignal = Paths.get(directory.toPath().toAbsolutePath().toString() + "/" + i); key.signalEvent(ENTRY_CREATE, toSignal); key.signalEvent(ENTRY_MODIFY, toSignal); key.signalEvent(ENTRY_DELETE, toSignal); totalEvents += 3; } int overflowCount = 0; List<WatchEvent<?>> events = key.pollEvents(); for (WatchEvent<?> event : events) { if (event.kind() == OVERFLOW) { overflowCount = event.count(); break; } } assertTrue("OVERFLOW event must exist", overflowCount > 0); assertTrue( "Overflow count must equal number of missing events", totalEvents == events.size() + overflowCount - 1); } @Test public void validateOsxDirectoryWatcher() throws Exception { Assume.assumeTrue(System.getProperty("os.name").toLowerCase().contains("mac")); File directory = new File(new File("").getAbsolutePath(), "target/directory"); FileUtils.deleteDirectory(directory); directory.mkdirs(); runWatcher(directory.toPath(), new MacOSXListeningWatchService()); } @Test public void validateOsxDirectoryWatcherRelativePath() throws Exception { Assume.assumeTrue(System.getProperty("os.name").toLowerCase().contains("mac")); File directory = new File(new File("").getAbsolutePath(), "target/directory"); FileUtils.deleteDirectory(directory); directory.mkdirs(); runWatcher(Paths.get("target/directory"), new MacOSXListeningWatchService()); } @Test public void validateOsxDirectoryWatcherNoHashing() throws Exception { Assume.assumeTrue(System.getProperty("os.name").toLowerCase().contains("mac")); File directory = new File(new File("").getAbsolutePath(), "target/directory"); FileUtils.deleteDirectory(directory); directory.mkdirs(); runWatcher( directory.toPath(), new MacOSXListeningWatchService( new MacOSXListeningWatchService.Config() { @Override public FileHasher fileHasher() { return null; } }), false); } @Test public void validateOsxDirectoryWatcherPreExistingSubdir() throws Exception { Assume.assumeTrue(System.getProperty("os.name").toLowerCase().contains("mac")); File directory = new File(new File("").getAbsolutePath(), "target/directory"); FileUtils.deleteDirectory(directory); directory.mkdirs(); // make a dir before the watch is started File zupDir = Paths.get(directory.toString(), "zup").toFile(); zupDir.mkdir(); assertTrue(zupDir.exists()); // prep a new file for the watched directory File fileInZupDir = new File(zupDir, "fileInZupDir.txt"); assertFalse(fileInZupDir.exists()); // write it to the zup subdirectory of the watched directory Files.write(fileInZupDir.toPath(), "some data".getBytes()); assertTrue(fileInZupDir.exists()); // files are written and done, now start the watcher runWatcher(directory.toPath(), new MacOSXListeningWatchService()); } @Test public void validateJdkDirectoryWatcher() throws Exception { // The JDK watch service is basically unusable on mac since it polls every 10s Assume.assumeFalse(System.getProperty("os.name").toLowerCase().contains("mac")); File directory = new File(new File("").getAbsolutePath(), "target/directory"); FileUtils.deleteDirectory(directory); directory.mkdirs(); runWatcher(directory.toPath(), FileSystems.getDefault().newWatchService()); } private void runWatcher(Path directory, WatchService watchService) throws Exception { runWatcher(directory, watchService, true); } private void runWatcher(Path directory, WatchService watchService, boolean fileHashing) throws Exception { // // start our service // play our events // stop when all our events have been drained and processed // // We wait 500ms before deletes are executed because any faster and the MacOS implementation // appears to not see them because the create/delete pair happen so fast it's like the file // is never there at all. int waitInMs = 500; FileSystem fileSystem = new FileSystem(directory) .wait(waitInMs) .create("one.txt") .wait(waitInMs) .create("two.txt") .wait(waitInMs) .create("three.txt") .wait(waitInMs) .update("three.txt", " 111111") .wait(waitInMs) .update("three.txt", " 222222") .wait(waitInMs) .delete("one.txt") .wait(waitInMs) .directory("testDir") .wait(waitInMs) .create("testDir/file1InDir.txt") .wait(waitInMs) .create("testDir/file2InDir.txt", " 111111") .wait(waitInMs) .update("testDir/file2InDir.txt", " 222222") .wait(waitInMs); // Collect our filesystem actions List<FileSystemAction> actions = fileSystem.actions(); TestDirectoryChangeListener listener = new TestDirectoryChangeListener(directory.toAbsolutePath(), actions, fileHashing); DirectoryWatcher watcher = DirectoryWatcher.builder() .path(directory) .listener(listener) .watchService(watchService) .fileHashing(fileHashing) .build(); // Fire up the filesystem watcher CompletableFuture<Void> future = watcher.watchAsync(); // Play our filesystem events fileSystem.playActions(); // Wait for the future to complete which is when the right number of events are captured future.get(10, TimeUnit.SECONDS); ListMultimap<String, WatchEvent.Kind<?>> events = listener.events; // Close the watcher watcher.close(); // Let's see if everything works! assertEquals("actions.size", actions.size(), events.size()); // // Now we make a map of the events keyed by the path. The order in which we // play the filesystem actions is not necessarily the order in which the events are // emitted. In the test above I often see the create file event for three.txt before // two.txt. We just want to make sure that the action for a particular path agrees // with the corresponding event for that file. For a given path we definitely want // the order of the played actions to match the order of the events emitted. // List<WatchEvent.Kind<?>> one = events.get("one.txt"); assertEquals("one.size", 2, one.size()); assertEquals(one.get(0), actions.get(0).kind); assertEquals(one.get(1), actions.get(5).kind); List<WatchEvent.Kind<?>> two = events.get("two.txt"); assertEquals("two.size", 1, two.size()); assertEquals(two.get(0), actions.get(1).kind); List<WatchEvent.Kind<?>> three = events.get("three.txt"); assertEquals("three.size", 3, three.size()); assertEquals(three.get(0), actions.get(2).kind); assertEquals(three.get(1), actions.get(3).kind); assertEquals(three.get(2), actions.get(4).kind); List<WatchEvent.Kind<?>> testDir = events.get("testDir"); assertEquals("testDir.size", 1, testDir.size()); assertEquals(testDir.get(0), actions.get(6).kind); List<WatchEvent.Kind<?>> four = events.get("testDir/file1InDir.txt"); assertEquals("four.size", 1, four.size()); assertEquals(three.get(0), actions.get(7).kind); List<WatchEvent.Kind<?>> five = events.get("testDir/file2InDir.txt"); assertEquals("five.size", 2, five.size()); assertEquals(three.get(0), actions.get(8).kind); assertEquals(three.get(1), actions.get(9).kind); } class TestDirectoryChangeListener implements DirectoryChangeListener { final Path directory; final List<FileSystemAction> actions; final ListMultimap<String, WatchEvent.Kind<?>> events = ArrayListMultimap.create(); final int totalActions; final boolean fileHashing; int actionsProcessed = 0; // keep track of recent creates so we can ignore create/modify pairs final ConcurrentHashMap<Path, Long> createTimes = new ConcurrentHashMap<>(); public TestDirectoryChangeListener( Path directory, List<FileSystemAction> actions, boolean fileHashing) { this.directory = directory; this.actions = actions; this.fileHashing = fileHashing; this.totalActions = actions.size(); } @Override public void onEvent(DirectoryChangeEvent event) throws IOException { if (event.eventType() == DirectoryChangeEvent.EventType.CREATE) { createTimes.putIfAbsent(event.path(), System.currentTimeMillis()); } if (!fileHashing && event.eventType() == DirectoryChangeEvent.EventType.MODIFY && System.currentTimeMillis() - createTimes.getOrDefault(event.path(), 0L) < 10) { /* ignore this event since it's a create paired with a modify, which we allow when file hashing is disabled */ return; } updateActions(event.path(), event.eventType().getWatchEventKind()); } void updateActions(Path path, WatchEvent.Kind<?> kind) { String relativePath = directory.relativize(path).toString().replace('\\', '/'); System.out.println(kind + " ----> " + relativePath); events.put(relativePath, kind); actionsProcessed++; System.out.println(actionsProcessed + "/" + totalActions + " actions processed."); } @Override public boolean isWatching() { return actionsProcessed < totalActions; } } }