diff --git a/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/FileReadWriteTests.java b/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/FileReadWriteTests.java index 5999b35e1..54cc3da50 100644 --- a/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/FileReadWriteTests.java +++ b/main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/FileReadWriteTests.java @@ -4,13 +4,23 @@ import static org.cryptomator.filesystem.invariants.matchers.NodeMatchers.hasCon import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeThat; import java.nio.ByteBuffer; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import org.cryptomator.filesystem.File; import org.cryptomator.filesystem.FileSystem; +import org.cryptomator.filesystem.ReadableFile; import org.cryptomator.filesystem.WritableFile; import org.cryptomator.filesystem.invariants.FileSystemFactories.FileSystemFactory; import org.cryptomator.filesystem.invariants.WaysToObtainAFile.WayToObtainAFile; @@ -130,4 +140,58 @@ public class FileReadWriteTests { assertThat(file, hasContent(expectedData)); } + @Theory + public void testConcurrentPartialReadsDontInterfere(FileSystemFactory fileSystemFactory, WayToObtainAFile wayToObtainAnExistingFile) throws InterruptedException, ExecutionException { + assumeThat(wayToObtainAnExistingFile.returnedFilesExist(), is(true)); + + FileSystem fileSystem = fileSystemFactory.create(); + byte[] originalData = new byte[] {32, 44, 1, -3, 4, 66, 4}; + byte[] expectedData1 = new byte[] {-3, 4, 66}; + byte[] expectedData2 = new byte[] {44, 1, -3, 4}; + File file = wayToObtainAnExistingFile.fileWithNameAndContent(fileSystem, FILE_NAME, originalData); + + // control flag to make sure thread timing is synchronized correctly + AtomicInteger state = new AtomicInteger(); + + // set position, then wait before read: + byte[] actualData1 = new byte[3]; + Callable readTask1 = () -> { + try (ReadableFile readable = file.openReadable()) { + ByteBuffer buf = ByteBuffer.wrap(actualData1); + readable.position(3); + assertTrue("readTask1 must be the first to set its position", state.compareAndSet(0, 1)); + Thread.sleep(20); + assertTrue("readTask1 must be the last to actually read data", state.compareAndSet(3, 4)); + readable.read(buf); + return null; + } + }; + + // wait, then set position and read: + byte[] actualData2 = new byte[4]; + Callable readTask2 = () -> { + try (ReadableFile readable = file.openReadable()) { + ByteBuffer buf = ByteBuffer.wrap(actualData2); + Thread.sleep(10); + readable.position(1); + assertTrue("readTask2 must be second to set its position", state.compareAndSet(1, 2)); + readable.read(buf); + assertTrue("readTask2 must be first to finish reading", state.compareAndSet(2, 3)); + return null; + } + }; + + // start both read tasks at the same time: + ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 2, 0, TimeUnit.SECONDS, new LinkedBlockingQueue()); + executor.prestartAllCoreThreads(); + Future task1Completed = executor.submit(readTask1); + Future task2Completed = executor.submit(readTask2); + task1Completed.get(); + task2Completed.get(); + executor.shutdown(); + + assertArrayEquals(expectedData1, actualData1); + assertArrayEquals(expectedData2, actualData2); + } + }