package org.peerbox.watchservice.integration; import static org.junit.Assert.assertTrue; import java.io.IOException; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import java.util.Vector; import java.util.concurrent.BlockingQueue; import org.apache.commons.io.FileUtils; import org.hive2hive.core.utils.H2HWaiter; import org.junit.After; import org.junit.Before; import org.peerbox.BaseJUnitTest; import org.peerbox.client.ClientNode; import org.peerbox.client.NetworkStarter; import org.peerbox.testutils.FileTestUtils; import org.peerbox.watchservice.FileEventManager; import org.peerbox.watchservice.filetree.IFileTree; import org.peerbox.watchservice.filetree.composite.FileComponent; import org.peerbox.watchservice.filetree.composite.FolderComposite; import org.peerbox.watchservice.states.ExecutionHandle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.hash.Hashing; /** * @author Claudio * * This class provides useful functions for integration tests * with two peers like adding and deleting files and folders, * updating files, waiting for file existence or removal and * in particular to check if the internal data structures are * in a well-defined state on both client when the test is * supposed to end. */ public abstract class FileIntegrationTest extends BaseJUnitTest { protected static final Logger logger = LoggerFactory.getLogger(FileIntegrationTest.class); private static NetworkStarter network; protected static Path masterRootPath; protected static Path clientRootPath; protected static final int NUMBER_OF_CHARS = 10; protected static final int WAIT_TIME_VERY_SHORT = 5; protected static final int WAIT_TIME_SHORT = 60; protected static final int WAIT_TIME_LONG = 240; protected static final int WAIT_TIME_STRESSTEST = 60 * 30; protected TestPeerWaspConfig config = new TestPeerWaspConfig(); @Before public void beforeTest() throws IOException { network = new NetworkStarter(); FileUtils.cleanDirectory(network.getBasePath().toFile()); network.start(); // select client-0/ as master (operations will be executed within this path) masterRootPath = network.getRootPaths().get(0); clientRootPath = network.getRootPaths().get(1); } @After public void afterTest() throws IOException { logger.debug("Stop!"); network.stop(); network = null; } protected NetworkStarter getNetwork(){ return network; } protected Path addFolder() throws IOException { Path folder = FileTestUtils.createRandomFolder(masterRootPath); waitForExists(folder, WAIT_TIME_SHORT); return folder; } protected List<Path> addFolders(int numFolders) throws IOException { List<Path> folders = FileTestUtils.createRandomFolders(masterRootPath, numFolders); waitForExists(folders, WAIT_TIME_LONG); return folders; } protected Path addFile() throws IOException { return addFile(NUMBER_OF_CHARS); } protected Path addFile(int size) throws IOException { return addFile(size, true); } protected Path addFile(boolean waitForExists) throws IOException { return addFile(NUMBER_OF_CHARS, waitForExists); } private Path addFile(int size, boolean waitForExists) throws IOException { Path file = FileTestUtils.createRandomFile(masterRootPath, size); if(waitForExists){ waitForExists(file, WAIT_TIME_SHORT); } return file; } protected Path addFileToDestination(Path dstFolder) throws IOException { Path file = FileTestUtils.createRandomFile(dstFolder, NUMBER_OF_CHARS); waitForExists(file, WAIT_TIME_SHORT); assertSyncClientPaths(); return file; } protected List<Path> addFiles(int nrFiles, int toWait) throws IOException { List<Path> files = FileTestUtils.createRandomFiles(masterRootPath, nrFiles, 100); waitForExists(files, toWait); return files; } protected List<Path> addFiles(Path dirPath) throws IOException { List<Path> files = FileTestUtils.createRandomFiles(dirPath, 100, NUMBER_OF_CHARS); waitForExists(files, WAIT_TIME_LONG); assertSyncClientPaths(); return files; } protected List<Path> addSingleFileInFolder() throws IOException { List<Path> files = FileTestUtils.createFolderWithFiles(masterRootPath, 1, NUMBER_OF_CHARS); waitForExists(files, WAIT_TIME_SHORT); return files; } protected List<Path> addSingleFileInManyFolders(int nrFolders) throws IOException { List<Path> files = new ArrayList<Path>(); for(int i = 0; i < nrFolders; i++){ files.addAll(FileTestUtils.createFolderWithFiles(masterRootPath, 1, NUMBER_OF_CHARS)); } waitForExists(files, WAIT_TIME_LONG); return files; } protected List<Path> addManyFilesInFolder(int nrFiles) throws IOException { return addManyFilesInManyFolders(1, nrFiles); //FileTestUtils.createFolderWithFiles(masterRootPath, 10, NUMBER_OF_CHARS); } protected List<Path> addManyFilesInManyFolders(int nrFolders, int nrFilesPerFolder) throws IOException { List<Path> files = new ArrayList<>(); for(int i = 0; i < nrFolders; ++i) { List<Path> f = FileTestUtils.createFolderWithFiles(masterRootPath, nrFilesPerFolder, NUMBER_OF_CHARS); files.addAll(f); } waitForExists(files, WAIT_TIME_LONG); return files; } protected static void deleteSingleFile(Path filePath, boolean waitForNotExists) throws IOException { deleteFileOnClient(filePath, 0); if(waitForNotExists){ waitForNotExists(filePath, WAIT_TIME_SHORT); } } protected static void deleteSingleFile(Path filePath) throws IOException{ deleteSingleFile(filePath, false); } protected void deleteManyFiles(List<Path> files) throws IOException { for(Path file : files){ deleteFileOnClient(file, 0); } waitForNotExists(files, WAIT_TIME_LONG); } protected void updateSingleFile(Path f, boolean wait) throws IOException { FileTestUtils.writeRandomData(f, 100000); if(wait){ waitForUpdate(f, WAIT_TIME_SHORT); } } protected void waitForExists(Path path, int seconds) { H2HWaiter waiter = new H2HWaiter(seconds); do { waiter.tickASecond(); } while(!pathExistsOnAllNodes(path)); } protected void waitForExistsLocally(Path path, int seconds) { H2HWaiter waiter = new H2HWaiter(seconds); do { waiter.tickASecond(); } while(!pathExistsLocally(path)); } protected void waitForNotExistsLocally(List<Path> paths, int seconds){ H2HWaiter waiter = new H2HWaiter(seconds); do{ waiter.tickASecond(); } while(!pathNotExistsLocally(paths)); } protected void waitForNotExistsLocally(Path path, int seconds){ H2HWaiter waiter = new H2HWaiter(seconds); do{ waiter.tickASecond(); } while(!pathNotExistsLocally(path)); } protected void waitForExists(List<Path> paths, int seconds) { H2HWaiter waiter = new H2HWaiter(seconds); do { waiter.tickASecond(); } while(!pathExistsOnAllNodes(paths)); } private static void deleteFileOnClient(Path filePath, int i) { assertTrue(network.getClients().size() == 2); FileEventManager manager = network.getClientNode(0).getFileEventManager(); logger.debug("Delete file: {}", filePath); logger.debug("Manager ID: {}", manager.hashCode()); manager.onLocalFileHardDelete(filePath); sleepMillis(10); } private boolean pathExistsOnAllNodes(Path absPath) { Path relativePath = masterRootPath.relativize(absPath); for(Path rp : network.getRootPaths()) { if(!Files.exists(rp.resolve(relativePath))) { logger.debug("Missing {}", relativePath); return false; } } return true; } private boolean pathExistsOnAllNodes(List<Path> absPaths) { for(Path p : absPaths) { if(!pathExistsOnAllNodes(p)) { logger.debug("Missing {}", p); return false; } } return true; } private boolean pathExistsLocally(Path path){ return path.toFile().exists(); } private boolean pathNotExistsLocally(List<Path> paths){ for(Path p : paths) { if(!pathNotExistsLocally(p)) { return false; } } return true; } private boolean pathNotExistsLocally(Path path){ return !path.toFile().exists(); } protected static void waitForNotExists(Path path, int seconds) { H2HWaiter waiter = new H2HWaiter(seconds); do { waiter.tickASecond(); } while(!pathNotExistsOnAllNodes(path)); } protected static void waitForNotExists(List<Path> paths, int seconds) { H2HWaiter waiter = new H2HWaiter(seconds); do { waiter.tickASecond(); } while(!pathNotExistsOnAllNodes(paths)); } protected static void waitForActionQueueEmpty(BlockingQueue<FileComponent> collection, int seconds){ H2HWaiter waiter = new H2HWaiter(seconds); do { waiter.tickASecond(); Vector<FileComponent> vec = new Vector<FileComponent>(collection); if(vec.size() != 0){ for(int i = 0; i < vec.size(); i++){ logger.debug("Pending in queue: {}. {}:{}", i, vec.get(i).getPath(), vec.get(i).getAction().getCurrentState()); } } } while(!isQueueEmpty(collection)); } protected static void waitForAsyncHandlesEmpty(BlockingQueue<ExecutionHandle> collection, int seconds){ H2HWaiter waiter = new H2HWaiter(seconds); do { waiter.tickASecond(); Vector<ExecutionHandle> vec = new Vector<ExecutionHandle>(collection); if(vec.size() != 0){ for(int i = 0; i < collection.size(); i++){ FileComponent file = vec.get(i).getAction().getFile(); logger.debug("Pending in exec: {}. {}:{}", i, file.getPath(), file.getAction().getCurrentState()); } } } while(!isQueueEmpty(collection)); } private static boolean isQueueEmpty(BlockingQueue<?> queue) { if(queue.size() == 0){ return true; } else { return false; } } private static boolean pathNotExistsOnAllNodes(Path absPath) { Path relativePath = masterRootPath.relativize(absPath); for(Path rp : network.getRootPaths()) { if(Files.exists(rp.resolve(relativePath))) { return false; } } return true; } private static boolean pathNotExistsOnAllNodes(List<Path> absPaths) { for(Path p : absPaths) { if(!pathNotExistsOnAllNodes(p)) { logger.debug("Missing {}", p); return false; } } return true; } protected void waitForUpdate(Path path, int seconds) throws IOException { H2HWaiter waiter = new H2HWaiter(seconds); do { waiter.tickASecond(); } while(!pathEqualsOnAllNodes(path)); } protected void waitForUpdate(List<Path> paths, int seconds) throws IOException { H2HWaiter waiter = new H2HWaiter(seconds); do { waiter.tickASecond(); } while(!pathEqualsOnAllNodes(paths)); } protected void waitForContentEquals(Path source, byte[] content, int seconds) throws IOException{ H2HWaiter waiter = new H2HWaiter(seconds); do { waiter.tickASecond(); } while(!contentIsRecoveredLocally(source, content)); } private boolean contentIsRecoveredLocally(Path source, byte[] content) throws IOException{ byte[] newContent = Files.readAllBytes(source); System.out.println(new String(content)); System.out.println(new String(newContent)); return Arrays.equals(content, newContent); } private boolean pathEqualsOnAllNodes(Path absPath) throws IOException { String thisHash = com.google.common.io.Files.hash(absPath.toFile(), Hashing.sha256()).toString(); Path relativePath = masterRootPath.relativize(absPath); for(Path rp : network.getRootPaths()) { Path otherPath = rp.resolve(relativePath); // make sure path exists already. if(!Files.exists(otherPath)) { return false; } // check hashes String otherHash = com.google.common.io.Files.hash(otherPath.toFile(), Hashing.sha256()).toString(); if(!thisHash.equals(otherHash)) { return false; } } return true; } private boolean pathEqualsOnAllNodes(List<Path> paths) throws IOException { for(Path p : paths) { if(!pathEqualsOnAllNodes(p)) { return false; } } return true; } protected void assertSyncClientPaths() throws IOException { assertSyncClientPaths(true); } /** * Asserts that all root paths of all clients have the same content. * @throws IOException */ protected void assertSyncClientPaths(boolean compareTwoway) throws IOException { // compute client index as a reference IndexRootPath clientIndex = new IndexRootPath(masterRootPath); Files.walkFileTree(masterRootPath, clientIndex); // compare index with other root paths for(Path rp : network.getRootPaths()) { if(rp.equals(masterRootPath)) continue; // ignore comparison with itself IndexRootPath indexOther = new IndexRootPath(rp); Files.walkFileTree(rp, indexOther); assertSyncPathIndices(clientIndex, indexOther); if(compareTwoway) assertSyncPathIndices(indexOther, clientIndex); } logger.info("Client paths are equal!"); } protected void assertRootContains(int nrFiles) throws IOException { IndexRootPath clientIndex = new IndexRootPath(masterRootPath); Files.walkFileTree(masterRootPath, clientIndex); int contained = clientIndex.getHashes().size(); logger.info("Root contains/expected: {}/{}", contained, nrFiles + 1); if(nrFiles == -1){ return; } assertTrue(contained == nrFiles + 1); } protected void assertQueuesAreEmpty(){ List<ClientNode> clients = network.getClients(); for(ClientNode client : clients){ BlockingQueue<FileComponent> queue = client.getFileEventManager().getFileComponentQueue().getQueue(); BlockingQueue<ExecutionHandle> execs = client.getActionExecutor().getRunningJobs(); waitForActionQueueEmpty(queue, WAIT_TIME_LONG); waitForAsyncHandlesEmpty(execs, WAIT_TIME_LONG); assertSetMultimapsAreEmpty(client.getFileEventManager().getFileTree()); } } private static void assertSetMultimapsAreEmpty(IFileTree fileTree) { for(Map.Entry<String, FileComponent> entry : fileTree.getCreatedByContentHash().entries()) logger.trace("Created file left: {}", entry.getValue().getPath()); for(Map.Entry<String, FileComponent> entry : fileTree.getDeletedByContentHash().entries()) logger.trace("Deleted file left: {}", entry.getValue().getPath()); for(Map.Entry<String, FolderComposite> entry : fileTree.getCreatedByStructureHash().entries()) logger.trace("Created folder left: {}", entry.getValue().getPath()); for(Map.Entry<String, FolderComposite> entry : fileTree.getDeletedByStructureHash().entries()) logger.trace("Deleted file left: {}", entry.getValue().getPath()); assertTrue(fileTree.getCreatedByContentHash().size() == 0); assertTrue(fileTree.getCreatedByStructureHash().size() == 0); assertTrue(fileTree.getDeletedByContentHash().size() == 0); assertTrue(fileTree.getDeletedByStructureHash().size() == 0); } /** * Compares and asserts equality of two indices by looking at the paths and hashes of the content * @param indexThis * @param rootOther * @throws IOException */ private void assertSyncPathIndices(IndexRootPath indexThis, IndexRootPath indexOther) throws IOException { compareContentByPaths(indexThis, indexOther); compareContentByHashes(indexThis, indexOther); } private void compareContentByHashes(IndexRootPath indexThis, IndexRootPath indexOther) { for (java.util.Map.Entry<Path, String> e : indexThis.getHashes().entrySet()) { Path relativePath = e.getKey(); String thisHash = e.getValue(); String otherHash = indexOther.getHashes().get(relativePath); boolean hashesEqual = thisHash.equals(otherHash); if (!hashesEqual) { Path thisPath = indexThis.getRootPath().resolve(relativePath); Path otherPath = indexOther.getRootPath().resolve(relativePath); logger.error("Hashes not equal: {} ({}) <-> {} ({})", thisPath, thisHash, otherPath, otherHash); } assertTrue(hashesEqual); } } private void compareContentByPaths(IndexRootPath indexThis, IndexRootPath indexOther) { Set<Path> difference = new HashSet<Path>(indexThis.getHashes().keySet()); difference.removeAll(indexOther.getHashes().keySet()); for (Path relativePath : difference) { Path thisPath = indexThis.getRootPath().resolve(relativePath); Path otherPath = indexOther.getRootPath().resolve(relativePath); logger.error("Different path: {} ({}) <-> {} ({})", thisPath, Files.exists(thisPath) ? "exists" : "not exists", otherPath, Files.exists(otherPath) ? "exists" : "not exists"); } assertTrue(difference.isEmpty()); } /** * Wait the defined time interval. Useful to guarantee different timestamps in * milliseconds if events are programatically created. Furthermore allows to wait * for a cleaned action queue if ActionExecutor.ACTION_TIME_TO_WAIT * 2 is passed * as millisToSleep */ public static void sleepMillis(long millisToSleep){ try { Thread.sleep(millisToSleep); } catch (InterruptedException e) { e.printStackTrace(); } } private class IndexRootPath extends SimpleFileVisitor<Path> { private Path rootPath; private SortedMap<Path, String> pathToHash; public IndexRootPath(Path rootPath) { this.rootPath = rootPath; this.pathToHash = new TreeMap<Path, String>(); } public Path getRootPath() { return rootPath; } public SortedMap<Path, String> getHashes() { return pathToHash; } @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { Path relative = rootPath.relativize(dir); pathToHash.put(relative, ""); return super.preVisitDirectory(dir, attrs); } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { String hash = com.google.common.io.Files.hash(file.toFile(), Hashing.sha256()).toString(); Path relative = rootPath.relativize(file); logger.debug("Add path: {}", file); pathToHash.put(relative, hash); return super.visitFile(file, attrs); } @Override public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { logger.warn("visitFileFailed: {}", exc); return super.visitFileFailed(file, exc); } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { return super.postVisitDirectory(dir, exc); } } /** * This method verifies the following properties on both peers: * 1.) The root folders are synchronized, i.e. their contents are * equal. * 2.) The internal data structures are empty (no pending executing actions, * no pending failed actions, no move candidates left in the various SetMultimaps) * 3.) The root folders contain recursively the defined number of files. this * ensures that no files are missing or unexpectedly existing on both peers. * * @param existingFiles the number of recursively expected files contained in the root folder. * @throws IOException */ protected void assertCleanedUpState(int existingFiles) throws IOException { assertSyncClientPaths(true); assertQueuesAreEmpty(); assertRootContains(existingFiles); } protected void assertCleanedUpState(int existingFiles, boolean compareTwoway) throws IOException { assertSyncClientPaths(compareTwoway); assertQueuesAreEmpty(); assertRootContains(existingFiles); } protected void waitForSynchronized(Path path, int seconds, boolean sync) { H2HWaiter waiter = new H2HWaiter(seconds); do { waiter.tickASecond(); } while(!pathIsSynchronized(path, sync)); } protected boolean pathIsSynchronized(Path path, boolean sync){ IFileTree fileTree = getNetwork().getClients().get(0).getFileEventManager().getFileTree(); if(sync){ if(fileTree.getFile(path) != null && !fileTree.getFile(path).isSynchronized()){ return false; } } else { if(fileTree.getFile(path) != null && fileTree.getFile(path).isSynchronized()){ return false; } } return true; } }