package co.codewizards.cloudstore.test; import static co.codewizards.cloudstore.core.oio.OioFileFactory.*; import static org.assertj.core.api.Assertions.*; import java.io.IOException; import java.util.Collection; import java.util.UUID; import mockit.Invocation; import mockit.Mock; import mockit.MockUp; import mockit.integration.junit4.JMockit; import net.jcip.annotations.NotThreadSafe; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import co.codewizards.cloudstore.client.CloudStoreClient; import co.codewizards.cloudstore.core.objectfactory.ObjectFactory; import co.codewizards.cloudstore.core.oio.File; import co.codewizards.cloudstore.core.progress.LoggerProgressMonitor; import co.codewizards.cloudstore.core.repo.local.LocalRepoManager; import co.codewizards.cloudstore.core.repo.local.LocalRepoTransaction; import co.codewizards.cloudstore.core.repo.sync.RepoToRepoSync; import co.codewizards.cloudstore.local.persistence.FileInProgressMarker; import co.codewizards.cloudstore.local.persistence.FileInProgressMarkerDao; import co.codewizards.cloudstore.local.transport.TempChunkFileManager; /** * TODO rewrite this entire test! It is currently based on pretty fragile multi-threading. It might be better to use a different approach. Marco :-) * Maybe we should using mocking or somehow replace the real services by some sub-classes that interact with the test. This should * be more reliable than watching the file system from the outside on a different thread than the actual sync. * * @author Sebastian Schefczyk */ @RunWith(JMockit.class) @NotThreadSafe // seems to be necessary because mocking of ObjectFactory otherwise does not work :-( public class SyncAbortIT extends AbstractRepoAwareIT { private static final Logger logger = LoggerFactory.getLogger(SyncAbortIT.class); private LocalRepoManager localRepoManagerLocal; private LocalRepoManager localRepoManagerRemote; private enum Sync { /** local to remote */ UP, /** remote to local */ DOWN } @Override @Before public void before() throws Exception { super.before(); // I tried to directly mock the RepoToRepoSync in a downstream project and was not able to do so. Mocking the // object factory works well, though => mocking here the ObjectFactory instead to return our actual mock. new MockUp<ObjectFactory>() { @Mock <T> T createObject(Invocation invocation, Class<T> clazz, Class<?>[] parameterTypes, Object ... parameters) { if (TempChunkFileManager.class.isAssignableFrom(clazz)) { return clazz.cast(new MockTempChunkFileManager()); } return invocation.proceed(); } }; localPathPrefix = ""; remotePathPrefix = ""; localRoot = newTestRepositoryLocalRoot("local"); assertThat(localRoot.exists()).isFalse(); localRoot.mkdirs(); assertThat(localRoot.isDirectory()).isTrue(); remoteRoot = newTestRepositoryLocalRoot("remote"); assertThat(remoteRoot.exists()).isFalse(); remoteRoot.mkdirs(); assertThat(remoteRoot.isDirectory()).isTrue(); localRepoManagerLocal = localRepoManagerFactory.createLocalRepoManagerForNewRepository(localRoot); assertThat(localRepoManagerLocal).isNotNull(); localRepoManagerRemote = localRepoManagerFactory.createLocalRepoManagerForNewRepository(remoteRoot); assertThat(localRepoManagerRemote).isNotNull(); final UUID remoteRepositoryId = localRepoManagerRemote.getRepositoryId(); remoteRootURLWithPathPrefix = getRemoteRootURLWithPathPrefix(remoteRepositoryId); new CloudStoreClient("requestRepoConnection", getLocalRootWithPathPrefix().getPath(), remoteRootURLWithPathPrefix.toExternalForm()).execute(); new CloudStoreClient("acceptRepoConnection", getRemoteRootWithPathPrefix().getPath()).execute(); // initially there should be no files in progress! assertNoFilesInProgress(); } /** * Special TempChunkFileManager slowing down operations in order to make watching them asynchronously * more reliable. * <p> * I had a few times the situation that tests worked fine on one machine and didn't on another. * This seemed to be depending on CPU and disk as the tests here are heavily relying on multi-threading. */ private static class MockTempChunkFileManager extends TempChunkFileManager { protected MockTempChunkFileManager() { System.err.println("MockTempChunkFileManager instantiated."); } @Override protected synchronized File createTempChunkFile(File destFile, long offset, boolean createNewFile) { File result = super.createTempChunkFile(destFile, offset, createNewFile); System.err.println("createTempChunkFile: " + destFile.getName() + "; createNewFile=" + createNewFile); if (createNewFile) sleep(200); return result; } @Override protected void moveOrFail(File oldFile, File newFile) throws IOException { super.moveOrFail(oldFile, newFile); System.err.println("moveOrFail: " + oldFile.getName() + " => " + newFile.getName()); sleep(200); } @Override protected void deleteOrFail(File file) throws IOException { super.deleteOrFail(file); System.err.println("deleteOrFail: " + file.getName()); sleep(200); } } private static void sleep(long millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { logger.warn("sleep: " + e, e); } } @Test public void syncAbortResume_remoteToLocal() throws Exception { // one file, on remote-side, made of exactly two chunks final String fileName = "a"; final File file = createFileWithChunks(remoteRoot, remoteRoot, fileName, 2); final FileWatcher fileWatcher = new FileWatcher(localRoot, fileName, file.length()); // special: delegate the repoToRepoSync.sync into fileWatcher, to be // able to interrupt immediately. try (RepoToRepoSync repoToRepoSync = RepoToRepoSync.create(getLocalRootWithPathPrefix(), remoteRootURLWithPathPrefix);) { // the sync will start and get interrupted inside the fileWatcher! fileWatcher.syncOneChunk(repoToRepoSync, new LoggerProgressMonitor(logger)); } assertFilesInProgress(Sync.DOWN, 1); try (RepoToRepoSync repoToRepoSync = RepoToRepoSync.create(getLocalRootWithPathPrefix(), remoteRootURLWithPathPrefix);) { fileWatcher.createDeleteChunks(repoToRepoSync, localRepoManagerLocal, new LoggerProgressMonitor(logger), 1, 2); } assertThatFilesInRepoAreCorrect(remoteRoot); localRepoManagerLocal.close(); localRepoManagerRemote.close(); assertThatNoCollisionInRepo(localRoot); assertThatNoCollisionInRepo(remoteRoot); assertDirectoriesAreEqualRecursively(getLocalRootWithPathPrefix(), getRemoteRootWithPathPrefix()); } @Test public void syncAbortResume_localToRemote() throws Exception { final String fileName = "b"; final File file = createFileWithChunks(localRoot, localRoot, fileName, 2); final FileWatcher fileWatcher = new FileWatcher(remoteRoot, fileName, file.length()); // special: delegate the repoToRepoSync.sync into fileWatcher, to be // able to interrupt immediately. try (RepoToRepoSync repoToRepoSync = RepoToRepoSync.create(getLocalRootWithPathPrefix(), remoteRootURLWithPathPrefix);) { // the sync will start and get interrupted inside the fileWatcher! fileWatcher.syncOneChunk(repoToRepoSync, new LoggerProgressMonitor(logger)); } assertFilesInProgress(Sync.UP, 1); try (RepoToRepoSync repoToRepoSync = RepoToRepoSync.create(getLocalRootWithPathPrefix(), remoteRootURLWithPathPrefix);) { fileWatcher.createDeleteChunks(repoToRepoSync, localRepoManagerRemote, new LoggerProgressMonitor(logger), 1, 2); } afterSyncCompleteAssertionsAndCloseOperations(localRoot); } @Test public void syncAbortResume_remoteToLocal_deleteChunk() throws Exception { final String fileName = "c"; final File file = createFileWithChunks(remoteRoot, remoteRoot, fileName, 2); final FileWatcher fileWatcher = new FileWatcher(localRoot, fileName, file.length()); // special: delegate the repoToRepoSync.sync into fileWatcher, to be // able to interrupt immediately. try (RepoToRepoSync repoToRepoSync = RepoToRepoSync.create(getLocalRootWithPathPrefix(), remoteRootURLWithPathPrefix);) { // the sync will start and get interrupted inside the fileWatcher! fileWatcher.syncOneChunk(repoToRepoSync, new LoggerProgressMonitor(logger)); } // check on file inProgress assertFilesInProgress(Sync.DOWN, 1); // delete the chunks; the sync algorithm should tolerate this fileWatcher.deleteTempDir(); try (RepoToRepoSync repoToRepoSync = RepoToRepoSync.create(getLocalRootWithPathPrefix(), remoteRootURLWithPathPrefix);) { fileWatcher.createDeleteChunks(repoToRepoSync, localRepoManagerLocal, new LoggerProgressMonitor(logger), 2, 2); } afterSyncCompleteAssertionsAndCloseOperations(remoteRoot); } @Test public void syncAbortResume_remoteToLocal_deleteSource() throws Exception { final String fileName = "d"; final File file = createFileWithChunks(remoteRoot, remoteRoot, fileName, 2); final FileWatcher fileWatcher = new FileWatcher(localRoot, fileName, file.length()); // special: delegate the repoToRepoSync.sync into fileWatcher, to be // able to interrupt immediately. try (RepoToRepoSync repoToRepoSync = RepoToRepoSync.create(getLocalRootWithPathPrefix(), remoteRootURLWithPathPrefix);) { // the sync will start and get interrupted inside the fileWatcher! fileWatcher.syncOneChunk(repoToRepoSync, new LoggerProgressMonitor(logger)); } // check on file inProgress assertFilesInProgress(Sync.DOWN, 1); // delete the chunks; the sync algorithm should tolerate this fileWatcher.deleteTempDir(); try (RepoToRepoSync repoToRepoSync = RepoToRepoSync.create(getLocalRootWithPathPrefix(), remoteRootURLWithPathPrefix);) { fileWatcher.createDeleteChunks(repoToRepoSync, localRepoManagerLocal, new LoggerProgressMonitor(logger), 2, 2); } afterSyncCompleteAssertionsAndCloseOperations(remoteRoot); } @Test public void syncAbortResume_remoteToLocal_modifySource() throws Exception { final String fileName = "e"; File file = createFileWithChunks(remoteRoot, remoteRoot, fileName, 2); final FileWatcher fileWatcher = new FileWatcher(localRoot, fileName, file.length()); // special: delegate the repoToRepoSync.sync into fileWatcher, to be // able to interrupt immediately. try (RepoToRepoSync repoToRepoSync = RepoToRepoSync.create(getLocalRootWithPathPrefix(), remoteRootURLWithPathPrefix);) { // the sync will start and get interrupted inside the fileWatcher! fileWatcher.syncOneChunk(repoToRepoSync, new LoggerProgressMonitor(logger)); } // check on file inProgress assertFilesInProgress(Sync.DOWN, 1); // modify the source file file = createFileWithChunks(remoteRoot, remoteRoot, fileName, 2); try (RepoToRepoSync repoToRepoSync = RepoToRepoSync.create(getLocalRootWithPathPrefix(), remoteRootURLWithPathPrefix);) { // chunks that will differ after modification of the source file // will be overwritten without deletion; so no difference in amount // of creation/deletion. fileWatcher.createDeleteChunks(repoToRepoSync, localRepoManagerLocal, new LoggerProgressMonitor(logger), 1, 2, file.length()); } afterSyncCompleteAssertionsAndCloseOperations(remoteRoot); } @Test public void syncAbortResume_remoteToLocal_renameSource() throws Exception { final String fileName = "ee"; final File file = createFileWithChunks(remoteRoot, remoteRoot, fileName, 2); final FileWatcher fileWatcher = new FileWatcher(localRoot, fileName, file.length()); // special: delegate the repoToRepoSync.sync into fileWatcher, to be // able to interrupt immediately. try (RepoToRepoSync repoToRepoSync = RepoToRepoSync.create(getLocalRootWithPathPrefix(), remoteRootURLWithPathPrefix);) { // the sync will start and get interrupted inside the fileWatcher! fileWatcher.syncOneChunk(repoToRepoSync, new LoggerProgressMonitor(logger)); } // check on file inProgress assertFilesInProgress(Sync.DOWN, 1); // modify the source file final String newFileName = "ee-renamed"; moveFile(remoteRoot, file, createFile(remoteRoot, newFileName)); try (RepoToRepoSync repoToRepoSync = RepoToRepoSync.create(getLocalRootWithPathPrefix(), remoteRootURLWithPathPrefix);) { // because the chunk will be moved, the move operation is also observed as create/delete; so for the first // chunk (move: 1 create, 1 delete), for the second (1 create); after appending the chunks to the // destination file, 2 deletes. fileWatcher.createDeleteChunks(repoToRepoSync, localRepoManagerLocal, new LoggerProgressMonitor(logger), 2, 3, newFileName); } afterSyncCompleteAssertionsAndCloseOperations(remoteRoot); } @Test public void syncAbortResume_localToRemote_renameSource() throws Exception { final String fileName = "lrrs"; final File file = createFileWithChunks(localRoot, localRoot, fileName, 2); final FileWatcher fileWatcher = new FileWatcher(remoteRoot, fileName, file.length()); // special: delegate the repoToRepoSync.sync into fileWatcher, to be // able to interrupt immediately. try (RepoToRepoSync repoToRepoSync = RepoToRepoSync.create(getLocalRootWithPathPrefix(), remoteRootURLWithPathPrefix);) { // the sync will start and get interrupted inside the fileWatcher! fileWatcher.syncOneChunk(repoToRepoSync, new LoggerProgressMonitor(logger)); } // check on file inProgress assertFilesInProgress(Sync.UP, 1); // modify the source file final String newFileName = "lrrs-renamed"; moveFile(localRoot, file, createFile(localRoot, newFileName)); try (RepoToRepoSync repoToRepoSync = RepoToRepoSync.create(getLocalRootWithPathPrefix(), remoteRootURLWithPathPrefix);) { // because the chunk will be moved, the move operation is also observed as create/delete; so for the first // chunk (move: 1 create, 1 delete), for the second (1 create); after appending the chunks to the // destination file, 2 deletes. fileWatcher.createDeleteChunks(repoToRepoSync, localRepoManagerRemote, new LoggerProgressMonitor(logger), 2, 3, newFileName); } afterSyncCompleteAssertionsAndCloseOperations(localRoot); } @Test public void syncAbortResume_remoteToLocal_watchOrder() throws Exception { final String fileName1 = "f1"; final File file1 = createFileWithChunks(remoteRoot, remoteRoot, fileName1, 2); final FileWatcher fileWatcher = new FileWatcher(localRoot, fileName1, file1.length()); // special: delegate the repoToRepoSync.sync into fileWatcher, to be // able to interrupt immediately. try (RepoToRepoSync repoToRepoSync = RepoToRepoSync.create(getLocalRootWithPathPrefix(), remoteRootURLWithPathPrefix);) { // the sync will start and get interrupted inside the fileWatcher! fileWatcher.syncOneChunk(repoToRepoSync, new LoggerProgressMonitor(logger)); } // check on file inProgress assertFilesInProgress(Sync.DOWN, 1); final String fileName0 = "f0"; final String fileName2 = "f2"; final File file0 = createFileWithChunks(remoteRoot, remoteRoot, fileName0, 2); final File file2 = createFileWithChunks(remoteRoot, remoteRoot, fileName2, 2); try (RepoToRepoSync repoToRepoSync = RepoToRepoSync.create(getLocalRootWithPathPrefix(), remoteRootURLWithPathPrefix);) { // we expect the aborted file to resume at first, then syncing the rest (and not again the first). fileWatcher.watchSyncOrder(repoToRepoSync, localRepoManagerLocal, new LoggerProgressMonitor(logger), fileName1, file1.length(), fileName0, file0.length(), fileName2, file2.length()); } afterSyncCompleteAssertionsAndCloseOperations(remoteRoot); } /** * Assert and close operations needed on every test! * * @throws IOException */ private void afterSyncCompleteAssertionsAndCloseOperations(final File root) throws IOException { // re-check files inProgress: assertNoFilesInProgress(); assertThatFilesInRepoAreCorrect(root); localRepoManagerLocal.close(); localRepoManagerRemote.close(); assertThatNoCollisionInRepo(localRoot); assertThatNoCollisionInRepo(remoteRoot); assertDirectoriesAreEqualRecursively(getLocalRootWithPathPrefix(), getRemoteRootWithPathPrefix()); } private void assertNoFilesInProgress() { // Primary check proves against zero, the rest will also be checked against zero: assertFilesInProgress(Sync.UP, 0); } /** * Check files in progress (fileWatcher should have caused an interrupted sync state). The opposite direction and * both directions on the other side must be zero. * @param syncDirection * UP would be local to remote, and DOWN vice versa. * @param size * The expected amount of files in progress on the local side. */ @SuppressWarnings("resource") private void assertFilesInProgress(final Sync syncDirection, final int size) { final LocalRepoManager fromRepoManager = (syncDirection.equals(Sync.UP)) ? localRepoManagerLocal : localRepoManagerRemote; final LocalRepoManager toRepoManager = (syncDirection.equals(Sync.DOWN)) ? localRepoManagerLocal : localRepoManagerRemote; try (final LocalRepoTransaction transaction = localRepoManagerLocal.beginWriteTransaction();) { final FileInProgressMarkerDao fileInProgressMarkerDao = transaction.getDao(FileInProgressMarkerDao.class); Collection<FileInProgressMarker> fileInProgressMarkers = fileInProgressMarkerDao.getFileInProgressMarkers(fromRepoManager.getRepositoryId(), toRepoManager.getRepositoryId()); assertThat(fileInProgressMarkers.size()).isEqualTo(size); // also assert files are not in Progress in the opposite direction: fileInProgressMarkers = fileInProgressMarkerDao.getFileInProgressMarkers(toRepoManager.getRepositoryId(), fromRepoManager.getRepositoryId()); assertThat(fileInProgressMarkers.size()).isEqualTo(0); } // also assert there is nothing on the other side (local / remote): try (final LocalRepoTransaction transaction = localRepoManagerRemote.beginWriteTransaction();) { final FileInProgressMarkerDao fileInProgressMarkerDao = transaction.getDao(FileInProgressMarkerDao.class); Collection<FileInProgressMarker> fileInProgressMarkers = fileInProgressMarkerDao.getFileInProgressMarkers(fromRepoManager.getRepositoryId(), toRepoManager.getRepositoryId()); assertThat(fileInProgressMarkers.size()).isEqualTo(0); fileInProgressMarkers = fileInProgressMarkerDao.getFileInProgressMarkers(toRepoManager.getRepositoryId(), fromRepoManager.getRepositoryId()); assertThat(fileInProgressMarkers.size()).isEqualTo(0); } } }