diff --git a/acceptance-tests/acceptance-tests-avaje-inject/src/test/groovy/io/github/ascopes/jct/acceptancetests/avajeinject/AvajeInjectTest.groovy b/acceptance-tests/acceptance-tests-avaje-inject/src/test/groovy/io/github/ascopes/jct/acceptancetests/avajeinject/AvajeInjectTest.groovy index 1f86db409..b8672b8ce 100644 --- a/acceptance-tests/acceptance-tests-avaje-inject/src/test/groovy/io/github/ascopes/jct/acceptancetests/avajeinject/AvajeInjectTest.groovy +++ b/acceptance-tests/acceptance-tests-avaje-inject/src/test/groovy/io/github/ascopes/jct/acceptancetests/avajeinject/AvajeInjectTest.groovy @@ -15,42 +15,62 @@ */ package io.github.ascopes.jct.acceptancetests.avajeinject +import static io.github.ascopes.jct.assertions.JctAssertions.assertThatCompilation + import io.github.ascopes.jct.compilers.JctCompiler import io.github.ascopes.jct.filemanagers.LoggingMode +import io.github.ascopes.jct.junit.EcjCompilerTest import io.github.ascopes.jct.junit.JavacCompilerTest +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 -import static io.github.ascopes.jct.assertions.JctAssertions.assertThatCompilation - @DisplayName("Avaje Inject acceptance tests") class AvajeInjectTest { - @DisplayName("Dependency injection code gets generated as expected") + @DisplayName("Dependency injection code gets generated as expected for Javac") @JavacCompilerTest(minVersion = 11) - void dependencyInjectionCodeGetsGeneratedAsExpected(JctCompiler compiler) { + void dependencyInjectionCodeGetsGeneratedAsExpectedForJavac(JctCompiler compiler) { // Given try (def workspace = Workspaces.newWorkspace()) { - workspace - .createSourcePathPackage() - .copyContentsFrom("src", "test", "resources", "code") - - // When - def compilation = compiler - .diagnosticLoggingMode(LoggingMode.STACKTRACES) - .compile(workspace) + runTest(compiler, workspace) + } + } - // Then - assertThatCompilation(compilation) - .isSuccessfulWithoutWarnings() - .classOutputPackages() - .allFilesExist( - 'org/example/CoffeeMaker.class', - 'org/example/Grinder.class', - 'org/example/Pump.class', - 'org/example/CoffeeMaker$DI.class', - 'org/example/Grinder$DI.class', - 'org/example/Pump$DI.class', - ) + @DisplayName("Dependency injection code gets generated as expected for ECJ") + @EcjCompilerTest(minVersion = 11) + void dependencyInjectionCodeGetsGeneratedAsExpectedForEcj(JctCompiler compiler) { + // Given + try (def workspace = Workspaces.newWorkspace(PathStrategy.TEMP_DIRECTORIES)) { + runTest(compiler, workspace) } } + + private static void runTest(JctCompiler compiler, Workspace workspace) { + workspace + .createSourcePathPackage() + .copyContentsFrom("src", "test", "resources", "code") + + // When + def compilation = compiler + // TODO(ascopes): disable this + .fileManagerLoggingMode(LoggingMode.ENABLED) + .diagnosticLoggingMode(LoggingMode.STACKTRACES) + .verbose(true) + // end temporary block + .compile(workspace) + + // Then + assertThatCompilation(compilation) + .isSuccessfulWithoutWarnings() + .classOutputPackages() + .allFilesExist( + 'org/example/CoffeeMaker.class', + 'org/example/Grinder.class', + 'org/example/Pump.class', + 'org/example/CoffeeMaker$DI.class', + 'org/example/Grinder$DI.class', + 'org/example/Pump$DI.class', + ) + } } diff --git a/acceptance-tests/acceptance-tests-dogfood/pom.xml b/acceptance-tests/acceptance-tests-dogfood/pom.xml index b770b9109..35adb2bd6 100644 --- a/acceptance-tests/acceptance-tests-dogfood/pom.xml +++ b/acceptance-tests/acceptance-tests-dogfood/pom.xml @@ -37,6 +37,11 @@ test + + org.eclipse.jdt + ecj + + org.junit.jupiter junit-jupiter diff --git a/java-compiler-testing/pom.xml b/java-compiler-testing/pom.xml index c637be310..d6def850d 100644 --- a/java-compiler-testing/pom.xml +++ b/java-compiler-testing/pom.xml @@ -59,6 +59,11 @@ assertj-core + + org.eclipse.jdt + ecj + + org.jspecify jspecify diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/JctCompilers.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/JctCompilers.java index 34d670a5f..aedcd9286 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/JctCompilers.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/JctCompilers.java @@ -15,6 +15,7 @@ */ package io.github.ascopes.jct.compilers; +import io.github.ascopes.jct.compilers.impl.EcjJctCompilerImpl; import io.github.ascopes.jct.compilers.impl.JavacJctCompilerImpl; import io.github.ascopes.jct.utils.UtilityClass; import org.apiguardian.api.API; @@ -44,4 +45,18 @@ private JctCompilers() { public static JctCompiler newPlatformCompiler() { return new JavacJctCompilerImpl(); } + + /** + * Create a new instance of an ECJ compiler. + * + *

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 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): + * + *


+ *   class SomeTest {
+ *     {@literal @EcjCompilerTest(minVersion = 11, maxVersion = 14)}
+ *     void canCompileHelloWorld(JctCompiler<?, ?>> compiler) {
+ *       // Given
+ *       try (var workspace = Workspaces.newWorkspace()) {
+ *         workspace
+ *            .createFile("org", "example", "HelloWorld.java")
+ *            .withContents("""
+ *              package org.example;
+ *
+ *              public class HelloWorld {
+ *                public static void main(String[] args) {
+ *                  System.out.println("Hello, World!");
+ *                }
+ *              }
+ *            """);
+ *
+ *         var compilation = compiler.compile(workspace);
+ *
+ *         assertThat(compilation)
+ *             .isSuccessfulWithoutWarnings();
+ *       }
+ *     }
+ *   }
+ * 
+ * + *

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: + * + *


+   * module mytests {
+   *   requires io.github.ascopes.jct;
+   *   requires org.junit.jupiter.api;
+   *
+   *   opens org.example.mytests to io.github.ascopes.jct;
+   * }
+   * 
+ * + * An example of usage: + * + *

+   *   public class WerrorConfigurer implements JctCompilerConfigurer<RuntimeException> {
+   *     {@literal @Override}
+   *     public void configure(JctCompiler<?, ?> compiler) {
+   *       compiler.failOnWarnings(true);
+   *     }
+   *   }
+   *
+   *   // ...
+   *
+   *   class SomeTest {
+   *     {@literal @EcjCompilerTest(configurers = WerrorConfigurer.class)}
+   *     void someTest(JctCompiler<?, ?> compiler) {
+   *       // ...
+   *     }
+   *   }
+   * 
+ * + * @return an array of classes to run to configure the compiler. These run in the given order. + */ + Class>[] 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 expectedServiceLoader = mock(); + when(fileManagerImpl.getServiceLoader(any(), any())).thenReturn(expectedServiceLoader); + var expectedLocation = someLocation(); + + // When + var actualServiceLoader = forwardingFileManager + .getServiceLoader(expectedLocation, SomeClass.class); + + // Then + verify(fileManagerImpl).getServiceLoader(expectedLocation, SomeClass.class); + verifyNoMoreInteractions(fileManagerImpl); + assertThat(actualServiceLoader).isSameAs(expectedServiceLoader); + } + + @DisplayName(".inferModuleName(...) calls the same method on the implementation") + @Test + void inferModuleNameCallsTheSameMethodOnTheImplementation() throws IOException { + // Given + var expectedModuleName = someModuleName(); + when(fileManagerImpl.inferModuleName(any())).thenReturn(expectedModuleName); + var expectedLocation = someLocation(); + + // When + var actualModuleName = forwardingFileManager.inferModuleName(expectedLocation); + + // Then + verify(fileManagerImpl).inferModuleName(expectedLocation); + verifyNoMoreInteractions(fileManagerImpl); + assertThat(actualModuleName).isEqualTo(expectedModuleName); + } + + @DisplayName(".listLocationsForModules(...) calls the same method on the implementation") + @Test + void listLocationsForModulesCallsTheSameMethodOnTheImplementation() throws IOException { + // Given + Iterable> expectedIterable = mock(); + when(fileManagerImpl.listLocationsForModules(any())).thenReturn(expectedIterable); + var expectedLocation = someLocation(); + + // When + var actualIterable = forwardingFileManager.listLocationsForModules(expectedLocation); + + // Then + verify(fileManagerImpl).listLocationsForModules(expectedLocation); + verifyNoMoreInteractions(fileManagerImpl); + assertThat(actualIterable).isEqualTo(expectedIterable); + } + + @DisplayName(".contains(...) calls the same method on the implementation") + @Test + void containsCallsTheSameMethodOnTheImplementation() throws IOException { + // Given + var expectedResult = someBoolean(); + when(fileManagerImpl.contains(any(), any())).thenReturn(expectedResult); + var expectedLocation = someLocation(); + var expectedFileObject = someJavaFileObject(); + + // When + var actualResult = forwardingFileManager.contains(expectedLocation, expectedFileObject); + + // Then + verify(fileManagerImpl).contains(expectedLocation, expectedFileObject); + verifyNoMoreInteractions(fileManagerImpl); + assertThat(actualResult).isEqualTo(expectedResult); + } + + ////////////////////////////////////////////////////////////////////// + /// Implementations provided directly by ForwardingJctFileManager. /// + ////////////////////////////////////////////////////////////////////// + + @DisplayName(".addPath(...) calls the same method on the implementation") + @Test + void addPathCallsTheSameMethodOnTheImplementation() { + // Given + doNothing().when(fileManagerImpl).addPath(any(), any()); + var expectedLocation = someLocation(); + var expectedPathRoot = somePathRoot(); + + // When + forwardingFileManager.addPath(expectedLocation, expectedPathRoot); + + // Then + verify(fileManagerImpl).addPath(expectedLocation, expectedPathRoot); + verifyNoMoreInteractions(fileManagerImpl); + } + + @DisplayName(".addPaths(...) calls the same method on the implementation") + @Test + void addPathsCallsTheSameMethodOnTheImplementation() { + // Given + doNothing().when(fileManagerImpl).addPaths(any(), any()); + var expectedLocation = someLocation(); + Collection expectedPathRoots = mock(); + + // When + forwardingFileManager.addPaths(expectedLocation, expectedPathRoots); + + // Then + verify(fileManagerImpl).addPaths(expectedLocation, expectedPathRoots); + verifyNoMoreInteractions(fileManagerImpl); + } + + @DisplayName(".copyContainers(...) calls the same method on the implementation") + @Test + void copyContainersCallsTheSameMethodOnTheImplementation() { + // Given + doNothing().when(fileManagerImpl).copyContainers(any(), any()); + var expectedFromLocation = someLocation(); + var expectedToLocation = someLocation(); + + // When + forwardingFileManager.copyContainers(expectedFromLocation, expectedToLocation); + + // Then + verify(fileManagerImpl).copyContainers(expectedFromLocation, expectedToLocation); + verifyNoMoreInteractions(fileManagerImpl); + } + + @DisplayName(".createEmptyLocation(...) calls the same method on the implementation") + @Test + void createEmptyLocationCallsTheSameMethodOnTheImplementation() { + // Given + doNothing().when(fileManagerImpl).createEmptyLocation(any()); + var expectedLocation = someLocation(); + + // When + forwardingFileManager.createEmptyLocation(expectedLocation); + + // Then + verify(fileManagerImpl).createEmptyLocation(expectedLocation); + verifyNoMoreInteractions(fileManagerImpl); + } + + @DisplayName(".getEffectiveRelease() calls the same method on the implementation") + @Test + void getEffectiveReleaseCallsTheSameMethodOnTheImplementation() { + // Given + var expectedResult = someRelease(); + when(fileManagerImpl.getEffectiveRelease()).thenReturn(expectedResult); + + // When + var actualResult = forwardingFileManager.getEffectiveRelease(); + + // Then + verify(fileManagerImpl).getEffectiveRelease(); + verifyNoMoreInteractions(fileManagerImpl); + assertThat(actualResult).isEqualTo(expectedResult); + } + + @DisplayName(".getPackageContainerGroup(...) calls the same method on the implementation") + @Test + void getPackageContainerGroupCallsTheSameMethodOnTheImplementation() { + // Given + var expectedContainerGroup = mock(PackageContainerGroup.class); + when(fileManagerImpl.getPackageContainerGroup(any())).thenReturn(expectedContainerGroup); + var expectedLocation = someLocation(); + + // When + var actualContainerGroup = forwardingFileManager.getPackageContainerGroup(expectedLocation); + + // Then + verify(fileManagerImpl).getPackageContainerGroup(expectedLocation); + verifyNoMoreInteractions(fileManagerImpl); + assertThat(actualContainerGroup).isSameAs(expectedContainerGroup); + } + + @DisplayName(".getPackageContainerGroups() calls the same method on the implementation") + @Test + void getPackageContainerGroupsCallsTheSameMethodOnTheImplementation() { + // Given + Collection expectedContainerGroups = mock(); + when(fileManagerImpl.getPackageContainerGroups()).thenReturn(expectedContainerGroups); + + // When + var actualContainerGroups = forwardingFileManager.getPackageContainerGroups(); + + // Then + verify(fileManagerImpl).getPackageContainerGroups(); + verifyNoMoreInteractions(fileManagerImpl); + assertThat(actualContainerGroups).isSameAs(expectedContainerGroups); + } + + @DisplayName(".getModuleContainerGroup(...) calls the same method on the implementation") + @Test + void getModuleContainerGroupCallsTheSameMethodOnTheImplementation() { + // Given + var expectedContainerGroup = mock(ModuleContainerGroup.class); + when(fileManagerImpl.getModuleContainerGroup(any())).thenReturn(expectedContainerGroup); + var expectedLocation = someLocation(); + + // When + var actualContainerGroup = forwardingFileManager.getModuleContainerGroup(expectedLocation); + + // Then + verify(fileManagerImpl).getModuleContainerGroup(expectedLocation); + verifyNoMoreInteractions(fileManagerImpl); + assertThat(actualContainerGroup).isSameAs(expectedContainerGroup); + } + + @DisplayName(".getModuleContainerGroups() calls the same method on the implementation") + @Test + void getModuleContainerGroupsCallsTheSameMethodOnTheImplementation() { + // Given + Collection expectedContainerGroups = mock(); + when(fileManagerImpl.getModuleContainerGroups()).thenReturn(expectedContainerGroups); + + // When + var actualContainerGroups = forwardingFileManager.getModuleContainerGroups(); + + // Then + verify(fileManagerImpl).getModuleContainerGroups(); + verifyNoMoreInteractions(fileManagerImpl); + assertThat(actualContainerGroups).isSameAs(expectedContainerGroups); + } + + @DisplayName(".getOutputContainerGroup(...) calls the same method on the implementation") + @Test + void getOutputContainerGroupCallsTheSameMethodOnTheImplementation() { + // Given + var expectedContainerGroup = mock(OutputContainerGroup.class); + when(fileManagerImpl.getOutputContainerGroup(any())).thenReturn(expectedContainerGroup); + var expectedLocation = someLocation(); + + // When + var actualContainerGroup = forwardingFileManager.getOutputContainerGroup(expectedLocation); + + // Then + verify(fileManagerImpl).getOutputContainerGroup(expectedLocation); + verifyNoMoreInteractions(fileManagerImpl); + assertThat(actualContainerGroup).isSameAs(expectedContainerGroup); + } + + @DisplayName(".getOutputContainerGroups() calls the same method on the implementation") + @Test + void getOutputContainerGroupsCallsTheSameMethodOnTheImplementation() { + // Given + Collection expectedContainerGroups = mock(); + when(fileManagerImpl.getOutputContainerGroups()).thenReturn(expectedContainerGroups); + + // When + var actualContainerGroups = forwardingFileManager.getOutputContainerGroups(); + + // Then + verify(fileManagerImpl).getOutputContainerGroups(); + verifyNoMoreInteractions(fileManagerImpl); + assertThat(actualContainerGroups).isSameAs(expectedContainerGroups); + } + + @DisplayName(".list(...) calls the same method on the implementation") + @Test + void listCallsTheSameMethodOnTheImplementation() throws IOException { + // Given + Set expectedSet = mock(); + when(fileManagerImpl.list(any(), any(), any(), anyBoolean())).thenReturn(expectedSet); + var expectedLocation = someLocation(); + var expectedPackageName = somePackageName(); + var expectedKinds = someOf(JavaFileObject.Kind.class); + var expectedRecurse = someBoolean(); + + // When + var actualSet = forwardingFileManager + .list(expectedLocation, expectedPackageName, expectedKinds, expectedRecurse); + + // Then + verify(fileManagerImpl) + .list(expectedLocation, expectedPackageName, expectedKinds, expectedRecurse); + verifyNoMoreInteractions(fileManagerImpl); + assertThat(actualSet).isSameAs(expectedSet); + } + + static final class ForwardingJctFileManagerImpl extends ForwardingJctFileManager { + + ForwardingJctFileManagerImpl(JctFileManager fileManager) { + super(fileManager); + } + } +} diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/junit/EcjCompilersProviderTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/junit/EcjCompilersProviderTest.java new file mode 100644 index 000000000..f23cab468 --- /dev/null +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/junit/EcjCompilersProviderTest.java @@ -0,0 +1,170 @@ +/* + * 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.junit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.junit.jupiter.params.support.AnnotationConsumerInitializer.initialize; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import io.github.ascopes.jct.compilers.JctCompilerConfigurer; +import io.github.ascopes.jct.compilers.impl.EcjJctCompilerImpl; +import io.github.ascopes.jct.junit.EcjCompilerTest; +import io.github.ascopes.jct.junit.EcjCompilersProvider; +import io.github.ascopes.jct.junit.VersionStrategy; +import java.lang.reflect.AnnotatedElement; +import java.util.stream.Collectors; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; + +/** + * {@link EcjCompilersProvider} tests. + */ +@DisplayName("EcjCompilersProvider tests") +class EcjCompilersProviderTest { + + @DisplayName("Provider uses the user-provided compiler version bounds when valid") + @Test + void providerUsesTheUserProvidedVersionRangesWhenValid() { + // Given + try (var ecjMock = mockStatic(EcjJctCompilerImpl.class)) { + ecjMock.when(EcjJctCompilerImpl::getEarliestSupportedVersionInt).thenReturn(8); + ecjMock.when(EcjJctCompilerImpl::getLatestSupportedVersionInt).thenReturn(17); + var annotation = someAnnotation(10, 15); + var test = someAnnotatedElement(annotation); + var context = mock(ExtensionContext.class); + + // When + var consumer = initialize(test, new EcjCompilersProvider()); + var compilers = consumer.provideArguments(context) + .map(args -> (EcjJctCompilerImpl) args.get()[0]) + .collect(Collectors.toList()); + + // Then + assertThat(compilers) + .as("compilers that were initialised (%s)", compilers) + .hasSize(6); + + assertSoftly(softly -> { + for (var i = 0; i < compilers.size(); ++i) { + var compiler = compilers.get(i); + softly.assertThat(compiler.getName()) + .as("compilers[%d].getName()", i) + .isEqualTo("ECJ (release = Java %d)", 10 + i); + softly.assertThat(compiler.getRelease()) + .as("compilers[%d].getRelease()", i) + .isEqualTo("%d", 10 + i); + } + }); + } + } + + @DisplayName("Provider uses the minimum compiler version that is allowed if exceeded") + @Test + void providerUsesTheMinCompilerVersionAllowedIfExceeded() { + // Given + try (var ecjMock = mockStatic(EcjJctCompilerImpl.class)) { + ecjMock.when(EcjJctCompilerImpl::getEarliestSupportedVersionInt).thenReturn(10); + ecjMock.when(EcjJctCompilerImpl::getLatestSupportedVersionInt).thenReturn(17); + var annotation = someAnnotation(1, 15); + var test = someAnnotatedElement(annotation); + var context = mock(ExtensionContext.class); + + // When + var consumer = initialize(test, new EcjCompilersProvider()); + var compilers = consumer.provideArguments(context) + .map(args -> (EcjJctCompilerImpl) args.get()[0]) + .collect(Collectors.toList()); + + // Then + assertThat(compilers) + .as("compilers that were initialised (%s)", compilers) + .hasSize(6); + + assertSoftly(softly -> { + for (var i = 0; i < compilers.size(); ++i) { + var compiler = compilers.get(i); + softly.assertThat(compiler.getName()) + .as("compilers[%d].getName()", i) + .isEqualTo("ECJ (release = Java %d)", 10 + i); + softly.assertThat(compiler.getRelease()) + .as("compilers[%d].getRelease()", i) + .isEqualTo("%d", 10 + i); + } + }); + } + } + + @DisplayName("Provider uses the maximum compiler version that is allowed if exceeded") + @Test + void providerUsesTheMaxCompilerVersionAllowedIfExceeded() { + // Given + try (var ecjMock = mockStatic(EcjJctCompilerImpl.class)) { + ecjMock.when(EcjJctCompilerImpl::getEarliestSupportedVersionInt).thenReturn(8); + ecjMock.when(EcjJctCompilerImpl::getLatestSupportedVersionInt).thenReturn(17); + var annotation = someAnnotation(10, 17); + var test = someAnnotatedElement(annotation); + var context = mock(ExtensionContext.class); + + // When + var consumer = initialize(test, new EcjCompilersProvider()); + var compilers = consumer.provideArguments(context) + .map(args -> (EcjJctCompilerImpl) args.get()[0]) + .collect(Collectors.toList()); + + // Then + assertThat(compilers) + .as("compilers that were initialised (%s)", compilers) + .hasSize(8); + + assertSoftly(softly -> { + for (var i = 0; i < compilers.size(); ++i) { + var compiler = compilers.get(i); + softly.assertThat(compiler.getName()) + .as("compilers[%d].getName()", i) + .isEqualTo("ECJ (release = Java %d)", 10 + i); + softly.assertThat(compiler.getRelease()) + .as("compilers[%d].getRelease()", i) + .isEqualTo("%d", 10 + i); + } + }); + } + } + + @SafeVarargs + final EcjCompilerTest someAnnotation( + int min, + int max, + Class>... configurers + ) { + var annotation = mock(EcjCompilerTest.class); + when(annotation.minVersion()).thenReturn(min); + when(annotation.maxVersion()).thenReturn(max); + when(annotation.configurers()).thenReturn(configurers); + when(annotation.versionStrategy()).thenReturn(VersionStrategy.RELEASE); + when(annotation.annotationType()).thenAnswer(ctx -> EcjCompilerTest.class); + return annotation; + } + + AnnotatedElement someAnnotatedElement(EcjCompilerTest annotation) { + var element = mock(AnnotatedElement.class); + when(element.getDeclaredAnnotation(EcjCompilerTest.class)).thenReturn(annotation); + return element; + } +} diff --git a/pom.xml b/pom.xml index f5854366d..b2b2aaf8e 100644 --- a/pom.xml +++ b/pom.xml @@ -96,6 +96,7 @@ 1.1.2 3.24.2 4.2.0 + 3.33.0 1.4.0 4.0.13 0.3.0 @@ -212,6 +213,13 @@ ${groovy.version} + + + org.eclipse.jdt + ecj + ${ecj.version} + + org.jspecify jspecify 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)" "${@}"