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();
}