diff --git a/.mvn/checkstyle/checkstyle.xml b/.mvn/checkstyle/checkstyle.xml index cd8faf615..040119a08 100644 --- a/.mvn/checkstyle/checkstyle.xml +++ b/.mvn/checkstyle/checkstyle.xml @@ -136,7 +136,7 @@ - + diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/AbstractJctCompiler.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/AbstractJctCompiler.java index 55684eeb0..4f78d6c59 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/AbstractJctCompiler.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/AbstractJctCompiler.java @@ -18,17 +18,22 @@ import static io.github.ascopes.jct.utils.IterableUtils.requireNonNullValues; import static java.util.Objects.requireNonNull; +import io.github.ascopes.jct.compilers.impl.JctCompilationFactoryImpl; import io.github.ascopes.jct.compilers.impl.JctCompilationImpl; -import io.github.ascopes.jct.compilers.impl.JctJsr199Interop; +import io.github.ascopes.jct.diagnostics.TracingDiagnosticListener; +import io.github.ascopes.jct.ex.JctCompilerException; import io.github.ascopes.jct.filemanagers.AnnotationProcessorDiscovery; +import io.github.ascopes.jct.filemanagers.JctFileManagerFactory; import io.github.ascopes.jct.filemanagers.LoggingMode; import io.github.ascopes.jct.workspaces.Workspace; +import java.io.IOException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Locale; import javax.annotation.Nullable; +import javax.annotation.WillNotClose; import javax.annotation.concurrent.NotThreadSafe; import javax.annotation.processing.Processor; import org.apiguardian.api.API; @@ -55,7 +60,7 @@ @API(since = "0.0.1", status = Status.STABLE) @NotThreadSafe public abstract class AbstractJctCompiler> - implements JctCompiler { + implements JctCompiler { private final List annotationProcessors; private final List annotationProcessorOptions; @@ -113,19 +118,13 @@ protected AbstractJctCompiler(String defaultName) { } @Override - public JctCompilationImpl compile(Workspace workspace) { - var flagBuilder = getJctFlagBuilderFactory().createFlagBuilder(); - var compiler = getJsr199CompilerFactory().createCompiler(); - - return JctJsr199Interop.compile(workspace, myself(), compiler, flagBuilder, null); + public JctCompilation compile(Workspace workspace) { + return compileInternal(workspace, null); } @Override - public JctCompilationImpl compile(Workspace workspace, Collection classNames) { - var flagBuilder = getJctFlagBuilderFactory().createFlagBuilder(); - var compiler = getJsr199CompilerFactory().createCompiler(); - - return JctJsr199Interop.compile(workspace, myself(), compiler, flagBuilder, classNames); + public JctCompilation compile(Workspace workspace, Collection classNames) { + return compileInternal(workspace, classNames); } @Override @@ -249,6 +248,19 @@ public A addCompilerOptions(Iterable compilerOptions) { return myself(); } + @Override + public String getEffectiveRelease() { + if (release != null) { + return release; + } + + if (target != null) { + return target; + } + + return getDefaultRelease(); + } + @Nullable @Override public String getRelease() { @@ -432,14 +444,37 @@ public final String toString() { * * @return the factory. */ - public abstract JctFlagBuilderFactory getJctFlagBuilderFactory(); + public abstract JctFlagBuilderFactory getFlagBuilderFactory(); /** * Get the JSR-199 compiler factory to use for initialising an internal compiler. * * @return the factory. */ - public abstract Jsr199CompilerFactory getJsr199CompilerFactory(); + public abstract Jsr199CompilerFactory getCompilerFactory(); + + /** + * Get the file manager factory to use for building a file manager during compilation. + * + * @return the factory. + */ + public abstract JctFileManagerFactory getFileManagerFactory(); + + /** + * Get the compilation factory to use for building a compilation. + * + *

By default, this uses a common internal implementation that is designed to work with + * compilers that have interfaces the same as, and behave the same as Javac. + * + *

Some obscure compiler implementations with potentially satanic rituals for initialising + * and configuring components correctly may need to provide a custom implementation here instead. + * In this case, this method should be overridden. + * + * @return the compilation factory. + */ + public JctCompilationFactory getCompilationFactory() { + return new JctCompilationFactoryImpl(this); + } /** * {@inheritDoc} @@ -460,4 +495,47 @@ protected final A myself() { return me; } + + /** + * Build the list of flags from this compiler object using the flag builder. + * + *

Implementations should not need to override this unless there is a special edge case + * that needs configuring differently. This is exposed to assist in these kinds of cases. + * + * @param flagBuilder the flag builder to apply the flag configuration to. + * @return the string flags to use. + */ + protected List buildFlags(JctFlagBuilder flagBuilder) { + return flagBuilder + .annotationProcessorOptions(getAnnotationProcessorOptions()) + .showDeprecationWarnings(isShowDeprecationWarnings()) + .failOnWarnings(isFailOnWarnings()) + .compilerOptions(getCompilerOptions()) + .previewFeatures(isPreviewFeatures()) + .release(getRelease()) + .source(getSource()) + .target(getTarget()) + .verbose(isVerbose()) + .showWarnings(isShowWarnings()) + .build(); + } + + @SuppressWarnings("NullableProblems") // https://youtrack.jetbrains.com/issue/IDEA-311124 + private JctCompilation compileInternal( + @WillNotClose Workspace workspace, + @Nullable Collection classNames + ) { + var fileManagerFactory = getFileManagerFactory(); + var flagBuilderFactory = getFlagBuilderFactory(); + var compilerFactory = getCompilerFactory(); + var compilationFactory = getCompilationFactory(); + + try (var fileManager = fileManagerFactory.createFileManager(workspace)) { + var flags = buildFlags(flagBuilderFactory.createFlagBuilder()); + var compiler = compilerFactory.createCompiler(); + return compilationFactory.createCompilation(flags, fileManager, compiler, classNames); + } catch (IOException ex) { + throw new JctCompilerException("Failed to close file manager", ex); + } + } } diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/JctCompilationFactory.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/JctCompilationFactory.java new file mode 100644 index 000000000..70ef77aeb --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/JctCompilationFactory.java @@ -0,0 +1,55 @@ +/* + * 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.compilers; + +import io.github.ascopes.jct.ex.JctCompilerException; +import io.github.ascopes.jct.filemanagers.JctFileManager; +import java.util.Collection; +import java.util.List; +import javax.annotation.Nullable; +import javax.tools.JavaCompiler; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * Factory for producing {@link JctCompilation} objects by performing a physical compilation with a + * compiler. + * + * @author Ashley Scopes + * @since 0.0.1 (0.0.1-M7) + */ +@API(since = "0.0.1", status = Status.STABLE) +public interface JctCompilationFactory { + + /** + * Create a compilation. + * + * @param flags the flags to pass to the compiler. + * @param fileManager the file manager to use for file management. + * @param jsr199Compiler the compiler backend to use. + * @param classNames the binary names of the classes to compile. If this is null, then classes + * should be discovered automatically. + * @return the compilation result that contains whether the compiler succeeded or failed, amongst + * other information. + * @throws JctCompilerException if compiler raises an unhandled exception and cannot complete. + */ + JctCompilation createCompilation( + List flags, + JctFileManager fileManager, + JavaCompiler jsr199Compiler, + @Nullable Collection classNames + ); +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/JctCompiler.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/JctCompiler.java index 2862fdba6..5091a29de 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/JctCompiler.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/JctCompiler.java @@ -460,6 +460,17 @@ default C addCompilerOptions(String compilerOption, String... compilerOptions) { */ String getDefaultRelease(); + /** + * Get the effective release to use for the actual compilation. + * + *

This may be determined from the {@link #getSource() source}, + * {@link #getTarget() target}, {@link #getRelease() release}, and + * {@link #getDefaultRelease() default release.} + * + * @return the effective release. + */ + String getEffectiveRelease(); + /** * Get the current release version that is set, or {@code null} if left to the compiler to decide. * default. diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/impl/JctCompilationFactoryImpl.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/impl/JctCompilationFactoryImpl.java new file mode 100644 index 000000000..3aa41e082 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/impl/JctCompilationFactoryImpl.java @@ -0,0 +1,160 @@ +/* + * 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.compilers.impl; + +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.toList; + +import io.github.ascopes.jct.compilers.JctCompilation; +import io.github.ascopes.jct.compilers.JctCompilationFactory; +import io.github.ascopes.jct.compilers.JctCompiler; +import io.github.ascopes.jct.diagnostics.TeeWriter; +import io.github.ascopes.jct.diagnostics.TracingDiagnosticListener; +import io.github.ascopes.jct.ex.JctCompilerException; +import io.github.ascopes.jct.filemanagers.JctFileManager; +import io.github.ascopes.jct.filemanagers.LoggingMode; +import io.github.ascopes.jct.utils.IterableUtils; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import javax.annotation.Nullable; +import javax.annotation.WillNotClose; +import javax.annotation.concurrent.Immutable; +import javax.annotation.concurrent.ThreadSafe; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.JavaFileObject.Kind; +import javax.tools.StandardLocation; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Default implementation of a compilation factory that performs the actual compilation of user + * provided sources and configurations from the JCT API descriptors. + * + * @author Ashley Scopes + * @since 0.0.1 (0.0.1-M7) + */ +@API(since = "0.0.1", status = Status.INTERNAL) +@Immutable +@ThreadSafe +public final class JctCompilationFactoryImpl implements JctCompilationFactory { + private static final Logger LOGGER = LoggerFactory.getLogger(JctCompilationFactoryImpl.class); + + private final JctCompiler compiler; + + public JctCompilationFactoryImpl(JctCompiler compiler) { + this.compiler = compiler; + } + + @Override + public JctCompilation createCompilation( + List flags, + JctFileManager fileManager, + JavaCompiler jsr199Compiler, + @Nullable Collection classNames + ) { + try { + return createCheckedCompilation(flags, fileManager, jsr199Compiler, classNames); + } catch (Exception ex) { + throw new JctCompilerException( + "Failed to perform compilation, an unexpected exception was raised", ex + ); + } + } + + private JctCompilation createCheckedCompilation( + List flags, + JctFileManager fileManager, + JavaCompiler jsr199Compiler, + @Nullable Collection classNames + ) throws Exception { + var compilationUnits = findCompilationUnits(fileManager); + + // Do not close stdout, it breaks test engines, especially IntellIJ. + @WillNotClose + var writer = new TeeWriter(new OutputStreamWriter(System.out, compiler.getLogCharset())); + + var diagnosticListener = new TracingDiagnosticListener<>( + compiler.getDiagnosticLoggingMode() != LoggingMode.DISABLED, + compiler.getDiagnosticLoggingMode() == LoggingMode.STACKTRACES + ); + + var task = jsr199Compiler.getTask( + writer, + fileManager, + diagnosticListener, + flags, + classNames, + compilationUnits + ); + + var processors = compiler.getAnnotationProcessors(); + if (!processors.isEmpty()) { + task.setProcessors(processors); + } + + LOGGER.info("Starting compilation"); + + var start = System.nanoTime(); + var success = requireNonNull( + task.call(), "Compiler task .call() method returned null unexpectedly!" + ); + var delta = (System.nanoTime() - start) / 1_000_000L; + + LOGGER + .atInfo() + .setMessage("Compilation {} after approximately {}ms") + .addArgument(() -> success ? "completed successfully" : "failed") + .addArgument(delta) + .log(); + + return JctCompilationImpl + .builder() + .compilationUnits(compilationUnits) + .fileManager(fileManager) + .outputLines(writer.toString().lines().collect(toList())) + .diagnostics(diagnosticListener.getDiagnostics()) + .success(success) + .failOnWarnings(compiler.isFailOnWarnings()) + .build(); + } + + private Set findCompilationUnits(JctFileManager fileManager) throws IOException { + var modules = IterableUtils + .flatten(fileManager.listLocationsForModules(StandardLocation.MODULE_SOURCE_PATH)); + + var locations = modules.isEmpty() + ? Set.of(StandardLocation.SOURCE_PATH) + : modules; + + var objects = new LinkedHashSet(); + + for (var location : locations) { + var items = fileManager.list(location, "", Set.of(Kind.SOURCE), true); + for (var fileObject : items) { + objects.add(fileObject); + } + } + + return objects; + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/impl/JctJsr199Interop.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/impl/JctJsr199Interop.java deleted file mode 100644 index 4efe2c339..000000000 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/impl/JctJsr199Interop.java +++ /dev/null @@ -1,634 +0,0 @@ -/* - * 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.compilers.impl; - -import io.github.ascopes.jct.compilers.CompilationMode; -import io.github.ascopes.jct.compilers.JctCompiler; -import io.github.ascopes.jct.compilers.JctFlagBuilder; -import io.github.ascopes.jct.diagnostics.TeeWriter; -import io.github.ascopes.jct.diagnostics.TracingDiagnosticListener; -import io.github.ascopes.jct.ex.JctCompilerException; -import io.github.ascopes.jct.ex.JctException; -import io.github.ascopes.jct.filemanagers.JctFileManager; -import io.github.ascopes.jct.filemanagers.LoggingFileManagerProxy; -import io.github.ascopes.jct.filemanagers.LoggingMode; -import io.github.ascopes.jct.filemanagers.impl.JctFileManagerImpl; -import io.github.ascopes.jct.utils.IterableUtils; -import io.github.ascopes.jct.utils.SpecialLocationUtils; -import io.github.ascopes.jct.utils.StringUtils; -import io.github.ascopes.jct.utils.UtilityClass; -import io.github.ascopes.jct.workspaces.Workspace; -import io.github.ascopes.jct.workspaces.impl.WrappingDirectoryImpl; -import java.io.IOException; -import java.lang.module.FindException; -import java.lang.module.ModuleFinder; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; -import javax.annotation.Nullable; -import javax.annotation.concurrent.Immutable; -import javax.annotation.concurrent.ThreadSafe; -import javax.tools.JavaCompiler; -import javax.tools.JavaCompiler.CompilationTask; -import javax.tools.JavaFileManager; -import javax.tools.JavaFileManager.Location; -import javax.tools.JavaFileObject; -import javax.tools.JavaFileObject.Kind; -import javax.tools.StandardLocation; -import org.apiguardian.api.API; -import org.apiguardian.api.API.Status; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Helper for performing the actual compilation logic during a compilation run. - * - *

This class currently contains the majority of the procedural logic needed to configure - * a JSR-199 compiler and trigger the compilation, given the components in the JCT framework. - * - *

While I have considered other implementation models, such as an interceptor-chain pattern, - * or factories, this has turned out to be the simplest, albeit least Java-y way to achieve what I - * want while keeping stuff easily testable and easy to debug. - * - *

If this ends up getting more complex in the future, then I may reconsider how this is - * implemented (this may need to change for ECJ support in the future possibly, not sure yet). - * - *

You should not need to call most methods outside this class other than {@link #compile}. - * The methods are exposed to simplify testing. - * - * @author Ashley Scopes - * @since 0.0.1 - */ -@API(since = "0.0.1", status = Status.INTERNAL) -@Immutable -@ThreadSafe -public final class JctJsr199Interop extends UtilityClass { - - private static final Logger LOGGER = LoggerFactory.getLogger(JctJsr199Interop.class); - - // Locations to duplicate paths for when using annotation processor path discovery with - // inheritance enabled. - // Mapping of source location to target location. - private static final Map INHERITED_AP_PATHS = Map.of( - // https://stackoverflow.com/q/53084037 - // Seems that javac will always use the classpath to implement this behaviour, and never - // the module path. Let's keep this simple and mimic this behaviour. If someone complains - // about it being problematic in the future, then I am open to change how this works to - // keep it sensible. - // (from -> to) - StandardLocation.CLASS_PATH, StandardLocation.ANNOTATION_PROCESSOR_PATH - ); - - // Locations that we have to ensure exist before the compiler is run. - private static final Set REQUIRED_LOCATIONS = Set.of( - // We have to manually create this one as javac will not attempt to access it lazily. Instead, - // it will just abort if it is not present. This means we cannot take advantage of the - // container group creating the roots as we try to access them for this specific case. - StandardLocation.SOURCE_OUTPUT, - // Annotation processors that create files will need this directory to exist if it is to - // work properly. - StandardLocation.CLASS_OUTPUT, - // We need to provide a header output path in case header generation is enabled at any stage. - // I might make this disabled by default in the future if there is too much overhead from - // doing this by default. - StandardLocation.NATIVE_HEADER_OUTPUT - ); - - private JctJsr199Interop() { - // Static-only class. - } - - /** - * Initialise a new instance of this compilation factory internally and run the compilation. - * - * @param workspace the workspace to use. - * @param compiler the compiler to use. - * @param jsr199Compiler the JSR-199 compiler to use. - * @param flagBuilder the flag builder to use. - * @param classNames the class names to compile, or {@code null} to automatically detect all - * classes. - * @return the compilation factory. - */ - @SuppressWarnings("NullableProblems") // https://youtrack.jetbrains.com/issue/IDEA-311124 - public static JctCompilationImpl compile( - Workspace workspace, - JctCompiler compiler, - JavaCompiler jsr199Compiler, - JctFlagBuilder flagBuilder, - @Nullable Collection classNames - ) { - // This method sucks, I hate it. If there is a nicer way of doing this without a load of - // additional overhead, additional code, or additional complexity either in this class or the - // unit tests, then I am all for ripping all of this out and reimplementing it in the future. - - try (var fileManager = buildFileManager(compiler, workspace)) { - // DO NOT CLOSE THE WRITER, IT IS ATTACHED TO SYSTEM.OUT. - // Closing SYSTEM.OUT causes IntelliJ to abort the entire test runner. - // See https://youtrack.jetbrains.com/issue/IDEA-120628 - // Other platforms may see other weird behaviour if we do this (Surefire, for example). - var writer = buildWriter(compiler); - - var flags = buildFlags(compiler, flagBuilder); - var diagnosticListener = buildDiagnosticListener(compiler); - var compilationUnits = findCompilationUnits(fileManager); - - var result = performCompilerPass( - compiler, - jsr199Compiler, - writer, - flags, - fileManager, - diagnosticListener, - compilationUnits, - classNames - ); - - var outputLines = writer.toString().lines().collect(Collectors.toList()); - - return JctCompilationImpl.builder() - .failOnWarnings(compiler.isFailOnWarnings()) - .success(result) - .outputLines(outputLines) - .compilationUnits(Set.copyOf(compilationUnits)) - .diagnostics(diagnosticListener.getDiagnostics()) - .fileManager(fileManager) - .build(); - } catch (Exception ex) { - throw new JctCompilerException("Failed to compile due to an error: " + ex, ex); - } - } - - /** - * Build a TeeWriter. - * - * @param compiler the compiler to use. - * @return the tee writer. - */ - public static TeeWriter buildWriter(JctCompiler compiler) { - return TeeWriter.wrap(compiler.getLogCharset(), System.out); - } - - /** - * Build the flags for the compiler. - * - * @param compiler the compiler. - * @param flagBuilder the flag builder to use. - * @return the flags. - */ - public static List buildFlags(JctCompiler compiler, JctFlagBuilder flagBuilder) { - return flagBuilder - .annotationProcessorOptions(compiler.getAnnotationProcessorOptions()) - .showDeprecationWarnings(compiler.isShowDeprecationWarnings()) - .failOnWarnings(compiler.isFailOnWarnings()) - .compilerOptions(compiler.getCompilerOptions()) - .previewFeatures(compiler.isPreviewFeatures()) - .release(compiler.getRelease()) - .source(compiler.getSource()) - .target(compiler.getTarget()) - .verbose(compiler.isVerbose()) - .showWarnings(compiler.isShowWarnings()) - .build(); - } - - /** - * Build a file manager from the compiler and workspace. - * - *

This also applies any logging proxy that is required. - * - * @param compiler the compiler to use. - * @param workspace the workspace to use. - * @return the file manager. - */ - public static JctFileManager buildFileManager( - JctCompiler compiler, - Workspace workspace - ) { - var release = determineRelease(compiler); - var fileManager = JctFileManagerImpl.forRelease(release); - - configureWorkspacePaths(workspace, fileManager); - configureClassPath(compiler, fileManager); - configureModulePath(compiler, fileManager); - configurePlatformClassPath(compiler, fileManager); - configureJvmSystemModules(compiler, fileManager); - configureAnnotationProcessorPaths(compiler, fileManager); - configureRequiredLocations(workspace, fileManager); - - switch (compiler.getFileManagerLoggingMode()) { - case STACKTRACES: - return LoggingFileManagerProxy.wrap(fileManager, true); - case ENABLED: - return LoggingFileManagerProxy.wrap(fileManager, false); - case DISABLED: - default: - return fileManager; - } - } - - /** - * Determine the effective release to run the compiler under. - * - * @param compiler the compiler to determine the release from. - * @return the release. - */ - public static String determineRelease(JctCompiler compiler) { - if (compiler.getRelease() != null) { - LOGGER.debug("Using explicitly set release as the base release version internally"); - return compiler.getRelease(); - } - - if (compiler.getTarget() != null) { - LOGGER.debug("Using explicitly set target as the base release version internally"); - return compiler.getTarget(); - } - - LOGGER.debug("Using compiler default release as the base release version internally"); - return compiler.getDefaultRelease(); - } - - /** - * Configure all workspace paths into the file manager. - * - * @param workspace the workspace. - * @param fileManager the file manager. - */ - public static void configureWorkspacePaths(Workspace workspace, JctFileManagerImpl fileManager) { - // Copy all other explicit locations across first to give them priority. - workspace.getAllPaths().forEach(fileManager::addPaths); - } - - /** - * Configure the classpath for the compiler in the file manager. - * - * @param compiler the compiler to use. - * @param fileManager the file manager to use. - */ - public static void configureClassPath( - JctCompiler compiler, - JctFileManagerImpl fileManager - ) { - if (compiler.isInheritClassPath()) { - for (var path : SpecialLocationUtils.currentClassPathLocations()) { - var wrapper = new WrappingDirectoryImpl(path); - - LOGGER.trace("Adding {} to the class path", path); - fileManager.addPath(StandardLocation.CLASS_PATH, wrapper); - - // IntelliJ appears to place modules on the classpath if we are not building the base - // project with JPMS. This is a problem because it means we cannot compile a module - // within a test pack not using JPMS, since the modules will be on the classpath rather - // than the module path. Fix this by adding classpath components with modules inside into - // the module path as well. - if (compiler.isFixJvmModulePathMismatch() && containsModules(path)) { - LOGGER.trace("Adding {} to the module path as well since it contains modules", path); - fileManager.addPath(StandardLocation.MODULE_PATH, wrapper); - } - } - } - } - - /** - * Determine if the given path root contains modules. - * - * @param path the path to check - * @return {@code true} if modules are found, or {@code false} otherwise - */ - public static boolean containsModules(Path path) { - try { - return !ModuleFinder.of(path).findAll().isEmpty(); - } catch (FindException ex) { - // Ignore, this just means that an invalid file name was found. - LOGGER.trace("Ignoring exception finding modules in {}", path, ex); - return false; - } - } - - /** - * Configure the module path for the compiler in the file manager. - * - * @param compiler the compiler to use. - * @param fileManager the file manager to use. - */ - public static void configureModulePath( - JctCompiler compiler, - JctFileManagerImpl fileManager - ) { - if (compiler.isInheritModulePath()) { - for (var path : SpecialLocationUtils.currentModulePathLocations()) { - var wrapper = new WrappingDirectoryImpl(path); - - LOGGER.trace("Adding {} to the module path and class path", path); - - // Since we do not know if the code being compiled will use modules or not just yet, - // make sure any modules are on the class path as well so that they remain accessible - // in unnamed modules. - fileManager.addPath(StandardLocation.CLASS_PATH, wrapper); - fileManager.addPath(StandardLocation.MODULE_PATH, wrapper); - } - } - } - - /** - * Configure the platform classpath for the compiler in the file manager. - * - * @param compiler the compiler to use. - * @param fileManager the file manager to use. - */ - public static void configurePlatformClassPath( - JctCompiler compiler, - JctFileManagerImpl fileManager - ) { - if (compiler.isInheritPlatformClassPath()) { - for (var path : SpecialLocationUtils.currentPlatformClassPathLocations()) { - var wrapper = new WrappingDirectoryImpl(path); - - LOGGER.trace("Adding {} to the platform class path", path); - fileManager.addPath(StandardLocation.PLATFORM_CLASS_PATH, wrapper); - } - } - } - - /** - * Configure the JVM system modules for the compiler in the file manager. - * - * @param compiler the compiler to use. - * @param fileManager the file manager to use. - */ - public static void configureJvmSystemModules( - JctCompiler compiler, - JctFileManagerImpl fileManager - ) { - if (compiler.isInheritSystemModulePath()) { - for (var path : SpecialLocationUtils.javaRuntimeLocations()) { - var wrapper = new WrappingDirectoryImpl(path); - - LOGGER.trace("Adding {} to the system module path", path); - fileManager.addPath(StandardLocation.SYSTEM_MODULES, wrapper); - } - } - } - - /** - * Configure the annotation processor paths for the compiler in the file manager. - * - * @param compiler the compiler to use. - * @param fileManager the file manager to use. - */ - public static void configureAnnotationProcessorPaths( - JctCompiler compiler, - JctFileManagerImpl fileManager - ) { - if (compiler.getCompilationMode() == CompilationMode.COMPILATION_ONLY) { - LOGGER.debug( - "Not configuring annotation processor paths as annotation processing is disabled " - + "by this compiler mode" - ); - - return; - } - - switch (compiler.getAnnotationProcessorDiscovery()) { - case INCLUDE_DEPENDENCIES: - LOGGER.debug("Copying classpath dependencies into the annotation processor path"); - INHERITED_AP_PATHS.forEach(fileManager::copyContainers); - fileManager.ensureEmptyLocationExists(StandardLocation.ANNOTATION_PROCESSOR_PATH); - break; - - case ENABLED: - LOGGER.debug("Annotation processor discovery is enabled, ensuring empty location exists"); - fileManager.ensureEmptyLocationExists(StandardLocation.ANNOTATION_PROCESSOR_PATH); - break; - - case DISABLED: - default: - LOGGER.debug("Not configuring annotation processor discovery"); - // There is nothing to do to the file manager to configure annotation processing at this - // time. - break; - } - } - - /** - * Configure the required locations in the workspace and add them to the file manager. - * - * @param workspace the workspace. - * @param fileManager the file manager. - */ - public static void configureRequiredLocations( - Workspace workspace, - JctFileManagerImpl fileManager - ) { - for (var location : REQUIRED_LOCATIONS) { - if (!fileManager.hasLocation(location)) { - LOGGER.debug("Creating a new package workspace for {}", location); - fileManager.addPath(location, workspace.createPackage(location)); - } - } - } - - /** - * Find any compilation units to use in a compilation. - * - * @param fileManager the file manager to search. - * @return the compilation units. - * @throws IOException if an IO error occurs. - */ - public static List findCompilationUnits( - JavaFileManager fileManager - ) throws IOException { - var objects = new ArrayList(); - - for (var location : findCompilationUnitLocations(fileManager)) { - var items = fileManager.list(location, "", Set.of(Kind.SOURCE), true); - for (var fileObject : items) { - objects.add(fileObject); - } - } - - return objects; - } - - /** - * Find interesting locations for compilation units. - * - *

If there are any modules in the module source path, these will be returned. - * If none are found, this will return the legacy source path instead. This is done to mimic the - * behaviour of Javac. - * - * @param fileManager the file manager to use. - * @return the list of locations. - * @throws IOException if an IO error occurs. - */ - public static List findCompilationUnitLocations( - JavaFileManager fileManager - ) throws IOException { - var modules = IterableUtils - .flatten(fileManager.listLocationsForModules(StandardLocation.MODULE_SOURCE_PATH)); - - return modules.isEmpty() - ? List.of(StandardLocation.SOURCE_PATH) - : modules; - } - - /** - * Build a tracing diagnostic listener for the compiler. - * - * @param compiler the compiler. - * @return the tracing diagnostic listener. - */ - public static TracingDiagnosticListener buildDiagnosticListener( - JctCompiler compiler - ) { - var logging = compiler.getDiagnosticLoggingMode(); - - return new TracingDiagnosticListener<>( - logging != LoggingMode.DISABLED, - logging == LoggingMode.STACKTRACES - ); - } - - /** - * Perform an individual compilation pass. - * - * @param compiler the compiler to use. - * @param jsr199Compiler the JSR-199 compiler to build a compilation task from. - * @param writer the tee writer to use. - * @param flags the compiler flags to pass. - * @param fileManager the file manager to use. - * @param diagnosticListener the tracing diagnostic listener to write diagnostics to. - * @param compilationUnits the compilation units to compile. - * @return {@code true} if compilation succeeded, or {@code false} if it failed. - */ - public static boolean performCompilerPass( - JctCompiler compiler, - JavaCompiler jsr199Compiler, - TeeWriter writer, - List flags, - JctFileManager fileManager, - TracingDiagnosticListener diagnosticListener, - List compilationUnits, - @Nullable Collection classNames - ) { - var name = compiler.toString(); - - var task = jsr199Compiler.getTask( - writer, - fileManager, - diagnosticListener, - flags, - classNames, - compilationUnits - ); - - task.setLocale(compiler.getLocale()); - configureAnnotationProcessorDiscovery(compiler, task); - - LOGGER - .atInfo() - .addArgument(compilationUnits::size) - .addArgument(() -> StringUtils.quoted(name)) - .addArgument(() -> StringUtils.quotedIterable(flags)) - .log("Starting compilation of {} file(s) with compiler {} using flags {}"); - - var start = System.nanoTime(); - - try { - var result = task.call(); - var duration = System.nanoTime() - start; - - if (result == null) { - throw new JctCompilerException( - "Compiler " + StringUtils.quoted(name) + " failed to produce a valid result, this is a " - + "bug in the compiler implementation, please report it to the compiler vendor!"); - } - - LOGGER - .atInfo() - .addArgument(() -> StringUtils.quoted(name)) - .addArgument(() -> result ? "succeeded" : "failed") - .addArgument(() -> StringUtils.formatNanos(duration)) - .log("Compilation with compiler {} {} after ~{}"); - - return result; - - } catch (JctException ex) { - throw ex; - } catch (Exception ex) { - var duration = System.nanoTime() - start; - - LOGGER - .atWarn() - .addArgument(() -> StringUtils.quoted(name)) - .addArgument(() -> StringUtils.formatNanos(duration)) - .addArgument(() -> ex.getClass().getName()) - .addArgument(() -> StringUtils.quoted(ex.getMessage())) - .log("Compilation with compiler {} threw an unhandled exception after ~{} -- {}: {}"); - - throw new JctCompilerException( - "Compiler " + StringUtils.quoted(name) + " raised an unhandled exception", ex - ); - } - } - - /** - * Configure annotation processor discovery on the given compilation task. - * - * @param compiler the compiler to use. - * @param task the compilation task to use. - */ - public static void configureAnnotationProcessorDiscovery( - JctCompiler compiler, - CompilationTask task - ) { - var processors = compiler.getAnnotationProcessors(); - var discovery = compiler.getAnnotationProcessorDiscovery(); - - if (compiler.getCompilationMode() == CompilationMode.COMPILATION_ONLY) { - LOGGER.debug( - "Not configuring annotation processor discovery as annotation processing is disabled " - + "by this compiler mode" - ); - return; - } - - if (!processors.isEmpty()) { - LOGGER.debug("Annotation processor discovery is disabled (processors explicitly provided)"); - task.setProcessors(processors); - return; - } - - switch (discovery) { - case INCLUDE_DEPENDENCIES: - LOGGER.debug("Annotation processor discovery will scan the source paths and dependencies"); - break; - - case ENABLED: - LOGGER.debug("Annotation processor discovery will scan the source paths"); - break; - - case DISABLED: - default: - LOGGER.debug("Annotation processor discovery will be disabled"); - // Set an empty list to avoid discovery being performed. - task.setProcessors(List.of()); - break; - } - } -} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/javac/JavacJctCompilerImpl.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/javac/JavacJctCompilerImpl.java index 96eb53825..e138b369a 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/javac/JavacJctCompilerImpl.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/javac/JavacJctCompilerImpl.java @@ -20,6 +20,8 @@ import io.github.ascopes.jct.compilers.AbstractJctCompiler; import io.github.ascopes.jct.compilers.JctFlagBuilderFactory; import io.github.ascopes.jct.compilers.Jsr199CompilerFactory; +import io.github.ascopes.jct.filemanagers.JctFileManagerFactory; +import io.github.ascopes.jct.filemanagers.impl.JctFileManagerFactoryImpl; import javax.annotation.concurrent.NotThreadSafe; import javax.lang.model.SourceVersion; import javax.tools.ToolProvider; @@ -51,17 +53,22 @@ public String getDefaultRelease() { } @Override - public JctFlagBuilderFactory getJctFlagBuilderFactory() { + public JctFlagBuilderFactory getFlagBuilderFactory() { return JavacJctFlagBuilderImpl::new; } @Override - public Jsr199CompilerFactory getJsr199CompilerFactory() { + public Jsr199CompilerFactory getCompilerFactory() { // RequireNonNull to ensure the return result is non-null, since the ToolProvider // method is not annotated. return () -> requireNonNull(ToolProvider.getSystemJavaCompiler()); } + @Override + public JctFileManagerFactory getFileManagerFactory() { + return new JctFileManagerFactoryImpl(this); + } + /** * Get the minimum version of Javac that is supported. * diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/diagnostics/TeeWriter.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/diagnostics/TeeWriter.java index 893dd22f6..429c42949 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/diagnostics/TeeWriter.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/diagnostics/TeeWriter.java @@ -50,7 +50,7 @@ public final class TeeWriter extends Writer { // and the delegated output writer at the same time. private final StringBuilder builder; - private TeeWriter(@WillCloseWhenClosed Writer writer) { + public TeeWriter(@WillCloseWhenClosed Writer writer) { lock = new Object(); closed = false; @@ -105,36 +105,4 @@ private void ensureOpen() { throw new IllegalStateException("TeeWriter is closed"); } } - - /** - * Initialize this writer by wrapping an output stream in an internally-held writer. - * - *

Note that this will not buffer the output stream itself. That is up to you to do. - * - * @param charset the charset to write with. - * @param outputStream the output stream to delegate to. - * @return the tee writer. - */ - public static TeeWriter wrap( - Charset charset, - @WillCloseWhenClosed OutputStream outputStream - ) { - var writer = new OutputStreamWriter( - requireNonNull(outputStream, "outputStream"), - requireNonNull(charset, "charset") - ); - return wrap(writer); - } - - /** - * Initialize this writer by wrapping an output stream in an internally-held writer. - * - *

Note that this will not buffer the output stream itself. That is up to you to do. - * - * @param writer the writer to wrap. - * @return the tee writer. - */ - public static TeeWriter wrap(@WillCloseWhenClosed Writer writer) { - return new TeeWriter(writer); - } } diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/JctFileManagerFactory.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/JctFileManagerFactory.java new file mode 100644 index 000000000..aa36db8a3 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/JctFileManagerFactory.java @@ -0,0 +1,41 @@ +/* + * 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.filemanagers; + +import io.github.ascopes.jct.workspaces.Workspace; +import javax.annotation.WillNotClose; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * Factory interface for building a file manager object. + * + * @author Ashley Scopes + * @since 0.0.1 (0.0.1-M7) + */ +@API(since = "0.0.1", status = Status.STABLE) +@FunctionalInterface +public interface JctFileManagerFactory { + + /** + * Create and configure a file manager for a workspace. + * + * @param workspace the workspace to access files in. + * @return the file manager. + */ + @WillNotClose + JctFileManager createFileManager(@WillNotClose Workspace workspace); +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/ModuleLocation.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/ModuleLocation.java index 8083cbec2..751f42699 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/ModuleLocation.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/ModuleLocation.java @@ -30,7 +30,7 @@ * @author Ashley Scopes * @since 0.0.1 */ -@API(since = "0.0.1", status = Status.INTERNAL) +@API(since = "0.0.1", status = Status.STABLE) @Immutable @ThreadSafe public final class ModuleLocation implements Location { diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerAnnotationProcessorClassPathConfigurer.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerAnnotationProcessorClassPathConfigurer.java new file mode 100644 index 000000000..3da892d15 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerAnnotationProcessorClassPathConfigurer.java @@ -0,0 +1,100 @@ +/* + * 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.filemanagers.config; + +import io.github.ascopes.jct.compilers.JctCompiler; +import io.github.ascopes.jct.filemanagers.AnnotationProcessorDiscovery; +import io.github.ascopes.jct.filemanagers.JctFileManager; +import java.util.Map; +import javax.annotation.WillNotClose; +import javax.annotation.concurrent.Immutable; +import javax.annotation.concurrent.ThreadSafe; +import javax.tools.StandardLocation; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Configurer for a file manager that makes annotation processors in the classpath accessible to the + * annotation processor path. + * + *

If annotation processor discovery is disabled for dependencies, this will be skipped. + * + * @author Ashley Scopes + * @since 0.0.1 (0.0.1-M7) + */ +@API(since = "0.0.1", status = Status.INTERNAL) +@Immutable +@ThreadSafe +public final class JctFileManagerAnnotationProcessorClassPathConfigurer implements + JctFileManagerConfigurer { + + private static final Logger LOGGER = LoggerFactory + .getLogger(JctFileManagerAnnotationProcessorClassPathConfigurer.class); + + private static final Map INHERITED_AP_PATHS = Map.of( + // https://stackoverflow.com/q/53084037 + // Seems that javac will always use the classpath to implement this behaviour, and never + // the module path. Let's keep this simple and mimic this behaviour. If someone complains + // about it being problematic in the future, then I am open to change how this works to + // keep it sensible. + // (from -> to) + StandardLocation.CLASS_PATH, StandardLocation.ANNOTATION_PROCESSOR_PATH + ); + + private final JctCompiler compiler; + + /** + * Initialise the configurer with the desired compiler. + * + * @param compiler the compiler to wrap. + */ + public JctFileManagerAnnotationProcessorClassPathConfigurer(JctCompiler compiler) { + this.compiler = compiler; + } + + @Override + public JctFileManager configure(@WillNotClose JctFileManager fileManager) { + LOGGER.debug("Configuring annotation processor discovery mechanism"); + + switch (compiler.getAnnotationProcessorDiscovery()) { + case ENABLED: + LOGGER.trace("Annotation processor discovery is enabled, ensuring empty location exists"); + + INHERITED_AP_PATHS.values().forEach(fileManager::ensureEmptyLocationExists); + + return fileManager; + + case INCLUDE_DEPENDENCIES: + LOGGER.trace("Annotation processor discovery is enabled, copying classpath dependencies " + + "into the annotation processor path"); + + INHERITED_AP_PATHS.forEach(fileManager::copyContainers); + INHERITED_AP_PATHS.values().forEach(fileManager::ensureEmptyLocationExists); + + return fileManager; + + default: + throw new IllegalStateException("Cannot configure annotation processor discovery"); + } + } + + @Override + public boolean isEnabled() { + return compiler.getAnnotationProcessorDiscovery() != AnnotationProcessorDiscovery.DISABLED; + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerConfigurer.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerConfigurer.java new file mode 100644 index 000000000..8abef027a --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerConfigurer.java @@ -0,0 +1,49 @@ +/* + * 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.filemanagers.config; + +import io.github.ascopes.jct.filemanagers.JctFileManager; +import javax.annotation.WillNotClose; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * Interface for a configurer of Java File Manager objects. + * + * @author Ashley Scopes + * @since 0.0.1 (0.0.1-M7) + */ +@API(since = "0.0.1", status = Status.STABLE) +@FunctionalInterface +public interface JctFileManagerConfigurer { + + /** + * Configure the file manager implementation. + * + * @param fileManager the file manager implementation. + * @return the new file manager (this may be the same as the input file manager). + */ + JctFileManager configure(@WillNotClose JctFileManager fileManager); + + /** + * Determine if this configurer is enabled or not. + * + * @return {@code true} if enabled, {@code false} if disabled. + */ + default boolean isEnabled() { + return true; + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerConfigurerChain.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerConfigurerChain.java new file mode 100644 index 000000000..b369cafd0 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerConfigurerChain.java @@ -0,0 +1,100 @@ +/* + * 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.filemanagers.config; + +import io.github.ascopes.jct.filemanagers.JctFileManager; +import java.util.Deque; +import java.util.LinkedList; +import java.util.List; +import javax.annotation.concurrent.NotThreadSafe; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A chain of configurers to apply to a file manager. + * + * @author Ashley Scopes + * @since 0.0.1 (0.0.1-M7) + */ +@API(since = "0.0.1", status = Status.STABLE) +@NotThreadSafe +public final class JctFileManagerConfigurerChain { + + private static final Logger LOGGER = LoggerFactory.getLogger(JctFileManagerConfigurerChain.class); + + private final Deque configurers; + + /** + * Initialise this chain. + */ + public JctFileManagerConfigurerChain() { + configurers = new LinkedList<>(); + } + + /** + * Add a configurer to the start of the chain. + * + * @param configurer the configurer to add. + * @return this chain for further calls. + */ + public JctFileManagerConfigurerChain addFirst(JctFileManagerConfigurer configurer) { + configurers.addFirst(configurer); + return this; + } + + /** + * Add a configurer to the end of the chain. + * + * @param configurer the configurer to add. + * @return this chain for further calls. + */ + public JctFileManagerConfigurerChain addLast(JctFileManagerConfigurer configurer) { + configurers.addLast(configurer); + return this; + } + + /** + * Get an immutable copy of the list of configurers. + * + * @return the list of configurers. + */ + public List list() { + return List.copyOf(configurers); + } + + /** + * Apply each configurer to the given file manager in order. + * + * @param fileManager the file manager to configure. + * @return the configured file manager to use. This may or may not be the same + * object as the input parameter, depending on how the configurers manipulate + * the input object. + */ + public JctFileManager configure(JctFileManager fileManager) { + for (var configurer : configurers) { + if (configurer.isEnabled()) { + LOGGER.trace("Applying {} to file manager", configurer); + fileManager = configurer.configure(fileManager); + } else { + LOGGER.trace("Skipping {}", configurer); + } + } + + return fileManager; + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerJvmClassPathConfigurer.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerJvmClassPathConfigurer.java new file mode 100644 index 000000000..a7f788f1a --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerJvmClassPathConfigurer.java @@ -0,0 +1,78 @@ +/* + * 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.filemanagers.config; + +import static java.util.stream.Collectors.partitioningBy; + +import io.github.ascopes.jct.compilers.JctCompiler; +import io.github.ascopes.jct.filemanagers.JctFileManager; +import io.github.ascopes.jct.utils.SpecialLocationUtils; +import io.github.ascopes.jct.workspaces.impl.WrappingDirectoryImpl; +import javax.annotation.WillNotClose; +import javax.annotation.concurrent.Immutable; +import javax.annotation.concurrent.ThreadSafe; +import javax.tools.StandardLocation; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Configurer for a file manager that applies the running JVM's classpath to the file manager. + * + *

If classpath inheritance is disabled in the compiler, then this will not run. + * + * @author Ashley Scopes + * @since 0.0.1 (0.0.1-M7) + */ +@API(since = "0.0.1", status = Status.INTERNAL) +@Immutable +@ThreadSafe +public final class JctFileManagerJvmClassPathConfigurer implements JctFileManagerConfigurer { + + private static final Logger LOGGER = LoggerFactory + .getLogger(JctFileManagerJvmClassPathConfigurer.class); + + private final JctCompiler compiler; + + /** + * Initialise the configurer with the desired compiler. + * + * @param compiler the compiler to wrap. + */ + public JctFileManagerJvmClassPathConfigurer(JctCompiler compiler) { + this.compiler = compiler; + } + + @Override + public JctFileManager configure(@WillNotClose JctFileManager fileManager) { + LOGGER.debug("Configuring the class path"); + + SpecialLocationUtils + .currentClassPathLocations() + .stream() + .peek(loc -> LOGGER.trace("Adding {} to file manager classpath (inherited from JVM)", loc)) + .map(WrappingDirectoryImpl::new) + .forEach(dir -> fileManager.addPath(StandardLocation.CLASS_PATH, dir)); + + return fileManager; + } + + @Override + public boolean isEnabled() { + return compiler.isInheritClassPath(); + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerJvmClassPathModuleConfigurer.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerJvmClassPathModuleConfigurer.java new file mode 100644 index 000000000..8e0bd772f --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerJvmClassPathModuleConfigurer.java @@ -0,0 +1,83 @@ +/* + * 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.filemanagers.config; + +import io.github.ascopes.jct.compilers.JctCompiler; +import io.github.ascopes.jct.filemanagers.JctFileManager; +import io.github.ascopes.jct.utils.SpecialLocationUtils; +import io.github.ascopes.jct.workspaces.impl.WrappingDirectoryImpl; +import javax.annotation.WillNotClose; +import javax.annotation.concurrent.Immutable; +import javax.annotation.concurrent.ThreadSafe; +import javax.tools.StandardLocation; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Configurer for a file manager that detects and applies classpath paths that contain JPMS + * modules to the module path. + * + *

If classpath inheritance or module fixing is disabled in the compiler, + * this will not run. + * + *

This fixes some common configuration issues when IDEs invoke JUnit. + * + * @author Ashley Scopes + * @since 0.0.1 (0.0.1-M7) + */ +@API(since = "0.0.1", status = Status.INTERNAL) +@Immutable +@ThreadSafe +public final class JctFileManagerJvmClassPathModuleConfigurer implements JctFileManagerConfigurer { + + private static final Logger LOGGER = LoggerFactory + .getLogger(JctFileManagerJvmClassPathModuleConfigurer.class); + + private final JctCompiler compiler; + + /** + * Initialise the configurer with the desired compiler. + * + * @param compiler the compiler to wrap. + */ + public JctFileManagerJvmClassPathModuleConfigurer(JctCompiler compiler) { + this.compiler = compiler; + } + + @Override + public JctFileManager configure(@WillNotClose JctFileManager fileManager) { + LOGGER.debug( + "Copying any misplaced modules that exist within the class path onto the module path" + ); + + SpecialLocationUtils + .currentClassPathLocations() + .stream() + .peek(loc -> LOGGER.trace("Adding {} to file manager module path (inherited from JVM)", loc)) + .map(WrappingDirectoryImpl::new) + // File manager will pull out the actual modules automatically. + .forEach(dir -> fileManager.addPath(StandardLocation.MODULE_PATH, dir)); + + return fileManager; + } + + @Override + public boolean isEnabled() { + return compiler.isInheritClassPath() && compiler.isFixJvmModulePathMismatch(); + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerJvmModulePathConfigurer.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerJvmModulePathConfigurer.java new file mode 100644 index 000000000..36bd7a518 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerJvmModulePathConfigurer.java @@ -0,0 +1,83 @@ +/* + * 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.filemanagers.config; + +import io.github.ascopes.jct.compilers.JctCompiler; +import io.github.ascopes.jct.filemanagers.JctFileManager; +import io.github.ascopes.jct.utils.SpecialLocationUtils; +import io.github.ascopes.jct.workspaces.impl.WrappingDirectoryImpl; +import javax.annotation.WillNotClose; +import javax.annotation.concurrent.Immutable; +import javax.annotation.concurrent.ThreadSafe; +import javax.tools.StandardLocation; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Configurer for a file manager that applies the running JVM's module path to the file manager. + * + *

If module path inheritance is disabled in the compiler, then this will not run. + * + * @author Ashley Scopes + * @since 0.0.1 (0.0.1-M7) + */ +@API(since = "0.0.1", status = Status.INTERNAL) +@Immutable +@ThreadSafe +public final class JctFileManagerJvmModulePathConfigurer implements JctFileManagerConfigurer { + + private static final Logger LOGGER = LoggerFactory + .getLogger(JctFileManagerJvmModulePathConfigurer.class); + + private final JctCompiler compiler; + + /** + * Initialise the configurer with the desired compiler. + * + * @param compiler the compiler to wrap. + */ + public JctFileManagerJvmModulePathConfigurer(JctCompiler compiler) { + this.compiler = compiler; + } + + + @Override + public JctFileManager configure(@WillNotClose JctFileManager fileManager) { + LOGGER.debug("Configuring module path"); + + SpecialLocationUtils + .currentModulePathLocations() + .stream() + .peek(loc -> LOGGER.trace("Adding {} to file manager modulepath (inherited from JVM)", loc)) + .map(WrappingDirectoryImpl::new) + .forEach(dir -> { + // Since we do not know if the code being compiled will use modules or not just yet, + // make sure any modules are on the class path as well so that they remain accessible + // in unnamed modules. + fileManager.addPath(StandardLocation.MODULE_PATH, dir); + fileManager.addPath(StandardLocation.CLASS_PATH, dir); + }); + + return fileManager; + } + + @Override + public boolean isEnabled() { + return compiler.isInheritModulePath(); + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerJvmPlatformClassPathConfigurer.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerJvmPlatformClassPathConfigurer.java new file mode 100644 index 000000000..9f8599eb6 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerJvmPlatformClassPathConfigurer.java @@ -0,0 +1,80 @@ +/* + * 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.filemanagers.config; + +import io.github.ascopes.jct.compilers.JctCompiler; +import io.github.ascopes.jct.filemanagers.JctFileManager; +import io.github.ascopes.jct.utils.SpecialLocationUtils; +import io.github.ascopes.jct.workspaces.impl.WrappingDirectoryImpl; +import javax.annotation.WillNotClose; +import javax.annotation.concurrent.Immutable; +import javax.annotation.concurrent.ThreadSafe; +import javax.tools.StandardLocation; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Configurer for a file manager that applies the running JVM's platform classpath to the + * file manager. + * + *

If platform classpath inheritance is disabled in the compiler, then this will not run. + * + * @author Ashley Scopes + * @since 0.0.1 (0.0.1-M7) + */ +@API(since = "0.0.1", status = Status.INTERNAL) +@Immutable +@ThreadSafe +public final class JctFileManagerJvmPlatformClassPathConfigurer implements + JctFileManagerConfigurer { + + private static final Logger LOGGER = LoggerFactory + .getLogger(JctFileManagerJvmPlatformClassPathConfigurer.class); + + private final JctCompiler compiler; + + /** + * Initialise the configurer with the desired compiler. + * + * @param compiler the compiler to wrap. + */ + public JctFileManagerJvmPlatformClassPathConfigurer(JctCompiler compiler) { + this.compiler = compiler; + } + + @Override + public JctFileManager configure(@WillNotClose JctFileManager fileManager) { + LOGGER.debug("Configuring JVM platform class path"); + + SpecialLocationUtils + .currentPlatformClassPathLocations() + .stream() + .peek(loc -> LOGGER.trace( + "Adding {} to file manager platform classpath (inherited from JVM)", loc + )) + .map(WrappingDirectoryImpl::new) + .forEach(dir -> fileManager.addPath(StandardLocation.PLATFORM_CLASS_PATH, dir)); + + return fileManager; + } + + @Override + public boolean isEnabled() { + return compiler.isInheritPlatformClassPath(); + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerJvmSystemModulesConfigurer.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerJvmSystemModulesConfigurer.java new file mode 100644 index 000000000..e6cd909e2 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerJvmSystemModulesConfigurer.java @@ -0,0 +1,79 @@ +/* + * 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.filemanagers.config; + +import io.github.ascopes.jct.compilers.JctCompiler; +import io.github.ascopes.jct.filemanagers.JctFileManager; +import io.github.ascopes.jct.utils.SpecialLocationUtils; +import io.github.ascopes.jct.workspaces.impl.WrappingDirectoryImpl; +import javax.annotation.WillNotClose; +import javax.annotation.concurrent.Immutable; +import javax.annotation.concurrent.ThreadSafe; +import javax.tools.StandardLocation; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Configurer for a file manager that applies the running JVM's system modules to the + * file manager. + * + *

If system module inheritance is disabled in the compiler, then this will not run. + * + * @author Ashley Scopes + * @since 0.0.1 (0.0.1-M7) + */ +@API(since = "0.0.1", status = Status.INTERNAL) +@Immutable +@ThreadSafe +public final class JctFileManagerJvmSystemModulesConfigurer implements JctFileManagerConfigurer { + + private static final Logger LOGGER = LoggerFactory + .getLogger(JctFileManagerJvmSystemModulesConfigurer.class); + + private final JctCompiler compiler; + + /** + * Initialise the configurer with the desired compiler. + * + * @param compiler the compiler to wrap. + */ + public JctFileManagerJvmSystemModulesConfigurer(JctCompiler compiler) { + this.compiler = compiler; + } + + @Override + public JctFileManager configure(@WillNotClose JctFileManager fileManager) { + LOGGER.debug("Configuring JVM system modules path"); + + SpecialLocationUtils + .javaRuntimeLocations() + .stream() + .peek(loc -> LOGGER.trace( + "Adding {} to the system modules path (inherited from JVM)", loc + )) + .map(WrappingDirectoryImpl::new) + .forEach(dir -> fileManager.addPath(StandardLocation.SYSTEM_MODULES, dir)); + + return fileManager; + } + + @Override + public boolean isEnabled() { + return compiler.isInheritSystemModulePath(); + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerLoggingProxyConfigurer.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerLoggingProxyConfigurer.java new file mode 100644 index 000000000..08089015c --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerLoggingProxyConfigurer.java @@ -0,0 +1,75 @@ +/* + * 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.filemanagers.config; + +import io.github.ascopes.jct.compilers.JctCompiler; +import io.github.ascopes.jct.filemanagers.JctFileManager; +import io.github.ascopes.jct.filemanagers.LoggingFileManagerProxy; +import io.github.ascopes.jct.filemanagers.LoggingMode; +import javax.annotation.concurrent.Immutable; +import javax.annotation.concurrent.ThreadSafe; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * File manager configurer that optionally wraps the file manager in a logging proxy that outputs + * interaction details to the console logs. + * + * @author Ashley Scopes + * @since 0.0.1 (0.0.1-M7) + */ +@API(since = "0.0.1", status = Status.INTERNAL) +@Immutable +@ThreadSafe +public final class JctFileManagerLoggingProxyConfigurer implements JctFileManagerConfigurer { + + private static final Logger LOGGER = LoggerFactory + .getLogger(JctFileManagerLoggingProxyConfigurer.class); + + private final JctCompiler compiler; + + /** + * Initialise this configurer. + * + * @param compiler the compiler to apply to the file manager. + */ + public JctFileManagerLoggingProxyConfigurer(JctCompiler compiler) { + this.compiler = compiler; + } + + @Override + public JctFileManager configure(JctFileManager fileManager) { + LOGGER.debug("Configuring compiler operation audit logging"); + + switch (compiler.getFileManagerLoggingMode()) { + case STACKTRACES: + LOGGER.trace("Decorating file manager {} in a logging proxy with stacktraces", fileManager); + return LoggingFileManagerProxy.wrap(fileManager, true); + case ENABLED: + LOGGER.trace("Decorating file manager {} in a logging proxy", fileManager); + return LoggingFileManagerProxy.wrap(fileManager, false); + default: + throw new IllegalStateException("Cannot configure logging proxy"); + } + } + + @Override + public boolean isEnabled() { + return compiler.getFileManagerLoggingMode() != LoggingMode.DISABLED; + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerRequiredLocationsConfigurer.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerRequiredLocationsConfigurer.java new file mode 100644 index 000000000..58e04e01b --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerRequiredLocationsConfigurer.java @@ -0,0 +1,90 @@ +/* + * 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.filemanagers.config; + +import static java.util.function.Predicate.not; + +import io.github.ascopes.jct.filemanagers.JctFileManager; +import io.github.ascopes.jct.workspaces.Workspace; +import java.util.Set; +import javax.annotation.WillNotClose; +import javax.annotation.concurrent.Immutable; +import javax.annotation.concurrent.ThreadSafe; +import javax.tools.StandardLocation; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Configurer for a file manager that creates missing required locations to the file manager. + * + *

These locations will be created as empty paths in the workspace. + * + * @author Ashley Scopes + * @since 0.0.1 (0.0.1-M7) + */ +@API(since = "0.0.1", status = Status.INTERNAL) +@Immutable +@ThreadSafe +public final class JctFileManagerRequiredLocationsConfigurer implements JctFileManagerConfigurer { + + private static final Logger LOGGER + = LoggerFactory.getLogger(JctFileManagerRequiredLocationsConfigurer.class); + + // Locations that we have to ensure exist before the compiler is run. + private static final Set REQUIRED_LOCATIONS = Set.of( + // We have to manually create this one as javac will not attempt to access it lazily. Instead, + // it will just abort if it is not present. This means we cannot take advantage of the + // container group creating the roots as we try to access them for this specific case. + StandardLocation.SOURCE_OUTPUT, + // Annotation processors that create files will need this directory to exist if it is to + // work properly. + StandardLocation.CLASS_OUTPUT, + // We need to provide a header output path in case header generation is enabled at any stage. + // I might make this disabled by default in the future if there is too much overhead from + // doing this by default. + StandardLocation.NATIVE_HEADER_OUTPUT + ); + + private final Workspace workspace; + + /** + * Initialise this configurer. + * + * @param workspace the workspace to bind to. + */ + public JctFileManagerRequiredLocationsConfigurer(@WillNotClose Workspace workspace) { + this.workspace = workspace; + } + + @Override + public JctFileManager configure(@WillNotClose JctFileManager fileManager) { + LOGGER.debug("Configuring required locations that do not yet exist"); + + REQUIRED_LOCATIONS + .stream() + .filter(not(fileManager::hasLocation)) + .forEach(location -> { + LOGGER.trace( + "Required location {} does not exist, so will be created in the workspace", + location + ); + fileManager.addPath(location, workspace.createPackage(location)); + }); + return fileManager; + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerWorkspaceConfigurer.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerWorkspaceConfigurer.java new file mode 100644 index 000000000..7578bbbe8 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/JctFileManagerWorkspaceConfigurer.java @@ -0,0 +1,60 @@ +/* + * 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.filemanagers.config; + +import io.github.ascopes.jct.filemanagers.JctFileManager; +import io.github.ascopes.jct.workspaces.Workspace; +import javax.annotation.WillNotClose; +import javax.annotation.concurrent.Immutable; +import javax.annotation.concurrent.ThreadSafe; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Configurer for a file manager that applies the given workspace. + * + * @author Ashley Scopes + * @since 0.0.1 (0.0.1-M7) + */ +@API(since = "0.0.1", status = Status.INTERNAL) +@Immutable +@ThreadSafe +public final class JctFileManagerWorkspaceConfigurer implements JctFileManagerConfigurer { + + private static final Logger LOGGER + = LoggerFactory.getLogger(JctFileManagerWorkspaceConfigurer.class); + + private final Workspace workspace; + + /** + * Initialise the configurer with the desired workspace. + * + * @param workspace the workspace to wrap. + */ + public JctFileManagerWorkspaceConfigurer(@WillNotClose Workspace workspace) { + this.workspace = workspace; + } + + @Override + public JctFileManager configure(@WillNotClose JctFileManager fileManager) { + var paths = workspace.getAllPaths(); + LOGGER.debug("Copying user-defined paths from workspace ({})", paths); + workspace.getAllPaths().forEach(fileManager::addPaths); + return fileManager; + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/package-info.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/package-info.java new file mode 100644 index 000000000..a5508116f --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/config/package-info.java @@ -0,0 +1,27 @@ +/* + * 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. + */ +/** + * File manager configuration rules. + */ +@API(since = "0.0.1", status = Status.INTERNAL) +@NonNullApi +@NonNullImpl +package io.github.ascopes.jct.filemanagers.config; + +import io.github.ascopes.jct.utils.NonNullApi; +import io.github.ascopes.jct.utils.NonNullImpl; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/impl/JctFileManagerFactoryImpl.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/impl/JctFileManagerFactoryImpl.java new file mode 100644 index 000000000..faa499171 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/impl/JctFileManagerFactoryImpl.java @@ -0,0 +1,93 @@ +/* + * 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.filemanagers.impl; + +import io.github.ascopes.jct.compilers.JctCompiler; +import io.github.ascopes.jct.filemanagers.JctFileManager; +import io.github.ascopes.jct.filemanagers.JctFileManagerFactory; +import io.github.ascopes.jct.filemanagers.config.JctFileManagerAnnotationProcessorClassPathConfigurer; +import io.github.ascopes.jct.filemanagers.config.JctFileManagerConfigurerChain; +import io.github.ascopes.jct.filemanagers.config.JctFileManagerJvmClassPathConfigurer; +import io.github.ascopes.jct.filemanagers.config.JctFileManagerJvmClassPathModuleConfigurer; +import io.github.ascopes.jct.filemanagers.config.JctFileManagerJvmModulePathConfigurer; +import io.github.ascopes.jct.filemanagers.config.JctFileManagerJvmPlatformClassPathConfigurer; +import io.github.ascopes.jct.filemanagers.config.JctFileManagerJvmSystemModulesConfigurer; +import io.github.ascopes.jct.filemanagers.config.JctFileManagerLoggingProxyConfigurer; +import io.github.ascopes.jct.filemanagers.config.JctFileManagerRequiredLocationsConfigurer; +import io.github.ascopes.jct.filemanagers.config.JctFileManagerWorkspaceConfigurer; +import io.github.ascopes.jct.workspaces.Workspace; +import javax.annotation.WillNotClose; +import javax.annotation.concurrent.Immutable; +import javax.annotation.concurrent.ThreadSafe; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * Basic implementation for a file manager factory that returns a {@link JctFileManagerImpl} + * instance. + * + *

This implementation binds to a given {@link JctCompiler} object on construction to enable + * potential reuse. + * + * @author Ashley Scopes + * @since 0.0.1 (0.0.1-M7) + */ +@API(since = "0.0.1", status = Status.INTERNAL) +@Immutable +@ThreadSafe +public final class JctFileManagerFactoryImpl implements JctFileManagerFactory { + + private final JctCompiler compiler; + + /** + * Initialise this factory. + * + * @param compiler the compiler to pull configuration details from. + */ + public JctFileManagerFactoryImpl(JctCompiler compiler) { + this.compiler = compiler; + } + + @Override + @WillNotClose + public JctFileManager createFileManager(@WillNotClose Workspace workspace) { + var release = compiler.getEffectiveRelease(); + var fileManager = new JctFileManagerImpl(release); + return createConfigurerChain(workspace) + .configure(fileManager); + } + + /** + * Create the default configurer chain to use for the given workspace. + * + *

This is visible for testing only. + * + * @param workspace the workspace to configure with. + * @return the chain to use. + */ + public JctFileManagerConfigurerChain createConfigurerChain(@WillNotClose Workspace workspace) { + return new JctFileManagerConfigurerChain() + .addLast(new JctFileManagerWorkspaceConfigurer(workspace)) + .addLast(new JctFileManagerJvmClassPathConfigurer(compiler)) + .addLast(new JctFileManagerJvmClassPathModuleConfigurer(compiler)) + .addLast(new JctFileManagerJvmModulePathConfigurer(compiler)) + .addLast(new JctFileManagerJvmPlatformClassPathConfigurer(compiler)) + .addLast(new JctFileManagerJvmSystemModulesConfigurer(compiler)) + .addLast(new JctFileManagerAnnotationProcessorClassPathConfigurer(compiler)) + .addLast(new JctFileManagerRequiredLocationsConfigurer(workspace)) + .addLast(new JctFileManagerLoggingProxyConfigurer(compiler)); + } +} 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 842bf59ad..1a1a56ad9 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 @@ -30,6 +30,7 @@ import io.github.ascopes.jct.utils.ToStringBuilder; import io.github.ascopes.jct.workspaces.PathRoot; import java.io.IOException; +import java.lang.module.FindException; import java.lang.module.ModuleFinder; import java.util.ArrayList; import java.util.Collection; @@ -48,6 +49,8 @@ import javax.tools.JavaFileObject.Kind; import org.apiguardian.api.API; import org.apiguardian.api.API.Status; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Simple implementation of a {@link JctFileManager}. @@ -59,6 +62,7 @@ @ThreadSafe public final class JctFileManagerImpl implements JctFileManager { + private static final Logger LOGGER = LoggerFactory.getLogger(JctFileManagerImpl.class); private static final int UNSUPPORTED_ARGUMENT = -1; private final String release; @@ -66,7 +70,7 @@ public final class JctFileManagerImpl implements JctFileManager { private final Map modules; private final Map outputs; - private JctFileManagerImpl(String release) { + public JctFileManagerImpl(String release) { this.release = requireNonNull(release, "release"); packages = new ConcurrentHashMap<>(); modules = new ConcurrentHashMap<>(); @@ -94,15 +98,19 @@ public void addPath(Location location, PathRoot path) { // Attempt to find modules. var moduleGroup = getOrCreateModule(location); - for (var ref : ModuleFinder.of(path.getPath()).findAll()) { - var module = ref.descriptor().name(); - - // Right now, assume the module is not in a nested directory. Not sure if there are - // cases where this isn't true, but I spotted some weird errors with paths being appended - // to the end of JAR paths if I uncomment the following line. - moduleGroup.getOrCreateModule(module) - //.addPackage(new BasicPathWrapperImpl(pathWrapper, module)); - .addPackage(path); + try { + for (var ref : ModuleFinder.of(path.getPath()).findAll()) { + var module = ref.descriptor().name(); + + // Right now, assume the module is not in a nested directory. Not sure if there are + // cases where this isn't true, but I spotted some weird errors with paths being appended + // to the end of JAR paths if I uncomment the following line. + moduleGroup.getOrCreateModule(module) + //.addPackage(new BasicPathWrapperImpl(pathWrapper, module)); + .addPackage(path); + } + } catch (FindException ex) { + LOGGER.trace("Dropping {} from module path as no modules were resolved", path, ex); } } else { @@ -562,14 +570,4 @@ private void requirePackageOrientedLocation(Location location) { ); } } - - /** - * Initialize this file manager. - * - * @param release the release to use for multi-release JARs internally. - */ - public static JctFileManagerImpl forRelease(String release) { - // Easier to stub and verify than a constructor elsewhere. - return new JctFileManagerImpl(release); - } } diff --git a/java-compiler-testing/src/main/java/module-info.java b/java-compiler-testing/src/main/java/module-info.java index 4981e61cc..a32a88a7b 100644 --- a/java-compiler-testing/src/main/java/module-info.java +++ b/java-compiler-testing/src/main/java/module-info.java @@ -86,6 +86,11 @@ * */ module io.github.ascopes.jct { + + //////////////////// + /// DEPENDENCIES /// + //////////////////// + requires java.compiler; requires java.management; requires jimfs; @@ -96,21 +101,31 @@ requires static transitive org.junit.jupiter.params; requires org.slf4j; + ////////////////// + /// PUBLIC API /// + ////////////////// + exports io.github.ascopes.jct.assertions; exports io.github.ascopes.jct.containers; exports io.github.ascopes.jct.compilers; exports io.github.ascopes.jct.diagnostics; exports io.github.ascopes.jct.ex; exports io.github.ascopes.jct.filemanagers; + exports io.github.ascopes.jct.filemanagers.config; exports io.github.ascopes.jct.junit; exports io.github.ascopes.jct.repr; exports io.github.ascopes.jct.workspaces; + //////////////////////////////////////////////////////////////////////// + /// EXPOSURE OF JUNIT ANNOTATIONS TO JUNIT COMPONENTS FOR REFLECTION /// + //////////////////////////////////////////////////////////////////////// + opens io.github.ascopes.jct.junit; ////////////////////////////////////////////////////// /// EXPOSURE OF INTERNALS TO THE TESTING NAMESPACE /// ////////////////////////////////////////////////////// + exports io.github.ascopes.jct.compilers.impl to io.github.ascopes.jct.testing; exports io.github.ascopes.jct.compilers.javac to io.github.ascopes.jct.testing; exports io.github.ascopes.jct.containers.impl to io.github.ascopes.jct.testing; @@ -118,6 +133,10 @@ exports io.github.ascopes.jct.utils to io.github.ascopes.jct.testing; exports io.github.ascopes.jct.workspaces.impl to io.github.ascopes.jct.testing; + ////////////////////////////////////////////////////////////////////////// + /// EXPOSURE OF ALL COMPONENTS TO THE TESTING NAMESPACE FOR REFLECTION /// + ////////////////////////////////////////////////////////////////////////// + opens io.github.ascopes.jct.assertions to io.github.ascopes.jct.testing; opens io.github.ascopes.jct.compilers to io.github.ascopes.jct.testing; opens io.github.ascopes.jct.compilers.impl to io.github.ascopes.jct.testing; @@ -127,6 +146,7 @@ opens io.github.ascopes.jct.diagnostics to io.github.ascopes.jct.testing; opens io.github.ascopes.jct.ex to io.github.ascopes.jct.testing; opens io.github.ascopes.jct.filemanagers to io.github.ascopes.jct.testing; + opens io.github.ascopes.jct.filemanagers.config to io.github.ascopes.jct.testing; opens io.github.ascopes.jct.filemanagers.impl to io.github.ascopes.jct.testing; opens io.github.ascopes.jct.repr to io.github.ascopes.jct.testing; opens io.github.ascopes.jct.utils to io.github.ascopes.jct.testing; diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/AbstractJctCompilerTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/AbstractJctCompilerTest.java index ef97b2c29..f167d3cec 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/AbstractJctCompilerTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/AbstractJctCompilerTest.java @@ -37,8 +37,9 @@ import io.github.ascopes.jct.compilers.JctFlagBuilderFactory; import io.github.ascopes.jct.compilers.Jsr199CompilerFactory; import io.github.ascopes.jct.compilers.impl.JctCompilationImpl; -import io.github.ascopes.jct.compilers.impl.JctJsr199Interop; import io.github.ascopes.jct.filemanagers.AnnotationProcessorDiscovery; +import io.github.ascopes.jct.filemanagers.JctFileManager; +import io.github.ascopes.jct.filemanagers.JctFileManagerFactory; import io.github.ascopes.jct.filemanagers.LoggingMode; import io.github.ascopes.jct.workspaces.Workspace; import java.io.FileNotFoundException; @@ -53,6 +54,7 @@ import java.util.Locale; import java.util.Set; import java.util.stream.Stream; +import javax.annotation.Nullable; import javax.annotation.processing.Processor; import javax.tools.JavaCompiler; import org.assertj.core.api.AbstractObjectAssert; @@ -65,6 +67,7 @@ import org.junit.jupiter.api.TestClassOrder; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.NullAndEmptySource; @@ -299,50 +302,51 @@ void constructorInitialisesAnnotationProcessorDiscoveryToDefaultValue() { } } - @DisplayName(".compile(Workspace) builds the expected compilation object") - @Test - void compileWorkspaceBuildsTheExpectedCompilationObject() { - try (var interopCls = mockStatic(JctJsr199Interop.class)) { - // Given - var expectedCompilation = mock(JctCompilationImpl.class); - interopCls.when(() -> JctJsr199Interop.compile(any(), any(), any(), any(), any())) - .thenReturn(expectedCompilation); - var expectedWorkspace = mock(Workspace.class); - - // When - var actualCompilation = compiler.compile(expectedWorkspace); - - // Then - interopCls.verify(() -> JctJsr199Interop - .compile(expectedWorkspace, compiler, jsr199Compiler, flagBuilder, null)); - verify(compiler).getJsr199CompilerFactory(); - verify(compiler).getJctFlagBuilderFactory(); - - assertThat(actualCompilation).isSameAs(expectedCompilation); - } - } - - @DisplayName(".compile(Workspace, Collection) builds the expected compilation object") - @Test - void compileWorkspaceCollectionBuildsTheExpectedCompilationObject() { - try (var factoryCls = mockStatic(JctJsr199Interop.class)) { - // Given - var expectedCompilation = mock(JctCompilationImpl.class); - factoryCls.when(() -> JctJsr199Interop.compile(any(), any(), any(), any(), any())) - .thenReturn(expectedCompilation); - var expectedWorkspace = mock(Workspace.class); - var classes = Set.of("foo.bar", "baz.bork", "qux.quxx"); - - // When - var actualCompilation = compiler.compile(expectedWorkspace, classes); - - // Then - factoryCls.verify(() -> JctJsr199Interop - .compile(expectedWorkspace, compiler, jsr199Compiler, flagBuilder, classes)); - - assertThat(actualCompilation).isSameAs(expectedCompilation); - } - } + // TODO(ascopes): reimplement this +// @DisplayName(".compile(Workspace) builds the expected compilation object") +// @Test +// void compileWorkspaceBuildsTheExpectedCompilationObject() { +// try (var interopCls = mockStatic(JctJsr199Interop.class)) { +// // Given +// var expectedCompilation = mock(JctCompilationImpl.class); +// interopCls.when(() -> JctJsr199Interop.compile(any(), any(), any(), any(), any())) +// .thenReturn(expectedCompilation); +// var expectedWorkspace = mock(Workspace.class); +// +// // When +// var actualCompilation = compiler.compile(expectedWorkspace); +// +// // Then +// interopCls.verify(() -> JctJsr199Interop +// .compile(expectedWorkspace, compiler, jsr199Compiler, flagBuilder, null)); +// verify(compiler).getCompilerFactory(); +// verify(compiler).getFlagBuilderFactory(); +// +// assertThat(actualCompilation).isSameAs(expectedCompilation); +// } +// } +// +// @DisplayName(".compile(Workspace, Collection) builds the expected compilation object") +// @Test +// void compileWorkspaceCollectionBuildsTheExpectedCompilationObject() { +// try (var factoryCls = mockStatic(JctJsr199Interop.class)) { +// // Given +// var expectedCompilation = mock(JctCompilationImpl.class); +// factoryCls.when(() -> JctJsr199Interop.compile(any(), any(), any(), any(), any())) +// .thenReturn(expectedCompilation); +// var expectedWorkspace = mock(Workspace.class); +// var classes = Set.of("foo.bar", "baz.bork", "qux.quxx"); +// +// // When +// var actualCompilation = compiler.compile(expectedWorkspace, classes); +// +// // Then +// factoryCls.verify(() -> JctJsr199Interop +// .compile(expectedWorkspace, compiler, jsr199Compiler, flagBuilder, classes)); +// +// assertThat(actualCompilation).isSameAs(expectedCompilation); +// } +// } @DisplayName("AbstractJctCompiler#configure tests") @Nested @@ -854,6 +858,31 @@ void addCompilerOptionsReturnsTheCompiler() { } } + @DisplayName(".getEffectiveRelease() returns the expected values") + @CsvSource({ + "10, , 12, 10", + " , 11, 12, 11", + " , , 12, 12", + }) + @ParameterizedTest(name = "for release = {0}, target = {1}, defaultRelease = {2}, expect = {3}") + void getEffectiveReleaseReturnsExpectedValues( + @Nullable String release, + @Nullable String target, + String defaultRelease, + String expectedEffectiveRelease + ) { + // Given + var compiler = new CompilerImpl("test", defaultRelease); + compiler.release(release); + compiler.target(target); + + // When + var actualEffectiveRelease = compiler.getEffectiveRelease(); + + // Then + assertThat(actualEffectiveRelease).isEqualTo(expectedEffectiveRelease); + } + @DisplayName(".getRelease() returns the expected values") @NullAndEmptySource @ValueSource(strings = {"8", "9", "11", "17", "21"}) @@ -1504,14 +1533,19 @@ public String getDefaultRelease() { } @Override - public JctFlagBuilderFactory getJctFlagBuilderFactory() { + public JctFlagBuilderFactory getFlagBuilderFactory() { return () -> flagBuilder; } @Override - public Jsr199CompilerFactory getJsr199CompilerFactory() { + public Jsr199CompilerFactory getCompilerFactory() { return () -> jsr199Compiler; } + + @Override + public JctFileManagerFactory getFileManagerFactory() { + return workspace -> mock(); + } } @SafeVarargs diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/impl/JctJsr199InteropTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/impl/JctJsr199InteropTest.java deleted file mode 100644 index 85d086578..000000000 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/impl/JctJsr199InteropTest.java +++ /dev/null @@ -1,1994 +0,0 @@ -/* - * 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.compilers.impl; - -import static io.github.ascopes.jct.compilers.impl.JctJsr199Interop.buildDiagnosticListener; -import static io.github.ascopes.jct.compilers.impl.JctJsr199Interop.buildFileManager; -import static io.github.ascopes.jct.compilers.impl.JctJsr199Interop.buildFlags; -import static io.github.ascopes.jct.compilers.impl.JctJsr199Interop.buildWriter; -import static io.github.ascopes.jct.compilers.impl.JctJsr199Interop.compile; -import static io.github.ascopes.jct.compilers.impl.JctJsr199Interop.configureAnnotationProcessorDiscovery; -import static io.github.ascopes.jct.compilers.impl.JctJsr199Interop.configureAnnotationProcessorPaths; -import static io.github.ascopes.jct.compilers.impl.JctJsr199Interop.configureClassPath; -import static io.github.ascopes.jct.compilers.impl.JctJsr199Interop.configureJvmSystemModules; -import static io.github.ascopes.jct.compilers.impl.JctJsr199Interop.configureModulePath; -import static io.github.ascopes.jct.compilers.impl.JctJsr199Interop.configurePlatformClassPath; -import static io.github.ascopes.jct.compilers.impl.JctJsr199Interop.configureRequiredLocations; -import static io.github.ascopes.jct.compilers.impl.JctJsr199Interop.configureWorkspacePaths; -import static io.github.ascopes.jct.compilers.impl.JctJsr199Interop.containsModules; -import static io.github.ascopes.jct.compilers.impl.JctJsr199Interop.determineRelease; -import static io.github.ascopes.jct.compilers.impl.JctJsr199Interop.findCompilationUnitLocations; -import static io.github.ascopes.jct.compilers.impl.JctJsr199Interop.findCompilationUnits; -import static io.github.ascopes.jct.compilers.impl.JctJsr199Interop.performCompilerPass; -import static io.github.ascopes.jct.tests.helpers.Fixtures.someBoolean; -import static io.github.ascopes.jct.tests.helpers.Fixtures.someCharset; -import static io.github.ascopes.jct.tests.helpers.Fixtures.someCompilationUnits; -import static io.github.ascopes.jct.tests.helpers.Fixtures.someFlags; -import static io.github.ascopes.jct.tests.helpers.Fixtures.someIoException; -import static io.github.ascopes.jct.tests.helpers.Fixtures.someJavaFileObject; -import static io.github.ascopes.jct.tests.helpers.Fixtures.someLinesOfText; -import static io.github.ascopes.jct.tests.helpers.Fixtures.someLocale; -import static io.github.ascopes.jct.tests.helpers.Fixtures.someLocation; -import static io.github.ascopes.jct.tests.helpers.Fixtures.someModuleReference; -import static io.github.ascopes.jct.tests.helpers.Fixtures.somePath; -import static io.github.ascopes.jct.tests.helpers.Fixtures.somePathRoot; -import static io.github.ascopes.jct.tests.helpers.Fixtures.someRelease; -import static io.github.ascopes.jct.tests.helpers.Fixtures.someTraceDiagnostics; -import static io.github.ascopes.jct.tests.helpers.Fixtures.someUncheckedException; -import static io.github.ascopes.jct.tests.helpers.GenericMock.mockRaw; -import static io.github.ascopes.jct.utils.IterableUtils.flatten; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.SoftAssertions.assertSoftly; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.same; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockConstruction; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -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 static org.mockito.Mockito.withSettings; - -import io.github.ascopes.jct.compilers.CompilationMode; -import io.github.ascopes.jct.compilers.JctCompiler; -import io.github.ascopes.jct.compilers.JctFlagBuilder; -import io.github.ascopes.jct.compilers.impl.JctCompilationImpl; -import io.github.ascopes.jct.compilers.impl.JctJsr199Interop; -import io.github.ascopes.jct.diagnostics.TeeWriter; -import io.github.ascopes.jct.diagnostics.TracingDiagnosticListener; -import io.github.ascopes.jct.ex.JctCompilerException; -import io.github.ascopes.jct.filemanagers.AnnotationProcessorDiscovery; -import io.github.ascopes.jct.filemanagers.JctFileManager; -import io.github.ascopes.jct.filemanagers.LoggingFileManagerProxy; -import io.github.ascopes.jct.filemanagers.LoggingMode; -import io.github.ascopes.jct.filemanagers.ModuleLocation; -import io.github.ascopes.jct.filemanagers.impl.JctFileManagerImpl; -import io.github.ascopes.jct.tests.helpers.Fixtures; -import io.github.ascopes.jct.tests.helpers.UtilityClassTestTemplate; -import io.github.ascopes.jct.utils.SpecialLocationUtils; -import io.github.ascopes.jct.workspaces.ManagedDirectory; -import io.github.ascopes.jct.workspaces.PathRoot; -import io.github.ascopes.jct.workspaces.Workspace; -import io.github.ascopes.jct.workspaces.impl.WrappingDirectoryImpl; -import java.io.IOException; -import java.lang.module.FindException; -import java.lang.module.ModuleFinder; -import java.nio.file.Path; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import javax.annotation.Nullable; -import javax.tools.JavaCompiler; -import javax.tools.JavaCompiler.CompilationTask; -import javax.tools.JavaFileManager.Location; -import javax.tools.JavaFileObject; -import javax.tools.JavaFileObject.Kind; -import javax.tools.StandardLocation; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -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.CsvSource; -import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.EnumSource.Mode; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.Answers; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockedConstruction; -import org.mockito.MockedStatic; -import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.quality.Strictness; - -/** - * {@link JctJsr199Interop} tests. - * - * @author Ashley Scopes - */ -@DisplayName("JctJsr199Interop tests") -class JctJsr199InteropTest implements UtilityClassTestTemplate { - - @Override - public Class getTypeBeingTested() { - return JctJsr199Interop.class; - } - - ///////////////////// - /// .compile(...) /// - ///////////////////// - - @DisplayName("JctJsr199Interop#compile tests") - @ExtendWith(MockitoExtension.class) - @Nested - class CompileTest { - - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - MockedStatic staticMock; - - @Mock - Workspace workspace; - - @Mock - JctCompiler compiler; - - @Mock - JavaCompiler jsr199Compiler; - - @Mock - JctFlagBuilder flagBuilder; - - @BeforeEach - void setUp() { - staticMock.when(() -> compile(any(), any(), any(), any(), any())) - .thenCallRealMethod(); - } - - JctCompilationImpl doCompile(@Nullable Collection classNames) { - return compile(workspace, compiler, jsr199Compiler, flagBuilder, classNames); - } - - @DisplayName("the writer is built using the compiler") - @MethodSource( - "io.github.ascopes.jct.tests.unit.compilers.impl.JctJsr199InteropTest#explicitClassesArgs" - ) - @ParameterizedTest(name = "for classes = {0}") - @SuppressWarnings("resource") - void writerIsBuiltUsingCompiler(@Nullable Collection classes) { - // When - doCompile(classes); - - // Then - staticMock.verify(() -> buildWriter(compiler)); - } - - @DisplayName("errors while building writers get re-raised") - @MethodSource( - "io.github.ascopes.jct.tests.unit.compilers.impl.JctJsr199InteropTest#explicitClassesArgs" - ) - @ParameterizedTest(name = "for classes = {0}") - @SuppressWarnings("resource") - void errorsBuildingWritersAreReraised(@Nullable Collection classes) { - // Given - var ex = someUncheckedException(); - staticMock.when(() -> buildWriter(any())) - .thenThrow(ex); - - // Then - assertThatThrownBy(() -> doCompile(classes)) - .isInstanceOf(JctCompilerException.class) - .hasMessage("Failed to compile due to an error: %s", ex) - .hasCause(ex); - } - - @DisplayName("the writer is NOT closed after usage") - @MethodSource( - "io.github.ascopes.jct.tests.unit.compilers.impl.JctJsr199InteropTest#explicitClassesArgs" - ) - @ParameterizedTest(name = "for classes = {0}") - @SuppressWarnings("resource") - void writerIsNotClosedAfterUsage(@Nullable Collection classes) throws IOException { - // DO NOT CLOSE THE WRITER, IT IS ATTACHED TO SYSTEM.OUT. - // Closing SYSTEM.OUT causes IntelliJ to abort the entire test runner. - // See https://youtrack.jetbrains.com/issue/IDEA-120628 - // Other platforms may see other weird behaviour if we do this (Surefire, for example). - - // Given - var writer = mock(TeeWriter.class); - staticMock.when(() -> buildWriter(any())).thenReturn(writer); - - // When - doCompile(classes); - - // Then - verify(writer, never()).close(); - } - - @DisplayName("the file manager is built using the compiler and workspace") - @MethodSource( - "io.github.ascopes.jct.tests.unit.compilers.impl.JctJsr199InteropTest#explicitClassesArgs" - ) - @ParameterizedTest(name = "for classes = {0}") - void fileManagerIsBuiltUsingCompilerAndWorkspace(@Nullable Collection classes) { - // When - doCompile(classes); - - // Then - staticMock.verify(() -> buildFileManager(compiler, workspace)); - } - - @DisplayName("errors while building file managers get re-raised") - @MethodSource( - "io.github.ascopes.jct.tests.unit.compilers.impl.JctJsr199InteropTest#explicitClassesArgs" - ) - @ParameterizedTest(name = "for classes = {0}") - void errorsBuildingFileManagersAreReraised(@Nullable Collection classes) { - // Given - var ex = someUncheckedException(); - staticMock.when(() -> buildFileManager(any(), any())) - .thenThrow(ex); - - // Then - assertThatThrownBy(() -> doCompile(classes)) - .isInstanceOf(JctCompilerException.class) - .hasMessage("Failed to compile due to an error: %s", ex) - .hasCause(ex); - } - - @DisplayName("the file manager is closed after usage") - @MethodSource( - "io.github.ascopes.jct.tests.unit.compilers.impl.JctJsr199InteropTest#explicitClassesArgs" - ) - @ParameterizedTest(name = "for classes = {0}") - void fileManagerIsClosedAfterUsage(@Nullable Collection classes) { - // Given - var fileManager = mock(JctFileManagerImpl.class); - staticMock.when(() -> buildFileManager(any(), any())) - .thenReturn(fileManager); - - // When - doCompile(classes); - - // Then - verify(fileManager).close(); - } - - @DisplayName("the flags are built using the compiler and flag builder") - @MethodSource( - "io.github.ascopes.jct.tests.unit.compilers.impl.JctJsr199InteropTest#explicitClassesArgs" - ) - @ParameterizedTest(name = "for classes = {0}") - void flagsAreBuiltUsingCompilerAndFlagBuilder(@Nullable Collection classes) { - // When - doCompile(classes); - - // Then - staticMock.verify(() -> buildFlags(compiler, flagBuilder)); - } - - @DisplayName("errors while building flags get re-raised") - @MethodSource( - "io.github.ascopes.jct.tests.unit.compilers.impl.JctJsr199InteropTest#explicitClassesArgs" - ) - @ParameterizedTest(name = "for classes = {0}") - void errorsBuildingFlagsAreReraised(@Nullable Collection classes) { - // Given - var ex = someUncheckedException(); - staticMock.when(() -> buildFlags(any(), any())) - .thenThrow(ex); - - // Then - assertThatThrownBy(() -> doCompile(classes)) - .isInstanceOf(JctCompilerException.class) - .hasMessage("Failed to compile due to an error: %s", ex) - .hasCause(ex); - } - - @DisplayName("the diagnostic listener is built using the compiler") - @MethodSource( - "io.github.ascopes.jct.tests.unit.compilers.impl.JctJsr199InteropTest#explicitClassesArgs" - ) - @ParameterizedTest(name = "for classes = {0}") - void diagnosticListenerIsBuiltUsingCompiler(@Nullable Collection classes) { - // When - doCompile(classes); - - // Then - staticMock.verify(() -> buildDiagnosticListener(compiler)); - } - - @DisplayName("errors while building diagnostic listeners get re-raised") - @MethodSource( - "io.github.ascopes.jct.tests.unit.compilers.impl.JctJsr199InteropTest#explicitClassesArgs" - ) - @ParameterizedTest(name = "for classes = {0}") - void errorsBuildingDiagnosticListenersAreReraised(@Nullable Collection classes) { - // Given - var ex = someUncheckedException(); - staticMock.when(() -> buildDiagnosticListener(any())) - .thenThrow(ex); - - // Then - assertThatThrownBy(() -> doCompile(classes)) - .isInstanceOf(JctCompilerException.class) - .hasMessage("Failed to compile due to an error: %s", ex) - .hasCause(ex); - } - - @DisplayName("compilation units are discovered using the file manager") - @MethodSource( - "io.github.ascopes.jct.tests.unit.compilers.impl.JctJsr199InteropTest#explicitClassesArgs" - ) - @ParameterizedTest(name = "for classes = {0}") - void compilationUnitsAreDiscoveredUsingTheFileManager(@Nullable Collection classes) { - // Given - var fileManager = mock(JctFileManagerImpl.class); - staticMock.when(() -> buildFileManager(any(), any())) - .thenReturn(fileManager); - - // When - doCompile(classes); - - // Then - staticMock.verify(() -> findCompilationUnits(fileManager)); - } - - @DisplayName("errors finding compilation units are reraised") - @MethodSource( - "io.github.ascopes.jct.tests.unit.compilers.impl.JctJsr199InteropTest#explicitClassesArgs" - ) - @ParameterizedTest(name = "for classes = {0}") - void errorsFindingCompilationUnitsAreReraised(@Nullable Collection classes) { - // Given - var ex = someIoException(); - staticMock.when(() -> findCompilationUnits(any())) - .thenThrow(ex); - - // Then - assertThatThrownBy(() -> doCompile(classes)) - .isInstanceOf(JctCompilerException.class) - .hasMessage("Failed to compile due to an error: %s", ex) - .hasCause(ex); - } - - @DisplayName("performCompilerPass is called with the expected arguments") - @MethodSource( - "io.github.ascopes.jct.tests.unit.compilers.impl.JctJsr199InteropTest#explicitClassesArgs" - ) - @ParameterizedTest(name = "for classes = {0}") - @SuppressWarnings("resource") - void performCompilerPassCalledWithExpectedArguments(@Nullable Collection classes) { - // Given - var flags = someFlags(); - staticMock.when(() -> buildFlags(any(), any())) - .thenReturn(flags); - - var diagnosticListener = mockRaw(TracingDiagnosticListener.class) - .>upcastedTo() - .build(); - staticMock.when(() -> buildDiagnosticListener(any())) - .thenReturn(diagnosticListener); - - var writer = mock(TeeWriter.class); - staticMock.when(() -> buildWriter(any())) - .thenReturn(writer); - - var fileManager = mock(JctFileManagerImpl.class); - staticMock.when(() -> buildFileManager(any(), any())) - .thenReturn(fileManager); - - var compilationUnits = someCompilationUnits(); - staticMock.when(() -> findCompilationUnits(any())) - .thenReturn(compilationUnits); - - // When - doCompile(classes); - - // Then - staticMock.verify(() -> performCompilerPass( - compiler, - jsr199Compiler, - writer, - flags, - fileManager, - diagnosticListener, - compilationUnits, - classes - )); - } - - @DisplayName("errors performing the compilation pass units are reraised") - @MethodSource( - "io.github.ascopes.jct.tests.unit.compilers.impl.JctJsr199InteropTest#explicitClassesArgs" - ) - @ParameterizedTest(name = "for classes = {0}") - void errorsPerformingTheCompilationPassAreReraised(@Nullable Collection classes) { - // Given - var ex = someUncheckedException(); - staticMock - .when(() -> performCompilerPass(any(), any(), any(), any(), any(), any(), any(), any())) - .thenThrow(ex); - - // Then - assertThatThrownBy(() -> doCompile(classes)) - .isInstanceOf(JctCompilerException.class) - .hasMessage("Failed to compile due to an error: %s", ex) - .hasCause(ex); - } - - @DisplayName("compilation results are returned") - @SuppressWarnings("resource") - @ValueSource(booleans = {true, false}) - @ParameterizedTest(name = "for a compilation returning {0}") - void compilationResultsAreReturned(boolean result) { - // Given - var failOnWarnings = someBoolean(); - when(compiler.isFailOnWarnings()).thenReturn(failOnWarnings); - - var flags = someFlags(); - staticMock.when(() -> buildFlags(any(), any())) - .thenReturn(flags); - - var diagnosticListener = mockRaw(TracingDiagnosticListener.class) - .>upcastedTo() - .build(); - staticMock.when(() -> buildDiagnosticListener(any())) - .thenReturn(diagnosticListener); - - var diagnostics = someTraceDiagnostics(); - when(diagnosticListener.getDiagnostics()) - .thenReturn(diagnostics); - - var writer = mock(TeeWriter.class); - staticMock.when(() -> buildWriter(any())) - .thenReturn(writer); - - var outputLines = someLinesOfText(); - when(writer.toString()) - .thenReturn(outputLines); - - var fileManager = mock(JctFileManagerImpl.class); - staticMock.when(() -> buildFileManager(any(), any())) - .thenReturn(fileManager); - - var compilationUnits = someCompilationUnits(); - staticMock.when(() -> findCompilationUnits(any())) - .thenReturn(compilationUnits); - - staticMock - .when(() -> performCompilerPass(any(), any(), any(), any(), any(), any(), any(), any())) - .thenReturn(result); - - // When - var compilation = doCompile(null); - - // Then - assertThat(compilation) - .as("compilation") - .isNotNull(); - - assertSoftly(softly -> { - softly.assertThat(compilation.getCompilationUnits()) - .as(".compilationUnits") - .isInstanceOf(Set.class) - .containsExactlyInAnyOrderElementsOf(compilationUnits); - - softly.assertThat(compilation.getDiagnostics()) - .as(".diagnostics") - .containsExactlyElementsOf(diagnostics); - - softly.assertThat(compilation.getFileManager()) - .as(".fileManager") - .isSameAs(fileManager); - - softly.assertThat(compilation.getOutputLines()) - .as(".outputLines") - .containsExactly(outputLines.split("\n")); - - softly.assertThat(compilation.isSuccessful()) - .as(".successful") - .isEqualTo(result); - - softly.assertThat(compilation.isFailOnWarnings()) - .as(".failOnWarnings") - .isEqualTo(failOnWarnings); - }); - } - - @SuppressWarnings("resource") - @DisplayName("errors extracting the writer lines are reraised") - @MethodSource( - "io.github.ascopes.jct.tests.unit.compilers.impl.JctJsr199InteropTest#explicitClassesArgs" - ) - @ParameterizedTest(name = "for classes = {0}") - void errorsExtractingWriterLinesAreReraised(@Nullable Collection classes) { - // Given - var writer = mock(TeeWriter.class); - staticMock.when(() -> buildWriter(any())) - .thenReturn(writer); - - var ex = someUncheckedException(); - when(writer.toString()) - .thenThrow(ex); - - // Then - assertThatThrownBy(() -> doCompile(classes)) - .isInstanceOf(JctCompilerException.class) - .hasMessage("Failed to compile due to an error: %s", ex) - .hasCause(ex); - } - } - - ///////////////////////// - /// .buildWriter(...) /// - ///////////////////////// - - @DisplayName("JctJsr199Interop#buildWriter tests") - @ExtendWith(MockitoExtension.class) - @Nested - class BuildWriterTest { - - @Mock - JctCompiler compiler; - - @Mock - MockedStatic teeWriterCls; - - TeeWriter doBuildWriter() { - return buildWriter(compiler); - } - - @DisplayName(".buildWriter(...) initialises the writer and returns it") - @Test - void buildWriterInitialisesWriter() { - // Given - var charset = someCharset(); - when(compiler.getLogCharset()).thenReturn(charset); - - var expectedWriter = mock(TeeWriter.class); - teeWriterCls.when(() -> TeeWriter.wrap(any(), any())) - .thenReturn(expectedWriter); - - // When - var actualWriter = doBuildWriter(); - - // Then - teeWriterCls.verify(() -> TeeWriter.wrap(charset, System.out)); - teeWriterCls.verifyNoMoreInteractions(); - assertThat(actualWriter).isSameAs(expectedWriter); - } - } - - //////////////////////// - /// .buildFlags(...) /// - //////////////////////// - - @DisplayName("JctJsr199Interop#buildFlags tests") - @ExtendWith(MockitoExtension.class) - @Nested - class BuildFlagsTest { - - List expectedFlags; - - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - JctCompiler jctCompiler; - - @Mock(answer = Answers.RETURNS_SELF) - JctFlagBuilder flagBuilder; - - @BeforeEach - void setUp() { - expectedFlags = someFlags(); - - when(flagBuilder.build()) - .thenReturn(expectedFlags); - } - - List doBuildFlags() { - return buildFlags(jctCompiler, flagBuilder); - } - - @DisplayName(".buildFlags(...) builds the flags and returns them") - @Test - void buildFlagsBuildsTheFlags() { - // When - final var actualFlags = doBuildFlags(); - - // Then - verify(flagBuilder) - .annotationProcessorOptions(jctCompiler.getAnnotationProcessorOptions()); - verify(flagBuilder) - .showDeprecationWarnings(jctCompiler.isShowDeprecationWarnings()); - verify(flagBuilder) - .failOnWarnings(jctCompiler.isFailOnWarnings()); - verify(flagBuilder) - .compilerOptions(jctCompiler.getCompilerOptions()); - verify(flagBuilder) - .previewFeatures(jctCompiler.isPreviewFeatures()); - verify(flagBuilder) - .release(jctCompiler.getRelease()); - verify(flagBuilder) - .source(jctCompiler.getSource()); - verify(flagBuilder) - .target(jctCompiler.getTarget()); - verify(flagBuilder) - .verbose(jctCompiler.isVerbose()); - verify(flagBuilder) - .showWarnings(jctCompiler.isShowWarnings()); - verify(flagBuilder) - .build(); - verifyNoMoreInteractions(flagBuilder); - - assertThat(actualFlags).isSameAs(expectedFlags); - } - } - - ///////////////////////// - /// .buildFileManager /// - ///////////////////////// - - @DisplayName("JctJsr199Interop#buildFileManager tests") - @ExtendWith(MockitoExtension.class) - @Nested - @SuppressWarnings("resource") - class BuildFileManagerTest { - - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - MockedStatic staticMock; - - @Mock - MockedStatic fileManagerStaticMock; - - @Mock - MockedStatic proxyStaticMock; - - @Mock - JctFileManagerImpl fileManager; - - @Mock - JctCompiler compiler; - - @Mock - Workspace workspace; - - @BeforeEach - void setUp() { - staticMock.when(() -> JctJsr199Interop.buildFileManager(any(), any())) - .thenCallRealMethod(); - fileManagerStaticMock.when(() -> JctFileManagerImpl.forRelease(any())) - .thenReturn(fileManager); - when(compiler.getFileManagerLoggingMode()) - .thenReturn(LoggingMode.DISABLED); - } - - JctFileManager doBuild() { - return buildFileManager(compiler, workspace); - } - - @DisplayName("the release is determined using the compiler") - @Test - void releaseIsDeterminedUsingCompiler() { - // When - doBuild(); - - // Then - staticMock.verify(() -> determineRelease(compiler)); - } - - @DisplayName("the file manager is built using the determined release") - @Test - void fileManagerIsBuiltForTheDeterminedRelease() { - // Given - var release = someRelease(); - staticMock.when(() -> determineRelease(any())) - .thenReturn(release); - - // When - doBuild(); - - // Then - fileManagerStaticMock.verify(() -> JctFileManagerImpl.forRelease(release)); - } - - @DisplayName("file manager paths are configured in the correct order") - @Test - void fileManagerPathsAreConfiguredInTheCorrectOrder() { - // Given - var order = inOrder(JctJsr199Interop.class); - - // When - doBuild(); - - // Then - order.verify(staticMock, () -> configureWorkspacePaths(workspace, fileManager)); - order.verify(staticMock, () -> configureClassPath(compiler, fileManager)); - order.verify(staticMock, () -> configureModulePath(compiler, fileManager)); - order.verify(staticMock, () -> configurePlatformClassPath(compiler, fileManager)); - order.verify(staticMock, () -> configureJvmSystemModules(compiler, fileManager)); - order.verify(staticMock, () -> configureAnnotationProcessorPaths(compiler, fileManager)); - order.verify(staticMock, () -> configureRequiredLocations(workspace, fileManager)); - order.verifyNoMoreInteractions(); - } - - @DisplayName("no proxy is used when file manager logging is disabled") - @Test - void noProxyIsUsedWhenFileManagerLoggingIsDisabled() { - // Given - when(compiler.getFileManagerLoggingMode()).thenReturn(LoggingMode.DISABLED); - - // When - var actualFileManager = doBuild(); - - // Then - assertThat(actualFileManager).isSameAs(fileManager); - proxyStaticMock.verifyNoInteractions(); - } - - @DisplayName("a proxy is used when file manager logging is enabled") - @CsvSource({ - "ENABLED, false", - "STACKTRACES, true", - }) - @ParameterizedTest(name = "enable stacktraces = {1} when LoggingMode = {0}") - void proxyIsUsedWhenFileManagerLoggingIsEnabled( - LoggingMode loggingMode, - boolean enableStacktraces - ) { - // Given - when(compiler.getFileManagerLoggingMode()).thenReturn(loggingMode); - var proxyFileManger = mock(JctFileManager.class); - proxyStaticMock.when(() -> LoggingFileManagerProxy.wrap(any(), anyBoolean())) - .thenReturn(proxyFileManger); - - // When - var actualFileManager = doBuild(); - - // Then - proxyStaticMock.verify(() -> LoggingFileManagerProxy.wrap(fileManager, enableStacktraces)); - proxyStaticMock.verifyNoMoreInteractions(); - - assertThat(actualFileManager) - .isSameAs(proxyFileManger) - .isNotSameAs(fileManager); - } - } - - ///////////////////////// - /// .determineRelease /// - ///////////////////////// - - @DisplayName("JctJsr199Interop#determineRelease tests") - @Nested - class DetermineReleaseTest { - - @DisplayName("The correct release should be determined") - @CsvSource({ - // Preferring release - "12, , , 11, 12", - "12, , , 13, 12", - "12, , , 11, 12", - "12, 14, , 11, 12", - "12, 13, , 13, 12", - "12, 10, , 11, 12", - "12, , 8, 11, 12", - "12, , 9, 13, 12", - "12, , 10, 11, 12", - "12, 14, 11, 11, 12", - "12, 13, 13, 13, 12", - "12, 10, 14, 11, 12", - // Preferring target - " , , 8, 11, 8", - " , , 9, 13, 9", - " , , 10, 11, 10", - " , 14, 11, 10, 11", - " , 13, 13, 13, 13", - " , 10, 14, 11, 14", - // Preferring default release - " , , , 13, 13", - " , , , 11, 11", - " , 14, , 10, 10", - " , 13, , 17, 17", - }) - @ParameterizedTest( - name = "for release = {0}, source = {1}, target = {2}, defaultRelease = {3}, expect {4}" - ) - void theCorrectReleaseShouldBeDetermined( - String release, - String source, - String target, - String defaultRelease, - String expectedRelease - ) { - // Given - var compiler = mockRaw(JctCompiler.class) - .>upcastedTo() - .build(withSettings().strictness(Strictness.LENIENT)); - when(compiler.getRelease()).thenReturn(release); - when(compiler.getSource()).thenReturn(source); - when(compiler.getTarget()).thenReturn(target); - when(compiler.getDefaultRelease()).thenReturn(defaultRelease); - - // Then - assertThat(determineRelease(compiler)) - .isEqualTo(expectedRelease); - } - } - - //////////////////////////////// - /// .configureWorkspacePaths /// - //////////////////////////////// - - @DisplayName("JctJsr199Interop#configureWorkspacePaths tests") - @ExtendWith(MockitoExtension.class) - @Nested - class ConfigureWorkspacePathsTest { - - @Mock - Workspace workspace; - - @Mock - JctFileManagerImpl fileManager; - - void doConfigureWorkspacePaths() { - configureWorkspacePaths(workspace, fileManager); - } - - @DisplayName("all paths should be added to the file manager") - @Test - void allPathsShouldBeAddedToTheFileManager() { - // Given - var paths = Map.>of( - someLocation(), List.of(somePathRoot()), - someLocation(), List.of(somePathRoot(), somePathRoot()), - someLocation(), List.of(somePathRoot(), somePathRoot(), somePathRoot()), - someLocation(), List.of(somePathRoot()) - ); - when(workspace.getAllPaths()).thenReturn(paths); - - // When - doConfigureWorkspacePaths(); - - // Then - paths.forEach((location, roots) -> verify(fileManager).addPaths(location, roots)); - } - } - - /////////////////////////// - /// .configureClassPath /// - /////////////////////////// - - @DisplayName("JctJsr199Interop#configureClassPath tests") - @ExtendWith(MockitoExtension.class) - @Nested - class ConfigureClassPathTest { - - @Mock(answer = Answers.RETURNS_MOCKS) - MockedStatic staticMock; - - @Mock - MockedStatic specialLocationUtils; - - @Mock - JctCompiler compiler; - - @Mock - JctFileManagerImpl fileManager; - - MockedConstruction wrappingDirectory; - - @BeforeEach - void setUp() { - staticMock.when(() -> configureClassPath(any(), any())) - .thenCallRealMethod(); - - wrappingDirectory = mockConstruction( - WrappingDirectoryImpl.class, - (obj, ctx) -> when(obj.getPath()).thenReturn((Path) ctx.arguments().get(0)) - ); - } - - @AfterEach - void tearDown() { - wrappingDirectory.closeOnDemand(); - } - - void doConfigureClassPath() { - configureClassPath(compiler, fileManager); - } - - @DisplayName("Nothing is configured if classpath inheritance is disabled") - @Test - void nothingIsConfiguredIfClasspathInheritanceIsDisabled() { - // Given - when(compiler.isInheritClassPath()).thenReturn(false); - - // When - doConfigureClassPath(); - - // Then - verifyNoInteractions(fileManager); - } - - @DisplayName("Paths are registered when module path mismatch fixing is disabled") - @Test - void pathsAreRegisteredWhenModulePathMismatchFixingIsDisabled() { - // Given - var paths = Stream - .generate(Fixtures::somePath) - .limit(5) - .collect(Collectors.toList()); - specialLocationUtils.when(SpecialLocationUtils::currentClassPathLocations) - .thenReturn(paths); - - when(compiler.isInheritClassPath()) - .thenReturn(true); - when(compiler.isFixJvmModulePathMismatch()) - .thenReturn(false); - - // When - doConfigureClassPath(); - - // Then - var captor = ArgumentCaptor.forClass(WrappingDirectoryImpl.class); - verify(fileManager, times(5)) - .addPath(same(StandardLocation.CLASS_PATH), captor.capture()); - assertThat(captor.getAllValues()) - .allSatisfy(pathRoot -> assertThat(pathRoot.getPath()).isIn(paths)); - verifyNoMoreInteractions(fileManager); - } - - @DisplayName("Paths are registered when module path mismatch fixing is enabled") - @Test - void pathsAreRegisteredWhenModulePathMismatchFixingIsEnabled() { - // Given - var modulePaths = Stream - .generate(Fixtures::somePath) - .limit(3) - .collect(Collectors.toList()); - - var classPaths = Stream - .generate(Fixtures::somePath) - .limit(5) - .collect(Collectors.toList()); - - var paths = Stream.concat(modulePaths.stream(), classPaths.stream()) - .collect(Collectors.toList()); - - specialLocationUtils.when(SpecialLocationUtils::currentClassPathLocations) - .thenReturn(paths); - - when(compiler.isInheritClassPath()) - .thenReturn(true); - when(compiler.isFixJvmModulePathMismatch()) - .thenReturn(true); - - classPaths.forEach(path -> staticMock.when(() -> containsModules(path)).thenReturn(false)); - modulePaths.forEach(path -> staticMock.when(() -> containsModules(path)).thenReturn(true)); - - // When - doConfigureClassPath(); - - // Then - var classPathCaptor = ArgumentCaptor.forClass(WrappingDirectoryImpl.class); - verify(fileManager, times(8)) - .addPath(same(StandardLocation.CLASS_PATH), classPathCaptor.capture()); - assertThat(classPathCaptor.getAllValues()) - .allSatisfy(pathRoot -> assertThat(pathRoot.getPath()).isIn(paths)); - - var modulePathCaptor = ArgumentCaptor.forClass(WrappingDirectoryImpl.class); - verify(fileManager, times(3)) - .addPath(same(StandardLocation.MODULE_PATH), classPathCaptor.capture()); - assertThat(modulePathCaptor.getAllValues()) - .allSatisfy(pathRoot -> assertThat(pathRoot.getPath()).isIn(modulePaths)); - - verifyNoMoreInteractions(fileManager); - } - } - - //////////////////////// - /// .containsModules /// - //////////////////////// - - @DisplayName("JctJsr199Interop#containsModules tests") - @ExtendWith(MockitoExtension.class) - @Nested - class ContainsModulesTest { - - @Mock - MockedStatic moduleFinderStaticMock; - - @Mock - ModuleFinder moduleFinder; - - @BeforeEach - @SuppressWarnings("ResultOfMethodCallIgnored") - void setUp() { - moduleFinderStaticMock.when(() -> ModuleFinder.of(any())).thenReturn(moduleFinder); - } - - @DisplayName("ModuleFinder is initialised using the given path") - @SuppressWarnings("ResultOfMethodCallIgnored") - @Test - void moduleFinderIsInitialisedUsingTheGivenPath() { - // Given - var path = somePath(); - - // When - containsModules(path); - - // Then - moduleFinderStaticMock.verify(() -> ModuleFinder.of(path)); - moduleFinderStaticMock.verifyNoMoreInteractions(); - } - - @DisplayName("Expect true when modules exist") - @Test - void expectTrueWhenModulesExist() { - // Given - when(moduleFinder.findAll()).thenReturn(Set.of( - someModuleReference(), - someModuleReference(), - someModuleReference() - )); - - // When - var result = containsModules(somePath()); - - // Then - assertThat(result).isTrue(); - } - - @DisplayName("Expect false when modules do not exist") - @Test - void expectFalseWhenModulesDoNotExist() { - // Given - when(moduleFinder.findAll()).thenReturn(Set.of()); - - // When - var result = containsModules(somePath()); - - // Then - assertThat(result).isFalse(); - } - - @DisplayName("Expect false when errors resolving modules occur") - @Test - void expectFalseWhenErrorsResolvingModulesOccur() { - // Given - when(moduleFinder.findAll()).thenThrow(FindException.class); - - // When - var result = containsModules(somePath()); - - // Then - assertThat(result).isFalse(); - } - } - - //////////////////////////// - /// .configureModulePath /// - //////////////////////////// - - @DisplayName("JctJsr199Interop#configureModulePath tests") - @ExtendWith(MockitoExtension.class) - @Nested - class ConfigureModulePathTest { - - @Mock - MockedStatic specialLocationUtils; - - @Mock - JctCompiler compiler; - - @Mock - JctFileManagerImpl fileManager; - - MockedConstruction wrappingDirectory; - - @BeforeEach - void setUp() { - wrappingDirectory = mockConstruction( - WrappingDirectoryImpl.class, - (obj, ctx) -> when(obj.getPath()).thenReturn((Path) ctx.arguments().get(0)) - ); - } - - @AfterEach - void tearDown() { - wrappingDirectory.closeOnDemand(); - } - - void doConfigureModulePath() { - configureModulePath(compiler, fileManager); - } - - @DisplayName("Nothing is configured if module path inheritance is disabled") - @Test - void nothingIsConfiguredIfModulePathInheritanceIsDisabled() { - // Given - when(compiler.isInheritModulePath()).thenReturn(false); - - // When - doConfigureModulePath(); - - // Then - verifyNoInteractions(fileManager); - } - - @DisplayName("Paths are registered") - @Test - void pathsAreRegistered() { - // Given - var paths = Stream - .generate(Fixtures::somePath) - .limit(5) - .collect(Collectors.toList()); - - specialLocationUtils.when(SpecialLocationUtils::currentModulePathLocations) - .thenReturn(paths); - - when(compiler.isInheritModulePath()) - .thenReturn(true); - - // When - doConfigureModulePath(); - - // Then - var modulePathCaptor = ArgumentCaptor.forClass(WrappingDirectoryImpl.class); - var classPathCaptor = ArgumentCaptor.forClass(WrappingDirectoryImpl.class); - - verify(fileManager, times(5)) - .addPath(same(StandardLocation.MODULE_PATH), modulePathCaptor.capture()); - verify(fileManager, times(5)) - .addPath(same(StandardLocation.CLASS_PATH), classPathCaptor.capture()); - assertThat(modulePathCaptor.getAllValues()) - .allSatisfy(pathRoot -> assertThat(pathRoot.getPath()).isIn(paths)); - assertThat(classPathCaptor.getAllValues()) - .allSatisfy(pathRoot -> assertThat(pathRoot.getPath()).isIn(paths)); - verifyNoMoreInteractions(fileManager); - } - } - - /////////////////////////////////// - /// .configurePlatformClassPath /// - /////////////////////////////////// - - @DisplayName("JctJsr199Interop#configurePlatformClassPath tests") - @ExtendWith(MockitoExtension.class) - @Nested - class ConfigurePlatformClassPathTest { - - @Mock - MockedStatic specialLocationUtils; - - @Mock - JctCompiler compiler; - - @Mock - JctFileManagerImpl fileManager; - - MockedConstruction wrappingDirectory; - - @BeforeEach - void setUp() { - wrappingDirectory = mockConstruction( - WrappingDirectoryImpl.class, - (obj, ctx) -> when(obj.getPath()).thenReturn((Path) ctx.arguments().get(0)) - ); - } - - @AfterEach - void tearDown() { - wrappingDirectory.closeOnDemand(); - } - - void doConfigurePlatformClassPath() { - configurePlatformClassPath(compiler, fileManager); - } - - @DisplayName("Nothing is configured if platform classpath inheritance is disabled") - @Test - void nothingIsConfiguredIfPlatformClasspathInheritanceIsDisabled() { - // Given - when(compiler.isInheritPlatformClassPath()).thenReturn(false); - - // When - doConfigurePlatformClassPath(); - - // Then - verifyNoInteractions(fileManager); - } - - @DisplayName("Paths are registered") - @Test - void pathsAreRegistered() { - // Given - var paths = Stream - .generate(Fixtures::somePath) - .limit(5) - .collect(Collectors.toList()); - - specialLocationUtils.when(SpecialLocationUtils::currentPlatformClassPathLocations) - .thenReturn(paths); - - when(compiler.isInheritPlatformClassPath()) - .thenReturn(true); - - // When - doConfigurePlatformClassPath(); - - // Then - var captor = ArgumentCaptor.forClass(WrappingDirectoryImpl.class); - - verify(fileManager, times(5)) - .addPath(same(StandardLocation.PLATFORM_CLASS_PATH), captor.capture()); - assertThat(captor.getAllValues()) - .allSatisfy(pathRoot -> assertThat(pathRoot.getPath()).isIn(paths)); - verifyNoMoreInteractions(fileManager); - } - } - - ////////////////////////////////// - /// .configureJvmSystemModules /// - ////////////////////////////////// - - @DisplayName("JctJsr199Interop#configureJvmSystemModules tests") - @ExtendWith(MockitoExtension.class) - @Nested - class ConfigureJvmSystemModulesTest { - - @Mock - MockedStatic specialLocationUtils; - - @Mock - JctCompiler compiler; - - @Mock - JctFileManagerImpl fileManager; - - MockedConstruction wrappingDirectory; - - @BeforeEach - void setUp() { - wrappingDirectory = mockConstruction( - WrappingDirectoryImpl.class, - (obj, ctx) -> when(obj.getPath()).thenReturn((Path) ctx.arguments().get(0)) - ); - } - - @AfterEach - void tearDown() { - wrappingDirectory.closeOnDemand(); - } - - void doConfigureJvmSystemModules() { - configureJvmSystemModules(compiler, fileManager); - } - - @DisplayName("Nothing is configured if system module inheritance is disabled") - @Test - void nothingIsConfiguredIfSystemModuleInheritanceIsDisabled() { - // Given - when(compiler.isInheritSystemModulePath()).thenReturn(false); - - // When - doConfigureJvmSystemModules(); - - // Then - verifyNoInteractions(fileManager); - } - - @DisplayName("Paths are registered") - @Test - void pathsAreRegistered() { - // Given - var paths = Stream - .generate(Fixtures::somePath) - .limit(5) - .collect(Collectors.toList()); - - specialLocationUtils.when(SpecialLocationUtils::javaRuntimeLocations) - .thenReturn(paths); - - when(compiler.isInheritSystemModulePath()) - .thenReturn(true); - - // When - doConfigureJvmSystemModules(); - - // Then - var captor = ArgumentCaptor.forClass(WrappingDirectoryImpl.class); - - verify(fileManager, times(5)) - .addPath(same(StandardLocation.SYSTEM_MODULES), captor.capture()); - assertThat(captor.getAllValues()) - .allSatisfy(pathRoot -> assertThat(pathRoot.getPath()).isIn(paths)); - verifyNoMoreInteractions(fileManager); - } - } - - ////////////////////////////////////////// - /// .configureAnnotationProcessorPaths /// - ////////////////////////////////////////// - - @DisplayName("JctJsr199Interop#configureAnnotationProcessorPaths tests") - @ExtendWith(MockitoExtension.class) - @Nested - class ConfigureAnnotationProcessorPathsTest { - - @Mock(strictness = Mock.Strictness.LENIENT) - JctCompiler compiler; - - @Mock - JctFileManagerImpl fileManager; - - void doConfigureAnnotationProcessorPaths() { - configureAnnotationProcessorPaths(compiler, fileManager); - } - - @DisplayName("Ensure no containers are copied if annotation processing is disabled") - @EnumSource(AnnotationProcessorDiscovery.class) - @ParameterizedTest(name = "for discovery mode = {0}") - void ensureNoOperationsWhenAnnotationProcessingDisabled(AnnotationProcessorDiscovery discovery) { - // Given - when(compiler.getCompilationMode()) - .thenReturn(CompilationMode.COMPILATION_ONLY); - when(compiler.getAnnotationProcessorDiscovery()) - .thenReturn(discovery); - - // When - doConfigureAnnotationProcessorPaths(); - - // Then - verify(compiler).getCompilationMode(); - verifyNoMoreInteractions(compiler); - verifyNoInteractions(fileManager); - } - - @DisplayName("Ensure containers copied when AP discovery included with dependencies") - @EnumSource(value = CompilationMode.class, names = "COMPILATION_ONLY", mode = Mode.EXCLUDE) - @ParameterizedTest(name = "for compilation mode = {0}") - void ensureContainersAreCopiedWhenApDiscoveryIncludedWithDependencies(CompilationMode mode) { - // Given - when(compiler.getCompilationMode()) - .thenReturn(mode); - - when(compiler.getAnnotationProcessorDiscovery()) - .thenReturn(AnnotationProcessorDiscovery.INCLUDE_DEPENDENCIES); - - // When - doConfigureAnnotationProcessorPaths(); - - // Then - verify(fileManager) - .copyContainers(StandardLocation.CLASS_PATH, StandardLocation.ANNOTATION_PROCESSOR_PATH); - } - - @DisplayName("Ensure ANNOTATION_PROCESSOR_PATH exists when AP discovery enabled") - @CsvSource({ - "INCLUDE_DEPENDENCIES, COMPILATION_AND_ANNOTATION_PROCESSING", - "INCLUDE_DEPENDENCIES, ANNOTATION_PROCESSING_ONLY", - "ENABLED, COMPILATION_AND_ANNOTATION_PROCESSING", - "ENABLED, ANNOTATION_PROCESSING_ONLY", - }) - @ParameterizedTest(name = "when AnnotationProcessorDiscovery = {0} and compilation mode = {1}") - void ensureAnnotationProcessorPathExistsWhenApDiscoveryEnabled( - AnnotationProcessorDiscovery discovery, - CompilationMode mode - ) { - // Given - when(compiler.getCompilationMode()) - .thenReturn(mode); - when(compiler.getAnnotationProcessorDiscovery()) - .thenReturn(discovery); - - // When - doConfigureAnnotationProcessorPaths(); - - // Then - verify(fileManager).ensureEmptyLocationExists(StandardLocation.ANNOTATION_PROCESSOR_PATH); - } - - @DisplayName("Ensure no changes if AP discovery is disabled") - @EnumSource(value = CompilationMode.class, names = "COMPILATION_ONLY", mode = Mode.EXCLUDE) - @ParameterizedTest(name = "for compilation mode = {0}") - void ensureNoChangesIfApDiscoveryDisabled(CompilationMode compilationMode) { - // Given - when(compiler.getCompilationMode()) - .thenReturn(compilationMode); - when(compiler.getAnnotationProcessorDiscovery()) - .thenReturn(AnnotationProcessorDiscovery.DISABLED); - - // When - doConfigureAnnotationProcessorPaths(); - - // Then - verifyNoInteractions(fileManager); - } - } - - /////////////////////////////////// - /// .configureRequiredLocations /// - /////////////////////////////////// - - @DisplayName("JctJsr199Interop#configureRequiredLocations tests") - @ExtendWith(MockitoExtension.class) - @Nested - class ConfigureRequiredLocationsTest { - - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - Workspace workspace; - - @Mock - JctFileManagerImpl fileManager; - - void doConfigureRequiredLocations() { - configureRequiredLocations(workspace, fileManager); - } - - @DisplayName("The expected locations are created when not present") - @EnumSource( - value = StandardLocation.class, - names = { - "SOURCE_OUTPUT", - "CLASS_OUTPUT", - "NATIVE_HEADER_OUTPUT", - } - ) - @ParameterizedTest(name = "An empty path for {0} is created when it is not present") - void expectedLocationsAreCreated(Location expectedLocation) { - // Given - when(fileManager.hasLocation(any())).thenReturn(true); - when(fileManager.hasLocation(expectedLocation)).thenReturn(false); - - var expectedManagedDirectory = mock(ManagedDirectory.class); - when(workspace.createPackage(expectedLocation)).thenReturn(expectedManagedDirectory); - - // When - doConfigureRequiredLocations(); - - // Then - verify(workspace).createPackage(expectedLocation); - verify(fileManager).addPath(expectedLocation, expectedManagedDirectory); - verifyNoMoreInteractions(workspace); - } - - @DisplayName("No locations are created if all are present") - @Test - void noLocationsAreCreatedIfAllArePresent() { - // Given - when(fileManager.hasLocation(any())).thenReturn(true); - - // When - doConfigureRequiredLocations(); - - // Then - verifyNoInteractions(workspace); - verify(fileManager, atLeastOnce()).hasLocation(any()); - verifyNoMoreInteractions(fileManager); - } - } - - ///////////////////////////// - /// .findCompilationUnits /// - ///////////////////////////// - - @DisplayName("JctJsr199Interop#findCompilationUnits tests") - @ExtendWith(MockitoExtension.class) - @Nested - class FindCompilationUnitsTest { - - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - MockedStatic staticMock; - - @Mock - JctFileManagerImpl fileManager; - - @BeforeEach - void setUp() { - staticMock.when(() -> findCompilationUnits(any())) - .thenCallRealMethod(); - } - - List doFindCompilationUnits() throws IOException { - return findCompilationUnits(fileManager); - } - - @DisplayName("All compilation units are returned") - @Test - void allCompilationUnitsAreReturned() throws IOException { - // Given - var locationsAndFiles = Map.>of( - someLocation(), List.of(someJavaFileObject(), someJavaFileObject()), - someLocation(), List.of(), - someLocation(), List.of(someJavaFileObject(), someJavaFileObject(), someJavaFileObject()), - someLocation(), List.of(someJavaFileObject(), someJavaFileObject(), someJavaFileObject()), - someLocation(), List.of(someJavaFileObject()) - ); - - staticMock.when(() -> findCompilationUnitLocations(any())) - .thenReturn(List.copyOf(locationsAndFiles.keySet())); - - for (var location : locationsAndFiles.keySet()) { - when(fileManager.list(same(location), any(), any(), anyBoolean())) - .thenReturn(locationsAndFiles.get(location)); - } - - // When - var actualFiles = doFindCompilationUnits(); - - // Then - for (var location : locationsAndFiles.keySet()) { - verify(fileManager).list(location, "", Set.of(Kind.SOURCE), true); - } - - assertThat(actualFiles) - .containsExactlyInAnyOrderElementsOf(flatten(locationsAndFiles.values())); - - verifyNoMoreInteractions(fileManager); - } - } - - ///////////////////////////////////// - /// .findCompilationUnitLocations /// - ///////////////////////////////////// - - @DisplayName("JctJsr199Interop#findCompilationUnitLocations tests") - @ExtendWith(MockitoExtension.class) - @Nested - class FindCompilationUnitLocationsTest { - - @Mock - JctFileManagerImpl fileManager; - - List doFindCompilationUnitLocations() throws IOException { - return findCompilationUnitLocations(fileManager); - } - - @DisplayName("Modules are returned if present") - @Test - void modulesAreReturnedIfPresent() throws IOException { - // Given - var module1 = someModuleLocation("module1"); - var module2 = someModuleLocation("module2"); - var module3 = someModuleLocation("module3"); - var module4 = someModuleLocation("module4"); - var module5 = someModuleLocation("module5"); - var module6 = someModuleLocation("module6"); - var module7 = someModuleLocation("module7"); - - var listLocationsForModulesResult = List.>of( - Set.of(module1, module2, module3), - Set.of(), - Set.of(module4), - Set.of(), - Set.of(module5, module6), - Set.of(module7) - ); - - when(fileManager.listLocationsForModules(StandardLocation.MODULE_SOURCE_PATH)) - .thenReturn(listLocationsForModulesResult); - - // When - var result = doFindCompilationUnitLocations(); - - // Then - verify(fileManager).listLocationsForModules(StandardLocation.MODULE_SOURCE_PATH); - verifyNoMoreInteractions(fileManager); - assertThat(result) - .containsExactlyInAnyOrder(module1, module2, module3, module4, module5, module6, module7); - } - - @DisplayName("Source path is not returned if modules are present") - @Test - void sourcePathIsNotReturnedIfModulesArePresent() throws IOException { - // Given - var module1 = someModuleLocation("module1"); - var module2 = someModuleLocation("module2"); - var module3 = someModuleLocation("module3"); - var module4 = someModuleLocation("module4"); - var module5 = someModuleLocation("module5"); - var module6 = someModuleLocation("module6"); - var module7 = someModuleLocation("module7"); - - var listLocationsForModulesResult = List.>of( - Set.of(module1, module2, module3), - Set.of(), - Set.of(module4), - Set.of(), - Set.of(module5, module6), - Set.of(module7) - ); - - when(fileManager.listLocationsForModules(StandardLocation.MODULE_SOURCE_PATH)) - .thenReturn(listLocationsForModulesResult); - - // When - var result = doFindCompilationUnitLocations(); - - // Then - assertThat(result).doesNotContain(StandardLocation.SOURCE_PATH); - } - - @DisplayName("Source path is returned if no modules are present") - @Test - void sourcePathIsReturnedIfNoModulesArePresent() throws IOException { - when(fileManager.listLocationsForModules(StandardLocation.MODULE_SOURCE_PATH)) - .thenReturn(List.of()); - - // When - var result = doFindCompilationUnitLocations(); - - // Then - assertThat(result) - .hasSize(1) - .containsExactly(StandardLocation.SOURCE_PATH); - } - - ModuleLocation someModuleLocation(String name) { - return new ModuleLocation(StandardLocation.MODULE_SOURCE_PATH, name); - } - } - - //////////////////////////////// - /// .buildDiagnosticListener /// - //////////////////////////////// - - @DisplayName("JctJsr199Interop#buildDiagnosticListener tests") - @ExtendWith(MockitoExtension.class) - @Nested - class BuildDiagnosticListenerTest { - - @Mock - JctCompiler compiler; - - TracingDiagnosticListener doBuildDiagnosticListener() { - return buildDiagnosticListener(compiler); - } - - @DisplayName("TracingDiagnosticListener is initialised with the expected arguments") - @CsvSource({ - "STACKTRACES, true, true", - " ENABLED, true, false", - " DISABLED, false, false" - }) - @ParameterizedTest(name = "LoggingMode.{0} implies logging = {1}, stackTraces = {2}") - void tracingDiagnosticListenerIsInitialisedWithExpectedArguments( - LoggingMode loggingMode, - boolean logging, - boolean stackTraces - ) { - // Given - when(compiler.getDiagnosticLoggingMode()).thenReturn(loggingMode); - - // When - var listener = doBuildDiagnosticListener(); - - // Then - assertSoftly(softly -> { - softly.assertThat(listener.isLoggingEnabled()) - .as("listener.isLoggingEnabled()") - .isEqualTo(logging); - softly.assertThat(listener.isStackTraceReportingEnabled()) - .as("listener.isStackTraceReportingEnabled()") - .isEqualTo(stackTraces); - }); - } - } - - //////////////////////////// - /// .performCompilerPass /// - //////////////////////////// - - @DisplayName("JctJsr199Interop#performCompilerPass tests") - @ExtendWith(MockitoExtension.class) - @Nested - class PerformCompilerPassTest { - - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - MockedStatic staticMock; - - @Mock(name = "compiler that is mocked with Mockito") - JctCompiler compiler; - - @Mock(strictness = Mock.Strictness.LENIENT) - JavaCompiler jctJsr199Compiler; - - @Mock - TeeWriter writer; - - List flags; - - @Mock - JctFileManager fileManager; - - @Mock - TracingDiagnosticListener tracingDiagnosticListener; - - List compilationUnits; - - @Mock - CompilationTask task; - - @BeforeEach - void setUp() { - staticMock - .when(() -> performCompilerPass(any(), any(), any(), any(), any(), any(), any(), any())) - .thenCallRealMethod(); - when(jctJsr199Compiler.getTask(any(), any(), any(), any(), any(), any())) - .thenReturn(task); - flags = someFlags(); - compilationUnits = someCompilationUnits(); - } - - boolean doPerformCompilerPass(@Nullable Collection classes) { - return performCompilerPass( - compiler, - jctJsr199Compiler, - writer, - flags, - fileManager, - tracingDiagnosticListener, - compilationUnits, - classes - ); - } - - @DisplayName("the compilation task is initialised in the correct order before being called") - @MethodSource( - "io.github.ascopes.jct.tests.unit.compilers.impl.JctJsr199InteropTest#explicitClassesArgs" - ) - @ParameterizedTest(name = "for classes = {0}") - void theCompilationTaskIsInitialisedInTheCorrectOrderBeforeBeingCalled( - @Nullable Collection classes - ) { - // Given - var locale = someLocale(); - when(compiler.getLocale()).thenReturn(locale); - when(task.call()).thenReturn(true); - var orderedMock = inOrder(jctJsr199Compiler, JctJsr199Interop.class, task); - - // When - doPerformCompilerPass(classes); - - // Then - orderedMock.verify(jctJsr199Compiler).getTask( - writer, - fileManager, - tracingDiagnosticListener, - flags, - classes, - compilationUnits - ); - orderedMock.verify(task).setLocale(locale); - orderedMock.verify(staticMock, () -> configureAnnotationProcessorDiscovery(compiler, task)); - orderedMock.verify(task).call(); - orderedMock.verifyNoMoreInteractions(); - } - - @DisplayName("Compilations return the result") - @ValueSource(booleans = {true, false}) - @ParameterizedTest(name = "when calling the compilation task returns {0}") - void compilationsReturnTheResult(boolean expectedResult) { - // Given - when(task.call()).thenReturn(expectedResult); - - // When - var actualResult = doPerformCompilerPass(null); - - // Then - assertThat(actualResult).isEqualTo(expectedResult); - verify(task).call(); - } - - @DisplayName("Buggy compilers returning null from task#call() raise an exception") - @Test - void buggyCompilersReturningNullFromTaskCallRaiseException() { - // Given - when(task.call()).thenReturn(null); - - // Then - assertThatThrownBy(() -> doPerformCompilerPass(null)) - .isInstanceOf(JctCompilerException.class) - .hasNoCause() - .hasMessage( - "Compiler \"%s\" failed to produce a valid result, this is a bug in the " - + "compiler implementation, please report it to the compiler vendor!", - compiler - ) - .hasNoSuppressedExceptions(); - } - - @DisplayName("Exceptions thrown by the compiler are wrapped and reraised") - @Test - void exceptionsThrownByTheCompilerAreWrappedAndReraised() { - // Given - var cause = someUncheckedException(); - when(task.call()).thenThrow(cause); - - // Then - assertThatThrownBy(() -> doPerformCompilerPass(null)) - .isInstanceOf(JctCompilerException.class) - .hasCause(cause) - .hasMessage( - "Compiler \"%s\" raised an unhandled exception", - compiler - ) - .hasNoSuppressedExceptions(); - } - } - - ////////////////////////////////////////////// - /// .configureAnnotationProcessorDiscovery /// - ////////////////////////////////////////////// - - @DisplayName("JctJsr199Interop#configureAnnotationProcessorDiscovery tests") - @ExtendWith(MockitoExtension.class) - @Nested - class ConfigureAnnotationProcessorDiscoveryTest { - - @Mock - JctCompiler compiler; - - @Mock - CompilationTask task; - - void doConfigureAnnotationProcessorDiscovery() { - configureAnnotationProcessorDiscovery(compiler, task); - } - - @DisplayName("Disable AP discovery if any Processors are provided") - @CsvSource({ - "ENABLED, COMPILATION_AND_ANNOTATION_PROCESSING, 1", - "ENABLED, ANNOTATION_PROCESSING_ONLY, 1", - "ENABLED, COMPILATION_AND_ANNOTATION_PROCESSING, 2", - "ENABLED, ANNOTATION_PROCESSING_ONLY, 2", - "ENABLED, COMPILATION_AND_ANNOTATION_PROCESSING, 3", - "ENABLED, ANNOTATION_PROCESSING_ONLY, 3", - "ENABLED, COMPILATION_AND_ANNOTATION_PROCESSING, 5", - "ENABLED, ANNOTATION_PROCESSING_ONLY, 5", - "ENABLED, COMPILATION_AND_ANNOTATION_PROCESSING, 10", - "ENABLED, ANNOTATION_PROCESSING_ONLY, 10", - "INCLUDE_DEPENDENCIES, COMPILATION_AND_ANNOTATION_PROCESSING, 1", - "INCLUDE_DEPENDENCIES, ANNOTATION_PROCESSING_ONLY, 1", - "INCLUDE_DEPENDENCIES, COMPILATION_AND_ANNOTATION_PROCESSING, 2", - "INCLUDE_DEPENDENCIES, ANNOTATION_PROCESSING_ONLY, 2", - "INCLUDE_DEPENDENCIES, COMPILATION_AND_ANNOTATION_PROCESSING, 3", - "INCLUDE_DEPENDENCIES, ANNOTATION_PROCESSING_ONLY, 3", - "INCLUDE_DEPENDENCIES, COMPILATION_AND_ANNOTATION_PROCESSING, 5", - "INCLUDE_DEPENDENCIES, ANNOTATION_PROCESSING_ONLY, 5", - "INCLUDE_DEPENDENCIES, COMPILATION_AND_ANNOTATION_PROCESSING, 10", - "INCLUDE_DEPENDENCIES, ANNOTATION_PROCESSING_ONLY, 10", - "DISABLED, COMPILATION_AND_ANNOTATION_PROCESSING, 1", - "DISABLED, ANNOTATION_PROCESSING_ONLY, 1", - "DISABLED, COMPILATION_AND_ANNOTATION_PROCESSING, 2", - "DISABLED, ANNOTATION_PROCESSING_ONLY, 2", - "DISABLED, COMPILATION_AND_ANNOTATION_PROCESSING, 3", - "DISABLED, ANNOTATION_PROCESSING_ONLY, 3", - "DISABLED, COMPILATION_AND_ANNOTATION_PROCESSING, 5", - "DISABLED, ANNOTATION_PROCESSING_ONLY, 5", - "DISABLED, COMPILATION_AND_ANNOTATION_PROCESSING, 10", - "DISABLED, ANNOTATION_PROCESSING_ONLY, 10", - }) - @ParameterizedTest( - name = "for {2} explicit processor(s) when compilation mode = {1} and discovery = {0}" - ) - void disableApDiscoveryIfAnyProcessorsAreProvidedExplicitly( - AnnotationProcessorDiscovery discovery, - CompilationMode compilationMode, - int processorCount - ) { - // Given - var processors = Stream.generate(Fixtures::someAnnotationProcessor) - .limit(processorCount) - .collect(Collectors.toList()); - - when(compiler.getCompilationMode()) - .thenReturn(compilationMode); - - when(compiler.getAnnotationProcessorDiscovery()) - .thenReturn(discovery); - when(compiler.getAnnotationProcessors()) - .thenReturn(processors); - - // When - doConfigureAnnotationProcessorDiscovery(); - - // Then - verify(task).setProcessors(processors); - verifyNoMoreInteractions(task); - verify(compiler).getAnnotationProcessorDiscovery(); - verify(compiler).getAnnotationProcessors(); - verifyNoMoreInteractions(compiler); - } - - @DisplayName("Do nothing when the compiler mode disables annotation processing") - @EnumSource(value = AnnotationProcessorDiscovery.class) - @ParameterizedTest(name = "for discovery mode {0}") - void ignoreAnnotationProcessing(AnnotationProcessorDiscovery discovery) { - // Given - when(compiler.getCompilationMode()) - .thenReturn(CompilationMode.COMPILATION_ONLY); - when(compiler.getAnnotationProcessorDiscovery()) - .thenReturn(discovery); - when(compiler.getAnnotationProcessors()) - .thenReturn(List.of()); - - // When - doConfigureAnnotationProcessorDiscovery(); - - // Then - verifyNoInteractions(task); - } - - @DisplayName("Enable AP discovery when no processors are provided and discovery is enabled") - @EnumSource( - value = AnnotationProcessorDiscovery.class, - mode = Mode.EXCLUDE, - names = {"DISABLED"} - ) - @ParameterizedTest(name = "for discovery mode {0}") - void enableApDiscovery(AnnotationProcessorDiscovery discovery) { - // Given - when(compiler.getAnnotationProcessorDiscovery()) - .thenReturn(discovery); - when(compiler.getAnnotationProcessors()) - .thenReturn(List.of()); - - // When - doConfigureAnnotationProcessorDiscovery(); - - // Then - verifyNoInteractions(task); - } - - @DisplayName("Disable AP discovery when no processors are provided and discovery is disabled") - @Test - void disableApDiscovery() { - // Given - when(compiler.getAnnotationProcessorDiscovery()) - .thenReturn(AnnotationProcessorDiscovery.DISABLED); - when(compiler.getAnnotationProcessors()) - .thenReturn(List.of()); - - // When - doConfigureAnnotationProcessorDiscovery(); - - // Then - verify(task).setProcessors(List.of()); - verifyNoMoreInteractions(task); - } - } - - static Stream> explicitClassesArgs() { - return Stream.of( - null, - Set.of("org.example.Foo", "org.example.Bar") - ); - } -} diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/javac/JavacJctCompilerImplTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/javac/JavacJctCompilerImplTest.java index 0bdee0519..7e6b47c44 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/javac/JavacJctCompilerImplTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/javac/JavacJctCompilerImplTest.java @@ -59,7 +59,7 @@ void compilersHaveTheExpectedCompilerFactory() { toolProviderMock.when(ToolProvider::getSystemJavaCompiler).thenReturn(jsr199Compiler); // When - var actualCompiler = compiler.getJsr199CompilerFactory().createCompiler(); + var actualCompiler = compiler.getCompilerFactory().createCompiler(); // Then toolProviderMock.verify(ToolProvider::getSystemJavaCompiler); @@ -73,7 +73,7 @@ void compilersHaveTheExpectedFlagBuilderFactory() { // Given try (var flagBuilderMock = mockConstruction(JavacJctFlagBuilderImpl.class)) { // When - var flagBuilder = compiler.getJctFlagBuilderFactory().createFlagBuilder(); + var flagBuilder = compiler.getFlagBuilderFactory().createFlagBuilder(); // Then assertThat(flagBuilderMock.constructed()).hasSize(1); diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/diagnostics/TeeWriterTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/diagnostics/TeeWriterTest.java index 2c736b37d..1b1540f43 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/diagnostics/TeeWriterTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/diagnostics/TeeWriterTest.java @@ -25,12 +25,9 @@ import static org.mockito.Mockito.times; import io.github.ascopes.jct.diagnostics.TeeWriter; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.OutputStream; import java.io.StringWriter; import java.io.Writer; -import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -40,27 +37,13 @@ * @author Ashley Scopes */ @DisplayName("TeeWriter tests") -@SuppressWarnings("resource") +@SuppressWarnings({"resource", "ConstantConditions"}) class TeeWriterTest { - @DisplayName("Null charsets are disallowed") - @Test - void nullCharsetsAreDisallowed() { - assertThatCode(() -> TeeWriter.wrap(null, new ByteArrayOutputStream())) - .isInstanceOf(NullPointerException.class); - } - - @DisplayName("Null output streams are disallowed") - @Test - void nullOutputStreamsAreDisallowed() { - assertThatCode(() -> TeeWriter.wrap(StandardCharsets.UTF_8, null)) - .isInstanceOf(NullPointerException.class); - } - @DisplayName("Null writers are disallowed") @Test void nullWritersAreDisallowed() { - assertThatCode(() -> TeeWriter.wrap(null)) + assertThatCode(() -> new TeeWriter(null)) .isInstanceOf(NullPointerException.class); } @@ -69,7 +52,7 @@ void nullWritersAreDisallowed() { void writeFailsIfWriterIsClosed() throws IOException { // Given var writer = new StringWriter(); - var tee = TeeWriter.wrap(writer); + var tee = new TeeWriter(writer); var text = someText(); tee.close(); @@ -89,7 +72,7 @@ void writeFailsIfWriterIsClosed() throws IOException { void writeDelegatesToTheWriter() throws IOException { // Given var writer = new StringWriter(); - var tee = TeeWriter.wrap(writer); + var tee = new TeeWriter(writer); var text = someText(); // When @@ -104,7 +87,7 @@ void writeDelegatesToTheWriter() throws IOException { void flushFailsIfTheWriterIsClosed() throws IOException { // Given var writer = mock(Writer.class); - var tee = TeeWriter.wrap(writer); + var tee = new TeeWriter(writer); tee.close(); clearInvocations(writer); @@ -120,8 +103,8 @@ void flushFailsIfTheWriterIsClosed() throws IOException { @Test void flushDelegatesToTheWriter() throws IOException { // Given - var writer = mock(OutputStream.class); - var tee = TeeWriter.wrap(StandardCharsets.UTF_8, writer); + var writer = mock(Writer.class); + var tee = new TeeWriter(writer); // When tee.flush(); @@ -138,7 +121,7 @@ void closeDelegatesToTheWriter() throws IOException { // Given var writer = mock(Writer.class); - try (var ignoredTee = TeeWriter.wrap(writer)) { + try (var ignoredTee = new TeeWriter(writer)) { // Do nothing } @@ -152,7 +135,7 @@ void closeDelegatesToTheWriter() throws IOException { @Test void closeIsIdempotent() throws IOException { var writer = mock(Writer.class); - var tee = TeeWriter.wrap(writer); + var tee = new TeeWriter(writer); for (var i = 0; i < 10; ++i) { tee.close(); @@ -166,8 +149,8 @@ void closeIsIdempotent() throws IOException { @Test void toStringShouldReturnTheBufferContent() throws IOException { // Given - var writer = mock(OutputStream.class); - var tee = TeeWriter.wrap(StandardCharsets.UTF_8, writer); + var writer = mock(Writer.class); + var tee = new TeeWriter(writer); // When tee.write("Hello, "); diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerAnnotationProcessorClassPathConfigurerTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerAnnotationProcessorClassPathConfigurerTest.java new file mode 100644 index 000000000..e6c66a4fe --- /dev/null +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerAnnotationProcessorClassPathConfigurerTest.java @@ -0,0 +1,150 @@ +/* + * 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.filemanagers.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import io.github.ascopes.jct.compilers.JctCompiler; +import io.github.ascopes.jct.filemanagers.AnnotationProcessorDiscovery; +import io.github.ascopes.jct.filemanagers.JctFileManager; +import io.github.ascopes.jct.filemanagers.config.JctFileManagerAnnotationProcessorClassPathConfigurer; +import javax.tools.StandardLocation; +import org.junit.jupiter.api.DisplayName; +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.CsvSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * {@link JctFileManagerAnnotationProcessorClassPathConfigurer} tests. + * + * @author Ashley Scopes + */ +@DisplayName("JctFileManagerAnnotationProcessorClassPathConfigurer tests") +@ExtendWith(MockitoExtension.class) +class JctFileManagerAnnotationProcessorClassPathConfigurerTest { + + @Mock + JctCompiler compiler; + + @Mock + JctFileManager fileManager; + + @InjectMocks + JctFileManagerAnnotationProcessorClassPathConfigurer configurer; + + @DisplayName(".configure(...) ensures an empty location exists when discovery is enabled") + @Test + void configureEnsuresAnEmptyLocationExistsWhenDiscoveryIsEnabled() { + // Given + when(compiler.getAnnotationProcessorDiscovery()) + .thenReturn(AnnotationProcessorDiscovery.ENABLED); + + // When + configurer.configure(fileManager); + + // Then + verify(fileManager).ensureEmptyLocationExists(StandardLocation.ANNOTATION_PROCESSOR_PATH); + verifyNoMoreInteractions(fileManager); + } + + @DisplayName(".configure(...) returns the file manager when discovery is enabled") + @Test + void configureReturnsTheFileManagerWhenDiscoveryIsEnabled() { + // Given + when(compiler.getAnnotationProcessorDiscovery()) + .thenReturn(AnnotationProcessorDiscovery.ENABLED); + + // When + var actualFileManager = configurer.configure(fileManager); + + // Then + assertThat(actualFileManager).isSameAs(fileManager); + } + + @DisplayName( + ".configure(...) ensures an empty location exists when discovery is enabled with dependencies" + ) + @Test + void configureEnsuresAnEmptyLocationExistsWhenDiscoveryIsEnabledWithDependencies() { + // Given + var ordered = inOrder(fileManager); + + when(compiler.getAnnotationProcessorDiscovery()) + .thenReturn(AnnotationProcessorDiscovery.INCLUDE_DEPENDENCIES); + + // When + configurer.configure(fileManager); + + // Then + ordered.verify(fileManager) + .copyContainers(StandardLocation.CLASS_PATH, StandardLocation.ANNOTATION_PROCESSOR_PATH); + ordered.verify(fileManager) + .ensureEmptyLocationExists(StandardLocation.ANNOTATION_PROCESSOR_PATH); + ordered.verifyNoMoreInteractions(); + } + + @DisplayName( + ".configure(...) returns the file manager when discovery is enabled with dependencies" + ) + @Test + void configureReturnsTheFileManagerWhenDiscoveryIsEnabledWithDependencies() { + // Given + when(compiler.getAnnotationProcessorDiscovery()) + .thenReturn(AnnotationProcessorDiscovery.INCLUDE_DEPENDENCIES); + + // When + var actualFileManager = configurer.configure(fileManager); + + // Then + assertThat(actualFileManager).isSameAs(fileManager); + } + + @DisplayName(".configure() raises an exception if discovery is disabled") + @Test + void configureRaisesAnExceptionIfDiscoveryIsDisabled() { + // Given + when(compiler.getAnnotationProcessorDiscovery()) + .thenReturn(AnnotationProcessorDiscovery.DISABLED); + + // Then + assertThatThrownBy(() -> configurer.configure(fileManager)) + .isInstanceOf(IllegalStateException.class); + } + + @DisplayName(".isEnabled() returns the expected value") + @CsvSource({ + "ENABLED, true", + "INCLUDE_DEPENDENCIES, true", + "DISABLED, false" + }) + @ParameterizedTest(name = "expect {1} when annotationProcessorDiscovery is {0}") + void isEnabledReturnsTheExpectedValue(AnnotationProcessorDiscovery discovery, boolean enabled) { + // Given + when(compiler.getAnnotationProcessorDiscovery()).thenReturn(discovery); + + // Then + assertThat(configurer.isEnabled()).isEqualTo(enabled); + } +} diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerConfigurerChainTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerConfigurerChainTest.java new file mode 100644 index 000000000..b32a34a1a --- /dev/null +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerConfigurerChainTest.java @@ -0,0 +1,197 @@ +/* + * 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.filemanagers.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.github.ascopes.jct.filemanagers.JctFileManager; +import io.github.ascopes.jct.filemanagers.config.JctFileManagerConfigurer; +import io.github.ascopes.jct.filemanagers.config.JctFileManagerConfigurerChain; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.stubbing.Answer; + +/** + * {@link JctFileManagerConfigurerChain} tests. + * + * @author Ashley Scopes + */ +@DisplayName("JctFileManagerConfigurerChain tests") +class JctFileManagerConfigurerChainTest { + + JctFileManagerConfigurerChain chain; + + @BeforeEach + void setUp() { + chain = new JctFileManagerConfigurerChain(); + } + + @DisplayName(".addFirst(...) prepends configurers") + @Test + void addFirstPrependsConfigurers() { + // Given + var configurer1 = mock(JctFileManagerConfigurer.class); + var configurer2 = mock(JctFileManagerConfigurer.class); + var configurer3 = mock(JctFileManagerConfigurer.class); + var configurer4 = mock(JctFileManagerConfigurer.class); + + // When + var actualConfigurers = chain + .addFirst(configurer1) + .addFirst(configurer2) + .addFirst(configurer3) + .addFirst(configurer4) + .list(); + + // Then + assertThat(actualConfigurers) + .containsExactly(configurer4, configurer3, configurer2, configurer1); + } + + @DisplayName(".addFirst(...) appends configurers") + @Test + void addLastAppendsConfigurers() { + // Given + var configurer1 = mock(JctFileManagerConfigurer.class); + var configurer2 = mock(JctFileManagerConfigurer.class); + var configurer3 = mock(JctFileManagerConfigurer.class); + var configurer4 = mock(JctFileManagerConfigurer.class); + + // When + var actualConfigurers = chain + .addLast(configurer1) + .addLast(configurer2) + .addLast(configurer3) + .addLast(configurer4) + .list(); + + // Then + assertThat(actualConfigurers) + .containsExactly(configurer1, configurer2, configurer3, configurer4); + } + + @DisplayName(".list() returns an immutable view") + @Test + void listReturnsAnImmutableView() { + // Given + var configurer1 = mock(JctFileManagerConfigurer.class); + var configurer2 = mock(JctFileManagerConfigurer.class); + var configurer3 = mock(JctFileManagerConfigurer.class); + var configurer4 = mock(JctFileManagerConfigurer.class); + + // When, Then + var actualConfigurers = chain + .addFirst(configurer1) + .addLast(configurer2) + .addFirst(configurer3) + .list(); + + assertThat(actualConfigurers) + .hasSize(3); + + assertThat(chain.addFirst(configurer4).list()) + .isNotSameAs(actualConfigurers) + .hasSize(4); + + assertThatThrownBy(() -> actualConfigurers.add(configurer4)) + .isInstanceOf(UnsupportedOperationException.class); + } + + @DisplayName(".configure(...) folds and applies all configurers in the given order") + @Test + void configureFoldsAndAppliesAllConfigurersInTheGivenOrder() { + // Given + var fileManager1 = mock(JctFileManager.class); + + var configurer1 = mock(JctFileManagerConfigurer.class); + when(configurer1.isEnabled()).thenReturn(true); + when(configurer1.configure(any())).then(returnParameter()); + + var configurer2 = mock(JctFileManagerConfigurer.class); + when(configurer2.isEnabled()).thenReturn(true); + when(configurer2.configure(any())).then(returnParameter()); + + // This one will return a different file manager in the transformation. + var fileManager2 = mock(JctFileManager.class); + var configurer3 = mock(JctFileManagerConfigurer.class); + when(configurer3.isEnabled()).thenReturn(true); + when(configurer3.configure(any())).thenReturn(fileManager2); + + // This one will return a different file manager in the transformation. + var fileManager3 = mock(JctFileManager.class); + var configurer4 = mock(JctFileManagerConfigurer.class); + when(configurer4.isEnabled()).thenReturn(true); + when(configurer4.configure(any())).thenReturn(fileManager3); + + // This one is not enabled, so should be skipped. + var configurer5 = mock(JctFileManagerConfigurer.class); + when(configurer5.isEnabled()).thenReturn(false); + + var configurer6 = mock(JctFileManagerConfigurer.class); + when(configurer6.isEnabled()).thenReturn(true); + when(configurer6.configure(any())).then(returnParameter()); + + var order = inOrder( + configurer1, configurer2, configurer3, configurer4, configurer5, configurer6 + ); + + chain + .addLast(configurer1) + .addLast(configurer2) + .addLast(configurer3) + .addLast(configurer4) + .addLast(configurer5) + .addLast(configurer6); + + // When + var resultFileManager = chain.configure(fileManager1); + + // Then + order.verify(configurer1).isEnabled(); + order.verify(configurer1).configure(fileManager1); + order.verify(configurer2).isEnabled(); + order.verify(configurer2).configure(fileManager1); + order.verify(configurer3).isEnabled(); + order.verify(configurer3).configure(fileManager1); + order.verify(configurer4).isEnabled(); + order.verify(configurer4).configure(fileManager2); + order.verify(configurer5).isEnabled(); + order.verify(configurer6).isEnabled(); + order.verify(configurer6).configure(fileManager3); + order.verifyNoMoreInteractions(); + + assertThat(resultFileManager) + .isSameAs(fileManager3); + } + + @SafeVarargs + @SuppressWarnings("unchecked") + static Answer returnParameter(T... sentinel) { + if (sentinel.length > 0) { + throw new IllegalArgumentException( + "varargs here are a hack to retrieve type info implicitly. " + + "Do not provide any arguments here." + ); + } + return ctx -> (T) ctx.getArgument(0, sentinel.getClass().getComponentType()); + } +} diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerConfigurerTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerConfigurerTest.java new file mode 100644 index 000000000..1051440ef --- /dev/null +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerConfigurerTest.java @@ -0,0 +1,45 @@ +/* + * 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.filemanagers.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.github.ascopes.jct.filemanagers.config.JctFileManagerConfigurer; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * {@link JctFileManagerConfigurer} tests. + * + * @author Ashley Scopes + */ +@DisplayName("JctFileManagerConfigurer tests") +@ExtendWith(MockitoExtension.class) +class JctFileManagerConfigurerTest { + @Mock(answer = Answers.CALLS_REAL_METHODS) + JctFileManagerConfigurer configurer; + + @DisplayName(".isEnabled() defaults to returning true") + @Test + void isEnabledReturnsTrue() { + // Then + assertThat(configurer.isEnabled()).isTrue(); + } +} diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerJvmClassPathConfigurerTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerJvmClassPathConfigurerTest.java new file mode 100644 index 000000000..8687e8fcb --- /dev/null +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerJvmClassPathConfigurerTest.java @@ -0,0 +1,112 @@ +/* + * 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.filemanagers.config; + +import static io.github.ascopes.jct.tests.helpers.Fixtures.somePath; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.github.ascopes.jct.compilers.JctCompiler; +import io.github.ascopes.jct.filemanagers.impl.JctFileManagerImpl; +import io.github.ascopes.jct.filemanagers.config.JctFileManagerJvmClassPathConfigurer; +import io.github.ascopes.jct.utils.SpecialLocationUtils; +import io.github.ascopes.jct.workspaces.impl.WrappingDirectoryImpl; +import java.util.List; +import javax.tools.StandardLocation; +import org.junit.jupiter.api.DisplayName; +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.ValueSource; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * {@link JctFileManagerJvmClassPathConfigurer} tests. + * + * @author Ashley Scopes + */ +@DisplayName("JctFileManagerJvmClassPathConfigurer tests") +@ExtendWith(MockitoExtension.class) +class JctFileManagerJvmClassPathConfigurerTest { + + @Mock + JctCompiler compiler; + + @Mock + JctFileManagerImpl fileManager; + + @Mock + MockedStatic specialLocationUtilsStatic; + + @InjectMocks + JctFileManagerJvmClassPathConfigurer configurer; + + @DisplayName(".configure(...) will configure the file manager with the JVM classpath") + @Test + void configureAddsTheClassPathToTheFileManager() { + // Given + var paths = List.of( + somePath(), + somePath(), + somePath(), + somePath() + ); + + specialLocationUtilsStatic.when(SpecialLocationUtils::currentClassPathLocations) + .thenReturn(paths); + + // When + configurer.configure(fileManager); + + // Then + var captor = ArgumentCaptor.forClass(WrappingDirectoryImpl.class); + + verify(fileManager, times(paths.size())) + .addPath(eq(StandardLocation.CLASS_PATH), captor.capture()); + + assertThat(captor.getAllValues()) + .map(WrappingDirectoryImpl::getPath) + .containsExactlyElementsOf(paths); + } + + @DisplayName(".configure(...) returns the input file manager") + @Test + void configureReturnsTheInputFileManager() { + // When + var result = configurer.configure(fileManager); + + // Then + assertThat(result).isSameAs(fileManager); + } + + @DisplayName(".isEnabled() returns the expected result") + @ValueSource(booleans = {true, false}) + @ParameterizedTest(name = "when JctCompiler.isInheritClassPath() returns {0}") + void isEnabledReturnsTheExpectedResult(boolean inheritClassPath) { + // Given + when(compiler.isInheritClassPath()).thenReturn(inheritClassPath); + + // Then + assertThat(configurer.isEnabled()).isEqualTo(inheritClassPath); + } +} diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerJvmClassPathModuleConfigurerTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerJvmClassPathModuleConfigurerTest.java new file mode 100644 index 000000000..1012a8aef --- /dev/null +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerJvmClassPathModuleConfigurerTest.java @@ -0,0 +1,126 @@ +/* + * 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.filemanagers.config; + +import static io.github.ascopes.jct.tests.helpers.Fixtures.somePath; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.github.ascopes.jct.compilers.JctCompiler; +import io.github.ascopes.jct.filemanagers.impl.JctFileManagerImpl; +import io.github.ascopes.jct.filemanagers.config.JctFileManagerJvmClassPathModuleConfigurer; +import io.github.ascopes.jct.utils.SpecialLocationUtils; +import io.github.ascopes.jct.workspaces.impl.WrappingDirectoryImpl; +import java.util.List; +import javax.tools.StandardLocation; +import org.junit.jupiter.api.DisplayName; +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.CsvSource; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mock.Strictness; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * {@link JctFileManagerJvmClassPathModuleConfigurer} tests. + * + * @author Ashley Scopes + */ +@DisplayName("JctFileManagerJvmClassPathModuleConfigurer tests") +@ExtendWith(MockitoExtension.class) +class JctFileManagerJvmClassPathModuleConfigurerTest { + + @Mock(strictness = Strictness.LENIENT) + JctCompiler compiler; + + @Mock + JctFileManagerImpl fileManager; + + @Mock + MockedStatic specialLocationUtilsStatic; + + @InjectMocks + JctFileManagerJvmClassPathModuleConfigurer configurer; + + @DisplayName( + ".configure(...) will configure the file manager with the modules from the JVM classpath" + ) + @Test + void configureAddsTheClassPathToTheFileManagerModulePath() { + // Given + var paths = List.of( + somePath(), + somePath(), + somePath(), + somePath(), + somePath() + ); + + specialLocationUtilsStatic.when(SpecialLocationUtils::currentClassPathLocations) + .thenReturn(paths); + + // When + configurer.configure(fileManager); + + // Then + var captor = ArgumentCaptor.forClass(WrappingDirectoryImpl.class); + + verify(fileManager, times(paths.size())) + .addPath(eq(StandardLocation.MODULE_PATH), captor.capture()); + + assertThat(captor.getAllValues()) + .map(WrappingDirectoryImpl::getPath) + .containsExactlyElementsOf(paths); + } + + @DisplayName(".configure(...) returns the input file manager") + @Test + void configureReturnsTheInputFileManager() { + // When + var result = configurer.configure(fileManager); + + // Then + assertThat(result).isSameAs(fileManager); + } + + @DisplayName(".isEnabled() returns the expected result") + @CsvSource({ + " true, true , true", + " true, false, false", + "false, true, false", + "false, false, false", + }) + @ParameterizedTest + void isEnabledReturnsTheExpectedResult( + boolean inheritClassPath, + boolean fixModulePathMismatch, + boolean expectedResult + ) { + // Given + when(compiler.isInheritClassPath()).thenReturn(inheritClassPath); + when(compiler.isFixJvmModulePathMismatch()).thenReturn(fixModulePathMismatch); + + // Then + assertThat(configurer.isEnabled()).isEqualTo(expectedResult); + } +} diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerJvmModulePathConfigurerTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerJvmModulePathConfigurerTest.java new file mode 100644 index 000000000..b1eaa0415 --- /dev/null +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerJvmModulePathConfigurerTest.java @@ -0,0 +1,124 @@ +/* + * 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.filemanagers.config; + +import static io.github.ascopes.jct.tests.helpers.Fixtures.somePath; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.github.ascopes.jct.compilers.JctCompiler; +import io.github.ascopes.jct.filemanagers.impl.JctFileManagerImpl; +import io.github.ascopes.jct.filemanagers.config.JctFileManagerJvmModulePathConfigurer; +import io.github.ascopes.jct.utils.SpecialLocationUtils; +import io.github.ascopes.jct.workspaces.impl.WrappingDirectoryImpl; +import java.util.List; +import javax.tools.StandardLocation; +import org.junit.jupiter.api.DisplayName; +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.ValueSource; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * {@link JctFileManagerJvmModulePathConfigurer} tests. + * + * @author Ashley Scopes + */ +@DisplayName("JctFileManagerJvmModulePathConfigurer tests") +@ExtendWith(MockitoExtension.class) +class JctFileManagerJvmModulePathConfigurerTest { + + @Mock + JctCompiler compiler; + + @Mock + JctFileManagerImpl fileManager; + + @Mock + MockedStatic specialLocationUtilsStatic; + + @InjectMocks + JctFileManagerJvmModulePathConfigurer configurer; + + @DisplayName(".configure(...) will configure the file manager with the JVM modulepath") + @Test + void configureAddsTheModulePathToTheFileManager() { + // Given + var paths = List.of( + somePath(), + somePath(), + somePath(), + somePath() + ); + + specialLocationUtilsStatic.when(SpecialLocationUtils::currentModulePathLocations) + .thenReturn(paths); + + // When + configurer.configure(fileManager); + + // Then + var classPathCaptor = ArgumentCaptor.forClass(WrappingDirectoryImpl.class); + + verify(fileManager, times(paths.size())) + .addPath(eq(StandardLocation.CLASS_PATH), classPathCaptor.capture()); + + var modulePathCaptor = ArgumentCaptor.forClass(WrappingDirectoryImpl.class); + + verify(fileManager, times(paths.size())) + .addPath(eq(StandardLocation.MODULE_PATH), modulePathCaptor.capture()); + + assertSoftly(softly -> { + softly.assertThat(classPathCaptor.getAllValues()) + .map(WrappingDirectoryImpl::getPath) + .containsExactlyElementsOf(paths); + + softly.assertThat(modulePathCaptor.getAllValues()) + .map(WrappingDirectoryImpl::getPath) + .containsExactlyElementsOf(paths); + }); + } + + @DisplayName(".configure(...) returns the input file manager") + @Test + void configureReturnsTheInputFileManager() { + // When + var result = configurer.configure(fileManager); + + // Then + assertThat(result).isSameAs(fileManager); + } + + @DisplayName(".isEnabled() returns the expected result") + @ValueSource(booleans = {true, false}) + @ParameterizedTest(name = "when JctCompiler.isInheritModulePath() returns {0}") + void isEnabledReturnsTheExpectedResult(boolean inheritModulePath) { + // Given + when(compiler.isInheritModulePath()).thenReturn(inheritModulePath); + + // Then + assertThat(configurer.isEnabled()).isEqualTo(inheritModulePath); + } +} diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerJvmPlatformClassPathConfigurerTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerJvmPlatformClassPathConfigurerTest.java new file mode 100644 index 000000000..2a0f83ffa --- /dev/null +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerJvmPlatformClassPathConfigurerTest.java @@ -0,0 +1,112 @@ +/* + * 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.filemanagers.config; + +import static io.github.ascopes.jct.tests.helpers.Fixtures.somePath; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.github.ascopes.jct.compilers.JctCompiler; +import io.github.ascopes.jct.filemanagers.impl.JctFileManagerImpl; +import io.github.ascopes.jct.filemanagers.config.JctFileManagerJvmPlatformClassPathConfigurer; +import io.github.ascopes.jct.utils.SpecialLocationUtils; +import io.github.ascopes.jct.workspaces.impl.WrappingDirectoryImpl; +import java.util.List; +import javax.tools.StandardLocation; +import org.junit.jupiter.api.DisplayName; +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.ValueSource; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * {@link JctFileManagerJvmPlatformClassPathConfigurer} tests. + * + * @author Ashley Scopes + */ +@DisplayName("JctFileManagerJvmPlatformClassPathConfigurer tests") +@ExtendWith(MockitoExtension.class) +class JctFileManagerJvmPlatformClassPathConfigurerTest { + + @Mock + JctCompiler compiler; + + @Mock + JctFileManagerImpl fileManager; + + @Mock + MockedStatic specialLocationUtilsStatic; + + @InjectMocks + JctFileManagerJvmPlatformClassPathConfigurer configurer; + + @DisplayName(".configure(...) will configure the file manager with the JVM platform class path") + @Test + void configureAddsThePlatformClassPathToTheFileManager() { + // Given + var paths = List.of( + somePath(), + somePath(), + somePath(), + somePath() + ); + + specialLocationUtilsStatic.when(SpecialLocationUtils::currentPlatformClassPathLocations) + .thenReturn(paths); + + // When + configurer.configure(fileManager); + + // Then + var captor = ArgumentCaptor.forClass(WrappingDirectoryImpl.class); + + verify(fileManager, times(paths.size())) + .addPath(eq(StandardLocation.PLATFORM_CLASS_PATH), captor.capture()); + + assertThat(captor.getAllValues()) + .map(WrappingDirectoryImpl::getPath) + .containsExactlyElementsOf(paths); + } + + @DisplayName(".configure(...) returns the input file manager") + @Test + void configureReturnsTheInputFileManager() { + // When + var result = configurer.configure(fileManager); + + // Then + assertThat(result).isSameAs(fileManager); + } + + @DisplayName(".isEnabled() returns the expected result") + @ValueSource(booleans = {true, false}) + @ParameterizedTest(name = "when JctCompiler.isInheritPlatformClassPath() returns {0}") + void isEnabledReturnsTheExpectedResult(boolean inheritPlatformClassPath) { + // Given + when(compiler.isInheritPlatformClassPath()).thenReturn(inheritPlatformClassPath); + + // Then + assertThat(configurer.isEnabled()).isEqualTo(inheritPlatformClassPath); + } +} diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerJvmSystemModulesConfigurerTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerJvmSystemModulesConfigurerTest.java new file mode 100644 index 000000000..95269fb19 --- /dev/null +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerJvmSystemModulesConfigurerTest.java @@ -0,0 +1,113 @@ +/* + * 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.filemanagers.config; + +import static io.github.ascopes.jct.tests.helpers.Fixtures.somePath; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.github.ascopes.jct.compilers.JctCompiler; +import io.github.ascopes.jct.filemanagers.impl.JctFileManagerImpl; +import io.github.ascopes.jct.filemanagers.config.JctFileManagerJvmPlatformClassPathConfigurer; +import io.github.ascopes.jct.filemanagers.config.JctFileManagerJvmSystemModulesConfigurer; +import io.github.ascopes.jct.utils.SpecialLocationUtils; +import io.github.ascopes.jct.workspaces.impl.WrappingDirectoryImpl; +import java.util.List; +import javax.tools.StandardLocation; +import org.junit.jupiter.api.DisplayName; +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.ValueSource; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * {@link JctFileManagerJvmPlatformClassPathConfigurer} tests. + * + * @author Ashley Scopes + */ +@DisplayName("JctFileManagerJvmSystemModulesConfigurer tests") +@ExtendWith(MockitoExtension.class) +class JctFileManagerJvmSystemModulesConfigurerTest { + + @Mock + JctCompiler compiler; + + @Mock + JctFileManagerImpl fileManager; + + @Mock + MockedStatic specialLocationUtilsStatic; + + @InjectMocks + JctFileManagerJvmSystemModulesConfigurer configurer; + + @DisplayName(".configure(...) will configure the file manager with the JVM system modules") + @Test + void configureAddsTheSystemModulesToTheFileManager() { + // Given + var paths = List.of( + somePath(), + somePath(), + somePath(), + somePath() + ); + + specialLocationUtilsStatic.when(SpecialLocationUtils::javaRuntimeLocations) + .thenReturn(paths); + + // When + configurer.configure(fileManager); + + // Then + var captor = ArgumentCaptor.forClass(WrappingDirectoryImpl.class); + + verify(fileManager, times(paths.size())) + .addPath(eq(StandardLocation.SYSTEM_MODULES), captor.capture()); + + assertThat(captor.getAllValues()) + .map(WrappingDirectoryImpl::getPath) + .containsExactlyElementsOf(paths); + } + + @DisplayName(".configure(...) returns the input file manager") + @Test + void configureReturnsTheInputFileManager() { + // When + var result = configurer.configure(fileManager); + + // Then + assertThat(result).isSameAs(fileManager); + } + + @DisplayName(".isEnabled() returns the expected result") + @ValueSource(booleans = {true, false}) + @ParameterizedTest(name = "when JctCompiler.isInheritSystemModules() returns {0}") + void isEnabledReturnsTheExpectedResult(boolean inheritSystemModules) { + // Given + when(compiler.isInheritSystemModulePath()).thenReturn(inheritSystemModules); + + // Then + assertThat(configurer.isEnabled()).isEqualTo(inheritSystemModules); + } +} diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerLoggingProxyConfigurerTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerLoggingProxyConfigurerTest.java new file mode 100644 index 000000000..ce0b802fe --- /dev/null +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerLoggingProxyConfigurerTest.java @@ -0,0 +1,135 @@ +/* + * 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.filemanagers.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.when; + +import io.github.ascopes.jct.compilers.JctCompiler; +import io.github.ascopes.jct.filemanagers.JctFileManager; +import io.github.ascopes.jct.filemanagers.LoggingFileManagerProxy; +import io.github.ascopes.jct.filemanagers.LoggingMode; +import io.github.ascopes.jct.filemanagers.config.JctFileManagerLoggingProxyConfigurer; +import io.github.ascopes.jct.filemanagers.impl.JctFileManagerImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +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.CsvSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.EnumSource.Mode; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mock.Strictness; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * {@link JctFileManagerLoggingProxyConfigurer} tests. + * + * @author Ashley Scopes + */ +@DisplayName("JctFileManagerLoggingProxyConfigurer tests") +@ExtendWith(MockitoExtension.class) +class JctFileManagerLoggingProxyConfigurerTest { + + @Mock(strictness = Strictness.LENIENT) + MockedStatic loggingFileManagerProxyStatic; + + @Mock + JctCompiler compiler; + + @Mock + JctFileManager proxiedFileManager; + + @Mock + JctFileManagerImpl fileManager; + + @InjectMocks + JctFileManagerLoggingProxyConfigurer configurer; + + @BeforeEach + void setUp() { + loggingFileManagerProxyStatic.when(() -> LoggingFileManagerProxy.wrap(any(), anyBoolean())) + .thenReturn(proxiedFileManager); + } + + @DisplayName(".configure(...) will wrap the file manager in a proxy") + @CsvSource({ + "STACKTRACES, true", + "ENABLED, false", + }) + @ParameterizedTest( + name = ".configure(...) for logging mode {0} will initialise a proxy with stacktraces = {0}" + ) + void configureWillCopyAllWorkspacePathsToTheFileManager(LoggingMode mode, boolean stacktraces) { + // Given + when(compiler.getFileManagerLoggingMode()).thenReturn(mode); + + // When + configurer.configure(fileManager); + + // Then + loggingFileManagerProxyStatic + .verify(() -> LoggingFileManagerProxy.wrap(fileManager, stacktraces)); + } + + @DisplayName(".configure(...) will raise an IllegalStateException if logging is disabled") + @Test + void configureThrowsIllegalStateExceptionIfLoggingDisabled() { + // Given + when(compiler.getFileManagerLoggingMode()).thenReturn(LoggingMode.DISABLED); + + // Then + assertThatThrownBy(() -> configurer.configure(fileManager)) + .isInstanceOf(IllegalStateException.class); + } + + @DisplayName(".configure(...) returns the proxied file manager") + @EnumSource(value = LoggingMode.class, names = "DISABLED", mode = Mode.EXCLUDE) + @ParameterizedTest(name = "for logging mode = {0}") + void configureReturnsTheProxiedFileManager(LoggingMode mode) { + // Given + when(compiler.getFileManagerLoggingMode()).thenReturn(mode); + + // When + var result = configurer.configure(fileManager); + + // Then + assertThat(result) + .isNotSameAs(fileManager) + .isSameAs(proxiedFileManager); + } + + @DisplayName(".isEnabled() returns the expected result") + @CsvSource({ + "STACKTRACES, true", + "ENABLED, true", + "DISABLED, false", + }) + @ParameterizedTest(name = "returns {1} when logging mode is {0}") + void isEnabledReturnsExpectedResult(LoggingMode mode, boolean enabled) { + // Given + when(compiler.getFileManagerLoggingMode()).thenReturn(mode); + + // Then + assertThat(configurer.isEnabled()).isEqualTo(enabled); + } +} diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerRequiredLocationsConfigurerTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerRequiredLocationsConfigurerTest.java new file mode 100644 index 000000000..2b18ddc95 --- /dev/null +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerRequiredLocationsConfigurerTest.java @@ -0,0 +1,121 @@ +/* + * 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.filemanagers.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.github.ascopes.jct.filemanagers.config.JctFileManagerRequiredLocationsConfigurer; +import io.github.ascopes.jct.filemanagers.impl.JctFileManagerImpl; +import io.github.ascopes.jct.workspaces.ManagedDirectory; +import io.github.ascopes.jct.workspaces.Workspace; +import javax.tools.StandardLocation; +import org.junit.jupiter.api.DisplayName; +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.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mock.Strictness; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * {@link JctFileManagerRequiredLocationsConfigurer} tests. + * + * @author Ashley Scopes + */ +@DisplayName("JctFileManagerRequiredLocationsConfigurer tests") +@ExtendWith(MockitoExtension.class) +class JctFileManagerRequiredLocationsConfigurerTest { + + @Mock(answer = Answers.RETURNS_MOCKS, strictness = Strictness.LENIENT) + Workspace workspace; + + @Mock + JctFileManagerImpl fileManager; + + @InjectMocks + JctFileManagerRequiredLocationsConfigurer configurer; + + @DisplayName(".configure(...) will ensure all required locations are present") + @EnumSource( + value = StandardLocation.class, + names = { + "SOURCE_OUTPUT", + "CLASS_OUTPUT", + "NATIVE_HEADER_OUTPUT", + } + ) + @ParameterizedTest(name = ".configure(...) will configure location {0}") + void configureWillCreateAllRequiredLocations(StandardLocation location) { + // Given + var managedDirectory = mock(ManagedDirectory.class); + when(workspace.createPackage(location)).thenReturn(managedDirectory); + + // When + configurer.configure(fileManager); + + // Then + verify(workspace).createPackage(location); + verify(fileManager).addPath(location, managedDirectory); + } + + @DisplayName(".configure(...) will not configure locations that already exist") + @EnumSource( + value = StandardLocation.class, + names = { + "SOURCE_OUTPUT", + "CLASS_OUTPUT", + "NATIVE_HEADER_OUTPUT", + } + ) + @ParameterizedTest(name = ".configure(...) will not configure existing location {0}") + void configureWillNotConfigureExistingLocation(StandardLocation location) { + // Given + when(fileManager.hasLocation(any())).thenReturn(false); + when(fileManager.hasLocation(location)).thenReturn(true); + + // When + configurer.configure(fileManager); + + // Then + verify(fileManager, never()).addPath(eq(location), any()); + } + + @DisplayName(".configure(...) returns the input file manager") + @Test + void configureReturnsTheInputFileManager() { + // When + var result = configurer.configure(fileManager); + + // Then + assertThat(result).isSameAs(fileManager); + } + + @DisplayName(".isEnabled() returns true") + @Test + void isEnabledReturnsTrue() { + // Then + assertThat(configurer.isEnabled()).isTrue(); + } +} diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerWorkspaceConfigurerTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerWorkspaceConfigurerTest.java new file mode 100644 index 000000000..6f4ab12df --- /dev/null +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/config/JctFileManagerWorkspaceConfigurerTest.java @@ -0,0 +1,91 @@ +/* + * 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.filemanagers.config; + +import static io.github.ascopes.jct.tests.helpers.Fixtures.someLocation; +import static io.github.ascopes.jct.tests.helpers.Fixtures.somePathRoot; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.github.ascopes.jct.filemanagers.impl.JctFileManagerImpl; +import io.github.ascopes.jct.filemanagers.config.JctFileManagerWorkspaceConfigurer; +import io.github.ascopes.jct.workspaces.PathRoot; +import io.github.ascopes.jct.workspaces.Workspace; +import java.util.List; +import java.util.Map; +import javax.tools.JavaFileManager.Location; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * {@link JctFileManagerWorkspaceConfigurer} tests. + * + * @author Ashley Scopes + */ +@DisplayName("JctFileManagerWorkspaceConfigurer tests") +@ExtendWith(MockitoExtension.class) +class JctFileManagerWorkspaceConfigurerTest { + + @Mock + Workspace workspace; + + @Mock + JctFileManagerImpl fileManager; + + @InjectMocks + JctFileManagerWorkspaceConfigurer configurer; + + @DisplayName(".configure(...) will copy all workspace paths to the file manager") + @Test + void configureWillCopyAllWorkspacePathsToTheFileManager() { + // Given + var paths = Map.>of( + someLocation(), List.of(somePathRoot()), + someLocation(), List.of(somePathRoot(), somePathRoot()), + someLocation(), List.of(somePathRoot(), somePathRoot(), somePathRoot()), + someLocation(), List.of(somePathRoot()) + ); + when(workspace.getAllPaths()).thenReturn(paths); + + // When + configurer.configure(fileManager); + + // Then + paths.forEach((location, roots) -> verify(fileManager).addPaths(location, roots)); + } + + @DisplayName(".configure(...) returns the input file manager") + @Test + void configureReturnsTheInputFileManager() { + // When + var result = configurer.configure(fileManager); + + // Then + assertThat(result).isSameAs(fileManager); + } + + @DisplayName(".isEnabled() returns true") + @Test + void isEnabledReturnsTrue() { + // Then + assertThat(configurer.isEnabled()).isTrue(); + } +} 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 658d77774..2faaa2845 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 @@ -33,15 +33,15 @@ class JctFileManagerImplTest { @Test @DisplayName("generates JctFileManager instance for a release") void testGettingJctFileManagerImplInstance() { - assertThat(JctFileManagerImpl.forRelease("test")).isInstanceOf(JctFileManagerImpl.class); + assertThat(new JctFileManagerImpl("test")) + .isInstanceOf(JctFileManagerImpl.class); } @Test @DisplayName("null release is disallowed") void testIfNullPointerExceptionThrownIfReleaseNull() { - assertThatThrownBy(() -> { - JctFileManagerImpl.forRelease(null); - }).isInstanceOf(NullPointerException.class) + assertThatThrownBy(() -> new JctFileManagerImpl(null)) + .isInstanceOf(NullPointerException.class) .hasMessage("release"); } @@ -55,7 +55,7 @@ void testAddPathForPackageLocation() { // we mock path because it is needed by AbstractPackageContainerGroup given(pathRoot.getPath()).willReturn(path); - var jctFileManager = JctFileManagerImpl.forRelease("test"); + var jctFileManager = new JctFileManagerImpl("test"); jctFileManager.addPath(packageLocation, pathRoot); assertThat(jctFileManager.hasLocation(packageLocation)).isTrue(); } @@ -71,7 +71,7 @@ void testAddPathForOutputLocation() { given(pathRoot.getPath()).willReturn(path); given(outputLocation.isOutputLocation()).willReturn(true); - var jctFileManager = JctFileManagerImpl.forRelease("test"); + var jctFileManager = new JctFileManagerImpl("test"); jctFileManager.addPath(outputLocation, pathRoot); assertThat(jctFileManager.hasLocation(outputLocation)).isTrue(); }