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. + * + * @author Ashley Scopes + * @since TBC + */ +@ArgumentsSource(EcjCompilersProvider.class) +@DisabledInNativeImage +@Documented +@ParameterizedTest(name = "for compiler \"{0}\"") +@Retention(RetentionPolicy.RUNTIME) +@Tags({ + @Tag("java-compiler-testing-test"), + @Tag("ecj-test") +}) +@Target({ + ElementType.ANNOTATION_TYPE, + ElementType.METHOD, +}) +@TestTemplate +public @interface EcjCompilerTest { + + /** + * Minimum version to use (inclusive). + * + *
By default, it will use the lowest possible version supported by the compiler. This + * varies between versions of the JDK that are 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 + * varies between versions of the JDK that are 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 extends JctCompilerConfigurer>>[] configurers() default {};
+
+ /**
+ * 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 00000000..9587a0d1
--- /dev/null
+++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/EcjCompilersProvider.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2022 - 2024, 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 org.junit.jupiter.params.support.AnnotationConsumer;
+
+/**
+ * Argument provider for the {@link EcjCompilerTest} annotation.
+ *
+ * @author Ashley Scopes
+ * @since 5.0.0
+ */
+public final class EcjCompilersProvider extends AbstractCompilersProvider
+ implements AnnotationConsumer This is only visible for testing purposes, users should have no need to
+ * initialise this class directly.
+ */
+ 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 annotation) {
+ var min = annotation.minVersion();
+ var max = annotation.maxVersion();
+ var configurers = annotation.configurers();
+ var versioning = annotation.versionStrategy();
+ configure(min, max, configurers, versioning);
+ }
+}
diff --git a/java-compiler-testing/src/main/java/module-info.java b/java-compiler-testing/src/main/java/module-info.java
index ce510b28..d6a9ff90 100644
--- a/java-compiler-testing/src/main/java/module-info.java
+++ b/java-compiler-testing/src/main/java/module-info.java
@@ -75,7 +75,8 @@
*
* class JsonSchemaAnnotationProcessorTest {
*
- * {@literal @JavacCompilerTest(minVersion=11, maxVersion=19)}
+ * {@literal @EcjCompilerTest(minVersion=17)}
+ * {@literal @JavacCompilerTest(minVersion=17)}
* void theJsonSchemaIsCreatedFromTheInputCode(JctCompiler compiler) {
*
* try (var workspace = Workspaces.newWorkspace()) {
@@ -125,6 +126,7 @@
requires java.management;
requires me.xdrop.fuzzywuzzy;
requires org.assertj.core;
+ requires org.eclipse.jdt.core.compiler.batch;
requires static org.jspecify;
requires static org.junit.jupiter.api;
requires static org.junit.jupiter.params;
diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/compilers/JctCompilersTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/compilers/JctCompilersTest.java
index c89636de..3bd0c3c1 100644
--- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/compilers/JctCompilersTest.java
+++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/compilers/JctCompilersTest.java
@@ -17,6 +17,7 @@
import static org.assertj.core.api.Assertions.assertThat;
+import io.github.ascopes.jct.compilers.impl.EcjJctCompilerImpl;
import io.github.ascopes.jct.compilers.impl.JavacJctCompilerImpl;
import io.github.ascopes.jct.fixtures.UtilityClassTestTemplate;
import org.junit.jupiter.api.DisplayName;
@@ -53,4 +54,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/compilers/impl/EcjJctCompilerImplTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/compilers/impl/EcjJctCompilerImplTest.java
new file mode 100644
index 00000000..5febde2d
--- /dev/null
+++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/compilers/impl/EcjJctCompilerImplTest.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2022 - 2024, the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.github.ascopes.jct.compilers.impl;
+
+import static io.github.ascopes.jct.fixtures.Fixtures.someInt;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.mockStatic;
+
+import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
+import org.eclipse.jdt.internal.compiler.tool.EclipseCompiler;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+/**
+ * {@link EcjJctCompilerImpl} tests.
+ *
+ * @author Ashley Scopes
+ */
+@DisplayName("EcjJctCompilerImpl tests")
+class EcjJctCompilerImplTest {
+
+ EcjJctCompilerImpl compiler;
+
+ @BeforeEach
+ void setUp() {
+ compiler = new EcjJctCompilerImpl();
+ }
+
+ @DisplayName("Compilers have the expected JSR-199 compiler factory")
+ @Test
+ void compilersHaveTheExpectedCompilerFactory() {
+ // When
+ var actualCompiler = compiler.getCompilerFactory().createCompiler();
+
+ // Then
+ assertThat(actualCompiler).isInstanceOf(EclipseCompiler.class);
+ }
+
+ @DisplayName("Compilers have the expected flag builder factory")
+ @Test
+ void compilersHaveTheExpectedFlagBuilderFactory() {
+ // Given
+ try (var flagBuilderMock = mockConstruction(EcjJctFlagBuilderImpl.class)) {
+ // When
+ var flagBuilder = compiler.getFlagBuilderFactory().createFlagBuilder();
+
+ // Then
+ assertThat(flagBuilderMock.constructed()).hasSize(1);
+ assertThat(flagBuilder).isSameAs(flagBuilderMock.constructed().get(0));
+ }
+ }
+
+ @DisplayName("Compilers have the expected default release string")
+ @Test
+ void compilersHaveTheExpectedDefaultRelease() {
+ // Given
+ try (var compilerClassMock = mockStatic(EcjJctCompilerImpl.class)) {
+ var latestSupportedInt = someInt(17, 21);
+ compilerClassMock
+ .when(EcjJctCompilerImpl::getLatestSupportedVersionInt)
+ .thenReturn(latestSupportedInt);
+
+ // When
+ var defaultRelease = compiler.getDefaultRelease();
+
+ // Then
+ compilerClassMock
+ .verify(EcjJctCompilerImpl::getLatestSupportedVersionInt);
+
+ assertThat(defaultRelease)
+ .isEqualTo("%d", latestSupportedInt);
+ }
+ }
+
+ @DisplayName("Compilers have the expected default name")
+ @Test
+ void compilersHaveTheExpectedDefaultName() {
+ // Then
+ assertThat(compiler.getName()).isEqualTo("ECJ");
+ }
+
+ @DisplayName("Compilers have no default compiler flags set")
+ @Test
+ void compilersHaveNoDefaultCompilerFlagsSet() {
+ // Then
+ assertThat(compiler.getCompilerOptions()).isEmpty();
+ }
+
+ @DisplayName("Compilers have no default annotation processor flags set")
+ @Test
+ void compilersHaveNoDefaultAnnotationProcessorFlagsSet() {
+ // Then
+ assertThat(compiler.getAnnotationProcessorOptions()).isEmpty();
+ }
+
+ @DisplayName("Compilers have no default annotation processors set")
+ @Test
+ void compilersHaveNoDefaultAnnotationProcessorsSet() {
+ // Then
+ assertThat(compiler.getAnnotationProcessors()).isEmpty();
+ }
+
+ @DisplayName("Compilers have the expected latest release")
+ @Test
+ void latestSupportedVersionReturnsTheExpectedValue() {
+ // Given
+ var expected = (int) ((ClassFileConstants.getLatestJDKLevel() >> 16L)
+ - ClassFileConstants.MAJOR_VERSION_0);
+
+ // When
+ var actual = EcjJctCompilerImpl.getLatestSupportedVersionInt();
+
+ // Then
+ assertThat(expected).isEqualTo(actual);
+ }
+
+ @DisplayName("Compilers have the expected earliest release")
+ @Test
+ void earliestSupportedVersionReturnsTheExpectedValue() {
+ // Given
+ var expected = (int) ((ClassFileConstants.JDK1_8 >> 16L)
+ - ClassFileConstants.MAJOR_VERSION_0);
+
+ // When
+ var actual = EcjJctCompilerImpl.getEarliestSupportedVersionInt();
+
+ // Then
+ assertThat(expected).isEqualTo(actual);
+ }
+}
diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/compilers/impl/EcjJctFlagBuilderImplTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/compilers/impl/EcjJctFlagBuilderImplTest.java
new file mode 100644
index 00000000..ac49b202
--- /dev/null
+++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/compilers/impl/EcjJctFlagBuilderImplTest.java
@@ -0,0 +1,542 @@
+/*
+ * Copyright (C) 2022 - 2024, the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.github.ascopes.jct.compilers.impl;
+
+import static io.github.ascopes.jct.fixtures.Fixtures.someBoolean;
+import static io.github.ascopes.jct.fixtures.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.DebuggingInfo;
+import io.github.ascopes.jct.fixtures.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.CsvSource;
+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 '-verbose' flag")
+ @Test
+ void addsFlagIfTrue() {
+ // When
+ flagBuilder.verbose(true);
+
+ // Then
+ assertThat(flagBuilder.build()).contains("-verbose");
+ }
+
+ @DisplayName("Setting .verbose(false) does not add the '-verbose' flag")
+ @Test
+ void doesNotAddFlagIfFalse() {
+ // When
+ flagBuilder.verbose(false);
+
+ // Then
+ assertThat(flagBuilder.build()).doesNotContain("-verbose");
+ }
+
+ @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 '-Werror' flag")
+ @Test
+ void doesNotAddFlagIfFalse() {
+ // When
+ flagBuilder.failOnWarnings(false);
+
+ // Then
+ assertThat(flagBuilder.build()).doesNotContain("-Werror");
+ }
+
+ @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_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(COMPILATION_AND_ANNOTATION_PROCESSING) adds nothing")
+ @Test
+ void compilationAndAnnotationProcessingAddsNothing() {
+ // When
+ flagBuilder.compilationMode(CompilationMode.COMPILATION_AND_ANNOTATION_PROCESSING);
+
+ // Then
+ assertThat(flagBuilder.build()).isEmpty();
+ }
+
+ @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