Note that this is highly experimental, and may break in strange and unexpected ways, or may
+ * even be removed without notice. Use at your own risk.
+ *
+ * @return the compiler instance.
+ * @since TBC
+ */
+ @API(status = Status.EXPERIMENTAL, since = "TBC")
+ public static JctCompiler newEcjCompiler() {
+ return new EcjJctCompilerImpl();
+ }
}
diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/impl/EcjJctCompilerImpl.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/impl/EcjJctCompilerImpl.java
new file mode 100644
index 000000000..f932c867e
--- /dev/null
+++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/impl/EcjJctCompilerImpl.java
@@ -0,0 +1,99 @@
+/*
+ * 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.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.EcjJctFileManagerFactoryImpl;
+import org.apiguardian.api.API;
+import org.apiguardian.api.API.Status;
+import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
+import org.eclipse.jdt.internal.compiler.tool.EclipseCompiler;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Implementation of an {@code ECJ} compiler.
+ *
+ * @author Ashley Scopes
+ * @since TBC
+ */
+@API(since = "TBC", status = Status.INTERNAL)
+public final class EcjJctCompilerImpl extends AbstractJctCompiler {
+
+ private static final String NAME = "ECJ";
+
+ /**
+ * Initialize this compiler.
+ */
+ public EcjJctCompilerImpl() {
+ super(NAME);
+ }
+
+ @Override
+ public JctFlagBuilderFactory getFlagBuilderFactory() {
+ return EcjJctFlagBuilderImpl::new;
+ }
+
+ @Override
+ public Jsr199CompilerFactory getCompilerFactory() {
+ return EclipseCompiler::new;
+ }
+
+ @Override
+ public JctFileManagerFactory getFileManagerFactory() {
+ return new EcjJctFileManagerFactoryImpl(this);
+ }
+
+ @Override
+ public String getDefaultRelease() {
+ return Integer.toString(getLatestSupportedVersionInt());
+ }
+
+ /**
+ * Get the minimum version of ECJ that is supported.
+ *
+ * @return the minimum supported version.
+ */
+ public static int getEarliestSupportedVersionInt() {
+ return decodeMajorVersion(ClassFileConstants.JDK1_8);
+ }
+
+ /**
+ * Get the ECJ version that is loaded.
+ *
+ * @return the ECJ version that is loaded on the class path, or {@code null} if the information is
+ * not available.
+ */
+ @Nullable
+ public static String getEcjVersion() {
+ return EclipseCompiler.class.getPackage().getImplementationVersion();
+ }
+
+ /**
+ * Get the maximum version of ECJ that is supported.
+ *
+ * @return the maximum supported version.
+ */
+ public static int getLatestSupportedVersionInt() {
+ return decodeMajorVersion(ClassFileConstants.getLatestJDKLevel());
+ }
+
+ private static int decodeMajorVersion(long classFileConstant) {
+ return (int) ((classFileConstant >> 16L) - ClassFileConstants.MAJOR_VERSION_0);
+ }
+}
diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/impl/EcjJctFlagBuilderImpl.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/impl/EcjJctFlagBuilderImpl.java
new file mode 100644
index 000000000..a04b0b98f
--- /dev/null
+++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/impl/EcjJctFlagBuilderImpl.java
@@ -0,0 +1,153 @@
+/*
+ * 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.JctFlagBuilder;
+import java.util.ArrayList;
+import java.util.List;
+import org.apiguardian.api.API;
+import org.apiguardian.api.API.Status;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Helper to build flags for the ECJ compiler implementation.
+ *
+ * @author Ashley Scopes
+ * @since TBC
+ */
+@API(since = "TBC", status = Status.INTERNAL)
+public final class EcjJctFlagBuilderImpl implements JctFlagBuilder {
+
+ private static final String VERBOSE = "-verbose";
+ private static final String PRINT_ANNOTATION_PROCESSOR_INFO = "-XprintProcessorInfo";
+ private static final String PRINT_ANNOTATION_PROCESSOR_ROUNDS = "-XprintRounds";
+ private static final String ENABLE_PREVIEW = "--enable-preview";
+ private static final String NOWARN = "-nowarn";
+ private static final String FAIL_ON_WARNING = "--failOnWarning";
+ private static final String DEPRECATION = "-deprecation";
+ private static final String RELEASE = "--release";
+ private static final String SOURCE = "-source";
+ private static final String TARGET = "-target";
+ private static final String ANNOTATION_OPT = "-A";
+ private static final String PROC_NONE = "-proc:none";
+ private static final String PROC_ONLY = "-proc:only";
+
+ private final List craftedFlags;
+
+ /**
+ * Initialize this flag builder.
+ */
+ public EcjJctFlagBuilderImpl() {
+ craftedFlags = new ArrayList<>();
+ }
+
+ @Override
+ public EcjJctFlagBuilderImpl verbose(boolean enabled) {
+ return addFlagIfTrue(enabled, VERBOSE)
+ .addFlagIfTrue(enabled, PRINT_ANNOTATION_PROCESSOR_INFO)
+ .addFlagIfTrue(enabled, PRINT_ANNOTATION_PROCESSOR_ROUNDS);
+ }
+
+ @Override
+ public EcjJctFlagBuilderImpl previewFeatures(boolean enabled) {
+ return addFlagIfTrue(enabled, ENABLE_PREVIEW);
+ }
+
+ @Override
+ public EcjJctFlagBuilderImpl showWarnings(boolean enabled) {
+ return addFlagIfTrue(!enabled, NOWARN);
+ }
+
+ @Override
+ public EcjJctFlagBuilderImpl failOnWarnings(boolean enabled) {
+ return addFlagIfTrue(enabled, FAIL_ON_WARNING);
+ }
+
+ @Override
+ public JctFlagBuilder compilationMode(CompilationMode compilationMode) {
+ switch (compilationMode) {
+ case COMPILATION_ONLY:
+ craftedFlags.add(PROC_NONE);
+ break;
+
+ case ANNOTATION_PROCESSING_ONLY:
+ craftedFlags.add(PROC_ONLY);
+ break;
+
+ default:
+ // Do nothing. The default behaviour is to allow this.
+ break;
+ }
+
+ return this;
+ }
+
+ @Override
+ public EcjJctFlagBuilderImpl showDeprecationWarnings(boolean enabled) {
+ return addFlagIfTrue(enabled, DEPRECATION);
+ }
+
+ @Override
+ public EcjJctFlagBuilderImpl release(@Nullable String version) {
+ return addVersionIfPresent(RELEASE, version);
+ }
+
+ @Override
+ public EcjJctFlagBuilderImpl source(@Nullable String version) {
+ return addVersionIfPresent(SOURCE, version);
+ }
+
+ @Override
+ public EcjJctFlagBuilderImpl target(@Nullable String version) {
+ return addVersionIfPresent(TARGET, version);
+ }
+
+ @Override
+ public EcjJctFlagBuilderImpl annotationProcessorOptions(List options) {
+ options.forEach(option -> craftedFlags.add(ANNOTATION_OPT + option));
+ return this;
+ }
+
+ @Override
+ public EcjJctFlagBuilderImpl compilerOptions(List options) {
+ craftedFlags.addAll(options);
+ return this;
+ }
+
+ @Override
+ public List build() {
+ // Immutable copy.
+ return List.copyOf(craftedFlags);
+ }
+
+ private EcjJctFlagBuilderImpl addFlagIfTrue(boolean condition, String flag) {
+ if (condition) {
+ craftedFlags.add(flag);
+ }
+
+ return this;
+ }
+
+ private EcjJctFlagBuilderImpl addVersionIfPresent(String flagPrefix, @Nullable String version) {
+ if (version != null) {
+ craftedFlags.add(flagPrefix);
+ craftedFlags.add(version);
+ }
+
+ return this;
+ }
+}
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
index a5904a235..85a27fe4e 100644
--- 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
@@ -96,7 +96,7 @@ private JctCompilation createCheckedCompilation(
}
// Do not close stdout, it breaks test engines, especially IntellIJ.
- var writer = TeeWriter.wrapOutputStream(System.out, compiler.getLogCharset());
+ var writer = TeeWriter.wrapOutputStream(System.err, compiler.getLogCharset());
var diagnosticListener = new TracingDiagnosticListener<>(
compiler.getDiagnosticLoggingMode() != LoggingMode.DISABLED,
diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/containers/impl/PathWrappingContainerImpl.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/containers/impl/PathWrappingContainerImpl.java
index 30f860e13..82f4c614c 100644
--- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/containers/impl/PathWrappingContainerImpl.java
+++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/containers/impl/PathWrappingContainerImpl.java
@@ -21,6 +21,7 @@
import io.github.ascopes.jct.filemanagers.PathFileObject;
import io.github.ascopes.jct.filemanagers.impl.PathFileObjectImpl;
import io.github.ascopes.jct.utils.FileUtils;
+import io.github.ascopes.jct.utils.StringUtils;
import io.github.ascopes.jct.utils.ToStringBuilder;
import io.github.ascopes.jct.workspaces.PathRoot;
import java.io.IOException;
@@ -163,17 +164,38 @@ public void listFileObjects(
boolean recurse,
Collection collection
) throws IOException {
+ var initialSize = collection.size();
var maxDepth = recurse ? Integer.MAX_VALUE : 1;
var basePath = FileUtils.packageNameToPath(root.getPath(), packageName);
+ LOGGER
+ .atTrace()
+ .setMessage("Performing lookup of files matching kinds {} in package {} ({}) (depth = {})")
+ .addArgument(() -> StringUtils.quotedIterable(kinds))
+ .addArgument(() -> StringUtils.quoted(packageName))
+ .addArgument(() -> StringUtils.quoted(basePath))
+ .addArgument(maxDepth)
+ .log();
+
try (var walker = Files.walk(basePath, maxDepth, FileVisitOption.FOLLOW_LINKS)) {
walker
.filter(FileUtils.fileWithAnyKind(kinds))
.map(path -> new PathFileObjectImpl(location, root.getPath(), path))
.forEach(collection::add);
} catch (NoSuchFileException ex) {
- LOGGER.trace("Directory {} does not exist so is being ignored", root.getPath());
+ LOGGER.atTrace()
+ .setMessage("File system object {} does not exist, so is being skipped ({})")
+ .addArgument(() -> StringUtils.quoted(ex.getFile()))
+ .addArgument(() -> StringUtils.quoted(ex.getReason()))
+ .log();
}
+
+ LOGGER.atTrace()
+ .setMessage("Found {} matches for criteria in package {} ({})")
+ .addArgument(() -> collection.size() - initialSize)
+ .addArgument(() -> StringUtils.quoted(packageName))
+ .addArgument(() -> StringUtils.quoted(basePath))
+ .log();
}
@Override
diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/ForwardingJctFileManager.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/ForwardingJctFileManager.java
new file mode 100644
index 000000000..4743d6ffc
--- /dev/null
+++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/ForwardingJctFileManager.java
@@ -0,0 +1,118 @@
+/*
+ * 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.containers.ModuleContainerGroup;
+import io.github.ascopes.jct.containers.OutputContainerGroup;
+import io.github.ascopes.jct.containers.PackageContainerGroup;
+import io.github.ascopes.jct.workspaces.PathRoot;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Set;
+import javax.tools.ForwardingJavaFileManager;
+import javax.tools.JavaFileObject;
+import org.apiguardian.api.API;
+
+/**
+ * Abstract base for a {@link JctFileManager} that forwards all methods by default to a
+ * delegate object.
+ *
+ * @param the file manager implementation to delegate to.
+ * @author Ashley Scopes
+ * @since TBC
+ */
+@API(since = "TBC", status = API.Status.STABLE)
+public abstract class ForwardingJctFileManager
+ extends ForwardingJavaFileManager
+ implements JctFileManager {
+
+ /**
+ * Creates a new instance of {@code ForwardingJavaFileManager}.
+ *
+ * @param fileManager delegate to this file manager
+ */
+ protected ForwardingJctFileManager(M fileManager) {
+ super(fileManager);
+ }
+
+ @Override
+ public void addPath(Location location, PathRoot path) {
+ fileManager.addPath(location, path);
+ }
+
+ @Override
+ public void addPaths(Location location, Collection extends PathRoot> paths) {
+ fileManager.addPaths(location, paths);
+ }
+
+ @Override
+ public void copyContainers(Location from, Location to) {
+ fileManager.copyContainers(from, to);
+ }
+
+ @Override
+ public void createEmptyLocation(Location location) {
+ fileManager.createEmptyLocation(location);
+ }
+
+ @Override
+ public String getEffectiveRelease() {
+ return fileManager.getEffectiveRelease();
+ }
+
+ @Override
+ public PackageContainerGroup getPackageContainerGroup(Location location) {
+ return fileManager.getPackageContainerGroup(location);
+ }
+
+ @Override
+ public Collection getPackageContainerGroups() {
+ return fileManager.getPackageContainerGroups();
+ }
+
+ @Override
+ public ModuleContainerGroup getModuleContainerGroup(Location location) {
+ return fileManager.getModuleContainerGroup(location);
+ }
+
+ @Override
+ public Collection getModuleContainerGroups() {
+ return fileManager.getModuleContainerGroups();
+ }
+
+ @Override
+ public OutputContainerGroup getOutputContainerGroup(Location location) {
+ return fileManager.getOutputContainerGroup(location);
+ }
+
+ @Override
+ public Collection getOutputContainerGroups() {
+ return fileManager.getOutputContainerGroups();
+ }
+
+ // We have to override this method since the compiler will get confused that the JctFileManager
+ // interface is promoting the return type of this method to a more specific type than the
+ // ForwardingJavaFileManager class implements.
+ @Override
+ public Set list(
+ Location location,
+ String packageName,
+ Set kinds,
+ boolean recurse
+ ) throws IOException {
+ return fileManager.list(location, packageName, kinds, recurse);
+ }
+}
diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/impl/EcjJctFileManagerFactoryImpl.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/impl/EcjJctFileManagerFactoryImpl.java
new file mode 100644
index 000000000..92b990c22
--- /dev/null
+++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/impl/EcjJctFileManagerFactoryImpl.java
@@ -0,0 +1,63 @@
+/*
+ * 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.workspaces.PathStrategy;
+import io.github.ascopes.jct.workspaces.Workspace;
+import org.apiguardian.api.API;
+
+/**
+ * A file manager factory that is used for ECJ compilers. This wraps a regular
+ * {@link JctFileManagerFactory}, modifying the result slightly.
+ *
+ * @author Ashley Scopes
+ * @since TBC
+ */
+@API(since = "TBC", status = API.Status.INTERNAL)
+public final class EcjJctFileManagerFactoryImpl implements JctFileManagerFactory {
+
+ private final JctFileManagerFactory factory;
+
+ /**
+ * Initialise this factory.
+ *
+ * @param compiler the compiler to use.
+ */
+ public EcjJctFileManagerFactoryImpl(JctCompiler compiler) {
+ factory = new JctFileManagerFactoryImpl(compiler);
+ }
+
+ @Override
+ public JctFileManager createFileManager(Workspace workspace) {
+ // ECJ uses the java.io.File API in arbitrary places, which means RAM_DIRECTORIES will fail
+ // to perform lookups in certain places. Therefore, we have to enforce that we only use
+ // workspaces that reside in the root file system.
+ if (workspace.getPathStrategy() != PathStrategy.TEMP_DIRECTORIES) {
+ throw new IllegalArgumentException(
+ "The ECJ compiler only supports the TEMP_DIRECTORIES path strategy. " +
+ "Specify this explicitly when you create the workspace to fix this."
+ );
+ }
+
+ var fileManager = factory.createFileManager(workspace);
+
+ // Wrap the result in the EcjJctFileManagerImpl to mitigate incorrect API usage from ECJ.
+ return new EcjJctFileManagerImpl(fileManager);
+ }
+}
diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/impl/EcjJctFileManagerImpl.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/impl/EcjJctFileManagerImpl.java
new file mode 100644
index 000000000..6d394792e
--- /dev/null
+++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/impl/EcjJctFileManagerImpl.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.filemanagers.impl;
+
+import io.github.ascopes.jct.filemanagers.ForwardingJctFileManager;
+import io.github.ascopes.jct.filemanagers.JctFileManager;
+import java.io.IOException;
+import java.util.Set;
+import javax.tools.FileObject;
+import javax.tools.JavaFileObject;
+import javax.tools.JavaFileObject.Kind;
+import org.apiguardian.api.API;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * A wrapper around a regular {@link JctFileManager} that intercepts and fixes some of the inputs
+ * and return values that ECJ can produce. This is needed since ECJ does not fully adhere to the
+ * JSR-199 specification for valid parameter types and behaviours.
+ *
+ * @author Ashley Scopes
+ * @since TBC
+ */
+@API(since = "TBC", status = API.Status.INTERNAL)
+public final class EcjJctFileManagerImpl extends ForwardingJctFileManager {
+
+ /**
+ * Initialise this wrapper.
+ *
+ * @param fileManager delegate to this file manager
+ */
+ public EcjJctFileManagerImpl(JctFileManager fileManager) {
+ super(fileManager);
+ }
+
+ @Override
+ public JavaFileObject getJavaFileForInput(
+ Location location,
+ String className,
+ Kind kind
+ ) throws IOException {
+ className = fixClassOrPackageName(className);
+ return super.getJavaFileForInput(location, className, kind);
+ }
+
+ @Override
+ public JavaFileObject getJavaFileForOutput(
+ Location location,
+ String className,
+ Kind kind,
+ @Nullable FileObject sibling
+ ) throws IOException {
+ className = fixClassOrPackageName(className);
+ return super.getJavaFileForOutput(location, className, kind, sibling);
+ }
+
+ @Override
+ public FileObject getFileForInput(
+ Location location,
+ String packageName,
+ String relativeName
+ ) throws IOException {
+ packageName = fixClassOrPackageName(packageName);
+ return super.getFileForInput(location, packageName, relativeName);
+ }
+
+ @Override
+ public FileObject getFileForOutput(
+ Location location,
+ String packageName,
+ String relativeName,
+ FileObject sibling
+ ) throws IOException {
+ packageName = fixClassOrPackageName(packageName);
+ return super.getFileForOutput(location, packageName, relativeName, sibling);
+ }
+
+ @Nullable
+ @Override
+ public Set list(
+ Location location,
+ String packageName,
+ Set kinds,
+ boolean recurse
+ ) throws IOException {
+ // ECJ passes invalid locations into this method, so we need to handle those explicitly.
+ if (location.isModuleOrientedLocation()) {
+ return Set.of();
+ }
+
+ packageName = fixClassOrPackageName(packageName);
+ return super.list(location, packageName, kinds, recurse);
+ }
+
+ private String fixClassOrPackageName(String providedBinaryName) {
+ // ECJ passes around forward-slashes in binary names rather than periods. This is incorrect
+ // and will confuse the JctFileManagerImpl implementation if we do not fix it.
+ return providedBinaryName.replace('/', '.');
+ }
+}
diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/CommonTags.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/CommonTags.java
new file mode 100644
index 000000000..8e77d2586
--- /dev/null
+++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/CommonTags.java
@@ -0,0 +1,34 @@
+/*
+ * 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.junit;
+
+import io.github.ascopes.jct.utils.UtilityClass;
+import org.apiguardian.api.API;
+import org.apiguardian.api.API.Status;
+
+/**
+ * Common tag strings used in annotations in this package.
+ *
+ * @author Ashley Scopes
+ * @since TBC
+ */
+@API(since = "TBC", status = Status.INTERNAL)
+final class CommonTags extends UtilityClass {
+ static final String JAVA_COMPILER_TESTING_TEST = "java-compiler-testing-test";
+
+ static final String ECJ_TEST = "ecj-test";
+ static final String JAVAC_TEST = "javac-test";
+}
diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/EcjCompilerTest.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/EcjCompilerTest.java
new file mode 100644
index 000000000..10638e5b2
--- /dev/null
+++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/EcjCompilerTest.java
@@ -0,0 +1,189 @@
+/*
+ * 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.junit;
+
+import io.github.ascopes.jct.compilers.JctCompilerConfigurer;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.apiguardian.api.API;
+import org.apiguardian.api.API.Status;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Tags;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.condition.DisabledInNativeImage;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ArgumentsSource;
+
+/**
+ * Annotation that can be applied to a JUnit parameterized test to invoke that test case across
+ * multiple ECJ compilers, each configured to a specific version in a range of Java language
+ * versions.
+ *
+ *
This will also add the {@code "java-compiler-testing-test"} tag and {@code "ecj-test"}
+ * tags to your test method, meaning you can instruct your IDE or build system to optionally only
+ * run tests annotated with this method for development purposes. As an example, Maven Surefire
+ * could be instructed to only run these tests by passing {@code -Dgroup="ecj-test"} to Maven.
+ *
+ *
If your build is running in a GraalVM Native Image, then this test will not execute, as
+ * the Java Compiler Testing API is not yet tested within Native Images.
+ *
+ *
For example, to run a simple test on Java 11 through 14 (inclusive):
+ *
+ *
Note that this is highly experimental, and may break in strange and unexpected ways, or may
+ * even be removed without notice. Use at your own risk.
+ *
+ * @author Ashley Scopes
+ * @since TBC
+ */
+@API(since = "TBC", status = Status.EXPERIMENTAL)
+@ArgumentsSource(EcjCompilersProvider.class)
+@DisabledInNativeImage
+@Documented
+@Inherited
+@ParameterizedTest(name = "for compiler \"{0}\"")
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE})
+@TestTemplate
+@Tags({
+ @Tag(CommonTags.JAVA_COMPILER_TESTING_TEST),
+ @Tag(CommonTags.ECJ_TEST)
+})
+public @interface EcjCompilerTest {
+
+ /**
+ * Minimum version to use (inclusive).
+ *
+ *
By default, it will use the lowest possible version supported by the compiler. This depends
+ * on the version of ECJ that is in use.
+ *
+ *
If the version is lower than the minimum supported version, then the minimum supported
+ * version of the compiler will be used instead. This enables writing tests that will work on
+ * a range of JDKs during builds without needing to duplicate the test to satisfy different
+ * JDK supported version ranges.
+ *
+ * @return the minimum version.
+ */
+ int minVersion() default Integer.MIN_VALUE;
+
+ /**
+ * Maximum version to use (inclusive).
+ *
+ *
By default, it will use the highest possible version supported by the compiler. This
+ * depends on the version of ECJ that is in use.
+ *
+ *
If the version is higher than the maximum supported version, then the maximum supported
+ * version of the compiler will be used instead. This enables writing tests that will work on
+ * a range of JDKs during builds without needing to duplicate the test to satisfy different
+ * JDK supported version ranges.
+ *
+ * @return the maximum version.
+ */
+ int maxVersion() default Integer.MAX_VALUE;
+
+ /**
+ * Get an array of compiler configurer classes to apply in-order before starting the test.
+ *
+ *
Each configurer must have a public no-args constructor, and their package must be
+ * open to this module if JPMS modules are in-use, for example:
+ *
+ *
+ *
+ * @return an array of classes to run to configure the compiler. These run in the given order.
+ */
+ Class extends JctCompilerConfigurer>>[] configurers() default {};
+
+ /**
+ * Whether we need to support modules or not.
+ *
+ *
Setting this to true will skip any versions of the compiler that do not support JPMS
+ * modules.
+ *
+ * @return {@code true} if we need to support modules, or {@code false} if we do not.
+ * @deprecated this will be removed in a future release, since Java 8 is reaching end-of-life.
+ */
+ @Deprecated(forRemoval = true, since = "0.1.0")
+ boolean modules() default false;
+
+ /**
+ * The version strategy to use.
+ *
+ *
This determines whether the version number being iterated across specifies the
+ * release, source, target, or source and target versions.
+ *
+ *
The default is to specify the release.
+ *
+ * @return the version strategy to use.
+ */
+ VersionStrategy versionStrategy() default VersionStrategy.RELEASE;
+}
diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/EcjCompilersProvider.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/EcjCompilersProvider.java
new file mode 100644
index 000000000..85e098f9c
--- /dev/null
+++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/EcjCompilersProvider.java
@@ -0,0 +1,74 @@
+/*
+ * 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.junit;
+
+import io.github.ascopes.jct.compilers.JctCompiler;
+import io.github.ascopes.jct.compilers.impl.EcjJctCompilerImpl;
+import io.github.ascopes.jct.utils.VisibleForTestingOnly;
+import org.apiguardian.api.API;
+import org.apiguardian.api.API.Status;
+import org.junit.jupiter.params.support.AnnotationConsumer;
+
+/**
+ * Argument provider for the {@link EcjCompilerTest} annotation.
+ *
+ *
Note that this is highly experimental, and may break in strange and unexpected ways, or may
+ * even be removed without notice. Use at your own risk.
+ *
+ * @author Ashley Scopes
+ * @since TBC
+ */
+@API(since = "TBC", status = Status.EXPERIMENTAL)
+public final class EcjCompilersProvider extends AbstractCompilersProvider
+ implements AnnotationConsumer {
+
+ /**
+ * Initialise the provider.
+ *
+ *
This is only visible for testing purposes, users should have no need to
+ * initialise this class directly.
+ */
+ @VisibleForTestingOnly
+ public EcjCompilersProvider() {
+ // Visible for testing only.
+ }
+
+ @Override
+ protected JctCompiler initializeNewCompiler() {
+ return new EcjJctCompilerImpl();
+ }
+
+ @Override
+ protected int minSupportedVersion() {
+ return EcjJctCompilerImpl.getEarliestSupportedVersionInt();
+ }
+
+ @Override
+ protected int maxSupportedVersion() {
+ return EcjJctCompilerImpl.getLatestSupportedVersionInt();
+ }
+
+ @Override
+ public void accept(EcjCompilerTest javacCompilers) {
+ // Super is needed here to prevent IntelliJ getting confused.
+ super.configure(
+ javacCompilers.minVersion(),
+ javacCompilers.maxVersion(),
+ javacCompilers.configurers(),
+ javacCompilers.versionStrategy()
+ );
+ }
+}
diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/JavacCompilerTest.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/JavacCompilerTest.java
index 1c1499a2b..61e712cd8 100644
--- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/JavacCompilerTest.java
+++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/JavacCompilerTest.java
@@ -81,8 +81,8 @@
@ParameterizedTest(name = "for compiler \"{0}\"")
@Retention(RetentionPolicy.RUNTIME)
@Tags({
- @Tag("java-compiler-testing-test"),
- @Tag("javac-test")
+ @Tag(CommonTags.JAVA_COMPILER_TESTING_TEST),
+ @Tag(CommonTags.JAVAC_TEST)
})
@Target({
ElementType.ANNOTATION_TYPE,
diff --git a/java-compiler-testing/src/main/java/module-info.java b/java-compiler-testing/src/main/java/module-info.java
index 96d7729f0..2ff1d47d8 100644
--- a/java-compiler-testing/src/main/java/module-info.java
+++ b/java-compiler-testing/src/main/java/module-info.java
@@ -107,6 +107,7 @@
requires me.xdrop.fuzzywuzzy;
requires static transitive org.apiguardian.api;
requires org.assertj.core;
+ requires org.eclipse.jdt.core.compiler.batch;
requires static org.jspecify;
requires static transitive org.junit.jupiter.api;
requires static transitive org.junit.jupiter.params;
diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/helpers/Fixtures.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/helpers/Fixtures.java
index 8ce2a1f75..64748abe2 100644
--- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/helpers/Fixtures.java
+++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/helpers/Fixtures.java
@@ -40,6 +40,7 @@
import java.util.List;
import java.util.Objects;
import java.util.Random;
+import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -475,6 +476,20 @@ public static T oneOf(Collection items) {
}
}
+ /**
+ * Return one or more of the members of a given enum in a set.
+ *
+ * @param cls the enum class.
+ * @param the enum type.
+ * @return the set of the enum members.
+ */
+ public static > Set someOf(Class cls) {
+ return Stream
+ .generate(() -> oneOf(cls))
+ .limit(RANDOM.nextInt(cls.getEnumConstants().length))
+ .collect(Collectors.toSet());
+ }
+
private static void createStubMethodsFor(Diagnostic diagnostic) {
var col = someLong(1, 100);
when(diagnostic.getColumnNumber()).thenReturn(col);
diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/integration/IntegrationTestConfigurer.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/integration/IntegrationTestConfigurer.java
new file mode 100644
index 000000000..a9fb9cdbb
--- /dev/null
+++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/integration/IntegrationTestConfigurer.java
@@ -0,0 +1,33 @@
+/*
+ * 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.integration;
+
+import io.github.ascopes.jct.compilers.JctCompiler;
+import io.github.ascopes.jct.compilers.JctCompilerConfigurer;
+
+/**
+ * Default configuration settings for integration tests.
+ *
+ * @author Ashley Scopes
+ */
+public final class IntegrationTestConfigurer implements JctCompilerConfigurer {
+ @Override
+ public void configure(JctCompiler compiler) {
+ compiler
+ .inheritClassPath(false)
+ .inheritModulePath(false);
+ }
+}
diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/integration/compilation/BasicLegacyCompilationIntegrationTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/integration/compilation/BasicLegacyCompilationIntegrationTest.java
index 33c3b89da..0e6129bb0 100644
--- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/integration/compilation/BasicLegacyCompilationIntegrationTest.java
+++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/integration/compilation/BasicLegacyCompilationIntegrationTest.java
@@ -18,9 +18,12 @@
import static io.github.ascopes.jct.assertions.JctAssertions.assertThatCompilation;
import io.github.ascopes.jct.compilers.JctCompiler;
+import io.github.ascopes.jct.junit.EcjCompilerTest;
import io.github.ascopes.jct.junit.JavacCompilerTest;
import io.github.ascopes.jct.tests.integration.AbstractIntegrationTest;
+import io.github.ascopes.jct.tests.integration.IntegrationTestConfigurer;
import io.github.ascopes.jct.workspaces.PathStrategy;
+import io.github.ascopes.jct.workspaces.Workspace;
import io.github.ascopes.jct.workspaces.Workspaces;
import javax.tools.StandardLocation;
import org.junit.jupiter.api.DisplayName;
@@ -33,44 +36,44 @@
@DisplayName("Basic legacy compilation integration tests")
class BasicLegacyCompilationIntegrationTest extends AbstractIntegrationTest {
- @DisplayName("I can compile a 'Hello, World!' program using a RAM directory")
- @JavacCompilerTest
- void helloWorldJavacRamDirectory(JctCompiler compiler) {
+ @DisplayName("I can compile a 'Hello, World!' program using a RAM directory on Javac")
+ @JavacCompilerTest(configurers = IntegrationTestConfigurer.class)
+ void helloWorldRamDirectoryJavac(JctCompiler compiler) {
try (var workspace = Workspaces.newWorkspace(PathStrategy.RAM_DIRECTORIES)) {
- workspace
- .createPackage(StandardLocation.SOURCE_PATH)
- .createDirectory("com", "example")
- .copyContentsFrom(resourcesDirectory());
- var compilation = compiler.compile(workspace);
-
- assertThatCompilation(compilation)
- .isSuccessfulWithoutWarnings();
+ runTest(compiler, workspace);
+ }
+ }
- assertThatCompilation(compilation)
- .classOutputPackages()
- .fileExists("com", "example", "HelloWorld.class")
- .isNotEmptyFile();
+ @DisplayName("I can compile a 'Hello, World!' program using a temp directory on Javac")
+ @JavacCompilerTest(configurers = IntegrationTestConfigurer.class)
+ void helloWorldTempDirectoryJavac(JctCompiler compiler) {
+ try (var workspace = Workspaces.newWorkspace(PathStrategy.TEMP_DIRECTORIES)) {
+ runTest(compiler, workspace);
}
}
- @DisplayName("I can compile a 'Hello, World!' program using a temp directory")
- @JavacCompilerTest
- void helloWorldJavacTempDirectory(JctCompiler compiler) {
+ @DisplayName("I can compile a 'Hello, World!' program using a temp directory on ECJ")
+ @EcjCompilerTest(configurers = IntegrationTestConfigurer.class)
+ void helloWorldTempDirectoryEcj(JctCompiler compiler) {
try (var workspace = Workspaces.newWorkspace(PathStrategy.TEMP_DIRECTORIES)) {
- workspace
- .createPackage(StandardLocation.SOURCE_PATH)
- .createDirectory("com", "example")
- .copyContentsFrom(resourcesDirectory());
+ runTest(compiler, workspace);
+ }
+ }
- var compilation = compiler.compile(workspace);
+ private void runTest(JctCompiler compiler, Workspace workspace) {
+ workspace
+ .createPackage(StandardLocation.SOURCE_PATH)
+ .createDirectory("com", "example")
+ .copyContentsFrom(resourcesDirectory());
- assertThatCompilation(compilation)
- .isSuccessfulWithoutWarnings();
+ var compilation = compiler.compile(workspace);
- assertThatCompilation(compilation)
- .classOutputPackages()
- .fileExists("com", "example", "HelloWorld.class")
- .isNotEmptyFile();
- }
+ assertThatCompilation(compilation)
+ .isSuccessfulWithoutWarnings();
+
+ assertThatCompilation(compilation)
+ .classOutputPackages()
+ .fileExists("com", "example", "HelloWorld.class")
+ .isNotEmptyFile();
}
}
diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/integration/compilation/BasicModuleCompilationIntegrationTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/integration/compilation/BasicModuleCompilationIntegrationTest.java
index 0b82a3112..0a53c43b4 100644
--- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/integration/compilation/BasicModuleCompilationIntegrationTest.java
+++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/integration/compilation/BasicModuleCompilationIntegrationTest.java
@@ -16,11 +16,16 @@
package io.github.ascopes.jct.tests.integration.compilation;
import static io.github.ascopes.jct.assertions.JctAssertions.assertThatCompilation;
+import static io.github.ascopes.jct.assertions.JctAssertions.assertThatContainerGroup;
+import io.github.ascopes.jct.compilers.JctCompilation;
import io.github.ascopes.jct.compilers.JctCompiler;
+import io.github.ascopes.jct.junit.EcjCompilerTest;
import io.github.ascopes.jct.junit.JavacCompilerTest;
import io.github.ascopes.jct.tests.integration.AbstractIntegrationTest;
+import io.github.ascopes.jct.tests.integration.IntegrationTestConfigurer;
import io.github.ascopes.jct.workspaces.PathStrategy;
+import io.github.ascopes.jct.workspaces.Workspace;
import io.github.ascopes.jct.workspaces.Workspaces;
import org.junit.jupiter.api.DisplayName;
@@ -32,59 +37,73 @@
@DisplayName("Basic module compilation integration tests")
class BasicModuleCompilationIntegrationTest extends AbstractIntegrationTest {
- @DisplayName("I can compile a 'Hello, World!' module program using a RAM disk")
- @JavacCompilerTest(minVersion = 9)
- void helloWorldRamDisk(JctCompiler compiler) {
+ @DisplayName("I can compile a 'Hello, World!' module program using a RAM disk on Javac")
+ @JavacCompilerTest(minVersion = 9, configurers = IntegrationTestConfigurer.class)
+ void helloWorldRamDiskJavac(JctCompiler compiler) {
try (var workspace = Workspaces.newWorkspace(PathStrategy.RAM_DIRECTORIES)) {
- // Given
- workspace
- .createSourcePathPackage()
- .copyContentsFrom(resourcesDirectory());
+ runHelloWorldTestExpectingPackages(compiler, workspace);
+ }
+ }
- // When
- var compilation = compiler.compile(workspace);
+ @DisplayName("I can compile a 'Hello, World!' module program using a temp directory on Javac")
+ @JavacCompilerTest(minVersion = 9, configurers = IntegrationTestConfigurer.class)
+ void helloWorldUsingTempDirectoryJavac(JctCompiler compiler) {
+ try (var workspace = Workspaces.newWorkspace(PathStrategy.TEMP_DIRECTORIES)) {
+ runHelloWorldTestExpectingPackages(compiler, workspace);
+ }
+ }
+
+ @DisplayName("I can compile a 'Hello, World!' module program using a temp directory on ECJ")
+ @EcjCompilerTest(minVersion = 9, configurers = IntegrationTestConfigurer.class)
+ void helloWorldUsingTempDirectoryEcj(JctCompiler compiler) {
+ try (var workspace = Workspaces.newWorkspace(PathStrategy.TEMP_DIRECTORIES)) {
+ runHelloWorldTestExpectingModules(compiler, workspace);
+ }
+ }
- // Then
- assertThatCompilation(compilation)
- .isSuccessfulWithoutWarnings();
+ private void runHelloWorldTestExpectingPackages(JctCompiler compiler, Workspace workspace) {
+ var compilation = runHelloWorldTestStart(compiler, workspace);
- assertThatCompilation(compilation)
- .classOutputPackages()
- .fileExists("com", "example", "HelloWorld.class")
- .isNotEmptyFile();
+ assertThatCompilation(compilation)
+ .classOutputPackages()
+ .fileExists("com", "example", "HelloWorld.class")
+ .isNotEmptyFile();
- assertThatCompilation(compilation)
- .classOutputPackages()
- .fileExists("module-info.class")
- .isNotEmptyFile();
- }
+ assertThatCompilation(compilation)
+ .classOutputPackages()
+ .fileExists("module-info.class")
+ .isNotEmptyFile();
}
- @DisplayName("I can compile a 'Hello, World!' module program using a temporary directory")
- @JavacCompilerTest(minVersion = 9)
- void helloWorldUsingTempDirectory(JctCompiler compiler) {
- try (var workspace = Workspaces.newWorkspace(PathStrategy.TEMP_DIRECTORIES)) {
- // Given
- workspace
- .createSourcePathPackage()
- .copyContentsFrom(resourcesDirectory());
+ private void runHelloWorldTestExpectingModules(JctCompiler compiler, Workspace workspace) {
+ var compilation = runHelloWorldTestStart(compiler, workspace);
- // When
- var compilation = compiler.compile(workspace);
+ assertThatCompilation(compilation)
+ .classOutputModules()
+ .moduleExists("hello.world")
+ .satisfies(
+ module -> assertThatContainerGroup(module)
+ .fileExists("com", "example", "HelloWorld.class")
+ .isNotEmptyFile(),
+ module -> assertThatContainerGroup(module)
+ .fileExists("module-info.class")
+ .isNotEmptyFile()
+ );
+ }
- // Then
- assertThatCompilation(compilation)
- .isSuccessfulWithoutWarnings();
+ private JctCompilation runHelloWorldTestStart(JctCompiler compiler, Workspace workspace) {
+ // Given
+ workspace
+ .createSourcePathPackage()
+ .copyContentsFrom(resourcesDirectory());
- assertThatCompilation(compilation)
- .classOutputPackages()
- .fileExists("com", "example", "HelloWorld.class")
- .isNotEmptyFile();
+ // When
+ var compilation = compiler.compile(workspace);
- assertThatCompilation(compilation)
- .classOutputPackages()
- .fileExists("module-info.class")
- .isNotEmptyFile();
- }
+ // Then
+ assertThatCompilation(compilation)
+ .isSuccessfulWithoutWarnings();
+
+ return compilation;
}
}
diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/integration/compilation/BasicMultiModuleCompilationIntegrationTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/integration/compilation/BasicMultiModuleCompilationIntegrationTest.java
index c0f124806..36281a416 100644
--- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/integration/compilation/BasicMultiModuleCompilationIntegrationTest.java
+++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/integration/compilation/BasicMultiModuleCompilationIntegrationTest.java
@@ -19,10 +19,14 @@
import static io.github.ascopes.jct.assertions.JctAssertions.assertThatContainerGroup;
import io.github.ascopes.jct.compilers.JctCompiler;
+import io.github.ascopes.jct.junit.EcjCompilerTest;
import io.github.ascopes.jct.junit.JavacCompilerTest;
import io.github.ascopes.jct.tests.integration.AbstractIntegrationTest;
+import io.github.ascopes.jct.tests.integration.IntegrationTestConfigurer;
import io.github.ascopes.jct.workspaces.PathStrategy;
+import io.github.ascopes.jct.workspaces.Workspace;
import io.github.ascopes.jct.workspaces.Workspaces;
+import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
/**
@@ -33,150 +37,127 @@
@DisplayName("Basic multi-module compilation integration tests")
class BasicMultiModuleCompilationIntegrationTest extends AbstractIntegrationTest {
- @DisplayName("I can compile a single module using multi-module layout using a RAM disk")
- @JavacCompilerTest(minVersion = 9)
- void singleModuleInMultiModuleLayoutRamDisk(JctCompiler compiler) {
+ @DisplayName("I can compile a single module using multi-module layout using a RAM disk on Javac")
+ @JavacCompilerTest(minVersion = 9, configurers = IntegrationTestConfigurer.class)
+ void singleModuleInMultiModuleLayoutRamDiskJavac(JctCompiler compiler) {
try (var workspace = Workspaces.newWorkspace(PathStrategy.RAM_DIRECTORIES)) {
- // Given
- workspace
- .createSourcePathModule("hello.world.singlemodule")
- .copyContentsFrom(resourcesDirectory().resolve("hello.world.singlemodule"));
-
- // When
- var compilation = compiler.compile(workspace);
-
- // Then
- assertThatCompilation(compilation)
- .isSuccessfulWithoutWarnings()
- .classOutputModules()
- .moduleExists("hello.world.singlemodule")
- .satisfies(
- module -> assertThatContainerGroup(module)
- .fileExists("com", "example", "HelloWorld.class")
- .isNotEmptyFile(),
- module -> assertThatContainerGroup(module)
- .fileExists("module-info.class")
- .isNotEmptyFile()
- );
+ runSingleModuleInMultiModuleLayoutTest(compiler, workspace);
}
}
- @DisplayName("I can compile a single module using multi-module layout using a temp directory")
- @JavacCompilerTest(minVersion = 9)
- void singleModuleInMultiModuleLayoutTempDirectory(JctCompiler compiler) {
+ @DisplayName(
+ "I can compile a single module using multi-module layout using a temp directory on Javac"
+ )
+ @JavacCompilerTest(minVersion = 9, configurers = IntegrationTestConfigurer.class)
+ void singleModuleInMultiModuleLayoutTempDirectoryJavac(JctCompiler compiler) {
try (var workspace = Workspaces.newWorkspace(PathStrategy.TEMP_DIRECTORIES)) {
- // Given
- workspace
- .createSourcePathModule("hello.world.singlemodule")
- .copyContentsFrom(resourcesDirectory().resolve("hello.world.singlemodule"));
-
- // When
- var compilation = compiler.compile(workspace);
-
- // Then
- assertThatCompilation(compilation)
- .isSuccessfulWithoutWarnings()
- .classOutputModules()
- .moduleExists("hello.world.singlemodule")
- .satisfies(
- module -> assertThatContainerGroup(module)
- .fileExists("com", "example", "HelloWorld.class")
- .isNotEmptyFile(),
- module -> assertThatContainerGroup(module)
- .fileExists("module-info.class")
- .isNotEmptyFile()
- );
+ runSingleModuleInMultiModuleLayoutTest(compiler, workspace);
}
}
- @DisplayName("I can compile multiple modules using multi-module layout using a RAM disk")
- @JavacCompilerTest(minVersion = 9)
- void multipleModulesInMultiModuleLayoutRamDisk(JctCompiler compiler) {
+ @DisplayName("I can compile multiple modules using multi-module layout using a RAM disk on Javac")
+ @JavacCompilerTest(minVersion = 9, configurers = IntegrationTestConfigurer.class)
+ void multipleModulesInMultiModuleLayoutRamDiskJavac(JctCompiler compiler) {
try (var workspace = Workspaces.newWorkspace(PathStrategy.RAM_DIRECTORIES)) {
- // Given
- workspace
- .createSourcePathModule("hello.world.crossmodule")
- .copyContentsFrom(resourcesDirectory().resolve("hello.world.crossmodule"));
-
- workspace
- .createSourcePathModule("greeter")
- .copyContentsFrom(resourcesDirectory().resolve("greeter"));
-
- // When
- var compilation = compiler.compile(workspace);
-
- // Then
- assertThatCompilation(compilation)
- .isSuccessfulWithoutWarnings();
-
- assertThatCompilation(compilation)
- .classOutputModules()
- .moduleExists("hello.world.crossmodule")
- .satisfies(
- module -> assertThatContainerGroup(module)
- .fileExists("com", "example", "HelloWorld.class")
- .isNotEmptyFile(),
- module -> assertThatContainerGroup(module)
- .fileExists("module-info.class")
- .isNotEmptyFile()
- );
-
- assertThatCompilation(compilation)
- .classOutputModules()
- .moduleExists("greeter")
- .satisfies(
- module -> assertThatContainerGroup(module)
- .fileExists("com", "example", "greeter", "Greeter.class")
- .isNotEmptyFile(),
- module -> assertThatContainerGroup(module)
- .fileExists("module-info.class")
- .isNotEmptyFile()
- );
+ runMultipleModulesInMultiModuleLayoutTest(compiler, workspace);
}
}
- @DisplayName("I can compile multiple modules using multi-module layout using a temp directory")
- @JavacCompilerTest(minVersion = 9)
- void multipleModulesInMultiModuleLayoutTempDirectory(JctCompiler compiler) {
+ @DisplayName(
+ "I can compile multiple modules using multi-module layout using a temp directory on Javac"
+ )
+ @JavacCompilerTest(minVersion = 9, configurers = IntegrationTestConfigurer.class)
+ void multipleModulesInMultiModuleLayoutTempDirectoryJavac(JctCompiler compiler) {
try (var workspace = Workspaces.newWorkspace(PathStrategy.TEMP_DIRECTORIES)) {
- // Given
- workspace
- .createSourcePathModule("hello.world.crossmodule")
- .copyContentsFrom(resourcesDirectory().resolve("hello.world.crossmodule"));
-
- workspace
- .createSourcePathModule("greeter")
- .copyContentsFrom(resourcesDirectory().resolve("greeter"));
-
- // When
- var compilation = compiler.compile(workspace);
-
- assertThatCompilation(compilation)
- .isSuccessfulWithoutWarnings();
-
- assertThatCompilation(compilation)
- .classOutputModules()
- .moduleExists("hello.world.crossmodule")
- .satisfies(
- module -> assertThatContainerGroup(module)
- .fileExists("com", "example", "HelloWorld.class")
- .isNotEmptyFile(),
- module -> assertThatContainerGroup(module)
- .fileExists("module-info.class")
- .isNotEmptyFile()
- );
-
- assertThatCompilation(compilation)
- .classOutputModules()
- .moduleExists("greeter")
- .satisfies(
- module -> assertThatContainerGroup(module)
- .fileExists("com", "example", "greeter", "Greeter.class")
- .isNotEmptyFile(),
- module -> assertThatContainerGroup(module)
- .fileExists("module-info.class")
- .isNotEmptyFile()
- );
+ runMultipleModulesInMultiModuleLayoutTest(compiler, workspace);
}
}
+
+ @Disabled("ECJ support is buggy for multi-module sources")
+ @DisplayName(
+ "I can compile a single module using multi-module layout using a temp directory on ECJ"
+ )
+ @EcjCompilerTest(minVersion = 9, configurers = IntegrationTestConfigurer.class)
+ void singleModuleInMultiModuleLayoutTempDirectoryEcj(JctCompiler compiler) {
+ try (var workspace = Workspaces.newWorkspace(PathStrategy.TEMP_DIRECTORIES)) {
+ runSingleModuleInMultiModuleLayoutTest(compiler, workspace);
+ }
+ }
+
+ @Disabled("ECJ support is buggy for multi-module sources")
+ @DisplayName(
+ "I can compile multiple modules using multi-module layout using a temp directory on ECJ"
+ )
+ @EcjCompilerTest(minVersion = 9, configurers = IntegrationTestConfigurer.class)
+ void multipleModulesInMultiModuleLayoutTempDirectoryEcj(JctCompiler compiler) {
+ try (var workspace = Workspaces.newWorkspace(PathStrategy.TEMP_DIRECTORIES)) {
+ runMultipleModulesInMultiModuleLayoutTest(compiler, workspace);
+ }
+ }
+
+ private void runSingleModuleInMultiModuleLayoutTest(
+ JctCompiler compiler,
+ Workspace workspace
+ ) {
+ workspace
+ .createSourcePathModule("hello.world.singlemodule")
+ .copyContentsFrom(resourcesDirectory().resolve("hello.world.singlemodule"));
+
+ // When
+ var compilation = compiler.compile(workspace);
+
+ // Then
+ assertThatCompilation(compilation)
+ .isSuccessfulWithoutWarnings();
+
+ assertThatCompilation(compilation)
+ .classOutputModules()
+ .moduleExists("hello.world.singlemodule")
+ .fileExists("com", "example", "HelloWorld.class").isNotEmptyFile();
+
+ assertThatCompilation(compilation)
+ .classOutputModules()
+ .moduleExists("hello.world.singlemodule")
+ .fileExists("module-info.class").isNotEmptyFile();
+ }
+
+ private void runMultipleModulesInMultiModuleLayoutTest(
+ JctCompiler compiler,
+ Workspace workspace
+ ) {
+ // Given
+ workspace
+ .createSourcePathModule("hello.world.crossmodule")
+ .copyContentsFrom(resourcesDirectory().resolve("hello.world.crossmodule"));
+
+ workspace
+ .createSourcePathModule("greeter")
+ .copyContentsFrom(resourcesDirectory().resolve("greeter"));
+
+ // When
+ var compilation = compiler.compile(workspace);
+
+ assertThatCompilation(compilation)
+ .isSuccessfulWithoutWarnings();
+
+ assertThatCompilation(compilation)
+ .classOutputModules()
+ .moduleExists("hello.world.crossmodule")
+ .fileExists("com", "example", "HelloWorld.class").isNotEmptyFile();
+
+ assertThatCompilation(compilation)
+ .classOutputModules()
+ .moduleExists("hello.world.crossmodule")
+ .fileExists("module-info.class").isNotEmptyFile();
+
+ assertThatCompilation(compilation)
+ .classOutputModules()
+ .moduleExists("greeter")
+ .fileExists("com", "example", "greeter", "Greeter.class").isNotEmptyFile();
+
+ assertThatCompilation(compilation)
+ .classOutputModules()
+ .moduleExists("greeter")
+ .fileExists("module-info.class").isNotEmptyFile();
+ }
}
diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/integration/compilation/CompilingSpecificClassesIntegrationTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/integration/compilation/CompilingSpecificClassesIntegrationTest.java
index 04c6c22f1..109fbc48d 100644
--- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/integration/compilation/CompilingSpecificClassesIntegrationTest.java
+++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/integration/compilation/CompilingSpecificClassesIntegrationTest.java
@@ -18,8 +18,12 @@
import static io.github.ascopes.jct.assertions.JctAssertions.assertThat;
import io.github.ascopes.jct.compilers.JctCompiler;
+import io.github.ascopes.jct.junit.EcjCompilerTest;
import io.github.ascopes.jct.junit.JavacCompilerTest;
import io.github.ascopes.jct.tests.integration.AbstractIntegrationTest;
+import io.github.ascopes.jct.tests.integration.IntegrationTestConfigurer;
+import io.github.ascopes.jct.workspaces.PathStrategy;
+import io.github.ascopes.jct.workspaces.Workspace;
import io.github.ascopes.jct.workspaces.Workspaces;
import org.junit.jupiter.api.DisplayName;
@@ -31,21 +35,33 @@
@DisplayName("Compiling specific classes integration tests")
class CompilingSpecificClassesIntegrationTest extends AbstractIntegrationTest {
- @DisplayName("Only the classes that I specify get compiled")
- @JavacCompilerTest
- void onlyTheClassesSpecifiedGetCompiled(JctCompiler compiler) {
+ @DisplayName("Only the classes that I specify get compiled with Javac")
+ @JavacCompilerTest(configurers = IntegrationTestConfigurer.class)
+ void onlyTheClassesSpecifiedGetCompiledJavac(JctCompiler compiler) {
try (var workspace = Workspaces.newWorkspace()) {
- workspace
- .createSourcePathPackage()
- .copyContentsFrom(resourcesDirectory());
-
- var compilation = compiler.compile(workspace, "Fibonacci", "HelloWorld");
+ runTest(compiler, workspace);
+ }
+ }
- assertThat(compilation)
- .isSuccessfulWithoutWarnings()
- .classOutputPackages()
- .allFilesExist("Fibonacci.class", "HelloWorld.class")
- .fileDoesNotExist("Sum.class");
+ @DisplayName("Only the classes that I specify get compiled with ECJ")
+ @EcjCompilerTest(configurers = IntegrationTestConfigurer.class)
+ void onlyTheClassesSpecifiedGetCompiledEcj(JctCompiler compiler) {
+ try (var workspace = Workspaces.newWorkspace(PathStrategy.TEMP_DIRECTORIES)) {
+ runTest(compiler, workspace);
}
}
+
+ private void runTest(JctCompiler compiler, Workspace workspace) {
+ workspace
+ .createSourcePathPackage()
+ .copyContentsFrom(resourcesDirectory());
+
+ var compilation = compiler.compile(workspace, "Fibonacci", "HelloWorld");
+
+ assertThat(compilation)
+ .isSuccessfulWithoutWarnings()
+ .classOutputPackages()
+ .allFilesExist("Fibonacci.class", "HelloWorld.class")
+ .fileDoesNotExist("Sum.class");
+ }
}
diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/integration/compilation/MultiTieredCompilationIntegrationTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/integration/compilation/MultiTieredCompilationIntegrationTest.java
index f63b489d6..db05da236 100644
--- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/integration/compilation/MultiTieredCompilationIntegrationTest.java
+++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/integration/compilation/MultiTieredCompilationIntegrationTest.java
@@ -18,8 +18,12 @@
import static io.github.ascopes.jct.assertions.JctAssertions.assertThatCompilation;
import io.github.ascopes.jct.compilers.JctCompiler;
+import io.github.ascopes.jct.junit.EcjCompilerTest;
import io.github.ascopes.jct.junit.JavacCompilerTest;
import io.github.ascopes.jct.tests.integration.AbstractIntegrationTest;
+import io.github.ascopes.jct.tests.integration.IntegrationTestConfigurer;
+import io.github.ascopes.jct.workspaces.PathStrategy;
+import io.github.ascopes.jct.workspaces.Workspace;
import io.github.ascopes.jct.workspaces.Workspaces;
import org.junit.jupiter.api.DisplayName;
@@ -34,85 +38,134 @@ class MultiTieredCompilationIntegrationTest extends AbstractIntegrationTest {
@DisplayName(
"I can compile sources to classes and provide them in the classpath to a second compilation"
+ + "for Javac"
)
- @JavacCompilerTest
- void compileSourcesToClassesAndProvideThemInClassPathToSecondCompilation(
+ @JavacCompilerTest(configurers = IntegrationTestConfigurer.class)
+ void compileSourcesToClassesAndProvideThemInClassPathToSecondCompilationForJavac(
JctCompiler compiler
) {
try (
var firstWorkspace = Workspaces.newWorkspace();
var secondWorkspace = Workspaces.newWorkspace()
) {
- firstWorkspace
- .createSourcePathPackage()
- .createDirectory("org", "example", "first")
- .copyContentsFrom(resourcesDirectory().resolve("first"));
-
- var firstCompilation = compiler.compile(firstWorkspace);
- assertThatCompilation(firstCompilation)
- .isSuccessfulWithoutWarnings()
- .classOutputPackages()
- .fileExists("org", "example", "first", "Adder.class")
- .isRegularFile()
- .isNotEmptyFile();
-
- secondWorkspace.addClassPathPackage(firstWorkspace.getClassOutputPackages().get(0).getPath());
- secondWorkspace.createSourcePathPackage()
- .createDirectory("org", "example", "second")
- .copyContentsFrom(resourcesDirectory().resolve("second"));
-
- var secondCompilation = compiler.compile(secondWorkspace);
- assertThatCompilation(secondCompilation)
- .isSuccessfulWithoutWarnings()
- .classOutputPackages()
- .fileExists("org", "example", "second", "Main.class")
- .isRegularFile()
- .isNotEmptyFile();
+ runClassPathPackagesTest(compiler, firstWorkspace, secondWorkspace);
}
}
@DisplayName(
"I can compile sources to classes and provide them in the classpath to a second "
- + "compilation within a JAR"
+ + "compilation within a JAR for Javac"
)
- @JavacCompilerTest
- void compileSourcesToClassesAndProvideThemInClassPathToSecondCompilationWithinJar(
+ @JavacCompilerTest(configurers = IntegrationTestConfigurer.class)
+ void compileSourcesToClassesAndProvideThemInClassPathToSecondCompilationWithinJarForJavac(
JctCompiler compiler
) {
try (
var firstWorkspace = Workspaces.newWorkspace();
var secondWorkspace = Workspaces.newWorkspace()
) {
- firstWorkspace
- .createSourcePathPackage()
- .copyContentsFrom(resourcesDirectory().resolve("first"));
-
- var firstCompilation = compiler.compile(firstWorkspace);
- assertThatCompilation(firstCompilation)
- .isSuccessfulWithoutWarnings()
- .classOutputPackages()
- .fileExists("org", "example", "first", "Adder.class")
- .isRegularFile()
- .isNotEmptyFile();
-
- firstWorkspace
- .createClassOutputPackage()
- .createFile("first.jar")
- .asJarFrom(firstWorkspace.getClassOutputPackages().get(0));
-
- var firstJar = firstWorkspace.getClassOutputPackages().get(1).getPath().resolve("first.jar");
- secondWorkspace.addClassPathPackage(firstJar);
- secondWorkspace
- .createSourcePathPackage()
- .copyContentsFrom(resourcesDirectory().resolve("second"));
-
- var secondCompilation = compiler.compile(secondWorkspace);
- assertThatCompilation(secondCompilation)
- .isSuccessfulWithoutWarnings()
- .classOutputPackages()
- .fileExists("org", "example", "second", "Main.class")
- .isRegularFile()
- .isNotEmptyFile();
+ runClassPathJarTest(compiler, firstWorkspace, secondWorkspace);
}
}
+
+ @DisplayName(
+ "I can compile sources to classes and provide them in the classpath to a second compilation"
+ + "for ECJ"
+ )
+ @EcjCompilerTest(configurers = IntegrationTestConfigurer.class)
+ void compileSourcesToClassesAndProvideThemInClassPathToSecondCompilationForEcj(
+ JctCompiler compiler
+ ) {
+ try (
+ var firstWorkspace = Workspaces.newWorkspace(PathStrategy.TEMP_DIRECTORIES);
+ var secondWorkspace = Workspaces.newWorkspace(PathStrategy.TEMP_DIRECTORIES)
+ ) {
+ runClassPathPackagesTest(compiler, firstWorkspace, secondWorkspace);
+ }
+ }
+
+ @DisplayName(
+ "I can compile sources to classes and provide them in the classpath to a second "
+ + "compilation within a JAR for ECJ"
+ )
+ @EcjCompilerTest(configurers = IntegrationTestConfigurer.class)
+ void compileSourcesToClassesAndProvideThemInClassPathToSecondCompilationWithinJarForEcj(
+ JctCompiler compiler
+ ) {
+ try (
+ var firstWorkspace = Workspaces.newWorkspace(PathStrategy.TEMP_DIRECTORIES);
+ var secondWorkspace = Workspaces.newWorkspace(PathStrategy.TEMP_DIRECTORIES)
+ ) {
+ runClassPathJarTest(compiler, firstWorkspace, secondWorkspace);
+ }
+ }
+
+ private void runClassPathPackagesTest(
+ JctCompiler compiler,
+ Workspace firstWorkspace,
+ Workspace secondWorkspace
+ ) {
+ firstWorkspace
+ .createSourcePathPackage()
+ .createDirectory("org", "example", "first")
+ .copyContentsFrom(resourcesDirectory().resolve("first"));
+
+ var firstCompilation = compiler.compile(firstWorkspace);
+ assertThatCompilation(firstCompilation)
+ .isSuccessfulWithoutWarnings()
+ .classOutputPackages()
+ .fileExists("org", "example", "first", "Adder.class")
+ .isRegularFile()
+ .isNotEmptyFile();
+
+ secondWorkspace.addClassPathPackage(firstWorkspace.getClassOutputPackages().get(0).getPath());
+ secondWorkspace.createSourcePathPackage()
+ .createDirectory("org", "example", "second")
+ .copyContentsFrom(resourcesDirectory().resolve("second"));
+
+ var secondCompilation = compiler.compile(secondWorkspace);
+ assertThatCompilation(secondCompilation)
+ .isSuccessfulWithoutWarnings()
+ .classOutputPackages()
+ .fileExists("org", "example", "second", "Main.class")
+ .isRegularFile()
+ .isNotEmptyFile();
+ }
+
+ private void runClassPathJarTest(
+ JctCompiler compiler,
+ Workspace firstWorkspace,
+ Workspace secondWorkspace
+ ) {
+ firstWorkspace
+ .createSourcePathPackage()
+ .copyContentsFrom(resourcesDirectory().resolve("first"));
+
+ var firstCompilation = compiler.compile(firstWorkspace);
+ assertThatCompilation(firstCompilation)
+ .isSuccessfulWithoutWarnings()
+ .classOutputPackages()
+ .fileExists("org", "example", "first", "Adder.class")
+ .isRegularFile()
+ .isNotEmptyFile();
+
+ firstWorkspace
+ .createClassOutputPackage()
+ .createFile("first.jar")
+ .asJarFrom(firstWorkspace.getClassOutputPackages().get(0));
+
+ var firstJar = firstWorkspace.getClassOutputPackages().get(1).getPath().resolve("first.jar");
+ secondWorkspace.addClassPathPackage(firstJar);
+ secondWorkspace
+ .createSourcePathPackage()
+ .copyContentsFrom(resourcesDirectory().resolve("second"));
+
+ var secondCompilation = compiler.compile(secondWorkspace);
+ assertThatCompilation(secondCompilation)
+ .isSuccessfulWithoutWarnings()
+ .classOutputPackages()
+ .fileExists("org", "example", "second", "Main.class")
+ .isRegularFile()
+ .isNotEmptyFile();
+ }
}
diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/JctCompilersTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/JctCompilersTest.java
index e61fd16a8..09762c6c8 100644
--- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/JctCompilersTest.java
+++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/JctCompilersTest.java
@@ -18,6 +18,7 @@
import static org.assertj.core.api.Assertions.assertThat;
import io.github.ascopes.jct.compilers.JctCompilers;
+import io.github.ascopes.jct.compilers.impl.EcjJctCompilerImpl;
import io.github.ascopes.jct.compilers.impl.JavacJctCompilerImpl;
import io.github.ascopes.jct.tests.helpers.UtilityClassTestTemplate;
import org.junit.jupiter.api.DisplayName;
@@ -54,4 +55,22 @@ void newPlatformCompilerReturnsTheExpectedInstance() {
.satisfies(constructed -> assertThat(compiler).isSameAs(constructed));
}
}
+
+ @DisplayName(".newEcjCompiler() creates an EcjJctCompilerImpl instance")
+ @Test
+ void newEcjCompilerReturnsTheExpectedInstance() {
+ try (var ecjJctCompilerImplMock = Mockito.mockConstruction(EcjJctCompilerImpl.class)) {
+ // When
+ var compiler = JctCompilers.newEcjCompiler();
+
+ // Then
+ assertThat(compiler)
+ .isInstanceOf(EcjJctCompilerImpl.class);
+
+ assertThat(ecjJctCompilerImplMock.constructed())
+ .singleElement()
+ // Nested assertion to swap expected/actual args.
+ .satisfies(constructed -> assertThat(compiler).isSameAs(constructed));
+ }
+ }
}
diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/impl/EcjJctFlagBuilderImplTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/impl/EcjJctFlagBuilderImplTest.java
new file mode 100644
index 000000000..3abec815a
--- /dev/null
+++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/impl/EcjJctFlagBuilderImplTest.java
@@ -0,0 +1,484 @@
+/*
+ * 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.tests.helpers.Fixtures.someBoolean;
+import static io.github.ascopes.jct.tests.helpers.Fixtures.someRelease;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import io.github.ascopes.jct.compilers.CompilationMode;
+import io.github.ascopes.jct.compilers.impl.EcjJctFlagBuilderImpl;
+import io.github.ascopes.jct.tests.helpers.Fixtures;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/**
+ * {@link EcjJctFlagBuilderImpl} tests.
+ *
+ * @author Ashley Scopes
+ */
+@DisplayName("EcjJctFlagBuilderImpl tests")
+@TestMethodOrder(OrderAnnotation.class)
+class EcjJctFlagBuilderImplTest {
+
+ EcjJctFlagBuilderImpl flagBuilder;
+
+ @BeforeEach
+ void setUp() {
+ flagBuilder = new EcjJctFlagBuilderImpl();
+ }
+
+ @DisplayName(".verbose(boolean) tests")
+ @Nested
+ class VerboseFlagTest {
+
+ @DisplayName("Setting .verbose(true) adds the expected flags")
+ @ValueSource(strings = {
+ "-verbose",
+ "-XprintProcessorInfo",
+ "-XprintRounds",
+ })
+ @ParameterizedTest(name = "for \"{0}\"")
+ void addsFlagIfTrue(String flag) {
+ // When
+ flagBuilder.verbose(true);
+
+ // Then
+ assertThat(flagBuilder.build()).contains(flag);
+ }
+
+ @DisplayName("Setting .verbose(false) does not add the expected flags")
+ @ValueSource(strings = {
+ "-verbose",
+ "-XprintProcessorInfo",
+ "-XprintRounds",
+ })
+ @ParameterizedTest(name = "for \"{0}\"")
+ void doesNotAddFlagIfFalse(String flag) {
+ // When
+ flagBuilder.verbose(false);
+
+ // Then
+ assertThat(flagBuilder.build()).doesNotContain(flag);
+ }
+
+ @DisplayName(".verbose(...) returns the flag builder")
+ @Test
+ void returnsFlagBuilder() {
+ // Then
+ assertThat(flagBuilder.verbose(someBoolean()))
+ .isSameAs(flagBuilder);
+ }
+ }
+
+ @DisplayName(".previewFeatures(boolean) tests")
+ @Nested
+ class PreviewFeaturesFlagTest {
+
+ @DisplayName("Setting .previewFeatures(true) adds the '--enable-preview' flag")
+ @Test
+ void addsFlagIfTrue() {
+ // When
+ flagBuilder.previewFeatures(true);
+
+ // Then
+ assertThat(flagBuilder.build()).contains("--enable-preview");
+ }
+
+ @DisplayName("Setting .previewFeatures(false) does not add the '--enable-preview' flag")
+ @Test
+ void doesNotAddFlagIfFalse() {
+ // When
+ flagBuilder.previewFeatures(false);
+
+ // Then
+ assertThat(flagBuilder.build()).doesNotContain("--enable-preview");
+ }
+
+ @DisplayName(".previewFeatures(...) returns the flag builder")
+ @Test
+ void returnsFlagBuilder() {
+ // Then
+ assertThat(flagBuilder.previewFeatures(someBoolean()))
+ .isSameAs(flagBuilder);
+ }
+ }
+
+ @DisplayName(".showWarnings(boolean) tests")
+ @Nested
+ class ShowWarningsFlagTest {
+
+ @DisplayName("Setting .showWarnings(true) does not add the '-nowarn' flag")
+ @Test
+ void doesNotAddFlagIfTrue() {
+ // When
+ flagBuilder.showWarnings(true);
+
+ // Then
+ assertThat(flagBuilder.build()).doesNotContain("-nowarn");
+ }
+
+ @DisplayName("Setting .showWarnings(false) adds the '-nowarn' flag")
+ @Test
+ void addsFlagIfFalse() {
+ // When
+ flagBuilder.showWarnings(false);
+
+ // Then
+ assertThat(flagBuilder.build()).contains("-nowarn");
+ }
+
+ @DisplayName(".showWarnings(...) returns the flag builder")
+ @Test
+ void returnsFlagBuilder() {
+ // Then
+ assertThat(flagBuilder.showWarnings(someBoolean()))
+ .isSameAs(flagBuilder);
+ }
+ }
+
+ @DisplayName(".failOnWarnings(boolean) tests")
+ @Nested
+ class FailOnWarningsFlagTest {
+
+ @DisplayName("Setting .failOnWarnings(true) adds the '--failOnWarning' flag")
+ @Test
+ void addsFlagIfTrue() {
+ // When
+ flagBuilder.failOnWarnings(true);
+
+ // Then
+ assertThat(flagBuilder.build()).contains("--failOnWarning");
+ }
+
+ @DisplayName("Setting .failOnWarnings(false) does not add the '--failOnWarning' flag")
+ @Test
+ void doesNotAddFlagIfFalse() {
+ // When
+ flagBuilder.failOnWarnings(false);
+
+ // Then
+ assertThat(flagBuilder.build()).doesNotContain("--failOnWarning");
+ }
+
+ @DisplayName(".failOnWarnings(...) returns the flag builder")
+ @Test
+ void returnsFlagBuilder() {
+ // Then
+ assertThat(flagBuilder.failOnWarnings(someBoolean()))
+ .isSameAs(flagBuilder);
+ }
+ }
+
+ @DisplayName(".compilationMode(CompilationMode) tests")
+ @Nested
+ class CompilationModeFlagTest {
+
+ @DisplayName(".compilationMode(COMPILATION_AND_ANNOTATION_PROCESSING) adds no flags")
+ @Test
+ void compilationAndAnnotationProcessingAddsNoFlags() {
+ // When
+ flagBuilder.compilationMode(CompilationMode.COMPILATION_AND_ANNOTATION_PROCESSING);
+
+ // Then
+ assertThat(flagBuilder.build()).isEmpty();
+ }
+
+ @DisplayName(".compilationMode(COMPILATION_ONLY) adds -proc:none")
+ @Test
+ void compilationOnlyAddsProcNone() {
+ // When
+ flagBuilder.compilationMode(CompilationMode.COMPILATION_ONLY);
+
+ // Then
+ assertThat(flagBuilder.build()).containsExactly("-proc:none");
+ }
+
+ @DisplayName(".compilationMode(ANNOTATION_PROCESSING_ONLY) adds -proc:only")
+ @Test
+ void annotationProcessingOnlyAddsProcOnly() {
+ // When
+ flagBuilder.compilationMode(CompilationMode.ANNOTATION_PROCESSING_ONLY);
+
+ // Then
+ assertThat(flagBuilder.build()).containsExactly("-proc:only");
+ }
+
+ @DisplayName(".compilationMode(...) returns the flag builder")
+ @EnumSource(CompilationMode.class)
+ @ParameterizedTest(name = "for compilationMode = {0}")
+ void returnsFlagBuilder(CompilationMode mode) {
+ // Then
+ assertThat(flagBuilder.compilationMode(mode))
+ .isSameAs(flagBuilder);
+ }
+ }
+
+ @DisplayName(".showDeprecationWarnings(boolean) tests")
+ @Nested
+ class ShowDeprecationWarningsFlagTest {
+
+ @DisplayName("Setting .showDeprecationWarnings(true) adds the '-deprecation' flag")
+ @Test
+ void addsFlagIfTrue() {
+ // When
+ flagBuilder.showDeprecationWarnings(true);
+
+ // Then
+ assertThat(flagBuilder.build()).contains("-deprecation");
+ }
+
+ @DisplayName("Setting .showDeprecationWarnings(false) does not add the '-deprecation' flag")
+ @Test
+ void doesNotAddFlagIfFalse() {
+ // When
+ flagBuilder.showDeprecationWarnings(false);
+
+ // Then
+ assertThat(flagBuilder.build()).doesNotContain("-deprecation");
+ }
+
+ @DisplayName(".showDeprecationWarnings(...) returns the flag builder")
+ @Test
+ void returnsFlagBuilder() {
+ // Then
+ assertThat(flagBuilder.showDeprecationWarnings(someBoolean()))
+ .isSameAs(flagBuilder);
+ }
+ }
+
+ @DisplayName(".release(String) tests")
+ @Nested
+ class ReleaseFlagTest {
+
+ @DisplayName("Setting .release(String) adds the '--release ' flag")
+ @ValueSource(strings = {"8", "11", "17"})
+ @ParameterizedTest(name = "Setting .release(String) adds the '--release {0}' flag")
+ void addsFlagIfPresent(String version) {
+ // When
+ flagBuilder.release(version);
+
+ // Then
+ assertThat(flagBuilder.build()).containsSequence("--release", version);
+ }
+
+ @DisplayName("Setting .release(null) does not add the '--release' flag")
+ @Test
+ void doesNotAddFlagIfNotPresent() {
+ // When
+ flagBuilder.release(null);
+
+ // Then
+ assertThat(flagBuilder.build()).doesNotContain("--release");
+ }
+
+ @DisplayName(".release(...) returns the flag builder")
+ @Test
+ void returnsFlagBuilder() {
+ // Then
+ assertThat(flagBuilder.release(someRelease()))
+ .isSameAs(flagBuilder);
+ }
+ }
+
+ @DisplayName(".source(String) tests")
+ @Nested
+ class SourceFlagTest {
+
+ @DisplayName("Setting .source(String) adds the '-source ' flag")
+ @ValueSource(strings = {"8", "11", "17"})
+ @ParameterizedTest(name = "Setting .source(String) adds the '-source {0}' flag")
+ void addsFlagIfPresent(String version) {
+ // When
+ flagBuilder.source(version);
+
+ // Then
+ assertThat(flagBuilder.build()).containsSequence("-source", version);
+ }
+
+ @DisplayName("Setting .source(null) does not add the '-source' flag")
+ @Test
+ void doesNotAddFlagIfNotPresent() {
+ // When
+ flagBuilder.source(null);
+
+ // Then
+ assertThat(flagBuilder.build()).doesNotContain("-source");
+ }
+
+
+ @DisplayName(".source(...) returns the flag builder")
+ @Test
+ void returnsFlagBuilder() {
+ // Then
+ assertThat(flagBuilder.source(someRelease()))
+ .isSameAs(flagBuilder);
+ }
+ }
+
+ @DisplayName(".target(String) tests")
+ @Nested
+ class TargetFlagTest {
+
+ @DisplayName("Setting .target(String) adds the '-target ' flag")
+ @ValueSource(strings = {"8", "11", "17"})
+ @ParameterizedTest(name = "Setting .target(String) adds the '-target {0}' flag")
+ void addsFlagIfPresent(String version) {
+ // When
+ flagBuilder.target(version);
+
+ // Then
+ assertThat(flagBuilder.build()).containsSequence("-target", version);
+ }
+
+ @DisplayName("Setting .target(null) does not add the '-target' flag")
+ @Test
+ void doesNotAddFlagIfNotPresent() {
+ // When
+ flagBuilder.target(null);
+
+ // Then
+ assertThat(flagBuilder.build()).doesNotContain("-target");
+ }
+
+ @DisplayName(".target(...) returns the flag builder")
+ @Test
+ void returnsFlagBuilder() {
+ // Then
+ assertThat(flagBuilder.target(someRelease()))
+ .isSameAs(flagBuilder);
+ }
+ }
+
+ @DisplayName(".addAnnotationProcessorOptions(List) tests")
+ @Nested
+ class AnnotationProcessorOptionsTest {
+
+ @DisplayName("Setting .annotationProcessorOptions(List) adds the options")
+ @Test
+ void addsAnnotationProcessorOptions() {
+ // Given
+ var options = Stream
+ .generate(Fixtures::someText)
+ .limit(5)
+ .collect(Collectors.toList());
+
+ // When
+ flagBuilder.annotationProcessorOptions(options);
+
+ // Then
+ assertThat(flagBuilder.build())
+ .containsSequence(options.stream()
+ .map("-A"::concat)
+ .collect(Collectors.toList()));
+ }
+
+ @DisplayName(".annotationProcessorOptions(...) returns the flag builder")
+ @Test
+ void returnsFlagBuilder() {
+ // Given
+ var options = Stream
+ .generate(Fixtures::someText)
+ .limit(5)
+ .collect(Collectors.toList());
+
+ // Then
+ assertThat(flagBuilder.annotationProcessorOptions(options))
+ .isSameAs(flagBuilder);
+ }
+ }
+
+ @DisplayName(".compilerOptions(List) tests")
+ @Nested
+ class CompilerOptionsTest {
+
+ @DisplayName("Setting .compilerOptions(List) adds the options")
+ @Test
+ void addsCompilerOptions() {
+ // Given
+ var options = Stream
+ .generate(Fixtures::someText)
+ .limit(5)
+ .collect(Collectors.toList());
+
+ // When
+ flagBuilder.compilerOptions(options);
+
+ // Then
+ assertThat(flagBuilder.build())
+ .containsSequence(options);
+ }
+
+ @DisplayName(".compilerOptions(...) returns the flag builder")
+ @Test
+ void returnsFlagBuilder() {
+ // Given
+ var options = Stream
+ .generate(Fixtures::someText)
+ .limit(5)
+ .collect(Collectors.toList());
+
+ // Then
+ assertThat(flagBuilder.compilerOptions(options))
+ .isSameAs(flagBuilder);
+ }
+ }
+
+ @Order(Integer.MAX_VALUE - 1)
+ @DisplayName("The flag builder adds multiple flags correctly")
+ @Test
+ void addsMultipleFlagsCorrectly() {
+ // When
+ var flags = flagBuilder
+ .compilerOptions(List.of("--foo", "--bar"))
+ .release("15")
+ .annotationProcessorOptions(List.of("--baz", "--bork"))
+ .build();
+
+ // Then
+ assertThat(flags)
+ .containsExactly("--foo", "--bar", "--release", "15", "-A--baz", "-A--bork");
+ }
+
+ @Order(Integer.MAX_VALUE)
+ @DisplayName("The flag builder produces an immutable list as the result")
+ @SuppressWarnings("DataFlowIssue")
+ @Test
+ void resultIsImmutable() {
+ // When
+ var flags = flagBuilder
+ .compilerOptions(List.of("--foo", "--bar"))
+ .release("15")
+ .annotationProcessorOptions(List.of("--baz", "--bork"))
+ .build();
+
+ // Then
+ assertThatThrownBy(() -> flags.add("something"))
+ .isInstanceOf(UnsupportedOperationException.class);
+ }
+}
diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/impl/JavacJctFlagBuilderImplTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/impl/JavacJctFlagBuilderImplTest.java
index 765365449..eec3c5cb9 100644
--- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/impl/JavacJctFlagBuilderImplTest.java
+++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/impl/JavacJctFlagBuilderImplTest.java
@@ -457,6 +457,7 @@ void addsMultipleFlagsCorrectly() {
@Order(Integer.MAX_VALUE)
@DisplayName("The flag builder produces an immutable list as the result")
+ @SuppressWarnings("DataFlowIssue")
@Test
void resultIsImmutable() {
// When
diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/impl/JctCompilationFactoryImplTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/impl/JctCompilationFactoryImplTest.java
index a194d3040..33c3ee27f 100644
--- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/impl/JctCompilationFactoryImplTest.java
+++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/impl/JctCompilationFactoryImplTest.java
@@ -335,9 +335,9 @@ void anErrorIsRaisedIfNoCompilationUnitsAreFound() throws IOException {
verifyNoInteractions(javaCompiler);
}
- @DisplayName("The compiler should write logs to System.out")
+ @DisplayName("The compiler should write logs to System.err")
@Test
- void theCompilerShouldWriteLogsToSystemOut() throws IOException {
+ void theCompilerShouldWriteLogsToSystemErr() throws IOException {
// Given
try (var teeWriterStatic = mockStatic(TeeWriter.class)) {
var teeWriter = mock(TeeWriter.class);
@@ -356,7 +356,7 @@ void theCompilerShouldWriteLogsToSystemOut() throws IOException {
// Then
teeWriterStatic.verify(() -> TeeWriter.wrapOutputStream(
- System.out,
+ System.err,
jctCompiler.getLogCharset()
));
teeWriterStatic.verifyNoMoreInteractions();
diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/ForwardingJctFileManagerTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/ForwardingJctFileManagerTest.java
new file mode 100644
index 000000000..8b8fe42c1
--- /dev/null
+++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/ForwardingJctFileManagerTest.java
@@ -0,0 +1,655 @@
+/*
+ * 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;
+
+import static io.github.ascopes.jct.tests.helpers.Fixtures.oneOf;
+import static io.github.ascopes.jct.tests.helpers.Fixtures.someBinaryName;
+import static io.github.ascopes.jct.tests.helpers.Fixtures.someBoolean;
+import static io.github.ascopes.jct.tests.helpers.Fixtures.someClassName;
+import static io.github.ascopes.jct.tests.helpers.Fixtures.someFlag;
+import static io.github.ascopes.jct.tests.helpers.Fixtures.someInt;
+import static io.github.ascopes.jct.tests.helpers.Fixtures.someJavaFileObject;
+import static io.github.ascopes.jct.tests.helpers.Fixtures.someLocation;
+import static io.github.ascopes.jct.tests.helpers.Fixtures.someModuleName;
+import static io.github.ascopes.jct.tests.helpers.Fixtures.someOf;
+import static io.github.ascopes.jct.tests.helpers.Fixtures.somePackageName;
+import static io.github.ascopes.jct.tests.helpers.Fixtures.somePathRoot;
+import static io.github.ascopes.jct.tests.helpers.Fixtures.someRelativePath;
+import static io.github.ascopes.jct.tests.helpers.Fixtures.someRelease;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import io.github.ascopes.jct.containers.ModuleContainerGroup;
+import io.github.ascopes.jct.containers.OutputContainerGroup;
+import io.github.ascopes.jct.containers.PackageContainerGroup;
+import io.github.ascopes.jct.filemanagers.ForwardingJctFileManager;
+import io.github.ascopes.jct.filemanagers.JctFileManager;
+import io.github.ascopes.jct.workspaces.PathRoot;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.ServiceLoader;
+import java.util.Set;
+import javax.tools.JavaFileManager;
+import javax.tools.JavaFileObject;
+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.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+/**
+ * {@link ForwardingJctFileManager} tests.
+ *
+ * @author Ashley Scopes
+ */
+@ExtendWith(MockitoExtension.class)
+class ForwardingJctFileManagerTest {
+
+ @Mock
+ JctFileManager fileManagerImpl;
+
+ @InjectMocks
+ ForwardingJctFileManagerImpl forwardingFileManager;
+
+ //////////////////////////////////////////////////////////////////////////////////
+ /// Implementations that are provided by ForwardingJavaFileManager in the JDK. ///
+ //////////////////////////////////////////////////////////////////////////////////
+
+ @DisplayName(".getClassLoader(...) calls the same method on the implementation")
+ @ValueSource(booleans = {true, false})
+ @ParameterizedTest(name = "when method returns null = {0}")
+ void getClassLoaderCallsTheSameMethodOnTheImplementation(boolean returnNull) {
+ // Given
+ var expectedClassLoader = returnNull ? null : mock(ClassLoader.class);
+ when(fileManagerImpl.getClassLoader(any())).thenReturn(expectedClassLoader);
+ var expectedLocation = someLocation();
+
+ // When
+ var actualClassLoader = forwardingFileManager.getClassLoader(expectedLocation);
+
+ // Then
+ verify(fileManagerImpl).getClassLoader(expectedLocation);
+ verifyNoMoreInteractions(fileManagerImpl);
+ assertThat(actualClassLoader).isSameAs(expectedClassLoader);
+ }
+
+ @DisplayName(".inferBinaryName(...) calls the same method on the implementation")
+ @Test
+ void inferBinaryNameCallsTheSameMethodOnTheImplementation() {
+ // Given
+ var expectedBinaryName = someBinaryName();
+ when(fileManagerImpl.inferBinaryName(any(), any())).thenReturn(expectedBinaryName);
+ var expectedLocation = someLocation();
+ var expectedJavaFileObject = someJavaFileObject();
+
+ // When
+ var actualBinaryName = forwardingFileManager
+ .inferBinaryName(expectedLocation, expectedJavaFileObject);
+
+ // Then
+ verify(fileManagerImpl).inferBinaryName(expectedLocation, expectedJavaFileObject);
+ verifyNoMoreInteractions(fileManagerImpl);
+ assertThat(actualBinaryName).isEqualTo(expectedBinaryName);
+ }
+
+ @DisplayName(".isSameFile(...) calls the same method on the implementation")
+ @Test
+ void isSameFileCallsTheSameMethodOnTheImplementation() {
+ // Given
+ var expectedResult = someBoolean();
+ when(fileManagerImpl.isSameFile(any(), any())).thenReturn(expectedResult);
+ var expectedFirstFileObject = someJavaFileObject();
+ var expectedSecondFileObject = someJavaFileObject();
+
+ // When
+ var actualResult = forwardingFileManager
+ .isSameFile(expectedFirstFileObject, expectedSecondFileObject);
+
+ // Then
+ verify(fileManagerImpl).isSameFile(expectedFirstFileObject, expectedSecondFileObject);
+ verifyNoMoreInteractions(fileManagerImpl);
+ assertThat(actualResult).isEqualTo(expectedResult);
+ }
+
+ @DisplayName(".handleOption(...) calls the same method on the implementation")
+ @Test
+ void handleOptionCallsTheSameMethodOnTheImplementation() {
+ // Given
+ var expectedResult = someBoolean();
+ when(fileManagerImpl.handleOption(any(), any())).thenReturn(expectedResult);
+ var expectedCurrent = someFlag();
+ Iterator expectedRemaining = mock();
+
+ // When
+ var actualResult = forwardingFileManager.handleOption(expectedCurrent, expectedRemaining);
+
+ // Then
+ verify(fileManagerImpl).handleOption(expectedCurrent, expectedRemaining);
+ verifyNoMoreInteractions(fileManagerImpl);
+ assertThat(actualResult).isEqualTo(expectedResult);
+ }
+
+ @DisplayName(".hasLocation(...) calls the same method on the implementation")
+ @Test
+ void hasLocationCallsTheSameMethodOnTheImplementation() {
+ // Given
+ var expectedResult = someBoolean();
+ when(fileManagerImpl.hasLocation(any())).thenReturn(expectedResult);
+ var expectedLocation = someLocation();
+
+ // When
+ var actualResult = forwardingFileManager.hasLocation(expectedLocation);
+
+ // Then
+ verify(fileManagerImpl).hasLocation(expectedLocation);
+ verifyNoMoreInteractions(fileManagerImpl);
+ assertThat(actualResult).isEqualTo(expectedResult);
+ }
+
+ @DisplayName(".isSupportedOption(...) calls the same method on the implementation")
+ @Test
+ void isSupportedOptionCallsTheSameMethodOnTheImplementation() {
+ // Given
+ var expectedResult = someInt(1, 5);
+ when(fileManagerImpl.isSupportedOption(any())).thenReturn(expectedResult);
+ var expectedOption = someFlag();
+
+ // When
+ var actualResult = forwardingFileManager.isSupportedOption(expectedOption);
+
+ // Then
+ verify(fileManagerImpl).isSupportedOption(expectedOption);
+ verifyNoMoreInteractions(fileManagerImpl);
+ assertThat(actualResult).isEqualTo(expectedResult);
+ }
+
+ @DisplayName(".getJavaFileForInput(...) calls the same method on the implementation")
+ @ValueSource(booleans = {true, false})
+ @ParameterizedTest(name = "when method returns null = {0}")
+ void getJavaFileForInputCallsTheSameMethodOnTheImplementation(boolean returnNull)
+ throws IOException {
+
+ // Given
+ var expectedFile = returnNull ? null : someJavaFileObject();
+ when(fileManagerImpl.getJavaFileForInput(any(), any(), any())).thenReturn(expectedFile);
+ var expectedLocation = someLocation();
+ var expectedClassName = someClassName();
+ var expectedKind = oneOf(JavaFileObject.Kind.class);
+
+ // When
+ var actualFile = forwardingFileManager
+ .getJavaFileForInput(expectedLocation, expectedClassName, expectedKind);
+
+ // Then
+ verify(fileManagerImpl).getJavaFileForInput(expectedLocation, expectedClassName, expectedKind);
+ verifyNoMoreInteractions(fileManagerImpl);
+ assertThat(actualFile).isSameAs(expectedFile);
+ }
+
+ @DisplayName(".getJavaFileForOutput(...) calls the same method on the implementation")
+ @ValueSource(booleans = {true, false})
+ @ParameterizedTest(name = "when method returns null = {0}")
+ void getJavaFileForOutputCallsTheSameMethodOnTheImplementation(boolean returnNull)
+ throws IOException {
+
+ // Given
+ var expectedFile = returnNull ? null : someJavaFileObject();
+ when(fileManagerImpl.getJavaFileForOutput(any(), any(), any(), any())).thenReturn(expectedFile);
+ var expectedLocation = someLocation();
+ var expectedClassName = someClassName();
+ var expectedKind = oneOf(JavaFileObject.Kind.class);
+ var expectedSibling = returnNull ? null : someJavaFileObject();
+
+ // When
+ var actualFile = forwardingFileManager
+ .getJavaFileForOutput(expectedLocation, expectedClassName, expectedKind, expectedSibling);
+
+ // Then
+ verify(fileManagerImpl)
+ .getJavaFileForOutput(expectedLocation, expectedClassName, expectedKind, expectedSibling);
+ verifyNoMoreInteractions(fileManagerImpl);
+ assertThat(actualFile).isSameAs(expectedFile);
+ }
+
+ @DisplayName(".getFileForInput(...) calls the same method on the implementation")
+ @ValueSource(booleans = {true, false})
+ @ParameterizedTest(name = "when method returns null = {0}")
+ void getFileForInputCallsTheSameMethodOnTheImplementation(boolean returnNull)
+ throws IOException {
+
+ // Given
+ var expectedFile = returnNull ? null : someJavaFileObject();
+ when(fileManagerImpl.getFileForInput(any(), any(), any())).thenReturn(expectedFile);
+ var expectedLocation = someLocation();
+ var expectedPackageName = someClassName();
+ var expectedRelativeName = someRelativePath().toString();
+
+ // When
+ var actualFile = forwardingFileManager
+ .getFileForInput(expectedLocation, expectedPackageName, expectedRelativeName);
+
+ // Then
+ verify(fileManagerImpl)
+ .getFileForInput(expectedLocation, expectedPackageName, expectedRelativeName);
+ verifyNoMoreInteractions(fileManagerImpl);
+ assertThat(actualFile).isSameAs(expectedFile);
+ }
+
+ @DisplayName(".getFileForOutput(...) calls the same method on the implementation")
+ @ValueSource(booleans = {true, false})
+ @ParameterizedTest(name = "when method returns null = {0}")
+ void getFileForOutputCallsTheSameMethodOnTheImplementation(boolean returnNull)
+ throws IOException {
+
+ // Given
+ var expectedFile = returnNull ? null : someJavaFileObject();
+ when(fileManagerImpl.getFileForOutput(any(), any(), any(), any())).thenReturn(expectedFile);
+ var expectedLocation = someLocation();
+ var expectedPackageName = someClassName();
+ var expectedRelativeName = someRelativePath().toString();
+ var expectedSibling = returnNull ? null : someJavaFileObject();
+
+ // When
+ var actualFile = forwardingFileManager.getFileForOutput(
+ expectedLocation,
+ expectedPackageName,
+ expectedRelativeName,
+ expectedSibling
+ );
+
+ // Then
+ verify(fileManagerImpl).getFileForOutput(
+ expectedLocation,
+ expectedPackageName,
+ expectedRelativeName,
+ expectedSibling
+ );
+ verifyNoMoreInteractions(fileManagerImpl);
+ assertThat(actualFile).isSameAs(expectedFile);
+ }
+
+ @DisplayName(".flush() calls the same method on the implementation")
+ @Test
+ void flushCallsTheSameMethodOnTheImplementation() throws IOException {
+ // Given
+ doNothing().when(fileManagerImpl).flush();
+
+ // When
+ forwardingFileManager.flush();
+
+ // Then
+ verify(fileManagerImpl).flush();
+ verifyNoMoreInteractions(fileManagerImpl);
+ }
+
+ @DisplayName(".close() calls the same method on the implementation")
+ @Test
+ void closeCallsTheSameMethodOnTheImplementation() throws IOException {
+ // Given
+ doNothing().when(fileManagerImpl).close();
+
+ // When
+ forwardingFileManager.close();
+
+ // Then
+ verify(fileManagerImpl).close();
+ verifyNoMoreInteractions(fileManagerImpl);
+ }
+
+ @DisplayName(
+ ".getLocationForModule(Location, String) calls the same method on the implementation"
+ )
+ @Test
+ void getLocationForModuleLocationStringCallsTheSameMethodOnTheImplementation()
+ throws IOException {
+
+ // Given
+ var expectedLocation = someLocation();
+ when(fileManagerImpl.getLocationForModule(any(), any(String.class)))
+ .thenReturn(expectedLocation);
+ var expectedInputLocation = someLocation();
+ var expectedModuleName = someModuleName();
+
+ // When
+ var actualLocation = forwardingFileManager
+ .getLocationForModule(expectedInputLocation, expectedModuleName);
+
+ // Then
+ verify(fileManagerImpl)
+ .getLocationForModule(expectedInputLocation, expectedModuleName);
+ verifyNoMoreInteractions(fileManagerImpl);
+ assertThat(actualLocation).isSameAs(expectedLocation);
+ }
+
+ @DisplayName(
+ ".getLocationForModule(Location, JavaFileObject) calls the same method on the implementation"
+ )
+ @Test
+ void getLocationForModuleLocationJavaFileObjectCallsTheSameMethodOnTheImplementation()
+ throws IOException {
+
+ // Given
+ var expectedLocation = someLocation();
+ when(fileManagerImpl.getLocationForModule(any(), any(JavaFileObject.class)))
+ .thenReturn(expectedLocation);
+ var expectedInputLocation = someLocation();
+ var expectedJavaFileObject = someJavaFileObject();
+
+ // When
+ var actualLocation = forwardingFileManager
+ .getLocationForModule(expectedInputLocation, expectedJavaFileObject);
+
+ // Then
+ verify(fileManagerImpl).getLocationForModule(expectedInputLocation, expectedJavaFileObject);
+ verifyNoMoreInteractions(fileManagerImpl);
+ assertThat(actualLocation).isSameAs(expectedLocation);
+ }
+
+ @DisplayName(".getServiceLoader(...) calls the same method on the implementation")
+ @Test
+ void getServiceLoaderCallsTheSameMethodOnTheImplementation() throws IOException {
+ // Given
+ class SomeClass {
+ // Nothing to do here.
+ }
+
+ ServiceLoader
+
+
+ org.eclipse.jdt
+ ecj
+ ${ecj.version}
+
+
org.jspecifyjspecify
diff --git a/scripts/ecj.sh b/scripts/ecj.sh
new file mode 100755
index 000000000..98e380799
--- /dev/null
+++ b/scripts/ecj.sh
@@ -0,0 +1,53 @@
+#!/usr/bin/env bash
+#
+# 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.
+#
+
+###
+### Shortcut to running the ECJ compiler.
+###
+
+set -o errexit
+set -o nounset
+
+project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+ecj_dir="${project_dir}/target/ecj"
+if [[ ! -d "${ecj_dir}" ]] || [[ ! "$(ls -A "${ecj_dir}")" ]]; then
+ mkdir -p "${ecj_dir}"
+
+ echo "[[Determining ECJ version to use, please wait...]]" >&2
+ ecj_version="$("${project_dir}/mvnw" -f "${project_dir}/pom.xml" help:evaluate \
+ --offline \
+ --quiet \
+ -Dexpression="ecj.version" \
+ -DforceStdout)"
+
+ echo "[[Downloading ECJ ${ecj_version} artifact, please wait...]]" >&2
+ "${project_dir}/mvnw" dependency:get \
+ --quiet \
+ -Dartifact="org.eclipse.jdt:ecj:${ecj_version}"
+
+ echo "[[Copying ECJ ${ecj_version} artifact into ${ecj_dir}, please wait...]]" >&2
+ "${project_dir}/mvnw" dependency:copy \
+ --offline \
+ --quiet \
+ -Dartifact="org.eclipse.jdt:ecj:${ecj_version}" \
+ -DoutputDirectory="${ecj_dir}" \
+ -Dtransitive=true
+
+ echo "[[Completed download of ECJ ${ecj_version}.]]" >&2
+fi
+
+java -jar "$(find "${ecj_dir}" -type f -name "*.jar" -print | head -n 1)" "${@}"