diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/workspaces/RamFileSystemProvider.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/workspaces/RamFileSystemProvider.java index f792c400d..a4ad4b61a 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/workspaces/RamFileSystemProvider.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/workspaces/RamFileSystemProvider.java @@ -15,7 +15,7 @@ */ package io.github.ascopes.jct.workspaces; -import io.github.ascopes.jct.workspaces.impl.DefaultFileSystemProviderImpl; +import io.github.ascopes.jct.workspaces.impl.JimfsFileSystemProviderImpl; import java.nio.file.FileSystem; import java.util.ServiceLoader; import org.apiguardian.api.API; @@ -50,6 +50,6 @@ static RamFileSystemProvider getInstance() { return ServiceLoader .load(RamFileSystemProvider.class) .findFirst() - .orElseGet(DefaultFileSystemProviderImpl::getInstance); + .orElseGet(JimfsFileSystemProviderImpl::getInstance); } } diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/workspaces/impl/DefaultFileSystemProviderImpl.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/workspaces/impl/JimfsFileSystemProviderImpl.java similarity index 72% rename from java-compiler-testing/src/main/java/io/github/ascopes/jct/workspaces/impl/DefaultFileSystemProviderImpl.java rename to java-compiler-testing/src/main/java/io/github/ascopes/jct/workspaces/impl/JimfsFileSystemProviderImpl.java index b8216b037..bfc809a30 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/workspaces/impl/DefaultFileSystemProviderImpl.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/workspaces/impl/JimfsFileSystemProviderImpl.java @@ -31,20 +31,24 @@ * @since 0.0.1 (0.0.1-M9) */ @API(since = "0.0.1", status = Status.INTERNAL) -public final class DefaultFileSystemProviderImpl implements RamFileSystemProvider { +public final class JimfsFileSystemProviderImpl implements RamFileSystemProvider { - private static final DefaultFileSystemProviderImpl INSTANCE = new DefaultFileSystemProviderImpl(); + // We could initialise this lazily, but this class has fewer fields and initialisation + // overhead than a lazy-loaded object would, so it doesn't really make sense to do it + // here. + private static final JimfsFileSystemProviderImpl INSTANCE + = new JimfsFileSystemProviderImpl(); /** * Get the singleton instance of this provider. * * @return the singleton instance. */ - public static DefaultFileSystemProviderImpl getInstance() { + public static JimfsFileSystemProviderImpl getInstance() { return INSTANCE; } - private DefaultFileSystemProviderImpl() { + private JimfsFileSystemProviderImpl() { // Singleton object. } @@ -52,7 +56,12 @@ private DefaultFileSystemProviderImpl() { public FileSystem createFileSystem(String name) { var config = Configuration .builder(PathType.unix()) - .setSupportedFeatures(Feature.LINKS, Feature.SYMBOLIC_LINKS, Feature.FILE_CHANNEL) + .setSupportedFeatures( + Feature.LINKS, + Feature.SYMBOLIC_LINKS, + Feature.FILE_CHANNEL, + Feature.SECURE_DIRECTORY_STREAM + ) .setAttributeViews("basic", "posix") .setRoots("/") .setWorkingDirectory("/") diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/workspaces/RamFileSystemProviderTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/workspaces/RamFileSystemProviderTest.java new file mode 100644 index 000000000..3e4cbbfec --- /dev/null +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/workspaces/RamFileSystemProviderTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2022 - 2023, the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.ascopes.jct.tests.unit.workspaces; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import io.github.ascopes.jct.workspaces.RamFileSystemProvider; +import io.github.ascopes.jct.workspaces.impl.JimfsFileSystemProviderImpl; +import java.util.Optional; +import java.util.ServiceLoader; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Isolated; + +/** + * {@link RamFileSystemProvider} tests. + * + * @author Ashley Scopes + */ +@DisplayName("RamFileSystemProvider tests") +@Isolated("messes with global ServiceLoader") +@SuppressWarnings("Java9UndeclaredServiceUsage") +class RamFileSystemProviderTest { + + @DisplayName(".getInstance() returns the first service provider instance if present") + @Test + void getInstanceReturnsTheFirstServiceProviderInstance() { + // Given + RamFileSystemProvider customRamProvider = mock(); + RamFileSystemProvider result; + + try (var serviceLoaderCls = mockStatic(ServiceLoader.class)) { + ServiceLoader serviceLoader = mock(); + + serviceLoaderCls.when(() -> ServiceLoader.load(RamFileSystemProvider.class)) + .thenReturn(serviceLoader); + when(serviceLoader.findFirst()) + .thenReturn(Optional.of(customRamProvider)); + + // When + result = RamFileSystemProvider.getInstance(); + + // Then + serviceLoaderCls.verify(() -> ServiceLoader.load(RamFileSystemProvider.class)); + serviceLoaderCls.verifyNoMoreInteractions(); + + verify(serviceLoader).findFirst(); + verifyNoMoreInteractions(serviceLoader); + } + + // AssertJ needs to use service loaders internally, so we cannot keep it mocked for + // any longer than needed. + assertThat(result).isSameAs(customRamProvider); + } + + @DisplayName(".getInstance() returns the default implementation if no provider is present") + @Test + void getInstanceReturnsTheDefaultImplementationIfNoProviderIsPresent() { + // Given + RamFileSystemProvider result; + + try (var serviceLoaderCls = mockStatic(ServiceLoader.class)) { + ServiceLoader serviceLoader = mock(); + + serviceLoaderCls.when(() -> ServiceLoader.load(RamFileSystemProvider.class)) + .thenReturn(serviceLoader); + when(serviceLoader.findFirst()) + .thenReturn(Optional.empty()); + + // When + result = RamFileSystemProvider.getInstance(); + + // Then + serviceLoaderCls.verify(() -> ServiceLoader.load(RamFileSystemProvider.class)); + serviceLoaderCls.verifyNoMoreInteractions(); + + verify(serviceLoader).findFirst(); + verifyNoMoreInteractions(serviceLoader); + } + + // AssertJ needs to use service loaders internally, so we cannot keep it mocked for + // any longer than needed. + assertThat(result) + .isSameAs(JimfsFileSystemProviderImpl.getInstance()); + } +} diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/workspaces/impl/JimfsFileSystemProviderImplTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/workspaces/impl/JimfsFileSystemProviderImplTest.java new file mode 100644 index 000000000..07ae15791 --- /dev/null +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/workspaces/impl/JimfsFileSystemProviderImplTest.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2022 - 2023, the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.ascopes.jct.tests.unit.workspaces.impl; + + +import static io.github.ascopes.jct.tests.helpers.Fixtures.someText; +import static org.assertj.core.api.Assertions.assertThat; + +import io.github.ascopes.jct.workspaces.impl.JimfsFileSystemProviderImpl; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SecureDirectoryStream; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + + +/** + * {@link JimfsFileSystemProviderImpl} tests. + * + * @author Ashley Scopes + */ +@DisplayName("JimfsFileSystemProviderImpl tests") +class JimfsFileSystemProviderImplTest { + + @DisplayName("The class is a singleton") + @Test + void theClassIsSingleton() { + // When + var instances = Stream + .generate(JimfsFileSystemProviderImpl::getInstance) + .limit(10) + .collect(Collectors.toList()); + + // Then + assertThat(instances) + .withFailMessage("One or more calls provided a different object") + .allMatch(JimfsFileSystemProviderImpl.getInstance()::equals); + } + + @DisplayName("The provider will create a JIMFS file system") + @Test + void theProviderWillCreateJimfsFileSystem() throws IOException { + // When + var instance = JimfsFileSystemProviderImpl.getInstance(); + var fsName = someText(); + try (var fileSystem = instance.createFileSystem(fsName)) { + // Then + assertThat(fileSystem.getClass().getSimpleName()) + .as("file system implementation class") + .isEqualTo("JimfsFileSystem"); + assertThat(fileSystem.getRootDirectories()) + .as("file system root directories") + .hasSize(1); + } + } + + @DisplayName("The created file system supports hard links") + @Test + void theCreatedFileSystemSupportsHardLinks() throws IOException { + // Given + var instance = JimfsFileSystemProviderImpl.getInstance(); + var fsName = someText(); + try (var fileSystem = instance.createFileSystem(fsName)) { + var root = fileSystem.getRootDirectories().iterator().next(); + var fooTxt = root.resolve("foo.txt"); + var barTxt = root.resolve("bar.txt"); + Files.writeString(fooTxt, "Hello, World!"); + + // When + Files.createLink(barTxt, fooTxt); + + // Then + assertThat(barTxt) + .exists() + .isRegularFile() + .hasContent("Hello, World!"); + } + } + + @DisplayName("The created file system supports symbolic links") + @Test + void theCreatedFileSystemSupportsSymbolicLinks() throws IOException { + // Given + var instance = JimfsFileSystemProviderImpl.getInstance(); + var fsName = someText(); + try (var fileSystem = instance.createFileSystem(fsName)) { + var root = fileSystem.getRootDirectories().iterator().next(); + var fooTxt = root.resolve("foo.txt"); + var barTxt = root.resolve("bar.txt"); + Files.writeString(fooTxt, "Hello, World!"); + + // When + Files.createSymbolicLink(barTxt, fooTxt); + + // Then + assertThat(barTxt) + .exists() + .isSymbolicLink() + .hasContent("Hello, World!"); + } + } + + @DisplayName("The created file system supports file channels") + @Test + void theCreatedFileSystemSupportsFileChannels() throws IOException { + // Given + var instance = JimfsFileSystemProviderImpl.getInstance(); + var fsName = someText(); + try (var fileSystem = instance.createFileSystem(fsName)) { + var root = fileSystem.getRootDirectories().iterator().next(); + var fooTxt = root.resolve("foo.txt"); + Files.writeString(fooTxt, "Hello, World!"); + + // When + var buff = ByteBuffer.allocate(4); + var baos = new ByteArrayOutputStream(); + try (var channel = Files.newByteChannel(fooTxt, StandardOpenOption.READ)) { + while (channel.read(buff) != -1) { + baos.write(buff.array(), 0, buff.position()); + buff.clear(); + } + } + + // Then + assertThat(baos.toString(StandardCharsets.UTF_8)) + .isEqualTo("Hello, World!"); + } + } + + @DisplayName("The created file system supports directory streams") + @Test + void theCreatedFileSystemSupportsDirectoryStreams() throws IOException { + // Given + var instance = JimfsFileSystemProviderImpl.getInstance(); + var fsName = someText(); + try (var fileSystem = instance.createFileSystem(fsName)) { + var root = fileSystem.getRootDirectories().iterator().next(); + Files.createDirectories(root.resolve("foo").resolve("bar").resolve("baz")); + Files.createDirectories(root.resolve("do").resolve("ray").resolve("me")); + + // When + var dirs = new ArrayList(); + + try (var dirStream = Files.newDirectoryStream(root)) { + assertThat(dirStream).isInstanceOf(SecureDirectoryStream.class); + dirStream.forEach(dirs::add); + } + + // Then + assertThat(dirs) + .containsExactlyInAnyOrder(root.resolve("foo"), root.resolve("do")); + } + } + + @DisplayName("The created file system supports URLs") + @Test + void theCreatedFileSystemSupportsUrls() throws IOException { + // Given + var instance = JimfsFileSystemProviderImpl.getInstance(); + var fsName = someText(); + try (var fileSystem = instance.createFileSystem(fsName)) { + var root = fileSystem.getRootDirectories().iterator().next(); + var fooTxt = root.resolve("foo.txt"); + Files.writeString(fooTxt, "Hello, World!"); + + // When + var url = fooTxt.toUri().toURL(); + + // Then + try (var is = url.openStream()) { + var baos = new ByteArrayOutputStream(); + is.transferTo(baos); + + assertThat(baos.toString(StandardCharsets.UTF_8)) + .isEqualTo("Hello, World!"); + } + } + } +}