diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/containers/OutputContainerGroup.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/containers/OutputContainerGroup.java index 81bb8bd75..f8c0ab79d 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/containers/OutputContainerGroup.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/containers/OutputContainerGroup.java @@ -26,6 +26,10 @@ *

These can behave as if they are module-oriented, or non-module-oriented. * It is down to the implementation to mediate access between modules and their files. * + *

Operations on modules should first {@link #getModule(String) get} or + * {@link #getOrCreateModule(String) create} the module, and then operate on that sub-container + * group. Operations on non-module packages should operate on this container group directly. + * * @author Ashley Scopes * @since 0.0.1 */ diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/containers/impl/ContainerGroupRepositoryImpl.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/containers/impl/ContainerGroupRepositoryImpl.java index 8ebef892b..1ca23b9df 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/containers/impl/ContainerGroupRepositoryImpl.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/containers/impl/ContainerGroupRepositoryImpl.java @@ -44,7 +44,7 @@ */ @API(since = "0.0.1", status = Status.STABLE) @ThreadSafe -public class ContainerGroupRepositoryImpl { +public final class ContainerGroupRepositoryImpl implements AutoCloseable { private static final Logger LOGGER = LoggerFactory.getLogger(ContainerGroupRepositoryImpl.class); @@ -88,14 +88,10 @@ public void addPath(Location location, PathRoot pathRoot) { } } - /** - * A bulk-style call for {@link #addPath(Location, PathRoot)}. - * - * @param location the location to add. - * @param pathRoots the path roots to register with the location. - */ - public void addPaths(Location location, Iterable pathRoots) { - pathRoots.forEach(pathRoot -> addPath(location, pathRoot)); + @Override + public void close() { + // Nothing to do here. This is a placeholder in case we ever need to allow closing logic + // in the future. } /** @@ -158,6 +154,14 @@ public void createEmptyLocation(Location location) { } } + /** + * Perform any flushing operation, if needed. + */ + public void flush() { + // Nothing to do here. This is a placeholder for a future implementation if we ever need to + // enable flushing. + } + /** * Get a container group. * @@ -421,5 +425,4 @@ private OutputContainerGroup getOrCreateOutputContainerGroup(Location location) outputLocation -> new OutputContainerGroupImpl(outputLocation, release) ); } - } diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/containers/impl/OutputContainerGroupImpl.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/containers/impl/OutputContainerGroupImpl.java index 4e2f6711c..9165d4739 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/containers/impl/OutputContainerGroupImpl.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/containers/impl/OutputContainerGroupImpl.java @@ -29,12 +29,10 @@ import java.util.HashMap; import java.util.Map; import java.util.Set; -import javax.annotation.Nullable; import javax.annotation.WillCloseWhenClosed; import javax.annotation.WillNotClose; import javax.annotation.concurrent.ThreadSafe; import javax.tools.JavaFileManager.Location; -import javax.tools.JavaFileObject.Kind; import org.apiguardian.api.API; import org.apiguardian.api.API.Status; @@ -44,6 +42,10 @@ *

These can contain packages and modules of packages together, and thus * are slightly more complicated internally as a result. * + *

Operations on modules should first {@link #getModule(String) get} or + * {@link #getOrCreateModule(String) create} the module, and then operate on that sub-container + * group. Operations on non-module packages should operate on this container group directly. + * * @author Ashley Scopes * @since 0.0.1 */ @@ -120,84 +122,6 @@ public PackageContainerGroup getModule(String module) { .orElse(null); } - @Override - @Nullable - public PathFileObject getFileForInput(String packageName, String relativeName) { - var moduleName = extractModuleName(packageName); - - if (moduleName != null) { - var module = getModule(moduleName); - - if (module != null) { - var realPackageName = extractPackageName(packageName); - var file = module.getFileForInput(realPackageName, relativeName); - - if (file != null) { - return file; - } - } - } - - return super.getFileForInput(packageName, relativeName); - } - - @Override - @Nullable - public PathFileObject getFileForOutput(String packageName, String relativeName) { - var moduleName = extractModuleName(packageName); - - if (moduleName != null) { - var module = getOrCreateModule(moduleName); - var realPackageName = extractPackageName(packageName); - var file = module.getFileForOutput(realPackageName, relativeName); - - if (file != null) { - return file; - } - } - - return super.getFileForOutput(packageName, relativeName); - } - - @Override - @Nullable - public PathFileObject getJavaFileForInput(String className, Kind kind) { - var moduleName = extractModuleName(className); - - if (moduleName != null) { - var module = getModule(moduleName); - - if (module != null) { - var realClassName = extractPackageName(className); - var file = module.getJavaFileForInput(realClassName, kind); - - if (file != null) { - return file; - } - } - } - - return super.getJavaFileForInput(className, kind); - } - - @Override - @Nullable - public PathFileObject getJavaFileForOutput(String className, Kind kind) { - var moduleName = extractModuleName(className); - - if (moduleName != null) { - var module = getOrCreateModule(moduleName); - var realClassName = extractPackageName(className); - var file = module.getJavaFileForOutput(realClassName, kind); - - if (file != null) { - return file; - } - } - - return super.getJavaFileForOutput(className, kind); - } - @Override public Set getLocationsForModules() { return Set.copyOf(modules.keySet()); @@ -238,19 +162,4 @@ private PackageContainerGroup newPackageGroup(ModuleLocation moduleLocation) { group.addPackage(pathWrapper); return group; } - - @Nullable - private static String extractModuleName(String binaryName) { - // extractModuleName("foo.bar.Baz") -> null - // extractModuleName("some.module/foo.bar.Baz") -> "some.module" - var separatorIndex = binaryName.indexOf('/'); - return separatorIndex == -1 ? null : binaryName.substring(0, separatorIndex); - } - - private static String extractPackageName(String binaryName) { - // extractPackageName("foo.bar.Baz") -> "foo.bar.Baz" - // extractPackageName("some.module/foo.bar.Baz") -> "foo.bar.Baz" - var separatorIndex = binaryName.indexOf('/'); - return separatorIndex == -1 ? binaryName : binaryName.substring(separatorIndex + 1); - } } diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/impl/JctFileManagerImpl.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/impl/JctFileManagerImpl.java index 8e3adf281..3e59d1164 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/impl/JctFileManagerImpl.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/impl/JctFileManagerImpl.java @@ -69,16 +69,16 @@ public void addPath(Location location, PathRoot pathRoot) { @Override public void addPaths(Location location, Collection pathRoots) { - repository.addPaths(location, pathRoots); + pathRoots.forEach(pathRoot -> addPath(location, pathRoot)); } @Override public void close() { - // Nothing to close here. + repository.close(); } @Override - public boolean contains(Location location, FileObject fo) throws IOException { + public boolean contains(Location location, FileObject fo) { if (!(fo instanceof PathFileObject)) { return false; } @@ -99,7 +99,7 @@ public void createEmptyLocation(Location location) { @Override public void flush() { - // Don't do anything else for now. + repository.flush(); } @Nullable @@ -144,25 +144,23 @@ public FileObject getFileForOutput( ) { requireOutputLocation(location); - // If we have a module, we may need to create a brand new location for it. + PackageContainerGroup group = null; + + // If we have a module, we may need to create a brand-new location for it. if (location instanceof ModuleLocation) { var moduleLocation = ((ModuleLocation) location); - var group = repository.getOutputContainerGroup(moduleLocation.getParent()); + var parentGroup = repository.getOutputContainerGroup(moduleLocation.getParent()); - if (group != null) { - return group - .getOrCreateModule(moduleLocation.getModuleName()) - .getFileForOutput(packageName, relativeName); + if (parentGroup != null) { + group = parentGroup.getOrCreateModule(moduleLocation.getModuleName()); } } else { - var group = repository.getOutputContainerGroup(location); - - if (group != null) { - return group.getFileForOutput(packageName, relativeName); - } + group = repository.getOutputContainerGroup(location); } - return null; + return group == null + ? null + : group.getFileForOutput(packageName, relativeName); } @Nullable @@ -189,37 +187,37 @@ public JavaFileObject getJavaFileForOutput( ) { requireOutputLocation(location); - // If we have a module, we may need to create a brand new location for it. + PackageContainerGroup group = null; + + // If we have a module, we may need to create a brand-new location for it. if (location instanceof ModuleLocation) { var moduleLocation = ((ModuleLocation) location); - var group = repository.getOutputContainerGroup(moduleLocation.getParent()); + var parentGroup = repository.getOutputContainerGroup(moduleLocation.getParent()); - if (group != null) { - return group - .getOrCreateModule(moduleLocation.getModuleName()) - .getJavaFileForOutput(className, kind); + if (parentGroup != null) { + group = parentGroup.getOrCreateModule(moduleLocation.getModuleName()); } } else { - var group = repository.getOutputContainerGroup(location); - - if (group != null) { - return group.getJavaFileForOutput(className, kind); - } + group = repository.getOutputContainerGroup(location); } - return null; + return group == null + ? null + : group.getJavaFileForOutput(className, kind); } @Override - public Location getLocationForModule(Location location, String moduleName) { - // This checks that the input location is module/output oriented within the constructor, - // so we don't need to do it here as well. + public ModuleLocation getLocationForModule(Location location, String moduleName) { + // ModuleLocation will also validate this, but we do this to keep consistent + // error messages. + requireOutputOrModuleOrientedLocation(location); + return new ModuleLocation(location, moduleName); } @Nullable @Override - public Location getLocationForModule(Location location, JavaFileObject fo) { + public ModuleLocation getLocationForModule(Location location, JavaFileObject fo) { requireOutputOrModuleOrientedLocation(location); if (fo instanceof PathFileObject) { @@ -227,7 +225,7 @@ public Location getLocationForModule(Location location, JavaFileObject fo) { var moduleLocation = pathFileObject.getLocation(); if (moduleLocation instanceof ModuleLocation) { - return moduleLocation; + return (ModuleLocation) moduleLocation; } // The expectation is to return null if this is not for a module. Certain frameworks like @@ -237,7 +235,7 @@ public Location getLocationForModule(Location location, JavaFileObject fo) { } throw new IllegalArgumentException( - "File object " + fo + " does not appear to be registered to a module" + "File object " + fo + " is not compatible with this file manager" ); } @@ -284,7 +282,7 @@ public ServiceLoader getServiceLoader(Location location, Class service if (group == null) { throw new NoSuchElementException( - "No container group for location " + location.getName() + " exists" + "No container group for location " + location.getName() + " exists in this file manager" ); } @@ -310,12 +308,12 @@ public String inferBinaryName(Location location, JavaFileObject file) { if (!(file instanceof PathFileObject)) { return null; } - var pathFileObject = (PathFileObject) file; + var group = repository.getPackageOrientedContainerGroup(location); return group == null ? null - : group.inferBinaryName(pathFileObject); + : group.inferBinaryName((PathFileObject) file); } @Nullable @@ -331,11 +329,7 @@ public String inferModuleName(Location location) { @Override public boolean isSameFile(@Nullable FileObject a, @Nullable FileObject b) { // Some annotation processors provide null values here for some reason. - if (a == null || b == null) { - return false; - } - - return Objects.equals(a.toUri(), b.toUri()); + return a != null && b != null && Objects.equals(a.toUri(), b.toUri()); } @Override @@ -367,7 +361,9 @@ public Iterable> listLocationsForModules(Location location) { @Override public String toString() { - return new ToStringBuilder(this).toString(); + return new ToStringBuilder(this) + .attribute("repository", repository) + .toString(); } private static void requireOutputOrModuleOrientedLocation(Location location) { diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/helpers/Fixtures.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/helpers/Fixtures.java index f4f5840f4..7fd12763d 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/helpers/Fixtures.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/helpers/Fixtures.java @@ -31,7 +31,6 @@ import io.github.ascopes.jct.utils.LoomPolyfill; import io.github.ascopes.jct.workspaces.PathRoot; import java.io.IOException; -import java.lang.module.ModuleReference; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.FileSystem; @@ -39,18 +38,14 @@ import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; -import java.util.Locale; import java.util.Objects; import java.util.Random; import java.util.UUID; import java.util.stream.Collectors; -import java.util.stream.IntStream; import java.util.stream.Stream; -import javax.annotation.processing.Processor; import javax.tools.Diagnostic; import javax.tools.Diagnostic.Kind; import javax.tools.JavaFileManager.Location; @@ -200,7 +195,6 @@ public static Diagnostic someDiagnostic() { * * @return the mock. */ - @SuppressWarnings("deprecation") public static TraceDiagnostic someTraceDiagnostic() { var mock = mockRaw(TraceDiagnostic.class) .>upcastedTo() @@ -250,61 +244,6 @@ public static List someStackTraceList() { .build(); } - /** - * Get a list of some trace diagnostics. - * - * @return some trace diagnostics. - */ - public static List> someTraceDiagnostics() { - return Stream - .generate(Fixtures::someTraceDiagnostic) - .limit(someInt(3, 8)) - .collect(Collectors.toList()); - } - - /** - * Get some compilation units. - * - * @return some compilation units. - */ - public static List someCompilationUnits() { - return Stream - .generate(() -> mock(JavaFileObject.class, withSettings().strictness(Strictness.LENIENT))) - .peek(mock -> when(mock.getName()).thenReturn(someText())) - .limit(someInt(3, 8)) - .collect(Collectors.toList()); - } - - /** - * Get some unchecked exception with a stacktrace. - * - * @return some exception. - */ - public static Throwable someUncheckedException() { - var message = Stream - .generate(UUID::randomUUID) - .map(UUID::toString) - .limit(someInt(1, 4)) - .collect(joining(" blah blah ")); - return new RuntimeException(message) - .fillInStackTrace(); - } - - /** - * Get some IO exception with a stacktrace. - * - * @return some exception. - */ - public static Throwable someIoException() { - var message = Stream - .generate(UUID::randomUUID) - .map(UUID::toString) - .limit(someInt(1, 4)) - .collect(joining(" blah blah ")); - return new IOException(message) - .fillInStackTrace(); - } - /** * Get some charset. * @@ -321,28 +260,6 @@ public static Charset someCharset() { ); } - /** - * Get some locale. - * - * @return some locale. - */ - public static Locale someLocale() { - return oneOf( - Locale.ROOT, - Locale.US, - Locale.UK, - Locale.ENGLISH, - Locale.GERMAN, - Locale.JAPAN, - Locale.GERMANY, - Locale.JAPANESE, - Locale.SIMPLIFIED_CHINESE, - Locale.TRADITIONAL_CHINESE, - Locale.CHINESE, - Locale.CHINA - ); - } - /** * Get some string release version. * @@ -368,6 +285,49 @@ public static String someModuleName() { .collect(joining(".")); } + /** + * Get a valid random package name. + * + * @return the valid package name. + */ + public static String somePackageName() { + return Stream + .generate(() -> Stream + .generate(() -> (char) someInt('a', 'z')) + .map(Objects::toString) + .limit(someInt(1, 10)) + .collect(joining())) + .limit(someInt(1, 5)) + .collect(joining(".")); + } + + /** + * Get a valid random class name. + * + * @return the valid class name. + */ + public static String someClassName() { + var firstChar = (char) someInt('A', 'Z'); + var restOfClassName = Stream + .generate(() -> (char) someInt('a', 'z')) + .map(Objects::toString) + .limit(someInt(1, 10)) + .collect(joining()); + + return somePackageName() + "." + firstChar + restOfClassName; + } + + /** + * Get some valid binary name. It may or may not be for a module. + * + * @return the binary name. + */ + public static String someBinaryName() { + return someBoolean() + ? someModuleName() + "/" + someClassName() + : someClassName(); + } + /** * Get some mock Java file object with a dummy name and some assigned {@link Kind}. * @@ -391,7 +351,16 @@ public static JavaFileObject someJavaFileObject() { * @return some mock location. */ public static Location someLocation() { - return mock(Location.class, "Location-" + someText()); + var name = "Location-" + someText(); + + Location location = mock(withSettings() + .strictness(Strictness.LENIENT) + .name(name)); + + when(location.getName()).thenReturn(name); + when(location.isOutputLocation()).thenReturn(someBoolean()); + when(location.isModuleOrientedLocation()).thenReturn(someBoolean()); + return location; } /** @@ -443,15 +412,6 @@ public static Path someRelativePath() { return absolutePath.relativize(relativePath.resolve("some-relative-path")); } - /** - * Get some module reference. - * - * @return the module reference. - */ - public static ModuleReference someModuleReference() { - return mock(ModuleReference.class); - } - /** * Get some temporary file system. * @@ -463,15 +423,6 @@ public static TempFileSystem someTemporaryFileSystem() { return new TempFileSystem(); } - /** - * Get some annotation processor. - * - * @return some annotation processor. - */ - public static Processor someAnnotationProcessor() { - return mock(Processor.class, someText() + " processor"); - } - /** * Return one of the given elements. * diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/impl/JctFileManagerImplTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/impl/JctFileManagerImplTest.java index 406efafa5..163c041d8 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/impl/JctFileManagerImplTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/impl/JctFileManagerImplTest.java @@ -15,27 +15,81 @@ */ package io.github.ascopes.jct.tests.unit.filemanagers.impl; +import static io.github.ascopes.jct.tests.helpers.Fixtures.oneOf; +import static io.github.ascopes.jct.tests.helpers.Fixtures.someAbsolutePath; +import static io.github.ascopes.jct.tests.helpers.Fixtures.someBinaryName; +import static io.github.ascopes.jct.tests.helpers.Fixtures.someBoolean; +import static io.github.ascopes.jct.tests.helpers.Fixtures.someClassName; +import static io.github.ascopes.jct.tests.helpers.Fixtures.someFlags; +import static io.github.ascopes.jct.tests.helpers.Fixtures.someInt; +import static io.github.ascopes.jct.tests.helpers.Fixtures.someJavaFileObject; import static io.github.ascopes.jct.tests.helpers.Fixtures.someLocation; +import static io.github.ascopes.jct.tests.helpers.Fixtures.someModuleName; +import static io.github.ascopes.jct.tests.helpers.Fixtures.somePackageName; +import static io.github.ascopes.jct.tests.helpers.Fixtures.somePathRoot; +import static io.github.ascopes.jct.tests.helpers.Fixtures.someRelativePath; +import static io.github.ascopes.jct.tests.helpers.Fixtures.someText; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.InstanceOfAssertFactories.collection; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import io.github.ascopes.jct.containers.ContainerGroup; +import io.github.ascopes.jct.containers.ModuleContainerGroup; +import io.github.ascopes.jct.containers.OutputContainerGroup; +import io.github.ascopes.jct.containers.PackageContainerGroup; import io.github.ascopes.jct.containers.impl.ContainerGroupRepositoryImpl; +import io.github.ascopes.jct.filemanagers.ModuleLocation; +import io.github.ascopes.jct.filemanagers.PathFileObject; import io.github.ascopes.jct.filemanagers.impl.JctFileManagerImpl; -import io.github.ascopes.jct.workspaces.PathRoot; +import io.github.ascopes.jct.tests.helpers.Fixtures; +import java.io.IOException; +import java.net.URI; +import java.util.Collection; +import java.util.NoSuchElementException; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.tools.FileObject; +import javax.tools.JavaFileManager.Location; +import javax.tools.JavaFileObject; +import javax.tools.JavaFileObject.Kind; +import javax.tools.StandardLocation; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.InOrder; import org.mockito.junit.jupiter.MockitoExtension; +/** + * {@link JctFileManagerImpl} tests. + * + * @author Ashley Scopes + */ @DisplayName("JctFileManagerImpl Tests") @ExtendWith(MockitoExtension.class) +@SuppressWarnings({"DataFlowIssue", "resource"}) class JctFileManagerImplTest { JctFileManagerImpl fileManager; ContainerGroupRepositoryImpl repository; + InOrder order; @BeforeEach void setUp() { @@ -44,30 +98,1308 @@ void setUp() { try (var construction = mockConstruction(ContainerGroupRepositoryImpl.class)) { fileManager = new JctFileManagerImpl("some-release"); repository = construction.constructed().iterator().next(); + order = inOrder(repository); } } - @DisplayName("null releases are disallowed") + @DisplayName("Constructor disallows null releases") @SuppressWarnings({"resource", "ConstantConditions"}) @Test - void testIfNullPointerExceptionThrownIfReleaseNull() { + void constructorDisallowsNullReleases() { // Then assertThatThrownBy(() -> new JctFileManagerImpl(null)) .isInstanceOf(NullPointerException.class) .hasMessage("release"); } - @DisplayName(".addPath(Location, PathRoot) adds the path to the repository") + @DisplayName(".addPath(...) delegates to the repository") @Test - void addPathAddsThePathToTheRepository() { + void addPathDelegatesToRepository() { // Given var location = someLocation(); - var pathRoot = mock(PathRoot.class); + var pathRoot = somePathRoot(); // When fileManager.addPath(location, pathRoot); // Then verify(repository).addPath(location, pathRoot); + verifyNoMoreInteractions(repository); + } + + @DisplayName(".addPaths(...) delegates multiple calls to the repository") + @Test + void addPathsDelegatesMultipleCallsToTheRepository() { + // Given + var location = someLocation(); + var pathRoots = Stream + .generate(Fixtures::somePathRoot) + .limit(10) + .collect(Collectors.toList()); + + // When + fileManager.addPaths(location, pathRoots); + + // Then + pathRoots.forEach(pathRoot -> order.verify(repository).addPath(location, pathRoot)); + order.verifyNoMoreInteractions(); + } + + @DisplayName(".close() closes the repository") + @Test + void closeDelegatesToTheRepository() { + // When + fileManager.close(); + + // Then + verify(repository).close(); + verifyNoMoreInteractions(repository); + } + + @DisplayName(".contains(...) tests") + @Nested + class ContainsTest { + + @DisplayName(".contains(...) returns false if the file object is not a PathFileObject") + @Test + void containsReturnsFalseIfFileObjectIsNotPathFileObject() { + // Given + var location = someLocation(); + FileObject fileObject = mock(); + + // When + var result = fileManager.contains(location, fileObject); + + // Then + assertThat(result).isFalse(); + verifyNoInteractions(repository); + } + + @DisplayName(".contains(...) returns false if the location is not in the repository") + @Test + void containsReturnsFalseIfTheLocationIsNotInTheRepository() { + var location = someLocation(); + PathFileObject fileObject = mock(); + + when(repository.getContainerGroup(any())) + .thenReturn(null); + + // When + var result = fileManager.contains(location, fileObject); + + // Then + verify(repository).getContainerGroup(location); + assertThat(result).isFalse(); + verifyNoMoreInteractions(repository); + } + + @DisplayName(".contains(...) checks the expected container group") + @ValueSource(booleans = {true, false}) + @ParameterizedTest(name = "for group.contains(...) = {0}") + void containsChecksTheExpectedContainerGroup(boolean contained) { + var location = someLocation(); + PathFileObject fileObject = mock(); + ContainerGroup containerGroup = mock(); + when(repository.getContainerGroup(any())) + .thenReturn(containerGroup); + when(containerGroup.contains(any())) + .thenReturn(contained); + + // When + var result = fileManager.contains(location, fileObject); + + // Then + verify(repository).getContainerGroup(location); + verifyNoMoreInteractions(repository); + + verify(containerGroup).contains(fileObject); + verifyNoMoreInteractions(containerGroup); + + assertThat(result).isEqualTo(contained); + } + } + + @DisplayName(".copyContainers(...) delegates to the repository") + @Test + void copyContainersDelegatesToTheRepository() { + // Given + var from = someLocation(); + var to = someLocation(); + + // When + fileManager.copyContainers(from, to); + + // Then + verify(repository).copyContainers(from, to); + verifyNoMoreInteractions(repository); + } + + @DisplayName(".createEmptyLocation(...) delegates to the repository") + @Test + void createEmptyLocationDelegatesToTheRepository() { + // Given + var location = someLocation(); + + // When + fileManager.createEmptyLocation(location); + + // Then + verify(repository).createEmptyLocation(location); + verifyNoMoreInteractions(repository); + } + + @DisplayName(".flush() flushes the repository") + @Test + void flushDelegatesToTheRepository() { + // When + fileManager.flush(); + + // Then + verify(repository).flush(); + verifyNoMoreInteractions(repository); + } + + @DisplayName(".getClassLoader(...) tests") + @Nested + class GetClassLoaderTest { + + @DisplayName(".getClassLoader(...) returns null if the group does not exist") + @Test + void getClassLoaderReturnsNullIfGroupDoesNotExist() { + // Given + var location = someLocation(); + when(repository.getPackageOrientedContainerGroup(any())) + .thenReturn(null); + + // When + var result = fileManager.getClassLoader(location); + + // Then + verify(repository).getPackageOrientedContainerGroup(location); + verifyNoMoreInteractions(repository); + assertThat(result).isNull(); + } + + @DisplayName(".getClassLoader(...) calls .getClassLoader() on the container group") + @Test + void getClassLoaderDelegatesToTheContainerGroup() { + // Given + var location = someLocation(); + PackageContainerGroup containerGroup = mock(); + ClassLoader classLoader = mock(); + + when(repository.getPackageOrientedContainerGroup(any())) + .thenReturn(containerGroup); + when(containerGroup.getClassLoader()) + .thenReturn(classLoader); + + // When + var result = fileManager.getClassLoader(location); + + // Then + verify(repository).getPackageOrientedContainerGroup(location); + verifyNoMoreInteractions(repository); + + verify(containerGroup).getClassLoader(); + verifyNoMoreInteractions(containerGroup); + + assertThat(result).isSameAs(classLoader); + } + } + + @DisplayName(".getEffectiveRelease() returns the effective release") + @ValueSource(strings = {"10", "11", "12", "foobar"}) + @ParameterizedTest(name = "for effective release = {0}") + void getEffectiveReleaseReturnsTheEffectiveRelease(String effectiveRelease) { + // Given + try (var fileManager = new JctFileManagerImpl(effectiveRelease)) { + // Then + assertThat(fileManager.getEffectiveRelease()).isEqualTo(effectiveRelease); + } + } + + @DisplayName(".getFileForInput(...) tests") + @Nested + class GetFileForInputTest { + + @DisplayName(".getFileForInput(...) returns null if the group does not exist") + @Test + void getFileForInputReturnsNullIfGroupDoesNotExist() { + // Given + var location = someLocation(); + var packageName = somePackageName(); + var relativeName = someRelativePath().toString(); + + when(repository.getPackageOrientedContainerGroup(any())) + .thenReturn(null); + + // When + var result = fileManager.getFileForInput(location, packageName, relativeName); + + // Then + verify(repository).getPackageOrientedContainerGroup(location); + verifyNoMoreInteractions(repository); + assertThat(result).isNull(); + } + + @DisplayName(".getFileForInput(...) calls .getFileForInput(...) on the container group") + @Test + void getFileForInputCallsGetFileForInputOnTheContainerGroup() { + // Given + var location = someLocation(); + var packageName = somePackageName(); + var relativeName = someRelativePath().toString(); + PackageContainerGroup containerGroup = mock(); + PathFileObject fileObject = mock(); + + when(repository.getPackageOrientedContainerGroup(any())) + .thenReturn(containerGroup); + when(containerGroup.getFileForInput(any(), any())) + .thenReturn(fileObject); + + // When + var result = fileManager.getFileForInput(location, packageName, relativeName); + + // Then + verify(repository).getPackageOrientedContainerGroup(location); + verifyNoMoreInteractions(repository); + + verify(containerGroup).getFileForInput(packageName, relativeName); + verifyNoMoreInteractions(containerGroup); + + assertThat(result).isSameAs(fileObject); + } + } + + @DisplayName(".getFileForOutput(...) tests") + @Nested + class GetFileForOutputTest { + + @DisplayName(".getFileForOutput(...) throws an exception if the location isn't output-oriented") + @Test + void throwsExceptionIfLocationIsNotOutputOriented() { + // Given + var name = someText(); + Location location = mock(someText()); + when(location.isOutputLocation()).thenReturn(false); + when(location.getName()).thenReturn(name); + + // Then + assertThatThrownBy(() -> fileManager + .getFileForOutput( + location, + somePackageName(), + someRelativePath().toString(), + someJavaFileObject() + )) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Location %s must be an output location", name); + } + + @DisplayName(".getFileForOutput(ModuleLocation, ...) returns null if the group does not exist") + @Test + void moduleLocationReturnsNullIfGroupDoesNotExist() { + // Given + var parentLocation = StandardLocation.CLASS_OUTPUT; + var moduleLocation = new ModuleLocation(parentLocation, someModuleName()); + var packageName = somePackageName(); + var relativeName = someRelativePath().toString(); + var sibling = someJavaFileObject(); + + when(repository.getOutputContainerGroup(any())) + .thenReturn(null); + + // When + var result = fileManager.getFileForOutput(moduleLocation, packageName, relativeName, sibling); + + // Then + verify(repository).getOutputContainerGroup(parentLocation); + verifyNoMoreInteractions(repository); + + assertThat(result).isNull(); + } + + @DisplayName( + ".getFileForOutput(ModuleLocation, ...) creates the new location and " + + "returns the file object") + @Test + void moduleLocationCreatesTheNewLocationAndReturnsTheFileObject() { + // Given + var parentLocation = StandardLocation.SOURCE_OUTPUT; + var moduleName = someModuleName(); + var moduleLocation = new ModuleLocation(parentLocation, moduleName); + var packageName = somePackageName(); + var relativeName = someRelativePath().toString(); + var sibling = someJavaFileObject(); + OutputContainerGroup outputContainerGroup = mock(); + PackageContainerGroup moduleGroup = mock(); + PathFileObject fileForOutput = mock(); + + when(repository.getOutputContainerGroup(any())) + .thenReturn(outputContainerGroup); + when(outputContainerGroup.getOrCreateModule(any())) + .thenReturn(moduleGroup); + when(moduleGroup.getFileForOutput(any(), any())) + .thenReturn(fileForOutput); + + // When + var result = fileManager.getFileForOutput(moduleLocation, packageName, relativeName, sibling); + + // Then + verify(repository).getOutputContainerGroup(parentLocation); + verifyNoMoreInteractions(repository); + + verify(outputContainerGroup).getOrCreateModule(moduleName); + verifyNoMoreInteractions(outputContainerGroup); + + verify(moduleGroup).getFileForOutput(packageName, relativeName); + verifyNoMoreInteractions(moduleGroup); + + assertThat(result).isSameAs(fileForOutput); + } + + @DisplayName(".getFileForOutput(Location, ...) returns null if the group does not exist") + @Test + void locationReturnsNullIfGroupDoesNotExist() { + // Given + var location = StandardLocation.SOURCE_OUTPUT; + var packageName = somePackageName(); + var relativeName = someRelativePath().toString(); + var sibling = someJavaFileObject(); + + when(repository.getOutputContainerGroup(any())) + .thenReturn(null); + + // When + var result = fileManager.getFileForOutput(location, packageName, relativeName, sibling); + + // Then + verify(repository).getOutputContainerGroup(location); + verifyNoMoreInteractions(repository); + + assertThat(result).isNull(); + } + + @DisplayName(".getFileForOutput(Location, ...) returns the file object") + @Test + void locationReturnsTheFileObject() { + // Given + var location = StandardLocation.NATIVE_HEADER_OUTPUT; + var packageName = somePackageName(); + var relativeName = someRelativePath().toString(); + var sibling = someJavaFileObject(); + OutputContainerGroup outputContainerGroup = mock(); + PathFileObject fileForOutput = mock(); + + when(repository.getOutputContainerGroup(any())) + .thenReturn(outputContainerGroup); + when(outputContainerGroup.getFileForOutput(any(), any())) + .thenReturn(fileForOutput); + + // When + var result = fileManager.getFileForOutput(location, packageName, relativeName, sibling); + + // Then + verify(repository).getOutputContainerGroup(location); + verifyNoMoreInteractions(repository); + + verify(outputContainerGroup).getFileForOutput(packageName, relativeName); + verifyNoMoreInteractions(outputContainerGroup); + + assertThat(result).isSameAs(fileForOutput); + } + } + + @DisplayName(".getJavaFileForInput(...) tests") + @Nested + class GetJavaFileForInputTest { + + @DisplayName(".getJavaFileForInput(...) returns null if the group does not exist") + @Test + void getJavaFileForInputReturnsNullIfGroupDoesNotExist() { + // Given + var location = someLocation(); + var className = someClassName(); + var kind = oneOf(Kind.class); + + when(repository.getPackageOrientedContainerGroup(any())) + .thenReturn(null); + + // When + var result = fileManager.getJavaFileForInput(location, className, kind); + + // Then + verify(repository).getPackageOrientedContainerGroup(location); + verifyNoMoreInteractions(repository); + assertThat(result).isNull(); + } + + @DisplayName(".getJavaFileForInput(...) calls .getJavaFileForInput(...) on the container group") + @Test + void getJavaFileForInputCallsGetJavaFileForInputOnTheContainerGroup() { + // Given + var location = someLocation(); + PackageContainerGroup containerGroup = mock(); + PathFileObject fileObject = mock(); + var className = someClassName(); + var kind = oneOf(Kind.class); + + when(repository.getPackageOrientedContainerGroup(any())) + .thenReturn(containerGroup); + when(containerGroup.getJavaFileForInput(any(), any())) + .thenReturn(fileObject); + + // When + var result = fileManager.getJavaFileForInput(location, className, kind); + + // Then + verify(repository).getPackageOrientedContainerGroup(location); + verifyNoMoreInteractions(repository); + + verify(containerGroup).getJavaFileForInput(className, kind); + verifyNoMoreInteractions(containerGroup); + + assertThat(result).isSameAs(fileObject); + } + } + + + @DisplayName(".getJavaFileForOutput(...) tests") + @Nested + class GetJavaFileForOutputTest { + + @DisplayName( + ".getJavaFileForOutput(...) throws an exception if the location isn't output-oriented" + ) + @Test + void throwsExceptionIfLocationIsNotOutputOriented() { + // Given + var name = someText(); + Location location = mock(someText()); + var kind = oneOf(Kind.class); + when(location.isOutputLocation()).thenReturn(false); + when(location.getName()).thenReturn(name); + + // Then + assertThatThrownBy(() -> fileManager + .getJavaFileForOutput( + location, + somePackageName(), + kind, + someJavaFileObject() + )) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Location %s must be an output location", name); + } + + @DisplayName( + ".getJavaFileForOutput(ModuleLocation, ...) returns null if the group does not exist" + ) + @Test + void moduleLocationReturnsNullIfGroupDoesNotExist() { + // Given + var parentLocation = StandardLocation.CLASS_OUTPUT; + var moduleLocation = new ModuleLocation(parentLocation, someModuleName()); + var className = someClassName(); + var kind = oneOf(Kind.class); + var sibling = someJavaFileObject(); + + when(repository.getOutputContainerGroup(any())) + .thenReturn(null); + + // When + var result = fileManager.getJavaFileForOutput(moduleLocation, className, kind, sibling); + + // Then + verify(repository).getOutputContainerGroup(parentLocation); + verifyNoMoreInteractions(repository); + + assertThat(result).isNull(); + } + + @DisplayName( + ".getJavaFileForOutput(ModuleLocation, ...) creates the new location and " + + "returns the file object") + @Test + void moduleLocationCreatesTheNewLocationAndReturnsTheFileObject() { + // Given + var parentLocation = StandardLocation.SOURCE_OUTPUT; + var moduleName = someModuleName(); + var moduleLocation = new ModuleLocation(parentLocation, moduleName); + var className = someClassName(); + var kind = oneOf(Kind.class); + var sibling = someJavaFileObject(); + OutputContainerGroup outputContainerGroup = mock(); + PackageContainerGroup moduleGroup = mock(); + PathFileObject javaFileForOutput = mock(); + + when(repository.getOutputContainerGroup(any())) + .thenReturn(outputContainerGroup); + when(outputContainerGroup.getOrCreateModule(any())) + .thenReturn(moduleGroup); + when(moduleGroup.getJavaFileForOutput(any(), any())) + .thenReturn(javaFileForOutput); + + // When + var result = fileManager.getJavaFileForOutput(moduleLocation, className, kind, sibling); + + // Then + verify(repository).getOutputContainerGroup(parentLocation); + verifyNoMoreInteractions(repository); + + verify(outputContainerGroup).getOrCreateModule(moduleName); + verifyNoMoreInteractions(outputContainerGroup); + + verify(moduleGroup).getJavaFileForOutput(className, kind); + verifyNoMoreInteractions(moduleGroup); + + assertThat(result).isSameAs(javaFileForOutput); + } + + @DisplayName(".getJavaFileForOutput(Location, ...) returns null if the group does not exist") + @Test + void locationReturnsNullIfGroupDoesNotExist() { + // Given + var location = StandardLocation.SOURCE_OUTPUT; + var className = someClassName(); + var kind = oneOf(Kind.class); + var sibling = someJavaFileObject(); + + when(repository.getOutputContainerGroup(any())) + .thenReturn(null); + + // When + var result = fileManager.getJavaFileForOutput(location, className, kind, sibling); + + // Then + verify(repository).getOutputContainerGroup(location); + verifyNoMoreInteractions(repository); + + assertThat(result).isNull(); + } + + @DisplayName(".getJavaFileForOutput(Location, ...) returns the file object") + @Test + void locationReturnsTheFileObject() { + // Given + var location = StandardLocation.NATIVE_HEADER_OUTPUT; + var className = someClassName(); + var kind = oneOf(Kind.class); + var sibling = someJavaFileObject(); + OutputContainerGroup outputContainerGroup = mock(); + PathFileObject javaFileForOutput = mock(); + + when(repository.getOutputContainerGroup(any())) + .thenReturn(outputContainerGroup); + when(outputContainerGroup.getJavaFileForOutput(any(), any())) + .thenReturn(javaFileForOutput); + + // When + var result = fileManager.getJavaFileForOutput(location, className, kind, sibling); + + // Then + verify(repository).getOutputContainerGroup(location); + verifyNoMoreInteractions(repository); + + verify(outputContainerGroup).getJavaFileForOutput(className, kind); + verifyNoMoreInteractions(outputContainerGroup); + + assertThat(result).isSameAs(javaFileForOutput); + } + } + + @DisplayName(".getLocationForModule(...) tests") + @Nested + class GetLocationForModuleTest { + + @DisplayName( + ".getLocationForModule(Location, String) throws an exception for package locations" + ) + @Test + void getLocationForModuleStringThrowsExceptionForPackageLocations() { + // Given + var location = StandardLocation.CLASS_PATH; + var moduleName = someModuleName(); + + // Then + assertThatThrownBy(() -> fileManager.getLocationForModule(location, moduleName)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Location %s must be output or module-oriented", location.getName()); + } + + @DisplayName(".getLocationForModule(Location, String) returns the module location") + @EnumSource( + value = StandardLocation.class, + names = {"CLASS_OUTPUT", "SOURCE_OUTPUT", "MODULE_SOURCE_PATH"} + ) + @ParameterizedTest(name = "for location StandardLocation.{0}") + void getLocationForModuleStringReturnsModuleLocation(Location location) { + // Given + var moduleName = someModuleName(); + + // When + var moduleLocation = fileManager.getLocationForModule(location, moduleName); + + // Then + assertThat(moduleLocation.getModuleName()).isEqualTo(moduleName); + assertThat(moduleLocation.getParent()).isEqualTo(location); + } + + @DisplayName( + ".getLocationForModule(Location, FileObject) throws an exception for package locations" + ) + @Test + void getLocationForModuleFileObjectThrowsExceptionForPackageLocations() { + // Given + var location = StandardLocation.CLASS_PATH; + var fileObject = someJavaFileObject(); + + // Then + assertThatThrownBy(() -> fileManager.getLocationForModule(location, fileObject)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Location %s must be output or module-oriented", location.getName()); + } + + @DisplayName(".getLocationForModule(Location, FileObject) throws an exception") + @Test + void getLocationForModuleFileObjectThrowsException() { + // Given + var location = StandardLocation.MODULE_SOURCE_PATH; + var fileObject = someJavaFileObject(); + + // Then + assertThatThrownBy(() -> fileManager.getLocationForModule(location, fileObject)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("File object %s is not compatible with this file manager", fileObject); + } + + @DisplayName( + ".getLocationForModule(Location, PathFileObject) returns null for non-module file objects" + ) + @Test + void getLocationForModulePathFileObjectReturnsNullForNonModuleFileObjects() { + // Given + var location = StandardLocation.MODULE_PATH; + PathFileObject fileObject = mock(); + when(fileObject.getLocation()).thenReturn(StandardLocation.SOURCE_PATH); + + // When + var moduleLocation = fileManager.getLocationForModule(location, fileObject); + + // Then + assertThat(moduleLocation).isNull(); + } + + @DisplayName( + ".getLocationForModule(Location, PathFileObject) returns the module " + + "location for file objects" + ) + @Test + void getLocationForModulePathFileObjectReturnsTheModuleLocationForFileObjects() { + // Given + var location = StandardLocation.MODULE_SOURCE_PATH; + var expectedModuleLocation = new ModuleLocation(location, someModuleName()); + PathFileObject fileObject = mock(); + when(fileObject.getLocation()).thenReturn(expectedModuleLocation); + + // When + var returnedModuleLocation = fileManager.getLocationForModule(location, fileObject); + + // Then + assertThat(returnedModuleLocation).isSameAs(expectedModuleLocation); + } + } + + @DisplayName(".getModuleContainerGroup(...) tests") + @Nested + class GetModuleContainerGroupTest { + + @DisplayName( + ".getModuleContainerGroup(...) throws an exception for non module-oriented container groups" + ) + @EnumSource( + value = StandardLocation.class, + names = {"SOURCE_PATH", "CLASS_OUTPUT"} + ) + @ParameterizedTest(name = "for location StandardLocation.{0}") + void getModuleContainerGroupThrowsExceptionForNonModuleOrientedContainerGroups( + Location location + ) { + // Then + assertThatThrownBy(() -> fileManager.getModuleContainerGroup(location)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Location %s must be module-oriented", location.getName()); + + verifyNoInteractions(repository); + } + + @DisplayName(".getModuleContainerGroup(...) delegates to the repository") + @Test + void getModuleContainerGroupDelegatesToTheRepository() { + // Given + var location = StandardLocation.MODULE_SOURCE_PATH; + ModuleContainerGroup containerGroup = mock(); + when(repository.getModuleContainerGroup(any())) + .thenReturn(containerGroup); + + // When + var result = fileManager.getModuleContainerGroup(location); + + // Then + verify(repository).getModuleContainerGroup(location); + verifyNoMoreInteractions(repository); + assertThat(result).isSameAs(containerGroup); + } + } + + @DisplayName(".getModuleContainerGroups() delegates to the repository") + @Test + void getModuleContainerGroupsDelegatesToTheRepository() { + // Given + Collection moduleContainerGroups = mock(); + when(repository.getModuleContainerGroups()) + .thenReturn(moduleContainerGroups); + + // When + var result = fileManager.getModuleContainerGroups(); + + // Then + verify(repository).getModuleContainerGroups(); + verifyNoMoreInteractions(repository); + assertThat(result).isSameAs(moduleContainerGroups); + } + + @DisplayName(".getOutputContainerGroup(...) tests") + @Nested + class GetOutputContainerGroupTest { + + @DisplayName( + ".getOutputContainerGroup(...) throws an exception for non output-oriented container groups" + ) + @EnumSource( + value = StandardLocation.class, + names = {"SOURCE_PATH", "MODULE_SOURCE_PATH"} + ) + @ParameterizedTest(name = "for location StandardLocation.{0}") + void getOutputContainerGroupThrowsExceptionForNonOutputOrientedContainerGroups( + Location location + ) { + // Then + assertThatThrownBy(() -> fileManager.getOutputContainerGroup(location)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Location %s must be an output location", location.getName()); + + verifyNoInteractions(repository); + } + + @DisplayName(".getOutputContainerGroup(...) delegates to the repository") + @Test + void getOutputContainerGroupDelegatesToTheRepository() { + // Given + var location = StandardLocation.CLASS_OUTPUT; + OutputContainerGroup containerGroup = mock(); + when(repository.getOutputContainerGroup(any())) + .thenReturn(containerGroup); + + // When + var result = fileManager.getOutputContainerGroup(location); + + // Then + verify(repository).getOutputContainerGroup(location); + verifyNoMoreInteractions(repository); + assertThat(result).isSameAs(containerGroup); + } + } + + + @DisplayName(".getOutputContainerGroups() delegates to the repository") + @Test + void getOutputContainerGroupsDelegatesToTheRepository() { + // Given + Collection outputContainerGroups = mock(); + when(repository.getOutputContainerGroups()) + .thenReturn(outputContainerGroups); + + // When + var result = fileManager.getOutputContainerGroups(); + + // Then + verify(repository).getOutputContainerGroups(); + verifyNoMoreInteractions(repository); + assertThat(result).isSameAs(outputContainerGroups); + } + + @DisplayName(".getPackageContainerGroup(...) tests") + @Nested + class GetPackageContainerGroupTest { + + @DisplayName( + ".getPackageContainerGroup(...) throws an exception for non package container groups" + ) + @EnumSource( + value = StandardLocation.class, + names = {"CLASS_OUTPUT", "MODULE_SOURCE_PATH"} + ) + @ParameterizedTest(name = "for location StandardLocation.{0}") + void getPackageContainerGroupThrowsExceptionForNonPackageContainerGroups(Location location) { + // Then + assertThatThrownBy(() -> fileManager.getPackageContainerGroup(location)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Location %s must be an input package location", location.getName()); + + verifyNoInteractions(repository); + } + + @DisplayName(".getPackageContainerGroup(...) delegates to the repository") + @Test + void getPackageContainerGroupDelegatesToTheRepository() { + // Given + var location = StandardLocation.SOURCE_PATH; + PackageContainerGroup containerGroup = mock(); + when(repository.getPackageContainerGroup(any())) + .thenReturn(containerGroup); + + // When + var result = fileManager.getPackageContainerGroup(location); + + // Then + verify(repository).getPackageContainerGroup(location); + verifyNoMoreInteractions(repository); + assertThat(result).isSameAs(containerGroup); + } + } + + @DisplayName(".getPackageContainerGroups() delegates to the repository") + @Test + void getPackageContainerGroupsDelegatesToTheRepository() { + // Given + Collection packageContainerGroups = mock(); + when(repository.getPackageContainerGroups()) + .thenReturn(packageContainerGroups); + + // When + var result = fileManager.getPackageContainerGroups(); + + // Then + verify(repository).getPackageContainerGroups(); + verifyNoMoreInteractions(repository); + assertThat(result).isSameAs(packageContainerGroups); + } + + @DisplayName(".getServiceLoader(...) tests") + @Nested + class GetServiceLoaderTest { + + @DisplayName(".getServiceLoader(...) throws an exception if the location does not exist") + @Test + void getServiceLoaderThrowsExceptionIfLocationDoesNotExist() { + // Given + class Some {} + + var location = someLocation(); + when(repository.getContainerGroup(any())) + .thenReturn(null); + + // Then + assertThatThrownBy(() -> fileManager.getServiceLoader(location, Some.class)) + .isInstanceOf(NoSuchElementException.class) + .hasMessage("No container group for location %s exists in this file manager", + location.getName()); + + verify(repository).getContainerGroup(location); + verifyNoMoreInteractions(repository); + } + + @DisplayName(".getServiceLoader(...) delegates to the group") + @Test + void getServiceLoaderDelegatesToTheGroup() { + // Given + class Some {} + var location = someLocation(); + ContainerGroup containerGroup = mock(); + ServiceLoader serviceLoader = mock(); + when(repository.getContainerGroup(any())) + .thenReturn(containerGroup); + when(containerGroup.getServiceLoader(any())) + .thenAnswer(ctx -> serviceLoader); + + // When + var result = fileManager.getServiceLoader(location, Some.class); + + // Then + verify(repository).getContainerGroup(location); + verifyNoMoreInteractions(repository); + + verify(containerGroup).getServiceLoader(Some.class); + verifyNoMoreInteractions(containerGroup); + + assertThat(result).isSameAs(serviceLoader); + } + } + + @DisplayName(".handleOption(...) always returns false") + @RepeatedTest(10) + void handleOptionAlwaysReturnsFalse() { + // Given + var originalFlagIterator = someFlags().iterator(); + var flagIterator = spy(originalFlagIterator); + var flag = originalFlagIterator.next(); + + // When + var result = fileManager.handleOption(flag, flagIterator); + + // Then + assertThat(result).isFalse(); + verifyNoInteractions(flagIterator); + } + + @DisplayName(".hasLocation(...) delegates to the repository") + @ValueSource(booleans = {true, false}) + @ParameterizedTest(name = "for repository.hasLocation(...) = {0}") + void hasLocationDelegatesToTheRepository(boolean hasLocation) { + // Given + var location = someLocation(); + when(repository.hasLocation(any())).thenReturn(hasLocation); + + // When + var result = fileManager.hasLocation(location); + + // Then + verify(repository).hasLocation(location); + verifyNoMoreInteractions(repository); + assertThat(result).isEqualTo(hasLocation); + } + + @DisplayName(".inferBinaryName(...) tests") + @Nested + class InferBinaryNameTest { + + @DisplayName(".inferBinaryName(...) throws an exception for module-oriented locations") + @EnumSource( + value = StandardLocation.class, + names = {"MODULE_SOURCE_PATH", "MODULE_PATH"} + ) + @ParameterizedTest(name = "for location StandardLocation.{0}") + void inferBinaryNameThrowsExceptionForModuleOrientedLocations(Location location) { + // Then + assertThatThrownBy(() -> fileManager.inferBinaryName(location, someJavaFileObject())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Location %s must be package-oriented", location.getName()); + + verifyNoInteractions(repository); + } + + @DisplayName(".inferBinaryName(...) returns null for non-PathFileObject objects") + @Test + void inferBinaryNameReturnsNullForNonPathFileObjects() { + // Given + var location = StandardLocation.SOURCE_PATH; + JavaFileObject fileObject = mock(); + + // When + var result = fileManager.inferBinaryName(location, fileObject); + + // Then + assertThat(result).isNull(); + verifyNoInteractions(repository); + } + + @DisplayName(".inferBinaryName(...) returns null if the location does not exist") + @Test + void inferBinaryNameReturnsNullIfTheLocationDoesNotExist() { + // Given + var location = StandardLocation.SOURCE_PATH; + PathFileObject fileObject = mock(); + + when(repository.getPackageOrientedContainerGroup(any())) + .thenReturn(null); + + // When + var result = fileManager.inferBinaryName(location, fileObject); + + // Then + assertThat(result).isNull(); + verify(repository).getPackageOrientedContainerGroup(location); + verifyNoMoreInteractions(repository); + } + + @DisplayName(".inferBinaryName(...) infers the binary name from the group") + @Test + void inferBinaryNameInfersTheBinaryNameFromTheGroup() { + // Given + var location = StandardLocation.SOURCE_PATH; + PathFileObject fileObject = mock(); + PackageContainerGroup containerGroup = mock(); + var binaryName = someBinaryName(); + + when(repository.getPackageOrientedContainerGroup(any())) + .thenReturn(containerGroup); + when(containerGroup.inferBinaryName(any())) + .thenReturn(binaryName); + + // When + var result = fileManager.inferBinaryName(location, fileObject); + + // Then + assertThat(result).isEqualTo(binaryName); + + verify(repository).getPackageOrientedContainerGroup(location); + verifyNoMoreInteractions(repository); + + verify(containerGroup).inferBinaryName(fileObject); + verifyNoMoreInteractions(containerGroup); + } + } + + @DisplayName(".inferModuleName(...) tests") + @Nested + class InferModuleNameTest { + + @DisplayName( + ".inferModuleName(...) throws an exception if the location is not package oriented" + ) + @EnumSource( + value = StandardLocation.class, + names = {"MODULE_SOURCE_PATH", "MODULE_PATH"} + ) + @ParameterizedTest(name = "for location StandardLocation.{0}") + void inferModuleNameThrowsAnExceptionIfLocationIsNotPackageOriented(Location location) { + // Then + assertThatThrownBy(() -> fileManager.inferModuleName(location)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Location %s must be package-oriented", location.getName()); + } + + @DisplayName(".inferModuleName(...) returns null for non-module locations") + @Test + void inferModuleNameReturnsNullForNonModuleLocations() { + // Given + var location = StandardLocation.SOURCE_PATH; + + // When + var result = fileManager.inferModuleName(location); + + // Then + assertThat(result).isNull(); + } + + @DisplayName(".inferModuleName(...) returns the module name for module locations") + @Test + void inferModuleNameReturnsModuleNameForModuleLocations() { + // Given + var parentLocation = StandardLocation.MODULE_SOURCE_PATH; + var moduleName = someModuleName(); + var moduleLocation = new ModuleLocation(parentLocation, moduleName); + + // When + var result = fileManager.inferModuleName(moduleLocation); + + // Then + assertThat(result).isEqualTo(moduleName); + } + } + + @DisplayName(".isSameFile(...) tests") + @Nested + class IsSameFileTest { + + @DisplayName(".isSameFile(...) returns false if the first argument is null") + @Test + void isSameFileReturnsFalseIfFirstArgumentIsNull() { + // Then + assertThat(fileManager.isSameFile(null, mock())).isFalse(); + } + + @DisplayName(".isSameFile(...) returns false if the second argument is null") + @Test + void isSameFileReturnsFalseIfSecondArgumentIsNull() { + // Then + assertThat(fileManager.isSameFile(mock(), null)).isFalse(); + } + + @DisplayName(".isSameFile(...) returns false if both arguments are null") + @Test + void isSameFileReturnsFalseIfBothArgumentsAreNull() { + // Then + assertThat(fileManager.isSameFile(null, null)).isFalse(); + } + + @DisplayName(".isSameFile(...) returns false if both files have different URIs") + @Test + void isSameFileReturnsFalseIfBothFilesHaveDifferentUris() { + // Given + FileObject first = mock(); + FileObject second = mock(); + + URI firstUri = someAbsolutePath().toUri(); + URI secondUri = someAbsolutePath().toUri(); + + when(first.toUri()).thenReturn(firstUri); + when(second.toUri()).thenReturn(secondUri); + + // Then + assertThat(fileManager.isSameFile(first, second)).isFalse(); + } + + @DisplayName(".isSameFile(...) returns true if both files have the same URI") + @Test + void isSameFileReturnsTrueIfBothFilesHaveTheSameUri() { + // Given + FileObject first = mock(); + FileObject second = mock(); + + URI uri = someAbsolutePath().toUri(); + + when(first.toUri()).thenReturn(uri); + when(second.toUri()).thenReturn(uri); + + // Then + assertThat(fileManager.isSameFile(first, second)).isTrue(); + } + } + + @DisplayName(".isSupportedOption(...) always returns -1") + @RepeatedTest(10) + void isSupportedOptionAlwaysReturnsFalse() { + // Given + var flag = someFlags().iterator().next(); + + // Then + assertThat(fileManager.isSupportedOption(flag)).isEqualTo(-1); + } + + @DisplayName(".list(...) tests") + @Nested + class ListTests { + + @DisplayName(".list(...) throws an exception if the location is not package oriented") + @EnumSource( + value = StandardLocation.class, + names = {"MODULE_SOURCE_PATH", "MODULE_PATH"} + ) + @ParameterizedTest(name = "for location StandardLocation.{0}") + void listThrowsAnExceptionIfLocationIsNotPackageOriented(Location location) { + // Given + var packageName = somePackageName(); + var kinds = Stream.generate(() -> oneOf(Kind.class)) + .limit(someInt(1, 4)) + .collect(Collectors.toSet()); + var recurse = someBoolean(); + + // Then + assertThatThrownBy(() -> fileManager.list(location, packageName, kinds, recurse)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Location %s must be package-oriented", location.getName()); + } + + @DisplayName(".list(...) returns an empty set if the location does not exist") + @Test + void listReturnsEmptySetIfLocationDoesNotExist() throws IOException { + // Given + var location = StandardLocation.SOURCE_PATH; + var packageName = somePackageName(); + var kinds = Stream.generate(() -> oneOf(Kind.class)) + .limit(someInt(1, 4)) + .collect(Collectors.toSet()); + var recurse = someBoolean(); + + when(repository.getPackageOrientedContainerGroup(any())) + .thenReturn(null); + + // When + var result = fileManager.list(location, packageName, kinds, recurse); + + // Then + assertThat(result) + .isInstanceOf(Set.class) + .isEmpty(); + + verify(repository).getPackageOrientedContainerGroup(location); + verifyNoMoreInteractions(repository); + } + + @DisplayName(".list(...) returns the file listing for the location") + @Test + void listReturnsFileListingForTheLocation() throws IOException { + // Given + var location = StandardLocation.SOURCE_PATH; + var packageName = somePackageName(); + var kinds = Stream.generate(() -> oneOf(Kind.class)) + .limit(someInt(1, 4)) + .collect(Collectors.toSet()); + + var recurse = someBoolean(); + PackageContainerGroup containerGroup = mock(); + var files = Set.of(someJavaFileObject(), someJavaFileObject(), someJavaFileObject()); + + when(repository.getPackageOrientedContainerGroup(any())) + .thenReturn(containerGroup); + when(containerGroup.listFileObjects(any(), any(), anyBoolean())) + .thenReturn(files); + + // When + var result = fileManager.list(location, packageName, kinds, recurse); + + // Then + assertThat(result) + .isInstanceOf(Set.class) + .containsExactlyInAnyOrderElementsOf(files); + + verify(repository).getPackageOrientedContainerGroup(location); + verifyNoMoreInteractions(repository); + + verify(containerGroup).listFileObjects(packageName, kinds, recurse); + verifyNoMoreInteractions(containerGroup); + } + } + + @DisplayName(".listLocationsForModules(...) tests") + @Nested + class ListLocationsForModulesTest { + + @DisplayName( + ".listLocationsForModules(...) throws an exception if the location is package oriented" + ) + @EnumSource( + value = StandardLocation.class, + names = {"SOURCE_PATH", "CLASS_PATH"} + ) + @ParameterizedTest(name = "for location StandardLocation.{0}") + void listLocationsForModulesThrowsExceptionIfLocationIsPackageOriented(Location location) { + // Then + assertThatThrownBy(() -> fileManager.listLocationsForModules(location)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Location %s must be output or module-oriented", location.getName()); + } + + @DisplayName(".listLocationsForModules(...) returns the results in a single-element list") + @Test + void listLocationsForModulesReturnsTheResultsInSingleElementList() { + // Given + var location = StandardLocation.MODULE_SOURCE_PATH; + var expectedLocations = Stream.generate(() -> new ModuleLocation(location, someModuleName())) + .limit(someInt(1, 10)) + .map(Location.class::cast) + .collect(Collectors.toSet()); + + when(repository.listLocationsForModules(location)) + .thenReturn(expectedLocations); + + // When + var result = fileManager.listLocationsForModules(location); + + // Then + assertThat(result) + .singleElement(collection(Location.class)) + .containsExactlyInAnyOrderElementsOf(expectedLocations); + } + } + + @DisplayName(".toString() returns the expected value") + @Test + void toStringReturnsExpectedValue() { + // Then + assertThat(fileManager.toString()) + .isEqualTo("JctFileManagerImpl{repository=%s}", repository); } }