Sfoglia il codice sorgente

Tests for filesystem-nio

* Renamed tests from ...IntegrationTest back to ...Test
** to allow better integration with moreunit
** because some methods of the classes can only be integration tested
some not which lead to a strange splitting of the tests
* Added more tests
Markus Kreusch 9 anni fa
parent
commit
ff4448bac0

+ 1 - 1
main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/ReadableNioFile.java

@@ -68,7 +68,7 @@ class ReadableNioFile implements ReadableFile {
 		long size = nioFile.channel().size();
 		long transferred = 0;
 		while (transferred < size) {
-			transferred += nioFile.channel().transferTo(transferred, size - transferred, targetChannel);
+			transferred += nioFile.channel().transferTo(transferred, size - transferred, targetChannel, transferred);
 		}
 	}
 

+ 29 - 6
main/filesystem-nio/src/main/java/org/cryptomator/filesystem/nio/SharedFileChannel.java

@@ -42,12 +42,26 @@ class SharedFileChannel {
 		assertOpenedByCurrentThread();
 		doLocked(() -> {
 			openedBy.remove(Thread.currentThread());
-			if (openedBy.isEmpty()) {
-				closeChannel();
+			try {
+				delegate.force(true);
+			} catch (IOException e) {
+				throw new UncheckedIOException(e);
+			} finally {
+				if (openedBy.isEmpty()) {
+					closeChannel();
+				}
 			}
 		});
 	}
 
+	/**
+	 * @deprecated only intended to be used in tests
+	 */
+	@Deprecated
+	void forceClose() {
+		closeSilently(delegate);
+	}
+
 	private void assertOpenedByCurrentThread() {
 		if (!openedBy.containsKey(Thread.currentThread())) {
 			throw new IllegalStateException("SharedFileChannel closed for current thread");
@@ -61,14 +75,22 @@ class SharedFileChannel {
 				readChannel = FileChannel.open(path, StandardOpenOption.READ);
 			}
 			delegate = FileChannel.open(path, StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE);
-			if (readChannel != null) {
-				readChannel.close();
-			}
+			closeSilently(readChannel);
 		} catch (IOException e) {
 			throw new UncheckedIOException(e);
 		}
 	}
 
+	private void closeSilently(FileChannel channel) {
+		if (channel != null) {
+			try {
+				channel.close();
+			} catch (IOException e) {
+				// ignore
+			}
+		}
+	}
+
 	private void closeChannel() {
 		try {
 			delegate.close();
@@ -121,13 +143,14 @@ class SharedFileChannel {
 		}
 	}
 
-	public long transferTo(long position, long count, SharedFileChannel targetChannel) {
+	public long transferTo(long position, long count, SharedFileChannel targetChannel, long targetPosition) {
 		assertOpenedByCurrentThread();
 		targetChannel.assertOpenedByCurrentThread();
 		try {
 			long maxPosition = delegate.size();
 			long maxCount = Math.min(count, maxPosition - position);
 			long remaining = maxCount;
+			targetChannel.delegate.position(targetPosition);
 			while (remaining > 0) {
 				remaining -= delegate.transferTo(maxPosition - remaining, remaining, targetChannel.delegate);
 			}

+ 1 - 1
main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFileSystemIntegrationTest.java

@@ -16,7 +16,7 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 
-public class NioFileSystemIntegrationTest {
+public class NioFileSystemTest {
 
 	@Rule
 	public ExpectedException thrown = ExpectedException.none();

+ 253 - 1
main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFileIntegrationTest.java

@@ -8,6 +8,8 @@ import static org.cryptomator.filesystem.nio.FilesystemSetupUtils.folder;
 import static org.cryptomator.filesystem.nio.FilesystemSetupUtils.testFilesystem;
 import static org.cryptomator.filesystem.nio.PathMatcher.doesNotExist;
 import static org.cryptomator.filesystem.nio.PathMatcher.isFile;
+import static org.cryptomator.filesystem.nio.ThreadStackMatcher.stackContains;
+import static org.hamcrest.CoreMatchers.instanceOf;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.CoreMatchers.sameInstance;
@@ -16,19 +18,25 @@ import static org.junit.Assert.assertThat;
 import java.io.UncheckedIOException;
 import java.nio.file.Path;
 import java.time.Instant;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.function.Supplier;
 
 import org.cryptomator.filesystem.File;
 import org.cryptomator.filesystem.FileSystem;
 import org.cryptomator.filesystem.Folder;
+import org.cryptomator.filesystem.ReadableFile;
+import org.cryptomator.filesystem.WritableFile;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
+import org.junit.rules.Timeout;
 import org.junit.runner.RunWith;
 
 import de.bechte.junit.runners.context.HierarchicalContextRunner;
 
 @RunWith(HierarchicalContextRunner.class)
-public class NioFileIntegrationTest {
+public class NioFileTest {
 
 	@Rule
 	public ExpectedException thrown = ExpectedException.none();
@@ -339,6 +347,250 @@ public class NioFileIntegrationTest {
 		fileWhichIsAFolder.moveTo(target);
 	}
 
+	public class OpenReadable {
+
+		@Test
+		public void testOpenReadableReturnsAReadableNioFileForAnExistingFile() {
+			File file = NioFileSystem.rootedAt(testFilesystem(file("fileName"))).file("fileName");
+
+			ReadableFile result = file.openReadable();
+
+			assertThat(result, is(instanceOf(ReadableNioFile.class)));
+		}
+
+		@Test
+		public void testOpenReadableThrowsAnUncheckedIOExceptionIfTheFileDoesNotExists() {
+			Path filesystemPath = emptyFilesystem();
+			Path nonExistingFilePath = filesystemPath.resolve("nonExistingFile");
+			File nonExistingFile = NioFileSystem.rootedAt(filesystemPath).file("nonExistingFile");
+
+			thrown.expect(UncheckedIOException.class);
+			thrown.expectMessage(nonExistingFilePath.toString());
+
+			nonExistingFile.openReadable();
+		}
+
+		@Test
+		public void testOpenReadableThrowsIllegalStateExceptionIfInvokedTwiceFromWithingTheSameThread() {
+			File file = NioFileSystem.rootedAt(testFilesystem(file("fileName"))).file("fileName");
+			file.openReadable();
+
+			thrown.expect(IllegalStateException.class);
+			thrown.expectMessage("Current thread is already reading this file");
+
+			file.openReadable();
+		}
+
+		@Test
+		public void testOpenReadableDoesNotThrowIllegalStateExceptionIfInvokedTwiceFromWithingTheSameThreadButTheFirstReadableFileHasBeenClosed() {
+			File file = NioFileSystem.rootedAt(testFilesystem(file("fileName"))).file("fileName");
+			file.openReadable().close();
+
+			ReadableFile result = file.openReadable();
+
+			assertThat(result, is(instanceOf(ReadableNioFile.class)));
+		}
+
+		@Test
+		public void testOpenReadableThrowsIllegalStateExceptionIfInvokedAfterOpenWritable() {
+			File file = NioFileSystem.rootedAt(testFilesystem(file("fileName"))).file("fileName");
+			file.openWritable();
+
+			thrown.expect(IllegalStateException.class);
+			thrown.expectMessage("Current thread is currently writing this file");
+
+			file.openReadable();
+		}
+
+		@Test
+		public void testOpenReadableDoesNotThrowIllegalStateExceptionIfInvokedAfterOpenWritableButTheWritableFileHasBeenClosed() {
+			File file = NioFileSystem.rootedAt(testFilesystem(file("fileName"))).file("fileName");
+			file.openWritable().close();
+
+			ReadableFile result = file.openReadable();
+
+			assertThat(result, is(instanceOf(ReadableNioFile.class)));
+		}
+
+	}
+
+	public class OpenWritable {
+
+		@Test
+		public void testOpenWritableReturnsAWritableNioFileForAnExistingFile() {
+			File file = NioFileSystem.rootedAt(testFilesystem(file("fileName"))).file("fileName");
+
+			WritableFile result = file.openWritable();
+
+			assertThat(result, is(instanceOf(WritableNioFile.class)));
+		}
+
+		@Test
+		public void testOpenWritableReturnsAWritableNioFileForANonExisitingFile() {
+			File file = NioFileSystem.rootedAt(emptyFilesystem()).file("nonExistingFile");
+
+			WritableFile result = file.openWritable();
+
+			assertThat(result, is(instanceOf(WritableNioFile.class)));
+		}
+
+		@Test
+		public void testOpenWritableDoesNotCreateANonExisitingFile() {
+			Path filesystemPath = emptyFilesystem();
+			Path filePath = filesystemPath.resolve("nonExistingFile");
+			File file = NioFileSystem.rootedAt(filesystemPath).file("nonExistingFile");
+
+			file.openWritable();
+
+			assertThat(filePath, doesNotExist());
+		}
+
+		@Test
+		public void testOpenWritableThrowsIllegalStateExceptionIfInvokedTwiceFromWithingTheSameThread() {
+			File file = NioFileSystem.rootedAt(testFilesystem(file("fileName"))).file("fileName");
+			file.openWritable();
+
+			thrown.expect(IllegalStateException.class);
+			thrown.expectMessage("Current thread is already writing this file");
+
+			file.openWritable();
+		}
+
+		@Test
+		public void testOpenWritableDoesNotThrowIllegalStateExceptionIfInvokedTwiceFromWithingTheSameThreadButTheFirstWritableFileHasBeenClosed() {
+			File file = NioFileSystem.rootedAt(testFilesystem(file("fileName"))).file("fileName");
+			file.openWritable().close();
+
+			WritableFile result = file.openWritable();
+
+			assertThat(result, is(instanceOf(WritableNioFile.class)));
+		}
+
+		@Test
+		public void testOpenWritableThrowsIllegalStateExceptionIfInvokedAfterOpenReadable() {
+			File file = NioFileSystem.rootedAt(testFilesystem(file("fileName"))).file("fileName");
+			file.openReadable();
+
+			thrown.expect(IllegalStateException.class);
+			thrown.expectMessage("Current thread is currently reading this file");
+
+			file.openWritable();
+		}
+
+		@Test
+		public void testOpenWritableDoesNotThrowIllegalStateExceptionIfInvokedAfterOpenReadableButTheReadableFileHasBeenClosed() {
+			File file = NioFileSystem.rootedAt(testFilesystem(file("fileName"))).file("fileName");
+			file.openReadable().close();
+
+			WritableFile result = file.openWritable();
+
+			assertThat(result, is(instanceOf(WritableNioFile.class)));
+		}
+
+	}
+
+	public class OpenWithMultipleThreads {
+
+		@Rule
+		public Timeout timeout = Timeout.seconds(5);
+
+		@Test
+		public void testOpenReadableInvokedInTwoThreadsCompletes() {
+			File file = NioFileSystem.rootedAt(testFilesystem(file("fileName"))).file("fileName");
+
+			ReadableFile readableFileFromThread1 = computeInThread(file::openReadable);
+			ReadableFile readableFileFromThread2 = computeInThread(file::openReadable);
+
+			assertThat(readableFileFromThread1, is(instanceOf(ReadableNioFile.class)));
+			assertThat(readableFileFromThread2, is(instanceOf(ReadableNioFile.class)));
+			assertThat(readableFileFromThread1, is(not(sameInstance(readableFileFromThread2))));
+		}
+
+		@Test
+		public void testOpenReadableInvokedWhileWritableFileIsOpenBlocks() {
+			File file = NioFileSystem.rootedAt(testFilesystem(file("fileName"))).file("fileName");
+			file.openWritable();
+
+			Thread thread = inThread(file::openReadable);
+
+			assertThat(thread, is(stackContains(NioFile.class, "openReadable")));
+		}
+
+		@Test
+		public void testOpenWritableInvokedWhileReadableFileIsOpenBlocks() {
+			File file = NioFileSystem.rootedAt(testFilesystem(file("fileName"))).file("fileName");
+			file.openReadable();
+
+			Thread thread = inThread(file::openWritable);
+
+			assertThat(thread, is(stackContains(NioFile.class, "openWritable")));
+		}
+
+		@Test
+		public void testOpenWritableInvokedWhileWritableFileIsOpenBlocks() {
+			File file = NioFileSystem.rootedAt(testFilesystem(file("fileName"))).file("fileName");
+			file.openWritable();
+
+			Thread thread = inThread(file::openWritable);
+
+			assertThat(thread, is(stackContains(NioFile.class, "openWritable")));
+		}
+
+		@Test
+		public void testOpenReadableInvokedWhileWritableFileIsOpenCompletesAfterClosingIt() throws InterruptedException {
+			File file = NioFileSystem.rootedAt(testFilesystem(file("fileName"))).file("fileName");
+			WritableFile writableFile = file.openWritable();
+			Thread thread = inThread(file::openReadable);
+			writableFile.close();
+
+			thread.join();
+		}
+
+		@Test
+		public void testOpenWritableInvokedWhileReadableFileIsOpenCompletesAfterClosingIt() throws InterruptedException {
+			File file = NioFileSystem.rootedAt(testFilesystem(file("fileName"))).file("fileName");
+			ReadableFile readableFile = file.openReadable();
+			Thread thread = inThread(file::openWritable);
+			readableFile.close();
+
+			thread.join();
+		}
+
+		@Test
+		public void testOpenWritableInvokedWhileWritableFileIsOpenCompletesAfterClosingIt() throws InterruptedException {
+			File file = NioFileSystem.rootedAt(testFilesystem(file("fileName"))).file("fileName");
+			WritableFile writableFile = file.openWritable();
+			Thread thread = inThread(file::openWritable);
+			writableFile.close();
+
+			thread.join();
+		}
+
+		private Thread inThread(Runnable task) {
+			Thread thread = new Thread(task);
+			thread.start();
+			try {
+				// give thread time to execute work
+				Thread.sleep(100);
+			} catch (InterruptedException e) {
+			}
+			return thread;
+		}
+
+		private <T> T computeInThread(Supplier<T> computation) {
+			CompletableFuture<T> future = new CompletableFuture<>();
+			inThread(() -> {
+				future.complete(computation.get());
+			});
+			try {
+				return future.get();
+			} catch (InterruptedException | ExecutionException e) {
+				throw new RuntimeException(e);
+			}
+		}
+
+	}
+
 	private int signum(int value) {
 		if (value > 0) {
 			return 1;

+ 1 - 1
main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/NioFolderIntegrationTest.java

@@ -31,7 +31,7 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 
-public class NioFolderIntegrationTest {
+public class NioFolderTest {
 
 	@Rule
 	public ExpectedException thrown = ExpectedException.none();

+ 4 - 0
main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/PathMatcher.java

@@ -67,6 +67,10 @@ class PathMatcher {
 			return new IsFileWithContentMatcher(value);
 		}
 
+		public Matcher<Path> thatIsEmpty() {
+			return withContent(new byte[0]);
+		}
+
 	}
 
 	public static class IsFileWithContentMatcher extends TypeSafeDiagnosingMatcher<Path> {

+ 8 - 8
main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/ReadableNioFileTest.java

@@ -213,7 +213,7 @@ public class ReadableNioFileTest {
 			when(otherFile.belongsToSameFilesystem(file)).thenReturn(true);
 			long sizeOfSourceChannel = 3283;
 			when(channel.size()).thenReturn(sizeOfSourceChannel);
-			when(channel.transferTo(0, sizeOfSourceChannel, otherChannel)).thenReturn(sizeOfSourceChannel);
+			when(channel.transferTo(0, sizeOfSourceChannel, otherChannel, 0)).thenReturn(sizeOfSourceChannel);
 
 			inTest.copyTo(writableOtherFile);
 
@@ -221,7 +221,7 @@ public class ReadableNioFileTest {
 			inOrder.verify(writableOtherFile).assertOpen();
 			inOrder.verify(writableOtherFile).ensureChannelIsOpened();
 			inOrder.verify(otherChannel).truncate(0);
-			inOrder.verify(channel).transferTo(0, sizeOfSourceChannel, otherChannel);
+			inOrder.verify(channel).transferTo(0, sizeOfSourceChannel, otherChannel, 0);
 		}
 
 		@Test
@@ -235,16 +235,16 @@ public class ReadableNioFileTest {
 			long sizeRemainingAfterFirstTransfer = sizeRemainingAfterSecondTransfer + secondTransferAmount;
 			long size = sizeRemainingAfterFirstTransfer + firstTransferAmount;
 			when(channel.size()).thenReturn(size);
-			when(channel.transferTo(0, size, otherChannel)).thenReturn(firstTransferAmount);
-			when(channel.transferTo(firstTransferAmount, sizeRemainingAfterFirstTransfer, otherChannel)).thenReturn(secondTransferAmount);
-			when(channel.transferTo(firstTransferAmount + secondTransferAmount, sizeRemainingAfterSecondTransfer, otherChannel)).thenReturn(thirdTransferAmount);
+			when(channel.transferTo(0, size, otherChannel, 0)).thenReturn(firstTransferAmount);
+			when(channel.transferTo(firstTransferAmount, sizeRemainingAfterFirstTransfer, otherChannel, firstTransferAmount)).thenReturn(secondTransferAmount);
+			when(channel.transferTo(firstTransferAmount + secondTransferAmount, sizeRemainingAfterSecondTransfer, otherChannel, firstTransferAmount + secondTransferAmount)).thenReturn(thirdTransferAmount);
 
 			inTest.copyTo(writableOtherFile);
 
 			InOrder inOrder = inOrder(channel);
-			inOrder.verify(channel).transferTo(0, size, otherChannel);
-			inOrder.verify(channel).transferTo(firstTransferAmount, sizeRemainingAfterFirstTransfer, otherChannel);
-			inOrder.verify(channel).transferTo(firstTransferAmount + secondTransferAmount, sizeRemainingAfterSecondTransfer, otherChannel);
+			inOrder.verify(channel).transferTo(0, size, otherChannel, 0);
+			inOrder.verify(channel).transferTo(firstTransferAmount, sizeRemainingAfterFirstTransfer, otherChannel, firstTransferAmount);
+			inOrder.verify(channel).transferTo(firstTransferAmount + secondTransferAmount, sizeRemainingAfterSecondTransfer, otherChannel, firstTransferAmount + secondTransferAmount);
 		}
 
 	}

+ 377 - 0
main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/SharedFileChannelTest.java

@@ -0,0 +1,377 @@
+package org.cryptomator.filesystem.nio;
+
+import static java.util.Arrays.copyOf;
+import static java.util.Arrays.copyOfRange;
+import static org.apache.commons.io.FileUtils.writeByteArrayToFile;
+import static org.cryptomator.filesystem.nio.PathMatcher.isFile;
+import static org.cryptomator.filesystem.nio.SharedFileChannel.EOF;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import javax.xml.ws.Holder;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+
+import de.bechte.junit.runners.context.HierarchicalContextRunner;
+
+@RunWith(HierarchicalContextRunner.class)
+public class SharedFileChannelTest {
+
+	private static final byte[] FILE_CONTENT = "FileContent".getBytes();
+	private static final byte[] OTHER_FILE_CONTENT = "Other".getBytes();
+
+	@Rule
+	public ExpectedException thrown = ExpectedException.none();
+
+	private Path pathOfTestfile;
+	private Path pathOfOtherTestfile;
+
+	private SharedFileChannel inTest;
+	private SharedFileChannel otherInTest;
+
+	@Before
+	public void setup() throws IOException {
+		pathOfTestfile = Files.createTempFile("sharedFileChannelTest", "tmp");
+		writeByteArrayToFile(pathOfTestfile.toFile(), FILE_CONTENT);
+		pathOfOtherTestfile = Files.createTempFile("sharedFileChannelTest", "tmp");
+		writeByteArrayToFile(pathOfOtherTestfile.toFile(), OTHER_FILE_CONTENT);
+
+		assertThat(pathOfTestfile, isFile().withContent(FILE_CONTENT));
+		assertThat(pathOfOtherTestfile, isFile().withContent(OTHER_FILE_CONTENT));
+
+		inTest = new SharedFileChannel(pathOfTestfile);
+		otherInTest = new SharedFileChannel(pathOfOtherTestfile);
+	}
+
+	public class ReadFully {
+
+		@Before
+		public void setup() {
+			inTest.open(OpenMode.READ);
+		}
+
+		@Test
+		public void testReadFullyReadsFileContents() {
+			byte[] result = new byte[FILE_CONTENT.length];
+			ByteBuffer buffer = ByteBuffer.wrap(result);
+
+			int read = inTest.readFully(0, buffer);
+
+			assertThat(result, is(FILE_CONTENT));
+			assertThat(read, is(FILE_CONTENT.length));
+		}
+
+		@Test
+		public void testReadFullyReadsFromPositionContents() {
+			int position = 5;
+			byte[] result = new byte[FILE_CONTENT.length - position];
+			ByteBuffer buffer = ByteBuffer.wrap(result);
+
+			int read = inTest.readFully(position, buffer);
+
+			assertThat(result, is(copyOfRange(FILE_CONTENT, position, FILE_CONTENT.length)));
+			assertThat(read, is(FILE_CONTENT.length - position));
+		}
+
+		@Test
+		public void testReadFullyReadsTillEndIfBufferHasMoreSpaceAvailabe() {
+			int position = 5;
+			byte[] result = new byte[FILE_CONTENT.length];
+			ByteBuffer buffer = ByteBuffer.wrap(result);
+			byte[] expected = copyOf(copyOfRange(FILE_CONTENT, position, FILE_CONTENT.length), FILE_CONTENT.length);
+
+			int read = inTest.readFully(position, buffer);
+
+			assertThat(result, is(expected));
+			assertThat(read, is(FILE_CONTENT.length - position));
+		}
+
+		@Test
+		public void testReadReadsNothingIfBufferHasNoSpaceAvailabe() {
+			ByteBuffer buffer = ByteBuffer.allocate(5);
+			buffer.position(5);
+
+			int read = inTest.readFully(0, buffer);
+
+			assertThat(buffer.array(), is(new byte[5]));
+			assertThat(read, is(0));
+		}
+
+		@Test
+		public void testReadReadsNothingIfPositionIsEndOfFile() {
+			ByteBuffer buffer = ByteBuffer.allocate(5);
+
+			int read = inTest.readFully(FILE_CONTENT.length, buffer);
+
+			assertThat(buffer.array(), is(new byte[5]));
+			assertThat(read, is(EOF));
+		}
+
+		@Test
+		public void testReadReadsNothingIfAfterEndOfFile() {
+			ByteBuffer buffer = ByteBuffer.allocate(5);
+
+			int read = inTest.readFully(FILE_CONTENT.length + 10, buffer);
+
+			assertThat(buffer.array(), is(new byte[5]));
+			assertThat(read, is(EOF));
+		}
+
+		@Test
+		public void testReadThrowsIllegalArgumentExceptionIfPositionIsNegative() {
+			thrown.expect(IllegalArgumentException.class);
+
+			inTest.readFully(-1, ByteBuffer.allocate(0));
+		}
+
+	}
+
+	public class Truncate {
+
+		@Before
+		public void setup() {
+			inTest.open(OpenMode.WRITE);
+		}
+
+		@Test
+		public void testTruncateToZeroDeletesFileContent() {
+			inTest.truncate(0);
+			inTest.close();
+
+			assertThat(pathOfTestfile, isFile().thatIsEmpty());
+		}
+
+		@Test
+		public void testTruncateToSmallerSizeTruncatesFile() {
+			inTest.truncate(5);
+			inTest.close();
+
+			assertThat(pathOfTestfile, isFile().withContent(copyOfRange(FILE_CONTENT, 0, 5)));
+		}
+
+		@Test
+		public void testTruncateToGreaterSizeDoesNotChangeFile() {
+			inTest.truncate(FILE_CONTENT.length + 10);
+			inTest.close();
+
+			assertThat(pathOfTestfile, isFile().withContent(FILE_CONTENT));
+		}
+	}
+
+	@Test
+	public void testSizeReturnsFileSize() {
+		inTest.open(OpenMode.READ);
+
+		assertThat(inTest.size(), is((long) FILE_CONTENT.length));
+	}
+
+	public class TransferTo {
+
+		@Before
+		public void setup() {
+			inTest.open(OpenMode.READ);
+			otherInTest.open(OpenMode.WRITE);
+		}
+
+		@Test
+		public void testTransferToCopiesSelectedRegionToTargetPosition() {
+			// TODO Markus Kreusch
+		}
+
+		@Test
+		public void testTransferToDoesNothingIfCountIsZero() {
+			// TODO Markus Kreusch
+		}
+
+		@Test
+		public void testTransferToTransfersLessThanCountBytesIfEndOfSourceFileIsReached() {
+			// TODO Markus Kreusch
+		}
+
+		@Test
+		public void testTransferToThrowsIllegalArgumentExceptionIfPositionIsSmallerZero() {
+			// TODO Markus Kreusch
+		}
+
+		@Test
+		public void testTransferToThrowsIllegalArgumentExceptionIfCountIsSmallerZero() {
+			// TODO Markus Kreusch
+		}
+
+		@Test
+		public void testTransferToThrowsIllegalArgumentExceptionIfTargetPositionIsSmallerZero() {
+			// TODO Markus Kreusch
+		}
+
+		@Test
+		public void testTransferToTransfersSelectedRegionToTargetAfterEndOfFile() {
+			// TODO Markus Kreusch
+		}
+
+	}
+
+	public class WriteFully {
+
+		@Before
+		public void setup() {
+			inTest.open(OpenMode.WRITE);
+		}
+
+		@Test
+		public void testWriteFullyWritesFileContents() {
+			// TODO Markus Kreusch
+		}
+
+		@Test
+		public void testWriteFullyWritesContentsToPosition() {
+			// TODO Markus Kreusch
+		}
+
+		@Test
+		public void testWriteFullyWritesContentsEvenIfPositionIsGreaterCurrentEndOfFile() {
+			// TODO Markus Kreusch
+		}
+
+		@Test
+		public void testWriteWritesNothingBufferHasNothingAvailabe() {
+			// TODO Markus Kreusch
+		}
+
+		@Test
+		public void testWriteThrowsIllegalArgumentExceptionIfPositionIsNegative() {
+			thrown.expect(IllegalArgumentException.class);
+
+			inTest.writeFully(-1, ByteBuffer.allocate(0));
+		}
+
+	}
+
+	public class MultipleOpenInvocation {
+
+		@Test
+		public void testInvokeOpenMultipleTimesFromSameThreadThrowsException() {
+			inTest.open(OpenMode.READ);
+
+			thrown.expect(IllegalStateException.class);
+			thrown.expectMessage("A thread can only open a SharedFileChannel once");
+
+			inTest.open(OpenMode.READ);
+		}
+
+		@Test
+		public void testInvokeOpenMultipleTimesFromDifferentThreadsWorks() {
+			inTest.open(OpenMode.READ);
+			inThreadRethrowingExceptions(() -> inTest.open(OpenMode.READ));
+		}
+
+	}
+
+	public class NonOpenChannel {
+
+		@Test
+		public void testClosingNonOpenChannelThrowsException() {
+			thrown.expect(IllegalStateException.class);
+			thrown.expectMessage("SharedFileChannel closed for current thread");
+
+			inTest.close();
+		}
+
+		@Test
+		public void testInvokingReadFullyOnNonOpenChannelThrowsException() {
+			thrown.expect(IllegalStateException.class);
+			thrown.expectMessage("SharedFileChannel closed for current thread");
+
+			inTest.readFully(0, null);
+		}
+
+		@Test
+		public void testInvokingTruncateOnNonOpenChannelThrowsException() {
+			thrown.expect(IllegalStateException.class);
+			thrown.expectMessage("SharedFileChannel closed for current thread");
+
+			inTest.truncate(0);
+		}
+
+		@Test
+		public void testInvokingSizeOnNonOpenChannelThrowsException() {
+			thrown.expect(IllegalStateException.class);
+			thrown.expectMessage("SharedFileChannel closed for current thread");
+
+			inTest.size();
+		}
+
+		@Test
+		public void testInvokingTransferToOnNonOpenChannelThrowsException() {
+			thrown.expect(IllegalStateException.class);
+			thrown.expectMessage("SharedFileChannel closed for current thread");
+
+			inTest.transferTo(0, 1, null, 0);
+		}
+
+		@Test
+		public void testInvokingTransferToPassingNonOpenChannelThrowsException() {
+			thrown.expect(IllegalStateException.class);
+			thrown.expectMessage("SharedFileChannel closed for current thread");
+
+			inTest.open(OpenMode.READ);
+
+			inTest.transferTo(0, 1, otherInTest, 0);
+		}
+
+		@Test
+		public void testInvokinWriteFullyOnNonOpenChannelThrowsException() {
+			thrown.expect(IllegalStateException.class);
+			thrown.expectMessage("SharedFileChannel closed for current thread");
+
+			inTest.writeFully(0, null);
+		}
+
+	}
+
+	@After
+	@SuppressWarnings("deprecation")
+	public void teardown() {
+		inTest.forceClose();
+		otherInTest.forceClose();
+		try {
+			Files.delete(pathOfTestfile);
+		} catch (IOException e) {
+			// ignore
+		}
+		try {
+			Files.delete(pathOfOtherTestfile);
+		} catch (IOException e) {
+			// ignore
+		}
+	}
+
+	private void inThreadRethrowingExceptions(Runnable task) {
+		Holder<RuntimeException> holder = new Holder<>();
+		Thread thread = new Thread(() -> {
+			try {
+				task.run();
+			} catch (RuntimeException e) {
+				holder.value = e;
+			}
+		});
+		thread.start();
+		try {
+			thread.join();
+		} catch (InterruptedException e) {
+		}
+		if (holder.value != null) {
+			throw holder.value;
+		}
+	}
+
+}

+ 58 - 0
main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/ThreadStackMatcher.java

@@ -0,0 +1,58 @@
+package org.cryptomator.filesystem.nio;
+
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeDiagnosingMatcher;
+
+class ThreadStackMatcher extends TypeSafeDiagnosingMatcher<Thread> {
+
+	public static Matcher<Thread> stackContains(Class<?> type, String methodName) {
+		return new ThreadStackMatcher(type, methodName);
+	}
+
+	private final Class<?> type;
+	private final String methodName;
+
+	private ThreadStackMatcher(Class<?> type, String methodName) {
+		super(Thread.class);
+		this.type = type;
+		this.methodName = methodName;
+	}
+
+	@Override
+	public void describeTo(Description description) {
+		description.appendText("a thread executing ") //
+				.appendValue(type) //
+				.appendText(".") //
+				.appendText(methodName);
+	}
+
+	@Override
+	protected boolean matchesSafely(Thread item, Description mismatchDescription) {
+		StackTraceElement[] stack = item.getStackTrace();
+		if (!containsNeededStackTraceElement(stack)) {
+			mismatchDescription.appendText("a thread not executing ") //
+					.appendValue(type) //
+					.appendText(".") //
+					.appendText(methodName);
+			return false;
+		} else {
+			return true;
+		}
+	}
+
+	private boolean containsNeededStackTraceElement(StackTraceElement[] stack) {
+		for (StackTraceElement stackTraceElement : stack) {
+			if (isNeededStackTraceElement(stackTraceElement)) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	private boolean isNeededStackTraceElement(StackTraceElement stackTraceElement) {
+		return type.getName().equals(stackTraceElement.getClassName()) //
+				&& methodName.equals(stackTraceElement.getMethodName());
+	}
+
+}

+ 84 - 0
main/filesystem-nio/src/test/java/org/cryptomator/filesystem/nio/WritableNioFileTest.java

@@ -3,6 +3,8 @@ package org.cryptomator.filesystem.nio;
 import static org.cryptomator.filesystem.nio.OpenMode.WRITE;
 import static org.hamcrest.CoreMatchers.is;
 import static org.junit.Assert.assertThat;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
@@ -158,6 +160,88 @@ public class WritableNioFileTest {
 		assertThat(result, is(resultOfWriteFully));
 	}
 
+	@Test
+	public void testTruncateFailsIfNotOpen() {
+		WritableNioFile inTest = new WritableNioFile(file);
+		inTest.close();
+
+		thrown.expect(UncheckedIOException.class);
+		thrown.expectMessage("already closed");
+
+		inTest.truncate();
+	}
+
+	@Test
+	public void testTruncateInvokesChannelsOpenWithModeWriteIfInvokedForTheFirstTimeBeforeInvokingTruncate() {
+		WritableNioFile inTest = new WritableNioFile(file);
+
+		inTest.truncate();
+
+		InOrder inOrder = inOrder(channel);
+		inOrder.verify(channel).open(WRITE);
+		inOrder.verify(channel).truncate(anyInt());
+	}
+
+	@Test
+	public void testTruncateChannelsTruncateWithZeroAsParameter() {
+		WritableNioFile inTest = new WritableNioFile(file);
+
+		inTest.truncate();
+
+		verify(channel).truncate(0);
+	}
+
+	@Test
+	public void testCloseClosesChannelIfOpenedAndUnlocksWriteLock() {
+		WritableNioFile inTest = new WritableNioFile(file);
+		inTest.truncate();
+
+		inTest.close();
+
+		InOrder inOrder = inOrder(channel, writeLock);
+		inOrder.verify(channel).close();
+		inOrder.verify(writeLock).unlock();
+	}
+
+	@Test
+	public void testCloseDoesNothingOnSecondInvocation() {
+		WritableNioFile inTest = new WritableNioFile(file);
+		inTest.truncate();
+
+		inTest.close();
+		inTest.close();
+
+		InOrder inOrder = inOrder(channel, writeLock);
+		inOrder.verify(channel).close();
+		inOrder.verify(writeLock).unlock();
+	}
+
+	@Test
+	public void testCloseOnlyUnlocksWriteLockIfChannelNotOpen() {
+		WritableNioFile inTest = new WritableNioFile(file);
+
+		inTest.close();
+
+		verify(writeLock).unlock();
+		verifyZeroInteractions(channel);
+	}
+
+	@Test
+	public void testCloseUnlocksWriteLockEvenIfCloseThrowsException() {
+		WritableNioFile inTest = new WritableNioFile(file);
+		inTest.truncate();
+		String message = "exceptionMessage";
+		doThrow(new RuntimeException(message)).when(channel).close();
+
+		thrown.expectMessage(message);
+
+		try {
+			inTest.close();
+		} finally {
+			verify(writeLock).unlock();
+		}
+	}
+
 	@Test
 	public void testToString() {
 		String fileToString = file.toString();