diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ea209d4be..23f8fc8a0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,8 +72,7 @@ jobs: env: cache-name: maven-cache with: - path: - ~/.m2 + path: ~/.m2 key: build-${{ env.cache-name }} - name: Compile and run tests diff --git a/java-compiler-testing/pom.xml b/java-compiler-testing/pom.xml index 0cd2177b0..0b252ebfa 100644 --- a/java-compiler-testing/pom.xml +++ b/java-compiler-testing/pom.xml @@ -53,6 +53,11 @@ assertj-core + + org.eclipse.jdt + ecj + + org.jspecify jspecify diff --git a/java-compiler-testing/src/it/avaje-http/src/test/java/io/github/ascopes/jct/acceptancetests/avajehttp/AvajeHttpTest.java b/java-compiler-testing/src/it/avaje-http/src/test/java/io/github/ascopes/jct/acceptancetests/avajehttp/AvajeHttpTest.java index aed596e38..3468c770a 100644 --- a/java-compiler-testing/src/it/avaje-http/src/test/java/io/github/ascopes/jct/acceptancetests/avajehttp/AvajeHttpTest.java +++ b/java-compiler-testing/src/it/avaje-http/src/test/java/io/github/ascopes/jct/acceptancetests/avajehttp/AvajeHttpTest.java @@ -18,6 +18,7 @@ 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.workspaces.Workspaces; import org.junit.jupiter.api.DisplayName; @@ -26,6 +27,7 @@ class AvajeHttpTest { @DisplayName("HTTP client code gets generated as expected") + @EcjCompilerTest(minVersion = 11) @JavacCompilerTest(minVersion = 11) void httpClientCodeGetsGeneratedAsExpected(JctCompiler compiler) { // Given @@ -40,7 +42,7 @@ void httpClientCodeGetsGeneratedAsExpected(JctCompiler compiler) { // Then assertThatCompilation(compilation) - .isSuccessfulWithoutWarnings() + .isSuccessful() .classOutputPackages() .allFilesExist( "org/example/httpclient/GeneratedHttpComponent.class", diff --git a/java-compiler-testing/src/it/avaje-inject/src/test/java/io/github/ascopes/jct/acceptancetests/avajeinject/AvajeInjectTest.java b/java-compiler-testing/src/it/avaje-inject/src/test/java/io/github/ascopes/jct/acceptancetests/avajeinject/AvajeInjectTest.java index 7c7e839a1..112a0d1af 100644 --- a/java-compiler-testing/src/it/avaje-inject/src/test/java/io/github/ascopes/jct/acceptancetests/avajeinject/AvajeInjectTest.java +++ b/java-compiler-testing/src/it/avaje-inject/src/test/java/io/github/ascopes/jct/acceptancetests/avajeinject/AvajeInjectTest.java @@ -18,6 +18,7 @@ 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.workspaces.PathStrategy; import io.github.ascopes.jct.workspaces.Workspaces; @@ -27,6 +28,7 @@ class AvajeInjectTest { @DisplayName("Dependency injection code gets generated as expected") + @EcjCompilerTest(minVersion = 11) @JavacCompilerTest(minVersion = 11) void dependencyInjectionCodeGetsGeneratedAsExpected(JctCompiler compiler) { // Given @@ -41,7 +43,7 @@ void dependencyInjectionCodeGetsGeneratedAsExpected(JctCompiler compiler) { // Then assertThatCompilation(compilation) - .isSuccessfulWithoutWarnings() + .isSuccessful() .classOutputPackages() .allFilesExist( "org/example/CoffeeMaker.class", diff --git a/java-compiler-testing/src/it/avaje-jsonb/src/test/java/io/github/ascopes/jct/acceptancetests/avajejsonb/AvajeJsonbTest.java b/java-compiler-testing/src/it/avaje-jsonb/src/test/java/io/github/ascopes/jct/acceptancetests/avajejsonb/AvajeJsonbTest.java index 8332c8f9e..22d798056 100644 --- a/java-compiler-testing/src/it/avaje-jsonb/src/test/java/io/github/ascopes/jct/acceptancetests/avajejsonb/AvajeJsonbTest.java +++ b/java-compiler-testing/src/it/avaje-jsonb/src/test/java/io/github/ascopes/jct/acceptancetests/avajejsonb/AvajeJsonbTest.java @@ -21,6 +21,7 @@ import io.avaje.jsonb.Jsonb; import io.avaje.jsonb.generator.Processor; 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.workspaces.Workspaces; import java.time.Instant; @@ -30,6 +31,7 @@ @DisplayName("Avaje Jsonb acceptance tests") class AvajeJsonbTest { @DisplayName("JSON handling logic is generated as expected") + @EcjCompilerTest(minVersion = 11) @JavacCompilerTest(minVersion = 11) void jsonHandlingLogicIsGeneratedAsExpected(JctCompiler compiler) throws Throwable { // Given @@ -46,7 +48,7 @@ void jsonHandlingLogicIsGeneratedAsExpected(JctCompiler compiler) throws Throwab // Then assertThatCompilation(compilation) - .isSuccessfulWithoutWarnings(); + .isSuccessful(); var userClass = compilation .getClassOutputs() diff --git a/java-compiler-testing/src/it/dagger/src/test/java/io/github/ascopes/jct/acceptancetests/dagger/DaggerTest.java b/java-compiler-testing/src/it/dagger/src/test/java/io/github/ascopes/jct/acceptancetests/dagger/DaggerTest.java index 4f1d6cafa..cbb065740 100644 --- a/java-compiler-testing/src/it/dagger/src/test/java/io/github/ascopes/jct/acceptancetests/dagger/DaggerTest.java +++ b/java-compiler-testing/src/it/dagger/src/test/java/io/github/ascopes/jct/acceptancetests/dagger/DaggerTest.java @@ -18,6 +18,7 @@ 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.workspaces.Workspaces; import org.junit.jupiter.api.DisplayName; @@ -25,6 +26,7 @@ @DisplayName("Dagger acceptance tests") class DaggerTest { @DisplayName("Dagger DI runs as expected in the annotation processing phase") + @EcjCompilerTest @JavacCompilerTest void daggerDiRunsAsExpectedInTheAnnotationProcessingPhase(JctCompiler compiler) { try (var workspace = Workspaces.newWorkspace()) { @@ -38,7 +40,7 @@ void daggerDiRunsAsExpectedInTheAnnotationProcessingPhase(JctCompiler compiler) var compilation = compiler.compile(workspace); // Then - assertThatCompilation(compilation).isSuccessfulWithoutWarnings(); + assertThatCompilation(compilation).isSuccessful(); assertThatCompilation(compilation) .sourceOutputPackages() diff --git a/java-compiler-testing/src/it/dogfood/src/test/java/io/github/ascopes/jct/acceptancetests/dogfood/JctDogfoodTest.java b/java-compiler-testing/src/it/dogfood/src/test/java/io/github/ascopes/jct/acceptancetests/dogfood/JctDogfoodTest.java index 2817aba72..04f3744ca 100644 --- a/java-compiler-testing/src/it/dogfood/src/test/java/io/github/ascopes/jct/acceptancetests/dogfood/JctDogfoodTest.java +++ b/java-compiler-testing/src/it/dogfood/src/test/java/io/github/ascopes/jct/acceptancetests/dogfood/JctDogfoodTest.java @@ -18,6 +18,7 @@ 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.workspaces.Workspaces; import java.io.IOException; @@ -52,6 +53,7 @@ class JctDogfoodTest { .resolve("classes"); @DisplayName("JCT can compile itself as a legacy module source") + @EcjCompilerTest(minVersion = 17, configurers = JctCompilationConfigurer.class) @JavacCompilerTest(minVersion = 17, configurers = JctCompilationConfigurer.class) void jctCanCompileItselfAsLegacyModule(JctCompiler compiler) throws IOException { // Given diff --git a/java-compiler-testing/src/it/google-auto-factory/src/test/java/io/github/ascopes/jct/acceptancetests/autofactory/AutoFactoryTest.java b/java-compiler-testing/src/it/google-auto-factory/src/test/java/io/github/ascopes/jct/acceptancetests/autofactory/AutoFactoryTest.java index 04a28737f..c9e3ede3b 100644 --- a/java-compiler-testing/src/it/google-auto-factory/src/test/java/io/github/ascopes/jct/acceptancetests/autofactory/AutoFactoryTest.java +++ b/java-compiler-testing/src/it/google-auto-factory/src/test/java/io/github/ascopes/jct/acceptancetests/autofactory/AutoFactoryTest.java @@ -19,6 +19,7 @@ import static org.assertj.core.api.SoftAssertions.assertSoftly; 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.workspaces.Workspaces; import java.time.Instant; @@ -28,6 +29,7 @@ class AutoFactoryTest { @DisplayName("The AutoFactory class is created as expected") + @EcjCompilerTest @JavacCompilerTest void autoFactoryClassIsCreatedAsExpected(JctCompiler compiler) throws Throwable { try (var workspace = Workspaces.newWorkspace()) { @@ -42,7 +44,7 @@ void autoFactoryClassIsCreatedAsExpected(JctCompiler compiler) throws Throwable // Then assertThatCompilation(compilation) - .isSuccessfulWithoutWarnings() + .isSuccessful() .classOutputPackages() .fileExists("org", "example", "UserFactory.class") .isNotEmptyFile(); diff --git a/java-compiler-testing/src/it/google-auto-service/src/test/java/io/github/ascopes/jct/acceptancetests/autoservice/AutoServiceTest.java b/java-compiler-testing/src/it/google-auto-service/src/test/java/io/github/ascopes/jct/acceptancetests/autoservice/AutoServiceTest.java index 4f81b41d1..e8fb74250 100644 --- a/java-compiler-testing/src/it/google-auto-service/src/test/java/io/github/ascopes/jct/acceptancetests/autoservice/AutoServiceTest.java +++ b/java-compiler-testing/src/it/google-auto-service/src/test/java/io/github/ascopes/jct/acceptancetests/autoservice/AutoServiceTest.java @@ -18,6 +18,7 @@ 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.workspaces.Workspaces; import org.junit.jupiter.api.DisplayName; @@ -26,6 +27,7 @@ class AutoServiceTest { @DisplayName("The AutoService descriptor is created as expected") + @EcjCompilerTest @JavacCompilerTest void autoServiceDescriptorIsCreatedAsExpected(JctCompiler compiler) { try (var workspace = Workspaces.newWorkspace()) { @@ -40,7 +42,7 @@ void autoServiceDescriptorIsCreatedAsExpected(JctCompiler compiler) { // Then assertThatCompilation(compilation) - .isSuccessfulWithoutWarnings() + .isSuccessful() .classOutputPackages() .fileExists("META-INF", "services", "org.example.SomeInterface") .hasContent("org.example.SomeImpl"); diff --git a/java-compiler-testing/src/it/google-auto-value/src/test/java/io/github/ascopes/jct/acceptancetests/autovalue/AutoValueTest.java b/java-compiler-testing/src/it/google-auto-value/src/test/java/io/github/ascopes/jct/acceptancetests/autovalue/AutoValueTest.java index 6428e27db..3766ac3dd 100644 --- a/java-compiler-testing/src/it/google-auto-value/src/test/java/io/github/ascopes/jct/acceptancetests/autovalue/AutoValueTest.java +++ b/java-compiler-testing/src/it/google-auto-value/src/test/java/io/github/ascopes/jct/acceptancetests/autovalue/AutoValueTest.java @@ -19,6 +19,7 @@ import static org.assertj.core.api.SoftAssertions.assertSoftly; 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.workspaces.Workspaces; import java.time.Instant; @@ -29,6 +30,7 @@ class AutoValueTest { @DisplayName("The AutoValue implementation class is created as expected") + @EcjCompilerTest @JavacCompilerTest void autoValueImplementationClassIsCreatedAsExpected(JctCompiler compiler) throws Throwable { try (var workspace = Workspaces.newWorkspace()) { @@ -43,7 +45,7 @@ void autoValueImplementationClassIsCreatedAsExpected(JctCompiler compiler) throw // Then assertThatCompilation(compilation) - .isSuccessfulWithoutWarnings() + .isSuccessful() .classOutputPackages() .allFilesExist( "org/example/AutoValue_User.class", diff --git a/java-compiler-testing/src/it/immutables/src/test/java/io/github/ascopes/jct/acceptancetests/immutables/ImmutablesTest.java b/java-compiler-testing/src/it/immutables/src/test/java/io/github/ascopes/jct/acceptancetests/immutables/ImmutablesTest.java index 6def86ae7..7d6eeaf4f 100644 --- a/java-compiler-testing/src/it/immutables/src/test/java/io/github/ascopes/jct/acceptancetests/immutables/ImmutablesTest.java +++ b/java-compiler-testing/src/it/immutables/src/test/java/io/github/ascopes/jct/acceptancetests/immutables/ImmutablesTest.java @@ -19,6 +19,7 @@ import static org.assertj.core.api.SoftAssertions.assertSoftly; 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.workspaces.Workspaces; import java.util.stream.Stream; @@ -36,6 +37,7 @@ class ImmutablesTest { @DisplayName("Immutables @Value produces the expected class") + @EcjCompilerTest @JavacCompilerTest void immutablesValueProducesTheExpectedClass(JctCompiler compiler) throws Throwable { try (var workspace = Workspaces.newWorkspace()) { @@ -68,6 +70,7 @@ void immutablesValueProducesTheExpectedClass(JctCompiler compiler) throws Throwa } @DisplayName("Immutables @Value produces the expected class for modules") + @EcjCompilerTest(minVersion = 9) @JavacCompilerTest(minVersion = 9) void immutablesValueProducesTheExpectedClassForModules(JctCompiler compiler) throws Throwable { try (var workspace = Workspaces.newWorkspace()) { diff --git a/java-compiler-testing/src/it/mapstruct/src/test/java/io/github/ascopes/jct/acceptancetests/mapstruct/MapStructTest.java b/java-compiler-testing/src/it/mapstruct/src/test/java/io/github/ascopes/jct/acceptancetests/mapstruct/MapStructTest.java index 72b5c6b5a..79930ce83 100644 --- a/java-compiler-testing/src/it/mapstruct/src/test/java/io/github/ascopes/jct/acceptancetests/mapstruct/MapStructTest.java +++ b/java-compiler-testing/src/it/mapstruct/src/test/java/io/github/ascopes/jct/acceptancetests/mapstruct/MapStructTest.java @@ -19,6 +19,7 @@ import static org.assertj.core.api.SoftAssertions.assertSoftly; 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.workspaces.Workspaces; import java.util.stream.Stream; @@ -28,6 +29,7 @@ class MapStructTest { @DisplayName("MapStruct generates expected mapping code") + @EcjCompilerTest @JavacCompilerTest void mapStructGeneratesExpectedMappingCode(JctCompiler compiler) throws Throwable { try (final var workspace = Workspaces.newWorkspace()) { @@ -40,7 +42,7 @@ void mapStructGeneratesExpectedMappingCode(JctCompiler compiler) throws Throwabl final var compilation = compiler.compile(workspace); // Then - assertThatCompilation(compilation).isSuccessfulWithoutWarnings(); + assertThatCompilation(compilation).isSuccessful(); final var classLoader = compilation.getFileManager() .getClassLoader(StandardLocation.CLASS_OUTPUT); @@ -68,6 +70,7 @@ void mapStructGeneratesExpectedMappingCode(JctCompiler compiler) throws Throwabl } @DisplayName("MapStruct generates expected mapping code for modules") + @EcjCompilerTest(minVersion = 9) @JavacCompilerTest(minVersion = 9) void mapStructGeneratesExpectedMappingCodeForModules(JctCompiler compiler) throws Throwable { try (final var workspace = Workspaces.newWorkspace()) { @@ -80,7 +83,7 @@ void mapStructGeneratesExpectedMappingCodeForModules(JctCompiler compiler) throw final var compilation = compiler.compile(workspace); // Then - assertThatCompilation(compilation).isSuccessfulWithoutWarnings(); + assertThatCompilation(compilation).isSuccessful(); final var classLoader = compilation.getFileManager() .getClassLoader(StandardLocation.CLASS_OUTPUT); diff --git a/java-compiler-testing/src/it/micronaut/src/test/java/io/github/ascopes/jct/acceptancetests/micronaut/MicronautIntegrationTest.java b/java-compiler-testing/src/it/micronaut/src/test/java/io/github/ascopes/jct/acceptancetests/micronaut/MicronautIntegrationTest.java index 12662ed36..dc05aa036 100644 --- a/java-compiler-testing/src/it/micronaut/src/test/java/io/github/ascopes/jct/acceptancetests/micronaut/MicronautIntegrationTest.java +++ b/java-compiler-testing/src/it/micronaut/src/test/java/io/github/ascopes/jct/acceptancetests/micronaut/MicronautIntegrationTest.java @@ -18,6 +18,7 @@ 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.workspaces.Workspaces; import org.junit.jupiter.api.DisplayName; @@ -26,6 +27,7 @@ class MicronautIntegrationTest { @DisplayName("Micronaut generates the expected code") + @EcjCompilerTest(configurers = MicronautConfigurer.class) @JavacCompilerTest(configurers = MicronautConfigurer.class) void micronautGeneratesTheExpectedCode(JctCompiler compiler) { try (var workspace = Workspaces.newWorkspace()) { diff --git a/java-compiler-testing/src/it/serviceloader-jpms/src/test/java/io/github/ascopes/jct/acceptancetests/serviceloaderjpms/testing/ServiceProcessorJpmsTest.java b/java-compiler-testing/src/it/serviceloader-jpms/src/test/java/io/github/ascopes/jct/acceptancetests/serviceloaderjpms/testing/ServiceProcessorJpmsTest.java index 410269e45..4a8ddaa86 100644 --- a/java-compiler-testing/src/it/serviceloader-jpms/src/test/java/io/github/ascopes/jct/acceptancetests/serviceloaderjpms/testing/ServiceProcessorJpmsTest.java +++ b/java-compiler-testing/src/it/serviceloader-jpms/src/test/java/io/github/ascopes/jct/acceptancetests/serviceloaderjpms/testing/ServiceProcessorJpmsTest.java @@ -19,6 +19,7 @@ import io.github.ascopes.jct.acceptancetests.serviceloaderjpms.ServiceProcessor; 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.workspaces.Workspaces; import org.junit.jupiter.api.DisplayName; @@ -27,6 +28,7 @@ class ServiceProcessorJpmsTest { @DisplayName("Expected files get created when the processor is run") + @EcjCompilerTest(minVersion = 9) @JavacCompilerTest(minVersion = 9) void expectedFilesGetCreated(JctCompiler compiler) { try (var workspace = Workspaces.newWorkspace()) { @@ -41,7 +43,7 @@ void expectedFilesGetCreated(JctCompiler compiler) { .compile(workspace); assertThatCompilation(compilation) - .isSuccessfulWithoutWarnings() + .isSuccessful() .classOutputPackages() .fileExists("META-INF", "services", "org.example.InsultProvider") .hasContent("org.example.MeanInsultProviderImpl"); @@ -62,7 +64,7 @@ void expectedFilesGetCreatedInMultiModuleRoots(JctCompiler compiler) { .compile(workspace); assertThatCompilation(compilation) - .isSuccessfulWithoutWarnings() + .isSuccessful() .classOutputModules().moduleExists("org.example") .fileExists("META-INF", "services", "org.example.InsultProvider") .hasContent("org.example.MeanInsultProviderImpl"); diff --git a/java-compiler-testing/src/it/serviceloader/src/test/java/io/github/ascopes/jct/acceptancetests/serviceloader/testing/ServiceProcessorTest.java b/java-compiler-testing/src/it/serviceloader/src/test/java/io/github/ascopes/jct/acceptancetests/serviceloader/testing/ServiceProcessorTest.java index 87138cb34..0fa377f64 100644 --- a/java-compiler-testing/src/it/serviceloader/src/test/java/io/github/ascopes/jct/acceptancetests/serviceloader/testing/ServiceProcessorTest.java +++ b/java-compiler-testing/src/it/serviceloader/src/test/java/io/github/ascopes/jct/acceptancetests/serviceloader/testing/ServiceProcessorTest.java @@ -19,6 +19,7 @@ import io.github.ascopes.jct.acceptancetests.serviceloader.ServiceProcessor; 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.workspaces.Workspaces; import org.junit.jupiter.api.DisplayName; @@ -27,6 +28,7 @@ class ServiceProcessorTest { @DisplayName("Expected files get created when the processor is run") + @EcjCompilerTest @JavacCompilerTest void expectedFilesGetCreated(JctCompiler compiler) { try (var workspace = Workspaces.newWorkspace()) { @@ -41,7 +43,7 @@ void expectedFilesGetCreated(JctCompiler compiler) { .compile(workspace); assertThatCompilation(compilation) - .isSuccessfulWithoutWarnings() + .isSuccessful() .classOutputPackages() .fileExists("META-INF", "services", "org.example.InsultProvider") .hasContent("org.example.MeanInsultProviderImpl"); diff --git a/java-compiler-testing/src/it/spring/src/test/java/io/github/ascopes/jct/acceptancetests/spring/SpringBootAutoconfigureProcessorTest.java b/java-compiler-testing/src/it/spring/src/test/java/io/github/ascopes/jct/acceptancetests/spring/SpringBootAutoconfigureProcessorTest.java index 7afad7356..4084e14ad 100644 --- a/java-compiler-testing/src/it/spring/src/test/java/io/github/ascopes/jct/acceptancetests/spring/SpringBootAutoconfigureProcessorTest.java +++ b/java-compiler-testing/src/it/spring/src/test/java/io/github/ascopes/jct/acceptancetests/spring/SpringBootAutoconfigureProcessorTest.java @@ -18,6 +18,7 @@ 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.workspaces.PathStrategy; import io.github.ascopes.jct.workspaces.Workspaces; @@ -28,6 +29,7 @@ class SpringBootAutoconfigureProcessorTest { @DisplayName("Spring will index the application context as expected") + @EcjCompilerTest(minVersion = 17) @JavacCompilerTest(minVersion = 17) void springWillIndexTheApplicationContextAsExpected(JctCompiler compiler) { try (var workspace = Workspaces.newWorkspace(PathStrategy.TEMP_DIRECTORIES)) { @@ -44,7 +46,6 @@ void springWillIndexTheApplicationContextAsExpected(JctCompiler compiler) { // Then assertThatCompilation(compilation) - .isSuccessfulWithoutWarnings() .classOutputPackages() .fileExists("META-INF", "spring-autoconfigure-metadata.properties") .isNotEmptyFile(); @@ -52,6 +53,7 @@ void springWillIndexTheApplicationContextAsExpected(JctCompiler compiler) { } @DisplayName("Spring will index the application context as expected when using modules") + @EcjCompilerTest(minVersion = 17) @JavacCompilerTest(minVersion = 17) void springWillIndexTheApplicationContextAsExpectedWhenUsingModules(JctCompiler compiler) { try (var workspace = Workspaces.newWorkspace(PathStrategy.TEMP_DIRECTORIES)) { @@ -67,7 +69,7 @@ void springWillIndexTheApplicationContextAsExpectedWhenUsingModules(JctCompiler // Then assertThatCompilation(compilation) - .isSuccessfulWithoutWarnings() + .isSuccessful() .classOutputPackages() .fileExists("META-INF", "spring-autoconfigure-metadata.properties") .isNotEmptyFile(); @@ -75,6 +77,7 @@ void springWillIndexTheApplicationContextAsExpectedWhenUsingModules(JctCompiler } @DisplayName("Spring will index the application context as expected when using multi-modules") + @EcjCompilerTest(minVersion = 17) @JavacCompilerTest(minVersion = 17) void springWillIndexTheApplicationContextAsExpectedWhenUsingMultiModules(JctCompiler compiler) { try (var workspace = Workspaces.newWorkspace(PathStrategy.TEMP_DIRECTORIES)) { @@ -90,7 +93,7 @@ void springWillIndexTheApplicationContextAsExpectedWhenUsingMultiModules(JctComp // Then assertThatCompilation(compilation) - .isSuccessfulWithoutWarnings() + .isSuccessful() .classOutputModules().moduleExists("org.example") .fileExists("META-INF", "spring-autoconfigure-metadata.properties") .isNotEmptyFile(); diff --git a/java-compiler-testing/src/it/spring/src/test/java/io/github/ascopes/jct/acceptancetests/spring/SpringBootConfigurationProcessorTest.java b/java-compiler-testing/src/it/spring/src/test/java/io/github/ascopes/jct/acceptancetests/spring/SpringBootConfigurationProcessorTest.java index 5dca1c831..747a1d70f 100644 --- a/java-compiler-testing/src/it/spring/src/test/java/io/github/ascopes/jct/acceptancetests/spring/SpringBootConfigurationProcessorTest.java +++ b/java-compiler-testing/src/it/spring/src/test/java/io/github/ascopes/jct/acceptancetests/spring/SpringBootConfigurationProcessorTest.java @@ -28,6 +28,7 @@ class SpringBootConfigurationProcessorTest { @DisplayName("Spring will index the application context as expected") + @EcjCompilerTest(minVersion = 17) @JavacCompilerTest(minVersion = 17) void springWillIndexTheApplicationContextAsExpected(JctCompiler compiler) { try (var workspace = Workspaces.newWorkspace(PathStrategy.TEMP_DIRECTORIES)) { @@ -52,6 +53,7 @@ void springWillIndexTheApplicationContextAsExpected(JctCompiler compiler) { } @DisplayName("Spring will index the application context as expected with modules") + @EcjCompilerTest(minVersion = 17) @JavacCompilerTest(minVersion = 17) void springWillIndexTheApplicationContextAsExpectedWithModules(JctCompiler compiler) { try (var workspace = Workspaces.newWorkspace(PathStrategy.TEMP_DIRECTORIES)) { @@ -75,6 +77,7 @@ void springWillIndexTheApplicationContextAsExpectedWithModules(JctCompiler compi } @DisplayName("Spring will index the application context as expected with multi-modules") + @EcjCompilerTest(minVersion = 17) @JavacCompilerTest(minVersion = 17) void springWillIndexTheApplicationContextAsExpectedWithMultiModules(JctCompiler compiler) { try (var workspace = Workspaces.newWorkspace(PathStrategy.TEMP_DIRECTORIES)) { diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/TypeAwareListAssert.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/TypeAwareListAssert.java index 72f82c12a..67e41fa8e 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/TypeAwareListAssert.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/TypeAwareListAssert.java @@ -57,6 +57,7 @@ protected A toAssert(@Nullable E value, String description) { Iterable iterable ) { var list = StreamSupport.stream(iterable.spliterator(), false).toList(); + return new TypeAwareListAssert<>(list, assertFactory); } } 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 9b901b564..e25cafbe7 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; @@ -40,4 +41,14 @@ private JctCompilers() { public static JctCompiler newPlatformCompiler() { return new JavacJctCompilerImpl(); } + + /** + * Create a new instance of the ECJ compiler (Eclipse Compiler for Java). + * + * @return the compiler instance. + * @since 5.0.0 + */ + 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..521164491 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/impl/EcjJctCompilerImpl.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2022 - 2025, 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 org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants; +import org.eclipse.jdt.internal.compiler.tool.EclipseCompiler; + + +/** + * Implementation of a JCT compiler that integrates with the Eclipse Java Compiler. + * + * @author Ashley Scopes + * @since 5.0.0 + */ +public final class EcjJctCompilerImpl extends AbstractJctCompiler { + + public EcjJctCompilerImpl() { + super("ECJ"); + } + + @Override + public JctFlagBuilderFactory getFlagBuilderFactory() { + return EcjJctFlagBuilderImpl::new; + } + + @Override + public Jsr199CompilerFactory getCompilerFactory() { + return EclipseCompiler::new; + } + + @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 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..249d98d23 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/impl/EcjJctFlagBuilderImpl.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2022 - 2025, 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.DebuggingInfo; +import io.github.ascopes.jct.compilers.JctFlagBuilder; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import org.jspecify.annotations.Nullable; + +/** + * Helper to build flags for the ECJ compiler implementation. + * + * @author Ashley Scopes + * @since 5.0.0 + */ +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 static final String DEBUG_LINES = "-g:lines"; + private static final String DEBUG_VARS = "-g:vars"; + private static final String DEBUG_SOURCE = "-g:source"; + private static final String DEBUG_NONE = "-g:none"; + private static final String PARAMETERS = "-parameters"; + + 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 debuggingInfo(Set set) { + if (set.isEmpty()) { + craftedFlags.add(DEBUG_NONE); + return this; + } + + if (set.contains(DebuggingInfo.LINES)) { + craftedFlags.add(DEBUG_LINES); + } + + if (set.contains(DebuggingInfo.SOURCE)) { + craftedFlags.add(DEBUG_SOURCE); + } + + if (set.contains(DebuggingInfo.VARS)) { + craftedFlags.add(DEBUG_VARS); + } + + return this; + } + + @Override + public EcjJctFlagBuilderImpl parameterInfoEnabled(boolean enabled) { + return addFlagIfTrue(enabled, PARAMETERS); + } + + @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/junit/EcjCompilerTest.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/EcjCompilerTest.java new file mode 100644 index 000000000..62532cc0e --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/EcjCompilerTest.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2022 - 2025, 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.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +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. + * + * @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>[] 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 000000000..4df27e4dd --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/EcjCompilersProvider.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2022 - 2025, 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 { + + /** + * Initialise the provider. + * + *

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 ce510b28c..d6a9ff905 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 c89636de4..3bd0c3c17 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 000000000..002833292 --- /dev/null +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/compilers/impl/EcjJctCompilerImplTest.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2022 - 2025, 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 000000000..ea4995d74 --- /dev/null +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/compilers/impl/EcjJctFlagBuilderImplTest.java @@ -0,0 +1,541 @@ +/* + * Copyright (C) 2022 - 2025, 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 ' 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(".debuggingInfo(Set) tests") + @Nested + class DebuggingInfoTest { + + @DisplayName("Setting .debuggingInfo with an empty set adds the '-g:none' flag") + @Test + void emptySetAddsGnoneFlag() { + // When + flagBuilder.debuggingInfo(DebuggingInfo.none()); + + // Then + assertThat(flagBuilder.build()).containsOnlyOnce("-g:none"); + } + + @DisplayName("Setting .debuggingInfo with some values set adds the '-g:xxx' flags") + @CsvSource({ + " LINES, -g:lines", + "SOURCE, -g:source", + " VARS, -g:vars", + }) + @ParameterizedTest(name = "expect {0} to set flag {1}") + void correctFlagsAreSet(DebuggingInfo flag, String flagString) { + // When + flagBuilder.debuggingInfo(DebuggingInfo.just(flag)); + + // Then + assertThat(flagBuilder.build()).containsExactly(flagString); + } + + @DisplayName("Setting .debuggingInfo with all values set adds the '-g:xxx' flags") + @Test + void allAddsValues() { + // When + flagBuilder.debuggingInfo(DebuggingInfo.all()); + + // Then + assertThat(flagBuilder.build()) + .doesNotContain("-g", "-g:none") + .containsOnlyOnce("-g:lines", "-g:source", "-g:vars"); + } + } + + @DisplayName(".parameterInfoEnabled(boolean) tests") + @Nested + class ParameterInfoEnabledTest { + + @DisplayName("Setting .parameterInfoEnabled(true) adds the '-parameters' flag") + @Test + void trueAddsFlag() { + // When + flagBuilder.parameterInfoEnabled(true); + + // Then + assertThat(flagBuilder.build()).containsOnlyOnce("-parameters"); + } + + @DisplayName("Setting .parameterInfoEnabled(false) does not add the '-parameters' flag") + @Test + void falseDoesNotAddFlag() { + // When + flagBuilder.parameterInfoEnabled(false); + + // Then + assertThat(flagBuilder.build()).doesNotContain("-parameters"); + } + } + + @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") + @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/integration/compilation/BasicLegacyCompilationIntegrationTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/integration/compilation/BasicLegacyCompilationIntegrationTest.java index 7a4d47fb7..6c343a3c3 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/integration/compilation/BasicLegacyCompilationIntegrationTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/integration/compilation/BasicLegacyCompilationIntegrationTest.java @@ -19,6 +19,7 @@ import io.github.ascopes.jct.compilers.JctCompiler; import io.github.ascopes.jct.integration.AbstractIntegrationTest; +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.Workspaces; @@ -34,6 +35,7 @@ class BasicLegacyCompilationIntegrationTest extends AbstractIntegrationTest { @DisplayName("I can compile a 'Hello, World!' program using a RAM directory") + @EcjCompilerTest @JavacCompilerTest void helloWorldJavacRamDirectory(JctCompiler compiler) { try (var workspace = Workspaces.newWorkspace(PathStrategy.RAM_DIRECTORIES)) { @@ -54,6 +56,7 @@ void helloWorldJavacRamDirectory(JctCompiler compiler) { } @DisplayName("I can compile a 'Hello, World!' program using a temp directory") + @EcjCompilerTest @JavacCompilerTest void helloWorldJavacTempDirectory(JctCompiler compiler) { try (var workspace = Workspaces.newWorkspace(PathStrategy.TEMP_DIRECTORIES)) { diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/integration/compilation/BasicModuleCompilationIntegrationTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/integration/compilation/BasicModuleCompilationIntegrationTest.java index 06cd8bf1f..26c7b2a73 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/integration/compilation/BasicModuleCompilationIntegrationTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/integration/compilation/BasicModuleCompilationIntegrationTest.java @@ -19,6 +19,7 @@ import io.github.ascopes.jct.compilers.JctCompiler; import io.github.ascopes.jct.integration.AbstractIntegrationTest; +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.Workspaces; @@ -33,6 +34,7 @@ class BasicModuleCompilationIntegrationTest extends AbstractIntegrationTest { @DisplayName("I can compile a 'Hello, World!' module program using a RAM disk") + @EcjCompilerTest(minVersion = 9) @JavacCompilerTest(minVersion = 9) void helloWorldRamDisk(JctCompiler compiler) { try (var workspace = Workspaces.newWorkspace(PathStrategy.RAM_DIRECTORIES)) { @@ -61,6 +63,7 @@ void helloWorldRamDisk(JctCompiler compiler) { } @DisplayName("I can compile a 'Hello, World!' module program using a temporary directory") + @EcjCompilerTest(minVersion = 9) @JavacCompilerTest(minVersion = 9) void helloWorldUsingTempDirectory(JctCompiler compiler) { try (var workspace = Workspaces.newWorkspace(PathStrategy.TEMP_DIRECTORIES)) { diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/integration/compilation/BasicMultiModuleCompilationIntegrationTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/integration/compilation/BasicMultiModuleCompilationIntegrationTest.java index 9dbf34106..22e611598 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/integration/compilation/BasicMultiModuleCompilationIntegrationTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/integration/compilation/BasicMultiModuleCompilationIntegrationTest.java @@ -20,6 +20,7 @@ import io.github.ascopes.jct.compilers.JctCompiler; import io.github.ascopes.jct.integration.AbstractIntegrationTest; +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.Workspaces; @@ -34,6 +35,7 @@ class BasicMultiModuleCompilationIntegrationTest extends AbstractIntegrationTest { @DisplayName("I can compile a single module using multi-module layout using a RAM disk") + @EcjCompilerTest(minVersion = 9) @JavacCompilerTest(minVersion = 9) void singleModuleInMultiModuleLayoutRamDisk(JctCompiler compiler) { try (var workspace = Workspaces.newWorkspace(PathStrategy.RAM_DIRECTORIES)) { @@ -62,6 +64,7 @@ void singleModuleInMultiModuleLayoutRamDisk(JctCompiler compiler) { } @DisplayName("I can compile a single module using multi-module layout using a temp directory") + @EcjCompilerTest(minVersion = 9) @JavacCompilerTest(minVersion = 9) void singleModuleInMultiModuleLayoutTempDirectory(JctCompiler compiler) { try (var workspace = Workspaces.newWorkspace(PathStrategy.TEMP_DIRECTORIES)) { diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/integration/compilation/CompilingSpecificClassesIntegrationTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/integration/compilation/CompilingSpecificClassesIntegrationTest.java index c73f7f61e..dffae5409 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/integration/compilation/CompilingSpecificClassesIntegrationTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/integration/compilation/CompilingSpecificClassesIntegrationTest.java @@ -19,6 +19,7 @@ import io.github.ascopes.jct.compilers.JctCompiler; import io.github.ascopes.jct.integration.AbstractIntegrationTest; +import io.github.ascopes.jct.junit.EcjCompilerTest; import io.github.ascopes.jct.junit.JavacCompilerTest; import io.github.ascopes.jct.workspaces.Workspaces; import org.junit.jupiter.api.DisplayName; @@ -32,6 +33,7 @@ class CompilingSpecificClassesIntegrationTest extends AbstractIntegrationTest { @DisplayName("Only the classes that I specify get compiled") + @EcjCompilerTest @JavacCompilerTest void onlyTheClassesSpecifiedGetCompiled(JctCompiler compiler) { try (var workspace = Workspaces.newWorkspace()) { diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/integration/compilation/MultiTieredCompilationIntegrationTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/integration/compilation/MultiTieredCompilationIntegrationTest.java index d46aa0bcf..86c1bbc06 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/integration/compilation/MultiTieredCompilationIntegrationTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/integration/compilation/MultiTieredCompilationIntegrationTest.java @@ -19,6 +19,7 @@ import io.github.ascopes.jct.compilers.JctCompiler; import io.github.ascopes.jct.integration.AbstractIntegrationTest; +import io.github.ascopes.jct.junit.EcjCompilerTest; import io.github.ascopes.jct.junit.JavacCompilerTest; import io.github.ascopes.jct.workspaces.Workspaces; import org.junit.jupiter.api.DisplayName; @@ -35,6 +36,7 @@ class MultiTieredCompilationIntegrationTest extends AbstractIntegrationTest { @DisplayName( "I can compile sources to classes and provide them in the classpath to a second compilation" ) + @EcjCompilerTest @JavacCompilerTest void compileSourcesToClassesAndProvideThemInClassPathToSecondCompilation( JctCompiler compiler @@ -81,6 +83,7 @@ void compileSourcesToClassesAndProvideThemInClassPathToSecondCompilation( "I can compile sources to classes and provide them in the classpath to a second " + "compilation within a JAR" ) + @EcjCompilerTest @JavacCompilerTest void compileSourcesToClassesAndProvideThemInClassPathToSecondCompilationWithinJar( JctCompiler compiler diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/junit/EcjCompilersProviderTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/junit/EcjCompilersProviderTest.java new file mode 100644 index 000000000..ca3b5a2ec --- /dev/null +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/junit/EcjCompilersProviderTest.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2022 - 2025, 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 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 java.lang.reflect.AnnotatedElement; +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]) + .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(8); + 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]) + .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)", 8 + i); + softly.assertThat(compiler.getRelease()) + .as("compilers[%d].getRelease()", i) + .isEqualTo("%d", 8 + 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]) + .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 64c43148c..625c9084b 100644 --- a/pom.xml +++ b/pom.xml @@ -85,6 +85,14 @@ https://github.com/ascopes + + + eclipse-snapshots + Eclipse Snapshots + https://repo.eclipse.org/content/repositories/eclipse-snapshots + + + https://github.com/ascopes/java-compiler-testing scm:git:https://github.com/ascopes/java-compiler-testing @@ -96,6 +104,7 @@ 4.0.0-M1 4.3.0 + 3.41.0-SNAPSHOT 1.4.0 1.0.0 5.12.2 @@ -126,6 +135,7 @@ true + INFO + org.eclipse.jdt + ecj + ${ecj.version} + + org.jspecify jspecify diff --git a/scripts/add-development-ecj-to-maven-repository.sh b/scripts/add-development-ecj-to-maven-repository.sh new file mode 100755 index 000000000..55d9eb45c --- /dev/null +++ b/scripts/add-development-ecj-to-maven-repository.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# +# 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. +# + +### +### Shortcut to injecting a development version of ECJ's JAR into the local Maven registry. +### + +set -o errexit +set -o nounset +[[ -n ${DEBUG+defined} ]] && set -o xtrace + +if [[ $# -ne 2 ]]; then + echo "USAGE: ${BASH_SOURCE[0]} " + echo "Inject the given URL to an ECJ JAR as the given version in the local Maven repository." + echo "" + echo "Arguments:" + echo " The URL to the ECJ JAR to use." + echo " The version number to use for that JAR." + echo "" + exit 1 +fi + +url=$1 +version=$2 + +maven_repository_dir=${M2_HOME:-${HOME}/.m2}/repository +target_dir=${maven_repository_dir}/org/eclipse/jdt/ecj/${version} + +if [[ -d ${target_dir} ]]; then + echo "Clearing existing directory out..." + rm -Rvf "${target_dir}" +fi + +echo "Making ECJ directory" +mkdir -pv "${target_dir}" + +echo "Working out the latest ECJ POM to use..." +latest_published_version=$(curl --fail --silent https://repo1.maven.org/maven2/org/eclipse/jdt/ecj/maven-metadata.xml \ + | grep -oE ".+?" \ + | sed -E 's@@@g' \ + | tail -n 1) + +echo "Making ECJ POM derived from the POM for v${latest_published_version}" +curl --fail --silent https://repo1.maven.org/maven2/org/eclipse/jdt/ecj/"${latest_published_version}"/ecj-"${latest_published_version}".pom \ + | sed 's@'"${latest_published_version}"'@'"${version}"'@g' \ + > "${target_dir}/ecj-${version}.pom" + +echo "Downloading ECJ JAR" +curl --fail --silent "${url}" > "${target_dir}/ecj-${version}.jar" + +echo "Computing SHA1 digest of ECJ JAR" +sha1sum < "${target_dir}/ecj-${version}.jar" | cut -d ' ' -f 1 > "${target_dir}/ecj-${version}.jar.sha1" + +echo "Making dummy _remote.repositories file" + +cat > "${target_dir}/_remote.repositories" <<-EOF +#NOTE: This is a Maven Resolver internal implementation file, its format can be changed without prior notice. +#$(date) +ecj-${version}.jar>central= +ecj-${version}.pom>central= +EOF + +echo "Done." diff --git a/scripts/ecj.sh b/scripts/ecj.sh new file mode 100755 index 000000000..cee74ae0f --- /dev/null +++ b/scripts/ecj.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# +# 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. +# + +### +### Shortcut to running the ECJ compiler. +### + +set -o errexit +set -o nounset +[[ -n ${DEBUG+defined} ]] && set -o xtrace + +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)" "${@}"