From 3828353ac537cdfd1ba7079c7b1861de31e278c4 Mon Sep 17 00:00:00 2001 From: Ashley Scopes <73482956+ascopes@users.noreply.github.com> Date: Sat, 28 Dec 2024 15:53:51 +0000 Subject: [PATCH] Implement support for ECJ Rejig RAM directory naming, add ability to dump tree view of workspaces Port across ECJ support This is still provisional and an active work in progress, please track https://github.com/eclipse-jdt/eclipse.jdt.core/issues/958 for this work. Enable tests for eclipse Run against CI build of ECJ temporarily Address ShellCheck warnings Fix ECJ injection script Add ability to adjust log level in POM Apply 2025 license headers to new ECJ classes Use eclipse snapshot repositories --- .github/workflows/build.yml | 3 +- java-compiler-testing/pom.xml | 5 + .../avajehttp/AvajeHttpTest.java | 4 +- .../avajeinject/AvajeInjectTest.java | 4 +- .../avajejsonb/AvajeJsonbTest.java | 4 +- .../acceptancetests/dagger/DaggerTest.java | 4 +- .../dogfood/JctDogfoodTest.java | 2 + .../autofactory/AutoFactoryTest.java | 4 +- .../autoservice/AutoServiceTest.java | 4 +- .../autovalue/AutoValueTest.java | 4 +- .../immutables/ImmutablesTest.java | 3 + .../mapstruct/MapStructTest.java | 7 +- .../micronaut/MicronautIntegrationTest.java | 2 + .../testing/ServiceProcessorJpmsTest.java | 6 +- .../testing/ServiceProcessorTest.java | 4 +- .../SpringBootAutoconfigureProcessorTest.java | 9 +- .../SpringBootConfigurationProcessorTest.java | 3 + .../jct/assertions/TypeAwareListAssert.java | 1 + .../ascopes/jct/compilers/JctCompilers.java | 11 + .../compilers/impl/EcjJctCompilerImpl.java | 73 +++ .../compilers/impl/EcjJctFlagBuilderImpl.java | 184 ++++++ .../ascopes/jct/junit/EcjCompilerTest.java | 140 +++++ .../jct/junit/EcjCompilersProvider.java | 64 +++ .../src/main/java/module-info.java | 4 +- .../jct/compilers/JctCompilersTest.java | 19 + .../impl/EcjJctCompilerImplTest.java | 145 +++++ .../impl/EcjJctFlagBuilderImplTest.java | 541 ++++++++++++++++++ ...BasicLegacyCompilationIntegrationTest.java | 3 + ...BasicModuleCompilationIntegrationTest.java | 3 + ...MultiModuleCompilationIntegrationTest.java | 3 + ...mpilingSpecificClassesIntegrationTest.java | 2 + ...MultiTieredCompilationIntegrationTest.java | 3 + .../jct/junit/EcjCompilersProviderTest.java | 166 ++++++ pom.xml | 19 +- ...add-development-ecj-to-maven-repository.sh | 77 +++ scripts/ecj.sh | 54 ++ 36 files changed, 1565 insertions(+), 19 deletions(-) create mode 100644 java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/impl/EcjJctCompilerImpl.java create mode 100644 java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/impl/EcjJctFlagBuilderImpl.java create mode 100644 java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/EcjCompilerTest.java create mode 100644 java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/EcjCompilersProvider.java create mode 100644 java-compiler-testing/src/test/java/io/github/ascopes/jct/compilers/impl/EcjJctCompilerImplTest.java create mode 100644 java-compiler-testing/src/test/java/io/github/ascopes/jct/compilers/impl/EcjJctFlagBuilderImplTest.java create mode 100644 java-compiler-testing/src/test/java/io/github/ascopes/jct/junit/EcjCompilersProviderTest.java create mode 100755 scripts/add-development-ecj-to-maven-repository.sh create mode 100755 scripts/ecj.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ea209d4b..23f8fc8a 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 0cd2177b..0b252ebf 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 aed596e3..3468c770 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 7c7e839a..112a0d1a 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 8332c8f9..22d79805 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 4f1d6caf..cbb06574 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 2817aba7..04f3744c 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 04a28737..c9e3ede3 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 4f81b41d..e8fb7425 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 6428e27d..3766ac3d 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 6def86ae..7d6eeaf4 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 72b5c6b5..79930ce8 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 12662ed3..dc05aa03 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 410269e4..4a8ddaa8 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 87138cb3..0fa377f6 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 7afad735..4084e14a 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 5dca1c83..747a1d70 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 72f82c12..67e41fa8 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 9b901b56..e25cafbe 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 00000000..52116449 --- /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 00000000..249d98d2 --- /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 00000000..62532cc0 --- /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 00000000..4df27e4d --- /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 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..00283329 --- /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 00000000..ea4995d7 --- /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 7a4d47fb..6c343a3c 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 06cd8bf1..26c7b2a7 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 9dbf3410..22e61159 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 c73f7f61..dffae540 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 d46aa0bc..86c1bbc0 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 00000000..ca3b5a2e --- /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 64c43148..625c9084 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 00000000..55d9eb45 --- /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 00000000..cee74ae0 --- /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)" "${@}"