diff --git a/main/commons-test/.gitignore b/main/commons-test/.gitignore new file mode 100644 index 000000000..b83d22266 --- /dev/null +++ b/main/commons-test/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/main/commons-test/pom.xml b/main/commons-test/pom.xml new file mode 100644 index 000000000..194ebf7a7 --- /dev/null +++ b/main/commons-test/pom.xml @@ -0,0 +1,23 @@ + + + + 4.0.0 + + org.cryptomator + main + 0.11.0-SNAPSHOT + + commons-test + Cryptomator common test dependencies + Shared utilities for tests + + + + org.hamcrest + hamcrest-all + + + + diff --git a/main/commons-test/src/main/java/org/cryptomator/commons/test/matcher/ContainsMatcher.java b/main/commons-test/src/main/java/org/cryptomator/commons/test/matcher/ContainsMatcher.java new file mode 100644 index 000000000..7e068bcb6 --- /dev/null +++ b/main/commons-test/src/main/java/org/cryptomator/commons/test/matcher/ContainsMatcher.java @@ -0,0 +1,27 @@ +package org.cryptomator.commons.test.matcher; + +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; + +/** + * Wraps hamcrest contains and containsInAny order matcher factory methods to + * avoid problems due to incorrect / inconsistent handling of generics by the + * several java compilers. + * + * @author Markus Kreusch + */ +public class ContainsMatcher { + + @SuppressWarnings({ "unchecked" }) + @SafeVarargs + public static Matcher> containsInAnyOrder(Matcher... matchers) { + return Matchers.containsInAnyOrder((Matcher[]) matchers); + } + + @SuppressWarnings({ "unchecked" }) + @SafeVarargs + public static Matcher> contains(Matcher... matchers) { + return Matchers.contains((Matcher[]) matchers); + } + +} diff --git a/main/commons-test/src/main/java/org/cryptomator/commons/test/matcher/PropertyMatcher.java b/main/commons-test/src/main/java/org/cryptomator/commons/test/matcher/PropertyMatcher.java new file mode 100644 index 000000000..2ab70c62f --- /dev/null +++ b/main/commons-test/src/main/java/org/cryptomator/commons/test/matcher/PropertyMatcher.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * Copyright (c) 2015 Markus Kreusch + * This file is licensed under the terms of the MIT license. + * See the LICENSE.txt file for more info. + ******************************************************************************/ +package org.cryptomator.commons.test.matcher; + +import java.util.function.Function; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeDiagnosingMatcher; + +public class PropertyMatcher extends TypeSafeDiagnosingMatcher { + + private final Class expectedType; + private final Function getter; + private final String name; + private final Matcher subMatcher; + + public PropertyMatcher(Class type, Function getter, String name, Matcher subMatcher) { + super(type); + this.expectedType = type; + this.getter = getter; + this.name = name; + this.subMatcher = subMatcher; + } + + @Override + public void describeTo(Description description) { + description.appendText("a ") // + .appendText(expectedType.getSimpleName()) // + .appendText(" with a ") // + .appendText(name) // + .appendText(" that is ") // + .appendDescriptionOf(subMatcher); + } + + @Override + protected boolean matchesSafely(T item, Description mismatchDescription) { + P propertyValue = getter.apply(item); + if (subMatcher.matches(propertyValue)) { + return true; + } else { + mismatchDescription.appendText("a ") // + .appendText(expectedType.getSimpleName()) // + .appendText(" with a ") // + .appendText(name) // + .appendText(" that was "); + subMatcher.describeMismatch(propertyValue, mismatchDescription); + return false; + } + } + +} diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/FileSystemVisitor.java b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/FileSystemVisitor.java new file mode 100644 index 000000000..41b278031 --- /dev/null +++ b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/FileSystemVisitor.java @@ -0,0 +1,86 @@ +package org.cryptomator.filesystem; + +import java.util.function.Consumer; + +public class FileSystemVisitor { + + private final Consumer beforeFolderVisitor; + private final Consumer afterFolderVisitor; + private final Consumer fileVisitor; + + private FileSystemVisitor(Consumer beforeFolderVisitor, Consumer afterFolderVisitor, Consumer fileVisitor) { + this.beforeFolderVisitor = beforeFolderVisitor; + this.afterFolderVisitor = afterFolderVisitor; + this.fileVisitor = fileVisitor; + } + + public static FileSystemVisitorBuilder fileSystemVisitor() { + return new FileSystemVisitorBuilder(); + } + + public FileSystemVisitor visit(Folder folder) { + beforeFolderVisitor.accept(folder); + folder.folders().forEach(this::visit); + folder.files().forEach(this::visit); + afterFolderVisitor.accept(folder); + return this; + } + + public FileSystemVisitor visit(File file) { + fileVisitor.accept(file); + return this; + } + + public static class FileSystemVisitorBuilder { + + private Consumer beforeFolderVisitor = noOp(); + private Consumer afterFolderVisitor = noOp(); + private Consumer fileVisitor = noOp(); + + private FileSystemVisitorBuilder() { + } + + public FileSystemVisitorBuilder beforeFolder(Consumer beforeFolderVisitor) { + if (beforeFolderVisitor == null) { + throw new IllegalArgumentException("Vistior may not be null"); + } + this.beforeFolderVisitor = beforeFolderVisitor; + return this; + } + + public FileSystemVisitorBuilder afterFolder(Consumer afterFolderVisitor) { + if (afterFolderVisitor == null) { + throw new IllegalArgumentException("Vistior may not be null"); + } + this.afterFolderVisitor = afterFolderVisitor; + return this; + } + + public FileSystemVisitorBuilder forEachFile(Consumer fileVisitor) { + if (fileVisitor == null) { + throw new IllegalArgumentException("Vistior may not be null"); + } + this.fileVisitor = fileVisitor; + return this; + } + + public FileSystemVisitor visit(Folder folder) { + return build().visit(folder); + } + + public FileSystemVisitor visit(File file) { + return build().visit(file); + } + + public FileSystemVisitor build() { + return new FileSystemVisitor(beforeFolderVisitor, afterFolderVisitor, fileVisitor); + } + + private static Consumer noOp() { + return ignoredParameter -> { + }; + } + + } + +} diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/Folder.java b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/Folder.java index 3fc987143..f07cd4c5c 100644 --- a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/Folder.java +++ b/main/filesystem-api/src/main/java/org/cryptomator/filesystem/Folder.java @@ -88,17 +88,7 @@ public interface Folder extends Node { *

* If the directory does not exist this method does nothing. */ - default void delete() throws UncheckedIOException { - if (!exists()) { - return; - } - folders().forEach(Folder::delete); - files().forEach(file -> { - try (WritableFile writableFile = file.openWritable()) { - writableFile.delete(); - } - }); - } + void delete(); /** * Moves this directory and its contents to the given destination. If the diff --git a/main/filesystem-nio/pom.xml b/main/filesystem-nio/pom.xml index cdb933926..f732c68b5 100644 --- a/main/filesystem-nio/pom.xml +++ b/main/filesystem-nio/pom.xml @@ -1,10 +1,8 @@ - - + + 4.0.0 org.cryptomator @@ -38,6 +36,19 @@ com.google.guava guava + + + org.cryptomator + commons-test + + + org.mockito + mockito-core + + + org.hamcrest + hamcrest-all + diff --git a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/DefaultNioNodeFactory.java b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/DefaultNioNodeFactory.java deleted file mode 100644 index 6289a7065..000000000 --- a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/DefaultNioNodeFactory.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.cryptomator.filesystem.nio; - -import java.nio.file.Path; -import java.util.Optional; - -class DefaultNioNodeFactory implements NioNodeFactory { - - @Override - public NioFile file(Optional parent, Path path) { - return new NioFile(parent, path, this); - } - - @Override - public NioFolder folder(Optional parent, Path path) { - return new NioFolder(parent, path, this); - } - -} diff --git a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFile.java b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFile.java index 722dff6da..ce520c782 100644 --- a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFile.java +++ b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFile.java @@ -1,5 +1,7 @@ package org.cryptomator.filesystem.nio; +import static java.lang.String.format; + import java.io.UncheckedIOException; import java.nio.ByteBuffer; import java.nio.file.Path; @@ -15,8 +17,8 @@ class NioFile extends NioNode implements File { private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true); - public NioFile(Optional parent, Path path, NioNodeFactory nodeFactory) { - super(parent, path, nodeFactory); + public NioFile(Optional parent, Path path) { + super(parent, path); } @Override @@ -99,4 +101,9 @@ class NioFile extends NioNode implements File { } } + @Override + public String toString() { + return format("NioFile(%s)", path); + } + } diff --git a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFileSystem.java b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFileSystem.java index fb00e6833..3ddd4476d 100644 --- a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFileSystem.java +++ b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFileSystem.java @@ -1,5 +1,7 @@ package org.cryptomator.filesystem.nio; +import static org.cryptomator.filesystem.FolderCreateMode.INCLUDING_PARENTS; + import java.nio.file.Path; import java.util.Optional; @@ -8,11 +10,12 @@ import org.cryptomator.filesystem.FileSystem; public class NioFileSystem extends NioFolder implements FileSystem { public static NioFileSystem rootedAt(Path root) { - return new NioFileSystem(root, new DefaultNioNodeFactory()); + return new NioFileSystem(root); } - NioFileSystem(Path root, NioNodeFactory nodeFactory) { - super(Optional.empty(), root, nodeFactory); + private NioFileSystem(Path root) { + super(Optional.empty(), root); + create(INCLUDING_PARENTS); } } diff --git a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFolder.java b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFolder.java index 48fd0eb0f..d4d3b22b9 100644 --- a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFolder.java +++ b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioFolder.java @@ -1,5 +1,7 @@ package org.cryptomator.filesystem.nio; +import static java.lang.String.format; +import static org.cryptomator.filesystem.FileSystemVisitor.fileSystemVisitor; import static org.cryptomator.filesystem.FolderCreateMode.INCLUDING_PARENTS; import java.io.IOException; @@ -13,14 +15,15 @@ import org.cryptomator.filesystem.File; import org.cryptomator.filesystem.Folder; import org.cryptomator.filesystem.FolderCreateMode; import org.cryptomator.filesystem.Node; +import org.cryptomator.filesystem.WritableFile; class NioFolder extends NioNode implements Folder { private final WeakValuedCache folders = WeakValuedCache.usingLoader(this::folderFromPath); private final WeakValuedCache files = WeakValuedCache.usingLoader(this::fileFromPath); - public NioFolder(Optional parent, Path path, NioNodeFactory nodeFactory) { - super(parent, path, nodeFactory); + public NioFolder(Optional parent, Path path) { + super(parent, path); } @Override @@ -41,11 +44,11 @@ class NioFolder extends NioNode implements Folder { } private NioFile fileFromPath(Path path) { - return nodeFactory.file(Optional.of(this), path); + return new NioFile(Optional.of(this), path); } private NioFolder folderFromPath(Path path) { - return nodeFactory.folder(Optional.of(this), path); + return new NioFolder(Optional.of(this), path); } @Override @@ -82,4 +85,31 @@ class NioFolder extends NioNode implements Folder { } } + @Override + public String toString() { + return format("NioFolder(%s)", path); + } + + @Override + public void delete() { + fileSystemVisitor() // + .forEachFile(NioFolder::deleteFile) // + .afterFolder(NioFolder::deleteEmptyFolder) // + .visit(this); + } + + private static final void deleteFile(File file) { + try (WritableFile writableFile = file.openWritable()) { + writableFile.delete(); + } + } + + private static final void deleteEmptyFolder(Folder folder) { + try { + Files.delete(((NioFolder) folder).path); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } diff --git a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioNode.java b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioNode.java index 92c4ac5d0..9cdfb97ab 100644 --- a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioNode.java +++ b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioNode.java @@ -14,17 +14,26 @@ class NioNode implements Node { protected final Optional parent; protected final Path path; - protected final NioNodeFactory nodeFactory; - public NioNode(Optional parent, Path path, NioNodeFactory nodeFactory) { + private NioFileSystem fileSystem; + + public NioNode(Optional parent, Path path) { this.path = path.toAbsolutePath(); - this.nodeFactory = nodeFactory; this.parent = parent; } + NioFileSystem fileSystem() { + if (fileSystem == null) { + fileSystem = parent // + .map(NioNode::fileSystem) // + .orElseGet(() -> (NioFileSystem) this); + } + return fileSystem; + } + boolean belongsToSameFilesystem(Node other) { return other instanceof NioNode // - && ((NioNode) other).nodeFactory == nodeFactory; + && ((NioNode) other).fileSystem() == fileSystem(); } @Override diff --git a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioNodeFactory.java b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioNodeFactory.java deleted file mode 100644 index 7bfeb49b6..000000000 --- a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/NioNodeFactory.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.cryptomator.filesystem.nio; - -import java.nio.file.Path; -import java.util.Optional; - -interface NioNodeFactory { - - NioFile file(Optional parent, Path path); - - NioFolder folder(Optional parent, Path path); - -} diff --git a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/WeakValuedCache.java b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/WeakValuedCache.java index 59f627c8a..a07040d7c 100644 --- a/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/WeakValuedCache.java +++ b/main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/WeakValuedCache.java @@ -6,6 +6,8 @@ import java.util.function.Function; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; +import com.google.common.util.concurrent.ExecutionError; +import com.google.common.util.concurrent.UncheckedExecutionException; class WeakValuedCache { @@ -30,7 +32,11 @@ class WeakValuedCache { try { return delegate.get(key); } catch (ExecutionException e) { - throw new RuntimeException(e); + throw new IllegalStateException("No checked exception can be thrown by loader", e); + } catch (UncheckedExecutionException e) { + throw (RuntimeException) e.getCause(); + } catch (ExecutionError e) { + throw (Error) e.getCause(); } } diff --git a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/FilesystemSetupUtils.java b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/FilesystemSetupUtils.java new file mode 100644 index 000000000..a580fa952 --- /dev/null +++ b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/FilesystemSetupUtils.java @@ -0,0 +1,89 @@ +package org.cryptomator.filesystem.nio; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.apache.commons.io.IOUtils; + +class FilesystemSetupUtils { + + public static Path emptyFilesystem() { + try { + return Files.createTempDirectory("test-filesystem"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public static Path testFilesystem(Entry firstEntry, Entry... entries) { + try { + Path root = Files.createTempDirectory("test-filesystem"); + firstEntry.create(root); + for (Entry entry : entries) { + entry.create(root); + } + return root; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static FileEntry file(String path) { + return new FileEntry(Paths.get(path)); + } + + public static FolderEntry folder(String path) { + return new FolderEntry(Paths.get(path)); + } + + interface Entry { + + void create(Path root) throws IOException; + + } + + public static class FileEntry implements Entry { + private Path relativePath; + private byte[] data = new byte[0]; + + public FileEntry(Path relativePath) { + this.relativePath = relativePath; + } + + public FileEntry withData(byte[] data) { + this.data = data; + return this; + } + + public FileEntry withData(String data) { + return withData(data.getBytes()); + } + + @Override + public void create(Path root) throws IOException { + Path filePath = root.resolve(relativePath); + Files.createDirectories(filePath.getParent()); + try (OutputStream out = Files.newOutputStream(filePath)) { + IOUtils.write(data, out); + } + } + } + + public static class FolderEntry implements Entry { + private Path relativePath; + + public FolderEntry(Path relativePath) { + this.relativePath = relativePath; + } + + @Override + public void create(Path root) throws IOException { + Files.createDirectories(root.resolve(relativePath)); + } + } + +} diff --git a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFileSystemTest.java b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFileSystemTest.java new file mode 100644 index 000000000..6d151fd37 --- /dev/null +++ b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFileSystemTest.java @@ -0,0 +1,52 @@ +package org.cryptomator.filesystem.nio; + +import static org.cryptomator.filesystem.nio.FilesystemSetupUtils.emptyFilesystem; +import static org.cryptomator.filesystem.nio.FilesystemSetupUtils.file; +import static org.cryptomator.filesystem.nio.FilesystemSetupUtils.testFilesystem; +import static org.cryptomator.filesystem.nio.PathMatcher.isDirectory; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class NioFileSystemTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void testParentIsEmpty() throws IOException { + NioFileSystem fileSystem = NioFileSystem.rootedAt(emptyFilesystem()); + + assertThat(fileSystem.parent().isPresent(), is(false)); + } + + @Test + public void testObtainingAFileSystemWithNonExistingRootCreatesItIncludingAllParentFolders() throws IOException { + Path emptyFilesystem = emptyFilesystem(); + Path nonExistingFilesystemRoot = emptyFilesystem.resolve("nonExistingRoot"); + Files.delete(emptyFilesystem); + + NioFileSystem.rootedAt(nonExistingFilesystemRoot); + + assertThat(nonExistingFilesystemRoot, isDirectory()); + } + + @Test + public void testObtainingAFileSystemWhooseRootIsAFileFails() throws IOException { + Path emptyFilesystem = testFilesystem(file("rootWhichIsAFile")); + Path rootWhichIsAFile = emptyFilesystem.resolve("rootWhichIsAFile"); + + thrown.expect(UncheckedIOException.class); + + NioFileSystem.rootedAt(rootWhichIsAFile); + } + +} \ No newline at end of file diff --git a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFolderTest.java b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFolderTest.java new file mode 100644 index 000000000..4234887bf --- /dev/null +++ b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFolderTest.java @@ -0,0 +1,174 @@ +package org.cryptomator.filesystem.nio; + +import static java.util.stream.Collectors.toList; +import static org.cryptomator.commons.test.matcher.ContainsMatcher.containsInAnyOrder; +import static org.cryptomator.filesystem.FolderCreateMode.FAIL_IF_PARENT_IS_MISSING; +import static org.cryptomator.filesystem.FolderCreateMode.INCLUDING_PARENTS; +import static org.cryptomator.filesystem.nio.FilesystemSetupUtils.emptyFilesystem; +import static org.cryptomator.filesystem.nio.FilesystemSetupUtils.file; +import static org.cryptomator.filesystem.nio.FilesystemSetupUtils.folder; +import static org.cryptomator.filesystem.nio.FilesystemSetupUtils.testFilesystem; +import static org.cryptomator.filesystem.nio.NioNodeMatcher.fileWithName; +import static org.cryptomator.filesystem.nio.NioNodeMatcher.folderWithName; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.empty; +import static org.junit.Assert.assertThat; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.cryptomator.filesystem.Folder; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class NioFolderTest { + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void testNameIsNameOfFolder() throws IOException { + final String folderName = "folderNameABC"; + NioFileSystem fileSystem = NioFileSystem.rootedAt(emptyFilesystem()); + Folder folder = fileSystem.folder(folderName); + + assertThat(folder, folderWithName(folderName)); + } + + @Test + public void testCreateWithOptionFailIfParentIsMissingFailsIfParentIsMissing() throws IOException { + NioFileSystem fileSystem = NioFileSystem.rootedAt(emptyFilesystem()); + Folder folderWithNonExistingParent = fileSystem.folder("a").folder("b"); + + thrown.expect(UncheckedIOException.class); + thrown.expectMessage(fileSystem.path.resolve("a/b").toString()); + + folderWithNonExistingParent.create(FAIL_IF_PARENT_IS_MISSING); + } + + @Test + public void testCreateWithOptionIncludingParentsSucceedsIfParentIsMissing() throws IOException { + Path emptyFilesystemPath = emptyFilesystem(); + NioFileSystem fileSystem = NioFileSystem.rootedAt(emptyFilesystemPath); + Folder folderWithNonExistingParent = fileSystem.folder("a").folder("b"); + + folderWithNonExistingParent.create(INCLUDING_PARENTS); + + assertThat(Files.isDirectory(emptyFilesystemPath.resolve("a/b")), is(true)); + } + + @Test + public void testCreateWithOptionFailIfParentIsMissingSucceedsIfParentIsPresent() throws IOException { + Path emptyFilesystemPath = emptyFilesystem(); + NioFileSystem fileSystem = NioFileSystem.rootedAt(emptyFilesystemPath); + Folder nonExistingFolder = fileSystem.folder("a"); + + nonExistingFolder.create(FAIL_IF_PARENT_IS_MISSING); + + assertThat(Files.isDirectory(emptyFilesystemPath.resolve("a")), is(true)); + } + + @Test + public void testChildrenOfEmptyNioFolderAreEmpty() throws IOException { + NioFolder folder = NioFileSystem.rootedAt(emptyFilesystem()); + + assertThat(folder.children().collect(toList()), is(empty())); + } + + @Test + public void testChildrenOfNonExistingFolderThrowsUncheckedIOExceptionWithAbolutePathOfFolderInMessage() throws IOException { + Path emptyFolderPath = emptyFilesystem(); + NioFolder folder = NioFileSystem.rootedAt(emptyFolderPath); + Files.delete(emptyFolderPath); + + thrown.expect(UncheckedIOException.class); + thrown.expectMessage(emptyFolderPath.toString()); + + folder.children(); + } + + @Test + public void testChildrenOfFolderAreCorrect() throws IOException { + String folderName1 = "folder1"; + String folderName2 = "folder2"; + String fileName1 = "file1"; + String fileName2 = "file2"; + + NioFolder folder = NioFileSystem.rootedAt(testFilesystem( // + folder(folderName1), // + folder(folderName2), // + file(fileName1), // + file(fileName2))); + + assertThat(folder.children().collect(toList()), + containsInAnyOrder( // + folderWithName(folderName1), // + folderWithName(folderName2), // + fileWithName(fileName1), // + fileWithName(fileName2))); + } + + @Test + public void testFilesDoesContainOnlyFileChildren() throws IOException { + String folderName1 = "folder1"; + String folderName2 = "folder2"; + String fileName1 = "file1"; + String fileName2 = "file2"; + + NioFolder folder = NioFileSystem.rootedAt(testFilesystem( // + folder(folderName1), // + folder(folderName2), // + file(fileName1), // + file(fileName2))); + + assertThat(folder.files().collect(toList()), + containsInAnyOrder( // + fileWithName(fileName1), // + fileWithName(fileName2))); + } + + @Test + public void testFilesOfNonExistingFolderThrowsUncheckedIOExceptionWithAbolutePathOfFolderInMessage() throws IOException { + Path emptyFolderPath = emptyFilesystem(); + NioFolder folder = NioFileSystem.rootedAt(emptyFolderPath); + Files.delete(emptyFolderPath); + + thrown.expect(UncheckedIOException.class); + thrown.expectMessage(emptyFolderPath.toString()); + + folder.files(); + } + + @Test + public void testFoldersDoesContainOnlyFolderChildren() throws IOException { + String folderName1 = "folder1"; + String folderName2 = "folder2"; + String fileName1 = "file1"; + String fileName2 = "file2"; + + NioFolder folder = NioFileSystem.rootedAt(testFilesystem( // + folder(folderName1), // + folder(folderName2), // + file(fileName1), // + file(fileName2))); + + assertThat(folder.folders().collect(toList()), + containsInAnyOrder( // + folderWithName(folderName1), // + folderWithName(folderName2))); + } + + @Test + public void testFoldersOfNonExistingFolderThrowsUncheckedIOExceptionWithAbolutePathOfFolderInMessage() throws IOException { + Path emptyFilesystemPath = emptyFilesystem(); + NioFolder folder = NioFileSystem.rootedAt(emptyFilesystemPath); + Files.delete(emptyFilesystemPath); + + thrown.expect(UncheckedIOException.class); + thrown.expectMessage(emptyFilesystemPath.toString()); + + folder.folders(); + } +} diff --git a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioNodeMatcher.java b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioNodeMatcher.java new file mode 100644 index 000000000..01cb4253d --- /dev/null +++ b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioNodeMatcher.java @@ -0,0 +1,20 @@ +package org.cryptomator.filesystem.nio; + +import static org.hamcrest.CoreMatchers.equalTo; + +import org.cryptomator.commons.test.matcher.PropertyMatcher; +import org.cryptomator.filesystem.File; +import org.cryptomator.filesystem.Folder; +import org.hamcrest.Matcher; + +class NioNodeMatcher { + + public static Matcher folderWithName(String name) { + return new PropertyMatcher<>(Folder.class, Folder::name, "name", equalTo(name)); + } + + public static Matcher fileWithName(String name) { + return new PropertyMatcher<>(File.class, File::name, "name", equalTo(name)); + } + +} diff --git a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/PathMatcher.java b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/PathMatcher.java new file mode 100644 index 000000000..a7b53ac4b --- /dev/null +++ b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/PathMatcher.java @@ -0,0 +1,22 @@ +package org.cryptomator.filesystem.nio; + +import static org.hamcrest.CoreMatchers.is; + +import java.nio.file.Files; +import java.nio.file.Path; + +import org.hamcrest.FeatureMatcher; +import org.hamcrest.Matcher; + +class PathMatcher { + + public static Matcher isDirectory() { + return new FeatureMatcher(is(true), "a path for which Files.isDirectory", "Files.isDirectory") { + @Override + protected Boolean featureValueOf(Path actual) { + return Files.isDirectory(actual); + } + }; + } + +} diff --git a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/SampleFilesystem.java b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/SampleFilesystem.java new file mode 100644 index 000000000..cc8e230d1 --- /dev/null +++ b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/SampleFilesystem.java @@ -0,0 +1,15 @@ +package org.cryptomator.filesystem.nio; + +import org.apache.commons.lang3.StringUtils; + +public enum SampleFilesystem { + + EMPTY_FILESYSTEM, SOME_FOLDERS_AND_FILES + + ; + + public String directoryName() { + return StringUtils.removeEnd(name(), "_FILESYSTEM").toLowerCase(); + } + +} diff --git a/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/WeakValuedCacheTest.java b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/WeakValuedCacheTest.java new file mode 100644 index 000000000..77b427847 --- /dev/null +++ b/main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/WeakValuedCacheTest.java @@ -0,0 +1,138 @@ +package org.cryptomator.filesystem.nio; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.function.Function; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; + +public class WeakValuedCacheTest { + + private final String A_KEY = "aKey"; + private final String ANOTHER_KEY = "anotherKey"; + + private WeakValuedCache inTest; + + private Function loader; + + @SuppressWarnings("unchecked") + @Before + public void setup() { + loader = Mockito.mock(Function.class); + inTest = WeakValuedCache.usingLoader(loader); + } + + @Test + public void testResultOfGetIsResultOfLoaderForTheSameKey() { + Value theValue = new Value(); + Value theOtherValue = new Value(); + when(loader.apply(A_KEY)).thenReturn(theValue); + when(loader.apply(ANOTHER_KEY)).thenReturn(theOtherValue); + + Value result = inTest.get(A_KEY); + Value anotherResult = inTest.get(ANOTHER_KEY); + + assertThat(result, is(sameInstance(theValue))); + assertThat(anotherResult, is(sameInstance(theOtherValue))); + } + + @Test + public void testCachedResultIsResultOfLoaderForTheSameKey() { + Value theValue = new Value(); + Value theOtherValue = new Value(); + when(loader.apply(A_KEY)).thenReturn(theValue); + when(loader.apply(ANOTHER_KEY)).thenReturn(theOtherValue); + + inTest.get(A_KEY); + inTest.get(ANOTHER_KEY); + Value result = inTest.get(A_KEY); + Value anotherResult = inTest.get(ANOTHER_KEY); + + assertThat(result, is(sameInstance(theValue))); + assertThat(anotherResult, is(sameInstance(theOtherValue))); + } + + @Test + public void testTwiceInvocationOfGetDoesNotInvokeLoaderTwice() { + Value theValue = new Value(); + when(loader.apply(A_KEY)).thenReturn(theValue); + + inTest.get(A_KEY); + inTest.get(A_KEY); + + verify(loader).apply(A_KEY); + } + + @Test + public void testSecondInvocationOfGetReturnsTheSameResult() { + Value theValue = new Value(); + when(loader.apply(A_KEY)).thenReturn(theValue); + + inTest.get(A_KEY); + Value result = inTest.get(A_KEY); + + assertThat(result, is(sameInstance(theValue))); + } + + @Test + public void testCacheDoesNotPreventGarbageCollectionOfValues() { + when(loader.apply(A_KEY)).thenAnswer(this::createValueUsingMoreThanHalfTheJvmMemory); + + inTest.get(A_KEY); + + // force garbage collection of previously created value by creating an + // object so large it can not coexist with the value + createByteArrayUsingMoreThanHalfTheJvmMemory(); + } + + @Test(expected = RuntimeExceptionThrownInLoader.class) + public void testCacheRethrowsRuntimeExceptionsFromLoader() { + when(loader.apply(A_KEY)).thenThrow(new RuntimeExceptionThrownInLoader()); + + inTest.get(A_KEY); + } + + @Test(expected = ErrorThrownInLoader.class) + public void testCacheRethrowsErrorsFromLoader() { + when(loader.apply(A_KEY)).thenThrow(new ErrorThrownInLoader()); + + inTest.get(A_KEY); + } + + private Value createValueUsingMoreThanHalfTheJvmMemory(InvocationOnMock invocation) { + byte[] data = createByteArrayUsingMoreThanHalfTheJvmMemory(); + Value value = new Value(); + value.setPayload(data); + return value; + } + + private byte[] createByteArrayUsingMoreThanHalfTheJvmMemory() { + int max = (int) Runtime.getRuntime().maxMemory(); + byte[] data = new byte[max / 2 + 1]; + return data; + } + + private static class RuntimeExceptionThrownInLoader extends RuntimeException { + } + + private static class ErrorThrownInLoader extends Error { + } + + private static class Value { + + @SuppressWarnings("unused") + private Object payload; + + public void setPayload(Object payload) { + this.payload = payload; + } + } + +} diff --git a/main/pom.xml b/main/pom.xml index 47f081266..308b2e008 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -35,6 +35,7 @@ 2.1 1.7.7 4.12 + 1.3 2.4 4.0 3.3.2 @@ -43,7 +44,7 @@ 2.4.4 1.10.19 - + jitpack.io @@ -54,6 +55,13 @@ + + org.cryptomator + commons-test + ${project.version} + test + + org.cryptomator filesystem-api @@ -74,7 +82,7 @@ filesystem-crypto ${project.version} - + org.cryptomator core @@ -173,18 +181,36 @@ ${jackson-databind.version} - + junit junit ${junit.version} test + + + hamcrest-core + org.hamcrest + + org.mockito mockito-core ${mockito.version} test + + + hamcrest-core + org.hamcrest + + + + + org.hamcrest + hamcrest-all + ${hamcrest.version} + test @@ -223,6 +249,7 @@ core ui jackrabbit-filesystem-adapter + commons-test