diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..7a494beaa --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: "maven" + directory: "/" + schedule: + interval: "weekly" + assignees: + - "ascopes" + labels: + - "enhancement" + - "housekeeping" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f411d0fd8..84936babd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,6 +39,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [windows-latest, ubuntu-latest] java-version: [ 11, 12, 13, 14, 15, 16, 17, 18 ] @@ -73,7 +74,7 @@ jobs: - name: Annotate test reports if: always() run: >- - .github/scripts/prepare-test-outputs-for-merge.sh + scripts/prepare-test-outputs-for-merge.sh ${{ matrix.java-version }} ${{ matrix.os }} diff --git a/.gitignore b/.gitignore index 9c27dd563..a3b80043f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,11 @@ build/ out/ target/ -# IntelliJ -.idea/ +# IntelliJ junk *.iml +.idea/* +# Allow providing file templates for simplicity. +!.idea/fileTemplates # VSCode .vscode/ diff --git a/.idea/fileTemplates/internal/AnnotationType.java b/.idea/fileTemplates/internal/AnnotationType.java new file mode 100644 index 000000000..835407826 --- /dev/null +++ b/.idea/fileTemplates/internal/AnnotationType.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) ${YEAR} Ashley Scopes + * + * 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 ${PACKAGE_NAME}; + +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * TODO(${USER}): update the documentation for this annotation. + * + *

Also, change the {@code since} tag to hold the current library + * version. + * + * @author ${USER} + * @since XXX + */ +@API(since = "XXX", status = Status.EXPERIMENTAL) +public @interface ${NAME} { +} diff --git a/.idea/fileTemplates/internal/Class.java b/.idea/fileTemplates/internal/Class.java new file mode 100644 index 000000000..66759200b --- /dev/null +++ b/.idea/fileTemplates/internal/Class.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) ${YEAR} Ashley Scopes + * + * 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 ${PACKAGE_NAME}; + +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * TODO(${USER}): update the documentation for this class. + * + *

Also, change the {@code since} tag to hold the current library + * version. + * + * @author ${USER} + * @since XXX + */ +@API(since = "XXX", status = Status.EXPERIMENTAL) +public class ${NAME} { +} \ No newline at end of file diff --git a/.idea/fileTemplates/internal/Enum.java b/.idea/fileTemplates/internal/Enum.java new file mode 100644 index 000000000..8e5ee61f1 --- /dev/null +++ b/.idea/fileTemplates/internal/Enum.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) ${YEAR} Ashley Scopes + * + * 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 ${PACKAGE_NAME}; + +import org.apiguardian.api.API; +import org.apiguardian.api.Status; + +/** + * TODO(${USER}): update the documentation for this enum. + * + *

Also, change the {@code since} tag to hold the current library + * version. + * + * @author ${USER} + * @since XXX + */ +@API(since = "XXX", status = Status.EXPERIMENTAL) +public enum ${NAME} { +} \ No newline at end of file diff --git a/.idea/fileTemplates/internal/Interface.java b/.idea/fileTemplates/internal/Interface.java new file mode 100644 index 000000000..40360e999 --- /dev/null +++ b/.idea/fileTemplates/internal/Interface.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) ${YEAR} Ashley Scopes + * + * 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 ${PACKAGE_NAME}; + +import org.apiguardian.api.API; +import org.apiguardian.api.Status; + +/** + * TODO(${USER}): update the documentation for this interface. + * + *

Also, change the {@code since} tag to hold the current library + * version. + * + * @author ${USER} + * @since XXX + */ +@API(since = "XXX", status = Status.EXPERIMENTAL) +public interface ${NAME} { +} \ No newline at end of file diff --git a/.idea/fileTemplates/internal/package-info.java b/.idea/fileTemplates/internal/package-info.java new file mode 100644 index 000000000..aa1ea35e7 --- /dev/null +++ b/.idea/fileTemplates/internal/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) ${YEAR} Ashley Scopes + * + * 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. + */ + +/** + * TODO(${USER}): update the documentation for this package. + */ +package ${NAME}; diff --git a/.mvn/checkstyle/checkstyle.xml b/.mvn/checkstyle/checkstyle.xml index 7200e8259..1411d388d 100644 --- a/.mvn/checkstyle/checkstyle.xml +++ b/.mvn/checkstyle/checkstyle.xml @@ -47,8 +47,13 @@ + + value="^package.*|^import.*|a href|href|http://|https://|ftp://|\s+extends |\s+implements "/> diff --git a/README.md b/README.md index 5d3d19e30..9c1bbb85a 100644 --- a/README.md +++ b/README.md @@ -27,95 +27,113 @@ prevent future issues for any projects deciding to use it. **This module is still under development.** Any contributions or feedback are always welcome! -## Example +## Examples -The following is an example of using this library with JUnit Jupiter to run both javac and ECJ: +The following is an example of using this library with JUnit Jupiter to run both javac and ECJ +across a single package: ```java -class HelloWorldTest { - @Test - void i_can_compile_hello_world_with_javac() { - // Given - var sources = RamPath - .createPath("sources") - .createFile( - "org/me/test/examples/HelloWorld.java", - "package org.me.test.examples;", - "", - "public class HelloWorld {", - " public static void main(String[] args) {", - " System.out.println(\"Hello, World!\");", - " }", - "}" - ) +@DisplayName("Example tests") +class ExampleTest { + + @DisplayName("I can compile a Hello World application") + @EcjCompilers + @JavacCompilers + @ParameterizedTest(name = "using {0}") + void canCompileHelloWorld(Compilable compiler) { + var sources = createPath("src") .createFile( - "module-info.java", - "module org.me.test.examples {", - " exports org.me.test.examples;", - "}" + "org/example/Message.java", + """ + package org.example; + + import lombok.Data; + import lombok.NonNull; + + @Data + public class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello, World!"); + } + } + """ ); // When - var compilation = Compilers - .javac() - .addSourceRamPaths(sources) - .release(11) + var compilation = compiler + .addSourcePath(sources) .compile(); // Then - assertThatCompilation(compilation).isSuccessfulWithoutWarnings(); - assertThatCompilation(compilation).diagnostics().isEmpty(); - assertThatCompilation(compilation).classOutput() - .file("org/me/test/examples/HelloWorld.class") - .exists() - .isNotEmptyFile(); - assertThatCompilation(compilation).classOutput() - .file("module-info.class") - .exists() - .isNotEmptyFile(); + assertThatCompilation(compilation) + .isSuccessfulWithoutWarnings(); } +} +``` + +Likewise, the following shows an example of compiling a multi-module style application with JPMS +support, running the Lombok annotation processor over the input. This assumes that the Lombok +JAR is already on the classpath for the JUnit test runner. + +```java +@DisplayName("Example tests") +class ExampleTest { - @Test - void i_can_compile_hello_world_with_ecj() { + @DisplayName("I can compile a module that is using Lombok") + @JavacCompilers(modules = true) + @ParameterizedTest(name = "using {0}") + void canCompileModuleUsingLombok(Compilable compiler) { // Given - var sources = RamPath - .createPath("sources") + var sources = createPath("hello.world") + .createFile( + "org/example/Message.java", + """ + package org.example; + + import lombok.Data; + import lombok.NonNull; + + @Data + public class Message { + @NonNull + private final String content; + } + """ + ) .createFile( - "org/me/test/examples/HelloWorld.java", - "package org.me.test.examples;", - "", - "public class HelloWorld {", - " public static void main(String[] args) {", - " System.out.println(\"Hello, World!\");", - " }", - "}" + "org/example/Main.java", + """ + package org.example; + + public class Main { + public static void main(String[] args) { + for (var arg : args) { + var message = new Message(arg); + System.out.println(arg); + } + } + } + """ ) .createFile( "module-info.java", - "module org.me.test.examples {", - " exports org.me.test.examples;", - "}" + """ + module hello.world { + requires java.base; + requires static lombok; + } + """ ); // When - var compilation = Compilers - .ecj() - .addSourceRamPaths(sources) - .release(11) + var compilation = compiler + .addModuleSourcePath("hello.world", sources) .compile(); // Then - assertThatCompilation(compilation).isSuccessfulWithoutWarnings(); - assertThatCompilation(compilation).diagnostics().isEmpty(); - assertThatCompilation(compilation).classOutput() - .file("org/me/test/examples/HelloWorld.class") - .exists() - .isNotEmptyFile(); - assertThatCompilation(compilation).classOutput() - .file("module-info.class") - .exists() - .isNotEmptyFile(); + assertThatCompilation(compilation) + .isSuccessfulWithoutWarnings(); } } ``` diff --git a/examples/example-lombok/pom.xml b/examples/example-lombok/pom.xml index 2a5b0d0ae..e9306c106 100644 --- a/examples/example-lombok/pom.xml +++ b/examples/example-lombok/pom.xml @@ -13,15 +13,10 @@ example-lombok - - 1.18.22 - - org.projectlombok lombok - ${lombok.version} @@ -42,4 +37,22 @@ test + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + + + true + + + + diff --git a/examples/example-lombok/src/test/java/io/github/ascopes/jct/examples/lombok/LombokIntegrationTest.java b/examples/example-lombok/src/test/java/io/github/ascopes/jct/examples/lombok/LombokIntegrationTest.java index ca65d8ba7..1188ddadd 100644 --- a/examples/example-lombok/src/test/java/io/github/ascopes/jct/examples/lombok/LombokIntegrationTest.java +++ b/examples/example-lombok/src/test/java/io/github/ascopes/jct/examples/lombok/LombokIntegrationTest.java @@ -16,17 +16,16 @@ package io.github.ascopes.jct.examples.lombok; +import static io.github.ascopes.jct.assertions.JctAssertions.assertThatCompilation; import static org.assertj.core.api.Assertions.assertThat; -import io.github.ascopes.jct.assertions.CompilationAssert; -import io.github.ascopes.jct.compilers.Compilers; +import io.github.ascopes.jct.compilers.Compilable; +import io.github.ascopes.jct.compilers.LoggingMode; +import io.github.ascopes.jct.junit.JavacCompilers; import io.github.ascopes.jct.paths.RamPath; -import java.util.stream.IntStream; -import javax.lang.model.SourceVersion; import javax.tools.StandardLocation; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; /** * Example integration test that makes use of the Lombok annotation processor. @@ -40,9 +39,9 @@ class LombokIntegrationTest { @DisplayName("Lombok @Data compiles the expected data class") - @MethodSource("supportedJavacVersions") - @ParameterizedTest(name = "for Java version {0}") - void lombokDataCompilesTheExpectedDataClass(int version) throws Exception { + @JavacCompilers + @ParameterizedTest(name = "for {0}") + void lombokDataCompilesTheExpectedDataClass(Compilable compiler) throws Exception { var sources = RamPath .createPath("sources") .createFile( @@ -59,28 +58,27 @@ void lombokDataCompilesTheExpectedDataClass(int version) throws Exception { "}" ); - var compilation = Compilers - .javac() - .addSourceRamPaths(sources) - .release(version) + var compilation = compiler + .addSourcePath(sources) + //.fileManagerLogging(LoggingMode.ENABLED) + //.diagnosticLogging(LoggingMode.STACKTRACES) .compile(); - CompilationAssert.assertThatCompilation(compilation) + assertThatCompilation(compilation) .isSuccessful(); // Github Issue #9 sanity check - Improve annotation processor discovery mechanism - CompilationAssert.assertThatCompilation(compilation) - .location(StandardLocation.ANNOTATION_PROCESSOR_PATH) - .containsAll(compilation - .getPathLocationRepository() - .getExpectedManager(StandardLocation.CLASS_PATH) - .getRoots()); + // TODO(ascopes): fix this to work with the file manager rewrite. + //CompilationAssert.assertThatCompilation(compilation) + // .location(StandardLocation.ANNOTATION_PROCESSOR_PATH) + // .containsAll(compilation + // .getFileManager() + // .getExpectedManager(StandardLocation.CLASS_PATH) + // .getRoots()); var animalClass = compilation - .getPathLocationRepository() - .getManager(StandardLocation.CLASS_OUTPUT) - .orElseThrow() - .getClassLoader() + .getFileManager() + .getClassLoader(StandardLocation.CLASS_OUTPUT) .loadClass("io.github.ascopes.jct.examples.lombok.dataclass.Animal"); var animal = animalClass @@ -92,8 +90,4 @@ void lombokDataCompilesTheExpectedDataClass(int version) throws Exception { .hasFieldOrPropertyWithValue("legCount", 4) .hasFieldOrPropertyWithValue("age", 5); } - - static IntStream supportedJavacVersions() { - return IntStream.range(11, SourceVersion.values().length); - } } diff --git a/examples/example-lombok/src/test/resources/logback-test.xml b/examples/example-lombok/src/test/resources/logback-test.xml index a4125bf5b..c8a7930b6 100644 --- a/examples/example-lombok/src/test/resources/logback-test.xml +++ b/examples/example-lombok/src/test/resources/logback-test.xml @@ -7,7 +7,7 @@ - + diff --git a/examples/example-serviceloader-with-jpms/pom.xml b/examples/example-serviceloader-with-jpms/pom.xml index ce4af7bff..fb7d35097 100644 --- a/examples/example-serviceloader-with-jpms/pom.xml +++ b/examples/example-serviceloader-with-jpms/pom.xml @@ -32,4 +32,22 @@ test + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + + + true + + + + diff --git a/examples/example-serviceloader-with-jpms/src/test/java/io/github/ascopes/jct/examples/serviceloaderjpms/testing/ServiceProcessorTest.java b/examples/example-serviceloader-with-jpms/src/test/java/io/github/ascopes/jct/examples/serviceloaderjpms/testing/ServiceProcessorTest.java index 77b328e78..fc9b14bca 100644 --- a/examples/example-serviceloader-with-jpms/src/test/java/io/github/ascopes/jct/examples/serviceloaderjpms/testing/ServiceProcessorTest.java +++ b/examples/example-serviceloader-with-jpms/src/test/java/io/github/ascopes/jct/examples/serviceloaderjpms/testing/ServiceProcessorTest.java @@ -16,21 +16,25 @@ package io.github.ascopes.jct.examples.serviceloaderjpms.testing; -import io.github.ascopes.jct.assertions.CompilationAssert; -import io.github.ascopes.jct.compilers.Compilers; +import static io.github.ascopes.jct.assertions.JctAssertions.assertThatCompilation; +import static io.github.ascopes.jct.paths.RamPath.createPath; + +import io.github.ascopes.jct.compilers.Compilable; import io.github.ascopes.jct.examples.serviceloaderjpms.ServiceProcessor; -import io.github.ascopes.jct.paths.RamPath; +import io.github.ascopes.jct.junit.EcjCompilers; +import io.github.ascopes.jct.junit.JavacCompilers; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; @DisplayName("ServiceProcessor tests (JPMS)") class ServiceProcessorTest { @DisplayName("Expected files get created when the processor is run") - @Test - void expectedFilesGetCreated() { - var sources = RamPath - .createPath("sources") + @EcjCompilers(modules = true, minVersion = 11) // jrt isn't found on 9 and 10 for some reason. + @JavacCompilers(modules = true) + @ParameterizedTest(name = "for {0}") + void expectedFilesGetCreated(Compilable compiler) { + var sources = createPath("sources") .createFile( "com/example/InsultProvider.java", "package com.example;", @@ -53,19 +57,17 @@ void expectedFilesGetCreated() { "}" ); - var compilation = Compilers - .javac() + var compilation = compiler .addAnnotationProcessors(new ServiceProcessor()) - .addSourceRamPaths(sources) - .inheritClassPath(true) - .release(11) + .addSourcePath(sources) .compile(); - CompilationAssert.assertThatCompilation(compilation) - .isSuccessfulWithoutWarnings() - .classOutput() - .file("META-INF/services/com.example.InsultProvider") - .exists() - .hasContent("com.example.MeanInsultProviderImpl"); + assertThatCompilation(compilation) + .isSuccessfulWithoutWarnings(); + // TODO(ascopes): fix this to work with the file manager rewrite. + //.classOutput() + //.file("META-INF/services/com.example.InsultProvider") + //.exists() + //.hasContent("com.example.MeanInsultProviderImpl"); } } diff --git a/examples/example-serviceloader/pom.xml b/examples/example-serviceloader/pom.xml index b1398dc1a..20900fccf 100644 --- a/examples/example-serviceloader/pom.xml +++ b/examples/example-serviceloader/pom.xml @@ -32,4 +32,22 @@ test + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + + + true + + + + diff --git a/examples/example-serviceloader/src/test/java/io/github/ascopes/jct/examples/serviceloader/testing/ServiceProcessorTest.java b/examples/example-serviceloader/src/test/java/io/github/ascopes/jct/examples/serviceloader/testing/ServiceProcessorTest.java index 3ed881c1f..71e09e2d6 100644 --- a/examples/example-serviceloader/src/test/java/io/github/ascopes/jct/examples/serviceloader/testing/ServiceProcessorTest.java +++ b/examples/example-serviceloader/src/test/java/io/github/ascopes/jct/examples/serviceloader/testing/ServiceProcessorTest.java @@ -16,21 +16,27 @@ package io.github.ascopes.jct.examples.serviceloader.testing; -import io.github.ascopes.jct.assertions.CompilationAssert; -import io.github.ascopes.jct.compilers.Compilers; +import static io.github.ascopes.jct.assertions.JctAssertions.assertThatCompilation; +import static io.github.ascopes.jct.paths.RamPath.createPath; + +import io.github.ascopes.jct.assertions.JctAssertions; +import io.github.ascopes.jct.compilers.Compilable; import io.github.ascopes.jct.examples.serviceloader.ServiceProcessor; -import io.github.ascopes.jct.paths.RamPath; +import io.github.ascopes.jct.junit.EcjCompilers; +import io.github.ascopes.jct.junit.JavacCompilers; +import javax.tools.StandardLocation; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; @DisplayName("ServiceProcessor tests (no JPMS)") class ServiceProcessorTest { @DisplayName("Expected files get created when the processor is run") - @Test - void expectedFilesGetCreated() { - var sources = RamPath - .createPath("sources") + @EcjCompilers + @JavacCompilers + @ParameterizedTest(name = "for {0}") + void expectedFilesGetCreated(Compilable compiler) { + var sources = createPath("sources") .createFile( "com/example/InsultProvider.java", "package com.example;", @@ -53,19 +59,18 @@ void expectedFilesGetCreated() { "}" ); - var compilation = Compilers - .javac() + var compilation = compiler .addAnnotationProcessors(new ServiceProcessor()) - .addSourceRamPaths(sources) - .inheritClassPath(true) - .release(11) + .addSourcePath(sources) .compile(); - CompilationAssert.assertThatCompilation(compilation) - .isSuccessfulWithoutWarnings() - .classOutput() - .file("META-INF/services/com.example.InsultProvider") - .exists() - .hasContent("com.example.MeanInsultProviderImpl"); + assertThatCompilation(compilation) + .isSuccessfulWithoutWarnings(); + + // TODO(ascopes): fix this to work with the file manager rewrite. + //.classOutput() + //.file("META-INF/services/com.example.InsultProvider") + //.exists() + //.hasContent("com.example.MeanInsultProviderImpl"); } } diff --git a/examples/pom.xml b/examples/pom.xml index 10f291ef2..273874df9 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -37,4 +37,22 @@ + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + true + + + + + diff --git a/java-compiler-testing/pom.xml b/java-compiler-testing/pom.xml index 154603050..18dc4183e 100644 --- a/java-compiler-testing/pom.xml +++ b/java-compiler-testing/pom.xml @@ -45,6 +45,14 @@ ecj + + + org.junit.jupiter + junit-jupiter-params + provided + true + + org.reflections reflections @@ -85,10 +93,20 @@ org.apache.maven.plugins - maven-surefire-plugin + maven-checkstyle-plugin + + + org.apache.maven.plugins + maven-compiler-plugin - true + + + -Xlint:-module + @@ -97,6 +115,15 @@ maven-javadoc-plugin + + org.apache.maven.plugins + maven-surefire-plugin + + + true + + + org.jacoco jacoco-maven-plugin diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/AbstractEnumAssert.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/AbstractEnumAssert.java new file mode 100644 index 000000000..fc6d5373a --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/AbstractEnumAssert.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.assertions; + +import static io.github.ascopes.jct.utils.IterableUtils.requireNonNullValues; +import static java.util.Objects.requireNonNull; + +import io.github.ascopes.jct.utils.IterableUtils; +import java.util.stream.Collectors; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; +import org.assertj.core.api.AbstractAssert; + + +/** + * Abstract base class for an assertion on an {@link Enum}. + * + * @param the implementation type. + * @param the enum type. + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public abstract class AbstractEnumAssert, E extends Enum> + extends AbstractAssert { + + private final String friendlyTypeName; + + /** + * Initialize this enum assertion. + * + * @param value the value to assert upon. + * @param selfType the type of this assertion implementation. + * @param friendlyTypeName the friendly type name to use. + */ + protected AbstractEnumAssert( + E value, + Class selfType, + String friendlyTypeName + ) { + super(value, selfType); + this.friendlyTypeName = requireNonNull(friendlyTypeName, "friendlyTypeName"); + } + + /** + * Assert that the value is one of the given values. + * + * @param first the first value to check for. + * @param more any additional values to check for. + * @return this assertion object. + */ + @SafeVarargs + public final S isOneOf(E first, E... more) { + requireNonNull(first, "first"); + requireNonNullValues(more, "more"); + + var all = IterableUtils.asList(first, more); + if (!all.contains(actual)) { + var actualStr = reprName(actual); + + String expectedStr; + if (all.size() > 1) { + expectedStr = "one of " + all + .stream() + .map(this::reprName) + .collect(Collectors.joining(", ")); + } else { + expectedStr = reprName(first); + } + + throw failureWithActualExpected( + actualStr, + expectedStr, + "Expected %s to be %s, but it was %s", + friendlyTypeName, + expectedStr, + actualStr + ); + } + + return myself; + } + + private String reprName(E e) { + return e == null + ? "null" + : "<" + e.name() + ">"; + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/AbstractFileObjectAssert.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/AbstractFileObjectAssert.java new file mode 100644 index 000000000..df28bd2c3 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/AbstractFileObjectAssert.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.assertions; + +import io.github.ascopes.jct.utils.IoExceptionUtils; +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import javax.tools.FileObject; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; +import org.assertj.core.api.AbstractAssert; +import org.assertj.core.api.ByteArrayAssert; +import org.assertj.core.api.InstantAssert; +import org.assertj.core.api.StringAssert; +import org.assertj.core.api.UriAssert; + +/** + * Abstract assertions for {@link FileObject file objects}. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public abstract class AbstractFileObjectAssert, A extends FileObject> + extends AbstractAssert { + + /** + * Initialize this assertion. + * + * @param actual the actual value to assert on. + * @param selfType the type of the assertion implementation. + */ + protected AbstractFileObjectAssert(A actual, Class selfType) { + super(actual, selfType); + } + + /** + * Get an assertion object on the URI of the file. + * + * @return the URI assertion. + */ + public UriAssert uri() { + return new UriAssert(actual.toUri()); + } + + /** + * Get an assertion object on the name of the file. + * + * @return the string assertion. + */ + public StringAssert name() { + return new StringAssert(actual.getName()); + } + + /** + * Get an assertion object on the binary content of the file. + * + * @return the byte array assertion. + */ + public ByteArrayAssert binaryContent() { + return new ByteArrayAssert(rawContent()); + } + + /** + * Get an assertion object on the content of the file, using {@link StandardCharsets#UTF_8 UTF-8} + * encoding. + * + * @return the string assertion. + */ + public StringAssert content() { + return content(StandardCharsets.UTF_8); + } + + /** + * Get an assertion object on the content of the file. + * + * @param charset the charset to decode the file with. + * @return the string assertion. + */ + public StringAssert content(Charset charset) { + return content(charset.newDecoder()); + } + + /** + * Get an assertion object on the content of the file. + * + * @param charsetDecoder the charset decoder to use to decode the file to a string. + * @return the string assertion. + */ + public StringAssert content(CharsetDecoder charsetDecoder) { + var content = IoExceptionUtils.uncheckedIo(() -> charsetDecoder + .decode(ByteBuffer.wrap(rawContent())) + .toString()); + + return new StringAssert(content); + } + + /** + * Get an assertion object on the last modified timestamp. + * + * @return the instant assertion. + */ + public InstantAssert lastModified() { + var instant = Instant.ofEpochMilli(actual.getLastModified()); + return new InstantAssert(instant); + } + + private byte[] rawContent() { + return IoExceptionUtils.uncheckedIo(() -> { + var baos = new ByteArrayOutputStream(); + try (var is = actual.openInputStream()) { + is.transferTo(baos); + } + return baos.toByteArray(); + }); + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/CompilationAssert.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/CompilationAssert.java index a0692a170..e65a6e61a 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/CompilationAssert.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/CompilationAssert.java @@ -17,283 +17,135 @@ package io.github.ascopes.jct.assertions; import io.github.ascopes.jct.compilers.Compilation; -import io.github.ascopes.jct.compilers.TraceDiagnostic; -import io.github.ascopes.jct.paths.ModuleLocation; +import io.github.ascopes.jct.jsr199.diagnostics.TraceDiagnostic; +import java.util.List; +import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.tools.Diagnostic.Kind; -import javax.tools.JavaFileManager.Location; -import javax.tools.JavaFileObject; -import javax.tools.StandardLocation; import org.apiguardian.api.API; import org.apiguardian.api.API.Status; -import org.assertj.core.api.AbstractObjectAssert; -import org.assertj.core.api.Assertions; -import org.assertj.core.api.ListAssert; +import org.assertj.core.api.AbstractAssert; /** - * Assertions to apply to compilation output. + * Assertions that apply to a {@link Compilation}. * - * @param the implementation of the compilation. * @author Ashley Scopes * @since 0.0.1 */ @API(since = "0.0.1", status = Status.EXPERIMENTAL) -public class CompilationAssert - extends AbstractObjectAssert, C> { +@SuppressWarnings("UnusedReturnValue") +public final class CompilationAssert extends AbstractAssert { - /** - * Initialize this set of assertions. - * - * @param compilation the compilation type. - */ - protected CompilationAssert(C compilation) { - super(compilation, CompilationAssert.class); - } - - /** - * Assert that the compilation succeeded. - * - * @return this object. - */ - public CompilationAssert isSuccessful() { - if (actual.isSuccessful()) { - return myself; - } - - Predicate filter = actual.isFailOnWarnings() - ? kind -> kind == Kind.ERROR || kind == Kind.MANDATORY_WARNING || kind == Kind.WARNING - : kind -> kind == Kind.ERROR; - - var errors = actual - .getDiagnostics() - .stream() - .filter(it -> filter.test(it.getKind())) - .collect(Collectors.toList()); + private static final Set WARNING_DIAGNOSTIC_KINDS = Stream + .of(Kind.WARNING, Kind.MANDATORY_WARNING) + .collect(Collectors.toUnmodifiableSet()); - if (errors.isEmpty()) { - throw failureWithActualExpected( - "failed", - "succeeded", - "Expected successful compilation but it failed without any error diagnostics" - ); - } + private static final Set ERROR_DIAGNOSTIC_KINDS = Stream + .of(Kind.ERROR) + .collect(Collectors.toUnmodifiableSet()); - throw failureWithActualExpected( - "failed", - "succeeded", - "Expected successful compilation but it failed with errors:%n%s", - new DiagnosticCollectionRepresentation().toStringOf(errors) - ); - } + private static final Set WARNING_AND_ERROR_DIAGNOSTIC_KINDS = Stream + .of(WARNING_DIAGNOSTIC_KINDS, ERROR_DIAGNOSTIC_KINDS) + .flatMap(Set::stream) + .collect(Collectors.toUnmodifiableSet()); /** - * Assert that the compilation succeeded without warnings or errors. + * Initialize this compilation assertion. * - * @return this object. + * @param value the value to assert on. */ - public CompilationAssert isSuccessfulWithoutWarnings() { - isSuccessful(); - warnings().withFailMessage("Expected no warnings").isEmpty(); - mandatoryWarnings().withFailMessage("Expected no mandatory warnings").isEmpty(); - return myself; + public CompilationAssert(Compilation value) { + super(value, CompilationAssert.class); } /** - * Assert that the compilation failed. + * Assert that the compilation was successful. * - * @return this object. + * @return this assertion object. */ - public CompilationAssert isFailure() { + public CompilationAssert isSuccessful() { if (actual.isFailure()) { - return myself; + // If we have error diagnostics, add them to the error message to provide helpful debugging + // information. If we are treating warnings as errors, then we want to include those in this + // as well. + Predicate> isErrorDiagnostic = actual.isFailOnWarnings() + ? diag -> WARNING_AND_ERROR_DIAGNOSTIC_KINDS.contains(diag.getKind()) + : diag -> ERROR_DIAGNOSTIC_KINDS.contains(diag.getKind()); + + var diagnostics = actual + .getDiagnostics() + .stream() + .filter(isErrorDiagnostic) + .collect(Collectors.toUnmodifiableList()); + + failWithDiagnostics(diagnostics, "Expected a successful compilation, but it failed."); } - throw failureWithActualExpected( - "succeeded", - "failed", - "Expected failed compilation but it succeeded" - ); - } - - /** - * Get assertions to perform on the collection of diagnostics that were returned. - * - * @return the assertions to perform across the diagnostics. - */ - public DiagnosticListAssert diagnostics() { - return DiagnosticListAssert.assertThatDiagnostics(actual.getDiagnostics()); - } - - /** - * Get assertions to perform on all diagnostics that match the given predicate. - * - * @param predicate the predicate to filter diagnostics by. - * @return the assertions to perform across the filtered diagnostics. - */ - public DiagnosticListAssert diagnostics( - Predicate> predicate - ) { - return actual - .getDiagnostics() - .stream() - .filter(predicate) - .collect(Collectors.collectingAndThen( - Collectors.toList(), - DiagnosticListAssert::assertThatDiagnostics - )); - } - - /** - * Get assertions to perform on all error diagnostics. - * - * @return the assertions to perform across the error diagnostics. - */ - public DiagnosticListAssert errors() { - return diagnostics(diagnostic -> diagnostic.getKind() == Kind.ERROR); - } - - /** - * Get assertions to perform on all warning diagnostics. - * - * @return the assertions to perform across the warning diagnostics. - */ - public DiagnosticListAssert warnings() { - return diagnostics(diagnostic -> diagnostic.getKind() == Kind.WARNING); - } - - /** - * Get assertions to perform on all mandatory warning diagnostics. - * - * @return the assertions to perform across the warning diagnostics. - */ - public DiagnosticListAssert mandatoryWarnings() { - return diagnostics(diagnostic -> diagnostic.getKind() == Kind.MANDATORY_WARNING); - } - - /** - * Get assertions to perform on all note diagnostics. - * - * @return the assertions to perform across the note diagnostics. - */ - public DiagnosticListAssert notes() { - return diagnostics(diagnostic -> diagnostic.getKind() == Kind.NOTE); - } - - /** - * Get assertions to perform on all {@link Kind#OTHER}-kinded diagnostics. - * - * @return the assertions to perform across the {@link Kind#OTHER}-kinded diagnostics. - */ - public DiagnosticListAssert otherDiagnostics() { - return diagnostics(diagnostic -> diagnostic.getKind() == Kind.OTHER); - } - - /** - * Get assertions to perform on the compiler log output. - * - * @return the assertions to perform on the compiler log output. - */ - public ListAssert outputLines() { - return Assertions.assertThat(actual.getOutputLines()); - } - - /** - * Get assertions to perform on a given location. - * - * @param location the location to perform assertions on. - * @return the assertions to perform. - */ - public PathLocationManagerAssert location(Location location) { - var locationManager = actual - .getPathLocationRepository() - .getManager(location) - .orElse(null); - - return PathLocationManagerAssert.assertThatLocation(locationManager); + return myself; } /** - * Get assertions to perform on a given location of a module. + * Assert that the compilation was successful and had no warnings. * - * @param location the location to perform assertions on. - * @param moduleName the module name within the location to perform assertions on. - * @return the assertions to perform. - */ - public PathLocationManagerAssert location(Location location, String moduleName) { - var locationManager = actual - .getPathLocationRepository() - .getManager(new ModuleLocation(location, moduleName)) - .orElse(null); - - return PathLocationManagerAssert.assertThatLocation(locationManager); - } - - /** - * Perform assertions on the class output roots. + *

If warnings were treated as errors by the compiler, then this is identical to calling + * {@link #isSuccessful()}. * - * @return the assertions to perform. + * @return this assertion object. */ - public PathLocationManagerAssert classOutput() { - return location(StandardLocation.CLASS_OUTPUT); + public CompilationAssert isSuccessfulWithoutWarnings() { + isSuccessful(); + diagnostics().hasNoErrorsOrWarnings(); + return myself; } /** - * Perform assertions on the class output roots for a given module name. + * Assert that the compilation was a failure. * - * @param moduleName the name of the module. - * @return the assertions to perform. + * @return this assertion object. */ - public PathLocationManagerAssert classOutput(String moduleName) { - return location(StandardLocation.CLASS_OUTPUT, moduleName); - } + public CompilationAssert isFailure() { + if (actual.isSuccessful()) { + var warnings = actual + .getDiagnostics() + .stream() + .filter(kind -> WARNING_DIAGNOSTIC_KINDS.contains(kind.getKind())) + .collect(Collectors.toUnmodifiableList()); - /** - * Perform assertions on the native header outputs. - * - * @return the assertions to perform. - */ - public PathLocationManagerAssert nativeHeaders() { - return location(StandardLocation.NATIVE_HEADER_OUTPUT); - } + failWithDiagnostics(warnings, "Expected compilation to fail, but it succeeded."); + } - /** - * Perform assertions on the native header outputs for a given module name. - * - * @param moduleName the name of the module. - * @return the assertions to perform. - */ - public PathLocationManagerAssert nativeHeaders(String moduleName) { - return location(StandardLocation.NATIVE_HEADER_OUTPUT, moduleName); + return myself; } /** - * Perform assertions on the generated source outputs. + * Get assertions for diagnostics. * - * @return the assertions to perform. + * @return assertions for the diagnostics. */ - public PathLocationManagerAssert generatedSources() { - return location(StandardLocation.SOURCE_OUTPUT); + public DiagnosticListAssert diagnostics() { + return new DiagnosticListAssert(actual.getDiagnostics()); } - /** - * Perform assertions on the generated source outputs for a given module name. - * - * @param moduleName the name of the module. - * @return the assertions to perform. - */ - public PathLocationManagerAssert generatedSources(String moduleName) { - return location(StandardLocation.SOURCE_PATH, moduleName); - } + private void failWithDiagnostics( + List> diagnostics, + String message, + Object... args + ) { + if (diagnostics.isEmpty()) { + failWithMessage(message, args); + } else { + var fullMessage = String.join( + "\n\n", + args.length > 0 + ? String.format(message, args) + : message, + "Diagnostics:", + DiagnosticListRepresentation.getInstance().toStringOf(diagnostics) + ); - /** - * Create a new assertion object. - * - * @param compilation the compilation to assert on. - * @param the compilation type. - * @return the compilation assertions to use. - */ - public static CompilationAssert assertThatCompilation(C compilation) { - return new CompilationAssert<>(compilation); + failWithMessage(fullMessage); + } } } diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/DiagnosticAssert.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/DiagnosticAssert.java index 649a454aa..2eb0141e0 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/DiagnosticAssert.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/DiagnosticAssert.java @@ -16,181 +16,189 @@ package io.github.ascopes.jct.assertions; -import io.github.ascopes.jct.compilers.TraceDiagnostic; +import static java.util.Objects.requireNonNull; + +import io.github.ascopes.jct.jsr199.diagnostics.TraceDiagnostic; import java.util.Locale; -import javax.tools.Diagnostic.Kind; +import java.util.OptionalLong; +import javax.tools.Diagnostic; import javax.tools.JavaFileObject; import org.apiguardian.api.API; import org.apiguardian.api.API.Status; -import org.assertj.core.api.AbstractComparableAssert; -import org.assertj.core.api.AbstractInstantAssert; -import org.assertj.core.api.AbstractLongAssert; -import org.assertj.core.api.AbstractObjectAssert; -import org.assertj.core.api.AbstractStringAssert; -import org.assertj.core.api.Assertions; -import org.assertj.core.api.ListAssert; +import org.assertj.core.api.AbstractAssert; +import org.assertj.core.api.InstantAssert; +import org.assertj.core.api.LongAssert; +import org.assertj.core.api.ObjectAssert; +import org.assertj.core.api.OptionalAssert; +import org.assertj.core.api.OptionalLongAssert; +import org.assertj.core.api.StringAssert; /** - * Assertions to apply to a diagnostic. + * Assertions for an individual {@link TraceDiagnostic trace diagnostic}. * * @author Ashley Scopes * @since 0.0.1 */ @API(since = "0.0.1", status = Status.EXPERIMENTAL) public final class DiagnosticAssert - extends AbstractObjectAssert> { + extends AbstractAssert> { /** - * Initialize this set of assertions. + * Initialize this assertion type. * - * @param traceDiagnostic the diagnostic to assert upon. + * @param value the value to assert on. */ - private DiagnosticAssert(TraceDiagnostic traceDiagnostic) { - super(traceDiagnostic, DiagnosticAssert.class); + public DiagnosticAssert(TraceDiagnostic value) { + super(value, DiagnosticAssert.class); + withRepresentation(DiagnosticRepresentation.getInstance()); } /** - * Perform an assertion on the timestamp of the diagnostic. + * Get assertions for the kind of the diagnostic. * - * @return an assertion to perform on the timestamp. + * @return the assertions for the diagnostic kind. */ - public AbstractInstantAssert timestamp() { - return Assertions.assertThat(actual.getTimestamp()); + public DiagnosticKindAssert kind() { + return new DiagnosticKindAssert(actual.getKind()); } /** - * Perform an assertion on the thread ID that the diagnostic was reported from. + * Get assertions for the source of the diagnostic. * - * @return an assertion to perform on the thread ID. + * @return the assertions for the source of the diagnostic. */ - public AbstractLongAssert threadId() { - return Assertions.assertThat(actual.getThreadId()); + public JavaFileObjectAssert source() { + return new JavaFileObjectAssert(actual.getSource()); } /** - * Perform an assertion on the optional thread name that the diagnostic was reported from. + * Get assertions for the position of the diagnostic. + * + *

The value may be empty if no position was provided. * - * @return an assertion to perform on the thread name. + * @return the assertions for the position of the diagnostic. */ - public AbstractStringAssert threadName() { - return Assertions.assertThat(actual.getThreadName().orElse(null)); + public OptionalLongAssert position() { + return assertPosition(actual.getPosition(), "position"); } /** - * Perform an assertion on the stack trace location that the diagnostic was reported from. + * Get assertions for the start position of the diagnostic. * - * @return an assertion to perform on the list of stack trace frames. + *

The value may be empty if no position was provided. + * + * @return the assertions for the start position of the diagnostic. */ - public ListAssert stackTrace() { - return Assertions.assertThat(actual.getStackTrace()); + public OptionalLongAssert startPosition() { + return assertPosition(actual.getPosition(), "startPosition"); } /** - * Perform an assertion on the kind of the diagnostic. + * Get assertions for the end position of the diagnostic. + * + *

The value may be empty if no position was provided. * - * @return an assertion to perform on the kind. + * @return the assertions for the end position of the diagnostic. */ - public AbstractComparableAssert kind() { - return Assertions.assertThat(actual.getKind()); + public OptionalLongAssert endPosition() { + return assertPosition(actual.getEndPosition(), "endPosition"); } /** - * Perform an assertion on the source that the diagnostic was reported from. + * Get assertions for the line number of the diagnostic. * - * @return the assertion to perform on the source. + *

The value may be empty if no position was provided. + * + * @return the assertions for the line number of the diagnostic. */ - public AbstractObjectAssert source() { - return Assertions.assertThat(actual.getSource()); + public OptionalLongAssert lineNumber() { + return assertPosition(actual.getLineNumber(), "lineNumber"); } /** - * Perform an assertion on the position in the source that the diagnostic was reported from. + * Get assertions for the column number of the diagnostic. + * + *

The value may be empty if no position was provided. * - * @return an assertion to perform on the position. + * @return the assertions for the column number of the diagnostic. */ - public AbstractLongAssert position() { - return Assertions.assertThat(actual.getPosition()); + public OptionalLongAssert columnNumber() { + return assertPosition(actual.getColumnNumber(), "columnNumber"); } /** - * Perform an assertion on the start position in the source that the diagnostic was reported - * from. + * Get assertions for the code of the diagnostic. * - * @return an assertion to perform on the start position. + * @return the assertions for the code of the diagnostic. */ - public AbstractLongAssert startPosition() { - return Assertions.assertThat(actual.getStartPosition()); + public StringAssert code() { + return new StringAssert(actual.getCode()); } /** - * Perform an assertion on the end position in the source that the diagnostic was reported from. + * Get assertions for the message of the diagnostic, assuming the default locale. * - * @return an assertion to perform on the end position. + * @return the assertions for the message of the diagnostic. */ - public AbstractLongAssert endPosition() { - return Assertions.assertThat(actual.getEndPosition()); + public StringAssert message() { + return new StringAssert(actual.getMessage(null)); } /** - * Perform an assertion on the line number in the source that the diagnostic was reported from. + * Get assertions for the message of the diagnostic. * - * @return an assertion to perform on the line number. + * @param locale the locale to use. + * @return the assertions for the message of the diagnostic. */ - public AbstractLongAssert lineNumber() { - return Assertions.assertThat(actual.getLineNumber()); + public StringAssert message(Locale locale) { + requireNonNull(locale, "locale"); + return new StringAssert(actual.getMessage(locale)); } /** - * Perform an assertion on the column number in the source that the diagnostic was reported from. + * Get assertions for the timestamp of the diagnostic. * - * @return an assertion to perform on the column number. + * @return the assertions for the timestamp of the diagnostic. */ - public AbstractLongAssert columnNumber() { - return Assertions.assertThat(actual.getColumnNumber()); + public InstantAssert timestamp() { + return new InstantAssert(actual.getTimestamp()); } /** - * Perform an assertion on the diagnostic code. - * - *

This is usually only present for compiler-provided diagnostics, and is not able to be - * specified in the standard annotation processing API (where this will be null). + * Get assertions for the thread ID of the thread that reported the diagnostic to the compiler. * - * @return an assertion to perform on the diagnostic code. + * @return the assertions for the thread ID. */ - public AbstractStringAssert code() { - return Assertions.assertThat(actual.getCode()); + public LongAssert threadId() { + return new LongAssert(actual.getThreadId()); } /** - * Perform an assertion on the diagnostic message, assuming a root locale. + * Get assertions for the thread name of the thread that reported the diagnostic. This may not be + * present in some situations. * - * @return an assertion to perform on the message, assuming a root locale. + * @return the assertions for the optional thread name. */ - public AbstractStringAssert message() { - return message(Locale.ROOT); + public OptionalAssert threadName() { + // TODO(ascopes): should I be initializing this in this way? + return new OptionalAssert<>(actual.getThreadName()) {}; } /** - * Perform an assertion on the diagnostic message, using the given locale. + * Get assertions for the stack trace of the location the diagnostic was reported to. * - * @param locale the locale to use. - * @return an assertion to perform on the message. + * @return the assertions for the stack trace. */ - public AbstractStringAssert message(Locale locale) { - return Assertions.assertThat(actual.getMessage(locale)); + public StackTraceAssert stackTrace() { + return new StackTraceAssert(actual.getStackTrace()); } - /** - * Create a new set of assertions for a specific diagnostic. - * - * @param diagnostic the diagnostic to assert on. - * @return the assertions. - */ - public static DiagnosticAssert assertThatDiagnostic( - TraceDiagnostic diagnostic - ) { - return new DiagnosticAssert(diagnostic) - .withRepresentation(new DiagnosticRepresentation()); + private OptionalLongAssert assertPosition(long position, String name) { + // TODO(ascopes): should I be initializing this in this way? + return new OptionalLongAssert( + position == Diagnostic.NOPOS ? OptionalLong.empty() : OptionalLong.of(position) + ) {}.describedAs("%s of %d", name, position); + + } } - diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/DiagnosticKindAssert.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/DiagnosticKindAssert.java new file mode 100644 index 000000000..1a8dd6635 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/DiagnosticKindAssert.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.assertions; + +import javax.tools.Diagnostic.Kind; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * Assertions for an individual diagnostic kind. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public final class DiagnosticKindAssert + extends AbstractEnumAssert { + + /** + * Initialize this assertion type. + * + * @param value the value to assert on. + */ + public DiagnosticKindAssert(Kind value) { + super(value, DiagnosticKindAssert.class, "kind"); + } + + /** + * Assert that the kind is {@link Kind#ERROR}. + * + * @return this assertion object. + */ + public DiagnosticKindAssert isError() { + return isOneOf(Kind.ERROR); + } + + /** + * Assert that the kind is either {@link Kind#WARNING} or {@link Kind#MANDATORY_WARNING}. + * + * @return this assertion object. + */ + public DiagnosticKindAssert isWarning() { + return isOneOf(Kind.WARNING, Kind.MANDATORY_WARNING); + } + + /** + * Assert that the kind is {@link Kind#WARNING}. + * + * @return this assertion object. + */ + public DiagnosticKindAssert isCustomWarning() { + return isOneOf(Kind.WARNING); + } + + /** + * Assert that the kind is {@link Kind#MANDATORY_WARNING}. + * + * @return this assertion object. + */ + public DiagnosticKindAssert isMandatoryWarning() { + return isOneOf(Kind.MANDATORY_WARNING); + } + + /** + * Assert that the kind is {@link Kind#NOTE}. + * + * @return this assertion object. + */ + public DiagnosticKindAssert isNote() { + return isOneOf(Kind.NOTE); + } + + /** + * Assert that the kind is {@link Kind#OTHER}. + * + * @return this assertion object. + */ + public DiagnosticKindAssert isOther() { + return isOneOf(Kind.OTHER); + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/DiagnosticListAssert.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/DiagnosticListAssert.java index 03e6f3469..676902f16 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/DiagnosticListAssert.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/DiagnosticListAssert.java @@ -16,51 +16,261 @@ package io.github.ascopes.jct.assertions; -import io.github.ascopes.jct.compilers.TraceDiagnostic; +import static java.util.Objects.requireNonNull; +import static java.util.function.Predicate.not; + +import io.github.ascopes.jct.jsr199.diagnostics.TraceDiagnostic; +import io.github.ascopes.jct.utils.IterableUtils; +import io.github.ascopes.jct.utils.StringUtils; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Locale; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import javax.tools.Diagnostic.Kind; import javax.tools.JavaFileObject; import org.apiguardian.api.API; import org.apiguardian.api.API.Status; -import org.assertj.core.api.FactoryBasedNavigableListAssert; +import org.assertj.core.api.AbstractListAssert; /** - * Assertions to apply to a list of diagnostics. + * Assertions for a list of diagnostics. * * @author Ashley Scopes * @since 0.0.1 */ -//@formatter:off @API(since = "0.0.1", status = Status.EXPERIMENTAL) -public final class DiagnosticListAssert - extends FactoryBasedNavigableListAssert< - DiagnosticListAssert, - List>, - TraceDiagnostic, - DiagnosticAssert - > { - //@formatter:on +public class DiagnosticListAssert + extends AbstractListAssert>, TraceDiagnostic, DiagnosticAssert> { /** - * Initialize these assertions. + * Initialize this assertion. * - * @param diagnostics the list of assertions to assert on. + * @param traceDiagnostics the diagnostics to perform assertions on. */ - private DiagnosticListAssert( - List> diagnostics + public DiagnosticListAssert( + List> traceDiagnostics ) { - super(diagnostics, DiagnosticListAssert.class, DiagnosticAssert::assertThatDiagnostic); + super(traceDiagnostics, DiagnosticListAssert.class); + withRepresentation(DiagnosticListRepresentation.getInstance()); + } + + /** + * Get a {@link DiagnosticListAssert} across all diagnostics that have the {@link Kind#ERROR} + * kind. + * + * @return the assertion object for {@link Kind#ERROR} diagnostics. + */ + public DiagnosticListAssert errors() { + return filteringBy(kind(Kind.ERROR)); + } + + /** + * Get a {@link DiagnosticListAssert} across all diagnostics that have the {@link Kind#WARNING} or + * {@link Kind#MANDATORY_WARNING} kind. + * + * @return the assertion object for {@link Kind#WARNING} and {@link Kind#MANDATORY_WARNING} + * diagnostics. + */ + public DiagnosticListAssert warnings() { + return filteringBy(kind(Kind.WARNING, Kind.MANDATORY_WARNING)); + } + + /** + * Get a {@link DiagnosticListAssert} across all diagnostics that have the {@link Kind#WARNING} + * kind. + * + * @return the assertion object for {@link Kind#WARNING} diagnostics. + */ + public DiagnosticListAssert customWarnings() { + return filteringBy(kind(Kind.WARNING)); + } + + /** + * Get a {@link DiagnosticListAssert} across all diagnostics that have the + * {@link Kind#MANDATORY_WARNING} kind. + * + * @return the assertion object for {@link Kind#MANDATORY_WARNING} diagnostics. + */ + public DiagnosticListAssert mandatoryWarnings() { + return filteringBy(kind(Kind.MANDATORY_WARNING)); + } + + /** + * Get a {@link DiagnosticListAssert} across all diagnostics that have the {@link Kind#NOTE} + * kind. + * + * @return the assertion object for {@link Kind#NOTE} diagnostics. + */ + public DiagnosticListAssert notes() { + return filteringBy(kind(Kind.NOTE)); + } + + /** + * Get a {@link DiagnosticListAssert} across all diagnostics that have the {@link Kind#OTHER} + * kind. + * + * @return the assertion object for {@link Kind#OTHER} diagnostics. + */ + public DiagnosticListAssert others() { + return filteringBy(kind(Kind.OTHER)); + } + + /** + * Get a {@link DiagnosticListAssert} that contains diagnostics corresponding to any of the given + * {@link Kind kinds}. + * + * @param kind the first kind to match. + * @param moreKinds additional kinds to match. + * @return the assertion object for the filtered diagnostics. + */ + public DiagnosticListAssert filterByKinds(Kind kind, Kind... moreKinds) { + return filteringBy(kind(kind, moreKinds)); + } + + /** + * Get a {@link DiagnosticListAssert} that contains diagnostics corresponding to none of the given + * {@link Kind kinds}. + * + * @param kind the first kind to ensure are not matched. + * @param moreKinds additional kinds to ensure are not matched. + * @return the assertion object for the filtered diagnostics. + */ + public DiagnosticListAssert filterExceptKinds(Kind kind, Kind... moreKinds) { + return filteringBy(not(kind(kind, moreKinds))); + } + + /** + * Assert that this list has no {@link Kind#ERROR} diagnostics. + * + * @return this assertion object for further call chaining. + */ + public DiagnosticListAssert hasNoErrors() { + return hasNoKinds(Kind.ERROR); } /** - * Create a new set of assertions for a list of diagnostics. + * Assert that this list has no {@link Kind#ERROR}, {@link Kind#WARNING}, or + * {@link Kind#MANDATORY_WARNING} diagnostics. * - * @param diagnostics the list of diagnostics to assert on. - * @return the assertions. + * @return this assertion object for further call chaining. */ - public static DiagnosticListAssert assertThatDiagnostics( - List> diagnostics + public DiagnosticListAssert hasNoErrorsOrWarnings() { + return hasNoKinds(Kind.ERROR, Kind.WARNING, Kind.MANDATORY_WARNING); + } + + /** + * Assert that this list has no {@link Kind#WARNING} or {@link Kind#MANDATORY_WARNING} + * diagnostics. + * + * @return this assertion object for further call chaining. + */ + public DiagnosticListAssert hasNoWarnings() { + return hasNoKinds(Kind.WARNING, Kind.MANDATORY_WARNING); + } + + /** + * Assert that this list has no {@link Kind#WARNING} diagnostics. + * + * @return this assertion object for further call chaining. + */ + public DiagnosticListAssert hasNoCustomWarnings() { + return hasNoKinds(Kind.WARNING); + } + + /** + * Assert that this list has no {@link Kind#MANDATORY_WARNING} diagnostics. + * + * @return this assertion object for further call chaining. + */ + public DiagnosticListAssert hasNoMandatoryWarnings() { + return hasNoKinds(Kind.MANDATORY_WARNING); + } + + /** + * Assert that this list has no {@link Kind#NOTE} diagnostics. + * + * @return this assertion object for further call chaining. + */ + public DiagnosticListAssert hasNoNotes() { + return hasNoKinds(Kind.NOTE); + } + + /** + * Assert that this list has no {@link Kind#OTHER} diagnostics. + * + * @return this assertion object for further call chaining. + */ + public DiagnosticListAssert hasNoOtherDiagnostics() { + return hasNoKinds(Kind.OTHER); + } + + /** + * Assert that this list has no diagnostics matching any of the given kinds. + * + * @param kind the first kind to check for. + * @param moreKinds any additional kinds to check for. + * @return this assertion object for further call chaining. + */ + public DiagnosticListAssert hasNoKinds(Kind kind, Kind... moreKinds) { + return filteringBy(kind(kind, moreKinds)) + .withFailMessage(() -> { + var allKindsString = IterableUtils + .asList(kind, moreKinds) + .stream() + .map(next -> next.name().toLowerCase(Locale.ROOT).replace('_', ' ')) + .collect(Collectors.collectingAndThen( + Collectors.toUnmodifiableList(), + names -> StringUtils.toWordedList(names, ", ", ", or ") + )); + + return String.format("Expected no %s diagnostics", allKindsString); + }); + } + + /** + * Filter diagnostics by a given predicate and return an assertion object that applies to all + * diagnostics that match that predicate. + * + * @param predicate the predicate to match. + * @return the assertion object for the diagnostics that match. + */ + public DiagnosticListAssert filteringBy( + Predicate> predicate + ) { + requireNonNull(predicate, "predicate"); + + return actual + .stream() + .filter(predicate) + .collect(Collectors.collectingAndThen( + Collectors.toUnmodifiableList(), + DiagnosticListAssert::new + )); + } + + @Override + protected DiagnosticAssert toAssert( + TraceDiagnostic value, + String description ) { - return new DiagnosticListAssert(diagnostics) - .withRepresentation(new DiagnosticCollectionRepresentation()); + return new DiagnosticAssert(value).describedAs(description); + } + + @Override + protected DiagnosticListAssert newAbstractIterableAssert( + Iterable> iterable + ) { + var list = new ArrayList>(); + iterable.forEach(list::add); + return new DiagnosticListAssert(list); + } + + private Predicate> kind(Kind kind, Kind... moreKinds) { + return diagnostic -> { + var actualKind = diagnostic.getKind(); + return actualKind.equals(kind) || Arrays.asList(moreKinds).contains(actualKind); + }; } } diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/DiagnosticCollectionRepresentation.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/DiagnosticListRepresentation.java similarity index 73% rename from java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/DiagnosticCollectionRepresentation.java rename to java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/DiagnosticListRepresentation.java index bab7ca38b..ae062be55 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/DiagnosticCollectionRepresentation.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/DiagnosticListRepresentation.java @@ -18,7 +18,7 @@ import static java.util.stream.Collectors.joining; -import io.github.ascopes.jct.compilers.TraceDiagnostic; +import io.github.ascopes.jct.jsr199.diagnostics.TraceDiagnostic; import java.util.Collection; import javax.tools.JavaFileObject; import org.apiguardian.api.API; @@ -32,12 +32,21 @@ * @since 0.0.1 */ @API(since = "0.0.1", status = Status.EXPERIMENTAL) -public class DiagnosticCollectionRepresentation implements Representation { +public final class DiagnosticListRepresentation implements Representation { + + private static final DiagnosticListRepresentation INSTANCE + = new DiagnosticListRepresentation(); /** - * Initialize this diagnostic collection representation. + * Get an instance of this diagnostic collection representation. + * + * @return the instance. */ - public DiagnosticCollectionRepresentation() { + public static DiagnosticListRepresentation getInstance() { + return INSTANCE; + } + + private DiagnosticListRepresentation() { // Nothing to see here, move along now. } @@ -52,7 +61,7 @@ public String toStringOf(Object object) { return diagnostics .stream() - .map(new DiagnosticRepresentation()::toStringOf) + .map(DiagnosticRepresentation.getInstance()::toStringOf) .map(" - "::concat) .collect(joining("\n\n")); } diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/DiagnosticRepresentation.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/DiagnosticRepresentation.java index bcbb7d29c..b7b1d31e1 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/DiagnosticRepresentation.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/DiagnosticRepresentation.java @@ -18,9 +18,9 @@ import static javax.tools.Diagnostic.NOPOS; -import io.github.ascopes.jct.compilers.TraceDiagnostic; -import io.github.ascopes.jct.intern.IoExceptionUtils; -import io.github.ascopes.jct.intern.StringUtils; +import io.github.ascopes.jct.jsr199.diagnostics.TraceDiagnostic; +import io.github.ascopes.jct.utils.IoExceptionUtils; +import io.github.ascopes.jct.utils.StringUtils; import java.io.IOException; import java.util.Locale; import java.util.Optional; @@ -37,14 +37,24 @@ * @since 0.0.1 */ @API(since = "0.0.1", status = Status.EXPERIMENTAL) -public class DiagnosticRepresentation implements Representation { - private static final int ADDITIONAL_CONTEXT_LINES = 2; - private static final String PADDING = " ".repeat(8); +public final class DiagnosticRepresentation implements Representation { + + private static final DiagnosticRepresentation INSTANCE + = new DiagnosticRepresentation(); /** - * Initialize this diagnostic representation. + * Get an instance of this diagnostic representation. + * + * @return the instance. */ - public DiagnosticRepresentation() { + public static DiagnosticRepresentation getInstance() { + return INSTANCE; + } + + private static final int ADDITIONAL_CONTEXT_LINES = 2; + private static final String PADDING = " ".repeat(4); + + private DiagnosticRepresentation() { // Nothing to see here, move along now. } @@ -65,15 +75,18 @@ public String toStringOf(Object object) { builder.append(code); } - builder - .append(' ') - .append(diagnostic.getSource().getName()) - .append(" (at line ") - .append(diagnostic.getLineNumber()) - .append(", col ") - .append(diagnostic.getColumnNumber()) - .append(")") - .append("\n\n"); + if (diagnostic.getSource() != null) { + builder + .append(' ') + .append(diagnostic.getSource().getName()) + .append(" (at line ") + .append(diagnostic.getLineNumber()) + .append(", col ") + .append(diagnostic.getColumnNumber()) + .append(")"); + } + + builder.append("\n\n"); IoExceptionUtils.uncheckedIo(() -> { extractSnippet(diagnostic) @@ -92,7 +105,13 @@ public String toStringOf(Object object) { private Optional extractSnippet( Diagnostic diagnostic ) throws IOException { - if (diagnostic.getStartPosition() == NOPOS || diagnostic.getEndPosition() == NOPOS) { + var source = diagnostic.getSource(); + + var noSnippet = source == null + || diagnostic.getStartPosition() == NOPOS + || diagnostic.getEndPosition() == NOPOS; + + if (noSnippet) { // No info available about position, so don't bother extracting anything. return Optional.empty(); } diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/FileManagerAssert.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/FileManagerAssert.java new file mode 100644 index 000000000..139e91a92 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/FileManagerAssert.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.assertions; + +import io.github.ascopes.jct.jsr199.FileManager; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; +import org.assertj.core.api.AbstractAssert; + +/** + * Assertions for a file manager. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public final class FileManagerAssert extends AbstractAssert { + + /** + * Initialize this file manager assertion object. + * + * @param fileManager the file manager to perform assertions upon. + */ + public FileManagerAssert(FileManager fileManager) { + super(fileManager, FileManagerAssert.class); + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/FileObjectAssert.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/FileObjectAssert.java new file mode 100644 index 000000000..a0576b627 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/FileObjectAssert.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.assertions; + +import javax.tools.FileObject; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * Assertions for {@link FileObject file objects}. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public final class FileObjectAssert + extends AbstractFileObjectAssert { + + /** + * Create a new instance of this assertion object. + * + * @param actual the file object to assert upon. + */ + public FileObjectAssert(FileObject actual) { + super(actual, FileObjectAssert.class); + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/JavaFileObjectAssert.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/JavaFileObjectAssert.java new file mode 100644 index 000000000..e1eac6f17 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/JavaFileObjectAssert.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.assertions; + +import javax.tools.JavaFileObject; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * Assertions for {@link JavaFileObject Java file objects}. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public final class JavaFileObjectAssert + extends AbstractFileObjectAssert { + + /** + * Create a new instance of this assertion object. + * + * @param actual the Java file object to assert upon. + */ + public JavaFileObjectAssert(JavaFileObject actual) { + super(actual, JavaFileObjectAssert.class); + } + + /** + * Perform an assertion on the file object kind. + * + * @return the assertions for the kind. + */ + public JavaFileObjectKindAssert kind() { + return new JavaFileObjectKindAssert(actual.getKind()); + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/JavaFileObjectKindAssert.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/JavaFileObjectKindAssert.java new file mode 100644 index 000000000..3ec4eb544 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/JavaFileObjectKindAssert.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.assertions; + +import javax.tools.JavaFileObject.Kind; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; +import org.assertj.core.api.StringAssert; + +/** + * Assertions for an individual {@link Kind Java file object kind}. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public final class JavaFileObjectKindAssert + extends AbstractEnumAssert { + + /** + * Initialize this assertion type. + * + * @param value the value to assert on. + */ + public JavaFileObjectKindAssert(Kind value) { + super(value, JavaFileObjectKindAssert.class, "kind"); + } + + /** + * Assert that the kind is a {@link Kind#SOURCE}. + * + * @return this assertion object. + */ + public JavaFileObjectKindAssert isSource() { + return isOneOf(Kind.SOURCE); + } + + /** + * Assert that the kind is a {@link Kind#CLASS}. + * + * @return this assertion object. + */ + public JavaFileObjectKindAssert isClass() { + return isOneOf(Kind.CLASS); + } + + /** + * Assert that the kind is an {@link Kind#HTML HTML source}. + * + * @return this assertion object. + */ + public JavaFileObjectKindAssert isHtml() { + return isOneOf(Kind.HTML); + } + + /** + * Assert that the kind is {@link Kind#OTHER some other unknown kind}. + * + * @return this assertion object. + */ + public JavaFileObjectKindAssert isOther() { + return isOneOf(Kind.OTHER); + } + + /** + * Perform an assertion on the file extension of the kind. + * + * @return the assertions for the file extension of the kind. + */ + public StringAssert extension() { + return new StringAssert(actual.extension); + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/JctAssertions.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/JctAssertions.java new file mode 100644 index 000000000..20c6721e2 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/JctAssertions.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.assertions; + +import io.github.ascopes.jct.compilers.Compilation; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * Helper class to provide fluent creation of assertions for compilations. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public final class JctAssertions { + + private JctAssertions() { + throw new UnsupportedOperationException("static-only class"); + } + + /** + * Perform a regular compilation assertion. + * + * @param compilation the compilation to assert on. + * @return the assertion. + */ + public static CompilationAssert assertThatCompilation(Compilation compilation) { + return thenCompilation(compilation); + } + + /** + * Perform a BDD-style compilation assertion. + * + * @param compilation the compilation to assert on. + * @return the assertion. + */ + public static CompilationAssert thenCompilation(Compilation compilation) { + return new CompilationAssert(compilation); + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/OptionalPathAssert.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/OptionalPathAssert.java deleted file mode 100644 index cd04af219..000000000 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/OptionalPathAssert.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright (C) 2022 Ashley Scopes - * - * 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.assertions; - -import io.github.ascopes.jct.paths.PathLocationManager; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Collection; -import java.util.LinkedHashSet; -import java.util.Objects; -import java.util.stream.Collectors; -import me.xdrop.fuzzywuzzy.FuzzySearch; -import me.xdrop.fuzzywuzzy.model.BoundExtractedResult; -import org.apiguardian.api.API; -import org.apiguardian.api.API.Status; -import org.assertj.core.api.AbstractAssert; -import org.assertj.core.api.PathAssert; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - - -/** - * Assertions on a path that we have requested for a given location that may not actually exist. - * - * @author Ashley Scopes - * @since 0.0.1 - */ -@API(since = "0.0.1", status = Status.EXPERIMENTAL) -public class OptionalPathAssert extends AbstractAssert { - - private static final Logger LOGGER = LoggerFactory.getLogger(OptionalPathAssert.class); - private final PathLocationManager manager; - private final String providedPath; - - private OptionalPathAssert( - PathLocationManager manager, - String providedPath, - Path resolvedPath - ) { - super(resolvedPath, OptionalPathAssert.class); - this.manager = Objects.requireNonNull(manager); - this.providedPath = Objects.requireNonNull(providedPath); - } - - /** - * Assert that the file exists. - * - * @return assertions to perform on the path itself. - */ - public PathAssert exists() { - if (actual == null) { - var similarMatches = findSimilarlyNamedPaths(); - - if (similarMatches.isEmpty()) { - throw failure( - "Path %s was not found in any of the roots for location %s", - providedPath, - manager.getLocation().getName() - ); - } else { - var names = similarMatches - .stream() - .map(Path::toString) - .map(" - "::concat) - .collect(Collectors.joining("\n")); - - throw failure( - "Path %s was not found in any of the roots for location %s.\n" - + " Maybe you meant:\n%s", - providedPath, - manager.getLocation().getName(), - names - ); - } - } - - return new PathAssert(actual); - } - - /** - * Assert that the file does not exist. - * - * @return this object for further call chaining. - */ - public OptionalPathAssert doesNotExist() { - if (actual != null) { - throw failure( - "Expected path %s to not exist but it was found in location %s as %s", - providedPath, - manager.getLocation().getName(), - actual - ); - } - - return this; - } - - private Collection findSimilarlyNamedPaths() { - var files = new LinkedHashSet(); - for (var root : manager.getRoots()) { - try (var walker = Files.walk(root)) { - walker - .filter(Files::isRegularFile) - .map(root::relativize) - .forEach(files::add); - } catch (IOException ex) { - LOGGER.error( - "Failed to walk file root {} in location {}", - root, - manager.getLocation(), - ex - ); - } - } - - return FuzzySearch - .extractAll(providedPath, files, Path::toString, 5) - .stream() - .filter(result -> result.getScore() > 80) - .map(BoundExtractedResult::getReferent) - .collect(Collectors.toList()); - } - - /** - * Create a new set of assertions for a potential path. - * - * @param manager the location manager to find the path in. - * @param providedPath the provided path we were requested to resolve. - * @param resolvedPath the resolved path, or {@code null} if not found. - * @return the assertions. - */ - public static OptionalPathAssert assertThatPath( - PathLocationManager manager, - String providedPath, - Path resolvedPath - ) { - return new OptionalPathAssert(manager, providedPath, resolvedPath); - } -} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/PathLocationManagerAssert.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/PathLocationManagerAssert.java deleted file mode 100644 index f2414012a..000000000 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/PathLocationManagerAssert.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (C) 2022 Ashley Scopes - * - * 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.assertions; - -import io.github.ascopes.jct.paths.PathLocationManager; -import java.nio.file.Path; -import org.apiguardian.api.API; -import org.apiguardian.api.API.Status; -import org.assertj.core.api.FactoryBasedNavigableIterableAssert; -import org.assertj.core.api.PathAssert; - -/** - * Assertions to perform on files within a {@link PathLocationManager}. - * - * @author Ashley Scopes - * @since 0.0.1 - */ -//@formatter:off -@API(since = "0.0.1", status = Status.EXPERIMENTAL) -public final class PathLocationManagerAssert - extends FactoryBasedNavigableIterableAssert< - PathLocationManagerAssert, - PathLocationManager, - Path, - PathAssert - > { - //@formatter:on - - private PathLocationManagerAssert(PathLocationManager pathLocationManager) { - super(pathLocationManager, PathLocationManagerAssert.class, PathAssert::new); - } - - /** - * Perform an assertion on the first instance of the given file name that exists. - * - *

If no file is found, the assertion will apply to a null value. - * - * @param path the path to look for. - * @return an assertion for the path of the matching file. - */ - public OptionalPathAssert file(String path) { - var actualFile = actual - .findFile(path) - .orElse(null); - - return OptionalPathAssert.assertThatPath(actual, path, actualFile); - } - - /** - * Create a new set of assertions for a path location manager. - * - * @param pathLocationManager the manager of paths to assert on for a specific location. - * @return the assertions. - */ - public static PathLocationManagerAssert assertThatLocation( - PathLocationManager pathLocationManager - ) { - return new PathLocationManagerAssert(pathLocationManager); - } -} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/StackTraceAssert.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/StackTraceAssert.java new file mode 100644 index 000000000..120a47d82 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/StackTraceAssert.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.assertions; + +import java.util.ArrayList; +import java.util.List; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; +import org.assertj.core.api.AbstractListAssert; + +/** + * Assertions for a list of {@link StackTraceElement stack trace frames}. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public final class StackTraceAssert + extends AbstractListAssert, StackTraceElement, StackTraceElementAssert> { + + /** + * Initialize a new assertions object. + * + * @param actual the list of stack trace elements to assert upon. + */ + public StackTraceAssert(List actual) { + super(actual, StackTraceAssert.class); + withRepresentation(StackTraceRepresentation.getInstance()); + } + + @Override + protected StackTraceElementAssert toAssert(StackTraceElement value, String description) { + return new StackTraceElementAssert(value).describedAs(description); + } + + @Override + protected StackTraceAssert newAbstractIterableAssert( + Iterable iterable + ) { + var list = new ArrayList(); + iterable.forEach(list::add); + return new StackTraceAssert(list); + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/StackTraceElementAssert.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/StackTraceElementAssert.java new file mode 100644 index 000000000..98aad57d0 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/StackTraceElementAssert.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.assertions; + +import java.util.OptionalInt; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; +import org.assertj.core.api.AbstractAssert; +import org.assertj.core.api.BooleanAssert; +import org.assertj.core.api.OptionalIntAssert; +import org.assertj.core.api.StringAssert; + +/** + * Assertions to perform on a {@link StackTraceElement stack trace frame}. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(status = Status.EXPERIMENTAL) +public class StackTraceElementAssert + extends AbstractAssert { + + /** + * Initialize this assertion object. + * + * @param actual the stacktrace element to assert upon. + */ + public StackTraceElementAssert(StackTraceElement actual) { + super(actual, StackTraceElementAssert.class); + } + + /** + * Get assertions for the filename of the stack trace frame. + * + * @return the assertions for the file name. + */ + public StringAssert fileName() { + return new StringAssert(actual.getFileName()); + } + + /** + * Get assertions for the line number of the stack trace frame. + * + *

The line number may be empty if the method is a {@link #nativeMethod() native method}. + * + * @return the assertions for the line number. + */ + public OptionalIntAssert lineNumber() { + // Null for irrelevant values is less surprising than a negative value. + return new OptionalIntAssert( + actual.getLineNumber() < 0 ? OptionalInt.empty() : OptionalInt.of(actual.getLineNumber()) + ) {}.describedAs("line number %s", actual.getLineNumber()); + } + + /** + * Get assertions for the module name of the stack trace frame. + * + *

The value may be null if not present. + * + * @return the assertions for the module name. + */ + public StringAssert moduleName() { + return new StringAssert(actual.getModuleName()); + } + + /** + * Get assertions for the module version of the stack trace frame. + * + *

The value may be null if not present. + * + * @return the assertions for the module version. + */ + public StringAssert moduleVersion() { + return new StringAssert(actual.getModuleVersion()); + } + + /** + * Get assertions for the name of the classloader of the class in the stack trace frame. + * + * @return the assertions for the classloader name. + */ + public StringAssert classLoaderName() { + return new StringAssert(actual.getClassLoaderName()); + } + + /** + * Get assertions for the class name of the stack trace frame. + * + * @return the assertions for the class name. + */ + public StringAssert className() { + return new StringAssert(actual.getClassName()); + } + + /** + * Get assertions for the method name of the stack trace frame. + * + * @return the assertions for the method name. + */ + public StringAssert methodName() { + return new StringAssert(actual.getMethodName()); + } + + /** + * Get assertions for whether the frame is for a native (JNI) method or not. + * + * @return the assertions for the method nativity. + */ + public BooleanAssert nativeMethod() { + return new BooleanAssert(actual.isNativeMethod()); + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/StackTraceRepresentation.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/StackTraceRepresentation.java new file mode 100644 index 000000000..fe6fd1a00 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/StackTraceRepresentation.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.assertions; + +import java.util.List; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; +import org.assertj.core.presentation.Representation; + +/** + * Representation of a {@link List list} of {@link StackTraceElement stack trace frames}. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public final class StackTraceRepresentation implements Representation { + + private static final StackTraceRepresentation INSTANCE + = new StackTraceRepresentation(); + + /** + * Get an instance of this stack trace representation. + * + * @return the instance. + */ + public static StackTraceRepresentation getInstance() { + return INSTANCE; + } + + + private StackTraceRepresentation() { + // Nothing to see here, move along now! + } + + @Override + @SuppressWarnings("unchecked") + public String toStringOf(Object object) { + var trace = (List) object; + var builder = new StringBuilder("Stacktrace:"); + for (var frame : trace) { + builder.append("\n\tat ").append(frame); + } + return builder.toString(); + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/AnnotationProcessorDiscovery.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/AnnotationProcessorDiscovery.java new file mode 100644 index 000000000..c0b99ba5e --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/AnnotationProcessorDiscovery.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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; + +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * Mode for annotation processor discovery when no explicit processors are provided. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public enum AnnotationProcessorDiscovery { + /** + * Discovery is enabled, and will also scan any dependencies in the classpath or module path. + */ + INCLUDE_DEPENDENCIES, + + /** + * Discovery is enabled using the provided processor paths. + */ + ENABLED, + + /** + * Discovery is disabled. + */ + DISABLED, +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/Compiler.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/Compilable.java similarity index 54% rename from java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/Compiler.java rename to java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/Compilable.java index 3bd891216..70e5c64aa 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/Compiler.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/Compilable.java @@ -16,14 +16,13 @@ package io.github.ascopes.jct.compilers; -import io.github.ascopes.jct.intern.IterableUtils; -import io.github.ascopes.jct.paths.PathLocationRepository; -import io.github.ascopes.jct.paths.RamPath; +import io.github.ascopes.jct.paths.NioPath; +import io.github.ascopes.jct.paths.PathLike; +import io.github.ascopes.jct.utils.IterableUtils; import java.io.UncheckedIOException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Path; -import java.util.Collection; import java.util.List; import java.util.Locale; import java.util.Optional; @@ -46,7 +45,7 @@ * @since 0.0.1 */ @API(since = "0.0.1", status = Status.EXPERIMENTAL) -public interface Compiler, R extends Compilation> { +public interface Compilable, R extends Compilation> { /** * Default setting for deprecation warnings ({@code true}). @@ -99,14 +98,14 @@ public interface Compiler, R extends Compilation> { boolean DEFAULT_INHERIT_SYSTEM_MODULE_PATH = true; /** - * Default setting for logging file manager operations ({@link Logging#DISABLED}). + * Default setting for logging file manager operations ({@link LoggingMode#DISABLED}). */ - Logging DEFAULT_FILE_MANAGER_LOGGING = Logging.DISABLED; + LoggingMode DEFAULT_FILE_MANAGER_LOGGING_MODE = LoggingMode.DISABLED; /** - * Default setting for logging diagnostics ({@link Logging#ENABLED}). + * Default setting for logging diagnostics ({@link LoggingMode#ENABLED}). */ - Logging DEFAULT_DIAGNOSTIC_LOGGING = Logging.ENABLED; + LoggingMode DEFAULT_DIAGNOSTIC_LOGGING_MODE = LoggingMode.ENABLED; /** * Default setting for how to apply annotation processor discovery when no processors are @@ -115,630 +114,394 @@ public interface Compiler, R extends Compilation> { AnnotationProcessorDiscovery DEFAULT_ANNOTATION_PROCESSOR_DISCOVERY = AnnotationProcessorDiscovery.INCLUDE_DEPENDENCIES; - /** - * Default charset to use for reading and writing files ({@link StandardCharsets#UTF_8}). - */ - Charset DEFAULT_FILE_CHARSET = StandardCharsets.UTF_8; - /** * Default charset to use for compiler logs ({@link StandardCharsets#UTF_8}). */ Charset DEFAULT_LOG_CHARSET = StandardCharsets.UTF_8; /** - * Apply a given configurer to this compiler. + * Apply a given configurer to this compiler that can throw a checked exception. * * @param any exception that may be thrown. * @param configurer the configurer to invoke. * @return this compiler object for further call chaining. * @throws T any exception that may be thrown by the configurer. */ - C configure(CompilerConfigurer configurer) throws T; + C configure(CompilerConfigurer configurer) throws T; /** - * Get the path location repository holding any paths that have been added. + * Add a path-like object to a given location. * - *

This is mutable, and care should be taken if using this interface directly. + *

The location must not be + * {@link Location#isModuleOrientedLocation() module-oriented}. * - * @return the path location repository. - */ - PathLocationRepository getPathLocationRepository(); - - // !!! BUG REGRESSION WARNING FOR THIS API !!!: - // DO NOT REPLACE COLLECTION WITH ITERABLE! THIS WOULD MAKE DIFFERENCES BETWEEN - // PATH AND COLLECTIONS OF PATHS DIFFICULT TO DISTINGUISH, SINCE PATHS ARE THEMSELVES - // ITERABLES OF PATHS! - - /** - * Add paths to the given location. + *

The path can be one of: * - * @param location the location to add paths to. - * @param paths the paths to add. - * @return this compiler object for further call chaining. - */ - C addPaths(Location location, Collection paths); - - /** - * Add paths to the given location. + *

* - * @param location the location to add paths to. - * @param path1 the first path to add. - * @param paths additional paths to add. + * @param location the location to add. + * @param pathLike the path-like object to add. * @return this compiler object for further call chaining. + * @throws IllegalArgumentException if the location is + * {@link Location#isModuleOrientedLocation() module-oriented} or + * an {@link Location#isOutputLocation()} output location}. */ - default C addPaths(Location location, Path path1, Path... paths) { - return addPaths(location, IterableUtils.combineOneOrMore(path1, paths)); - } + C addPath(Location location, PathLike pathLike); /** - * Add paths to the class output path. + * Add a {@link Path NIO Path} object to a given location. * - * @param paths the paths to add. - * @return this compiler object for further call chaining. - */ - default C addClassOutputPaths(Collection paths) { - return addPaths(StandardLocation.CLASS_OUTPUT, paths); - } - - /** - * Add paths to the class output path. + *

The location must not be + * {@link Location#isModuleOrientedLocation() module-oriented}. * - * @param path1 the first path to add. - * @param paths additional paths to add. - * @return this compiler object for further call chaining. - */ - default C addClassOutputPaths(Path path1, Path... paths) { - return addPaths(StandardLocation.CLASS_OUTPUT, path1, paths); - } - - /** - * Add paths to the source output path. + *

The path can be one of: * - * @param paths the paths to add. - * @return this compiler object for further call chaining. - */ - default C addSourceOutputPaths(Collection paths) { - return addPaths(StandardLocation.SOURCE_OUTPUT, paths); - } - - /** - * Add paths to the source output path. + *

    + *
  • A path to a directory containing a package;
  • + *
  • A path to a JAR containing a package or module;
  • + *
  • A path to a WAR containing a package or module; or
  • + *
  • A path to a ZIP containing a package or module.
  • + *
* - * @param path1 the first path to add. - * @param paths additional paths to add. + * @param location the location to add. + * @param path the path to add. * @return this compiler object for further call chaining. + * @throws IllegalArgumentException if the location is not + * {@link Location#isModuleOrientedLocation() module-oriented}. */ - default C addSourceOutputPaths(Path path1, Path... paths) { - return addPaths(StandardLocation.SOURCE_OUTPUT, path1, paths); + default C addPath(Location location, Path path) { + return addPath(location, new NioPath(path)); } /** - * Add paths to the class path. + * Add a path-like object within a module to a given location. * - * @param paths the paths to add. - * @return this compiler object for further call chaining. - */ - default C addClassPaths(Collection paths) { - return addPaths(StandardLocation.CLASS_PATH, paths); - } - - /** - * Add paths to the class path. + *

The location must be + * {@link Location#isModuleOrientedLocation() module-oriented}. * - * @param path1 the first path to add. - * @param paths any additional paths to add. - * @return this compiler object for further call chaining. - */ - default C addClassPaths(Path path1, Path... paths) { - return addPaths(StandardLocation.CLASS_PATH, path1, paths); - } - - /** - * Add paths to the source path. + *

The path can be one of: * - * @param paths the paths to add. - * @return this compiler object for further call chaining. - */ - default C addSourcePaths(Collection paths) { - return addPaths(StandardLocation.SOURCE_PATH, paths); - } - - /** - * Add paths to the source path. + *

    + *
  • A path to a directory containing a package;
  • + *
  • A path to a JAR containing a package or module;
  • + *
  • A path to a WAR containing a package or module; or
  • + *
  • A path to a ZIP containing a package or module.
  • + *
* - * @param path1 the first path to add. - * @param paths any additional paths to add. + * @param location the location to add. + * @param pathLike the path-like object to add. * @return this compiler object for further call chaining. + * @throws IllegalArgumentException if the location is not + * {@link Location#isModuleOrientedLocation() module-oriented}. */ - default C addSourcePaths(Path path1, Path... paths) { - return addPaths(StandardLocation.SOURCE_PATH, path1, paths); - } + C addPath(Location location, String moduleName, PathLike pathLike); /** - * Add paths to the annotation processor path. + * Add a {@link Path NIO Path} object within a module to a given location. * - * @param paths the paths to add. - * @return this compiler object for further call chaining. - */ - default C addAnnotationProcessorPaths(Collection paths) { - return addPaths(StandardLocation.ANNOTATION_PROCESSOR_PATH, paths); - } - - /** - * Add paths to the annotation processor path. + *

The location must be + * {@link Location#isModuleOrientedLocation() module-oriented}. * - * @param path1 the first path to add. - * @param paths any additional paths to add. - * @return this compiler object for further call chaining. - */ - default C addAnnotationProcessorPaths(Path path1, Path... paths) { - return addPaths(StandardLocation.ANNOTATION_PROCESSOR_PATH, path1, paths); - } - - /** - * Add paths to the annotation processor path. + *

The path can be one of: * - * @param paths the paths to add. - * @return this compiler object for further call chaining. - */ - default C addAnnotationProcessorModulePaths(Collection paths) { - return addPaths(StandardLocation.ANNOTATION_PROCESSOR_MODULE_PATH, paths); - } - - /** - * Add paths to the annotation processor module path. + *

    + *
  • A path to a directory containing a package;
  • + *
  • A path to a JAR containing a package or module;
  • + *
  • A path to a WAR containing a package or module; or
  • + *
  • A path to a ZIP containing a package or module.
  • + *
* - * @param path1 the first path to add. - * @param paths any additional paths to add. + * @param location the location to add. + * @param path the path to add. * @return this compiler object for further call chaining. + * @throws IllegalArgumentException if the location is not + * {@link Location#isModuleOrientedLocation() module-oriented}. */ - default C addAnnotationProcessorModulePaths(Path path1, Path... paths) { - return addPaths(StandardLocation.ANNOTATION_PROCESSOR_MODULE_PATH, path1, paths); + default C addPath(Location location, String moduleName, Path path) { + return addPath(location, moduleName, new NioPath(path)); } /** - * Add paths to the platform class path. + * Add a path-like object that contains a package to the class path. * - * @param paths the paths to add. - * @return this compiler object for further call chaining. - */ - default C addPlatformClassPaths(Collection paths) { - return addPaths(StandardLocation.PLATFORM_CLASS_PATH, paths); - } - - /** - * Add paths to the platform class path. + *

The path can be one of: * - * @param path1 the first path to add. - * @param paths any additional paths to add. - * @return this compiler object for further call chaining. - */ - default C addPlatformClassPaths(Path path1, Path... paths) { - return addPaths(StandardLocation.PLATFORM_CLASS_PATH, path1, paths); - } - - /** - * Add paths to the module source path. + *

    + *
  • A path to a directory containing a package;
  • + *
  • A path to a JAR containing a package or module;
  • + *
  • A path to a WAR containing a package or module; or
  • + *
  • A path to a ZIP containing a package or module.
  • + *
* - * @param paths the paths to add. - * @return this compiler object for further call chaining. - */ - default C addModuleSourcePaths(Collection paths) { - return addPaths(StandardLocation.MODULE_SOURCE_PATH, paths); - } - - /** - * Add paths to the module source path. + *

Note: to add modules, consider using + * {@link #addModulePath(String, PathLike)} instead.

* - * @param path1 the first path to add. - * @param paths any additional paths to add. + * @param pathLike the path-like object to add. * @return this compiler object for further call chaining. */ - default C addModuleSourcePaths(Path path1, Path... paths) { - return addPaths(StandardLocation.MODULE_SOURCE_PATH, path1, paths); + default C addClassPath(PathLike pathLike) { + return addPath(StandardLocation.CLASS_PATH, pathLike); } /** - * Add paths to the upgrade module path. + * Add a {@link Path NIO Path} that contains a package to the class path. * - * @param paths the paths to add. - * @return this compiler object for further call chaining. - */ - default C addUpgradeModulePaths(Collection paths) { - return addPaths(StandardLocation.UPGRADE_MODULE_PATH, paths); - } - - /** - * Add paths to the upgrade module path. + *

The path can be one of: * - * @param path1 the first path to add. - * @param paths any additional paths to add. - * @return this compiler object for further call chaining. - */ - default C addUpgradeModulePaths(Path path1, Path... paths) { - return addPaths(StandardLocation.UPGRADE_MODULE_PATH, path1, paths); - } - - /** - * Add paths to the system module path. + *

    + *
  • A path to a directory containing a package;
  • + *
  • A path to a JAR containing a package or module;
  • + *
  • A path to a WAR containing a package or module; or
  • + *
  • A path to a ZIP containing a package or module.
  • + *
* - * @param paths the paths to add. - * @return this compiler object for further call chaining. - */ - default C addSystemModulePaths(Collection paths) { - return addPaths(StandardLocation.SYSTEM_MODULES, paths); - } - - /** - * Add paths to the system module path. + *

Note: to add modules, consider using + * {@link #addModulePath(String, Path)} instead.

* - * @param path1 the first path to add. - * @param paths any additional paths to add. + * @param path the path to add. * @return this compiler object for further call chaining. */ - default C addSystemModulePaths(Path path1, Path... paths) { - return addPaths(StandardLocation.SYSTEM_MODULES, path1, paths); + default C addClassPath(Path path) { + return addPath(StandardLocation.CLASS_PATH, path); } /** - * Add paths to the module path. + * Add a path-like object that contains a module package to the module path. * - * @param paths the paths to add. - * @return this compiler object for further call chaining. - */ - default C addModulePaths(Collection paths) { - return addPaths(StandardLocation.MODULE_PATH, paths); - } - - /** - * Add paths to the module path. + *

The path can be one of: * - * @param path1 the first path to add. - * @param paths any additional paths to add. - * @return this compiler object for further call chaining. - */ - default C addModulePaths(Path path1, Path... paths) { - return addPaths(StandardLocation.MODULE_PATH, path1, paths); - } - - /** - * Add paths to the patch module path. + *

    + *
  • A path to a directory containing a package;
  • + *
  • A path to a JAR containing a package or module;
  • + *
  • A path to a WAR containing a package or module; or
  • + *
  • A path to a ZIP containing a package or module.
  • + *
* - * @param paths the paths to add. - * @return this compiler object for further call chaining. - */ - default C addPatchModulePaths(Collection paths) { - return addPaths(StandardLocation.PATCH_MODULE_PATH, paths); - } - - /** - * Add paths to the patch module path. + *

Note: to add regular packages, consider using + * {@link #addClassPath(PathLike)} instead.

* - * @param path1 the first path to add. - * @param paths any additional paths to add. + * @param moduleName the name of the module. + * @param pathLike the path-like object to add. * @return this compiler object for further call chaining. */ - default C addPatchModulePaths(Path path1, Path... paths) { - return addPaths(StandardLocation.PATCH_MODULE_PATH, path1, paths); + default C addModulePath(String moduleName, PathLike pathLike) { + return addPath(StandardLocation.MODULE_PATH, moduleName, pathLike); } /** - * Add multiple in-memory directories to the paths for a given location. + * Add a {@link Path NIO Path} that contains a module package to the module path. * - *

Note that this will take ownership of the path in the underlying file repository. + *

The path can be one of: * - * @param location the location to add. - * @param paths the in-memory directories to add. - * @return this compiler object for further call chaining. - */ - C addRamPaths(Location location, Collection paths); - - /** - * Add multiple in-memory directories to the paths for a given location. + *

    + *
  • A path to a directory containing a package;
  • + *
  • A path to a JAR containing a package or module;
  • + *
  • A path to a WAR containing a package or module; or
  • + *
  • A path to a ZIP containing a package or module.
  • + *
* - *

Note that this will take ownership of the path in the underlying file repository. + *

Note: to add regular packages, consider using + * {@link #addClassPath(Path)} instead.

* - * @param location the location to add. - * @param path1 the first in-memory directory to add. - * @param paths additional in-memory directories to add. + * @param moduleName the name of the module. + * @param path the path to add. * @return this compiler object for further call chaining. */ - default C addRamPaths(Location location, RamPath path1, RamPath... paths) { - return addRamPaths(location, IterableUtils.combineOneOrMore(path1, paths)); + default C addModulePath(String moduleName, Path path) { + return addPath(StandardLocation.MODULE_PATH, moduleName, path); } /** - * Add paths to the class output path. + * Add a path-like object that contains a package to the source path. * - * @param paths the paths to add. - * @return this compiler object for further call chaining. - */ - default C addClassOutputRamPaths(Collection paths) { - return addRamPaths(StandardLocation.CLASS_OUTPUT, paths); - } - - /** - * Add paths to the class output path. + *

Anything placed in here will be treated as a compilation unit by default. * - * @param path1 the first path to add. - * @param paths additional paths to add. - * @return this compiler object for further call chaining. - */ - default C addClassOutputRamPaths(RamPath path1, RamPath... paths) { - return addRamPaths(StandardLocation.CLASS_OUTPUT, path1, paths); - } - - /** - * Add paths to the source output path. + *

The path can be one of: * - * @param paths the paths to add. - * @return this compiler object for further call chaining. - */ - default C addSourceOutputRamPaths(Collection paths) { - return addRamPaths(StandardLocation.SOURCE_OUTPUT, paths); - } - - /** - * Add paths to the source output path. + *

    + *
  • A path to a directory containing a package;
  • + *
  • A path to a JAR containing a package or module;
  • + *
  • A path to a WAR containing a package or module; or
  • + *
  • A path to a ZIP containing a package or module.
  • + *
* - * @param path1 the first path to add. - * @param paths additional paths to add. - * @return this compiler object for further call chaining. - */ - default C addSourceOutputRamPaths(RamPath path1, RamPath... paths) { - return addRamPaths(StandardLocation.SOURCE_OUTPUT, path1, paths); - } - - /** - * Add paths to the class path. + *

Note: to add modules, consider using + * {@link #addModuleSourcePath(String, PathLike)} instead. You will not be able to mix this + * method and that method together.

* - * @param paths the paths to add. + * @param pathLike the path-like object to add. * @return this compiler object for further call chaining. */ - default C addClassRamPaths(Collection paths) { - return addRamPaths(StandardLocation.CLASS_PATH, paths); + default C addSourcePath(PathLike pathLike) { + return addPath(StandardLocation.SOURCE_PATH, pathLike); } /** - * Add paths to the class path. + * Add a {@link Path NIO Path} that contains a package to the class path. * - * @param path1 the first path to add. - * @param paths any additional paths to add. - * @return this compiler object for further call chaining. - */ - default C addClassRamPaths(RamPath path1, RamPath... paths) { - return addRamPaths(StandardLocation.CLASS_PATH, path1, paths); - } - - /** - * Add paths to the source path. + *

Anything placed in here will be treated as a compilation unit by default. * - * @param paths the paths to add. - * @return this compiler object for further call chaining. - */ - default C addSourceRamPaths(Collection paths) { - return addRamPaths(StandardLocation.SOURCE_PATH, paths); - } - - /** - * Add paths to the source path. + *

The path can be one of: * - * @param path1 the first path to add. - * @param paths any additional paths to add. - * @return this compiler object for further call chaining. - */ - default C addSourceRamPaths(RamPath path1, RamPath... paths) { - return addRamPaths(StandardLocation.SOURCE_PATH, path1, paths); - } - - /** - * Add paths to the annotation processor path. + *

    + *
  • A path to a directory containing a package;
  • + *
  • A path to a JAR containing a package or module;
  • + *
  • A path to a WAR containing a package or module; or
  • + *
  • A path to a ZIP containing a package or module.
  • + *
* - * @param paths the paths to add. - * @return this compiler object for further call chaining. - */ - default C addAnnotationProcessorRamPaths(Collection paths) { - return addRamPaths(StandardLocation.ANNOTATION_PROCESSOR_PATH, paths); - } - - /** - * Add paths to the annotation processor path. + *

Note: to add modules, consider using + * {@link #addModuleSourcePath(String, PathLike)} instead. You will not be able to mix this + * method and that method together.

* - * @param path1 the first path to add. - * @param paths any additional paths to add. + * @param path the path to add. * @return this compiler object for further call chaining. */ - default C addAnnotationProcessorRamPaths(RamPath path1, RamPath... paths) { - return addRamPaths(StandardLocation.ANNOTATION_PROCESSOR_PATH, path1, paths); + default C addSourcePath(Path path) { + return addPath(StandardLocation.SOURCE_PATH, path); } /** - * Add paths to the annotation processor module path. + * Add a path-like object that contains a module package to the source path. * - * @param paths the paths to add. - * @return this compiler object for further call chaining. - */ - default C addAnnotationProcessorModuleRamPaths(Collection paths) { - return addRamPaths(StandardLocation.ANNOTATION_PROCESSOR_MODULE_PATH, paths); - } - - /** - * Add paths to the annotation processor module path. + *

Anything placed in here will be treated as a compilation unit by default. * - * @param path1 the first path to add. - * @param paths any additional paths to add. - * @return this compiler object for further call chaining. - */ - default C addAnnotationProcessorModuleRamPaths(RamPath path1, RamPath... paths) { - return addRamPaths(StandardLocation.ANNOTATION_PROCESSOR_MODULE_PATH, path1, paths); - } - - /** - * Add paths to the platform class path. + *

The path can be one of: * - * @param paths the paths to add. - * @return this compiler object for further call chaining. - */ - default C addPlatformClassRamPaths(Collection paths) { - return addRamPaths(StandardLocation.PLATFORM_CLASS_PATH, paths); - } - - /** - * Add paths to the platform class path. + *

    + *
  • A path to a directory containing a package;
  • + *
  • A path to a JAR containing a package or module;
  • + *
  • A path to a WAR containing a package or module; or
  • + *
  • A path to a ZIP containing a package or module.
  • + *
* - * @param path1 the first path to add. - * @param paths any additional paths to add. - * @return this compiler object for further call chaining. - */ - default C addPlatformClassRamPaths(RamPath path1, RamPath... paths) { - return addRamPaths(StandardLocation.PLATFORM_CLASS_PATH, path1, paths); - } - - /** - * Add paths to the native header output path. + *

Note: to add non-modules, consider using + * {@link #addSourcePath(PathLike)} instead. You will not be able to mix this + * method and that method together.

* - * @param paths the paths to add. + * @param moduleName the name of the module to add. + * @param pathLike the path-like object to add. * @return this compiler object for further call chaining. */ - default C addNativeHeaderOutputPaths(Collection paths) { - return addPaths(StandardLocation.NATIVE_HEADER_OUTPUT, paths); + default C addModuleSourcePath(String moduleName, PathLike pathLike) { + return addPath(StandardLocation.MODULE_SOURCE_PATH, moduleName, pathLike); } /** - * Add paths to the native header output path. + * Add a {@link Path NIO Path} that contains a module package to the class path. * - * @param path1 the first path to add. - * @param paths additional paths to add. - * @return this compiler object for further call chaining. - */ - default C addNativeHeaderOutputPaths(Path path1, Path... paths) { - return addPaths(StandardLocation.NATIVE_HEADER_OUTPUT, path1, paths); - } - - /** - * Add paths to the native header output path. + *

Anything placed in here will be treated as a compilation unit by default. * - * @param paths the paths to add. - * @return this compiler object for further call chaining. - */ - default C addNativeHeaderOutputRamPaths(Collection paths) { - return addRamPaths(StandardLocation.NATIVE_HEADER_OUTPUT, paths); - } - - /** - * Add paths to the native header output path. + *

The path can be one of: * - * @param path1 the first path to add. - * @param paths additional paths to add. - * @return this compiler object for further call chaining. - */ - default C addNativeHeaderOutputRamPaths(RamPath path1, RamPath... paths) { - return addRamPaths(StandardLocation.NATIVE_HEADER_OUTPUT, path1, paths); - } - - /** - * Add paths to the module source path. + *

    + *
  • A path to a directory containing a package;
  • + *
  • A path to a JAR containing a package or module;
  • + *
  • A path to a WAR containing a package or module; or
  • + *
  • A path to a ZIP containing a package or module.
  • + *
* - * @param paths the paths to add. - * @return this compiler object for further call chaining. - */ - default C addModuleSourceRamPaths(Collection paths) { - return addRamPaths(StandardLocation.MODULE_SOURCE_PATH, paths); - } - - /** - * Add paths to the module source path. + *

Note: to add non-modules, consider using + * {@link #addSourcePath(Path)} instead. You will not be able to mix this + * method and that method together.

* - * @param path1 the first path to add. - * @param paths any additional paths to add. + * @param moduleName the name of the module to add. + * @param path the path to add. * @return this compiler object for further call chaining. */ - default C addModuleSourceRamPaths(RamPath path1, RamPath... paths) { - return addRamPaths(StandardLocation.MODULE_SOURCE_PATH, path1, paths); + default C addModuleSourcePath(String moduleName, Path path) { + return addPath(StandardLocation.MODULE_SOURCE_PATH, moduleName, path); } /** - * Add paths to the upgrade module path. + * Add a path-like object that contains a package to compiled annotation processors. * - * @param paths the paths to add. - * @return this compiler object for further call chaining. - */ - default C addUpgradeModuleRamPaths(Collection paths) { - return addRamPaths(StandardLocation.UPGRADE_MODULE_PATH, paths); - } - - /** - * Add paths to the upgrade module path. + *

This will be used for annotation processor discovery. * - * @param path1 the first path to add. - * @param paths any additional paths to add. - * @return this compiler object for further call chaining. - */ - default C addUpgradeModuleRamPaths(RamPath path1, RamPath... paths) { - return addRamPaths(StandardLocation.UPGRADE_MODULE_PATH, path1, paths); - } - - /** - * Add paths to the system module path. + *

The path can be one of: * - * @param paths the paths to add. - * @return this compiler object for further call chaining. - */ - default C addSystemModuleRamPaths(Collection paths) { - return addRamPaths(StandardLocation.SYSTEM_MODULES, paths); - } - - /** - * Add paths to the system module path. + *

    + *
  • A path to a directory containing a package;
  • + *
  • A path to a JAR containing a package or module;
  • + *
  • A path to a WAR containing a package or module; or
  • + *
  • A path to a ZIP containing a package or module.
  • + *
* - * @param path1 the first path to add. - * @param paths any additional paths to add. + * @param pathLike the path-like object to add. * @return this compiler object for further call chaining. */ - default C addSystemModuleRamPaths(RamPath path1, RamPath... paths) { - return addRamPaths(StandardLocation.SYSTEM_MODULES, path1, paths); + default C addAnnotationProcessorPath(PathLike pathLike) { + return addPath(StandardLocation.ANNOTATION_PROCESSOR_PATH, pathLike); } /** - * Add paths to the module path. + * Add a {@link Path NIO Path} that contains a package to compiled annotation processors. * - * @param paths the paths to add. - * @return this compiler object for further call chaining. - */ - default C addModuleRamPaths(Collection paths) { - return addRamPaths(StandardLocation.MODULE_PATH, paths); - } - - /** - * Add paths to the module path. + *

This will be used for annotation processor discovery. + * + *

The path can be one of: * - * @param path1 the first path to add. - * @param paths any additional paths to add. + *

    + *
  • A path to a directory containing a package;
  • + *
  • A path to a JAR containing a package or module;
  • + *
  • A path to a WAR containing a package or module; or
  • + *
  • A path to a ZIP containing a package or module.
  • + *
+ * + * @param path the path to add. * @return this compiler object for further call chaining. */ - default C addModuleRamPaths(RamPath path1, RamPath... paths) { - return addRamPaths(StandardLocation.MODULE_PATH, path1, paths); + default C addAnnotationProcessorPath(Path path) { + return addPath(StandardLocation.ANNOTATION_PROCESSOR_PATH, path); } /** - * Add paths to the patch module path. + * Add a path-like object that contains a module package to compiled annotation processors. + * + *

This will be used for annotation processor discovery. + * + *

The path can be one of: + * + *

    + *
  • A path to a directory containing a package;
  • + *
  • A path to a JAR containing a package or module;
  • + *
  • A path to a WAR containing a package or module; or
  • + *
  • A path to a ZIP containing a package or module.
  • + *
* - * @param paths the paths to add. + * @param moduleName the name of the module. + * @param pathLike the path-like object to add. * @return this compiler object for further call chaining. */ - default C addPatchModuleRamPaths(Collection paths) { - return addRamPaths(StandardLocation.PATCH_MODULE_PATH, paths); + default C addAnnotationProcessorModulePath(String moduleName, PathLike pathLike) { + return addPath(StandardLocation.ANNOTATION_PROCESSOR_MODULE_PATH, moduleName, pathLike); } /** - * Add paths to the patch module path. + * Add a {@link Path NIO Path} that contains a module package to compiled annotation processors. * - * @param path1 the first path to add. - * @param paths any additional paths to add. + *

This will be used for annotation processor discovery. + * + *

The path can be one of: + * + *

    + *
  • A path to a directory containing a package;
  • + *
  • A path to a JAR containing a package or module;
  • + *
  • A path to a WAR containing a package or module; or
  • + *
  • A path to a ZIP containing a package or module.
  • + *
+ * + * @param moduleName the name of the module. + * @param path the path to add. * @return this compiler object for further call chaining. */ - default C addPatchModuleRamPaths(RamPath path1, RamPath... paths) { - return addRamPaths(StandardLocation.PATCH_MODULE_PATH, path1, paths); + default C addAnnotationProcessorModulePath(String moduleName, Path path) { + return addPath(StandardLocation.ANNOTATION_PROCESSOR_MODULE_PATH, moduleName, path); } /** @@ -786,7 +549,7 @@ default C addAnnotationProcessorOptions( * Add annotation processors to invoke. * *

This bypasses the discovery process of annotation processors provided in - * {@link #addAnnotationProcessorPaths}. + * {@link #addAnnotationProcessorPath}. * * @param annotationProcessors the processors to invoke. * @return this compiler object for further call chaining. @@ -797,7 +560,7 @@ default C addAnnotationProcessorOptions( * Add annotation processors to invoke. * *

This bypasses the discovery process of annotation processors provided in - * {@link #addAnnotationProcessorPaths}. + * {@link #addAnnotationProcessorPath}. * * @param annotationProcessor the first processor to invoke. * @param annotationProcessors additional processors to invoke. @@ -864,27 +627,6 @@ default C addRuntimeOptions(String runtimeOption, String... runtimeOptions) { return addRuntimeOptions(IterableUtils.combineOneOrMore(runtimeOption, runtimeOptions)); } - /** - * Get the charset being used to read and write files with. - * - *

Unless otherwise changed or specified, implementations should default to - * {@link #DEFAULT_LOG_CHARSET}. - * - * @return the charset. - */ - Charset getFileCharset(); - - /** - * Set the charset being used to read and write files with. - * - *

Unless otherwise changed or specified, implementations should default to - * {@link #DEFAULT_LOG_CHARSET}. - * - * @param fileCharset the charset to use. - * @return this compiler for further call chaining. - */ - C fileCharset(Charset fileCharset); - /** * Determine whether verbose logging is enabled or not. * @@ -1003,6 +745,15 @@ default C addRuntimeOptions(String runtimeOption, String... runtimeOptions) { */ C failOnWarnings(boolean enabled); + /** + * Get the default release to use if no release or target version is specified. + * + *

This can not be configured. + * + * @return the default release version to use. + */ + String getDefaultRelease(); + /** * Get the current release version that is set, or an empty optional if left to the compiler * default. @@ -1314,43 +1065,43 @@ default C target(SourceVersion target) { * Get the current file manager logging mode. * *

Unless otherwise changed or specified, implementations should default to - * {@link #DEFAULT_FILE_MANAGER_LOGGING}. + * {@link #DEFAULT_FILE_MANAGER_LOGGING_MODE}. * * @return the current file manager logging mode. */ - Logging getFileManagerLogging(); + LoggingMode getFileManagerLoggingMode(); /** * Set how to handle logging calls to underlying file managers. * *

Unless otherwise changed or specified, implementations should default to - * {@link #DEFAULT_FILE_MANAGER_LOGGING}. + * {@link #DEFAULT_FILE_MANAGER_LOGGING_MODE}. * - * @param fileManagerLogging the mode to use for file manager logging. + * @param fileManagerLoggingMode the mode to use for file manager logging. * @return this compiler for further call chaining. */ - C fileManagerLogging(Logging fileManagerLogging); + C fileManagerLoggingMode(LoggingMode fileManagerLoggingMode); /** * Get the current diagnostic logging mode. * *

Unless otherwise changed or specified, implementations should default to - * {@link #DEFAULT_DIAGNOSTIC_LOGGING}. + * {@link #DEFAULT_DIAGNOSTIC_LOGGING_MODE}. * * @return the current diagnostic logging mode. */ - Logging getDiagnosticLogging(); + LoggingMode getDiagnosticLoggingMode(); /** * Set how to handle diagnostic capture. * *

Unless otherwise changed or specified, implementations should default to - * {@link #DEFAULT_DIAGNOSTIC_LOGGING}. + * {@link #DEFAULT_DIAGNOSTIC_LOGGING_MODE}. * - * @param diagnosticLogging the mode to use for diagnostic capture. + * @param diagnosticLoggingMode the mode to use for diagnostic capture. * @return this compiler for further call chaining. */ - C diagnosticLogging(Logging diagnosticLogging); + C diagnosticLoggingMode(LoggingMode diagnosticLoggingMode); /** * Get how to perform annotation processor discovery. @@ -1394,74 +1145,4 @@ default C target(SourceVersion target) { */ R compile(); - /** - * Options for how to handle logging on special internal components. - * - * @author Ashley Scopes - * @since 0.0.1 - */ - @API(since = "0.0.1", status = Status.EXPERIMENTAL) - enum Logging { - /** - * Enable basic logging. - */ - ENABLED, - - /** - * Enable logging and include stacktraces in the logs for each entry. - */ - STACKTRACES, - - /** - * Do not log anything. - */ - DISABLED, - } - - /** - * Mode for annotation processor discovery when no explicit processors are provided. - * - * @author Ashley Scopes - * @since 0.0.1 - */ - @API(since = "0.0.1", status = Status.EXPERIMENTAL) - enum AnnotationProcessorDiscovery { - /** - * Discovery is enabled, and will also scan any dependencies in the classpath or module path. - */ - INCLUDE_DEPENDENCIES, - - /** - * Discovery is enabled using the provided processor paths. - */ - ENABLED, - - /** - * Discovery is disabled. - */ - DISABLED, - } - - /** - * Function representing a configuration operation that can be applied to a compiler. - * - *

This can allow encapsulating common configuration logic across tests into a single place. - * - * @param the compiler type. - * @param the exception that may be thrown by the configurer. - * @author Ashley Scopes - * @since 0.0.1 - */ - @API(since = "0.0.1", status = Status.EXPERIMENTAL) - @FunctionalInterface - interface CompilerConfigurer, T extends Exception> { - - /** - * Apply configuration logic to the given compiler. - * - * @param compiler the compiler. - * @throws T any exception that may be thrown by the configurer. - */ - void configure(C compiler) throws T; - } } diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/Compilation.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/Compilation.java index 985f545c4..5967e4796 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/Compilation.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/Compilation.java @@ -16,7 +16,8 @@ package io.github.ascopes.jct.compilers; -import io.github.ascopes.jct.paths.PathLocationRepository; +import io.github.ascopes.jct.jsr199.FileManager; +import io.github.ascopes.jct.jsr199.diagnostics.TraceDiagnostic; import java.util.List; import java.util.Set; import javax.tools.JavaFileObject; @@ -79,9 +80,9 @@ default boolean isFailure() { List> getDiagnostics(); /** - * Get the location repository that was used to store files. + * Get the file manager that was used to store and manage files. * - * @return the location repository. + * @return the file manager. */ - PathLocationRepository getPathLocationRepository(); + FileManager getFileManager(); } diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/CompilerConfigurer.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/CompilerConfigurer.java new file mode 100644 index 000000000..f25b6bad4 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/CompilerConfigurer.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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; + +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * Function representing a configuration operation that can be applied to a compiler. + * + *

This can allow encapsulating common configuration logic across tests into a single place. + * + * @param the compiler type. + * @param the exception that may be thrown by the configurer. + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +@FunctionalInterface +public interface CompilerConfigurer, T extends Exception> { + + /** + * Apply configuration logic to the given compiler. + * + * @param compiler the compiler. + * @throws T any exception that may be thrown by the configurer. + */ + void configure(C compiler) throws T; +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/Compilers.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/Compilers.java deleted file mode 100644 index c497378e1..000000000 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/Compilers.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2022 Ashley Scopes - * - * 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; - -import io.github.ascopes.jct.compilers.ecj.EcjCompiler; -import io.github.ascopes.jct.compilers.javac.JavacCompiler; -import javax.tools.ToolProvider; -import org.apiguardian.api.API; -import org.apiguardian.api.API.Status; -import org.eclipse.jdt.internal.compiler.tool.EclipseCompiler; - -/** - * Utility class that allows initialization of several common types of compiler. - * - * @author Ashley Scopes - * @since 0.0.1 - */ -@API(since = "0.0.1", status = Status.EXPERIMENTAL) -public final class Compilers { - - private Compilers() { - throw new UnsupportedOperationException("static-only class"); - } - - /** - * Create an instance of the JDK-provided compiler. - * - * @return the JDK-provided compiler instance. - */ - public static Compiler javac() { - return new JavacCompiler(ToolProvider.getSystemJavaCompiler()); - } - - /** - * Create an instance of the Eclipse Compiler for Java. - * - *

This is bundled with this toolkit. - * - *

Note: the ECJ implementation does not currently work correctly with - * JPMS modules. - * - * @return the ECJ instance. - */ - public static Compiler ecj() { - return new EcjCompiler(new EclipseCompiler()); - } -} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/LoggingMode.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/LoggingMode.java new file mode 100644 index 000000000..4ef19bbf3 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/LoggingMode.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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; + +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * Options for how to handle logging on special internal components. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public enum LoggingMode { + /** + * Enable basic logging. + */ + ENABLED, + + /** + * Enable logging and include stacktraces in the logs for each entry. + */ + STACKTRACES, + + /** + * Do not log anything. + */ + DISABLED, +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/SimpleCompilation.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/SimpleCompilation.java index 0f2747783..c2f50f516 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/SimpleCompilation.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/SimpleCompilation.java @@ -16,11 +16,12 @@ package io.github.ascopes.jct.compilers; -import static io.github.ascopes.jct.intern.IterableUtils.nonNullUnmodifiableList; -import static io.github.ascopes.jct.intern.IterableUtils.nonNullUnmodifiableSet; +import static io.github.ascopes.jct.utils.IterableUtils.nonNullUnmodifiableList; +import static io.github.ascopes.jct.utils.IterableUtils.nonNullUnmodifiableSet; import static java.util.Objects.requireNonNull; -import io.github.ascopes.jct.paths.PathLocationRepository; +import io.github.ascopes.jct.jsr199.FileManager; +import io.github.ascopes.jct.jsr199.diagnostics.TraceDiagnostic; import java.util.List; import java.util.Set; import javax.tools.JavaFileObject; @@ -42,7 +43,7 @@ public final class SimpleCompilation implements Compilation { private final List outputLines; private final Set compilationUnits; private final List> diagnostics; - private final PathLocationRepository pathLocationRepository; + private final FileManager fileManager; private SimpleCompilation(Builder builder) { success = requireNonNull(builder.success, "success"); @@ -50,10 +51,7 @@ private SimpleCompilation(Builder builder) { outputLines = nonNullUnmodifiableList(builder.outputLines, "outputLines"); compilationUnits = nonNullUnmodifiableSet(builder.compilationUnits, "compilationUnits"); diagnostics = nonNullUnmodifiableList(builder.diagnostics, "diagnostics"); - pathLocationRepository = requireNonNull( - builder.pathLocationRepository, - "pathLocationRepository" - ); + fileManager = requireNonNull(builder.fileManager, "fileManager"); } @Override @@ -82,8 +80,8 @@ public List> getDiagnostics( } @Override - public PathLocationRepository getPathLocationRepository() { - return pathLocationRepository; + public FileManager getFileManager() { + return fileManager; } /** @@ -109,7 +107,7 @@ public static final class Builder { private List outputLines; private Set compilationUnits; private List> diagnostics; - private PathLocationRepository pathLocationRepository; + private FileManager fileManager; private Builder() { // Only initialized in this file. @@ -173,16 +171,13 @@ public Builder diagnostics( } /** - * Set the file repository. + * Set the file manager. * - * @param pathLocationRepository the file repository. + * @param fileManager the file manager. * @return this builder. */ - public Builder pathLocationRepository(PathLocationRepository pathLocationRepository) { - this.pathLocationRepository = requireNonNull( - pathLocationRepository, - "pathLocationRepository" - ); + public Builder fileManager(FileManager fileManager) { + this.fileManager = requireNonNull(fileManager, "fileManager"); return this; } diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/SimpleCompilationFactory.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/SimpleCompilationFactory.java index 4883fbf02..2b969d45e 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/SimpleCompilationFactory.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/SimpleCompilationFactory.java @@ -16,12 +16,14 @@ package io.github.ascopes.jct.compilers; -import io.github.ascopes.jct.compilers.Compiler.Logging; -import io.github.ascopes.jct.intern.SpecialLocations; -import io.github.ascopes.jct.intern.StringUtils; -import io.github.ascopes.jct.paths.LoggingJavaFileManagerProxy; -import io.github.ascopes.jct.paths.PathJavaFileManager; +import io.github.ascopes.jct.jsr199.FileManager; +import io.github.ascopes.jct.jsr199.LoggingFileManagerProxy; +import io.github.ascopes.jct.jsr199.diagnostics.TeeWriter; +import io.github.ascopes.jct.jsr199.diagnostics.TracingDiagnosticListener; +import io.github.ascopes.jct.paths.NioPath; import io.github.ascopes.jct.paths.RamPath; +import io.github.ascopes.jct.utils.SpecialLocations; +import io.github.ascopes.jct.utils.StringUtils; import java.io.IOException; import java.io.Writer; import java.util.ArrayList; @@ -51,7 +53,7 @@ * @since 0.0.1 */ @API(since = "0.0.1", status = Status.EXPERIMENTAL) -public class SimpleCompilationFactory> { +public class SimpleCompilationFactory> { private static final Logger LOGGER = LoggerFactory.getLogger(SimpleCompilationFactory.class); @@ -59,12 +61,14 @@ public class SimpleCompilationFactory> * Run the compilation for the given compiler and return the compilation result. * * @param compiler the compiler to run. + * @param template the template to compile files within. * @param jsr199Compiler the underlying JSR-199 compiler to run via. * @param flagBuilder the flag builder to use. * @return the compilation result. */ public SimpleCompilation compile( A compiler, + SimpleFileManagerTemplate template, JavaCompiler jsr199Compiler, FlagBuilder flagBuilder ) { @@ -73,7 +77,7 @@ public SimpleCompilation compile( var diagnosticListener = buildDiagnosticListener(compiler); var writer = buildWriter(compiler); - try (var fileManager = buildJavaFileManager(compiler)) { + try (var fileManager = buildFileManager(compiler, template)) { var compilationUnits = findCompilationUnits(fileManager); LOGGER.debug("Found {} compilation units {}", compilationUnits.size(), compilationUnits); @@ -97,7 +101,7 @@ public SimpleCompilation compile( .outputLines(outputLines) .compilationUnits(Set.copyOf(compilationUnits)) .diagnostics(diagnosticListener.getDiagnostics()) - .pathLocationRepository(compiler.getPathLocationRepository()) + .fileManager(fileManager) .build(); } } catch (IOException ex) { @@ -141,19 +145,25 @@ protected List buildFlags(A compiler, FlagBuilder flagBuilder) { /** * Build the {@link JavaFileManager} to use. * - *

Logging will be applied to this via - * {@link #applyLoggingToFileManager(Compiler, JavaFileManager)}, which will be handled by - * {@link #compile(Compiler, JavaCompiler, FlagBuilder)}. + *

LoggingMode will be applied to this via + * {@link #applyLoggingToFileManager(Compilable, FileManager)}, which will be handled by + * {@link #compile(Compilable, SimpleFileManagerTemplate, JavaCompiler, FlagBuilder)}. * * @param compiler the compiler to use. * @return the file manager to use. */ - protected JavaFileManager buildJavaFileManager(A compiler) { - ensureClassOutputPathExists(compiler); - registerClassPath(compiler); - registerPlatformClassPath(compiler); - registerSystemModulePath(compiler); - return new PathJavaFileManager(compiler.getPathLocationRepository()); + protected FileManager buildFileManager(A compiler, SimpleFileManagerTemplate template) { + var release = compiler.getRelease() + .or(compiler::getTarget) + .orElseGet(compiler::getDefaultRelease); + + var fileManager = template.createFileManager(release); + ensureClassOutputPathExists(fileManager); + registerClassPath(compiler, fileManager); + registerPlatformClassPath(compiler, fileManager); + registerSystemModulePath(compiler, fileManager); + registerAnnotationProcessorPaths(compiler, fileManager); + return fileManager; } /** @@ -190,26 +200,26 @@ protected List findCompilationUnits( /** * Apply the logging level to the file manager provided by - * {@link #buildJavaFileManager(Compiler)}. + * {@link #buildFileManager(Compilable, SimpleFileManagerTemplate)}. * *

The default implementation will wrap the given {@link JavaFileManager} in a - * {@link LoggingJavaFileManagerProxy} if the {@link Compiler#getFileManagerLogging()} field is - * not set to {@link Logging#DISABLED}. In the latter scenario, the input + * {@link LoggingFileManagerProxy} if the {@link Compilable#getFileManagerLoggingMode()} field is + * not set to {@link LoggingMode#DISABLED}. In the latter scenario, the input * will be returned to the caller with no other modifications. * * @param compiler the compiler to use. * @param fileManager the file manager to apply to. * @return the file manager to use for future operations. */ - protected JavaFileManager applyLoggingToFileManager( + protected FileManager applyLoggingToFileManager( A compiler, - JavaFileManager fileManager + FileManager fileManager ) { - switch (compiler.getFileManagerLogging()) { + switch (compiler.getFileManagerLoggingMode()) { case STACKTRACES: - return LoggingJavaFileManagerProxy.wrap(fileManager, true); + return LoggingFileManagerProxy.wrap(fileManager, true); case ENABLED: - return LoggingJavaFileManagerProxy.wrap(fileManager, false); + return LoggingFileManagerProxy.wrap(fileManager, false); default: return fileManager; } @@ -224,11 +234,11 @@ protected JavaFileManager applyLoggingToFileManager( * @return the diagnostics listener. */ protected TracingDiagnosticListener buildDiagnosticListener(A compiler) { - var logging = compiler.getDiagnosticLogging(); + var logging = compiler.getDiagnosticLoggingMode(); return new TracingDiagnosticListener<>( - logging != Logging.DISABLED, - logging == Logging.STACKTRACES + logging != LoggingMode.DISABLED, + logging == LoggingMode.STACKTRACES ); } @@ -248,7 +258,7 @@ protected CompilationTask buildCompilationTask( A compiler, JavaCompiler jsr199Compiler, Writer writer, - JavaFileManager fileManager, + FileManager fileManager, DiagnosticListener diagnosticListener, List flags, List compilationUnits @@ -264,7 +274,7 @@ protected CompilationTask buildCompilationTask( compilationUnits ); - configureAnnotationProcessors(compiler, task); + configureAnnotationProcessorDiscovery(compiler, task); task.setLocale(compiler.getLocale()); @@ -323,30 +333,21 @@ protected boolean runCompilationTask(A compiler, CompilationTask task) { } } - private void ensureClassOutputPathExists(A compiler) { + private void ensureClassOutputPathExists(FileManager fileManager) { // We have to manually create this one as javac will not attempt to access it lazily. Instead, // it will just abort if it is not present. This means we cannot take advantage of the // PathLocationRepository creating the roots as we try to access them for this specific case. - var classOutputManager = compiler - .getPathLocationRepository() - .getOrCreateManager(StandardLocation.CLASS_OUTPUT); - - // Ensure we have somewhere to dump our output. - if (classOutputManager.isEmpty()) { + if (!fileManager.hasLocation(StandardLocation.CLASS_OUTPUT)) { LOGGER.debug("No class output location was specified, so an in-memory path is being created"); var classOutput = RamPath.createPath("classes-" + UUID.randomUUID(), true); - classOutputManager.addRamPath(classOutput); + fileManager.addPath(StandardLocation.CLASS_OUTPUT, classOutput); } else { LOGGER.trace("At least one output path is present, so no in-memory path will be created"); } } - private void registerClassPath(A compiler) { + private void registerClassPath(A compiler, FileManager fileManager) { // ECJ requires that we always create this, otherwise it refuses to run. - var classPath = compiler - .getPathLocationRepository() - .getOrCreateManager(StandardLocation.CLASS_PATH); - if (!compiler.isInheritClassPath()) { return; } @@ -354,7 +355,9 @@ private void registerClassPath(A compiler) { var currentClassPath = SpecialLocations.currentClassPathLocations(); LOGGER.debug("Adding current classpath to compiler: {}", currentClassPath); - classPath.addPaths(currentClassPath); + for (var classPath : currentClassPath) { + fileManager.addPath(StandardLocation.CLASS_PATH, new NioPath(classPath)); + } var currentModulePath = SpecialLocations.currentModulePathLocations(); @@ -369,15 +372,14 @@ private void registerClassPath(A compiler) { // all dependencies being loaded, but not the code the user is actually trying to test. // // Weird, but it is what it is, I guess. - classPath.addPaths(currentModulePath); - - compiler - .getPathLocationRepository() - .getOrCreateManager(StandardLocation.MODULE_PATH) - .addPaths(currentModulePath); + for (var modulePath : currentModulePath) { + var modulePathLike = new NioPath(modulePath); + fileManager.addPath(StandardLocation.CLASS_PATH, modulePathLike); + fileManager.addPath(StandardLocation.MODULE_PATH, modulePathLike); + } } - private void registerPlatformClassPath(A compiler) { + private void registerPlatformClassPath(A compiler, FileManager fileManager) { if (!compiler.isInheritPlatformClassPath()) { return; } @@ -387,13 +389,13 @@ private void registerPlatformClassPath(A compiler) { if (!currentPlatformClassPath.isEmpty()) { LOGGER.debug("Adding current platform classpath to compiler: {}", currentPlatformClassPath); - compiler.getPathLocationRepository() - .getOrCreateManager(StandardLocation.PLATFORM_CLASS_PATH) - .addPaths(currentPlatformClassPath); + for (var classPath : currentPlatformClassPath) { + fileManager.addPath(StandardLocation.PLATFORM_CLASS_PATH, new NioPath(classPath)); + } } } - private void registerSystemModulePath(A compiler) { + private void registerSystemModulePath(A compiler, FileManager fileManager) { if (!compiler.isInheritSystemModulePath()) { return; } @@ -401,54 +403,46 @@ private void registerSystemModulePath(A compiler) { var jrtLocations = SpecialLocations.javaRuntimeLocations(); LOGGER.trace("Adding JRT locations to compiler: {}", jrtLocations); - compiler - .getPathLocationRepository() - .getOrCreateManager(StandardLocation.SYSTEM_MODULES) - .addPaths(jrtLocations); - } - - private void configureAnnotationProcessors(A compiler, CompilationTask task) { - if (compiler.getAnnotationProcessors().size() > 0) { - LOGGER.debug("Annotation processor discovery is disabled (processors explicitly provided)"); - task.setProcessors(compiler.getAnnotationProcessors()); - return; + for (var jrtLocation : jrtLocations) { + fileManager.addPath(StandardLocation.SYSTEM_MODULES, new NioPath(jrtLocation)); } + } + private void registerAnnotationProcessorPaths(A compiler, FileManager fileManager) { switch (compiler.getAnnotationProcessorDiscovery()) { case ENABLED: - // Ensure the paths exist. - compiler - .getPathLocationRepository() - .getManager(StandardLocation.ANNOTATION_PROCESSOR_MODULE_PATH) - .orElseGet(() -> compiler - .getPathLocationRepository() - .getOrCreateManager(StandardLocation.ANNOTATION_PROCESSOR_PATH)); + fileManager.ensureEmptyLocationExists(StandardLocation.ANNOTATION_PROCESSOR_PATH); break; - case INCLUDE_DEPENDENCIES: - compiler - .getPathLocationRepository() - .getManager(StandardLocation.ANNOTATION_PROCESSOR_MODULE_PATH) - .ifPresentOrElse( - procModules -> procModules.addPaths( - compiler - .getPathLocationRepository() - .getExpectedManager(StandardLocation.MODULE_PATH) - .getRoots()), - () -> compiler - .getPathLocationRepository() - .getOrCreateManager(StandardLocation.ANNOTATION_PROCESSOR_PATH) - .addPaths(compiler - .getPathLocationRepository() - .getExpectedManager(StandardLocation.CLASS_PATH) - .getRoots()) - ); + case INCLUDE_DEPENDENCIES: { + // https://stackoverflow.com/q/53084037 + // Seems that javac will always use the classpath to implement this behaviour, and never + // the module path. Let's keep this simple and mimic this behaviour. If someone complains + // about it being problematic in the future, then I am open to change how this works to + // keep it sensible. + fileManager.copyContainers( + StandardLocation.CLASS_PATH, + StandardLocation.ANNOTATION_PROCESSOR_PATH + ); + break; + } default: - // Set the processor list explicitly to instruct the compiler to not perform discovery. - task.setProcessors(List.of()); + // There is nothing to do to the file manager to configure annotation processing at this + // time. break; } } + + private void configureAnnotationProcessorDiscovery(A compiler, CompilationTask task) { + if (compiler.getAnnotationProcessors().size() > 0) { + LOGGER.debug("Annotation processor discovery is disabled (processors explicitly provided)"); + task.setProcessors(compiler.getAnnotationProcessors()); + } else if (compiler.getAnnotationProcessorDiscovery() + == AnnotationProcessorDiscovery.DISABLED) { + // Set the processor list explicitly to instruct the compiler to not perform discovery. + task.setProcessors(List.of()); + } + } } diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/SimpleCompiler.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/SimpleCompiler.java index 6f1514fec..7e8d5f3a5 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/SimpleCompiler.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/SimpleCompiler.java @@ -16,16 +16,12 @@ package io.github.ascopes.jct.compilers; -import static io.github.ascopes.jct.intern.IterableUtils.requireNonNullValues; +import static io.github.ascopes.jct.utils.IterableUtils.requireNonNullValues; import static java.util.Objects.requireNonNull; -import io.github.ascopes.jct.paths.PathJavaFileObjectFactory; -import io.github.ascopes.jct.paths.PathLocationRepository; -import io.github.ascopes.jct.paths.RamPath; +import io.github.ascopes.jct.paths.PathLike; import java.nio.charset.Charset; -import java.nio.file.Path; import java.util.ArrayList; -import java.util.Collection; import java.util.List; import java.util.Locale; import java.util.Optional; @@ -60,14 +56,13 @@ */ @API(since = "0.0.1", status = Status.EXPERIMENTAL) public abstract class SimpleCompiler> - implements Compiler { + implements Compilable { private static final Logger LOGGER = LoggerFactory.getLogger(SimpleCompiler.class); private final String name; private final JavaCompiler jsr199Compiler; private final FlagBuilder flagBuilder; - private final PathJavaFileObjectFactory pathJavaFileObjectFactory; - private final PathLocationRepository fileRepository; + private final SimpleFileManagerTemplate fileManagerTemplate; private final List annotationProcessors; private final List annotationProcessorOptions; private final List compilerOptions; @@ -86,54 +81,52 @@ public abstract class SimpleCompiler> private boolean inheritModulePath; private boolean inheritPlatformClassPath; private boolean inheritSystemModulePath; - private Logging fileManagerLogging; - private Logging diagnosticLogging; + private LoggingMode fileManagerLoggingMode; + private LoggingMode diagnosticLoggingMode; private AnnotationProcessorDiscovery annotationProcessorDiscovery; /** * Initialize this compiler. * - * @param name the friendly name of the compiler. - * @param jsr199Compiler the JSR-199 compiler implementation to use. - * @param flagBuilder the flag builder to use. + * @param name the friendly name of the compiler. + * @param fileManagerTemplate the simple file manager template to use. + * @param jsr199Compiler the JSR-199 compiler implementation to use. + * @param flagBuilder the flag builder to use. */ protected SimpleCompiler( String name, + SimpleFileManagerTemplate fileManagerTemplate, JavaCompiler jsr199Compiler, FlagBuilder flagBuilder ) { this.name = requireNonNull(name, "name"); + this.fileManagerTemplate = requireNonNull(fileManagerTemplate, "fileManagerTemplate"); this.jsr199Compiler = requireNonNull(jsr199Compiler, "jsr199Compiler"); this.flagBuilder = requireNonNull(flagBuilder, "flagBuilder"); - // We may want to be able to customize creation of missing roots in the future. For now, - // I am leaving this enabled by default. - pathJavaFileObjectFactory = new PathJavaFileObjectFactory(DEFAULT_FILE_CHARSET); - fileRepository = new PathLocationRepository(pathJavaFileObjectFactory); - annotationProcessors = new ArrayList<>(); annotationProcessorOptions = new ArrayList<>(); compilerOptions = new ArrayList<>(); runtimeOptions = new ArrayList<>(); - showWarnings = DEFAULT_SHOW_WARNINGS; - showDeprecationWarnings = DEFAULT_SHOW_DEPRECATION_WARNINGS; - failOnWarnings = DEFAULT_FAIL_ON_WARNINGS; - locale = DEFAULT_LOCALE; - logCharset = DEFAULT_LOG_CHARSET; - previewFeatures = DEFAULT_PREVIEW_FEATURES; + showWarnings = Compilable.DEFAULT_SHOW_WARNINGS; + showDeprecationWarnings = Compilable.DEFAULT_SHOW_DEPRECATION_WARNINGS; + failOnWarnings = Compilable.DEFAULT_FAIL_ON_WARNINGS; + locale = Compilable.DEFAULT_LOCALE; + logCharset = Compilable.DEFAULT_LOG_CHARSET; + previewFeatures = Compilable.DEFAULT_PREVIEW_FEATURES; release = null; source = null; target = null; - verbose = DEFAULT_VERBOSE; - inheritClassPath = DEFAULT_INHERIT_CLASS_PATH; - inheritModulePath = DEFAULT_INHERIT_MODULE_PATH; - inheritPlatformClassPath = DEFAULT_INHERIT_PLATFORM_CLASS_PATH; - inheritSystemModulePath = DEFAULT_INHERIT_SYSTEM_MODULE_PATH; - fileManagerLogging = DEFAULT_FILE_MANAGER_LOGGING; - diagnosticLogging = DEFAULT_DIAGNOSTIC_LOGGING; - annotationProcessorDiscovery = DEFAULT_ANNOTATION_PROCESSOR_DISCOVERY; + verbose = Compilable.DEFAULT_VERBOSE; + inheritClassPath = Compilable.DEFAULT_INHERIT_CLASS_PATH; + inheritModulePath = Compilable.DEFAULT_INHERIT_MODULE_PATH; + inheritPlatformClassPath = Compilable.DEFAULT_INHERIT_PLATFORM_CLASS_PATH; + inheritSystemModulePath = Compilable.DEFAULT_INHERIT_SYSTEM_MODULE_PATH; + fileManagerLoggingMode = Compilable.DEFAULT_FILE_MANAGER_LOGGING_MODE; + diagnosticLoggingMode = Compilable.DEFAULT_DIAGNOSTIC_LOGGING_MODE; + annotationProcessorDiscovery = Compilable.DEFAULT_ANNOTATION_PROCESSOR_DISCOVERY; } /** @@ -173,7 +166,9 @@ public SimpleCompilation compile() { } @Override - public final A configure(CompilerConfigurer configurer) throws T { + public final A configure( + CompilerConfigurer configurer + ) throws T { LOGGER.debug("configure({})", configurer); var me = myself(); configurer.configure(me); @@ -181,34 +176,16 @@ public final A configure(CompilerConfigurer configur } @Override - public A addPaths(Location location, Collection paths) { - LOGGER.trace("{}.paths += {}", location.getName(), paths); - fileRepository.getOrCreateManager(location).addPaths(paths); - return myself(); - } - - @Override - public A addRamPaths(Location location, Collection paths) { - LOGGER.trace("{}.paths += {}", location.getName(), paths); - fileRepository.getOrCreateManager(location).addRamPaths(paths); + public A addPath(Location location, PathLike pathLike) { + LOGGER.trace("{}.paths += {}", location.getName(), pathLike.getPath()); + fileManagerTemplate.addPath(location, pathLike); return myself(); } @Override - public PathLocationRepository getPathLocationRepository() { - return fileRepository; - } - - @Override - public Charset getFileCharset() { - return pathJavaFileObjectFactory.getCharset(); - } - - @Override - public A fileCharset(Charset fileCharset) { - requireNonNull(fileCharset, "fileCharset"); - LOGGER.trace("fileCharset {} -> {}", pathJavaFileObjectFactory.getCharset(), fileCharset); - pathJavaFileObjectFactory.setCharset(fileCharset); + public A addPath(Location location, String moduleName, PathLike pathLike) { + LOGGER.trace("{}[{}].paths += {}", location.getName(), moduleName, pathLike.getPath()); + fileManagerTemplate.addPath(location, moduleName, pathLike); return myself(); } @@ -451,36 +428,36 @@ public A logCharset(Charset logCharset) { } @Override - public Logging getFileManagerLogging() { - return fileManagerLogging; + public LoggingMode getFileManagerLoggingMode() { + return fileManagerLoggingMode; } @Override - public A fileManagerLogging(Logging fileManagerLogging) { - requireNonNull(fileManagerLogging, "fileManagerLogging"); + public A fileManagerLoggingMode(LoggingMode fileManagerLoggingMode) { + requireNonNull(fileManagerLoggingMode, "fileManagerLoggingMode"); LOGGER.trace( - "fileManagerLogging {} -> {}", - this.fileManagerLogging, - fileManagerLogging + "fileManagerLoggingMode {} -> {}", + this.fileManagerLoggingMode, + fileManagerLoggingMode ); - this.fileManagerLogging = fileManagerLogging; + this.fileManagerLoggingMode = fileManagerLoggingMode; return myself(); } @Override - public Logging getDiagnosticLogging() { - return diagnosticLogging; + public LoggingMode getDiagnosticLoggingMode() { + return diagnosticLoggingMode; } @Override - public A diagnosticLogging(Logging diagnosticLogging) { - requireNonNull(diagnosticLogging, "diagnosticLogging"); + public A diagnosticLoggingMode(LoggingMode diagnosticLoggingMode) { + requireNonNull(diagnosticLoggingMode, "diagnosticLoggingMode"); LOGGER.trace( - "diagnosticLogging {} -> {}", - this.diagnosticLogging, - diagnosticLogging + "diagnosticLoggingMode {} -> {}", + this.diagnosticLoggingMode, + diagnosticLoggingMode ); - this.diagnosticLogging = diagnosticLogging; + this.diagnosticLoggingMode = diagnosticLoggingMode; return myself(); } @@ -524,6 +501,7 @@ protected final A myself() { protected SimpleCompilation doCompile() { return new SimpleCompilationFactory().compile( myself(), + fileManagerTemplate, jsr199Compiler, flagBuilder ); diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/SimpleFileManagerTemplate.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/SimpleFileManagerTemplate.java new file mode 100644 index 000000000..4fb65819b --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/SimpleFileManagerTemplate.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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; + +import io.github.ascopes.jct.jsr199.ModuleLocation; +import io.github.ascopes.jct.jsr199.SimpleFileManager; +import io.github.ascopes.jct.paths.NioPath; +import io.github.ascopes.jct.paths.PathLike; +import io.github.ascopes.jct.utils.StringUtils; +import java.nio.file.Path; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import javax.lang.model.SourceVersion; +import javax.tools.JavaFileManager.Location; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + + +/** + * A template for creating a file manager later. + * + *

File manager creation is deferred until as late as possible as to enable the specification of + * the version to use when opening JARs that may be multi-release compatible. We have to do this to + * ensure the behaviour for opening JARs matches the release version the code is compiled against. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public class SimpleFileManagerTemplate { + + private final Map> locations; + + /** + * Initialize this workspace. + */ + public SimpleFileManagerTemplate() { + locations = new HashMap<>(); + } + + /** + * Add a path to a package. + * + * @param location the location the package resides within. + * @param path the path to associate with the location. + * @throws IllegalArgumentException if the location is module-oriented or output oriented. + */ + public void addPath(Location location, Path path) { + addPath(location, new NioPath(path)); + } + + /** + * Add a path to a module. + * + * @param location the location the module resides within. + * @param module the name of the module to add. + * @param path the path to associate with the module. + * @throws IllegalArgumentException if the {@code location} parameter is a + * {@link ModuleLocation}. + * @throws IllegalArgumentException if the {@code location} parameter is not + * {@link Location#isModuleOrientedLocation() module-oriented}. + * @throws IllegalArgumentException if the {@code module} parameter is not a valid module name, as + * defined by the Java Language Specification for the current + * JVM. + */ + public void addPath(Location location, String module, Path path) { + addPath(location, module, new NioPath(path)); + } + + /** + * Add a path to a package. + * + * @param location the location the package resides within. + * @param path the path to associate with the location. + * @throws IllegalArgumentException if the location is module-oriented or output oriented. + */ + public void addPath(Location location, PathLike path) { + if (location.isOutputLocation()) { + throw new IllegalArgumentException("Can not add paths to an output oriented location."); + } + + if (location.isModuleOrientedLocation()) { + throw new IllegalArgumentException( + "Can not add paths directly to a module oriented location. Consider using " + + "#addPath(Location, String, PathLike) or #addPath(Location, String, Path) instead" + ); + } + + locations.computeIfAbsent(location, ignored -> new LinkedHashSet<>()).add(path); + } + + /** + * Add a path to a module. + * + * @param location the location the module resides within. + * @param module the name of the module to add. + * @param path the path to associate with the module. + * @throws IllegalArgumentException if the {@code location} parameter is a + * {@link ModuleLocation}. + * @throws IllegalArgumentException if the {@code location} parameter is not + * {@link Location#isModuleOrientedLocation() module-oriented}. + * @throws IllegalArgumentException if the {@code module} parameter is not a valid module name, as + * defined by the Java Language Specification for the current + * JVM. + */ + public void addPath(Location location, String module, PathLike path) { + if (location instanceof ModuleLocation) { + throw new IllegalArgumentException( + "Cannot use a " + ModuleLocation.class.getName() + " with a custom module name. " + + "Use SimpleFileManagerTemplate#addPath(Location, PathLike) " + + "or SimpleFileManagerTemplate#addPath(Location, Path) instead." + ); + } + + if (!location.isModuleOrientedLocation()) { + throw new IllegalArgumentException( + "Location " + StringUtils.quoted(location.getName()) + " must be module-oriented " + + "or an output location to be able to associate a module with it." + ); + } + + if (!SourceVersion.isName(module)) { + throw new IllegalArgumentException( + "Module " + StringUtils.quoted(module) + " is not a valid module name" + ); + } + + addPath(new ModuleLocation(location, module), path); + } + + /** + * Create a file manager for this workspace. + * + * @param release the release version to use. + * @return the file manager. + */ + public SimpleFileManager createFileManager(String release) { + var manager = new SimpleFileManager(release); + locations.forEach((location, paths) -> { + for (var path : paths) { + manager.addPath(location, path); + } + }); + + return manager; + } + + /** + * Get the paths associated with the given package-oriented location. + * + * @param location the location to get. + * @return the paths. + * @throws IllegalArgumentException if the location is module-oriented. + */ + public List getPaths(Location location) { + if (location.isModuleOrientedLocation()) { + throw new IllegalArgumentException("Cannot get paths from a module-oriented location"); + } + + return Optional + .ofNullable(locations.get(location)) + .map(List::copyOf) + .orElseGet(List::of); + } + + /** + * Get the modules associated with the given module-oriented/output-oriented location. + * + * @param location the location to get. + * @return the locations. + * @throws IllegalArgumentException if the location is neither output nor package oriented. + */ + public Collection getModuleLocations(Location location) { + if (!location.isModuleOrientedLocation() && !location.isOutputLocation()) { + throw new IllegalArgumentException( + "Cannot get modules from a non-module-oriented/non-output location" + ); + } + + return locations + .keySet() + .stream() + .filter(ModuleLocation.class::isInstance) + .map(ModuleLocation.class::cast) + .filter(hasParent(location)) + .collect(Collectors.toUnmodifiableList()); + } + + private Predicate hasParent(Location parent) { + return moduleLocation -> moduleLocation.getParent().equals(parent); + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/ecj/EcjCompiler.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/ecj/EcjCompiler.java index ebe212161..a157bbbac 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/ecj/EcjCompiler.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/ecj/EcjCompiler.java @@ -17,9 +17,12 @@ package io.github.ascopes.jct.compilers.ecj; import io.github.ascopes.jct.compilers.SimpleCompiler; +import io.github.ascopes.jct.compilers.SimpleFileManagerTemplate; import javax.tools.JavaCompiler; import org.apiguardian.api.API; import org.apiguardian.api.API.Status; +import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants; +import org.eclipse.jdt.internal.compiler.tool.EclipseCompiler; /** * Implementation of an ECJ compiler. @@ -27,15 +30,58 @@ * @author Ashley Scopes * @since 0.0.1 */ -@API(since = "0.0.1", status = Status.INTERNAL) +@API(since = "0.0.1", status = Status.EXPERIMENTAL) public class EcjCompiler extends SimpleCompiler { + private static final String NAME = "Eclipse Compiler for Java"; + + /** + * Initialize a new ECJ compiler. + */ + public EcjCompiler() { + this(new EclipseCompiler()); + } + /** * Initialize a new ECJ compiler. * * @param jsr199Compiler the JSR-199 compiler backend to use. */ public EcjCompiler(JavaCompiler jsr199Compiler) { - super("ecj", jsr199Compiler, new EcjFlagBuilder()); + this(NAME, jsr199Compiler); + } + + /** + * Initialize a new ECJ compiler. + * + * @param name the name to give the compiler. + */ + public EcjCompiler(String name) { + super(name, new SimpleFileManagerTemplate(), new EclipseCompiler(), new EcjFlagBuilder()); + } + + /** + * Initialize a new ECJ compiler. + * + * @param name the name to give the compiler. + * @param jsr199Compiler the JSR-199 compiler backend to use. + */ + public EcjCompiler(String name, JavaCompiler jsr199Compiler) { + super(name, new SimpleFileManagerTemplate(), jsr199Compiler, new EcjFlagBuilder()); + } + + @Override + public String getDefaultRelease() { + return Integer.toString(getMaxVersion()); + } + + /** + * Get the maximum version of ECJ that is supported. + */ + public static int getMaxVersion() { + var version = (ClassFileConstants.getLatestJDKLevel() >> (Short.BYTES * 8)) + - ClassFileConstants.MAJOR_VERSION_0; + + return (int) version; } } diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/javac/JavacCompiler.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/javac/JavacCompiler.java index 2b8d3ecda..9f8599dba 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/javac/JavacCompiler.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/javac/JavacCompiler.java @@ -17,7 +17,10 @@ package io.github.ascopes.jct.compilers.javac; import io.github.ascopes.jct.compilers.SimpleCompiler; +import io.github.ascopes.jct.compilers.SimpleFileManagerTemplate; +import javax.lang.model.SourceVersion; import javax.tools.JavaCompiler; +import javax.tools.ToolProvider; import org.apiguardian.api.API; import org.apiguardian.api.API.Status; @@ -27,15 +30,61 @@ * @author Ashley Scopes * @since 0.0.1 */ -@API(since = "0.0.1", status = Status.INTERNAL) +@API(since = "0.0.1", status = Status.EXPERIMENTAL) public class JavacCompiler extends SimpleCompiler { + private static final String NAME = "JDK Compiler"; + + + /** + * Initialize a new Java compiler. + */ + public JavacCompiler() { + this(ToolProvider.getSystemJavaCompiler()); + } + /** - * Initialize a new Javac compiler. + * Initialize a new Java compiler. * * @param jsr199Compiler the JSR-199 compiler backend to use. */ public JavacCompiler(JavaCompiler jsr199Compiler) { - super("javac", jsr199Compiler, new JavacFlagBuilder()); + this(NAME, jsr199Compiler); + } + + /** + * Initialize a new Java compiler. + * + * @param name the name to give the compiler. + */ + public JavacCompiler(String name) { + super( + name, + new SimpleFileManagerTemplate(), + ToolProvider.getSystemJavaCompiler(), + new JavacFlagBuilder() + ); + } + + /** + * Initialize a new Java compiler. + * + * @param name the name to give the compiler. + * @param jsr199Compiler the JSR-199 compiler backend to use. + */ + public JavacCompiler(String name, JavaCompiler jsr199Compiler) { + super(name, new SimpleFileManagerTemplate(), jsr199Compiler, new JavacFlagBuilder()); + } + + @Override + public String getDefaultRelease() { + return Integer.toString(getMaxVersion()); + } + + /** + * Get the maximum version of Javac that is supported. + */ + public static int getMaxVersion() { + return SourceVersion.latestSupported().ordinal(); } } diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/package-info.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/package-info.java index e4c5e2e8e..bfaf19e62 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/package-info.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/package-info.java @@ -15,6 +15,6 @@ */ /** - * JSR-199 compiler integration, management, and configuration. + * Compiler frontends that allow invoking compilers easily from tests. */ package io.github.ascopes.jct.compilers; diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/PlatformLinkStrategy.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/PlatformLinkStrategy.java deleted file mode 100644 index ac405436d..000000000 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/PlatformLinkStrategy.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (C) 2022 Ashley Scopes - * - * 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.intern; - -import java.io.IOException; -import java.nio.file.FileSystemException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Locale; -import java.util.Properties; -import org.apiguardian.api.API; -import org.apiguardian.api.API.Status; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Strategy for creating file system links where possible. - * - *

Windows cannot create symbolic links without root, but can create hard links. - * - *

Hard links will often not work across separate mount points on Linux. - * - *

Some operating systems will support neither hard nor symbolic links, so we fall back - * to copying the target file to the link location in these cases. - * - * @author Ashley Scopes - * @since 0.0.1 - */ -@API(since = "0.0.1", status = Status.INTERNAL) -public final class PlatformLinkStrategy { - - private static final Logger LOGGER = LoggerFactory.getLogger(PlatformLinkStrategy.class); - private final Call impl; - private final String name; - - /** - * Initialize this strategy. - */ - public PlatformLinkStrategy(Properties systemProperties) { - var windows = systemProperties - .getProperty("os.name", "") - .toLowerCase(Locale.ROOT) - .startsWith("windows"); - - if (windows) { - // Windows cannot create symbolic links without root. - impl = Files::createLink; - name = "hard link"; - } else { - // /tmp on UNIX is usually a separate device, so hard links won't work. - impl = Files::createSymbolicLink; - name = "symbolic link"; - } - } - - /** - * Attempt to create a link, falling back to just copying the file if this is not possible. - * - * @param link the link to create. - * @param target the target to link to. - * @return the path to the created link or file. - * @throws IOException if something goes wrong. - */ - public Path createLinkOrCopy(Path link, Path target) throws IOException { - try { - var result = impl.createLink(link, target); - LOGGER.trace("Created {} from {} to {}", name, link, target); - return result; - } catch (UnsupportedOperationException | FileSystemException ex) { - LOGGER.trace( - "Failed to create {} from {} to {}, falling back to copying files", - name, - link, - target, - ex - ); - return Files.copy(target, link); - } - } - - @FunctionalInterface - private interface Call { - - /** - * Create the link. - * - * @param link the link to create. - * @param target the target to link to. - * @return the link. - * @throws IOException if the link fails to be created. - */ - Path createLink(Path link, Path target) throws IOException; - } -} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/FileManager.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/FileManager.java new file mode 100644 index 000000000..2b31dd1be --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/FileManager.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.jsr199; + +import io.github.ascopes.jct.jsr199.containers.ModuleOrientedContainerGroup; +import io.github.ascopes.jct.jsr199.containers.OutputOrientedContainerGroup; +import io.github.ascopes.jct.jsr199.containers.PackageOrientedContainerGroup; +import io.github.ascopes.jct.paths.PathLike; +import java.util.Optional; +import javax.tools.JavaFileManager; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * Extension around a {@link JavaFileManager} that allows adding of {@link PathLike} objects to the + * manager. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public interface FileManager extends JavaFileManager { + + /** + * Add a path to a given location. + * + * @param location the location to use. + * @param path the path to add. + */ + void addPath(Location location, PathLike path); + + /** + * Register an empty container for the given location to indicate to the compiler that the feature + * exists, but has no configured paths. + * + *

This is needed to coerce the behaviour for annotation processing in some cases. + * + *

If the location already exists, then do not do anything. + * + * @param location the location to apply an empty container for. + */ + void ensureEmptyLocationExists(Location location); + + /** + * Copy all containers from the first location to the second location. + * + * @param from the first location. + * @param to the second location. + */ + void copyContainers(Location from, Location to); + + /** + * Get the container group for the given package-oriented location. + * + * @param location the package oriented location. + * @return the container group, or an empty optional if one does not exist. + */ + Optional getPackageContainerGroup(Location location); + + /** + * Get the container group for the given module-oriented location. + * + * @param location the module oriented location. + * @return the container group, or an empty optional if one does not exist. + */ + Optional getModuleContainer(Location location); + + /** + * Get the container group for the given output-oriented location. + * + * @param location the output oriented location. + * @return the container group, or an empty optional if one does not exist. + */ + Optional getOutputContainers(Location location); +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/LoggingJavaFileManagerProxy.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/LoggingFileManagerProxy.java similarity index 75% rename from java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/LoggingJavaFileManagerProxy.java rename to java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/LoggingFileManagerProxy.java index 5b26b4ab7..54166ead3 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/LoggingJavaFileManagerProxy.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/LoggingFileManagerProxy.java @@ -14,15 +14,15 @@ * limitations under the License. */ -package io.github.ascopes.jct.paths; +package io.github.ascopes.jct.jsr199; import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Arrays; import java.util.Objects; import java.util.stream.Collectors; -import javax.tools.JavaFileManager; import org.apiguardian.api.API; import org.apiguardian.api.API.Status; import org.slf4j.Logger; @@ -30,7 +30,7 @@ /** - * A proxy that wraps a {@link JavaFileManager} in a proxy that can log all interactions with the + * A proxy that wraps a {@link FileManager} in a proxy that can log all interactions with the * JavaFileManager, along with a corresponding stacktrace. * *

This is useful for diagnosing difficult-to-find errors being produced by {@code javac} @@ -40,14 +40,14 @@ * @since 0.0.1 */ @API(since = "0.0.1", status = Status.EXPERIMENTAL) -public class LoggingJavaFileManagerProxy implements InvocationHandler { +public class LoggingFileManagerProxy implements InvocationHandler { - private static final Logger LOGGER = LoggerFactory.getLogger(LoggingJavaFileManagerProxy.class); + private static final Logger LOGGER = LoggerFactory.getLogger(LoggingFileManagerProxy.class); - private final JavaFileManager inner; + private final FileManager inner; private final boolean stackTraces; - private LoggingJavaFileManagerProxy(JavaFileManager inner, boolean stackTraces) { + private LoggingFileManagerProxy(FileManager inner, boolean stackTraces) { this.inner = inner; this.stackTraces = stackTraces; } @@ -89,9 +89,13 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl LOGGER.info("<<< {}({}) returns {}", method.getName(), argsStr, result); } return result; - } catch (Throwable ex) { - LOGGER.error("!!! {}({}) throws exception", method.getName(), argsStr, ex); - throw ex; + } catch (InvocationTargetException ex) { + var cause = ex.getCause() == null + ? ex + : ex.getCause(); + + LOGGER.error("!!! {}({}) throws exception", method.getName(), argsStr, cause); + throw cause; } } @@ -104,18 +108,18 @@ public String toString() { } /** - * Wrap the given {@link JavaFileManager} in a proxy that logs any calls. + * Wrap the given {@link FileManager} in a proxy that logs any calls. * * @param manager the manager to wrap. * @param stackTraces {@code true} to dump stacktraces on each interception, or {@code false} to * omit them. - * @return the proxy {@link JavaFileManager} to use. + * @return the proxy {@link FileManager} to use. */ - public static JavaFileManager wrap(JavaFileManager manager, boolean stackTraces) { - return (JavaFileManager) Proxy.newProxyInstance( - JavaFileManager.class.getClassLoader(), - new Class[]{JavaFileManager.class}, - new LoggingJavaFileManagerProxy(manager, stackTraces) + public static FileManager wrap(FileManager manager, boolean stackTraces) { + return (FileManager) Proxy.newProxyInstance( + FileManager.class.getClassLoader(), + new Class[]{FileManager.class}, + new LoggingFileManagerProxy(manager, stackTraces) ); } } diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/ModuleLocation.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/ModuleLocation.java similarity index 97% rename from java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/ModuleLocation.java rename to java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/ModuleLocation.java index 7cba80ea0..74e0c0229 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/ModuleLocation.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/ModuleLocation.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package io.github.ascopes.jct.paths; +package io.github.ascopes.jct.jsr199; -import io.github.ascopes.jct.intern.StringUtils; +import io.github.ascopes.jct.utils.StringUtils; import java.util.Objects; import javax.tools.JavaFileManager.Location; import org.apiguardian.api.API; diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/PathJavaFileObject.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/PathFileObject.java similarity index 66% rename from java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/PathJavaFileObject.java rename to java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/PathFileObject.java index 0d3675f63..29854f110 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/PathJavaFileObject.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/PathFileObject.java @@ -14,11 +14,12 @@ * limitations under the License. */ -package io.github.ascopes.jct.paths; +package io.github.ascopes.jct.jsr199; import static java.util.Objects.requireNonNull; -import io.github.ascopes.jct.intern.StringUtils; +import io.github.ascopes.jct.utils.FileUtils; +import io.github.ascopes.jct.utils.StringUtils; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; @@ -32,10 +33,11 @@ import java.io.Writer; import java.net.URI; import java.nio.ByteBuffer; -import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.nio.charset.CharsetEncoder; import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; import javax.lang.model.element.Modifier; @@ -48,61 +50,62 @@ import org.slf4j.LoggerFactory; /** - * File object that can be used with paths, and that is bound to a specific charset. - * - *

All inputs and outputs are buffered automatically. + * A very simple {@link JavaFileObject} that points to a path on some {@link FileSystem} somewhere. * * @author Ashley Scopes * @since 0.0.1 */ @API(since = "0.0.1", status = Status.EXPERIMENTAL) -public class PathJavaFileObject implements JavaFileObject { +public class PathFileObject implements JavaFileObject { - private static final Logger LOGGER = LoggerFactory.getLogger(PathJavaFileObject.class); + private static final Logger LOGGER = LoggerFactory.getLogger(PathFileObject.class); private static final long NOT_MODIFIED = 0L; private final Location location; - private final Path path; + private final Path root; + private final Path relativePath; + private final Path fullPath; private final String name; private final URI uri; private final Kind kind; - private final Charset charset; /** - * Initialize the file object. + * Initialize this file object. * - * @param location the location of the object. - * @param path the path to the object. - * @param name the given name to use to identify the file. - * @param kind the kind of the file. - * @param charset the charset to use to read the file textually. If the file is binary then this - * can be any value. + * @param root the root directory that the path is a package within. + * @param relativePath the path to point to. */ - public PathJavaFileObject(Location location, Path path, String name, Kind kind, Charset charset) { - this.location = requireNonNull(location); - this.path = requireNonNull(path); - this.name = requireNonNull(name); - uri = path.toUri(); - this.kind = requireNonNull(kind); - this.charset = requireNonNull(charset); + public PathFileObject(Location location, Path root, Path relativePath) { + this.location = requireNonNull(location, "location"); + this.root = requireNonNull(root, "root"); + this.relativePath = root.relativize(requireNonNull(relativePath, "relativePath")); + fullPath = root.resolve(relativePath); + name = relativePath.toString(); + uri = fullPath.toUri(); + kind = FileUtils.pathToKind(relativePath); } - /** - * Get the location of the file. - * - * @return the location. - */ - public Location getLocation() { - return location; + @Override + public boolean delete() { + try { + return Files.deleteIfExists(relativePath); + } catch (IOException ex) { + LOGGER.warn("Ignoring error deleting {}", uri, ex); + return false; + } } - /** - * Get the path to the file. - * - * @return the path. - */ - public Path getPath() { - return path; + @Override + public Modifier getAccessLevel() { + // Null implies that the access level is unknown. + return null; + } + + @Override + public String getCharContent(boolean ignoreEncodingErrors) throws IOException { + return decoder(ignoreEncodingErrors) + .decode(ByteBuffer.wrap(Files.readAllBytes(fullPath))) + .toString(); } @Override @@ -111,36 +114,56 @@ public Kind getKind() { } @Override - public boolean isNameCompatible(String simpleName, Kind kind) { - return path.getFileName().toString().endsWith(simpleName + kind.extension); + public long getLastModified() { + try { + return Files.getLastModifiedTime(relativePath).toMillis(); + } catch (IOException ex) { + LOGGER.warn("Ignoring error reading last modified time for {}", uri, ex); + return NOT_MODIFIED; + } + } + + /** + * Get the location of this path file object. + * + * @return the location. + */ + public Location getLocation() { + return location; } @Override - public NestingKind getNestingKind() { - // We do not have access to this info, so return null to - // indicate this to JSR-199. - return null; + public String getName() { + return name; } @Override - public Modifier getAccessLevel() { - // We do not have access to this info, so return null to - // indicate this to JSR-199. + public NestingKind getNestingKind() { + // Null implies that the nesting kind is unknown. return null; } - @Override - public URI toUri() { - // TODO(ascopes): mitigate bug where URI from path in JAR starts with - // jar://file:/// rather than jar:///, causing filesystem resolution - // to fail. This might be an issue if JARs are added from non-root file - // systems though, so I don't know the best way of working around this. - return uri; + /** + * Get the full path of this file object. + * + * @return the full path. + */ + public Path getFullPath() { + return fullPath; + } + + /** + * Get the relative path of this file object. + * + * @return the path of this file object. + */ + public Path getRelativePath() { + return relativePath; } @Override - public String getName() { - return name; + public boolean isNameCompatible(String simpleName, Kind kind) { + return relativePath.getFileName().toString().equals(simpleName + kind.extension); } @Override @@ -158,55 +181,29 @@ public BufferedReader openReader(boolean ignoreEncodingErrors) throws IOExceptio return new BufferedReader(openUnbufferedReader(ignoreEncodingErrors)); } - @Override - public String getCharContent(boolean ignoreEncodingErrors) throws IOException { - return decoder(ignoreEncodingErrors) - .decode(ByteBuffer.wrap(Files.readAllBytes(path))) - .toString(); - } - @Override public BufferedWriter openWriter() throws IOException { return new BufferedWriter(openUnbufferedWriter()); } @Override - public long getLastModified() { - try { - return Files.getLastModifiedTime(path).toMillis(); - } catch (IOException ex) { - LOGGER.warn("Ignoring error reading last modified time for {}", uri, ex); - return NOT_MODIFIED; - } - } - - @Override - public boolean delete() { - try { - return Files.deleteIfExists(path); - } catch (IOException ex) { - LOGGER.warn("Ignoring error deleting {}", uri, ex); - return false; - } + public URI toUri() { + return uri; } @Override public String toString() { - return getClass().getSimpleName() - + "{location=" + StringUtils.quoted(location.getName()) + ", " - + "uri=" + StringUtils.quoted(uri) + ", " - + "kind=" + kind - + "}"; + return getClass().getSimpleName() + "{uri=" + StringUtils.quoted(uri) + "}"; } private InputStream openUnbufferedInputStream() throws IOException { - return Files.newInputStream(path); + return Files.newInputStream(fullPath); } private OutputStream openUnbufferedOutputStream() throws IOException { // Ensure parent directories exist first. - Files.createDirectories(path.getParent()); - return Files.newOutputStream(path); + Files.createDirectories(fullPath.getParent()); + return Files.newOutputStream(fullPath); } private Reader openUnbufferedReader(boolean ignoreEncodingErrors) throws IOException { @@ -222,14 +219,14 @@ private CharsetDecoder decoder(boolean ignoreEncodingErrors) { ? CodingErrorAction.IGNORE : CodingErrorAction.REPORT; - return charset + return StandardCharsets.UTF_8 .newDecoder() .onUnmappableCharacter(action) .onMalformedInput(action); } private CharsetEncoder encoder() { - return charset + return StandardCharsets.UTF_8 .newEncoder() .onUnmappableCharacter(CodingErrorAction.REPORT) .onMalformedInput(CodingErrorAction.REPORT); diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/SimpleFileManager.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/SimpleFileManager.java new file mode 100644 index 000000000..a7fa323e4 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/SimpleFileManager.java @@ -0,0 +1,495 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.jsr199; + +import static java.util.Objects.requireNonNull; + +import io.github.ascopes.jct.jsr199.containers.ContainerGroup; +import io.github.ascopes.jct.jsr199.containers.ModuleOrientedContainerGroup; +import io.github.ascopes.jct.jsr199.containers.OutputOrientedContainerGroup; +import io.github.ascopes.jct.jsr199.containers.PackageOrientedContainerGroup; +import io.github.ascopes.jct.jsr199.containers.SimpleModuleOrientedContainerGroup; +import io.github.ascopes.jct.jsr199.containers.SimpleOutputOrientedContainerGroup; +import io.github.ascopes.jct.jsr199.containers.SimplePackageOrientedContainerGroup; +import io.github.ascopes.jct.paths.PathLike; +import io.github.ascopes.jct.paths.SubPath; +import io.github.ascopes.jct.utils.Nullable; +import java.io.IOException; +import java.lang.module.ModuleDescriptor; +import java.lang.module.ModuleFinder; +import java.lang.module.ModuleReference; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.stream.Collectors; +import javax.tools.FileObject; +import javax.tools.JavaFileObject; +import javax.tools.JavaFileObject.Kind; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * Simple implementation of a {@link FileManager}. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public class SimpleFileManager implements FileManager { + + private final String release; + private final Map packages; + private final Map modules; + private final Map outputs; + + /** + * Initialize this file manager. + * + * @param release the release to use for multi-release JARs internally. + */ + public SimpleFileManager(String release) { + this.release = requireNonNull(release, "release"); + packages = new HashMap<>(); + modules = new HashMap<>(); + outputs = new HashMap<>(); + } + + /** + * Add a path to the given location. + * + * @param location the location to use for the path. + * @param path the path to add. + */ + public void addPath(Location location, PathLike path) { + if (location instanceof ModuleLocation) { + var moduleLocation = (ModuleLocation) location; + + if (location.isOutputLocation()) { + getOrCreateOutput(moduleLocation.getParent()) + .addModule(moduleLocation.getModuleName(), path); + } else { + getOrCreateModule(moduleLocation.getParent()) + .addModule(moduleLocation.getModuleName(), path); + } + } else if (location.isOutputLocation()) { + getOrCreateOutput(location) + .addPackage(path); + + } else if (location.isModuleOrientedLocation()) { + // Attempt to find modules. + var moduleGroup = getOrCreateModule(location); + + ModuleFinder + .of(path.getPath()) + .findAll() + .stream() + .map(ModuleReference::descriptor) + .map(ModuleDescriptor::name) + .forEach(module -> moduleGroup + .forModule(module) + .addPackage(new SubPath(path, module))); + + } else { + getOrCreatePackage(location) + .addPackage(path); + } + } + + @Override + @SuppressWarnings("resource") + public void ensureEmptyLocationExists(Location location) { + if (location instanceof ModuleLocation) { + var moduleLocation = (ModuleLocation) location; + + if (location.isOutputLocation()) { + getOrCreateOutput(moduleLocation.getParent()) + .forModule(moduleLocation.getModuleName()); + + } else { + getOrCreateModule(moduleLocation.getParent()) + .forModule(moduleLocation.getModuleName()); + } + } else if (location.isOutputLocation()) { + getOrCreateOutput(location); + } else if (location.isModuleOrientedLocation()) { + getOrCreateModule(location); + } else { + getOrCreatePackage(location); + } + } + + @Override + public void copyContainers(Location from, Location to) { + if (from.isOutputLocation()) { + if (!to.isOutputLocation()) { + throw new IllegalArgumentException( + "Expected " + from.getName() + " and " + to.getName() + " to both be output locations" + ); + } + } + + if (from.isModuleOrientedLocation()) { + if (!to.isModuleOrientedLocation()) { + throw new IllegalArgumentException( + "Expected " + from.getName() + " and " + to.getName() + " to both be " + + "module-oriented locations" + ); + } + } + + if (from.isOutputLocation()) { + var toOutputs = getOrCreateOutput(to); + + Optional + .ofNullable(outputs.get(from)) + .ifPresent(fromOutputs -> { + fromOutputs.getPackages().forEach(toOutputs::addPackage); + fromOutputs.getModules().forEach((module, containers) -> containers + .getPackages() + .forEach(container -> toOutputs.addModule(module.getModuleName(), container)) + ); + }); + + } else if (from.isModuleOrientedLocation()) { + var toModules = getOrCreateModule(to); + + Optional + .ofNullable(modules.get(from)) + .map(ModuleOrientedContainerGroup::getModules) + .ifPresent(fromModules -> fromModules + .forEach((module, containers) -> containers + .getPackages() + .forEach(container -> toModules.addModule(module.getModuleName(), container)) + ) + ); + + } else { + var toPackages = getOrCreatePackage(to); + + Optional + .ofNullable(packages.get(from)) + .ifPresent(fromPackages -> fromPackages.getPackages().forEach(toPackages::addPackage)); + } + + } + + @Override + public Optional getPackageContainerGroup(Location location) { + return Optional.ofNullable(packages.get(location)); + } + + @Override + public Optional getModuleContainer(Location location) { + return Optional.ofNullable(modules.get(location)); + } + + @Override + public Optional getOutputContainers(Location location) { + return Optional.ofNullable(outputs.get(location)); + } + + @Nullable + @Override + public ClassLoader getClassLoader(Location location) { + return getPackageOrientedOrOutputGroup(location) + .map(ContainerGroup::getClassLoader) + .orElse(null); + } + + @Override + public Iterable list( + Location location, + String packageName, + Set kinds, + boolean recurse + ) throws IOException { + var maybeGroup = getPackageOrientedOrOutputGroup(location); + + if (maybeGroup.isEmpty()) { + return List.of(); + } + + // Coerce the generic type to help the compiler a bit. + // TODO(ascopes): avoid doing this by finding a workaround. + return maybeGroup + .get() + .list(packageName, kinds, recurse) + .stream() + .map(JavaFileObject.class::cast) + .collect(Collectors.toUnmodifiableList()); + } + + @Nullable + @Override + public String inferBinaryName(Location location, JavaFileObject file) { + if (!(file instanceof PathFileObject)) { + return null; + } + + var pathFileObject = (PathFileObject) file; + + return getPackageOrientedOrOutputGroup(location) + .flatMap(group -> group.inferBinaryName(pathFileObject)) + .orElse(null); + + } + + @Override + public boolean isSameFile(FileObject a, FileObject b) { + return Objects.equals(a.toUri(), b.toUri()); + } + + @Override + public boolean handleOption(String current, Iterator remaining) { + return false; + } + + @Override + public boolean hasLocation(Location location) { + if (location instanceof ModuleLocation) { + var moduleLocation = (ModuleLocation) location; + return getModuleOrientedOrOutputGroup(moduleLocation.getParent()) + .map(group -> group.hasLocation(moduleLocation)) + .orElse(false); + } + + return packages.containsKey(location) + || modules.containsKey(location) + || outputs.containsKey(location); + } + + @Nullable + @Override + public JavaFileObject getJavaFileForInput( + Location location, + String className, + Kind kind + ) { + return getPackageOrientedOrOutputGroup(location) + .flatMap(group -> group.getJavaFileForInput(className, kind)) + .orElse(null); + } + + @Nullable + @Override + public JavaFileObject getJavaFileForOutput( + Location location, + String className, + Kind kind, + FileObject sibling + ) { + return getPackageOrientedOrOutputGroup(location) + .flatMap(group -> group.getJavaFileForOutput(className, kind)) + .orElse(null); + } + + @Nullable + @Override + public FileObject getFileForInput( + Location location, + String packageName, + String relativeName + ) { + return getPackageOrientedOrOutputGroup(location) + .flatMap(group -> group.getFileForInput(packageName, relativeName)) + .orElse(null); + } + + @Nullable + @Override + public FileObject getFileForOutput( + Location location, + String packageName, + String relativeName, + FileObject sibling + ) { + return getPackageOrientedOrOutputGroup(location) + .flatMap(group -> group.getFileForOutput(packageName, relativeName)) + .orElse(null); + } + + @Override + public void flush() { + // Do nothing. + } + + @Override + public void close() throws IOException { + // TODO: close on GC rather than anywhere else. + } + + @Override + public Location getLocationForModule(Location location, String moduleName) { + return new ModuleLocation(location, moduleName); + } + + @Override + public Location getLocationForModule(Location location, JavaFileObject fo) { + if (fo instanceof PathFileObject) { + var pathFileObject = (PathFileObject) fo; + var moduleLocation = pathFileObject.getLocation(); + + if (moduleLocation instanceof ModuleLocation) { + return moduleLocation; + } + + throw new IllegalArgumentException("File object " + fo + " is not for a module"); + } + + throw new IllegalArgumentException( + "File object " + fo + " does not appear to be registered to a module" + ); + } + + @Override + public ServiceLoader getServiceLoader( + Location location, + Class service + ) { + return getGroup(location) + .flatMap(group -> group.getServiceLoader(service)) + .orElseThrow(() -> new NoSuchElementException( + "No service for " + service.getName() + " exists" + )); + } + + @Nullable + @Override + public String inferModuleName(Location location) { + requirePackageOrientedLocation(location); + + return location instanceof ModuleLocation + ? ((ModuleLocation) location).getModuleName() + : null; + } + + @Override + public Iterable> listLocationsForModules(Location location) { + requireOutputOrModuleOrientedLocation(location); + + return getModuleOrientedOrOutputGroup(location) + .map(ModuleOrientedContainerGroup::getLocationsForModules) + .orElseGet(List::of); + } + + @Override + public boolean contains(Location location, FileObject fo) throws IOException { + if (!(fo instanceof PathFileObject)) { + return false; + } + + return getGroup(location) + .map(group -> group.contains((PathFileObject) fo)) + .orElse(false); + } + + @Override + public int isSupportedOption(String option) { + return 0; + } + + private Optional getGroup(Location location) { + if (location instanceof ModuleLocation) { + var moduleLocation = (ModuleLocation) location; + return getModuleOrientedOrOutputGroup(moduleLocation.getParent()) + .map(group -> group.forModule(moduleLocation.getModuleName())); + } + + return Optional + .ofNullable(packages.get(location)) + .or(() -> Optional.ofNullable(modules.get(location))) + .or(() -> Optional.ofNullable(outputs.get(location))); + } + + private Optional getModuleOrientedOrOutputGroup(Location location) { + if (location instanceof ModuleLocation) { + throw new IllegalArgumentException( + "Cannot get a module-oriented group from a ModuleLocation" + ); + } + + return Optional + .ofNullable(modules.get(location)) + .or(() -> Optional.ofNullable(outputs.get(location))); + } + + private Optional getPackageOrientedOrOutputGroup( + Location location + ) { + if (location instanceof ModuleLocation) { + var moduleLocation = (ModuleLocation) location; + return Optional + .ofNullable(modules.get(moduleLocation.getParent())) + .or(() -> Optional.ofNullable(outputs.get(moduleLocation.getParent()))) + .map(group -> group.forModule(moduleLocation.getModuleName())); + } + + return Optional + .ofNullable(packages.get(location)) + .or(() -> Optional.ofNullable(outputs.get(location))); + } + + private PackageOrientedContainerGroup getOrCreatePackage(Location location) { + if (location instanceof ModuleLocation) { + throw new IllegalArgumentException("Cannot get a package for a module like this"); + } + + return packages + .computeIfAbsent( + location, + unused -> new SimplePackageOrientedContainerGroup(location, release) + ); + } + + private ModuleOrientedContainerGroup getOrCreateModule(Location location) { + return modules + .computeIfAbsent( + location, + unused -> new SimpleModuleOrientedContainerGroup(location, release) + ); + } + + private OutputOrientedContainerGroup getOrCreateOutput(Location location) { + return outputs + .computeIfAbsent( + location, + unused -> new SimpleOutputOrientedContainerGroup(location, release) + ); + } + + private void requireOutputOrModuleOrientedLocation(Location location) { + if (!location.isOutputLocation() && !location.isModuleOrientedLocation()) { + throw new IllegalArgumentException( + "Location " + location.getName() + " must be output or module-oriented" + ); + } + } + + private void requirePackageOrientedLocation(Location location) { + if (location.isModuleOrientedLocation()) { + throw new IllegalArgumentException( + "Location " + location.getName() + " must be package-oriented" + ); + } + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/AbstractPackageOrientedContainerGroup.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/AbstractPackageOrientedContainerGroup.java new file mode 100644 index 000000000..890fe5151 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/AbstractPackageOrientedContainerGroup.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.jsr199.containers; + +import static java.util.Objects.requireNonNull; + +import io.github.ascopes.jct.jsr199.ModuleLocation; +import io.github.ascopes.jct.jsr199.PathFileObject; +import io.github.ascopes.jct.paths.PathLike; +import io.github.ascopes.jct.utils.Lazy; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.Set; +import javax.tools.JavaFileObject.Kind; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * An abstract base implementation for a group of containers that relate to a specific location. + * + *

This mechanism enables the ability to have locations with more than one path in them, + * which is needed to facilitate the Java compiler's distributed class path, module handling, and + * other important features. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public abstract class AbstractPackageOrientedContainerGroup + implements PackageOrientedContainerGroup { + + private static final Set ARCHIVE_EXTENSIONS = Set.of( + ".zip", + ".jar", + ".war" + ); + + protected final String release; + private final List containers; + private final Lazy classLoaderLazy; + + /** + * Initialize this container group. + * + * @param release the release to use for multi-release JARs. + */ + protected AbstractPackageOrientedContainerGroup(String release) { + this.release = requireNonNull(release, "release"); + + containers = new ArrayList<>(); + classLoaderLazy = new Lazy<>(this::createClassLoader); + } + + @Override + public void addPackage(Container container) { + containers.add(container); + } + + @Override + public void addPackage(PathLike path) { + var actualPath = path.getPath(); + + var archive = ARCHIVE_EXTENSIONS + .stream() + .anyMatch(actualPath.getFileName().toString().toLowerCase(Locale.ROOT)::endsWith); + + var container = archive + ? new JarContainer(getLocation(), path, release) + : new DirectoryContainer(getLocation(), path); + + addPackage(container); + } + + @Override + public boolean contains(PathFileObject fileObject) { + return containers + .stream() + .anyMatch(container -> container.contains(fileObject)); + } + + @Override + public void close() throws IOException { + // Close everything in a best-effort fashion. + var exceptions = new ArrayList(); + + for (var container : containers) { + try { + container.close(); + } catch (IOException ex) { + exceptions.add(ex); + } + } + + try { + classLoaderLazy.destroy(); + } catch (Exception ex) { + exceptions.add(ex); + } + + if (exceptions.size() > 0) { + var ioEx = new IOException("One or more components failed to close"); + exceptions.forEach(ioEx::addSuppressed); + throw ioEx; + } + } + + @Override + public Optional findFile(String path) { + return containers + .stream() + .flatMap(container -> container.findFile(path).stream()) + .findFirst(); + } + + @Override + public ClassLoader getClassLoader() { + return classLoaderLazy.access(); + } + + @Override + public Optional getFileForInput( + String packageName, + String relativeName + ) { + return containers + .stream() + .flatMap(container -> container.getFileForInput(packageName, relativeName).stream()) + .findFirst(); + } + + @Override + public Optional getFileForOutput( + String packageName, + String relativeName + ) { + return containers + .stream() + .flatMap(container -> container.getFileForOutput(packageName, relativeName).stream()) + .findFirst(); + } + + @Override + public Optional getJavaFileForInput( + String className, + Kind kind + ) { + return containers + .stream() + .flatMap(container -> container.getJavaFileForInput(className, kind).stream()) + .findFirst(); + } + + @Override + public Optional getJavaFileForOutput( + String className, + Kind kind + ) { + return containers + .stream() + .flatMap(container -> container.getJavaFileForOutput(className, kind).stream()) + .findFirst(); + } + + @Override + public final List getPackages() { + return Collections.unmodifiableList(containers); + } + + @Override + public Optional> getServiceLoader(Class service) { + var location = getLocation(); + + if (location instanceof ModuleLocation) { + throw new UnsupportedOperationException("Cannot load services from specific modules"); + } + + return Optional.of(ServiceLoader.load(service, classLoaderLazy.access())); + } + + @Override + public Optional inferBinaryName(PathFileObject fileObject) { + return containers + .stream() + .flatMap(container -> container.inferBinaryName(fileObject).stream()) + .findFirst(); + } + + @Override + public boolean isEmpty() { + return containers.isEmpty(); + } + + @Override + public Collection list( + String packageName, + Set kinds, + boolean recurse + ) throws IOException { + var items = new ArrayList(); + + for (var container : containers) { + items.addAll(container.list(packageName, kinds, recurse)); + } + + return items; + } + + protected ContainerClassLoader createClassLoader() { + return new ContainerClassLoader(getLocation(), getPackages()); + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/ClassLoadingFailedException.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/ClassLoadingFailedException.java new file mode 100644 index 000000000..00559374e --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/ClassLoadingFailedException.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.jsr199.containers; + +import static java.util.Objects.requireNonNull; + +import javax.tools.JavaFileManager.Location; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * Exception that is thrown if class loading failed due to an unexpected exception. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public final class ClassLoadingFailedException extends ClassNotFoundException { + + private final String binaryName; + private final Location location; + + /** + * Initialize the exception. + * + * @param binaryName the binary name of the class being loaded. + * @param location the location the class was being loaded from. + * @param cause the reason that the loading failed. + */ + public ClassLoadingFailedException(String binaryName, Location location, Throwable cause) { + super( + String.format( + "Class '%s' failed to load from location '%s': %s", + requireNonNull(binaryName, "binaryName"), + requireNonNull(location, "location").getName(), + requireNonNull(cause).getMessage() + ), + cause + ); + + this.binaryName = binaryName; + this.location = location; + } + + /** + * Get the binary name of the class that failed to be loaded. + * + * @return the binary name. + */ + public String getBinaryName() { + return binaryName; + } + + /** + * Get the location that the class was being loaded from. + * + * @return the location that the class was being loaded from. + */ + public Location getLocation() { + return location; + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/ClassMissingException.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/ClassMissingException.java new file mode 100644 index 000000000..a09bd29b3 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/ClassMissingException.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.jsr199.containers; + +import static java.util.Objects.requireNonNull; + +import javax.tools.JavaFileManager.Location; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * Exception that is thrown if class loading returned no results because the class did not exist on + * the given paths. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public final class ClassMissingException extends ClassNotFoundException { + + private final String binaryName; + private final Location location; + + /** + * Initialize the exception. + * + * @param binaryName the binary name of the class that was not found. + * @param location the location that was being searched for the class. + */ + public ClassMissingException(String binaryName, Location location) { + super( + String.format( + "Class '%s' was not found in location '%s'", + requireNonNull(binaryName, "binaryName"), + requireNonNull(location, "location").getName() + ) + ); + + this.binaryName = binaryName; + this.location = location; + } + + /** + * Get the binary name of the class that failed to be loaded. + * + * @return the binary name. + */ + public String getBinaryName() { + return binaryName; + } + + /** + * Get the location that the class was being loaded from. + * + * @return the location that the class was being loaded from. + */ + public Location getLocation() { + return location; + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/Container.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/Container.java new file mode 100644 index 000000000..02a7be609 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/Container.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.jsr199.containers; + +import io.github.ascopes.jct.jsr199.PathFileObject; +import io.github.ascopes.jct.paths.PathLike; +import java.io.Closeable; +import java.io.IOException; +import java.lang.module.ModuleFinder; +import java.net.URL; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import javax.tools.FileObject; +import javax.tools.JavaFileManager.Location; +import javax.tools.JavaFileObject; +import javax.tools.JavaFileObject.Kind; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * Container that wraps a file path source of some description. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public interface Container extends Closeable { + + /** + * Determine if this container contains the path file object. + * + * @param fileObject the file object to look up. + * @return {@code true} if the object exists in this container, or {@code false} otherwise. + */ + boolean contains(PathFileObject fileObject); + + /** + * Find the physical path to a given string file path. + * + * @param path the path to the file to find. + * @return the path if the file exists, or an empty optional if it does not exist. + */ + Optional findFile(String path); + + /** + * Get the binary data for a class, if it exists. + * + * @param binaryName the binary name of the class. + * @return the binary data, if it exists, otherwise an empty optional. + * @throws IOException if an IO exception occurs. + */ + Optional getClassBinary(String binaryName) throws IOException; + + /** + * Get a {@link FileObject} for reading, if it exists. + * + *

If the file does not exist, an empty optional is returned. + * + * @param packageName the package name of the file. + * @param relativeName the relative name of the file in the package. + * @return the file object, or an empty optional if it does not exist. + */ + Optional getFileForInput( + String packageName, + String relativeName + ); + + /** + * Get a {@link FileObject} for writing. + * + *

If the container is read-only, an empty optional is returned. + * + * @param packageName the package name of the file. + * @param relativeName the relative name of the file in the package. + * @return the file object, or an empty optional if this container is read-only. + */ + Optional getFileForOutput( + String packageName, + String relativeName + ); + + /** + * Get a {@link JavaFileObject} for reading, if it exists. + * + *

If the file does not exist, an empty optional is returned. + * + * @param className the binary name of the class to open. + * @param kind the kind of file to open. + * @return the file object, or an empty optional if it does not exist. + */ + Optional getJavaFileForInput( + String className, + Kind kind + ); + + /** + * Get a {@link JavaFileObject} for writing. + * + *

If the container is read-only, an empty optional is returned. + * + * @param className the binary name of the class to open. + * @param kind the kind of file to open. + * @return the file object, or an empty optional if this container is read-only. + */ + Optional getJavaFileForOutput( + String className, + Kind kind + ); + + /** + * Get the location of this container. + * + * @return the location. + */ + Location getLocation(); + + /** + * Get a module finder for this container. + * + * @return the module finder for this container. + */ + ModuleFinder getModuleFinder(); + + /** + * Get the name of this container for human consumption. + * + * @return the container name. + */ + String getName(); + + /** + * Get the path of the container. + * + * @return the path. + */ + PathLike getPath(); + + /** + * Get a classpath resource for the given resource path if it exists. + * + *

If the resource does not exist, then return an empty optional. + * + * @param resourcePath the path to the resource. + * @return the URL to the resource if it exists, or an empty optional if it does not exist. + * @throws IOException if an IO error occurs looking for the resource. + */ + Optional getResource(String resourcePath) throws IOException; + + /** + * Infer the binary name of a given Java file object. + * + * @param javaFileObject the Java file object to infer the binary name of. + * @return the name, or an empty optional if the file does not exist in this container. + */ + Optional inferBinaryName(PathFileObject javaFileObject); + + /** + * List all the file objects that match the given criteria in this group. + * + * @param packageName the package name to look in. + * @param kinds the kinds of file to look for. + * @param recurse {@code true} to recurse subpackages, {@code false} to only consider the + * given package. + * @return the stream of results. + */ + Collection list( + String packageName, + Set kinds, + boolean recurse + ) throws IOException; + + +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/ContainerClassLoader.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/ContainerClassLoader.java new file mode 100644 index 000000000..8b4c948cf --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/ContainerClassLoader.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.jsr199.containers; + +import static java.util.Objects.requireNonNull; + +import io.github.ascopes.jct.utils.EnumerationAdapter; +import io.github.ascopes.jct.utils.ModulePrefix; +import io.github.ascopes.jct.utils.Nullable; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import javax.tools.JavaFileManager.Location; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class loader that can load from modules and packages held within various implementations of + * {@link Container}. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public class ContainerClassLoader extends ClassLoader { + + static { + ClassLoader.registerAsParallelCapable(); + } + + private static final Logger LOGGER = LoggerFactory.getLogger(ContainerClassLoader.class); + private static final List NO_PACKAGES = List.of(); + private static final Map> NO_MODULES = Map.of(); + + private final Location location; + private final List packageContainers; + private final Map> moduleContainers; + + /** + * Create a container class loader for a set of packages. + * + * @param location the location that the containers are for. + * @param packageContainers the package containers. + */ + public ContainerClassLoader(Location location, List packageContainers) { + this(location, packageContainers, NO_MODULES); + } + + /** + * Create a container class loader for a set of modules. + * + * @param location the location that the containers are for. + * @param moduleContainers the module containers. + */ + public ContainerClassLoader( + Location location, + Map> moduleContainers + ) { + this(location, NO_PACKAGES, moduleContainers); + } + + /** + * Create a container class loader for a set of packages and modules. + * + * @param location the location that the containers are for. + * @param packageContainers the package containers. + * @param moduleContainers the module containers. + */ + public ContainerClassLoader( + Location location, + List packageContainers, + Map> moduleContainers + ) { + this.location = requireNonNull(location, "location"); + this.packageContainers = requireNonNull(packageContainers, "packageContainers"); + this.moduleContainers = requireNonNull(moduleContainers, "moduleContainers"); + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + var maybeModule = ModulePrefix.tryExtract(name); + + if (maybeModule.isPresent()) { + var module = maybeModule.get(); + var moduleContainers = this.moduleContainers.get(module.getModuleName()); + + if (moduleContainers != null) { + var moduleClass = loadClassFrom(moduleContainers, module.getRest()); + + if (moduleClass != null) { + return moduleClass; + } + } + } + + var packageClass = loadClassFrom(packageContainers, name); + + if (packageClass != null) { + return packageClass; + } + + throw new ClassMissingException(name, location); + } + + @Nullable + @Override + protected URL findResource(String resourcePath) { + try { + var resources = findResources(resourcePath); + return resources.hasMoreElements() + ? resources.nextElement() + : null; + } catch (IOException ex) { + // We have to return null in this case. I don't think this is overly useful though, so + // log some diagnostic information to indicate what the issue is. + LOGGER.warn( + "Failed to find resource '{}' in {}. Will return null for this case", + resourcePath, + location.getName(), + ex + ); + return null; + } + } + + @Override + protected Enumeration findResources(String resourcePath) throws IOException { + resourcePath = removeLeadingForwardSlash(resourcePath); + + var resources = new ArrayList(); + + var maybeModule = ModulePrefix.tryExtract(resourcePath); + if (maybeModule.isPresent()) { + var module = maybeModule.get(); + var moduleContainers = this.moduleContainers.get(module.getModuleName()); + + if (moduleContainers != null) { + var trimmedResourcePath = module.getRest(); + + for (var container : moduleContainers) { + container.getResource(trimmedResourcePath).ifPresent(resource -> { + LOGGER.trace( + "Found resource '{}' in module container {} for module {} within {}", + resource, + container, + module.getModuleName(), + location.getName() + ); + + resources.add(resource); + }); + } + } + } + + for (var container : packageContainers) { + container.getResource(resourcePath).ifPresent(resource -> { + LOGGER.trace( + "Found resource '{}' in package container {} within {}", + resource, + container, + location.getName() + ); + + resources.add(resource); + }); + } + + return new EnumerationAdapter<>(resources.iterator()); + } + + @Nullable + private Class loadClassFrom( + List containers, + String binaryName + ) throws ClassLoadingFailedException { + try { + for (var container : containers) { + var clazz = container + .getClassBinary(binaryName) + .map(data -> defineClass(null, data, 0, data.length)); + + if (clazz.isPresent()) { + LOGGER.trace("Found class {} in {} for {}", binaryName, container, location.getName()); + return clazz.get(); + } + } + + LOGGER.trace("Class {} not found in {}", binaryName, location.getName()); + + return null; + } catch (IOException ex) { + throw new ClassLoadingFailedException(binaryName, location, ex); + } + } + + private static String removeLeadingForwardSlash(String name) { + var index = 0; + while (index < name.length() && name.charAt(index) == '/') { + ++index; + } + + return name.substring(index); + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/ContainerGroup.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/ContainerGroup.java new file mode 100644 index 000000000..d41c97f91 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/ContainerGroup.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.jsr199.containers; + +import io.github.ascopes.jct.jsr199.PathFileObject; +import java.io.Closeable; +import java.util.Optional; +import java.util.ServiceLoader; +import javax.tools.JavaFileManager.Location; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * Base container group interface. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public interface ContainerGroup extends Closeable { + + /** + * Determine whether this group contains the given file object anywhere. + * + * @param fileObject the file object to look for. + * @return {@code true} if the file object is contained in this group, or {@code false} otherwise. + */ + boolean contains(PathFileObject fileObject); + + /** + * Get a class loader for this group of containers. + * + *

Note that adding additional containers to this group after accessing this class loader + * may result in the class loader being destroyed or re-created. + * + * @return the class loader. + */ + ClassLoader getClassLoader(); + + /** + * Get the location of this container group. + * + * @return the location. + */ + Location getLocation(); + + /** + * Get a service loader for the given service class. + * + * @param service the service class to get. + * @param the service class type. + * @return the service loader, if this location supports loading plugins. If not, an empty + * optional is returned instead. + * @throws UnsupportedOperationException if the container group does not provide this + * functionality. + */ + Optional> getServiceLoader(Class service); +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/DirectoryContainer.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/DirectoryContainer.java new file mode 100644 index 000000000..6a49b32e0 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/DirectoryContainer.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.jsr199.containers; + +import static java.util.Objects.requireNonNull; + +import io.github.ascopes.jct.jsr199.PathFileObject; +import io.github.ascopes.jct.paths.PathLike; +import io.github.ascopes.jct.utils.FileUtils; +import io.github.ascopes.jct.utils.StringUtils; +import java.io.IOException; +import java.lang.module.ModuleFinder; +import java.net.URL; +import java.nio.file.FileVisitOption; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import javax.tools.JavaFileManager.Location; +import javax.tools.JavaFileObject.Kind; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * A container that wraps a known directory of files. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public class DirectoryContainer implements Container { + + private final Location location; + private final PathLike root; + private final String name; + + /** + * Initialize this container. + * + * @param location the location. + * @param root the root directory to hold. + */ + public DirectoryContainer(Location location, PathLike root) { + this.location = requireNonNull(location, "location"); + this.root = requireNonNull(root, "root"); + name = root.toString(); + } + + @Override + public void close() throws IOException { + // Do nothing for this implementation. We have nothing to close. + } + + @Override + public boolean contains(PathFileObject fileObject) { + var path = fileObject.getFullPath(); + return path.startsWith(root.getPath()) && Files.isRegularFile(path); + } + + @Override + public Optional findFile(String path) { + if (path.startsWith("/")) { + throw new IllegalArgumentException("Absolute paths are not supported (got '" + path + "')"); + } + + return Optional + .of(FileUtils.relativeResourceNameToPath(root.getPath(), path)) + .filter(Files::isRegularFile); + } + + @Override + public Optional getClassBinary(String binaryName) throws IOException { + var path = FileUtils.binaryNameToPath(root.getPath(), binaryName, Kind.CLASS); + return Files.isRegularFile(path) + ? Optional.of(Files.readAllBytes(path)) + : Optional.empty(); + } + + @Override + public Optional getFileForInput( + String packageName, + String relativeName + ) { + return Optional + .of(FileUtils.resourceNameToPath(root.getPath(), packageName, relativeName)) + .filter(Files::isRegularFile) + .map(path -> new PathFileObject(location, root.getPath(), path)); + } + + @Override + public Optional getFileForOutput( + String packageName, + String relativeName + ) { + return Optional + .of(FileUtils.resourceNameToPath(root.getPath(), packageName, relativeName)) + .map(path -> new PathFileObject(location, root.getPath(), path)); + } + + @Override + public Optional getJavaFileForInput( + String binaryName, + Kind kind + ) { + return Optional + .of(FileUtils.binaryNameToPath(root.getPath(), binaryName, kind)) + .filter(Files::isRegularFile) + .map(path -> new PathFileObject(location, root.getPath(), path)); + } + + @Override + public Optional getJavaFileForOutput( + String className, + Kind kind + ) { + return Optional + .of(FileUtils.binaryNameToPath(root.getPath(), className, kind)) + .map(path -> new PathFileObject(location, root.getPath(), path)); + } + + @Override + public Location getLocation() { + return location; + } + + @Override + public ModuleFinder getModuleFinder() { + return null; + } + + @Override + public String getName() { + return name; + } + + @Override + public PathLike getPath() { + return root; + } + + @Override + public Optional getResource(String resourcePath) throws IOException { + var path = FileUtils.relativeResourceNameToPath(root.getPath(), resourcePath); + // Getting a URL of a directory within a JAR breaks the JAR file system implementation + // completely. + return Files.isRegularFile(path) + ? Optional.of(path.toUri().toURL()) + : Optional.empty(); + } + + @Override + public Optional inferBinaryName(PathFileObject javaFileObject) { + return javaFileObject.getFullPath().startsWith(root.getPath()) + ? Optional.of(FileUtils.pathToBinaryName(javaFileObject.getRelativePath())) + : Optional.empty(); + } + + @Override + public Collection list( + String packageName, + Set kinds, + boolean recurse + ) throws IOException { + var maxDepth = recurse ? Integer.MAX_VALUE : 1; + + var basePath = FileUtils.packageNameToPath(root.getPath(), packageName); + + try (var walker = Files.walk(basePath, maxDepth, FileVisitOption.FOLLOW_LINKS)) { + return walker + .filter(FileUtils.fileWithAnyKind(kinds)) + .map(path -> new PathFileObject(location, root.getPath(), path)) + .collect(Collectors.toUnmodifiableList()); + } catch (NoSuchFileException ex) { + return List.of(); + } + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{uri=" + StringUtils.quoted(root.getUri()) + "}"; + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/JarContainer.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/JarContainer.java new file mode 100644 index 000000000..9809be77b --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/JarContainer.java @@ -0,0 +1,341 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.jsr199.containers; + +import static java.util.Objects.requireNonNull; + +import io.github.ascopes.jct.jsr199.PathFileObject; +import io.github.ascopes.jct.paths.NioPath; +import io.github.ascopes.jct.paths.PathLike; +import io.github.ascopes.jct.utils.FileUtils; +import io.github.ascopes.jct.utils.IoExceptionUtils; +import io.github.ascopes.jct.utils.Lazy; +import io.github.ascopes.jct.utils.Nullable; +import io.github.ascopes.jct.utils.StringUtils; +import java.io.IOException; +import java.lang.module.ModuleFinder; +import java.net.URL; +import java.nio.file.FileSystem; +import java.nio.file.FileVisitOption; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.ProviderNotFoundException; +import java.nio.file.spi.FileSystemProvider; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import javax.tools.JavaFileManager.Location; +import javax.tools.JavaFileObject.Kind; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * Container that wraps a JAR path and allows reading the contents of the JAR in-memory lazily. + * + *

Unlike the regular JAR file system provider, more than one of these + * containers can exist pointing to the same physical JAR at once without concurrency issues + * occurring. + * + *

The JAR will be opened lazily when needed, and then kept open until {@link #close() closed} + * explicitly. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public final class JarContainer implements Container { + + private final Location location; + private final PathLike jarPath; + private final String release; + private final Lazy holder; + + /** + * Initialize this JAR container. + * + * @param location the location. + * @param jarPath the path to the JAR to open. + * @param release the release version to use for {@code Multi-Release} JARs. + */ + public JarContainer(Location location, PathLike jarPath, String release) { + this.location = requireNonNull(location, "location"); + this.jarPath = requireNonNull(jarPath, "jarPath"); + this.release = requireNonNull(release, "release"); + holder = new Lazy<>(() -> IoExceptionUtils.uncheckedIo(PackageFileSystemHolder::new)); + } + + @Override + public void close() throws IOException { + holder.ifInitialized(PackageFileSystemHolder::close); + } + + @Override + public boolean contains(PathFileObject fileObject) { + var path = fileObject.getFullPath(); + for (var root : holder.access().getRootDirectories()) { + return path.startsWith(root) && Files.isRegularFile(path); + } + return false; + } + + @Override + public Optional findFile(String path) { + if (path.startsWith("/")) { + throw new IllegalArgumentException("Absolute paths are not supported (got '" + path + "')"); + } + + for (var root : holder.access().getRootDirectories()) { + var fullPath = FileUtils.relativeResourceNameToPath(root, path); + if (Files.isRegularFile(fullPath)) { + return Optional.of(fullPath); + } + } + + return Optional.empty(); + } + + @Override + public Optional getClassBinary(String binaryName) throws IOException { + var packageName = FileUtils.binaryNameToPackageName(binaryName); + var packageDir = holder.access().getPackage(packageName); + + if (packageDir == null) { + return Optional.empty(); + } + + var className = FileUtils.binaryNameToSimpleClassName(binaryName); + var classPath = FileUtils.simpleClassNameToPath(packageDir.getPath(), className, Kind.CLASS); + + return Files.isRegularFile(classPath) + ? Optional.of(Files.readAllBytes(classPath)) + : Optional.empty(); + } + + @Override + public Optional getFileForInput(String packageName, + String relativeName) { + return Optional + .ofNullable(holder.access().getPackage(packageName)) + .map(PathLike::getPath) + .map(packageDir -> FileUtils.relativeResourceNameToPath(packageDir, relativeName)) + .filter(Files::isRegularFile) + .map(path -> new PathFileObject(location, path.getRoot(), path)); + } + + @Override + public Optional getFileForOutput(String packageName, + String relativeName) { + // This JAR is read-only. + return Optional.empty(); + } + + @Override + public Optional getJavaFileForInput(String binaryName, Kind kind) { + var packageName = FileUtils.binaryNameToPackageName(binaryName); + var className = FileUtils.binaryNameToSimpleClassName(binaryName); + + return Optional + .ofNullable(holder.access().getPackage(packageName)) + .map(PathLike::getPath) + .map(packageDir -> FileUtils.simpleClassNameToPath(packageDir, className, kind)) + .filter(Files::isRegularFile) + .map(path -> new PathFileObject(location, path.getRoot(), path)); + } + + @Override + public Optional getJavaFileForOutput(String className, + Kind kind) { + // This JAR is read-only. + return Optional.empty(); + } + + @Override + public Location getLocation() { + return location; + } + + @Override + public ModuleFinder getModuleFinder() { + var paths = holder + .access() + .getRootDirectoriesStream() + .toArray(Path[]::new); + return ModuleFinder.of(paths); + } + + @Override + public String getName() { + return jarPath.toString(); + } + + @Override + public PathLike getPath() { + return jarPath; + } + + @Override + public Optional getResource(String resourcePath) throws IOException { + // TODO(ascopes): could we index these resources ahead-of-time in the lazy initializer? + for (var root : holder.access().getRootDirectories()) { + var path = FileUtils.relativeResourceNameToPath(root, resourcePath); + if (Files.isRegularFile(path)) { + return Optional.of(path.toUri().toURL()); + } + } + + return Optional.empty(); + } + + @Override + public Optional inferBinaryName(PathFileObject javaFileObject) { + // For some reason, converting a zip entry to a URI gives us a scheme of `jar://file://`, but + // we cannot then parse the URI back to a path without removing the `file://` bit first. Since + // we assume we always have instances of PathJavaFileObject here, let's just cast to that and + // get the correct path immediately. + var fullPath = javaFileObject.getFullPath(); + + for (var root : holder.access().getRootDirectories()) { + if (fullPath.startsWith(root)) { + return Optional.of(FileUtils.pathToBinaryName(javaFileObject.getRelativePath())); + } + } + + return Optional.empty(); + } + + @Override + public Collection list( + String packageName, + Set kinds, + boolean recurse + ) throws IOException { + var packageDir = holder.access().getPackages().get(packageName); + + if (packageDir == null) { + return List.of(); + } + + var maxDepth = recurse ? Integer.MAX_VALUE : 1; + + var items = new ArrayList(); + + var packagePath = packageDir.getPath(); + + try (var walker = Files.walk(packagePath, maxDepth, FileVisitOption.FOLLOW_LINKS)) { + walker + .filter(FileUtils.fileWithAnyKind(kinds)) + .map(path -> new PathFileObject(location, path.getRoot(), path)) + .forEach(items::add); + } + + return items; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{uri=" + StringUtils.quoted(jarPath.getUri()) + "}"; + } + + /** + * Wrapper around a set of packages and a file system that can be opened lazily. + */ + private class PackageFileSystemHolder { + + private final Map packages; + private final FileSystem fileSystem; + + private PackageFileSystemHolder() throws IOException { + // It turns out that we can open more than one ZIP file system pointing to the + // same file at once, but we cannot do this with the JAR file system itself. + // This is an issue since it hinders our ability to run tests in parallel where multiple tests + // might be trying to read the same JAR at once. + // + // This means we have to do a little of hacking around to get this to work how we need it to. + // Remember that JARs are just glorified zip folders. + + // Set the multi-release flag to enable reading META-INF/release/* files correctly if the + // MANIFEST.MF specifies the Multi-Release entry as true. + // Turns out the JDK implementation of the ZipFileSystem handles this for us. + packages = new HashMap<>(); + + var actualJarPath = jarPath.getPath(); + + var env = Map.of( + "releaseVersion", release, + "multi-release", release + ); + + // So, for some reason. I cannot make more than one instance of a ZipFileSystem + // if I pass a URI in here. If I pass a Path in here instead, then I can make + // multiple copies of it in memory. No idea why this is the way it is, but it + // appears to be how the JavacFileManager in the JDK can make itself run in parallel + // safely. While in Rome, I guess. + fileSystem = getJarFileSystemProvider().newFileSystem(actualJarPath, env); + + // Index packages ahead-of-time to improve performance. + for (var root : fileSystem.getRootDirectories()) { + try (var walker = Files.walk(root)) { + walker + .filter(Files::isDirectory) + .map(root::relativize) + .forEach(path -> packages.put( + FileUtils.pathToBinaryName(path), + new NioPath(root.resolve(path)) + )); + } + } + } + + private void close() throws IOException { + packages.clear(); + fileSystem.close(); + } + + private Map getPackages() { + return packages; + } + + @Nullable + private PathLike getPackage(String name) { + return packages.get(name); + } + + private Iterable getRootDirectories() { + return fileSystem.getRootDirectories(); + } + + private Stream getRootDirectoriesStream() { + return StreamSupport.stream(fileSystem.getRootDirectories().spliterator(), false); + } + } + + private static FileSystemProvider getJarFileSystemProvider() { + for (var fsProvider : FileSystemProvider.installedProviders()) { + if (fsProvider.getScheme().equals("jar")) { + return fsProvider; + } + } + + throw new ProviderNotFoundException("jar"); + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/ModuleOrientedContainerGroup.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/ModuleOrientedContainerGroup.java new file mode 100644 index 000000000..8c086a981 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/ModuleOrientedContainerGroup.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.jsr199.containers; + +import io.github.ascopes.jct.jsr199.ModuleLocation; +import io.github.ascopes.jct.paths.PathLike; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.tools.JavaFileManager.Location; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * A container group implementation that holds zero or more modules. + * + *

These modules can be accessed by their module name. Each one holds a separate group of + * containers, and has the ability to produce a custom class loader. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public interface ModuleOrientedContainerGroup extends ContainerGroup { + + /** + * Add a container to this group. + * + * @param module the module that the container is for. + * @param container the container to add. + */ + void addModule(String module, Container container); + + /** + * Add a path to this group for a module. + * + * @param module the name of the module that this is for. + * @param path the path to add. + */ + void addModule(String module, PathLike path); + + /** + * Get the {@link PackageOrientedContainerGroup} for a given module name, creating it if it does + * not yet exist. + * + * @param moduleName the module name to look up. + * @return the container group. + */ + PackageOrientedContainerGroup forModule(String moduleName); + + /** + * Get the module-oriented location. + * + * @return the module-oriented location. + */ + @Override + Location getLocation(); + + /** + * Get all locations that are modules. + * + * @return the locations that are modules. + */ + List> getLocationsForModules(); + + /** + * Get the module container groups in this group. + * + * @return the container groups. + */ + Map getModules(); + + /** + * Determine if this group contains a given module. + * + * @param location the module location to look for. + * @return {@code true} if present, or {@code false} if not. + */ + boolean hasLocation(ModuleLocation location); +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/OutputOrientedContainerGroup.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/OutputOrientedContainerGroup.java new file mode 100644 index 000000000..c830cd939 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/OutputOrientedContainerGroup.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.jsr199.containers; + +import javax.tools.JavaFileManager.Location; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * A base definition for an output-oriented container group. + * + *

These can behave as if they are module-oriented, or non-module-oriented. + * It is down to the implementation to mediate access between modules and their files. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public interface OutputOrientedContainerGroup + extends PackageOrientedContainerGroup, ModuleOrientedContainerGroup { + + /** + * Get the output-oriented location. + * + * @return the output-oriented location. + */ + @Override + Location getLocation(); +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/PackageOrientedContainerGroup.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/PackageOrientedContainerGroup.java new file mode 100644 index 000000000..cd386298e --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/PackageOrientedContainerGroup.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.jsr199.containers; + +import io.github.ascopes.jct.jsr199.PathFileObject; +import io.github.ascopes.jct.paths.PathLike; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import javax.tools.FileObject; +import javax.tools.JavaFileManager.Location; +import javax.tools.JavaFileObject; +import javax.tools.JavaFileObject.Kind; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * Interface describing a group of containers. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public interface PackageOrientedContainerGroup extends ContainerGroup { + + /** + * Add a container to this group. + * + * @param container the container to add. + */ + void addPackage(Container container); + + /** + * Add a path to this group. + * + *

Note that this will destroy the {@link #getClassLoader() classloader} if one is already + * allocated. + * + * @param path the path to add. + */ + void addPackage(PathLike path); + + /** + * Find the first occurrence of a given path to a file. + * + * @param path the path to the file to find. + * @return the first occurrence of the path in this group, or an empty optional if not found. + */ + Optional findFile(String path); + + /** + * Get a {@link FileObject} that can have content read from it. + * + *

This will return an empty optional if no file is found. + * + * @param packageName the package name of the file to read. + * @param relativeName the relative name of the file to read. + * @return the file object, or an empty optional if the file is not found. + */ + Optional getFileForInput( + String packageName, + String relativeName + ); + + /** + * Get a {@link FileObject} that can have content written to it for the given file. + * + *

This will attempt to write to the first writeable path in this group. An empty optional + * will be returned if no writeable paths exist in this group. + * + * @param packageName the name of the package the file is in. + * @param relativeName the relative name of the file within the package. + * @return the {@link FileObject} to write to, or an empty optional if this group has no paths + * that can be written to. + */ + Optional getFileForOutput( + String packageName, + String relativeName + ); + + /** + * Get a {@link JavaFileObject} that can have content written to it for the given file. + * + *

This will attempt to write to the first writeable path in this group. An empty optional + * will be returned if no writeable paths exist in this group. + * + * @param className the binary name of the class to read. + * @param kind the kind of file to read. + * @return the {@link JavaFileObject} to write to, or an empty optional if this group has no paths + * that can be written to. + */ + Optional getJavaFileForInput( + String className, + Kind kind + ); + + /** + * Get a {@link JavaFileObject} that can have content written to it for the given class. + * + *

This will attempt to write to the first writeable path in this group. An empty optional + * will be returned if no writeable paths exist in this group. + * + * @param className the name of the class. + * @param kind the kind of the class file. + * @return the {@link JavaFileObject} to write to, or an empty optional if this group has no paths + * that can be written to. + */ + Optional getJavaFileForOutput( + String className, + Kind kind + ); + + /** + * Get the package-oriented location that this group of paths is for. + * + * @return the package-oriented location. + */ + Location getLocation(); + + /** + * Get the package containers in this group. + * + * @return the containers. + */ + Iterable getPackages(); + + /** + * Try to infer the binary name of a given file object. + * + * @param fileObject the file object to infer the binary name for. + * @return the binary name if known, or an empty optional otherwise. + */ + Optional inferBinaryName(PathFileObject fileObject); + + /** + * Determine if this group has no paths registered. + * + * @return {@code true} if no paths are registered. {@code false} if paths are registered. + */ + boolean isEmpty(); + + /** + * List all the file objects that match the given criteria in this group. + * + * @param packageName the package name to look in. + * @param kinds the kinds of file to look for. + * @param recurse {@code true} to recurse subpackages, {@code false} to only consider the + * given package. + * @return an iterable of resultant file objects. + * @throws IOException if the file lookup fails due to an IO exception. + */ + Collection list( + String packageName, + Set kinds, + boolean recurse + ) throws IOException; +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/SimpleModuleOrientedContainerGroup.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/SimpleModuleOrientedContainerGroup.java new file mode 100644 index 000000000..53801e3c6 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/SimpleModuleOrientedContainerGroup.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.jsr199.containers; + +import static java.util.Objects.requireNonNull; + +import io.github.ascopes.jct.jsr199.ModuleLocation; +import io.github.ascopes.jct.jsr199.PathFileObject; +import io.github.ascopes.jct.paths.PathLike; +import io.github.ascopes.jct.utils.Lazy; +import io.github.ascopes.jct.utils.StringUtils; +import java.io.IOException; +import java.lang.module.ModuleFinder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.stream.Collectors; +import javax.tools.JavaFileManager.Location; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * Simple implementation of a {@link ModuleOrientedContainerGroup}. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public class SimpleModuleOrientedContainerGroup implements ModuleOrientedContainerGroup { + + private final Location location; + private final Map modules; + private final String release; + private final Lazy classLoaderLazy; + + /** + * Initialize this container group. + * + * @param location the module-oriented location. + * @param release the release to use for Multi-Release JARs. + * @throws UnsupportedOperationException if the {@code location} is not module-oriented, or is + * output-oriented. + */ + public SimpleModuleOrientedContainerGroup(Location location, String release) { + this.location = requireNonNull(location, "location"); + this.release = requireNonNull(release, "release"); + + if (location.isOutputLocation()) { + throw new UnsupportedOperationException( + "Cannot use output-oriented locations such as " + + StringUtils.quoted(location.getName()) + + " with this container" + ); + } + + if (!location.isModuleOrientedLocation()) { + throw new UnsupportedOperationException( + "Cannot use package-oriented locations such as " + + StringUtils.quoted(location.getName()) + + " with this container" + ); + } + + modules = new HashMap<>(); + classLoaderLazy = new Lazy<>(this::createClassLoader); + } + + @Override + @SuppressWarnings("resource") + public void addModule(String module, Container container) { + forModule(module).addPackage(container); + } + + @Override + @SuppressWarnings("resource") + public void addModule(String module, PathLike path) { + forModule(module).addPackage(path); + } + + @Override + public void close() throws IOException { + var exceptions = new ArrayList(); + for (var group : modules.values()) { + try { + group.close(); + } catch (IOException ex) { + exceptions.add(ex); + } + } + + if (!exceptions.isEmpty()) { + var ex = new IOException("One or more module groups failed to close"); + exceptions.forEach(ex::addSuppressed); + throw ex; + } + } + + @Override + @SuppressWarnings("SuspiciousMethodCalls") + public boolean contains(PathFileObject fileObject) { + return Optional + .ofNullable(modules.get(fileObject.getLocation())) + .map(module -> module.contains(fileObject)) + .orElse(false); + } + + @Override + public ClassLoader getClassLoader() { + return classLoaderLazy.access(); + } + + @Override + public Location getLocation() { + return location; + } + + @Override + public List> getLocationsForModules() { + return List.of(Set.copyOf(modules.keySet())); + } + + @Override + public Map getModules() { + return Map.copyOf(modules); + } + + @Override + public boolean hasLocation(ModuleLocation location) { + return modules.containsKey(location); + } + + @Override + public Optional> getServiceLoader(Class service) { + getClass().getModule().addUses(service); + + var finders = modules + .values() + .stream() + .map(SimpleModuleOrientedModuleContainerGroup::getPackages) + .flatMap(List::stream) + .map(Container::getModuleFinder) + .toArray(ModuleFinder[]::new); + + var composedFinder = ModuleFinder.compose(finders); + var bootLayer = ModuleLayer.boot(); + var config = bootLayer + .configuration() + .resolveAndBind(ModuleFinder.of(), composedFinder, Collections.emptySet()); + + var layer = bootLayer + .defineModulesWithOneLoader(config, ClassLoader.getSystemClassLoader()); + + return Optional.of(ServiceLoader.load(layer, service)); + } + + @Override + public PackageOrientedContainerGroup forModule(String moduleName) { + return modules + .computeIfAbsent( + new ModuleLocation(location, moduleName), + SimpleModuleOrientedModuleContainerGroup::new + ); + } + + private ContainerClassLoader createClassLoader() { + var moduleMapping = modules + .entrySet() + .stream() + .collect(Collectors.toUnmodifiableMap( + entry -> entry.getKey().getModuleName(), + entry -> entry.getValue().getPackages() + )); + + return new ContainerClassLoader(location, moduleMapping); + } + + /** + * Wrapper around a location that lacks the constraints that + * {@link SimplePackageOrientedContainerGroup} imposes. + */ + private class SimpleModuleOrientedModuleContainerGroup + extends AbstractPackageOrientedContainerGroup { + + private final Location location; + + private SimpleModuleOrientedModuleContainerGroup(Location location) { + super(SimpleModuleOrientedContainerGroup.this.release); + this.location = requireNonNull(location, "location"); + } + + @Override + public Location getLocation() { + return location; + } + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/SimpleOutputOrientedContainerGroup.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/SimpleOutputOrientedContainerGroup.java new file mode 100644 index 000000000..b136ebe82 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/SimpleOutputOrientedContainerGroup.java @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.jsr199.containers; + +import static java.util.Objects.requireNonNull; + +import io.github.ascopes.jct.jsr199.ModuleLocation; +import io.github.ascopes.jct.jsr199.PathFileObject; +import io.github.ascopes.jct.paths.PathLike; +import io.github.ascopes.jct.paths.SubPath; +import io.github.ascopes.jct.utils.IoExceptionUtils; +import io.github.ascopes.jct.utils.ModulePrefix; +import io.github.ascopes.jct.utils.StringUtils; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import javax.tools.JavaFileManager.Location; +import javax.tools.JavaFileObject.Kind; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * A group of containers that relate to a specific output location. + * + *

These can contain packages and modules of packages together, and thus + * are slightly more complicated internally as a result. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public class SimpleOutputOrientedContainerGroup + extends AbstractPackageOrientedContainerGroup + implements OutputOrientedContainerGroup { + + private final Location location; + private final Map modules; + + /** + * Initialize this container group. + * + * @param location the location of the group. + * @param release the release version. + */ + public SimpleOutputOrientedContainerGroup(Location location, String release) { + super(release); + this.location = requireNonNull(location, "location"); + modules = new HashMap<>(); + + if (location.isModuleOrientedLocation()) { + throw new UnsupportedOperationException( + "Cannot use module-oriented locations such as " + + StringUtils.quoted(location.getName()) + + " with this container" + ); + } + + if (!location.isOutputLocation()) { + throw new UnsupportedOperationException( + "Cannot use non-output locations such as " + + StringUtils.quoted(location.getName()) + + " with this container" + ); + } + } + + @Override + @SuppressWarnings("resource") + public void addModule(String module, Container container) { + forModule(module).addPackage(container); + } + + @Override + @SuppressWarnings("resource") + public void addModule(String module, PathLike path) { + forModule(module).addPackage(path); + } + + @Override + public boolean contains(PathFileObject fileObject) { + if (location instanceof ModuleLocation) { + return Optional + .ofNullable(modules.get(location)) + .map(module -> module.contains(fileObject)) + .orElse(false); + } + + return super.contains(fileObject); + } + + @Override + @SuppressWarnings("resource") + public Optional findFile(String path) { + return ModulePrefix + .tryExtract(path) + .flatMap(prefix -> forModule(prefix.getModuleName()) + .findFile(prefix.getRest())) + .or(() -> super.findFile(path)); + } + + @Override + @SuppressWarnings("resource") + public Optional getFileForInput( + String packageName, + String relativeName + ) { + // TODO(ascopes): can we have modules conceptually in this method call? + return ModulePrefix + .tryExtract(packageName) + .flatMap(prefix -> forModule(prefix.getModuleName()) + .getFileForInput(prefix.getModuleName(), relativeName)) + .or(() -> super.getFileForInput(packageName, relativeName)); + } + + @Override + @SuppressWarnings("resource") + public Optional getFileForOutput(String packageName, String relativeName) { + // TODO(ascopes): can we have modules conceptually in this method call? + return ModulePrefix + .tryExtract(packageName) + .flatMap(prefix -> forModule(prefix.getModuleName()) + .getFileForOutput(prefix.getRest(), relativeName)) + .or(() -> super.getFileForOutput(packageName, relativeName)); + } + + @Override + @SuppressWarnings("resource") + public Optional getJavaFileForInput(String className, Kind kind) { + return ModulePrefix + .tryExtract(className) + .flatMap(prefix -> forModule(prefix.getModuleName()) + .getJavaFileForInput(prefix.getRest(), kind)) + .or(() -> super.getJavaFileForInput(className, kind)); + } + + @Override + @SuppressWarnings("resource") + public Optional getJavaFileForOutput(String className, Kind kind) { + return ModulePrefix + .tryExtract(className) + .flatMap(prefix -> forModule(prefix.getModuleName()) + .getJavaFileForOutput(prefix.getRest(), kind)) + .or(() -> super.getJavaFileForOutput(className, kind)); + } + + @Override + public PackageOrientedContainerGroup forModule(String moduleName) { + return modules.computeIfAbsent( + new ModuleLocation(location, moduleName), + moduleLocation -> { + // For output locations, we only need the first root. We then just put a subdirectory + // in there, as it reduces the complexity of this tenfold and means we don't have to + // worry about creating more in-memory locations on the fly. + var group = new SimpleOutputOrientedModuleContainerGroup(moduleLocation); + var path = new SubPath(getPackages().iterator().next().getPath(), moduleName); + IoExceptionUtils.uncheckedIo(() -> Files.createDirectories(path.getPath())); + group.addPackage(path); + return group; + }); + } + + @Override + public Location getLocation() { + return location; + } + + @Override + public List> getLocationsForModules() { + return List.of(Set.copyOf(modules.keySet())); + } + + @Override + public Map getModules() { + return null; + } + + @Override + public boolean hasLocation(ModuleLocation location) { + return modules.containsKey(location); + } + + @Override + protected ContainerClassLoader createClassLoader() { + var moduleMapping = modules + .entrySet() + .stream() + .collect(Collectors.toUnmodifiableMap( + entry -> entry.getKey().getModuleName(), + entry -> entry.getValue().getPackages() + )); + + return new ContainerClassLoader(location, getPackages(), moduleMapping); + } + + /** + * Wrapper around a location that lacks the constraints that + * {@link SimplePackageOrientedContainerGroup} imposes. + */ + private class SimpleOutputOrientedModuleContainerGroup + extends AbstractPackageOrientedContainerGroup { + + private final Location location; + + private SimpleOutputOrientedModuleContainerGroup(Location location) { + super(SimpleOutputOrientedContainerGroup.this.release); + this.location = requireNonNull(location, "location"); + } + + @Override + public Location getLocation() { + return location; + } + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/SimplePackageOrientedContainerGroup.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/SimplePackageOrientedContainerGroup.java new file mode 100644 index 000000000..fb9de7aa1 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/SimplePackageOrientedContainerGroup.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.jsr199.containers; + +import static java.util.Objects.requireNonNull; + +import io.github.ascopes.jct.utils.StringUtils; +import javax.tools.JavaFileManager.Location; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * A group of containers that relate to a specific location. + * + *

This mechanism enables the ability to have locations with more than one path in them, + * which is needed to facilitate the Java compiler's distributed class path, module handling, and + * other important features. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public class SimplePackageOrientedContainerGroup extends AbstractPackageOrientedContainerGroup { + + private final Location location; + + /** + * Initialize this group. + * + * @param location the location of the group. + * @param release the release version to use for handling {@code Multi-Release} JARs in this + * location. + */ + public SimplePackageOrientedContainerGroup(Location location, String release) { + super(release); + + this.location = requireNonNull(location, "location"); + + if (location.isOutputLocation()) { + throw new UnsupportedOperationException( + "Cannot use output locations such as " + + StringUtils.quoted(location.getName()) + + " with this container" + ); + } + + if (location.isModuleOrientedLocation()) { + throw new UnsupportedOperationException( + "Cannot use module-oriented locations such as " + + StringUtils.quoted(location.getName()) + + " with this container" + ); + } + } + + @Override + public Location getLocation() { + return location; + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/package-info.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/package-info.java new file mode 100644 index 000000000..d2f23b8a1 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/containers/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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. + */ + +/** + * Implementations of containers and container-groups, which provide a means of mapping a + * {@link javax.tools.JavaFileManager file manager} to a group of distributed paths in multiple + * {@link javax.tools.JavaFileManager.Location locations}. + */ +package io.github.ascopes.jct.jsr199.containers; diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/ForwardingDiagnostic.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/diagnostics/ForwardingDiagnostic.java similarity index 81% rename from java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/ForwardingDiagnostic.java rename to java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/diagnostics/ForwardingDiagnostic.java index daff8f468..2bf40be7b 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/ForwardingDiagnostic.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/diagnostics/ForwardingDiagnostic.java @@ -14,10 +14,12 @@ * limitations under the License. */ -package io.github.ascopes.jct.compilers; +package io.github.ascopes.jct.jsr199.diagnostics; +import static java.util.Objects.requireNonNull; + +import io.github.ascopes.jct.utils.Nullable; import java.util.Locale; -import java.util.Objects; import javax.tools.Diagnostic; import org.apiguardian.api.API; import org.apiguardian.api.API.Status; @@ -43,7 +45,7 @@ public abstract class ForwardingDiagnostic implements Diagnostic { * @param original the original diagnostic to delegate to. */ protected ForwardingDiagnostic(Diagnostic original) { - this.original = Objects.requireNonNull(original); + this.original = requireNonNull(original, "original"); } @Override @@ -51,6 +53,7 @@ public Kind getKind() { return original.getKind(); } + @Nullable @Override public S getSource() { return original.getSource(); @@ -81,16 +84,25 @@ public long getColumnNumber() { return original.getColumnNumber(); } + @Nullable @Override public String getCode() { return original.getCode(); } @Override - public String getMessage(Locale locale) { + public String getMessage(@Nullable Locale locale) { return original.getMessage(locale); } + /** + * {@inheritDoc} + * + *

Note: this representation may vary depending on the compiler that + * initialized it.

+ * + * @return the string representation of the diagnostic. + */ @Override public String toString() { return original.toString(); diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/TeeWriter.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/diagnostics/TeeWriter.java similarity index 98% rename from java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/TeeWriter.java rename to java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/diagnostics/TeeWriter.java index ff2d40fa0..dff2872e9 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/TeeWriter.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/diagnostics/TeeWriter.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.github.ascopes.jct.compilers; +package io.github.ascopes.jct.jsr199.diagnostics; import static java.util.Objects.requireNonNull; diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/TraceDiagnostic.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/diagnostics/TraceDiagnostic.java similarity index 98% rename from java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/TraceDiagnostic.java rename to java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/diagnostics/TraceDiagnostic.java index 45d8f347e..95cc45e08 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/TraceDiagnostic.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/diagnostics/TraceDiagnostic.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.github.ascopes.jct.compilers; +package io.github.ascopes.jct.jsr199.diagnostics; import static java.util.Objects.requireNonNull; diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/TracingDiagnosticListener.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/diagnostics/TracingDiagnosticListener.java similarity index 98% rename from java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/TracingDiagnosticListener.java rename to java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/diagnostics/TracingDiagnosticListener.java index 4527f5dae..88a76deb5 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/TracingDiagnosticListener.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/diagnostics/TracingDiagnosticListener.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.github.ascopes.jct.compilers; +package io.github.ascopes.jct.jsr199.diagnostics; import static java.util.Objects.requireNonNull; diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/diagnostics/package-info.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/diagnostics/package-info.java new file mode 100644 index 000000000..7bf2a481b --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/diagnostics/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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. + */ + +/** + * Support for collecting and representing diagnostics from compiler implementations. + */ +package io.github.ascopes.jct.jsr199.diagnostics; diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/package-info.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/package-info.java new file mode 100644 index 000000000..067b59d34 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/jsr199/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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. + */ + +/** + * Support for the JSR-199 API. + */ +package io.github.ascopes.jct.jsr199; diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/AbstractCompilersProvider.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/AbstractCompilersProvider.java new file mode 100644 index 000000000..9a0b650a6 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/AbstractCompilersProvider.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.Compilable; +import java.util.function.IntFunction; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; + +/** + * Internal base for defining a compiler-supplying arguments provider for Junit Jupiter + * parameterised test support. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.INTERNAL) +abstract class AbstractCompilersProvider implements ArgumentsProvider { + + private final IntFunction> compilerSupplier; + private final int minCompilerVersionWithoutModules; + private final int minCompilerVersionWithModules; + private final int maxCompilerVersion; + + // Configured values by JUnit. + private int minVersion; + private int maxVersion; + + AbstractCompilersProvider( + IntFunction> compilerSupplier, + int minCompilerVersionWithoutModules, + int minCompilerVersionWithModules, + int maxCompilerVersion + ) { + this.compilerSupplier = compilerSupplier; + this.minCompilerVersionWithoutModules = minCompilerVersionWithoutModules; + this.minCompilerVersionWithModules = minCompilerVersionWithModules; + this.maxCompilerVersion = maxCompilerVersion; + minVersion = Integer.MIN_VALUE; + maxVersion = Integer.MAX_VALUE; + } + + @Override + public Stream provideArguments(ExtensionContext context) { + return IntStream + .rangeClosed(minVersion, maxVersion) + .mapToObj(compilerSupplier) + .map(Arguments::of); + } + + final void configure(int min, int max, boolean modules) { + min = Math.max(min, modules ? minCompilerVersionWithModules : minCompilerVersionWithoutModules); + max = Math.min(max, maxCompilerVersion); + + if (min > max) { + throw new IllegalArgumentException( + "Cannot set min version to a version higher than the max version" + ); + } + + minVersion = min; + maxVersion = max; + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/EcjCompilers.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/EcjCompilers.java new file mode 100644 index 000000000..e7842cc7a --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/EcjCompilers.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.ecj.EcjCompiler; +import io.github.ascopes.jct.junit.EcjCompilers.EcjCompilersProvider; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.support.AnnotationConsumer; + +/** + * Annotation that can be applied to a {@link org.junit.jupiter.params.ParameterizedTest} to enable + * passing in a range of {@link EcjCompiler} instances with specific configured versions as the + * first parameter. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +@ArgumentsSource(EcjCompilersProvider.class) +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE}) +public @interface EcjCompilers { + + /** + * Minimum version to use (inclusive). + */ + int minVersion() default Integer.MIN_VALUE; + + /** + * Maximum version to use (inclusive). + */ + int maxVersion() default Integer.MAX_VALUE; + + /** + * Whether we need to support modules or not. + * + *

Setting this to true will skip any versions of the compiler that do not support JPMS + * modules. + * + * @return {@code true} if we need to support modules, or {@code false} if we do not. + */ + boolean modules() default false; + + /** + * Argument provider for the {@link EcjCompilers} annotation. + * + * @author Ashley Scopes + * @since 0.0.1 + */ + @API(since = "0.0.1", status = Status.INTERNAL) + final class EcjCompilersProvider extends AbstractCompilersProvider implements + AnnotationConsumer { + + EcjCompilersProvider() { + super( + version -> new EcjCompiler("ECJ " + version).release(version), + 8, + 9, + EcjCompiler.getMaxVersion() + ); + } + + @Override + public void accept(EcjCompilers ecjCompilers) { + configure(ecjCompilers.minVersion(), ecjCompilers.maxVersion(), ecjCompilers.modules()); + } + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/JavacCompilers.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/JavacCompilers.java new file mode 100644 index 000000000..cd1eb10ce --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/JavacCompilers.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.javac.JavacCompiler; +import io.github.ascopes.jct.junit.JavacCompilers.JavacCompilersProvider; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.support.AnnotationConsumer; + +/** + * Annotation that can be applied to a {@link org.junit.jupiter.params.ParameterizedTest} to enable + * passing in a range of {@link JavacCompiler} instances with specific configured versions as the + * first parameter. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +@ArgumentsSource(JavacCompilersProvider.class) +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE}) +public @interface JavacCompilers { + + /** + * Minimum version to use (inclusive). + */ + int minVersion() default Integer.MIN_VALUE; + + /** + * Maximum version to use (inclusive). + */ + int maxVersion() default Integer.MAX_VALUE; + + /** + * Whether we need to support modules or not. + * + *

Setting this to true will skip any versions of the compiler that do not support JPMS + * modules. + * + * @return {@code true} if we need to support modules, or {@code false} if we do not. + */ + boolean modules() default false; + + /** + * Argument provider for the {@link JavacCompilers} annotation. + * + * @author Ashley Scopes + * @since 0.0.1 + */ + @API(since = "0.0.1", status = Status.INTERNAL) + final class JavacCompilersProvider + extends AbstractCompilersProvider + implements AnnotationConsumer { + + JavacCompilersProvider() { + super( + version -> new JavacCompiler("Javac " + version).release(version), + 8, + 9, + JavacCompiler.getMaxVersion() + ); + } + + @Override + public void accept(JavacCompilers javacCompilers) { + configure( + javacCompilers.minVersion(), + javacCompilers.maxVersion(), + javacCompilers.modules() + ); + } + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/package-info.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/package-info.java new file mode 100644 index 000000000..efdb788a1 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/junit/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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. + */ + +/** + * Additional functionality to simplify writing tests with Junit. + */ +package io.github.ascopes.jct.junit; diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/DirectoryClassLoader.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/DirectoryClassLoader.java deleted file mode 100644 index 16d99cc62..000000000 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/DirectoryClassLoader.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright (C) 2022 Ashley Scopes - * - * 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.paths; - -import io.github.ascopes.jct.intern.EnumerationAdapter; -import io.github.ascopes.jct.intern.StringUtils; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Collection; -import java.util.Enumeration; -import java.util.LinkedHashSet; -import java.util.Objects; -import org.apiguardian.api.API; -import org.apiguardian.api.API.Status; - - -/** - * A classloader for multiple {@link Path} types, similar to {@link java.net.URLClassLoader}, but - * that can only apply to one or more directory trees. - * - * @author Ashley Scopes - * @since 0.0.1 - */ -@API(since = "0.0.1", status = Status.EXPERIMENTAL) -public class DirectoryClassLoader extends ClassLoader { - - static { - registerAsParallelCapable(); - } - - private final Collection dirs; - - /** - * Initialize the classloader, using the system classloader as a parent. - * - * @param dirs the paths of directories to use. - */ - public DirectoryClassLoader(Iterable dirs) { - Objects.requireNonNull(dirs); - - // Retain insertion order. - this.dirs = new LinkedHashSet<>(); - dirs.forEach(this.dirs::add); - } - - @Override - public String toString() { - return "DirectoryClassLoader{" - + "dirs=" + StringUtils.quotedIterable(dirs) - + "}"; - } - - @Override - protected Class findClass(String name) throws ClassNotFoundException { - var pathName = name.replace('.', '/') + ".class"; - - for (var root : dirs) { - var fullPath = root.resolve(pathName); - if (Files.isRegularFile(fullPath)) { - try { - var classData = Files.readAllBytes(fullPath); - return defineClass(null, classData, 0, classData.length); - } catch (IOException ex) { - throw new ClassNotFoundException("Failed to read resource " + fullPath, ex); - } - } - } - - throw new ClassNotFoundException(name); - } - - /** - * Find a resource in the paths. - * - * @param name the name of the resource. - * @return the URL to the resource, or {@code null} if not found. - */ - @Override - protected URL findResource(String name) { - return dirs - .stream() - .map(root -> root.resolve(name)) - .filter(Files::isRegularFile) - .map(this::pathToUrl) - .findFirst() - .orElse(null); - } - - /** - * Find resources with the given name in the paths. - * - * @param name the name of the resource. - * @return an enumeration of the URLs of any resources found that match the given name. - */ - @Override - protected Enumeration findResources(String name) throws IOException { - try { - var iterator = dirs - .stream() - .map(root -> root.resolve(name)) - .filter(Files::isRegularFile) - .map(this::pathToUrl) - .iterator(); - return new EnumerationAdapter<>(iterator); - } catch (IllegalArgumentException ex) { - throw new IOException("Failed to read one or more resources", ex); - } - } - - /** - * Convert a path to a URL, throwing an unchecked exception if the conversion fails. - * - * @param path the path to convert. - * @return the URL. - * @throws IllegalArgumentException if the URL conversion fails. - */ - private URL pathToUrl(Path path) { - try { - return path.toUri().toURL(); - } catch (MalformedURLException ex) { - throw new IllegalArgumentException("Cannot convert path to URL", ex); - } - } -} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/NioPath.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/NioPath.java new file mode 100644 index 000000000..3ff02d811 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/NioPath.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.paths; + +import static java.util.Objects.requireNonNull; + +import io.github.ascopes.jct.utils.StringUtils; +import java.net.URI; +import java.nio.file.Path; +import java.util.Objects; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * A wrapper around a {@link Path Java NIO Path} that makes it compatible with {@link PathLike}. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public final class NioPath implements PathLike { + + private final Path path; + private final URI uri; + + /** + * Initialize this path. + * + * @param path the NIO path to wrap. + */ + public NioPath(Path path) { + this.path = requireNonNull(path, "path"); + uri = path.toUri(); + } + + @Override + public Path getPath() { + return path; + } + + @Override + public URI getUri() { + return uri; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof NioPath)) { + return false; + } + + var that = (NioPath) other; + + return uri.equals(that.uri); + } + + @Override + public int hashCode() { + return Objects.hash(uri); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{path=" + StringUtils.quoted(uri) + "}"; + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/ParentPathLocationManager.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/ParentPathLocationManager.java deleted file mode 100644 index 364f88f82..000000000 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/ParentPathLocationManager.java +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright (C) 2022 Ashley Scopes - * - * 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.paths; - -import io.github.ascopes.jct.intern.StringUtils; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; -import javax.tools.FileObject; -import javax.tools.JavaFileManager.Location; -import org.apiguardian.api.API; -import org.apiguardian.api.API.Status; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A path location manager that also supports having nested modules. - * - * @author Ashley Scopes - * @since 0.0.1 - */ -@API(since = "0.0.1", status = Status.EXPERIMENTAL) -public class ParentPathLocationManager extends PathLocationManager { - - private static final Logger LOGGER = LoggerFactory.getLogger(ParentPathLocationManager.class); - - private final Map modules; - - /** - * Initialize the manager. - * - * @param factory the {@link PathJavaFileObject} factory to use. - * @param location the location to represent. - */ - public ParentPathLocationManager(PathJavaFileObjectFactory factory, Location location) { - super(factory, location); - modules = new HashMap<>(); - } - - /** - * Get the module location for the given module name. - * - * @param moduleName the module name. - * @return the module location. - */ - public ModuleLocation getModuleLocationFor(String moduleName) { - // These are lightweight opaque descriptors, so duplicates should not really matter. - // We only create the backing file trees for these as-needed, too. - return new ModuleLocation(getLocation(), moduleName); - } - - /** - * Get the module location for the given file object, if it exists in any of the given roots. - * - * @param fileObject the file object to get the module location for. - * @return the module location if known, or an empty optional otherwise. - */ - public Optional getModuleLocationFor(FileObject fileObject) { - // TODO(ascopes): can we get non-path file objects here? - var path = ((PathJavaFileObject) fileObject).getPath(); - - return getRoots() - .stream() - .filter(path::startsWith) - .map(root -> root.relativize(path)) - .map(Path::iterator) - .map(Iterator::next) - .map(Path::toString) - .filter(modules::containsKey) - .map(this::getModuleLocationFor) - .findFirst(); - } - - /** - * Get or create the module location manager for the given module name. - * - * @param moduleName the module name. - * @return the manager if found, or an empty optional if it does not exist. - */ - public Optional getModuleLocationManager(String moduleName) { - LOGGER.trace("Getting location manager for module '{}' nested within {}", moduleName, this); - return Optional.ofNullable(modules.get(moduleName)); - } - - /** - * Get or create the module location manager for the given module name. - * - * @param moduleName the module name. - * @return the manager. - * @throws IllegalArgumentException if this object is already for a module. - */ - public PathLocationManager getOrCreateModuleLocationManager(String moduleName) { - LOGGER.trace( - "Getting/creating location manager for module '{}' nested within {}", - moduleName, - this - ); - return modules.computeIfAbsent( - moduleName, - this::buildLocationManagerForModule - ); - } - - /** - * Determine if this manager has a nested module manager. - * - * @param moduleName the name of the module. - * @return {@code true} if the nested manager is present, or {@code false} otherwise. - */ - public boolean hasModuleLocationManager(String moduleName) { - return modules.containsKey(moduleName); - } - - /** - * Get all module locations within this location. - * - * @return the module locations as a set. - */ - public Set listLocationsForModules() { - return modules - .values() - .stream() - .map(PathLocationManager::getLocation) - .collect(Collectors.toSet()); - } - - @Override - public String toString() { - var location = getLocation(); - return getClass().getSimpleName() + "{" - + "location=" + StringUtils.quoted(location.getName()) + ", " - + "modules=" + StringUtils.quotedIterable(modules.keySet()) - + "}"; - } - - - @Override - protected void registerPath(Path path) { - if (isPathCapableOfHoldingModules(path)) { - // If this path can contain modules, then we should convert that to a module location - // and store the path there. This just prevent any mistakes where we accidentally put a - // module in a module oriented location rather than in the corresponding submodule. - try (var stream = Files.list(path)) { - stream - .peek(next -> LOGGER.trace("Checking if {} is a source module", next)) - .filter(Files::isDirectory) - .filter(next -> Files.isRegularFile(next.resolve("module-info.java"))) - .forEach(module -> { - var name = module.getFileName().toString(); - LOGGER.debug("Found candidate module {} at {}", name, module); - // Make sure you call .addPath and not .addPaths. Paths are themselves iterable - // and it will cause otherwise confusing behaviour. - getOrCreateModuleLocationManager(name).addPath(module); - }); - } catch (IOException ex) { - throw new UncheckedIOException("Failed to read files in " + path, ex); - } - } - - super.registerPath(path); - } - - private PathLocationManager buildLocationManagerForModule( - String moduleName - ) { - var location = getLocation(); - var moduleLocation = new ModuleLocation(location, moduleName); - var moduleManager = new PathLocationManager(getPathJavaFileObjectFactory(), moduleLocation); - - var paths = getRoots(); - - // For nested modules, if we are an output location, then add a directory in the parent - // location manager for the module. This allows implicitly creating output sources as - // we need them. We don't bother making a whole new virtual file system here as it is slower, - // and more complicated to handle properly. - if (location.isOutputLocation()) { - if (paths.isEmpty()) { - LOGGER.trace( - "No paths for location {} exist, so no module directory will be made for {}", - location.getName(), - moduleName - ); - } else { - var modulePath = paths.iterator().next().resolve(moduleName); - LOGGER.debug("Creating module directory for {} at {}", moduleLocation, modulePath); - - try { - Files.createDirectories(modulePath); - } catch (IOException ex) { - throw new UncheckedIOException("Failed to create " + modulePath, ex); - } - } - } - - getRoots() - .stream() - .map(root -> root.resolve(moduleName)) - .peek(root -> LOGGER.trace("Adding {} to {}", root, moduleManager)) - .forEach(moduleManager::addPath); - - return moduleManager; - } - - private boolean isPathCapableOfHoldingModules(Path path) { - var location = getLocation(); - return (location.isModuleOrientedLocation() || location.isOutputLocation()) - && Files.isDirectory(path); - } -} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/PathJavaFileManager.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/PathJavaFileManager.java deleted file mode 100644 index 0a3f736f5..000000000 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/PathJavaFileManager.java +++ /dev/null @@ -1,343 +0,0 @@ -/* - * Copyright (C) 2022 Ashley Scopes - * - * 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.paths; - -import java.io.IOException; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.Objects; -import java.util.ServiceLoader; -import java.util.Set; -import java.util.function.Function; -import javax.tools.FileObject; -import javax.tools.JavaFileManager; -import javax.tools.JavaFileObject; -import javax.tools.JavaFileObject.Kind; -import org.apiguardian.api.API; -import org.apiguardian.api.API.Status; - - -/** - * Implementation of a {@link JavaFileManager} that works on a set of paths from any loaded - * {@link java.nio.file.FileSystem}s. - * - * @author Ashley Scopes - * @since 0.0.1 - */ -@API(since = "0.0.1", status = Status.EXPERIMENTAL) -@SuppressWarnings("RedundantThrows") // We keep the API contract to prevent breaking changes. -public class PathJavaFileManager implements JavaFileManager { - - private final PathLocationRepository repository; - private volatile boolean closed; - - /** - * Initialize the manager. - * - * @param repository the path repository to use. - */ - public PathJavaFileManager(PathLocationRepository repository) { - this.repository = repository; - closed = false; - } - - @Override - public void close() throws IOException { - closed = true; - } - - @Override - public boolean contains(Location location, FileObject fileObject) - throws IllegalStateException, IOException { - assertOpen(); - - return repository - .getManager(location) - .map(manager -> manager.contains(fileObject)) - .orElse(false); - } - - @Override - public void flush() throws IOException { - assertOpen(); - } - - @Override - public ClassLoader getClassLoader(Location location) { - return repository - .getManager(location) - .map(PathLocationManager::getClassLoader) - .orElse(null); - } - - @Override - public ServiceLoader getServiceLoader(Location location, Class service) - throws IOException { - return repository - .getManager(location) - .map(manager -> manager.getServiceLoader(service)) - .orElseThrow(() -> formatException( - NoSuchElementException::new, - "Cannot find a service loader for %s in location %s (%s)", - service.getName(), - location.getName(), - location.getClass().getName() - )); - } - - @Override - public FileObject getFileForInput(Location location, String packageName, String relativeName) - throws IOException { - assertKnownLocation(location); - - return repository - .getManager(location) - .flatMap(manager -> manager.getFileForInput(packageName, relativeName)) - .orElse(null); - } - - @Override - public FileObject getFileForOutput( - Location location, - String packageName, - String relativeName, - FileObject sibling - ) throws IOException { - assertOutputLocation(location); - assertKnownLocation(location); - - return repository - .getOrCreateManager(location) - .getFileForOutput(packageName, relativeName) - .orElse(null); - } - - @Override - public JavaFileObject getJavaFileForInput( - Location location, - String className, - Kind kind - ) throws IOException { - assertKnownLocation(location); - - return repository - .getManager(location) - .flatMap(manager -> manager.getJavaFileForInput(className, kind)) - .orElse(null); - } - - @Override - public JavaFileObject getJavaFileForOutput( - Location location, - String className, - Kind kind, - FileObject sibling - ) throws IOException { - assertOutputLocation(location); - - return repository - .getOrCreateManager(location) - .getJavaFileForOutput(className, kind) - .orElse(null); - } - - @Override - public Location getLocationForModule(Location location, String moduleName) throws IOException { - assertOpen(); - assertModuleOrientedOrOutputLocation(location); - assertKnownLocation(location); - - return new ModuleLocation(location, moduleName); - } - - @Override - public Location getLocationForModule(Location location, JavaFileObject fileObject) - throws IOException { - assertOpen(); - assertModuleOrientedOrOutputLocation(location); - - return repository - .getManager(location) - .map(ParentPathLocationManager.class::cast) - .orElseThrow(() -> unknownLocation(location)) - .getModuleLocationFor(fileObject) - .orElseThrow(() -> formatException( - IllegalArgumentException::new, - "File %s is not known to any modules within location %s (%s)", - fileObject.toUri(), - location.getName(), - location.getClass().getName() - )); - } - - @Override - public boolean handleOption(String current, Iterator remaining) { - assertOpen(); - return false; - } - - @Override - public boolean hasLocation(Location location) { - return repository - .getManager(location) - .isPresent(); - } - - - @Override - public String inferBinaryName(Location location, JavaFileObject file) { - return repository - .getManager(location) - .flatMap(manager -> manager.inferBinaryName(file)) - .orElse(null); - } - - @Override - public String inferModuleName(Location location) throws IOException { - assertKnownLocation(location); - assertModuleLocation(location); - - return ((ModuleLocation) location).getModuleName(); - } - - @Override - public boolean isSameFile(FileObject a, FileObject b) { - assertPathJavaFileObject(a); - assertPathJavaFileObject(b); - - return Objects.equals(a.toUri(), b.toUri()); - } - - @Override - public int isSupportedOption(String option) { - return -1; - } - - @Override - public Iterable list( - Location location, - String packageName, - Set kinds, - boolean recurse - ) throws IOException { - var manager = repository.getManager(location); - - return manager.isPresent() - ? manager.get().list(packageName, kinds, recurse) - : Collections.emptyList(); - } - - @Override - public Iterable> listLocationsForModules(Location location) throws IOException { - - assertOpen(); - assertModuleOrientedOrOutputLocation(location); - - return repository - .getManager(location) - .map(ParentPathLocationManager.class::cast) - .map(ParentPathLocationManager::listLocationsForModules) - .map(List::of) - .orElseGet(Collections::emptyList); - } - - @Override - public String toString() { - return "PathJavaFileManager{}"; - } - - private void assertKnownLocation(Location location) { - if (!repository.containsManager(location)) { - throw unknownLocation(location); - } - } - - private void assertModuleLocation(Location location) { - if (!(location instanceof ModuleLocation)) { - throw formatException( - IllegalArgumentException::new, - "Location %s (%s) is not a module location. This is disallowed here.", - location.getName(), - location.getClass().getName() - ); - } - } - - private void assertModuleOrientedOrOutputLocation(Location location) { - if (!location.isModuleOrientedLocation() && !location.isOutputLocation()) { - throw formatException( - IllegalArgumentException::new, - "Location %s (%s) is not a module-oriented or output location. This is disallowed here.", - location.getName(), - location.getClass().getName() - ); - } - } - - private void assertOutputLocation(Location location) { - if (!location.isOutputLocation()) { - throw formatException( - IllegalArgumentException::new, - "Location %s (%s) is not an output location. This is disallowed here.", - location.getName(), - location.getClass().getName() - ); - } - } - - private void assertPathJavaFileObject(FileObject fileObject) { - if (!(fileObject instanceof PathJavaFileObject)) { - throw formatException( - IllegalArgumentException::new, - "File object %s (%s pointing at %s) was not created by this file manager", - fileObject.getName(), - fileObject.getClass().getName(), - fileObject.toUri() - ); - } - } - - private void assertOpen() { - if (closed) { - throw formatException( - IllegalStateException::new, - "File manager is closed" - ); - } - } - - private IllegalArgumentException unknownLocation(Location location) { - return formatException( - IllegalArgumentException::new, - "Location %s (%s) is not known to this file manager", - location.getName(), - location.getClass().getName() - ); - } - - private static T formatException( - Function initializer, - String template, - Object... args - ) { - return args.length > 0 - ? initializer.apply(String.format(template, args)) - : initializer.apply(template); - } -} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/PathJavaFileObjectFactory.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/PathJavaFileObjectFactory.java deleted file mode 100644 index 79a488c79..000000000 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/PathJavaFileObjectFactory.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright (C) 2022 Ashley Scopes - * - * 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.paths; - -import static java.util.Objects.requireNonNull; - -import java.nio.charset.Charset; -import java.nio.file.Path; -import java.util.Collections; -import java.util.Optional; -import java.util.SortedMap; -import java.util.TreeMap; -import javax.tools.JavaFileManager.Location; -import javax.tools.JavaFileObject.Kind; - -/** - * Factory for creating {@link PathJavaFileObject} instances consistently, with a customizable - * charset. - * - *

This charset can be adjusted once initialized as needed. - * - * @author Ashley Scopes - * @since 0.0.1 - */ -public class PathJavaFileObjectFactory { - - // Determine this on startup to allow inclusion of new extensions that may be added in the - // future, potentially. - private static final SortedMap EXTENSIONS_TO_KINDS; - - static { - var kinds = new TreeMap(String::compareToIgnoreCase); - for (var kind : Kind.values()) { - if (kind != Kind.OTHER) { - kinds.put(kind.extension, kind); - } - } - EXTENSIONS_TO_KINDS = Collections.unmodifiableSortedMap(kinds); - } - - private volatile Charset charset; - - /** - * Initialize this factory. - * - * @param charset the charset to use. - */ - public PathJavaFileObjectFactory(Charset charset) { - this.charset = requireNonNull(charset); - } - - /** - * Get the charset being used for this factory to read and write files with. - * - * @return the charset. - */ - public Charset getCharset() { - return charset; - } - - /** - * Change the charset to use for subsequent calls. - * - * @param charset the charset to change to. - */ - public void setCharset(Charset charset) { - this.charset = requireNonNull(charset); - } - - /** - * Create a {@link PathJavaFileObject} without knowing the given name. - * - * @param location the location of the object. - * @param path the path to the object. - * @return the Java file object to use. - */ - public PathJavaFileObject create(Location location, Path path) { - return create(location, path, path.toString()); - } - - /** - * Create a {@link PathJavaFileObject}. - * - * @param location the location of the object. - * @param path the path to the object. - * @param givenName the name that the user gave the file. - * @return the Java file object to use. - */ - public PathJavaFileObject create(Location location, Path path, String givenName) { - var kind = guessKind(path); - return new PathJavaFileObject(location, path, givenName, kind, charset); - } - - private static Kind guessKind(Path path) { - var fileName = path.getFileName().toString(); - var dotIndex = Math.max(fileName.lastIndexOf('.'), 0); - - var extension = fileName.substring(dotIndex); - - return Optional - .ofNullable(EXTENSIONS_TO_KINDS.get(extension)) - .orElse(Kind.OTHER); - } -} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/PathLike.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/PathLike.java new file mode 100644 index 000000000..0e6d95b63 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/PathLike.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.paths; + +import java.net.URI; +import java.nio.file.FileSystem; +import java.nio.file.Path; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * A path-like object that can provide a {@link Path Java NIO Path}. + * + *

Not only does this enable us to have different types of path with varying behaviour, but + * we can also use this to enforce that other references related to the internal path are kept alive + * for as long as the path-like object itself is kept alive. + * + *

This becomes very useful for {@link RamPath}, which keeps a RAM-based {@link FileSystem} + * alive until it is garbage collected, or the {@link RamPath#close()} operation is called. The + * mechanism enables cleaning up of resources implicitly without resource-tidying logic polluting + * the user's test cases. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public interface PathLike { + + /** + * Get the {@link Path Java NIO Path} for this path-like object. + * + * @return the path. + */ + Path getPath(); + + /** + * Get a URI representation of this path-like object. + * + * @return the URI. + */ + URI getUri(); +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/PathLocationManager.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/PathLocationManager.java deleted file mode 100644 index b2c7a3e4c..000000000 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/PathLocationManager.java +++ /dev/null @@ -1,678 +0,0 @@ -/* - * Copyright (C) 2022 Ashley Scopes - * - * 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.paths; - -import static io.github.ascopes.jct.intern.IoExceptionUtils.uncheckedIo; -import static io.github.ascopes.jct.intern.IoExceptionUtils.wrapWithUncheckedIoException; -import static java.util.Objects.requireNonNull; - -import io.github.ascopes.jct.intern.AsyncResourceCloser; -import io.github.ascopes.jct.intern.Lazy; -import io.github.ascopes.jct.intern.PlatformLinkStrategy; -import io.github.ascopes.jct.intern.RecursiveDeleter; -import io.github.ascopes.jct.intern.StringSlicer; -import io.github.ascopes.jct.intern.StringUtils; -import java.io.Closeable; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.lang.module.ModuleFinder; -import java.lang.ref.Cleaner; -import java.nio.file.FileSystem; -import java.nio.file.FileSystemNotFoundException; -import java.nio.file.FileVisitOption; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.spi.FileSystemProvider; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.ServiceLoader; -import java.util.Set; -import java.util.function.Predicate; -import java.util.stream.Stream; -import javax.tools.FileObject; -import javax.tools.JavaFileManager.Location; -import javax.tools.JavaFileObject; -import javax.tools.JavaFileObject.Kind; -import org.apiguardian.api.API; -import org.apiguardian.api.API.Status; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - - -/** - * Manager of paths for a specific compiler location. - * - *

This provides access to file objects within any of the paths, as well as the ability - * to construct classloaders for the paths as needed. - * - * @author Ashley Scopes - * @since 0.0.1 - */ -@API(since = "0.0.1", status = Status.EXPERIMENTAL) -public class PathLocationManager implements Iterable { - - private static final Cleaner CLEANER = Cleaner.create(); - private static final Logger LOGGER = LoggerFactory.getLogger(PathLocationManager.class); - private static final StringSlicer PACKAGE_SPLITTER = new StringSlicer("."); - - private static final Set JAR_FILE_EXTENSIONS = Set.of( - ".jar", - ".war" - ); - - private final PathJavaFileObjectFactory factory; - private final Location location; - private final Set roots; - private final Lazy classLoader; - private final PlatformLinkStrategy platformLinkStrategy; - - // We use this to keep the references alive while the manager is alive, but we persist these - // outside this context, as the user may wish to reuse these file systems across multiple tests - // or otherwise. When the last reference to a RamPath is destroyed, and the RamPath becomes - // phantom-reachable, the garbage collector will reclaim the file system resource held within. - @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") - private final Set inMemoryDirectories; // lgtm [java/unused-container] - - // We open JARs as additional file systems as needed, and discard them when this manager gets - // discarded. To prevent parallel tests failing to open the same JAR file system at the same time, - // we use symbolic links to vary the name of the JAR first. - private final Map jarFileSystems; - - /** - * Initialize the manager. - * - * @param factory the {@link PathJavaFileObject} factory to use. - * @param location the location to represent. - */ - @SuppressWarnings("ThisEscapedInObjectConstruction") - public PathLocationManager(PathJavaFileObjectFactory factory, Location location) { - LOGGER.trace("Initializing for location {} with factory {}", location, factory); - - this.factory = requireNonNull(factory); - this.location = requireNonNull(location); - roots = new LinkedHashSet<>(); - classLoader = new Lazy<>(() -> new DirectoryClassLoader(roots)); - platformLinkStrategy = new PlatformLinkStrategy(System.getProperties()); - inMemoryDirectories = new HashSet<>(); - jarFileSystems = new HashMap<>(); - CLEANER.register(this, new AsyncResourceCloser(jarFileSystems)); - } - - /** - * Iterate over the paths to assert on. - * - * @return the iterator. - */ - @Override - public Iterator iterator() { - return roots.iterator(); - } - - /** - * Add a path to the manager, if it has not already been added. - * - *

This will destroy the existing classloader, if it already exists. - * - * @param path the path to add. - */ - public void addPath(Path path) { - LOGGER.debug( - "Adding paths {} to {} for location {}", - path, - getClass().getSimpleName(), - location - ); - registerPath(path); - destroyClassLoader(); - } - - // !!! BUG REGRESSION WARNING FOR THIS API !!!: - // DO NOT REPLACE COLLECTION WITH ITERABLE! THIS WOULD MAKE DIFFERENCES BETWEEN - // PATH AND COLLECTIONS OF PATHS DIFFICULT TO DISTINGUISH, SINCE PATHS ARE THEMSELVES - // ITERABLES OF PATHS! - - /** - * Add multiple paths to the manager, if they have not already been added. - * - *

This will destroy the existing classloader, if it already exists. - * - * @param paths the paths to add. - */ - public void addPaths(Collection paths) { - // Don't expand paths if this was incorrectly called with a single path, since paths themselves - // are iterables of paths. - LOGGER.debug( - "Adding paths {} to {} for location {}", - paths, - getClass().getSimpleName(), - location - ); - for (var path : paths) { - registerPath(path); - } - destroyClassLoader(); - } - - /** - * Add an in-memory directory to the manager, if it has not already been added. - * - *

This will destroy the existing classloader, if it already exists. - * - * @param path the path to add. - */ - public void addRamPath(RamPath path) { - LOGGER.debug( - "Registering {} to {} for location {}", - path, - getClass().getSimpleName(), - location - ); - registerPath(path.getPath()); - - // Keep the reference alive. - inMemoryDirectories.add(path); - destroyClassLoader(); - } - - /** - * Add in-memory directories to the manager, if it has not already been added. - * - *

This will destroy the existing classloader, if it already exists. - * - * @param paths the paths to add. - */ - public void addRamPaths(Collection paths) { - LOGGER.debug( - "Registering {} to {} for location {}", - paths, - getClass().getSimpleName(), - location - ); - for (var path : paths) { - registerPath(path.getPath()); - // Keep the reference alive. - inMemoryDirectories.add(path); - } - destroyClassLoader(); - } - - /** - * Determine if this manager contains the given file object anywhere. - * - * @param fileObject the file object to look for. - * @return {@code true} if present, {@code false} otherwise. - */ - public boolean contains(FileObject fileObject) { - // TODO(ascopes): can we get non-path file objects here? - var path = ((PathJavaFileObject) fileObject).getPath(); - - // While we could just return `Files.isRegularFile` from the start, - // we need to make sure the path is one of the roots in the location. - // Otherwise, we could give a false-positive. - for (var root : roots) { - if (path.startsWith(root)) { - return Files.isRegularFile(path); - } - } - - return false; - } - - /** - * Get the full path for a given string path to a file by finding the first occurrence where the - * given path exists as a file. - * - * @param path the path to resolve. - * @return the first full path that ends with the given path that is an existing file, or an empty - * optional if no results were found. - * @throws IllegalArgumentException if an absolute-style path is provided. - */ - public Optional findFile(String path) { - for (var root : roots) { - var fullPath = root.resolve(path); - - if (Files.exists(fullPath)) { - return Optional.of(fullPath); - } - } - - return Optional.empty(); - } - - /** - * Get a classloader for this location manager. - * - *

This will initialize a classloader if it has not been initialized since the last path was - * added. - * - * @return the class loader. - */ - public ClassLoader getClassLoader() { - return classLoader.access(); - } - - /** - * Find an existing file object with the given package and relative file name. - * - * @param packageName the package name. - * @param relativeName the relative file name. - * @return the file, or an empty optional if not found. - */ - public Optional getFileForInput(String packageName, String relativeName) { - var relativePathParts = packageNameToRelativePathParts(packageName); - for (var root : roots) { - var path = resolveNested(root, relativePathParts).resolve(relativeName); - if (Files.isRegularFile(path)) { - return Optional.of(factory.create(location, path, root.relativize(path).toString())); - } - } - - return Optional.empty(); - } - - /** - * Get or create a file for output. - * - *

This will always use the first path that was registered. - * - * @param packageName the package to create the file in. - * @param relativeName the relative name of the file to create. - * @return the file object for output, or an empty optional if no paths existed to place it in. - */ - public Optional getFileForOutput(String packageName, String relativeName) { - return roots - .stream() - .findFirst() - .flatMap(root -> { - var relativePathParts = packageNameToRelativePathParts(packageName); - var path = resolveNested(root, relativePathParts).resolve(relativeName); - return Optional.of(factory.create(location, path, root.relativize(path).toString())); - }); - } - - /** - * Find an existing file object with the given class name and kind. - * - * @param className the class name. - * @param kind the kind. - * @return the file, or an empty optional if not found. - */ - public Optional getJavaFileForInput(String className, Kind kind) { - var relativePathParts = classNameToRelativePathParts(className, kind.extension); - for (var root : roots) { - var path = resolveNested(root, relativePathParts); - if (Files.isRegularFile(path)) { - return Optional.of(factory.create(location, path, className)); - } - } - - return Optional.empty(); - } - - /** - * Get or create a file for output. - * - *

This will always use the first path that was registered. - * - * @param className the class name of the file to create. - * @param kind the kind of file to create. - * @return the file object for output, or an empty optional if no paths existed to place it in. - */ - public Optional getJavaFileForOutput(String className, Kind kind) { - return roots - .stream() - .findFirst() - .flatMap(root -> { - var relativePathParts = classNameToRelativePathParts(className, kind.extension); - var path = resolveNested(root, relativePathParts); - return Optional.of(factory.create(location, path, className)); - }); - } - - /** - * Get the corresponding {@link Location} handle for this manager. - * - * @return the location. - */ - public Location getLocation() { - return location; - } - - /** - * Get a snapshot of the iterable of the paths in this location. - * - * @return the list of the paths that were loaded at the time the method was called, in the order - * they are considered. - */ - public List getRoots() { - return List.copyOf(roots); - } - - /** - * Get a service loader for this location. - * - * @param service the service type. - * @param the service type. - * @return the service loader. - * @throws UnsupportedOperationException if this manager is for a module location. - */ - public ServiceLoader getServiceLoader(Class service) { - if (location instanceof ModuleLocation) { - throw new UnsupportedOperationException("Cannot load services from specific modules"); - } - - getClass().getModule().addUses(service); - if (location.isModuleOrientedLocation()) { - var finder = ModuleFinder.of(roots.toArray(new Path[0])); - var bootLayer = ModuleLayer.boot(); - var config = bootLayer - .configuration() - .resolveAndBind(ModuleFinder.of(), finder, Collections.emptySet()); - var layer = bootLayer - .defineModulesWithOneLoader(config, ClassLoader.getSystemClassLoader()); - return ServiceLoader.load(layer, service); - } else { - return ServiceLoader.load(service, classLoader.access()); - } - } - - /** - * Infer the binary name of the given file object. - * - *

This will attempt to find the corresponding root path for the file object, and then - * output the relative path, converted to a class name-like string. If the file object does not - * have a root in this manager, expect an empty optional to be returned instead. - * - * @param fileObject the file object to infer the binary name for. - * @return the binary name of the object, or an empty optional if unknown. - */ - public Optional inferBinaryName(JavaFileObject fileObject) { - // For some reason, converting a zip entry to a URI gives us a scheme of `jar://file://`, but - // we cannot then parse the URI back to a path without removing the `file://` bit first. Since - // we assume we always have instances of PathJavaFileObject here, let's just cast to that and - // get the correct path immediately. - var path = ((PathJavaFileObject) fileObject).getPath(); - - for (var root : roots) { - var resolvedPath = root.resolve(path.toString()); - if (path.startsWith(root) && Files.isRegularFile(resolvedPath)) { - var relativePath = root.relativize(resolvedPath); - return Optional.of(pathToObjectName(relativePath, fileObject.getKind().extension)); - } - } - - return Optional.empty(); - } - - /** - * Determine if the manager is empty or not. - * - * @return {@code true} if empty, {@code false} otherwise. - */ - public boolean isEmpty() { - return roots.isEmpty(); - } - - /** - * List the {@link JavaFileObject} objects in the given package. - * - * @param packageName the package name. - * @param kinds the kinds to allow. - * @param recurse {@code true} to search recursively, {@code false} to only consider the given - * package directly. - * @return an iterable of file objects that were found. - * @throws IOException if any of the paths cannot be read due to an IO error. - */ - public Iterable list( - String packageName, - Set kinds, - boolean recurse - ) throws IOException { - var relativePathParts = packageNameToRelativePathParts(packageName); - var maxDepth = walkDepth(recurse); - var results = new ArrayList(); - - for (var root : roots) { - var path = resolveNested(root, relativePathParts); - - if (!Files.exists(path)) { - continue; - } - - try (var stream = Files.walk(path, maxDepth)) { - stream - .filter(hasAnyKind(kinds).and(Files::isRegularFile)) - .map(nextFile -> factory.create(location, nextFile)) - .peek(fileObject -> LOGGER.trace( - "Found file object {} in root {} for list on package={}, kinds={}, recurse={}", - fileObject, - root, - packageName, - kinds, - recurse - )) - .forEach(results::add); - } - } - - if (results.isEmpty()) { - LOGGER.trace("No files found in any roots for package {}", packageName); - } - - return results; - } - - @Override - public String toString() { - return getClass().getSimpleName() - + "{location=" + StringUtils.quoted(location.getName()) - + "}"; - } - - /** - * Perform {@link Files#walk(Path, FileVisitOption...)} across each root path in this location - * manager, and return all results in a single stream. - * - * @param fileVisitOptions the options for file visiting within each root. - * @return the stream of paths. - */ - public Stream walk(FileVisitOption... fileVisitOptions) { - return walk(Integer.MAX_VALUE, fileVisitOptions); - } - - /** - * Perform {@link Files#walk(Path, int, FileVisitOption...)} across each root path in this - * location manager, and return all results in a single stream. - * - * @param maxDepth the max depth to recurse in each root. - * @param fileVisitOptions the options for file visiting within each root. - * @return the stream of paths. - */ - public Stream walk(int maxDepth, FileVisitOption... fileVisitOptions) { - return getRoots() - .stream() - .flatMap(root -> uncheckedIo(() -> Files.walk(root, maxDepth, fileVisitOptions))); - } - - /** - * Get the factory for creating {@link PathJavaFileObject} instances with. - * - * @return the factory. - */ - protected PathJavaFileObjectFactory getPathJavaFileObjectFactory() { - return factory; - } - - /** - * Register the given path to the roots of this manager. - * - * @param path the path to register. - */ - @SuppressWarnings("resource") - protected void registerPath(Path path) { - if (!Files.exists(path)) { - throw wrapWithUncheckedIoException(new FileNotFoundException(path.toString())); - } - - var absolutePath = path.toAbsolutePath(); - - if (Files.isDirectory(absolutePath)) { - registerDirectory(absolutePath); - return; - } - - // I previously used Files.probeContentType here, but it turns out that this is buggy on - // some JREs on MacOS and Windows, where it will always provide a null result. - // See https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8080369 - // Therefore, I am now just using a file extension check instead until I can think of a better - // way of probing this without opening each file and checking the header. - // TODO(ascopes): reconsider how I do this. - var fileName = path.getFileName().toString(); - - for (var extension : JAR_FILE_EXTENSIONS) { - if (fileName.endsWith(extension)) { - jarFileSystems - .computeIfAbsent(absolutePath.toString(), ignored -> openJarHandle(absolutePath)); - return; - } - } - - throw new UnsupportedOperationException( - "File at URI " + absolutePath.toUri() + " is not supported by this implementation." - ); - } - - private void registerDirectory(Path absolutePath) { - LOGGER.trace("Adding root {} to {}", absolutePath, this); - roots.add(absolutePath); - } - - private JarHandle openJarHandle(Path path) { - return uncheckedIo(() -> { - // When we open a JAR, we want to have a unique file name to work with. - // The reason behind this is that the ZipFileSystemProvider used to open the JARs only - // allows one open instance of each JAR at a time. If any tests using JCT run in parallel, - // then we end up with a potential race condition where tests reusing the same JAR cannot - // open a file system handle. This could also impact other unrelated unit tests that access - // a JAR file in the classpath directly, as it would conflict with this. Unfortunately, - // without reimplementing a random-access JAR reader (which the public JarInputStream API - // does not appear to provide us), we either have to load the entire JAR into RAM ahead of - // time, which is very slow, or we have to force all tests to run in series. If we do not - // close the JAR file system, we'd get a memory leak too. - var fileName = path.getFileName().toString(); - var tempDir = Files.createTempDirectory(fileName); - try { - var link = platformLinkStrategy.createLinkOrCopy(tempDir.resolve(fileName), path); - - for (var provider : FileSystemProvider.installedProviders()) { - if (provider.getScheme().equals("jar")) { - var fs = provider.newFileSystem(link, Map.of()); - roots.add(fs.getRootDirectories().iterator().next()); - return new JarHandle(link, fs); - } - } - - throw new FileSystemNotFoundException("jar"); - } catch (IOException ex) { - // Ensure we do not leak resources. - RecursiveDeleter.deleteAll(tempDir); - throw ex; - } - }); - } - - private void destroyClassLoader() { - classLoader.destroy(); - } - - private String pathToObjectName(Path path, String extension) { - assert path.getNameCount() != 0 : "Got an empty path somehow"; - - var parts = new ArrayList(); - for (var part : path) { - parts.add(part.toString()); - } - - // Remove file extension on the last element. - var lastIndex = parts.size() - 1; - var fileName = parts.get(lastIndex); - parts.set(lastIndex, fileName.substring(0, fileName.length() - extension.length())); - - // Join into a package name. - return String.join(".", parts); - } - - private String[] packageNameToRelativePathParts(String packageName) { - // First arg has to be empty to be able to accept variadic arguments properly. - return PACKAGE_SPLITTER.splitToArray(packageName); - } - - private String[] classNameToRelativePathParts(String className, String extension) { - var parts = PACKAGE_SPLITTER.splitToArray(className); - assert parts.length > 0 : "did not expect an empty classname"; - parts[parts.length - 1] += extension; - return parts; - } - - private Path resolveNested(Path base, String[] parts) { - for (var part : parts) { - base = base.resolve(part); - } - return base.normalize(); - } - - private Predicate hasAnyKind(Iterable kinds) { - return path -> { - var fileName = path.getFileName().toString(); - for (var kind : kinds) { - if (fileName.endsWith(kind.extension)) { - return true; - } - } - return false; - }; - } - - private int walkDepth(boolean recurse) { - return recurse ? Integer.MAX_VALUE : 1; - } - - private static final class JarHandle implements Closeable { - - private final Path link; - private final FileSystem fileSystem; - - private JarHandle(Path link, FileSystem fileSystem) { - this.link = link; - this.fileSystem = fileSystem; - } - - @Override - public void close() throws IOException { - fileSystem.close(); - RecursiveDeleter.deleteAll(link); - } - } -} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/PathLocationRepository.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/PathLocationRepository.java deleted file mode 100644 index d1b6c23ae..000000000 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/PathLocationRepository.java +++ /dev/null @@ -1,280 +0,0 @@ -/* - * Copyright (C) 2022 Ashley Scopes - * - * 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.paths; - -import static java.util.Objects.requireNonNull; - -import java.util.Comparator; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Optional; -import java.util.TreeMap; -import java.util.UUID; -import javax.tools.JavaFileManager.Location; -import javax.tools.StandardLocation; -import org.apiguardian.api.API; -import org.apiguardian.api.API.Status; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - - -/** - * Repository to store and manage path location managers. - * - *

This will treat {@link ModuleLocation} as a special case, resolving the parent manager - * first. - * - * @author Ashley Scopes - * @since 0.0.1 - */ -@API(since = "0.0.1", status = Status.EXPERIMENTAL) -public class PathLocationRepository implements AutoCloseable { - - private static final Logger LOGGER = LoggerFactory.getLogger(PathLocationRepository.class); - - private static final Comparator LOCATION_COMPARATOR = Comparator - .comparing(Location::getName) - .thenComparing(ModuleLocation.class::isInstance); - - private final PathJavaFileObjectFactory factory; - - private final Map managers; - - /** - * Initialize the repository. - * - * @param factory the factory to use to create {@link PathJavaFileObject} instances. - */ - public PathLocationRepository(PathJavaFileObjectFactory factory) { - // We use a navigable map here as there is no concrete guarantee that all implementations of - // Location will provide consistent equality and hashcode implementations, which may cause - // us problems when dealing with equivalence. - this.factory = requireNonNull(factory); - managers = new TreeMap<>(LOCATION_COMPARATOR); - } - - /** - * Close this repository by destroying any references it holds. - * - *

This will empty all locations. Any other resources you have opened must be freed - * separately. - */ - @Override - public void close() { - managers.clear(); - } - - /** - * Determine if the given location is registered with the manager. - * - * @param location the location to look for. - * @return {@code true} if registered, or {@code false} if not registered. - */ - public boolean containsManager(Location location) { - requireNonNull(location); - - if (location instanceof ModuleLocation) { - var moduleLocation = ((ModuleLocation) location); - var moduleName = moduleLocation.getModuleName(); - var manager = managers.get(moduleLocation.getParent()); - return manager != null && manager.hasModuleLocationManager(moduleName); - } - - return managers.containsKey(location); - } - - /** - * Determine if the given module is registered with the manager. - * - * @param parentLocation the location that the module is contained within. - * @param moduleName the name of the module. - * @return {@code true} if registered, or {@code false} if not registered. - */ - public boolean containsModuleManager(Location parentLocation, String moduleName) { - requireNonNull(parentLocation); - requireNonNull(moduleName); - return containsManager(new ModuleLocation(parentLocation, moduleName)); - } - - /** - * Get the manager for a location, if it exists. - * - * @param location the location to look up. - * @return the location manager, if present, or an empty optional if it does not exist. - */ - public Optional getManager(Location location) { - requireNonNull(location); - - if (location instanceof ModuleLocation) { - var moduleLocation = (ModuleLocation) location; - var moduleName = moduleLocation.getModuleName(); - var parentLocation = moduleLocation.getParent(); - return getManager(parentLocation) - .map(ParentPathLocationManager.class::cast) - .flatMap(manager -> manager.getModuleLocationManager(moduleName)); - } - - LOGGER.trace("Attempting to get location manager for location {}", location.getName()); - var manager = managers.get(location); - LOGGER.trace( - "Location manager for location {} was {}", - location.getName(), - manager == null ? "not present" : "present" - ); - - return Optional.ofNullable(manager); - } - - /** - * Get the manager for a module location, if it exists. - * - * @param parentLocation the location the module is contained within. - * @param moduleName the name of the module. - * @return the location manager, if present, or an empty optional if it does not exist. - */ - public Optional getModuleManager( - Location parentLocation, - String moduleName - ) { - requireNonNull(parentLocation); - requireNonNull(moduleName); - return getManager(new ModuleLocation(parentLocation, moduleName)); - } - - /** - * Get the manager for a location, if it exists, or throw an exception if it doesn't. - * - * @param location the location to look up. - * @return the location manager. - * @throws NoSuchElementException if the manager is not found. - */ - public PathLocationManager getExpectedManager(Location location) { - requireNonNull(location); - - return getManager(location) - .orElseThrow(() -> new NoSuchElementException( - "No location manager for location " + location.getName() + " was found" - )); - } - - /** - * Get the manager for a module location, if it exists, or throw an exception if it doesn't. - * - * @param parentLocation the location that the module is contained within. - * @param moduleName the name of the module. - * @return the location manager. - * @throws NoSuchElementException if the manager is not found. - */ - public PathLocationManager getExpectedModuleManager( - Location parentLocation, - String moduleName - ) { - requireNonNull(parentLocation); - requireNonNull(moduleName); - return getExpectedManager(new ModuleLocation(parentLocation, moduleName)); - } - - /** - * Get the manager for a location, creating it first if it does not yet exist. - * - *

Non-module output locations that do not have a path already configured will automatically - * have a {@link RamPath} created for them. - * - * @param location the location to look up. - * @return the location manager. - * @throws IllegalArgumentException if a {@link StandardLocation#MODULE_SOURCE_PATH} has already - * been created and this operation would create a - * {@link StandardLocation#SOURCE_PATH} location, or vice-versa. - */ - public PathLocationManager getOrCreateManager(Location location) { - requireNonNull(location); - ensureCompatibleLocation(location); - - if (location instanceof ModuleLocation) { - var moduleLocation = (ModuleLocation) location; - var moduleName = moduleLocation.getModuleName(); - var parentLocation = moduleLocation.getParent(); - return ((ParentPathLocationManager) getOrCreateManager(parentLocation)) - .getOrCreateModuleLocationManager(moduleName); - } - - return managers - .computeIfAbsent(location, unused -> createParentPathLocationManager(location)); - } - - /** - * Get the manager for a module location, creating it first if it does not yet exist. - * - * @param parentLocation the parent location that the module is contained within. - * @param moduleName the name of the module to get the location for. - * @return the location manager. - * @throws IllegalArgumentException if a {@link StandardLocation#MODULE_SOURCE_PATH} has already - * been created and this operation would create a - * {@link StandardLocation#SOURCE_PATH} location, or vice-versa. - */ - public PathLocationManager getOrCreateModuleManager(Location parentLocation, String moduleName) { - requireNonNull(parentLocation); - requireNonNull(moduleName); - return getOrCreateManager(new ModuleLocation(parentLocation, moduleName)); - } - - @Override - public String toString() { - return "PathLocationManagerRepository{}"; - } - - private void ensureCompatibleLocation(Location location) { - if (location instanceof ModuleLocation) { - location = ((ModuleLocation) location).getParent(); - } - - if (managers.containsKey(StandardLocation.SOURCE_PATH) - && location.equals(StandardLocation.MODULE_SOURCE_PATH)) { - throw new IllegalArgumentException( - "A source-path location has already been registered, meaning the compiler will run in " - + "legacy compilation mode. This means you cannot add a module-source-path to this " - + "configuration as well"); - } - - if (managers.containsKey(StandardLocation.MODULE_SOURCE_PATH) - && location.equals(StandardLocation.SOURCE_PATH)) { - throw new IllegalArgumentException( - "A module-source-path location has already been registered, meaning the compiler will " - + "run in multi-module compilation mode. This means you cannot add a source-path to " - + "this configuration as well"); - } - } - - private ParentPathLocationManager createParentPathLocationManager(Location location) { - var manager = new ParentPathLocationManager(factory, location); - - if (location.isOutputLocation()) { - var ramPath = RamPath.createPath( - location.getName() + "-" + UUID.randomUUID(), - true - ); - LOGGER.debug( - "Implicitly created new in-memory path {} for output location {}", - ramPath, - location.getName() - ); - manager.addRamPath(ramPath); - } - - return manager; - } -} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/RamPath.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/RamPath.java index ea0f97161..2c0bc5e84 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/RamPath.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/RamPath.java @@ -16,14 +16,15 @@ package io.github.ascopes.jct.paths; -import static io.github.ascopes.jct.intern.IoExceptionUtils.uncheckedIo; +import static io.github.ascopes.jct.utils.IoExceptionUtils.uncheckedIo; import static java.util.Objects.requireNonNull; import com.google.common.jimfs.Configuration; import com.google.common.jimfs.Feature; import com.google.common.jimfs.Jimfs; import com.google.common.jimfs.PathType; -import io.github.ascopes.jct.intern.AsyncResourceCloser; +import io.github.ascopes.jct.utils.AsyncResourceCloser; +import io.github.ascopes.jct.utils.StringUtils; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.FileNotFoundException; @@ -35,6 +36,7 @@ import java.net.URL; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.OpenOption; @@ -44,6 +46,7 @@ import java.nio.file.StandardOpenOption; import java.nio.file.attribute.BasicFileAttributes; import java.util.Locale; +import java.util.Objects; import org.apiguardian.api.API; import org.apiguardian.api.API.Status; import org.reflections.Reflections; @@ -59,21 +62,21 @@ *

This provides a utility for constructing complex and dynamic directory tree structures * quickly and simply using fluent chained methods. * - *

These file systems are integrated into the {@link java.nio.file.FileSystem} API, and can - * be configured to automatically destroy themselves once this RamPath handle is garbage collected. + *

These file systems are integrated into the {@link FileSystem} API, and can be configured to + * automatically destroy themselves once this RamPath handle is garbage collected. * * @author Ashley Scopes * @since 0.0.1 */ @API(since = "0.0.1", status = Status.EXPERIMENTAL) -public class RamPath { +public final class RamPath implements PathLike { private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; private static final Logger LOGGER = LoggerFactory.getLogger(RamPath.class); private static final Cleaner CLEANER = Cleaner.create(); - private final URI uri; private final Path path; + private final URI uri; private final String name; /** @@ -109,6 +112,16 @@ private RamPath(String name, boolean closeOnGarbageCollection) { LOGGER.trace("Initialized new in-memory directory {}", path); } + @Override + public Path getPath() { + return path; + } + + @Override + public URI getUri() { + return uri; + } + /** * Get the identifying name of the temporary file system. * @@ -118,15 +131,6 @@ public String getName() { return name; } - /** - * Get the root path of the in-memory directory. - * - * @return the root path. - */ - public Path getPath() { - return path; - } - /** * Close the underlying file system. * @@ -519,9 +523,29 @@ public Path copyToTempDir() { return tempPath; } + @Override + public boolean equals(Object other) { + if (!(other instanceof RamPath)) { + return false; + } + + var that = (RamPath) other; + + return name.equals(that.name) + && uri.equals(that.uri); + } + + @Override + public int hashCode() { + return Objects.hash(name, uri); + } + @Override public String toString() { - return uri.toString(); + return getClass().getSimpleName() + "{" + + "name=" + StringUtils.quoted(name) + ", " + + "path=" + StringUtils.quoted(uri) + + "}"; } private Path makeRelativeToHere(Path relativePath) { @@ -557,12 +581,11 @@ public static RamPath createPath(String name) { * @param name a symbolic name to give the path. This must be a valid POSIX * directory name. * @param closeOnGarbageCollection if {@code true}, then the {@link #close()} operation will be - * called on the underlying {@link java.nio.file.FileSystem} as - * soon as the returned object from this method is garbage - * collected. If {@code false}, then you must close the underlying - * file system manually using the {@link #close()} method on the - * returned object. Failing to do so will lead to resources being - * leaked. + * called on the underlying {@link FileSystem} as soon as the + * returned object from this method is garbage collected. If + * {@code false}, then you must close the underlying file system + * manually using the {@link #close()} method on the returned + * object. Failing to do so will lead to resources being leaked. * @return the in-memory path. * @see #createPath(String, boolean) */ diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/SubPath.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/SubPath.java new file mode 100644 index 000000000..a3bf7aacb --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/SubPath.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.paths; + +import static java.util.Objects.requireNonNull; + +import io.github.ascopes.jct.utils.IterableUtils; +import io.github.ascopes.jct.utils.StringUtils; +import java.net.URI; +import java.nio.file.Path; +import java.util.Objects; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * A wrapper around an existing {@link PathLike} which contains a path to some sub-location in the + * original path. + * + *

This mechanism enables keeping the original path-like reference alive, which + * enables handling {@link RamPath} garbage collection correctly. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public final class SubPath implements PathLike { + + private final PathLike parent; + private final Path root; + private final URI uri; + + /** + * Initialize this path. + * + * @param path the path-like path to wrap. + * @param subPathParts the parts of the subpath to point to. + */ + public SubPath(PathLike path, String... subPathParts) { + parent = requireNonNull(path, "path"); + + var root = path.getPath(); + for (var subPathPart : IterableUtils.requireNonNullValues(subPathParts, "subPathParts")) { + root = root.resolve(subPathPart); + } + this.root = root; + uri = root.toUri(); + } + + @Override + public Path getPath() { + return root; + } + + @Override + public URI getUri() { + return uri; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof SubPath)) { + return false; + } + + var that = (SubPath) other; + + return uri.equals(that.uri); + } + + @Override + public int hashCode() { + return Objects.hash(uri); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{path=" + StringUtils.quoted(uri) + "}"; + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/package-info.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/package-info.java index 69f623213..5a715b808 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/package-info.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/paths/package-info.java @@ -15,8 +15,6 @@ */ /** - * Implementation of a JSR-199-compliant Path {@link javax.tools.JavaFileManager} which supports - * modules, multiple paths per {@link javax.tools.JavaFileManager.Location}, and the ability to use - * in-memory file systems provided by {@link com.google.common.jimfs.Jimfs}. + * Facilities for handling paths. */ package io.github.ascopes.jct.paths; diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/AsyncResourceCloser.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/AsyncResourceCloser.java similarity index 98% rename from java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/AsyncResourceCloser.java rename to java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/AsyncResourceCloser.java index 97bea10b0..7cf474d60 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/AsyncResourceCloser.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/AsyncResourceCloser.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.github.ascopes.jct.intern; +package io.github.ascopes.jct.utils; import java.util.Map; import java.util.concurrent.CompletableFuture; diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/EnumerationAdapter.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/EnumerationAdapter.java similarity index 97% rename from java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/EnumerationAdapter.java rename to java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/EnumerationAdapter.java index 029bf65a4..684f63ce4 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/EnumerationAdapter.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/EnumerationAdapter.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.github.ascopes.jct.intern; +package io.github.ascopes.jct.utils; import java.util.Enumeration; import java.util.Iterator; diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/FileUtils.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/FileUtils.java new file mode 100644 index 000000000..b1131fddc --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/FileUtils.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.utils; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import javax.tools.JavaFileObject.Kind; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * Utilities for handling files in the file system. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.EXPERIMENTAL) +public final class FileUtils { + + private static final StringSlicer PACKAGE_SLICER = new StringSlicer("."); + private static final StringSlicer RESOURCE_SPLITTER = new StringSlicer("/"); + + private FileUtils() { + throw new UnsupportedOperationException("static-only class"); + } + + /** + * Convert a path to a binary name of a class. + * + * @param path the relative path to convert. + * @return the expected binary name. + * @throws IllegalArgumentException if the path is absolute. + */ + public static String pathToBinaryName(Path path) { + if (path.isAbsolute()) { + throw new IllegalArgumentException("Path cannot be absolute (got " + path + ")"); + } + + return IntStream + .range(0, path.getNameCount()) + .mapToObj(path::getName) + .map(Path::toString) + .map(FileUtils::stripFileExtension) + .collect(Collectors.joining(".")); + } + + /** + * Convert a binary class name to a package name. + * + * @param binaryName the binary name to convert. + * @return the expected package name. + */ + public static String binaryNameToPackageName(String binaryName) { + return stripClassName(binaryName); + } + + /** + * Convert a binary class name to a simple class name. + * + * @param binaryName the binary name to convert. + * @return the expected simple class name. + */ + public static String binaryNameToSimpleClassName(String binaryName) { + var lastDot = binaryName.lastIndexOf('.'); + + if (lastDot == -1) { + // The class has no package + return binaryName; + } + + return binaryName.substring(lastDot + 1); + } + + /** + * Convert a binary class name to a path. + * + * @param directory the base directory the package resides within. This is used to ensure the + * correct path root and provider is picked. + * @param binaryName the binary name to convert. + * @param kind the kind of the file. + * @return the expected path. + */ + public static Path binaryNameToPath(Path directory, String binaryName, Kind kind) { + var packageName = binaryNameToPackageName(binaryName); + var classFileName = binaryNameToSimpleClassName(binaryName) + kind.extension; + return resolve(directory, PACKAGE_SLICER.splitToArray(packageName)).resolve(classFileName); + } + + /** + * Convert a given package name to a path. + * + * @param directory the base directory the package resides within. This is used to ensure the + * correct path root and provider is picked. + * @param packageName the name of the package. + * @return the expected path. + */ + public static Path packageNameToPath(Path directory, String packageName) { + for (var part : PACKAGE_SLICER.splitToArray(packageName)) { + directory = directory.resolve(part); + } + return directory; + } + + + /** + * Convert a simple class name to a path. + * + * @param packageDirectory the directory the class resides within. + * @param className the simple class name. + * @param kind the kind of the file. + * @return the expected path. + */ + public static Path simpleClassNameToPath(Path packageDirectory, String className, Kind kind) { + var classFileName = className + kind.extension; + return resolve(packageDirectory, classFileName); + } + + /** + * Convert a resource name that is found in a given package to a NIO path. + * + * @param directory the base directory the package resides within. This is used to ensure the + * correct path root and provider is picked. + * @param packageName the package name that the resource resides within. + * @param relativeName the relative name of the resource. + * @return the expected path. + */ + public static Path resourceNameToPath(Path directory, String packageName, String relativeName) { + // If we have a relative name that starts with a `/`, then we assume that it is relative + // to the root package, so we ignore the given package name. + if (relativeName.startsWith("/")) { + var parts = RESOURCE_SPLITTER + .splitToStream(relativeName) + .dropWhile(String::isEmpty) + .toArray(String[]::new); + + return resolve(directory, parts); + } else { + var baseDir = resolve(directory, PACKAGE_SLICER.splitToArray(packageName)); + return relativeResourceNameToPath(baseDir, relativeName); + } + } + + /** + * Convert a relative classpath resource path to a NIO path. + * + * @param directory the directory the resource sits within. + * @param relativeName the relative path of the resource within the directory. + * @return the path to the resource on the file system. + */ + public static Path relativeResourceNameToPath(Path directory, String relativeName) { + var parts = RESOURCE_SPLITTER.splitToArray(relativeName); + return resolve(directory, parts); + } + + /** + * Determine the kind of file in the given path. + * + * @param path the path to inspect. + * @return the kind of file. If not known, this will return {@link Kind#OTHER}. + */ + public static Kind pathToKind(Path path) { + var fileName = path.getFileName().toString(); + + for (var kind : Kind.values()) { + if (Kind.OTHER.equals(kind)) { + continue; + } + + if (fileName.endsWith(kind.extension)) { + return kind; + } + } + + return Kind.OTHER; + } + + /** + * Return a predicate for NIO paths that filters out any files that do not match one of the given + * file kinds. + * + *

Note: this expects the file to exist for the predicate to return + * {@code true}. Any non-existant files will always return {@code false}, even if their name + * matches one of the provided kinds. + * + * @param kinds the set of kinds of file to allow. + * @return the predicate. + */ + public static Predicate fileWithAnyKind(Set kinds) { + return path -> Files.isRegularFile(path) && kinds.stream() + .map(kind -> kind.extension) + .anyMatch(path.toString()::endsWith); + } + + private static Path resolve(Path root, String... parts) { + for (var part : parts) { + root = root.resolve(part); + } + return root.normalize(); + } + + private static String stripClassName(String binaryName) { + var classIndex = binaryName.lastIndexOf('.'); + return classIndex == -1 + ? "" + : binaryName.substring(0, classIndex); + } + + private static String stripFileExtension(String name) { + var extIndex = name.lastIndexOf('.'); + return extIndex == -1 + ? name + : name.substring(0, extIndex); + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/IoExceptionUtils.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/IoExceptionUtils.java similarity index 98% rename from java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/IoExceptionUtils.java rename to java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/IoExceptionUtils.java index 05538db48..13b26ef7e 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/IoExceptionUtils.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/IoExceptionUtils.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.github.ascopes.jct.intern; +package io.github.ascopes.jct.utils; import java.io.IOException; import java.io.UncheckedIOException; diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/IterableUtils.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/IterableUtils.java similarity index 69% rename from java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/IterableUtils.java rename to java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/IterableUtils.java index 4074d17d9..ff49fbf06 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/IterableUtils.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/IterableUtils.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.github.ascopes.jct.intern; +package io.github.ascopes.jct.utils; import static java.util.Collections.unmodifiableList; import static java.util.Collections.unmodifiableSet; @@ -121,4 +121,61 @@ public static > T requireNonNullValues( return collection; } + + /** + * Ensure there are no {@code null} elements in the given array. + * + *

This also ensures the array itself is not null either. + * + * @param array the array to check. + * @param arrayName the name to give in the error message if anything is null. + * @param the input collection type. + * @return the input array. + */ + public static T[] requireNonNullValues( + T[] array, + String arrayName + ) { + // Duplicate this logic so that we do not have to wrap the array in Arrays.list. This prevents + // a copy of the entire array each time we do this check. + requireNonNull(array, arrayName); + + var badElements = Stream.builder(); + + var index = 0; + for (Object element : array) { + if (element == null) { + badElements.add(arrayName + "[" + index + "]"); + } + ++index; + } + + var error = badElements + .build() + .collect(Collectors.joining(", ")); + + if (!error.isEmpty()) { + throw new NullPointerException(error); + } + + return array; + } + + /** + * Create a list from a sequence of items, just like {@link Arrays#asList(Object[])}. This + * implementation provides a shortcut for methods enforcing at least one variadic argument in the + * signature. + * + * @param first the first item. + * @param more the rest of the items. + * @param the type of the items. + * @return the list of all items in the given order. + */ + @SafeVarargs + public static List asList(T first, T... more) { + var list = new ArrayList(); + list.add(first); + list.addAll(Arrays.asList(more)); + return list; + } } diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/Lazy.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/Lazy.java similarity index 63% rename from java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/Lazy.java rename to java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/Lazy.java index f023e19a5..3f6f26c25 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/Lazy.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/Lazy.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.github.ascopes.jct.intern; +package io.github.ascopes.jct.utils; import java.util.Objects; import java.util.function.Supplier; @@ -29,6 +29,7 @@ * *

This is thread-safe. * + * @param the type of lazy value to return when accessed. * @author Ashley Scopes * @since 0.0.1 */ @@ -78,8 +79,28 @@ public T access() { public void destroy() { if (initialized) { synchronized (lock) { - initialized = false; - data = null; + if (initialized) { + initialized = false; + data = null; + } + } + } + } + + /** + * Attempt to run some logic on the initialized value if and only if the value has already been + * initialized by the time this is called. + * + * @param consumer the consumer to consume the value if it is initialized. + * @param the exception type that the consumer can throw. + * @throws E the exception type that the consumer can throw. + */ + public void ifInitialized(ThrowingConsumer consumer) throws E { + if (initialized) { + synchronized (lock) { + if (initialized) { + consumer.consume(data); + } } } } @@ -98,4 +119,25 @@ public String toString() { return builder.append("}").toString(); } + + /** + * Consumer that throws some form of checked exception if something goes wrong. + * + * @param the exception type. + * @param the type to consume. + * @author Ashley Scopes + * @since 0.0.1 + */ + @API(since = "0.0.1", status = Status.INTERNAL) + @FunctionalInterface + public interface ThrowingConsumer { + + /** + * Consume a value. + * + * @param arg the value to consume. + * @throws E the exception that may be thrown if something goes wrong in the consumer. + */ + void consume(T arg) throws E; + } } diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/ModulePrefix.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/ModulePrefix.java new file mode 100644 index 000000000..bc963ac39 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/ModulePrefix.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.utils; + +import static java.util.Objects.requireNonNull; + +import java.util.Optional; +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * Helper class that extracts a module name from a string as a prefix and holds the result. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.INTERNAL) +public final class ModulePrefix { + + private final String original; + private final String moduleName; + private final String rest; + + private ModulePrefix(String original, String moduleName, String rest) { + this.original = requireNonNull(original, "original"); + this.moduleName = requireNonNull(moduleName, "moduleName"); + this.rest = requireNonNull(rest, "rest"); + } + + /** + * Get the original input string. + * + * @return the original input string. + */ + public String getOriginal() { + return original; + } + + /** + * Get the extracted module name. + * + * @return the module name. + */ + public String getModuleName() { + return moduleName; + } + + /** + * Get the rest of the string minus the module name. + * + * @return the rest of the string. + */ + public String getRest() { + return rest; + } + + /** + * Try and extract a module name from the given string. + * + * @param original the string to operate on. + * @return the extracted prefix, or an empty module if no module was found. + * @throws IllegalArgumentException if the input string starts with a forward slash. + */ + public static Optional tryExtract(String original) { + if (original.startsWith("/")) { + throw new IllegalArgumentException( + "Absolute paths are not supported (got '" + original + "')"); + } + + // If we have a valid module name at the start, we should check in that location first. + var firstSlash = original.indexOf('/'); + + if (firstSlash == -1) { + return Optional.empty(); + } + + var moduleName = original.substring(0, firstSlash); + var restOfPath = original.substring(firstSlash + 1); + return Optional.of(new ModulePrefix(original, moduleName, restOfPath)); + } +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/Nullable.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/Nullable.java new file mode 100644 index 000000000..cd4c95de3 --- /dev/null +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/Nullable.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.utils; + +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.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * Annotation for documentation purposes indicating that the annotated element may have a null + * value. + * + * @author Ashley Scopes + * @since 0.0.1 + */ +@API(since = "0.0.1", status = Status.INTERNAL) +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ + ElementType.ANNOTATION_TYPE, + ElementType.FIELD, + ElementType.LOCAL_VARIABLE, + ElementType.METHOD, + ElementType.PARAMETER, + ElementType.TYPE_PARAMETER, + ElementType.TYPE_USE, +}) +public @interface Nullable { +} diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/RecursiveDeleter.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/RecursiveDeleter.java similarity index 98% rename from java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/RecursiveDeleter.java rename to java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/RecursiveDeleter.java index 4260e17c1..3841b22f5 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/RecursiveDeleter.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/RecursiveDeleter.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.github.ascopes.jct.intern; +package io.github.ascopes.jct.utils; import java.io.IOException; import java.nio.file.FileVisitResult; @@ -35,6 +35,7 @@ */ @API(since = "0.0.1", status = Status.INTERNAL) public class RecursiveDeleter extends SimpleFileVisitor { + private static final Logger LOGGER = LoggerFactory.getLogger(RecursiveDeleter.class); private static final RecursiveDeleter INSTANCE = new RecursiveDeleter(); diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/SpecialLocations.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/SpecialLocations.java similarity index 99% rename from java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/SpecialLocations.java rename to java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/SpecialLocations.java index b4e0ce23e..c180db0e7 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/SpecialLocations.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/SpecialLocations.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.github.ascopes.jct.intern; +package io.github.ascopes.jct.utils; import java.io.File; import java.lang.management.ManagementFactory; diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/StringSlicer.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/StringSlicer.java similarity index 98% rename from java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/StringSlicer.java rename to java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/StringSlicer.java index 8fe4931a6..6a71f7c04 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/StringSlicer.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/StringSlicer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.github.ascopes.jct.intern; +package io.github.ascopes.jct.utils; import java.util.ArrayList; import java.util.Objects; diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/StringUtils.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/StringUtils.java similarity index 76% rename from java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/StringUtils.java rename to java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/StringUtils.java index 2fd20416a..202045dc4 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/StringUtils.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/StringUtils.java @@ -14,13 +14,14 @@ * limitations under the License. */ -package io.github.ascopes.jct.intern; +package io.github.ascopes.jct.utils; import java.math.BigDecimal; import java.math.RoundingMode; import java.text.DecimalFormat; import java.util.List; import java.util.Objects; +import java.util.Set; import org.apiguardian.api.API; import org.apiguardian.api.API.Status; @@ -33,6 +34,7 @@ @API(since = "0.0.1", status = Status.INTERNAL) public final class StringUtils { + private static final Set ES_ENDINGS = Set.of("s", "z", "ch", "sh", "x"); private static final BigDecimal THOUSAND = BigDecimal.valueOf(1000); private static final List TIME_UNITS = List.of("ns", "µs", "ms", "s"); private static final DecimalFormat TIME_FORMAT = new DecimalFormat("0.##"); @@ -43,6 +45,66 @@ private StringUtils() { throw new UnsupportedOperationException("static-only class"); } + /** + * Take the given list of strings and produce a connected single string, separating all but the + * last element by the {@code connector} value, and the last element by the {@code lastConnector} + * element. + * + *

This is designed to be able to take an input such as {@code List.of("foo", "bar", "baz")}, + * and be able to produce a string result such as {@code "foo, bar, or baz"} (where + * {@code connector} in this case would be {@code ", "} and {@code lastConnector} would be + * {@code ", or "}. + * + *

If no arguments are available, then an empty string is output instead. + * + *

Effectively, this is a more complex version of + * {@link String#join(CharSequence, CharSequence...)}. + * + * @param words the words to connect. + * @param connector the connector string to use. + * @param lastConnector the last connector string to use. + * @return the resulting string. + */ + public static String toWordedList( + List words, + CharSequence connector, + CharSequence lastConnector + ) { + if (words.isEmpty()) { + return ""; + } + + var builder = new StringBuilder(words.get(0)); + + var index = 1; + + for (; index < words.size() - 1; ++index) { + builder.append(connector).append(words.get(index)); + } + + if (index < words.size()) { + builder.append(lastConnector).append(words.get(index)); + } + + return builder.toString(); + } + + /** + * Left-pad the given content with the given padding char until it is the given length. + * + * @param content the content to process. + * @param length the max length of the resultant content. + * @param paddingChar the character to pad with. + * @return the padded string. + */ + public static String leftPad(String content, int length, char paddingChar) { + var builder = new StringBuilder(); + while (builder.length() + content.length() < length) { + builder.append(paddingChar); + } + return builder.append(content).toString(); + } + /** * Find the index for the start of the given line number (1-indexed). * @@ -51,7 +113,7 @@ private StringUtils() { *

The first line number will always be at index 0. If the line is not found, then * {@code -1} is returned. * - * @param content the content to read through. + * @param content the content to read through. * @param lineNumber the 1-indexed line number to find. * @return the index of the line. */ @@ -72,22 +134,6 @@ public static int indexOfLine(String content, int lineNumber) { : -1; } - /** - * Left-pad the given content with the given padding char until it is the given length. - * - * @param content the content to process. - * @param length the max length of the resultant content. - * @param paddingChar the character to pad with. - * @return the padded string. - */ - public static String leftPad(String content, int length, char paddingChar) { - var builder = new StringBuilder(); - while (builder.length() + content.length() < length) { - builder.append(paddingChar); - } - return builder.append(content).toString(); - } - /** * Find the index of the next UNIX end of line ({@code '\n'}) character from the given offset. * diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/package-info.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/package-info.java similarity index 94% rename from java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/package-info.java rename to java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/package-info.java index be64ceb69..fe0a53f46 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/intern/package-info.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/utils/package-info.java @@ -19,4 +19,4 @@ * *

Nothing in here is exposed in the public API. */ -package io.github.ascopes.jct.intern; +package io.github.ascopes.jct.utils; diff --git a/java-compiler-testing/src/main/java/module-info.java b/java-compiler-testing/src/main/java/module-info.java index 4a86cb03b..12b9bcee6 100644 --- a/java-compiler-testing/src/main/java/module-info.java +++ b/java-compiler-testing/src/main/java/module-info.java @@ -24,24 +24,36 @@ requires ecj; requires jimfs; requires me.xdrop.fuzzywuzzy; - requires org.apiguardian.api; - requires transitive org.assertj.core; + requires static org.apiguardian.api; + requires static org.junit.jupiter.params; + requires org.assertj.core; requires org.reflections; requires org.slf4j; exports io.github.ascopes.jct.assertions; exports io.github.ascopes.jct.compilers; + exports io.github.ascopes.jct.compilers.ecj; + exports io.github.ascopes.jct.compilers.javac; + exports io.github.ascopes.jct.junit; + exports io.github.ascopes.jct.jsr199; + exports io.github.ascopes.jct.jsr199.containers; + exports io.github.ascopes.jct.jsr199.diagnostics; exports io.github.ascopes.jct.paths; - // Testing access only. - exports io.github.ascopes.jct.compilers.ecj to io.github.ascopes.jct.testing; - exports io.github.ascopes.jct.compilers.javac to io.github.ascopes.jct.testing; - exports io.github.ascopes.jct.intern to io.github.ascopes.jct.testing; + // Junit annotation support. + opens io.github.ascopes.jct.junit; + // Testing exports only. + exports io.github.ascopes.jct.utils to io.github.ascopes.jct.testing; + + // Open the module for testing only. opens io.github.ascopes.jct.assertions to io.github.ascopes.jct.testing; opens io.github.ascopes.jct.compilers to io.github.ascopes.jct.testing; opens io.github.ascopes.jct.compilers.ecj to io.github.ascopes.jct.testing; opens io.github.ascopes.jct.compilers.javac to io.github.ascopes.jct.testing; + opens io.github.ascopes.jct.jsr199 to io.github.ascopes.jct.testing; + opens io.github.ascopes.jct.jsr199.containers to io.github.ascopes.jct.testing; + opens io.github.ascopes.jct.jsr199.diagnostics to io.github.ascopes.jct.testing; opens io.github.ascopes.jct.paths to io.github.ascopes.jct.testing; - opens io.github.ascopes.jct.intern to io.github.ascopes.jct.testing; + opens io.github.ascopes.jct.utils to io.github.ascopes.jct.testing; } diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/helpers/Skipping.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/helpers/Skipping.java deleted file mode 100644 index ef366acd2..000000000 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/helpers/Skipping.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (C) 2022 Ashley Scopes - * - * 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.testing.helpers; - -import org.opentest4j.TestAbortedException; - -/** - * Utilities for skipping common issues in tests. - * - * @author Ashley Scopes - */ -public final class Skipping { - - private Skipping() { - throw new UnsupportedOperationException("static-only class"); - } - - /** - * Skip the test because ECJ fails to support modules correctly. - */ - public static void skipBecauseEcjFailsToSupportModulesCorrectly() { - // FIXME(ascopes): Attempt to find the root cause of these issues. - // - // It appears to depend on passing the `--add-module` and `--module-source-paths` flags - // on the command line, but I cannot seem to get this solution to work via the JSR-199 interface - // itself. - skip( - "ECJ does not appear to currently support compiling nested module sources correctly from", - "the JSR-199 API implementation. This appears to be down to how ECJ detects source modules", - "as compiling the java-compiler-testing API itself using ECJ appears to create the same", - "errors in IntelliJ IDEA.", - "", - "One can expect an error message such as the following if this test is executed:", - "", - "\tERROR in module-info.java (at line 3)", - "\t\texports com.example;", - "\t\t ^^^^^^^^^^^", - "\tThe package com.example does not exist or is empty" - ); - } - - private static void skip(String... reasonLines) { - var reason = "Test is skipped.\n\n" + String.join("\n", reasonLines); - throw new TestAbortedException(reason.stripTrailing()); - } -} diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/basic/BasicLegacyCompilationTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/basic/BasicLegacyCompilationTest.java index 7a18a01b0..39eb92516 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/basic/BasicLegacyCompilationTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/basic/BasicLegacyCompilationTest.java @@ -16,16 +16,14 @@ package io.github.ascopes.jct.testing.integration.basic; -import io.github.ascopes.jct.assertions.CompilationAssert; -import io.github.ascopes.jct.compilers.Compilers; -import io.github.ascopes.jct.paths.RamPath; -import java.util.stream.IntStream; -import java.util.stream.LongStream; -import javax.lang.model.SourceVersion; -import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants; +import static io.github.ascopes.jct.assertions.JctAssertions.assertThatCompilation; +import static io.github.ascopes.jct.paths.RamPath.createPath; + +import io.github.ascopes.jct.compilers.Compilable; +import io.github.ascopes.jct.junit.EcjCompilers; +import io.github.ascopes.jct.junit.JavacCompilers; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; /** * Basic legacy compilation tests. @@ -35,38 +33,12 @@ @DisplayName("Basic legacy compilation integration tests") class BasicLegacyCompilationTest { - @DisplayName("I can compile a 'Hello, World!' program with javac") - @MethodSource("javacVersions") - @ParameterizedTest(name = "targeting Java {0}") - void helloWorldJavac(int version) { - var sources = RamPath - .createPath("sources") - .createFile( - "com/example/HelloWorld.java", - "package com.example;", - "public class HelloWorld {", - " public static void main(String[] args) {", - " System.out.println(\"Hello, World\");", - " }", - "}" - ); - - var compilation = Compilers - .javac() - .addSourceRamPaths(sources) - .showDeprecationWarnings(true) - .release(version) - .compile(); - - CompilationAssert.assertThatCompilation(compilation).isSuccessfulWithoutWarnings(); - } - - @DisplayName("I can compile a 'Hello, World!' program with ecj") - @MethodSource("ecjVersions") - @ParameterizedTest(name = "targeting Java {0}") - void helloWorldEcj(int version) { - var sources = RamPath - .createPath("sources") + @DisplayName("I can compile a 'Hello, World!' program") + @EcjCompilers + @JavacCompilers + @ParameterizedTest(name = "targeting {0}") + void helloWorldJavac(Compilable compiler) { + var sources = createPath("sources") .createFile( "com/example/HelloWorld.java", "package com.example;", @@ -77,25 +49,11 @@ void helloWorldEcj(int version) { "}" ); - var compilation = Compilers - .ecj() - .addSourceRamPaths(sources) - .showDeprecationWarnings(true) - .release(version) + var compilation = compiler + .addSourcePath(sources) .compile(); - CompilationAssert.assertThatCompilation(compilation).isSuccessfulWithoutWarnings(); - } - - static IntStream javacVersions() { - return IntStream.rangeClosed(8, SourceVersion.latestSupported().ordinal()); - } - - static IntStream ecjVersions() { - var maxEcjVersion = (ClassFileConstants.getLatestJDKLevel() >> (Short.BYTES * 8)) - - ClassFileConstants.MAJOR_VERSION_0; - - return LongStream.rangeClosed(8, maxEcjVersion) - .mapToInt(i -> (int) i); + assertThatCompilation(compilation) + .isSuccessfulWithoutWarnings(); } } diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/basic/BasicModuleCompilationTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/basic/BasicModuleCompilationTest.java index d32b1b683..08118ebac 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/basic/BasicModuleCompilationTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/basic/BasicModuleCompilationTest.java @@ -16,17 +16,13 @@ package io.github.ascopes.jct.testing.integration.basic; -import io.github.ascopes.jct.assertions.CompilationAssert; -import io.github.ascopes.jct.compilers.Compilers; -import io.github.ascopes.jct.paths.RamPath; -import io.github.ascopes.jct.testing.helpers.Skipping; -import java.util.stream.IntStream; -import java.util.stream.LongStream; -import javax.lang.model.SourceVersion; -import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants; +import static io.github.ascopes.jct.assertions.JctAssertions.assertThatCompilation; +import static io.github.ascopes.jct.paths.RamPath.createPath; + +import io.github.ascopes.jct.compilers.Compilable; +import io.github.ascopes.jct.junit.JavacCompilers; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; /** * Basic legacy compilation tests. @@ -36,47 +32,12 @@ @DisplayName("Basic module compilation integration tests") class BasicModuleCompilationTest { - @DisplayName("I can compile a 'Hello, World!' module program with javac") - @MethodSource("javacVersions") - @ParameterizedTest(name = "targeting Java {0}") - void helloWorldJavac(int version) { - var sources = RamPath - .createPath("sources") - .createFile( - "com/example/HelloWorld.java", - "package com.example;", - "public class HelloWorld {", - " public static void main(String[] args) {", - " System.out.println(\"Hello, World\");", - " }", - "}" - ) - .createFile( - "module-info.java", - "module hello.world {", - " requires java.base;", - " exports com.example;", - "}" - ); - - var compilation = Compilers - .javac() - .addSourceRamPaths(sources) - .showDeprecationWarnings(true) - .release(version) - .compile(); - - CompilationAssert.assertThatCompilation(compilation).isSuccessfulWithoutWarnings(); - } - - @DisplayName("I can compile a 'Hello, World!' module program with ecj") - @MethodSource("ecjVersions") - @ParameterizedTest(name = "targeting Java {0}") - void helloWorldEcj(int version) { - Skipping.skipBecauseEcjFailsToSupportModulesCorrectly(); - - var sources = RamPath - .createPath("sources") + // We skip ECJ as it does not support handling modules correctly on a non-default file system. + @DisplayName("I can compile a 'Hello, World!' module program") + @JavacCompilers(modules = true) + @ParameterizedTest(name = "targeting {0}") + void helloWorld(Compilable compiler) { + var sources = createPath("hello.world") .createFile( "com/example/HelloWorld.java", "package com.example;", @@ -94,25 +55,11 @@ void helloWorldEcj(int version) { "}" ); - var compilation = Compilers - .ecj() - .addSourceRamPaths(sources) - .showDeprecationWarnings(true) - .release(version) + var compilation = compiler + .addSourcePath(sources) .compile(); - CompilationAssert.assertThatCompilation(compilation).isSuccessfulWithoutWarnings(); - } - - static IntStream javacVersions() { - return IntStream.rangeClosed(9, SourceVersion.latestSupported().ordinal()); - } - - static IntStream ecjVersions() { - var maxEcjVersion = (ClassFileConstants.getLatestJDKLevel() >> (Short.BYTES * 8)) - - ClassFileConstants.MAJOR_VERSION_0; - - return LongStream.rangeClosed(9, maxEcjVersion) - .mapToInt(i -> (int) i); + assertThatCompilation(compilation) + .isSuccessfulWithoutWarnings(); } } diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/basic/BasicMultiModuleCompilationTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/basic/BasicMultiModuleCompilationTest.java index c407f8646..b7cb624de 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/basic/BasicMultiModuleCompilationTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/integration/basic/BasicMultiModuleCompilationTest.java @@ -16,18 +16,14 @@ package io.github.ascopes.jct.testing.integration.basic; -import io.github.ascopes.jct.assertions.CompilationAssert; -import io.github.ascopes.jct.compilers.Compiler.Logging; -import io.github.ascopes.jct.compilers.Compilers; -import io.github.ascopes.jct.paths.RamPath; -import io.github.ascopes.jct.testing.helpers.Skipping; -import java.util.stream.IntStream; -import java.util.stream.LongStream; -import javax.lang.model.SourceVersion; -import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants; +import static io.github.ascopes.jct.assertions.JctAssertions.assertThatCompilation; +import static io.github.ascopes.jct.paths.RamPath.createPath; + +import io.github.ascopes.jct.compilers.Compilable; +import io.github.ascopes.jct.junit.JavacCompilers; +import javax.tools.StandardLocation; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; /** * Basic multi-module compilation tests. @@ -37,14 +33,14 @@ @DisplayName("Basic multi-module compilation integration tests") class BasicMultiModuleCompilationTest { - @DisplayName("I can compile a multi-module 'Hello, World!' program with javac") - @MethodSource("javacVersions") - @ParameterizedTest(name = "targeting Java {0}") - void helloWorldJavac(int version) { - var sources = RamPath - .createPath("sources") + // We skip ECJ as it does not support handling modules correctly on a non-default file system. + @DisplayName("I can compile a single module using multi-module layout") + @JavacCompilers(modules = true) + @ParameterizedTest(name = "targeting {0}") + void singleModuleInMultiModuleLayout(Compilable compiler) { + var source = createPath("hello.world") .createFile( - "hello.world/com/example/HelloWorld.java", + "com/example/HelloWorld.java", "package com.example;", "public class HelloWorld {", " public static void main(String[] args) {", @@ -53,91 +49,42 @@ void helloWorldJavac(int version) { "}" ) .createFile( - "hello.world/module-info.java", + "module-info.java", "module hello.world {", " exports com.example;", "}" ); - var compilation = Compilers - .javac() - .addModuleSourceRamPaths(sources) + var compilation = compiler + .addPath(StandardLocation.MODULE_SOURCE_PATH, "hello.world", source) .showDeprecationWarnings(true) - .release(version) .compile(); - CompilationAssert.assertThatCompilation(compilation).isSuccessfulWithoutWarnings(); - - CompilationAssert.assertThatCompilation(compilation) - .classOutput() - .file("hello.world/com/example/HelloWorld.class") - .exists() - .isNotEmptyFile(); - - CompilationAssert.assertThatCompilation(compilation) - .classOutput() - .file("hello.world/module-info.class") - .exists() - .isNotEmptyFile(); + assertThatCompilation(compilation) + .isSuccessfulWithoutWarnings(); + + // TODO(ascopes): fix this to work with the file manager rewrite. + //CompilationAssert.assertThatCompilation(compilation) + // .classOutput() + // .file("hello.world/com/example/HelloWorld.class") + // .exists() + // .isNotEmptyFile(); + // + //CompilationAssert.assertThatCompilation(compilation) + // .classOutput() + // .file("hello.world/module-info.class") + // .exists() + // .isNotEmptyFile(); } - @DisplayName("I can compile a 'Hello, World!' program with ecj") - @MethodSource("ecjVersions") - @ParameterizedTest(name = "targeting Java {0}") - void helloWorldEcj(int version) { - Skipping.skipBecauseEcjFailsToSupportModulesCorrectly(); - - var sources = RamPath - .createPath("sources") + // We skip ECJ as it does not support handling modules correctly on a non-default file system. + @DisplayName("I can compile multiple modules using multi-module layout") + @JavacCompilers(modules = true) + @ParameterizedTest(name = "targeting {0}") + void multipleModulesInMultiModuleLayout(Compilable compiler) { + var helloWorld = createPath("hello.world") .createFile( - "hello.world/com/example/HelloWorld.java", - "package com.example;", - "public class HelloWorld {", - " public static void main(String[] args) {", - " System.out.println(\"Hello, World\");", - " }", - "}" - ) - .createFile( - "hello.world/module-info.java", - "module hello.world {", - " exports com.example;", - "}" - ); - - var compilation = Compilers - .ecj() - .addModuleSourceRamPaths(sources) - .showDeprecationWarnings(true) - .release(version) - .verbose(true) - .diagnosticLogging(Logging.STACKTRACES) - .fileManagerLogging(Logging.ENABLED) - .compile(); - - CompilationAssert.assertThatCompilation(compilation).isSuccessfulWithoutWarnings(); - - CompilationAssert.assertThatCompilation(compilation) - .classOutput() - .file("hello.world/com/example/HelloWorld.class") - .exists() - .isNotEmptyFile(); - - CompilationAssert.assertThatCompilation(compilation) - .classOutput() - .file("hello.world/module-info.class") - .exists() - .isNotEmptyFile(); - } - - @DisplayName("I can compile multiple modules with javac") - @MethodSource("javacVersions") - @ParameterizedTest(name = "targeting Java {0}") - void helloWorldMultiModuleJavac(int version) { - var sources = RamPath - .createPath("sources") - .createFile( - "hello.world/com/example/HelloWorld.java", + "com/example/HelloWorld.java", "package com.example;", "import com.example.greeter.Greeter;", "public class HelloWorld {", @@ -147,89 +94,15 @@ void helloWorldMultiModuleJavac(int version) { "}" ) .createFile( - "hello.world/module-info.java", + "module-info.java", "module hello.world {", " requires greeter;", " exports com.example;", "}" - ) - .createFile( - "greeter/com/example/greeter/Greeter.java", - "package com.example.greeter;", - "public class Greeter {", - " public static String greet(String name) {", - " return \"Hello, \" + name + \"!\";", - " }", - "}" - ) - .createFile( - "greeter/module-info.java", - "module greeter {", - " exports com.example.greeter;", - "}" ); - - var compilation = Compilers - .javac() - .addModuleSourceRamPaths(sources) - .showDeprecationWarnings(true) - .release(version) - .compile(); - - CompilationAssert.assertThatCompilation(compilation).isSuccessfulWithoutWarnings(); - - CompilationAssert.assertThatCompilation(compilation) - .classOutput() - .file("hello.world/com/example/HelloWorld.class") - .exists() - .isNotEmptyFile(); - - CompilationAssert.assertThatCompilation(compilation) - .classOutput() - .file("hello.world/module-info.class") - .exists() - .isNotEmptyFile(); - - CompilationAssert.assertThatCompilation(compilation) - .classOutput() - .file("greeter/com/example/greeter/Greeter.class") - .exists() - .isNotEmptyFile(); - - CompilationAssert.assertThatCompilation(compilation) - .classOutput() - .file("greeter/module-info.class") - .exists() - .isNotEmptyFile(); - } - - @DisplayName("I can compile multiple modules with ecj") - @MethodSource("ecjVersions") - @ParameterizedTest(name = "targeting Java {0}") - void helloWorldMultiModuleEcj(int version) { - Skipping.skipBecauseEcjFailsToSupportModulesCorrectly(); - - var sources = RamPath - .createPath("sources") + var greeter = createPath("greeter") .createFile( - "hello.world/com/example/HelloWorld.java", - "package com.example;", - "import com.example.greeter.Greeter;", - "public class HelloWorld {", - " public static void main(String[] args) {", - " System.out.println(Greeter.greet(\"World\"));", - " }", - "}" - ) - .createFile( - "hello.world/module-info.java", - "module hello.world {", - " requires greeter;", - " exports com.example;", - "}" - ) - .createFile( - "greeter/com/example/greeter/Greeter.java", + "com/example/greeter/Greeter.java", "package com.example.greeter;", "public class Greeter {", " public static String greet(String name) {", @@ -238,55 +111,43 @@ void helloWorldMultiModuleEcj(int version) { "}" ) .createFile( - "greeter/module-info.java", + "module-info.java", "module greeter {", " exports com.example.greeter;", "}" ); - var compilation = Compilers - .ecj() - .addModuleSourceRamPaths(sources) - .showDeprecationWarnings(true) - .release(version) + var compilation = compiler + .addModuleSourcePath("hello.world", helloWorld) + .addModuleSourcePath("greeter", greeter) .compile(); - CompilationAssert.assertThatCompilation(compilation).isSuccessfulWithoutWarnings(); - - CompilationAssert.assertThatCompilation(compilation) - .classOutput() - .file("hello.world/com/example/HelloWorld.class") - .exists() - .isNotEmptyFile(); - - CompilationAssert.assertThatCompilation(compilation) - .classOutput() - .file("hello.world/module-info.class") - .exists() - .isNotEmptyFile(); - - CompilationAssert.assertThatCompilation(compilation) - .classOutput() - .file("greeter/com/example/greeter/Greeter.class") - .exists() - .isNotEmptyFile(); - - CompilationAssert.assertThatCompilation(compilation) - .classOutput() - .file("greeter/module-info.class") - .exists() - .isNotEmptyFile(); - } - - static IntStream javacVersions() { - return IntStream.rangeClosed(9, SourceVersion.latestSupported().ordinal()); - } - - static IntStream ecjVersions() { - var maxEcjVersion = (ClassFileConstants.getLatestJDKLevel() >> (Short.BYTES * 8)) - - ClassFileConstants.MAJOR_VERSION_0; - - return LongStream.rangeClosed(9, maxEcjVersion) - .mapToInt(i -> (int) i); + assertThatCompilation(compilation) + .isSuccessfulWithoutWarnings(); + + // TODO(ascopes): fix this to work with the file manager rewrite. + //CompilationAssert.assertThatCompilation(compilation) + // .classOutput() + // .file("hello.world/com/example/HelloWorld.class") + // .exists() + // .isNotEmptyFile(); + // + //CompilationAssert.assertThatCompilation(compilation) + // .classOutput() + // .file("hello.world/module-info.class") + // .exists() + // .isNotEmptyFile(); + // + //CompilationAssert.assertThatCompilation(compilation) + // .classOutput() + // .file("greeter/com/example/greeter/Greeter.class") + // .exists() + // .isNotEmptyFile(); + // + //CompilationAssert.assertThatCompilation(compilation) + // .classOutput() + // .file("greeter/module-info.class") + // .exists() + // .isNotEmptyFile(); } } diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/CompilableTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/CompilableTest.java new file mode 100644 index 000000000..3bb5b8590 --- /dev/null +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/CompilableTest.java @@ -0,0 +1,496 @@ +/* + * Copyright (C) 2022 Ashley Scopes + * + * 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.testing.unit.compilers; + +import static io.github.ascopes.jct.testing.helpers.MoreMocks.mockCast; +import static io.github.ascopes.jct.testing.helpers.MoreMocks.stub; +import static java.util.function.Predicate.not; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.withSettings; + +import io.github.ascopes.jct.compilers.Compilable; +import io.github.ascopes.jct.paths.PathLike; +import io.github.ascopes.jct.testing.helpers.TypeRef; +import java.nio.file.Path; +import java.util.stream.Stream; +import javax.lang.model.SourceVersion; +import javax.tools.JavaFileManager.Location; +import javax.tools.StandardLocation; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * {@link Compilable} tests. + * + * @author Ashley Scopes + */ +@DisplayName("Compilable tests") +@ExtendWith(MockitoExtension.class) +class CompilableTest { + + @Mock + Compilable compiler; + + @DisplayName("addClassPath(...) tests") + @TestFactory + Stream addClassPathTests() { + return addPackagePathTestsFor( + "addClassPath", + Compilable::addClassPath, + Compilable::addClassPath, + StandardLocation.CLASS_PATH + ); + } + + @DisplayName("addModulePath(...) tests") + @TestFactory + Stream addModulePathTests() { + return addModulePathTestsFor( + "addModulePath", + Compilable::addModulePath, + Compilable::addModulePath, + StandardLocation.MODULE_PATH + ); + } + + @DisplayName("addSourcePath(...) tests") + @TestFactory + Stream addSourcePathTests() { + return addPackagePathTestsFor( + "addSourcePath", + Compilable::addSourcePath, + Compilable::addSourcePath, + StandardLocation.SOURCE_PATH + ); + } + + @DisplayName("addModuleSourcePath(...) tests") + @TestFactory + Stream addModuleSourcePathTests() { + return addModulePathTestsFor( + "addModuleSourcePath", + Compilable::addModuleSourcePath, + Compilable::addModuleSourcePath, + StandardLocation.MODULE_SOURCE_PATH + ); + } + + @DisplayName("releaseVersion(int) should call releaseVersion(String)") + @ValueSource(ints = {11, 12, 13, 14, 15, 16, 17}) + @ParameterizedTest(name = "for version = {0}") + void releaseVersionIntCallsReleaseVersionString(int versionInt) { + // Given + var versionString = "" + versionInt; + given(compiler.release(anyInt())).willCallRealMethod(); + given(compiler.release(anyString())).will(ctx -> compiler); + + // When + var result = compiler.release(versionInt); + + // Then + then(compiler).should().release(versionString); + assertThat(result).isSameAs(compiler); + } + + @DisplayName("releaseVersion(int) throws an IllegalArgumentException for negative versions") + @ValueSource(ints = {-1, -2, -5, -100_000}) + @ParameterizedTest(name = "for version = {0}") + void releaseVersionIntThrowsIllegalArgumentExceptionForNegativeVersions(int versionInt) { + // Given + given(compiler.release(anyInt())).willCallRealMethod(); + + // Then + assertThatThrownBy(() -> compiler.release(versionInt)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot provide a release version less than 0"); + } + + @DisplayName("releaseVersion(SourceVersion) should call releaseVersion(String)") + @MethodSource("sourceVersions") + @ParameterizedTest(name = "for version = {0}") + void releaseVersionSourceVersionCallsReleaseVersionString( + SourceVersion versionEnum, + String versionString + ) { + // Given + given(compiler.release(any(SourceVersion.class))).willCallRealMethod(); + given(compiler.release(anyString())).will(ctx -> compiler); + + // When + var result = compiler.release(versionEnum); + + // Then + then(compiler).should().release(versionString); + assertThat(result).isSameAs(compiler); + } + + @DisplayName("sourceVersion(int) should call sourceVersion(String)") + @ValueSource(ints = {11, 12, 13, 14, 15, 16, 17}) + @ParameterizedTest(name = "for version = {0}") + void sourceVersionIntCallsReleaseVersionString(int versionInt) { + // Given + var versionString = "" + versionInt; + given(compiler.source(anyInt())).willCallRealMethod(); + given(compiler.source(anyString())).will(ctx -> compiler); + + // When + var result = compiler.source(versionInt); + + // Then + then(compiler).should().source(versionString); + assertThat(result).isSameAs(compiler); + } + + @DisplayName("sourceVersion(int) throws an IllegalArgumentException for negative versions") + @ValueSource(ints = {-1, -2, -5, -100_000}) + @ParameterizedTest(name = "for version = {0}") + void sourceVersionIntThrowsIllegalArgumentExceptionForNegativeVersions(int versionInt) { + // Given + given(compiler.source(anyInt())).willCallRealMethod(); + + // Then + assertThatThrownBy(() -> compiler.source(versionInt)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot provide a source version less than 0"); + } + + @DisplayName("sourceVersion(SourceVersion) should call sourceVersion(String)") + @MethodSource("sourceVersions") + @ParameterizedTest(name = "for version = {0}") + void sourceVersionSourceVersionCallsReleaseVersionString( + SourceVersion versionEnum, + String versionString + ) { + // Given + given(compiler.source(any(SourceVersion.class))).willCallRealMethod(); + given(compiler.source(anyString())).will(ctx -> compiler); + + // When + var result = compiler.source(versionEnum); + + // Then + then(compiler).should().source(versionString); + assertThat(result).isSameAs(compiler); + } + + @DisplayName("targetVersion(int) should call targetVersion(String)") + @ValueSource(ints = {11, 12, 13, 14, 15, 16, 17}) + @ParameterizedTest(name = "for version = {0}") + void targetVersionIntCallsReleaseVersionString(int versionInt) { + // Given + var versionString = "" + versionInt; + given(compiler.target(anyInt())).willCallRealMethod(); + given(compiler.target(anyString())).will(ctx -> compiler); + + // When + var result = compiler.target(versionInt); + + // Then + then(compiler).should().target(versionString); + assertThat(result).isSameAs(compiler); + } + + @DisplayName("targetVersion(int) throws an IllegalArgumentException for negative versions") + @ValueSource(ints = {-1, -2, -5, -100_000}) + @ParameterizedTest(name = "for version = {0}") + void targetVersionIntThrowsIllegalArgumentExceptionForNegativeVersions(int versionInt) { + // Given + given(compiler.target(anyInt())).willCallRealMethod(); + + // Then + assertThatThrownBy(() -> compiler.target(versionInt)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot provide a target version less than 0"); + } + + @DisplayName("targetVersion(SourceVersion) should call targetVersion(String)") + @MethodSource("sourceVersions") + @ParameterizedTest(name = "for version = {0}") + void targetVersionSourceVersionCallsReleaseVersionString( + SourceVersion versionEnum, + String versionString + ) { + // Given + given(compiler.target(any(SourceVersion.class))).willCallRealMethod(); + given(compiler.target(anyString())).will(ctx -> compiler); + + // When + var result = compiler.target(versionEnum); + + // Then + then(compiler).should().target(versionString); + assertThat(result).isSameAs(compiler); + } + + static Stream sourceVersions() { + return Stream + .of(SourceVersion.values()) + .map(version -> Arguments.of(version, "" + version.ordinal())); + } + + static Stream addPackagePathTestsFor( + String name, + AddPackagePathAliasMethod pathLikeAdder, + AddPackagePathAliasMethod pathAdder, + Location location + ) { + var locName = location.getName(); + + var isPackageOriented = dynamicTest( + "expect location for " + name + " to not be module-oriented", + () -> { + // Then + assertThat(location) + .withFailMessage("%s is module oriented!", locName) + .matches(not(Location::isModuleOrientedLocation)); + } + ); + + var isNotOutputLocation = dynamicTest( + "expect location for " + name + " to not be an output location", + () -> { + // Then + assertThat(location) + .withFailMessage("%s is an output location!", locName) + .matches(not(Location::isOutputLocation)); + } + ); + + var pathLikeReturnsCompiler = dynamicTest( + name + "(PathLike) should return the compiler", + () -> { + // Given + var pathLike = stub(PathLike.class); + Compilable compiler = mockCast(new TypeRef<>() {}, withSettings().lenient()); + given(compiler.addPath(any(), any(PathLike.class))).will(ctx -> compiler); + // Stub this method to keep results consistent, even though we shouldn't call it. + // Just keeps the failure test results consistent and meaningful. + given(compiler.addPath(any(), any(), any(Path.class))).will(ctx -> compiler); + given(pathLikeAdder.add(compiler, pathLike)).willCallRealMethod(); + + // When + var result = pathLikeAdder.add(compiler, pathLike); + + // Then + assertThat(result).isSameAs(compiler); + } + ); + + var pathReturnsCompiler = dynamicTest( + name + "(Path) should return the compiler", + () -> { + // Given + var path = stub(Path.class); + Compilable compiler = mockCast(new TypeRef<>() {}, withSettings().lenient()); + given(compiler.addPath(any(), any(Path.class))).will(ctx -> compiler); + // Stub this method to keep results consistent, even though we shouldn't call it. + // Just keeps the failure test results consistent and meaningful. + given(compiler.addPath(any(), any(), any(Path.class))).will(ctx -> compiler); + given(pathAdder.add(compiler, path)).willCallRealMethod(); + + // When + var result = pathAdder.add(compiler, path); + + // Then + assertThat(result).isSameAs(compiler); + } + ); + + var callsAddPathLike = dynamicTest( + name + "(PathLike) should delegate to addPath(" + locName + ", PathLike)", + () -> { + // Given + var pathLike = stub(PathLike.class); + Compilable compiler = mockCast(new TypeRef<>() {}, withSettings().lenient()); + given(compiler.addPath(any(), any(PathLike.class))).will(ctx -> compiler); + given(pathLikeAdder.add(compiler, pathLike)).willCallRealMethod(); + + // When + pathLikeAdder.add(compiler, pathLike); + + // Then + then(compiler).should().addPath(location, pathLike); + } + ); + + var callsAddPath = dynamicTest( + name + "(Path) should delegate to addPath(" + locName + ", Path)", + () -> { + // Given + var path = stub(Path.class); + Compilable compiler = mockCast(new TypeRef<>() {}, withSettings().lenient()); + given(compiler.addPath(any(), any(Path.class))).will(ctx -> compiler); + given(pathAdder.add(compiler, path)).willCallRealMethod(); + + // When + pathAdder.add(compiler, path); + + // Then + then(compiler).should().addPath(location, path); + } + ); + + return Stream.of( + isPackageOriented, + isNotOutputLocation, + pathLikeReturnsCompiler, + pathReturnsCompiler, + callsAddPathLike, + callsAddPath + ); + } + + static Stream addModulePathTestsFor( + String name, + AddModulePathAliasMethod pathLikeAdder, + AddModulePathAliasMethod pathAdder, + Location location + ) { + var locName = location.getName(); + var moduleName = "foobar.baz"; + + var isModuleOriented = dynamicTest( + "expect location for " + name + " to be module-oriented", + () -> { + // Then + assertThat(location) + .withFailMessage("%s is not module-oriented!", locName) + .matches(Location::isModuleOrientedLocation); + } + ); + + var isNotOutputLocation = dynamicTest( + "expect location for " + name + " to not be an output location", + () -> { + // Then + assertThat(location) + .withFailMessage("%s is an output location!", locName) + .matches(not(Location::isOutputLocation)); + } + ); + + var pathLikeReturnsCompiler = dynamicTest( + name + "(String, PathLike) should return the compiler", + () -> { + // Given + var pathLike = stub(PathLike.class); + Compilable compiler = mockCast(new TypeRef<>() {}, withSettings().lenient()); + // Stub this method to keep results consistent, even though we shouldn't call it. + // Just keeps the failure test results consistent and meaningful. + given(compiler.addPath(any(), any(PathLike.class))).will(ctx -> compiler); + given(compiler.addPath(any(), any(), any(PathLike.class))).will(ctx -> compiler); + given(pathLikeAdder.add(compiler, moduleName, pathLike)).willCallRealMethod(); + + // When + var result = pathLikeAdder.add(compiler, moduleName, pathLike); + + // Then + assertThat(result).isSameAs(compiler); + } + ); + + var pathReturnsCompiler = dynamicTest( + name + "(String, Path) should return the compiler", + () -> { + // Given + var path = stub(Path.class); + Compilable compiler = mockCast(new TypeRef<>() {}, withSettings().lenient()); + // Stub this method to keep results consistent, even though we shouldn't call it. + // Just keeps the failure test results consistent and meaningful. + given(compiler.addPath(any(), any(Path.class))).will(ctx -> compiler); + given(compiler.addPath(any(), any(), any(Path.class))).will(ctx -> compiler); + given(pathAdder.add(compiler, moduleName, path)).willCallRealMethod(); + + // When + var result = pathAdder.add(compiler, moduleName, path); + + // Then + assertThat(result).isSameAs(compiler); + } + ); + + var callsAddPathLike = dynamicTest( + name + "(String, PathLike) should delegate to addPath(" + locName + ", String, PathLike)", + () -> { + // Given + var pathLike = stub(PathLike.class); + Compilable compiler = mockCast(new TypeRef<>() {}, withSettings().lenient()); + given(compiler.addPath(any(), any(PathLike.class))).will(ctx -> compiler); + given(pathLikeAdder.add(compiler, moduleName, pathLike)).willCallRealMethod(); + + // When + pathLikeAdder.add(compiler, moduleName, pathLike); + + // Then + then(compiler).should().addPath(location, moduleName, pathLike); + } + ); + + var callsAddPath = dynamicTest( + name + "(String, Path) should delegate to addPath(" + locName + ", String, Path)", + () -> { + // Given + var path = stub(Path.class); + Compilable compiler = mockCast(new TypeRef<>() {}, withSettings().lenient()); + given(compiler.addPath(any(), any(Path.class))).will(ctx -> compiler); + given(pathAdder.add(compiler, moduleName, path)).willCallRealMethod(); + + // When + pathAdder.add(compiler, moduleName, path); + + // Then + then(compiler).should().addPath(location, moduleName, path); + } + ); + + return Stream.of( + isModuleOriented, + isNotOutputLocation, + pathLikeReturnsCompiler, + pathReturnsCompiler, + callsAddPathLike, + callsAddPath + ); + } + + @FunctionalInterface + interface AddPackagePathAliasMethod

{ + + Compilable add(Compilable compiler, P path); + } + + @FunctionalInterface + interface AddModulePathAliasMethod

{ + + Compilable add(Compilable compiler, String moduleName, P path); + } +} diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/CompilerTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/CompilerTest.java deleted file mode 100644 index daf2a98a9..000000000 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/CompilerTest.java +++ /dev/null @@ -1,1346 +0,0 @@ -/* - * Copyright (C) 2022 Ashley Scopes - * - * 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.testing.unit.compilers; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.then; - -import io.github.ascopes.jct.compilers.Compiler; -import io.github.ascopes.jct.paths.RamPath; -import io.github.ascopes.jct.testing.helpers.MoreMocks; -import io.github.ascopes.jct.testing.helpers.TypeRef; -import java.nio.file.Path; -import java.util.Collection; -import java.util.List; -import java.util.stream.Stream; -import javax.annotation.processing.Processor; -import javax.lang.model.SourceVersion; -import javax.tools.JavaFileManager.Location; -import javax.tools.StandardLocation; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -/** - * {@link Compiler} tests. - * - * @author Ashley Scopes - */ -@DisplayName("Compiler tests") -@ExtendWith(MockitoExtension.class) -class CompilerTest { - - @Mock - Compiler compiler; - - @DisplayName("vararg overload for addPaths calls the correct method") - @Test - void varargOverloadForAddPathsCallsCorrectMethod() { - // Given - given(compiler.addPaths(any(), any(), any(), any())).willCallRealMethod(); - given(compiler.addPaths(any(), any())).will(ctx -> compiler); - var location = MoreMocks.stub(Location.class); - var path1 = MoreMocks.stub(Path.class); - var path2 = MoreMocks.stub(Path.class); - var path3 = MoreMocks.stub(Path.class); - - // When - var result = compiler.addPaths(location, path1, path2, path3); - - // Then - then(compiler).should().addPaths(location, List.of(path1, path2, path3)); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("addClassOutputPaths(paths) calls addPaths(CLASS_OUTPUT, paths)") - @Test - void addClassOutputPathsWithIterableCallsAddPaths() { - // Given - given(compiler.addClassOutputPaths(any())).willCallRealMethod(); - given(compiler.addPaths(any(), any())).will(ctx -> compiler); - var paths = MoreMocks.stubCast(new TypeRef>() {}); - - // When - var result = compiler.addClassOutputPaths(paths); - - // Then - then(compiler).should().addPaths(StandardLocation.CLASS_OUTPUT, paths); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("vararg overload for addClassOutputPaths calls varargs overload for addPaths") - @Test - void varargOverloadForAddClassOutputPathsCallsVarargsOverloadsAddPaths() { - // Given - var firstPath = MoreMocks.stub(Path.class); - var secondPath = MoreMocks.stub(Path.class); - var thirdPath = MoreMocks.stub(Path.class); - - given(compiler.addClassOutputPaths(any(), any(), any())).willCallRealMethod(); - given(compiler.addPaths(any(), eq(firstPath), eq(secondPath), eq(thirdPath))) - .will(ctx -> compiler); - - // When - var result = compiler.addClassOutputPaths(firstPath, secondPath, thirdPath); - - // Then - then(compiler).should() - .addPaths(StandardLocation.CLASS_OUTPUT, firstPath, secondPath, thirdPath); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("addSourceOutputPaths(paths) calls addPaths(SOURCE_OUTPUT, paths)") - @Test - void addSourceOutputPathsWithIterableCallsAddPaths() { - // Given - given(compiler.addSourceOutputPaths(any())).willCallRealMethod(); - given(compiler.addPaths(any(), any())).will(ctx -> compiler); - var paths = MoreMocks.stubCast(new TypeRef>() {}); - - // When - var result = compiler.addSourceOutputPaths(paths); - - // Then - then(compiler).should().addPaths(StandardLocation.SOURCE_OUTPUT, paths); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("vararg overload for addSourceOutputPaths calls varargs overload for addPaths") - @Test - void varargOverloadForAddSourceOutputPathsCallsVarargsOverloadsAddPaths() { - // Given - var firstPath = MoreMocks.stub(Path.class); - var secondPath = MoreMocks.stub(Path.class); - var thirdPath = MoreMocks.stub(Path.class); - - given(compiler.addSourceOutputPaths(any(), any(), any())).willCallRealMethod(); - given(compiler.addPaths(any(), eq(firstPath), eq(secondPath), eq(thirdPath))) - .will(ctx -> compiler); - - // When - var result = compiler.addSourceOutputPaths(firstPath, secondPath, thirdPath); - - // Then - then(compiler).should() - .addPaths(StandardLocation.SOURCE_OUTPUT, firstPath, secondPath, thirdPath); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("addClassPaths(paths) calls addPaths(CLASS_PATH, paths)") - @Test - void addClassPathsWithIterableCallsAddPaths() { - // Given - given(compiler.addClassPaths(any())).willCallRealMethod(); - given(compiler.addPaths(any(), any())).will(ctx -> compiler); - var paths = MoreMocks.stubCast(new TypeRef>() {}); - - // When - var result = compiler.addClassPaths(paths); - - // Then - then(compiler).should().addPaths(StandardLocation.CLASS_PATH, paths); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("vararg overload for addClassPaths calls varargs overload for addPaths") - @Test - void varargOverloadForAddClassPathsCallsVarargsOverloadsAddPaths() { - // Given - var firstPath = MoreMocks.stub(Path.class); - var secondPath = MoreMocks.stub(Path.class); - var thirdPath = MoreMocks.stub(Path.class); - - given(compiler.addClassPaths(any(), any(), any())).willCallRealMethod(); - given(compiler.addPaths(any(), eq(firstPath), eq(secondPath), eq(thirdPath))) - .will(ctx -> compiler); - - // When - var result = compiler.addClassPaths(firstPath, secondPath, thirdPath); - - // Then - then(compiler).should().addPaths(StandardLocation.CLASS_PATH, firstPath, secondPath, thirdPath); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("addSourcePaths(paths) calls addPaths(SOURCE_PATH, paths)") - @Test - void addSourcePathsWithIterableCallsAddPaths() { - // Given - given(compiler.addSourcePaths(any())).willCallRealMethod(); - given(compiler.addPaths(any(), any())).will(ctx -> compiler); - var paths = MoreMocks.stubCast(new TypeRef>() {}); - - // When - var result = compiler.addSourcePaths(paths); - - // Then - then(compiler).should().addPaths(StandardLocation.SOURCE_PATH, paths); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("vararg overload for addSourcePaths calls varargs overload for addPaths") - @Test - void varargOverloadForAddSourcePathsCallsVarargsOverloadsAddPaths() { - // Given - var firstPath = MoreMocks.stub(Path.class); - var secondPath = MoreMocks.stub(Path.class); - var thirdPath = MoreMocks.stub(Path.class); - - given(compiler.addSourcePaths(any(), any(), any())).willCallRealMethod(); - given(compiler.addPaths(any(), eq(firstPath), eq(secondPath), eq(thirdPath))) - .will(ctx -> compiler); - - // When - var result = compiler.addSourcePaths(firstPath, secondPath, thirdPath); - - // Then - then(compiler).should() - .addPaths(StandardLocation.SOURCE_PATH, firstPath, secondPath, thirdPath); - assertThat(result).isSameAs(compiler); - } - - @DisplayName( - "addAnnotationProcessorPaths(paths) calls addPaths(ANNOTATION_PROCESSOR_PATH, paths)" - ) - @Test - void addAnnotationProcessorPathsWithIterableCallsAddPaths() { - // Given - given(compiler.addAnnotationProcessorPaths(any())).willCallRealMethod(); - given(compiler.addPaths(any(), any())).will(ctx -> compiler); - var paths = MoreMocks.stubCast(new TypeRef>() {}); - - // When - var result = compiler.addAnnotationProcessorPaths(paths); - - // Then - then(compiler).should().addPaths(StandardLocation.ANNOTATION_PROCESSOR_PATH, paths); - assertThat(result).isSameAs(compiler); - } - - @DisplayName( - "vararg overload for addAnnotationProcessorPaths calls varargs overload for addPaths" - ) - @Test - void varargOverloadForAddAnnotationProcessorPathsCallsVarargsOverloadsAddPaths() { - // Given - var firstPath = MoreMocks.stub(Path.class); - var secondPath = MoreMocks.stub(Path.class); - var thirdPath = MoreMocks.stub(Path.class); - - given(compiler.addAnnotationProcessorPaths(any(), any(), any())).willCallRealMethod(); - given(compiler.addPaths(any(), eq(firstPath), eq(secondPath), eq(thirdPath))) - .will(ctx -> compiler); - - // When - var result = compiler.addAnnotationProcessorPaths(firstPath, secondPath, thirdPath); - - // Then - then(compiler).should() - .addPaths(StandardLocation.ANNOTATION_PROCESSOR_PATH, firstPath, secondPath, thirdPath); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("addAnnotationProcessorModulePaths(paths) calls " - + "addPaths(ANNOTATION_PROCESSOR_MODULE_PATH, paths)") - @Test - void addAnnotationProcessorModulePathsWithIterableCallsAddPaths() { - // Given - given(compiler.addAnnotationProcessorModulePaths(any())).willCallRealMethod(); - given(compiler.addPaths(any(), any())).will(ctx -> compiler); - var paths = MoreMocks.stubCast(new TypeRef>() {}); - - // When - var result = compiler.addAnnotationProcessorModulePaths(paths); - - // Then - then(compiler).should().addPaths(StandardLocation.ANNOTATION_PROCESSOR_MODULE_PATH, paths); - assertThat(result).isSameAs(compiler); - } - - @DisplayName( - "vararg overload for addAnnotationProcessorModulePaths calls varargs overload for addPaths" - ) - @Test - void varargOverloadForAddAnnotationProcessorModulePathsCallsVarargsOverloadsAddPaths() { - // Given - var firstPath = MoreMocks.stub(Path.class); - var secondPath = MoreMocks.stub(Path.class); - var thirdPath = MoreMocks.stub(Path.class); - - given(compiler.addAnnotationProcessorModulePaths(any(), any(), any())).willCallRealMethod(); - given(compiler.addPaths(any(), eq(firstPath), eq(secondPath), eq(thirdPath))) - .will(ctx -> compiler); - - // When - var result = compiler.addAnnotationProcessorModulePaths(firstPath, secondPath, thirdPath); - - // Then - then(compiler).should() - .addPaths(StandardLocation.ANNOTATION_PROCESSOR_MODULE_PATH, firstPath, secondPath, - thirdPath); - assertThat(result).isSameAs(compiler); - } - - @DisplayName( - "addPlatformClassPaths(paths) calls addPaths(PLATFORM_CLASS_PATH, paths)" - ) - @Test - void addPlatformClassPathsWithIterableCallsAddPaths() { - // Given - given(compiler.addPlatformClassPaths(any())).willCallRealMethod(); - given(compiler.addPaths(any(), any())).will(ctx -> compiler); - var paths = MoreMocks.stubCast(new TypeRef>() {}); - - // When - var result = compiler.addPlatformClassPaths(paths); - - // Then - then(compiler).should().addPaths(StandardLocation.PLATFORM_CLASS_PATH, paths); - assertThat(result).isSameAs(compiler); - } - - @DisplayName( - "vararg overload for addPlatformClassPaths calls varargs overload for addPaths" - ) - @Test - void varargOverloadForAddPlatformClassPathsCallsVarargsOverloadsAddPaths() { - // Given - var firstPath = MoreMocks.stub(Path.class); - var secondPath = MoreMocks.stub(Path.class); - var thirdPath = MoreMocks.stub(Path.class); - - given(compiler.addPlatformClassPaths(any(), any(), any())).willCallRealMethod(); - given(compiler.addPaths(any(), eq(firstPath), eq(secondPath), eq(thirdPath))) - .will(ctx -> compiler); - - // When - var result = compiler.addPlatformClassPaths(firstPath, secondPath, thirdPath); - - // Then - then(compiler).should() - .addPaths(StandardLocation.PLATFORM_CLASS_PATH, firstPath, secondPath, thirdPath); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("addNativeHeaderOutputPaths(paths) calls addPaths(NATIVE_HEADER_OUTPUT, paths)") - @Test - void addNativeHeaderOutputPathsWithIterableCallsAddPaths() { - // Given - given(compiler.addNativeHeaderOutputPaths(any())).willCallRealMethod(); - given(compiler.addPaths(any(), any())).will(ctx -> compiler); - var paths = MoreMocks.stubCast(new TypeRef>() {}); - - // When - var result = compiler.addNativeHeaderOutputPaths(paths); - - // Then - then(compiler).should().addPaths(StandardLocation.NATIVE_HEADER_OUTPUT, paths); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("vararg overload for addNativeHeaderOutputPaths calls varargs overload for addPaths") - @Test - void varargOverloadForAddNativeHeaderOutputPathsCallsVarargsOverloadsAddPaths() { - // Given - var firstPath = MoreMocks.stub(Path.class); - var secondPath = MoreMocks.stub(Path.class); - var thirdPath = MoreMocks.stub(Path.class); - - given(compiler.addNativeHeaderOutputPaths(any(), any(), any())).willCallRealMethod(); - given(compiler.addPaths(any(), eq(firstPath), eq(secondPath), eq(thirdPath))) - .will(ctx -> compiler); - - // When - var result = compiler.addNativeHeaderOutputPaths(firstPath, secondPath, thirdPath); - - // Then - then(compiler).should() - .addPaths(StandardLocation.NATIVE_HEADER_OUTPUT, firstPath, secondPath, thirdPath); - assertThat(result).isSameAs(compiler); - } - - @DisplayName( - "addModuleSourcePaths(paths) calls addPaths(MODULE_SOURCE_PATH, paths)" - ) - @Test - void addModuleSourcePathsWithIterableCallsAddPaths() { - // Given - given(compiler.addModuleSourcePaths(any())).willCallRealMethod(); - given(compiler.addPaths(any(), any())).will(ctx -> compiler); - var paths = MoreMocks.stubCast(new TypeRef>() {}); - - // When - var result = compiler.addModuleSourcePaths(paths); - - // Then - then(compiler).should().addPaths(StandardLocation.MODULE_SOURCE_PATH, paths); - assertThat(result).isSameAs(compiler); - } - - @DisplayName( - "vararg overload for addModuleSourcePaths calls varargs overload for addPaths" - ) - @Test - void varargOverloadForAddModuleSourcePathsCallsVarargsOverloadsAddPaths() { - // Given - var firstPath = MoreMocks.stub(Path.class); - var secondPath = MoreMocks.stub(Path.class); - var thirdPath = MoreMocks.stub(Path.class); - - given(compiler.addModuleSourcePaths(any(), any(), any())).willCallRealMethod(); - given(compiler.addPaths(any(), eq(firstPath), eq(secondPath), eq(thirdPath))) - .will(ctx -> compiler); - - // When - var result = compiler.addModuleSourcePaths(firstPath, secondPath, thirdPath); - - // Then - then(compiler).should() - .addPaths(StandardLocation.MODULE_SOURCE_PATH, firstPath, secondPath, thirdPath); - assertThat(result).isSameAs(compiler); - } - - @DisplayName( - "addUpgradeModulePaths(paths) calls addPaths(UPGRADE_MODULE_PATH, paths)" - ) - @Test - void addUpgradeModulePathsWithIterableCallsAddPaths() { - // Given - given(compiler.addUpgradeModulePaths(any())).willCallRealMethod(); - given(compiler.addPaths(any(), any())).will(ctx -> compiler); - var paths = MoreMocks.stubCast(new TypeRef>() {}); - - // When - var result = compiler.addUpgradeModulePaths(paths); - - // Then - then(compiler).should().addPaths(StandardLocation.UPGRADE_MODULE_PATH, paths); - assertThat(result).isSameAs(compiler); - } - - @DisplayName( - "vararg overload for addUpgradeModulePaths calls varargs overload for addPaths" - ) - @Test - void varargOverloadForAddUpgradeModulePathsCallsVarargsOverloadsAddPaths() { - // Given - var firstPath = MoreMocks.stub(Path.class); - var secondPath = MoreMocks.stub(Path.class); - var thirdPath = MoreMocks.stub(Path.class); - - given(compiler.addUpgradeModulePaths(any(), any(), any())).willCallRealMethod(); - given(compiler.addPaths(any(), eq(firstPath), eq(secondPath), eq(thirdPath))) - .will(ctx -> compiler); - - // When - var result = compiler.addUpgradeModulePaths(firstPath, secondPath, thirdPath); - - // Then - then(compiler).should() - .addPaths(StandardLocation.UPGRADE_MODULE_PATH, firstPath, secondPath, thirdPath); - assertThat(result).isSameAs(compiler); - } - - @DisplayName( - "addSystemModulePaths(paths) calls addPaths(SYSTEM_MODULES, paths)" - ) - @Test - void addSystemModulePathsWithIterableCallsAddPaths() { - // Given - given(compiler.addSystemModulePaths(any())).willCallRealMethod(); - given(compiler.addPaths(any(), any())).will(ctx -> compiler); - var paths = MoreMocks.stubCast(new TypeRef>() {}); - - // When - var result = compiler.addSystemModulePaths(paths); - - // Then - then(compiler).should().addPaths(StandardLocation.SYSTEM_MODULES, paths); - assertThat(result).isSameAs(compiler); - } - - @DisplayName( - "vararg overload for addSystemModulePaths calls varargs overload for addPaths" - ) - @Test - void varargOverloadForAddSystemModulePathsCallsVarargsOverloadsAddPaths() { - // Given - var firstPath = MoreMocks.stub(Path.class); - var secondPath = MoreMocks.stub(Path.class); - var thirdPath = MoreMocks.stub(Path.class); - - given(compiler.addSystemModulePaths(any(), any(), any())).willCallRealMethod(); - given(compiler.addPaths(any(), eq(firstPath), eq(secondPath), eq(thirdPath))) - .will(ctx -> compiler); - - // When - var result = compiler.addSystemModulePaths(firstPath, secondPath, thirdPath); - - // Then - then(compiler).should() - .addPaths(StandardLocation.SYSTEM_MODULES, firstPath, secondPath, thirdPath); - assertThat(result).isSameAs(compiler); - } - - @DisplayName( - "addModulePaths(paths) calls addPaths(MODULE_PATH, paths)" - ) - @Test - void addModulePathsWithIterableCallsAddPaths() { - // Given - given(compiler.addModulePaths(any())).willCallRealMethod(); - given(compiler.addPaths(any(), any())).will(ctx -> compiler); - var paths = MoreMocks.stubCast(new TypeRef>() {}); - - // When - var result = compiler.addModulePaths(paths); - - // Then - then(compiler).should().addPaths(StandardLocation.MODULE_PATH, paths); - assertThat(result).isSameAs(compiler); - } - - @DisplayName( - "vararg overload for addModulePaths calls varargs overload for addPaths" - ) - @Test - void varargOverloadForAddModulePathsCallsVarargsOverloadsAddPaths() { - // Given - var firstPath = MoreMocks.stub(Path.class); - var secondPath = MoreMocks.stub(Path.class); - var thirdPath = MoreMocks.stub(Path.class); - - given(compiler.addModulePaths(any(), any(), any())).willCallRealMethod(); - given(compiler.addPaths(any(), eq(firstPath), eq(secondPath), eq(thirdPath))) - .will(ctx -> compiler); - - // When - var result = compiler.addModulePaths(firstPath, secondPath, thirdPath); - - // Then - then(compiler).should() - .addPaths(StandardLocation.MODULE_PATH, firstPath, secondPath, thirdPath); - assertThat(result).isSameAs(compiler); - } - - @DisplayName( - "addPatchModulePaths(paths) calls addPaths(PATCH_MODULE_PATH, paths)" - ) - @Test - void addPatchModulePathsWithIterableCallsAddPaths() { - // Given - given(compiler.addPatchModulePaths(any())).willCallRealMethod(); - given(compiler.addPaths(any(), any())).will(ctx -> compiler); - var paths = MoreMocks.stubCast(new TypeRef>() {}); - - // When - var result = compiler.addPatchModulePaths(paths); - - // Then - then(compiler).should().addPaths(StandardLocation.PATCH_MODULE_PATH, paths); - assertThat(result).isSameAs(compiler); - } - - @DisplayName( - "vararg overload for addPatchModulePaths calls varargs overload for addPaths" - ) - @Test - void varargOverloadForAddPatchModulePathsCallsVarargsOverloadsAddPaths() { - // Given - var firstPath = MoreMocks.stub(Path.class); - var secondPath = MoreMocks.stub(Path.class); - var thirdPath = MoreMocks.stub(Path.class); - - given(compiler.addPatchModulePaths(any(), any(), any())).willCallRealMethod(); - given(compiler.addPaths(any(), eq(firstPath), eq(secondPath), eq(thirdPath))) - .will(ctx -> compiler); - - // When - var result = compiler.addPatchModulePaths(firstPath, secondPath, thirdPath); - - // Then - then(compiler).should() - .addPaths(StandardLocation.PATCH_MODULE_PATH, firstPath, secondPath, thirdPath); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("vararg overload for addRamPaths calls the correct method") - @Test - void varargOverloadForAddRamPathsCallsCorrectMethod() { - // Given - given(compiler.addRamPaths(any(), any(), any(), any())).willCallRealMethod(); - given(compiler.addRamPaths(any(), any())).will(ctx -> compiler); - var location = MoreMocks.stub(Location.class); - var ramPath1 = MoreMocks.stub(RamPath.class); - var ramPath2 = MoreMocks.stub(RamPath.class); - var ramPath3 = MoreMocks.stub(RamPath.class); - - // When - var result = compiler.addRamPaths(location, ramPath1, ramPath2, ramPath3); - - // Then - then(compiler).should().addRamPaths(location, List.of(ramPath1, ramPath2, ramPath3)); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("addClassOutputRamPaths(paths) calls addPaths(CLASS_OUTPUT, paths)") - @Test - void addClassOutputRamPathsWithIterableCallsAddPaths() { - // Given - given(compiler.addClassOutputRamPaths(any())).willCallRealMethod(); - given(compiler.addRamPaths(any(), any())).will(ctx -> compiler); - var ramPaths = MoreMocks.stubCast(new TypeRef>() {}); - - // When - var result = compiler.addClassOutputRamPaths(ramPaths); - - // Then - then(compiler).should().addRamPaths(StandardLocation.CLASS_OUTPUT, ramPaths); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("vararg overload for addClassOutputRamPaths calls varargs overload for addPaths") - @Test - void varargOverloadForAddClassOutputRamPathsCallsVarargsOverloadsAddPaths() { - // Given - var firstRamPath = MoreMocks.stub(RamPath.class); - var secondRamPath = MoreMocks.stub(RamPath.class); - var thirdRamPath = MoreMocks.stub(RamPath.class); - - given(compiler.addClassOutputRamPaths(any(), any(), any())).willCallRealMethod(); - given(compiler.addRamPaths(any(), eq(firstRamPath), eq(secondRamPath), eq(thirdRamPath))) - .will(ctx -> compiler); - - // When - var result = compiler.addClassOutputRamPaths(firstRamPath, secondRamPath, thirdRamPath); - - // Then - then(compiler).should() - .addRamPaths(StandardLocation.CLASS_OUTPUT, firstRamPath, secondRamPath, thirdRamPath); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("addSourceOutputRamPaths(paths) calls addPaths(SOURCE_OUTPUT, paths)") - @Test - void addSourceOutputRamPathsWithIterableCallsAddPaths() { - // Given - given(compiler.addSourceOutputRamPaths(any())).willCallRealMethod(); - given(compiler.addRamPaths(any(), any())).will(ctx -> compiler); - var ramPaths = MoreMocks.stubCast(new TypeRef>() {}); - - // When - var result = compiler.addSourceOutputRamPaths(ramPaths); - - // Then - then(compiler).should().addRamPaths(StandardLocation.SOURCE_OUTPUT, ramPaths); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("vararg overload for addSourceOutputRamPaths calls varargs overload for addPaths") - @Test - void varargOverloadForAddSourceOutputRamPathsCallsVarargsOverloadsAddPaths() { - // Given - var firstRamPath = MoreMocks.stub(RamPath.class); - var secondRamPath = MoreMocks.stub(RamPath.class); - var thirdRamPath = MoreMocks.stub(RamPath.class); - - given(compiler.addSourceOutputRamPaths(any(), any(), any())).willCallRealMethod(); - given(compiler.addRamPaths(any(), eq(firstRamPath), eq(secondRamPath), eq(thirdRamPath))) - .will(ctx -> compiler); - - // When - var result = compiler.addSourceOutputRamPaths(firstRamPath, secondRamPath, thirdRamPath); - - // Then - then(compiler).should() - .addRamPaths(StandardLocation.SOURCE_OUTPUT, firstRamPath, secondRamPath, thirdRamPath); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("addClassRamPaths(ramPaths) calls addRamPaths(CLASS_PATH, ramPaths)") - @Test - void addClassRamPathsWithIterableCallsAddRamPaths() { - // Given - given(compiler.addClassRamPaths(any())).willCallRealMethod(); - given(compiler.addRamPaths(any(), any())).will(ctx -> compiler); - var ramPaths = MoreMocks.stubCast(new TypeRef>() {}); - - // When - var result = compiler.addClassRamPaths(ramPaths); - - // Then - then(compiler).should().addRamPaths(StandardLocation.CLASS_PATH, ramPaths); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("vararg overload for addClassRamPaths calls varargs overload for addRamPaths") - @Test - void varargOverloadForAddClassRamPathsCallsVarargsOverloadsAddRamPaths() { - // Given - var firstRamPath = MoreMocks.stub(RamPath.class); - var secondRamPath = MoreMocks.stub(RamPath.class); - var thirdRamPath = MoreMocks.stub(RamPath.class); - - given(compiler.addClassRamPaths(any(), any(), any())).willCallRealMethod(); - given(compiler.addRamPaths(any(), eq(firstRamPath), eq(secondRamPath), eq(thirdRamPath))) - .will(ctx -> compiler); - - // When - var result = compiler.addClassRamPaths(firstRamPath, secondRamPath, thirdRamPath); - - // Then - then(compiler).should() - .addRamPaths(StandardLocation.CLASS_PATH, firstRamPath, secondRamPath, thirdRamPath); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("addSourceRamPaths(ramPaths) calls addRamPaths(SOURCE_PATH, ramPaths)") - @Test - void addSourceRamPathsWithIterableCallsAddRamPaths() { - // Given - given(compiler.addSourceRamPaths(any())).willCallRealMethod(); - given(compiler.addRamPaths(any(), any())).will(ctx -> compiler); - var ramPaths = MoreMocks.stubCast(new TypeRef>() {}); - - // When - var result = compiler.addSourceRamPaths(ramPaths); - - // Then - then(compiler).should().addRamPaths(StandardLocation.SOURCE_PATH, ramPaths); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("vararg overload for addSourceRamPaths calls varargs overload for addRamPaths") - @Test - void varargOverloadForAddSourceRamPathsCallsVarargsOverloadsAddRamPaths() { - // Given - var firstRamPath = MoreMocks.stub(RamPath.class); - var secondRamPath = MoreMocks.stub(RamPath.class); - var thirdRamPath = MoreMocks.stub(RamPath.class); - - given(compiler.addSourceRamPaths(any(), any(), any())).willCallRealMethod(); - given(compiler.addRamPaths(any(), eq(firstRamPath), eq(secondRamPath), eq(thirdRamPath))) - .will(ctx -> compiler); - - // When - var result = compiler.addSourceRamPaths(firstRamPath, secondRamPath, thirdRamPath); - - // Then - then(compiler).should() - .addRamPaths(StandardLocation.SOURCE_PATH, firstRamPath, secondRamPath, thirdRamPath); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("addAnnotationProcessorRamPaths(ramPaths) calls " - + "addRamPaths(ANNOTATION_PROCESSOR_PATH, ramPaths)") - @Test - void addAnnotationProcessorRamPathsWithIterableCallsAddRamPaths() { - // Given - given(compiler.addAnnotationProcessorRamPaths(any())).willCallRealMethod(); - given(compiler.addRamPaths(any(), any())).will(ctx -> compiler); - var ramPaths = MoreMocks.stubCast(new TypeRef>() {}); - - // When - var result = compiler.addAnnotationProcessorRamPaths(ramPaths); - - // Then - then(compiler).should().addRamPaths(StandardLocation.ANNOTATION_PROCESSOR_PATH, ramPaths); - assertThat(result).isSameAs(compiler); - } - - @DisplayName( - "vararg overload for addAnnotationProcessorRamPaths calls varargs overload for addRamPaths" - ) - @Test - void varargOverloadForAddAnnotationProcessorRamPathsCallsVarargsOverloadsAddRamPaths() { - // Given - var firstRamPath = MoreMocks.stub(RamPath.class); - var secondRamPath = MoreMocks.stub(RamPath.class); - var thirdRamPath = MoreMocks.stub(RamPath.class); - - given(compiler.addAnnotationProcessorRamPaths(any(), any(), any())).willCallRealMethod(); - given(compiler.addRamPaths(any(), eq(firstRamPath), eq(secondRamPath), eq(thirdRamPath))) - .will(ctx -> compiler); - - // When - var result = compiler.addAnnotationProcessorRamPaths(firstRamPath, secondRamPath, thirdRamPath); - - // Then - then(compiler).should() - .addRamPaths(StandardLocation.ANNOTATION_PROCESSOR_PATH, firstRamPath, secondRamPath, - thirdRamPath); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("addAnnotationProcessorModuleRamPaths(ramPaths) calls " - + "addRamPaths(ANNOTATION_PROCESSOR_MODULE_PATH, ramPaths)") - @Test - void addAnnotationProcessorModuleRamPathsWithIterableCallsAddRamPaths() { - // Given - given(compiler.addAnnotationProcessorModuleRamPaths(any())).willCallRealMethod(); - given(compiler.addRamPaths(any(), any())).will(ctx -> compiler); - var ramPaths = MoreMocks.stubCast(new TypeRef>() {}); - - // When - var result = compiler.addAnnotationProcessorModuleRamPaths(ramPaths); - - // Then - then(compiler).should() - .addRamPaths(StandardLocation.ANNOTATION_PROCESSOR_MODULE_PATH, ramPaths); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("vararg overload for addAnnotationProcessorModuleRamPaths calls varargs overload " - + "for addRamPaths") - @Test - void varargOverloadForAddAnnotationProcessorModuleRamPathsCallsVarargsOverloadsAddRamPaths() { - // Given - var firstRamPath = MoreMocks.stub(RamPath.class); - var secondRamPath = MoreMocks.stub(RamPath.class); - var thirdRamPath = MoreMocks.stub(RamPath.class); - - given(compiler.addAnnotationProcessorModuleRamPaths(any(), any(), any())).willCallRealMethod(); - given(compiler.addRamPaths(any(), eq(firstRamPath), eq(secondRamPath), eq(thirdRamPath))) - .will(ctx -> compiler); - - // When - var result = compiler.addAnnotationProcessorModuleRamPaths(firstRamPath, secondRamPath, - thirdRamPath); - - // Then - then(compiler).should() - .addRamPaths(StandardLocation.ANNOTATION_PROCESSOR_MODULE_PATH, firstRamPath, secondRamPath, - thirdRamPath); - assertThat(result).isSameAs(compiler); - } - - @DisplayName( - "addPlatformClassRamPaths(ramPaths) calls addRamPaths(PLATFORM_CLASS_PATH, ramPaths)" - ) - @Test - void addPlatformClassRamPathsWithIterableCallsAddRamPaths() { - // Given - given(compiler.addPlatformClassRamPaths(any())).willCallRealMethod(); - given(compiler.addRamPaths(any(), any())).will(ctx -> compiler); - var ramPaths = MoreMocks.stubCast(new TypeRef>() {}); - - // When - var result = compiler.addPlatformClassRamPaths(ramPaths); - - // Then - then(compiler).should().addRamPaths(StandardLocation.PLATFORM_CLASS_PATH, ramPaths); - assertThat(result).isSameAs(compiler); - } - - @DisplayName( - "vararg overload for addPlatformClassRamPaths calls varargs overload for addRamPaths" - ) - @Test - void varargOverloadForAddPlatformClassRamPathsCallsVarargsOverloadsAddRamPaths() { - // Given - var firstRamPath = MoreMocks.stub(RamPath.class); - var secondRamPath = MoreMocks.stub(RamPath.class); - var thirdRamPath = MoreMocks.stub(RamPath.class); - - given(compiler.addPlatformClassRamPaths(any(), any(), any())).willCallRealMethod(); - given(compiler.addRamPaths(any(), eq(firstRamPath), eq(secondRamPath), eq(thirdRamPath))) - .will(ctx -> compiler); - - // When - var result = compiler.addPlatformClassRamPaths(firstRamPath, secondRamPath, thirdRamPath); - - // Then - then(compiler).should() - .addRamPaths(StandardLocation.PLATFORM_CLASS_PATH, firstRamPath, secondRamPath, - thirdRamPath); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("addNativeHeaderOutputRamPaths(paths) calls addPaths(NATIVE_HEADER_OUTPUT, paths)") - @Test - void addNativeHeaderOutputRamPathsWithIterableCallsAddPaths() { - // Given - given(compiler.addNativeHeaderOutputRamPaths(any())).willCallRealMethod(); - given(compiler.addRamPaths(any(), any())).will(ctx -> compiler); - var ramPaths = MoreMocks.stubCast(new TypeRef>() {}); - - // When - var result = compiler.addNativeHeaderOutputRamPaths(ramPaths); - - // Then - then(compiler).should().addRamPaths(StandardLocation.NATIVE_HEADER_OUTPUT, ramPaths); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("vararg overload for addNativeHeaderOutputRamPaths calls " - + "varargs overload for addPaths") - @Test - void varargOverloadForAddNativeHeaderOutputRamPathsCallsVarargsOverloadsAddPaths() { - // Given - var firstRamPath = MoreMocks.stub(RamPath.class); - var secondRamPath = MoreMocks.stub(RamPath.class); - var thirdRamPath = MoreMocks.stub(RamPath.class); - - given(compiler.addNativeHeaderOutputRamPaths(any(), any(), any())).willCallRealMethod(); - given(compiler.addRamPaths(any(), eq(firstRamPath), eq(secondRamPath), eq(thirdRamPath))) - .will(ctx -> compiler); - - // When - var result = compiler.addNativeHeaderOutputRamPaths(firstRamPath, secondRamPath, thirdRamPath); - - // Then - then(compiler).should().addRamPaths( - StandardLocation.NATIVE_HEADER_OUTPUT, firstRamPath, secondRamPath, thirdRamPath); - assertThat(result).isSameAs(compiler); - } - - @DisplayName( - "addModuleSourceRamPaths(ramPaths) calls addRamPaths(MODULE_SOURCE_PATH, ramPaths)" - ) - @Test - void addModuleSourceRamPathsWithIterableCallsAddRamPaths() { - // Given - given(compiler.addModuleSourceRamPaths(any())).willCallRealMethod(); - given(compiler.addRamPaths(any(), any())).will(ctx -> compiler); - var ramPaths = MoreMocks.stubCast(new TypeRef>() {}); - - // When - var result = compiler.addModuleSourceRamPaths(ramPaths); - - // Then - then(compiler).should().addRamPaths(StandardLocation.MODULE_SOURCE_PATH, ramPaths); - assertThat(result).isSameAs(compiler); - } - - @DisplayName( - "vararg overload for addModuleSourceRamPaths calls varargs overload for addRamPaths" - ) - @Test - void varargOverloadForAddModuleSourceRamPathsCallsVarargsOverloadsAddRamPaths() { - // Given - var firstRamPath = MoreMocks.stub(RamPath.class); - var secondRamPath = MoreMocks.stub(RamPath.class); - var thirdRamPath = MoreMocks.stub(RamPath.class); - - given(compiler.addModuleSourceRamPaths(any(), any(), any())).willCallRealMethod(); - given(compiler.addRamPaths(any(), eq(firstRamPath), eq(secondRamPath), eq(thirdRamPath))) - .will(ctx -> compiler); - - // When - var result = compiler.addModuleSourceRamPaths(firstRamPath, secondRamPath, thirdRamPath); - - // Then - then(compiler).should() - .addRamPaths(StandardLocation.MODULE_SOURCE_PATH, firstRamPath, secondRamPath, - thirdRamPath); - assertThat(result).isSameAs(compiler); - } - - @DisplayName( - "addUpgradeModuleRamPaths(ramPaths) calls addRamPaths(UPGRADE_MODULE_PATH, ramPaths)" - ) - @Test - void addUpgradeModuleRamPathsWithIterableCallsAddRamPaths() { - // Given - given(compiler.addUpgradeModuleRamPaths(any())).willCallRealMethod(); - given(compiler.addRamPaths(any(), any())).will(ctx -> compiler); - var ramPaths = MoreMocks.stubCast(new TypeRef>() {}); - - // When - var result = compiler.addUpgradeModuleRamPaths(ramPaths); - - // Then - then(compiler).should().addRamPaths(StandardLocation.UPGRADE_MODULE_PATH, ramPaths); - assertThat(result).isSameAs(compiler); - } - - @DisplayName( - "vararg overload for addUpgradeModuleRamPaths calls varargs overload for addRamPaths" - ) - @Test - void varargOverloadForAddUpgradeModuleRamPathsCallsVarargsOverloadsAddRamPaths() { - // Given - var firstRamPath = MoreMocks.stub(RamPath.class); - var secondRamPath = MoreMocks.stub(RamPath.class); - var thirdRamPath = MoreMocks.stub(RamPath.class); - - given(compiler.addUpgradeModuleRamPaths(any(), any(), any())).willCallRealMethod(); - given(compiler.addRamPaths(any(), eq(firstRamPath), eq(secondRamPath), eq(thirdRamPath))) - .will(ctx -> compiler); - - // When - var result = compiler.addUpgradeModuleRamPaths(firstRamPath, secondRamPath, thirdRamPath); - - // Then - then(compiler).should() - .addRamPaths(StandardLocation.UPGRADE_MODULE_PATH, firstRamPath, secondRamPath, - thirdRamPath); - assertThat(result).isSameAs(compiler); - } - - @DisplayName( - "addSystemModuleRamPaths(ramPaths) calls addRamPaths(SYSTEM_MODULES, ramPaths)" - ) - @Test - void addSystemModuleRamPathsWithIterableCallsAddRamPaths() { - // Given - given(compiler.addSystemModuleRamPaths(any())).willCallRealMethod(); - given(compiler.addRamPaths(any(), any())).will(ctx -> compiler); - var ramPaths = MoreMocks.stubCast(new TypeRef>() {}); - - // When - var result = compiler.addSystemModuleRamPaths(ramPaths); - - // Then - then(compiler).should().addRamPaths(StandardLocation.SYSTEM_MODULES, ramPaths); - assertThat(result).isSameAs(compiler); - } - - @DisplayName( - "vararg overload for addSystemModuleRamPaths calls varargs overload for addRamPaths" - ) - @Test - void varargOverloadForAddSystemModuleRamPathsCallsVarargsOverloadsAddRamPaths() { - // Given - var firstRamPath = MoreMocks.stub(RamPath.class); - var secondRamPath = MoreMocks.stub(RamPath.class); - var thirdRamPath = MoreMocks.stub(RamPath.class); - - given(compiler.addSystemModuleRamPaths(any(), any(), any())).willCallRealMethod(); - given(compiler.addRamPaths(any(), eq(firstRamPath), eq(secondRamPath), eq(thirdRamPath))) - .will(ctx -> compiler); - - // When - var result = compiler.addSystemModuleRamPaths(firstRamPath, secondRamPath, thirdRamPath); - - // Then - then(compiler).should() - .addRamPaths(StandardLocation.SYSTEM_MODULES, firstRamPath, secondRamPath, thirdRamPath); - assertThat(result).isSameAs(compiler); - } - - @DisplayName( - "addModuleRamPaths(ramPaths) calls addRamPaths(MODULE_PATH, ramPaths)" - ) - @Test - void addModuleRamPathsWithIterableCallsAddRamPaths() { - // Given - given(compiler.addModuleRamPaths(any())).willCallRealMethod(); - given(compiler.addRamPaths(any(), any())).will(ctx -> compiler); - var ramPaths = MoreMocks.stubCast(new TypeRef>() {}); - - // When - var result = compiler.addModuleRamPaths(ramPaths); - - // Then - then(compiler).should().addRamPaths(StandardLocation.MODULE_PATH, ramPaths); - assertThat(result).isSameAs(compiler); - } - - @DisplayName( - "vararg overload for addModuleRamPaths calls varargs overload for addRamPaths" - ) - @Test - void varargOverloadForAddModuleRamPathsCallsVarargsOverloadsAddRamPaths() { - // Given - var firstRamPath = MoreMocks.stub(RamPath.class); - var secondRamPath = MoreMocks.stub(RamPath.class); - var thirdRamPath = MoreMocks.stub(RamPath.class); - - given(compiler.addModuleRamPaths(any(), any(), any())).willCallRealMethod(); - given(compiler.addRamPaths(any(), eq(firstRamPath), eq(secondRamPath), eq(thirdRamPath))) - .will(ctx -> compiler); - - // When - var result = compiler.addModuleRamPaths(firstRamPath, secondRamPath, thirdRamPath); - - // Then - then(compiler).should() - .addRamPaths(StandardLocation.MODULE_PATH, firstRamPath, secondRamPath, thirdRamPath); - assertThat(result).isSameAs(compiler); - } - - @DisplayName( - "addPatchModuleRamPaths(ramPaths) calls addRamPaths(PATCH_MODULE_PATH, ramPaths)" - ) - @Test - void addPatchModuleRamPathsWithIterableCallsAddRamPaths() { - // Given - given(compiler.addPatchModuleRamPaths(any())).willCallRealMethod(); - given(compiler.addRamPaths(any(), any())).will(ctx -> compiler); - var ramPaths = MoreMocks.stubCast(new TypeRef>() {}); - - // When - var result = compiler.addPatchModuleRamPaths(ramPaths); - - // Then - then(compiler).should().addRamPaths(StandardLocation.PATCH_MODULE_PATH, ramPaths); - assertThat(result).isSameAs(compiler); - } - - @DisplayName( - "vararg overload for addPatchModuleRamPaths calls varargs overload for addRamPaths" - ) - @Test - void varargOverloadForAddPatchModuleRamPathsCallsVarargsOverloadsAddRamPaths() { - // Given - var firstRamPath = MoreMocks.stub(RamPath.class); - var secondRamPath = MoreMocks.stub(RamPath.class); - var thirdRamPath = MoreMocks.stub(RamPath.class); - - given(compiler.addPatchModuleRamPaths(any(), any(), any())).willCallRealMethod(); - given(compiler.addRamPaths(any(), eq(firstRamPath), eq(secondRamPath), eq(thirdRamPath))) - .will(ctx -> compiler); - - // When - var result = compiler.addPatchModuleRamPaths(firstRamPath, secondRamPath, thirdRamPath); - - // Then - then(compiler).should() - .addRamPaths(StandardLocation.PATCH_MODULE_PATH, firstRamPath, secondRamPath, thirdRamPath); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("vararg overload for addAnnotationProcessorOptions calls the correct method") - @Test - void varargOverloadForAddAnnotationProcessorOptionsCallsCorrectMethod() { - // Given - given(compiler.addAnnotationProcessorOptions(any(), any())).willCallRealMethod(); - given(compiler.addAnnotationProcessorOptions(any())).will(ctx -> compiler); - - // When - var result = compiler.addAnnotationProcessorOptions("foo", "bar", "baz"); - - // Then - then(compiler).should().addAnnotationProcessorOptions(List.of("foo", "bar", "baz")); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("vararg overload for addAnnotationProcessors calls the correct method") - @Test - void varargOverloadForAddAnnotationProcessorsCallsCorrectMethod() { - // Given - given(compiler.addAnnotationProcessors(any(), any())).willCallRealMethod(); - given(compiler.addAnnotationProcessors(any())).will(ctx -> compiler); - - var firstProc = MoreMocks.stub(Processor.class); - var secondProc = MoreMocks.stub(Processor.class); - var thirdProc = MoreMocks.stub(Processor.class); - - // When - var result = compiler.addAnnotationProcessors(firstProc, secondProc, thirdProc); - - // Then - then(compiler).should().addAnnotationProcessors(List.of(firstProc, secondProc, thirdProc)); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("vararg overload for addCompilerOptions calls the correct method") - @Test - void varargOverloadForAddCompilerOptionsCallsCorrectMethod() { - // Given - given(compiler.addCompilerOptions(any(), any())).willCallRealMethod(); - given(compiler.addCompilerOptions(any())).will(ctx -> compiler); - - // When - var result = compiler.addCompilerOptions("neko", "neko", "nii"); - - // Then - then(compiler).should().addCompilerOptions(List.of("neko", "neko", "nii")); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("vararg overload for addCompilerOptions calls the correct method") - @Test - void varargOverloadForAddRuntimeOptionsCallsCorrectMethod() { - // Given - given(compiler.addRuntimeOptions(any(), any())).willCallRealMethod(); - given(compiler.addRuntimeOptions(any())).will(ctx -> compiler); - - // When - var result = compiler.addRuntimeOptions("super", "user", "do"); - - // Then - then(compiler).should().addRuntimeOptions(List.of("super", "user", "do")); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("releaseVersion(int) should call releaseVersion(String)") - @ValueSource(ints = {11, 12, 13, 14, 15, 16, 17}) - @ParameterizedTest(name = "for version = {0}") - void releaseVersionIntCallsReleaseVersionString(int versionInt) { - // Given - var versionString = "" + versionInt; - given(compiler.release(anyInt())).willCallRealMethod(); - given(compiler.release(anyString())).will(ctx -> compiler); - - // When - var result = compiler.release(versionInt); - - // Then - then(compiler).should().release(versionString); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("releaseVersion(int) throws an IllegalArgumentException for negative versions") - @ValueSource(ints = {-1, -2, -5, -100_000}) - @ParameterizedTest(name = "for version = {0}") - void releaseVersionIntThrowsIllegalArgumentExceptionForNegativeVersions(int versionInt) { - // Given - given(compiler.release(anyInt())).willCallRealMethod(); - - // Then - assertThatThrownBy(() -> compiler.release(versionInt)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Cannot provide a release version less than 0"); - } - - @DisplayName("releaseVersion(SourceVersion) should call releaseVersion(String)") - @MethodSource("sourceVersions") - @ParameterizedTest(name = "for version = {0}") - void releaseVersionSourceVersionCallsReleaseVersionString( - SourceVersion versionEnum, - String versionString - ) { - // Given - given(compiler.release(any(SourceVersion.class))).willCallRealMethod(); - given(compiler.release(anyString())).will(ctx -> compiler); - - // When - var result = compiler.release(versionEnum); - - // Then - then(compiler).should().release(versionString); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("sourceVersion(int) should call sourceVersion(String)") - @ValueSource(ints = {11, 12, 13, 14, 15, 16, 17}) - @ParameterizedTest(name = "for version = {0}") - void sourceVersionIntCallsReleaseVersionString(int versionInt) { - // Given - var versionString = "" + versionInt; - given(compiler.source(anyInt())).willCallRealMethod(); - given(compiler.source(anyString())).will(ctx -> compiler); - - // When - var result = compiler.source(versionInt); - - // Then - then(compiler).should().source(versionString); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("sourceVersion(int) throws an IllegalArgumentException for negative versions") - @ValueSource(ints = {-1, -2, -5, -100_000}) - @ParameterizedTest(name = "for version = {0}") - void sourceVersionIntThrowsIllegalArgumentExceptionForNegativeVersions(int versionInt) { - // Given - given(compiler.source(anyInt())).willCallRealMethod(); - - // Then - assertThatThrownBy(() -> compiler.source(versionInt)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Cannot provide a source version less than 0"); - } - - @DisplayName("sourceVersion(SourceVersion) should call sourceVersion(String)") - @MethodSource("sourceVersions") - @ParameterizedTest(name = "for version = {0}") - void sourceVersionSourceVersionCallsReleaseVersionString( - SourceVersion versionEnum, - String versionString - ) { - // Given - given(compiler.source(any(SourceVersion.class))).willCallRealMethod(); - given(compiler.source(anyString())).will(ctx -> compiler); - - // When - var result = compiler.source(versionEnum); - - // Then - then(compiler).should().source(versionString); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("targetVersion(int) should call targetVersion(String)") - @ValueSource(ints = {11, 12, 13, 14, 15, 16, 17}) - @ParameterizedTest(name = "for version = {0}") - void targetVersionIntCallsReleaseVersionString(int versionInt) { - // Given - var versionString = "" + versionInt; - given(compiler.target(anyInt())).willCallRealMethod(); - given(compiler.target(anyString())).will(ctx -> compiler); - - // When - var result = compiler.target(versionInt); - - // Then - then(compiler).should().target(versionString); - assertThat(result).isSameAs(compiler); - } - - @DisplayName("targetVersion(int) throws an IllegalArgumentException for negative versions") - @ValueSource(ints = {-1, -2, -5, -100_000}) - @ParameterizedTest(name = "for version = {0}") - void targetVersionIntThrowsIllegalArgumentExceptionForNegativeVersions(int versionInt) { - // Given - given(compiler.target(anyInt())).willCallRealMethod(); - - // Then - assertThatThrownBy(() -> compiler.target(versionInt)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Cannot provide a target version less than 0"); - } - - @DisplayName("targetVersion(SourceVersion) should call targetVersion(String)") - @MethodSource("sourceVersions") - @ParameterizedTest(name = "for version = {0}") - void targetVersionSourceVersionCallsReleaseVersionString( - SourceVersion versionEnum, - String versionString - ) { - // Given - given(compiler.target(any(SourceVersion.class))).willCallRealMethod(); - given(compiler.target(anyString())).will(ctx -> compiler); - - // When - var result = compiler.target(versionEnum); - - // Then - then(compiler).should().target(versionString); - assertThat(result).isSameAs(compiler); - } - - static Stream sourceVersions() { - return Stream - .of(SourceVersion.values()) - .map(version -> Arguments.of(version, "" + version.ordinal())); - } -} diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/CompilersTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/CompilersTest.java deleted file mode 100644 index 5a54f205b..000000000 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/CompilersTest.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (C) 2022 Ashley Scopes - * - * 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.testing.unit.compilers; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Answers.CALLS_REAL_METHODS; -import static org.mockito.Mockito.mockConstructionWithAnswer; - -import io.github.ascopes.jct.compilers.Compilers; -import io.github.ascopes.jct.compilers.ecj.EcjCompiler; -import io.github.ascopes.jct.compilers.javac.JavacCompiler; -import io.github.ascopes.jct.testing.helpers.StaticClassTestTemplate; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -/** - * Tests for {@link Compilers}. - * - * @author Ashley Scopes - */ -@DisplayName("Compilers tests") -class CompilersTest implements StaticClassTestTemplate { - - @Override - public Class getTypeBeingTested() { - return Compilers.class; - } - - @DisplayName("javac() returns a default Javac compiler") - @Test - void javacReturnsDefaultJavacCompiler() { - var javacCompilerMock = mockConstructionWithAnswer(JavacCompiler.class, CALLS_REAL_METHODS); - - try (javacCompilerMock) { - // When - var compiler = Compilers.javac(); - - // Then - assertThat(javacCompilerMock.constructed()) - .singleElement() - .isSameAs(compiler); - } - } - - @DisplayName("ecj() returns a default ECJ compiler") - @Test - void ecjReturnsDefaultEcjCompiler() { - var ecjCompilerMock = mockConstructionWithAnswer(EcjCompiler.class, CALLS_REAL_METHODS); - - try (ecjCompilerMock) { - // When - var compiler = Compilers.ecj(); - - // Then - assertThat(ecjCompilerMock.constructed()) - .singleElement() - .isSameAs(compiler); - } - } - -} diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/SimpleCompilationFactoryTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/SimpleCompilationFactoryTest.java index 49a6ec8cf..d1c3b317c 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/SimpleCompilationFactoryTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/SimpleCompilationFactoryTest.java @@ -24,11 +24,11 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; -import io.github.ascopes.jct.compilers.Compiler; +import io.github.ascopes.jct.compilers.Compilable; import io.github.ascopes.jct.compilers.FlagBuilder; import io.github.ascopes.jct.compilers.SimpleCompilation; import io.github.ascopes.jct.compilers.SimpleCompilationFactory; -import io.github.ascopes.jct.paths.PathLocationRepository; +import io.github.ascopes.jct.compilers.SimpleFileManagerTemplate; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -77,7 +77,7 @@ class SimpleCompilationFactoryTest { CompilationTask compilationTask; @Mock(lenient = true, answer = Answers.RETURNS_DEEP_STUBS) - PathLocationRepository pathLocationRepository; + SimpleFileManagerTemplate fileManagerTemplate; Boolean expectedCompilationResult; @@ -86,8 +86,6 @@ void setUp() { compilationFactory = new SimpleCompilationFactory<>(); expectedCompilationResult = true; - given(compiler.getPathLocationRepository()) - .willAnswer(ctx -> pathLocationRepository); given(jsr199Compiler.getTask(any(), any(), any(), any(), any(), any())) .willAnswer(ctx -> compilationTask); given(flagBuilder.build()) @@ -264,7 +262,7 @@ void toDo() { @DisplayName("apply logging to file manager tests") @Nested - class ApplyLoggingToFileManagerTest { + class ApplyLoggingToFileManagerTestMode { @Disabled("TODO: implement") @Test @@ -303,7 +301,7 @@ void toDo() { } private SimpleCompilation execute() { - return compilationFactory.compile(compiler, jsr199Compiler, flagBuilder); + return compilationFactory.compile(compiler, fileManagerTemplate, jsr199Compiler, flagBuilder); } private static List someListOfOptions() { @@ -314,6 +312,6 @@ private static List someListOfOptions() { } private abstract static class StubbedCompiler - implements Compiler { + implements Compilable { } } diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/SimpleCompilationTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/SimpleCompilationTest.java index 31da35131..f1d9ca2dd 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/SimpleCompilationTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/SimpleCompilationTest.java @@ -16,13 +16,14 @@ package io.github.ascopes.jct.testing.unit.compilers; +import static io.github.ascopes.jct.testing.helpers.MoreMocks.stub; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.InstanceOfAssertFactories.iterable; import io.github.ascopes.jct.compilers.SimpleCompilation; -import io.github.ascopes.jct.compilers.TraceDiagnostic; -import io.github.ascopes.jct.paths.PathLocationRepository; +import io.github.ascopes.jct.jsr199.FileManager; +import io.github.ascopes.jct.jsr199.diagnostics.TraceDiagnostic; import io.github.ascopes.jct.testing.helpers.MoreMocks; import io.github.ascopes.jct.testing.helpers.TypeRef; import java.util.Arrays; @@ -104,7 +105,7 @@ void getOutputLinesReturnsExpectedValue(int lineCount) { void getCompilationUnitsReturnsExpectedValue(int compilationUnitCount) { // Given var compilationUnits = Stream - .generate(() -> MoreMocks.stub(JavaFileObject.class)) + .generate(() -> stub(JavaFileObject.class)) .limit(compilationUnitCount) .collect(Collectors.toSet()); @@ -139,17 +140,17 @@ void getDiagnosticsReturnsExpectedValue(int diagnosticCount) { .containsExactlyElementsOf(diagnostics); } - @DisplayName("getFileRepository returns expected value") + @DisplayName("getFileManager returns expected value") @Test - void getFileRepositoryReturnsExpectedValue() { + void getFileManagerReturnsExpectedValue() { // Given - var fileRepository = MoreMocks.stub(PathLocationRepository.class); + var fileManager = stub(FileManager.class); var compilation = filledBuilder() - .pathLocationRepository(fileRepository) + .fileManager(fileManager) .build(); // Then - Assertions.assertThat(compilation.getPathLocationRepository()).isEqualTo(fileRepository); + Assertions.assertThat(compilation.getFileManager()).isEqualTo(fileManager); } @@ -163,7 +164,7 @@ void buildingWithoutSuccessSetRaisesNullPointerException() { // Given var builder = SimpleCompilation .builder() - .pathLocationRepository(MoreMocks.stub(PathLocationRepository.class)) + .fileManager(stub(FileManager.class)) .outputLines(List.of()) .diagnostics(List.of()) .compilationUnits(Set.of()) @@ -181,7 +182,7 @@ void buildingWithoutFailOnWarningsSetRaisesNullPointerException() { // Given var builder = SimpleCompilation .builder() - .pathLocationRepository(MoreMocks.stub(PathLocationRepository.class)) + .fileManager(stub(FileManager.class)) .outputLines(List.of()) .diagnostics(List.of()) .compilationUnits(Set.of()) @@ -211,7 +212,7 @@ void buildingWithoutCompilationUnitsRaisesNullPointerException() { // Given var builder = SimpleCompilation .builder() - .pathLocationRepository(MoreMocks.stub(PathLocationRepository.class)) + .fileManager(stub(FileManager.class)) .outputLines(List.of()) .diagnostics(List.of()) .success(RANDOM.nextBoolean()) @@ -229,17 +230,17 @@ void buildingWithNullCompilationUnitsRaisesNullPointerException() { // Given var builder = SimpleCompilation .builder() - .pathLocationRepository(MoreMocks.stub(PathLocationRepository.class)) + .fileManager(stub(FileManager.class)) .outputLines(List.of()) .diagnostics(List.of()) .success(RANDOM.nextBoolean()) .failOnWarnings(RANDOM.nextBoolean()) .compilationUnits(nullableSetOf( - MoreMocks.stub(JavaFileObject.class), - MoreMocks.stub(JavaFileObject.class), - MoreMocks.stub(JavaFileObject.class), + stub(JavaFileObject.class), + stub(JavaFileObject.class), + stub(JavaFileObject.class), null, - MoreMocks.stub(JavaFileObject.class) + stub(JavaFileObject.class) )); // Then @@ -266,7 +267,7 @@ void buildingWithoutDiagnosticsRaisesNullPointerException() { // Given var builder = SimpleCompilation .builder() - .pathLocationRepository(MoreMocks.stub(PathLocationRepository.class)) + .fileManager(stub(FileManager.class)) .outputLines(List.of()) .compilationUnits(Set.of()) .success(RANDOM.nextBoolean()) @@ -284,7 +285,7 @@ void buildingWithNullDiagnosticsRaisesNullPointerException() { // Given var builder = SimpleCompilation .builder() - .pathLocationRepository(MoreMocks.stub(PathLocationRepository.class)) + .fileManager(MoreMocks.stub(FileManager.class)) .outputLines(List.of()) .compilationUnits(Set.of()) .success(RANDOM.nextBoolean()) @@ -301,21 +302,21 @@ void buildingWithNullDiagnosticsRaisesNullPointerException() { .hasMessage("diagnostics[1]"); } - @DisplayName("Setting null file repositories raises a NullPointerException") + @DisplayName("Setting null file managers raises a NullPointerException") @Test - void settingNullFileRepositoriesRaisesNullPointerException() { + void settingNullFileManagerRaisesNullPointerException() { // Given var builder = filledBuilder(); // Then - assertThatThrownBy(() -> builder.pathLocationRepository(null)) + assertThatThrownBy(() -> builder.fileManager(null)) .isInstanceOf(NullPointerException.class) - .hasMessage("pathLocationRepository"); + .hasMessage("fileManager"); } - @DisplayName("Building without a file repository raises a NullPointerException") + @DisplayName("Building without a file manager raises a NullPointerException") @Test - void buildingWithoutFileRepositoryRaisesNullPointerException() { + void buildingWithoutFileManagerRaisesNullPointerException() { // Given var builder = SimpleCompilation .builder() @@ -328,7 +329,7 @@ void buildingWithoutFileRepositoryRaisesNullPointerException() { // Then assertThatThrownBy(builder::build) .isInstanceOf(NullPointerException.class) - .hasMessage("pathLocationRepository"); + .hasMessage("fileManager"); } @DisplayName("Setting null output lines raises a NullPointerException") @@ -349,7 +350,7 @@ void buildingWithoutOutputLinesRaisesNullPointerException() { // Given var builder = SimpleCompilation .builder() - .pathLocationRepository(MoreMocks.stub(PathLocationRepository.class)) + .fileManager(MoreMocks.stub(FileManager.class)) .diagnostics(List.of()) .compilationUnits(Set.of()) .success(RANDOM.nextBoolean()) @@ -367,7 +368,7 @@ void buildingWithNullOutputLinesRaisesNullPointerException() { // Given var builder = SimpleCompilation .builder() - .pathLocationRepository(MoreMocks.stub(PathLocationRepository.class)) + .fileManager(MoreMocks.stub(FileManager.class)) .outputLines(List.of()) .diagnostics(List.of()) .success(RANDOM.nextBoolean()) @@ -397,7 +398,7 @@ static SimpleCompilation.Builder filledBuilder() { .compilationUnits(Set.of()) .diagnostics(List.of()) .failOnWarnings(RANDOM.nextBoolean()) - .pathLocationRepository(MoreMocks.stub(PathLocationRepository.class)) + .fileManager(MoreMocks.stub(FileManager.class)) .outputLines(List.of()) .success(RANDOM.nextBoolean()); } diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/SimpleCompilerTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/SimpleCompilerTest.java index 7692d1a63..cf2211501 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/SimpleCompilerTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/SimpleCompilerTest.java @@ -28,19 +28,16 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.withSettings; -import io.github.ascopes.jct.compilers.Compiler.AnnotationProcessorDiscovery; -import io.github.ascopes.jct.compilers.Compiler.CompilerConfigurer; -import io.github.ascopes.jct.compilers.Compiler.Logging; +import io.github.ascopes.jct.compilers.AnnotationProcessorDiscovery; +import io.github.ascopes.jct.compilers.CompilerConfigurer; import io.github.ascopes.jct.compilers.FlagBuilder; +import io.github.ascopes.jct.compilers.LoggingMode; import io.github.ascopes.jct.compilers.SimpleCompilationFactory; import io.github.ascopes.jct.compilers.SimpleCompiler; -import io.github.ascopes.jct.paths.PathLocationRepository; -import io.github.ascopes.jct.paths.RamPath; -import io.github.ascopes.jct.testing.helpers.ReflectiveAccess; +import io.github.ascopes.jct.compilers.SimpleFileManagerTemplate; import io.github.ascopes.jct.testing.helpers.TypeRef; import io.github.ascopes.jct.testing.unit.compilers.SimpleCompilerTest.AttrTestPack.NullTests; import java.nio.charset.StandardCharsets; -import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -58,7 +55,7 @@ import java.util.stream.Stream; import javax.annotation.processing.Processor; import javax.tools.JavaCompiler; -import javax.tools.JavaFileManager.Location; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Test; @@ -81,6 +78,7 @@ void nameCannotBeNull() { assertThatThrownBy( () -> new StubbedCompiler( null, + stub(SimpleFileManagerTemplate.class), stub(JavaCompiler.class), stub(FlagBuilder.class) ) @@ -88,6 +86,21 @@ void nameCannotBeNull() { .hasMessage("name"); } + @DisplayName("fileManagerTemplate cannot be null") + @Test + void fileManagerTemplateCannotBeNull() { + // Then + assertThatThrownBy( + () -> new StubbedCompiler( + "foo", + null, + stub(JavaCompiler.class), + stub(FlagBuilder.class) + ) + ).isExactlyInstanceOf(NullPointerException.class) + .hasMessage("fileManagerTemplate"); + } + @DisplayName("jsr199Compiler cannot be null") @Test void jsr199CompilerCannotBeNull() { @@ -95,6 +108,7 @@ void jsr199CompilerCannotBeNull() { assertThatThrownBy( () -> new StubbedCompiler( "foobar", + stub(SimpleFileManagerTemplate.class), null, stub(FlagBuilder.class) ) @@ -110,6 +124,7 @@ void flagBuilderCannotBeNull() { assertThatThrownBy( () -> new StubbedCompiler( "foobar", + stub(SimpleFileManagerTemplate.class), stub(JavaCompiler.class), null ) @@ -122,7 +137,12 @@ void flagBuilderCannotBeNull() { void getFlagBuilderShouldGetTheFlagBuilder() { // Given var expectedFlagBuilder = stub(FlagBuilder.class); - var compiler = new StubbedCompiler("foo", stub(JavaCompiler.class), expectedFlagBuilder); + var compiler = new StubbedCompiler( + "foo", + stub(SimpleFileManagerTemplate.class), + stub(JavaCompiler.class), + expectedFlagBuilder + ); // When var actualFlagBuilder = compiler.getFlagBuilder(); @@ -136,7 +156,12 @@ void getFlagBuilderShouldGetTheFlagBuilder() { void getJsr199CompilerShouldGetTheJsr199Compiler() { // Given var expectedCompiler = stub(JavaCompiler.class); - var compiler = new StubbedCompiler("foo", expectedCompiler, stub(FlagBuilder.class)); + var compiler = new StubbedCompiler( + "foo", + stub(SimpleFileManagerTemplate.class), + expectedCompiler, + stub(FlagBuilder.class) + ); // When var actualCompiler = compiler.getJsr199Compiler(); @@ -152,6 +177,7 @@ void getNameShouldGetTheNameOfTheCompiler() { var expectedName = "Roy Rodgers McFreely with ID " + UUID.randomUUID(); var compiler = new StubbedCompiler( expectedName, + stub(SimpleFileManagerTemplate.class), stub(JavaCompiler.class), stub(FlagBuilder.class) ); @@ -171,7 +197,9 @@ void compileCallsPerformEntireCompilation() { try (var compilationFactory = mockConstruction(SimpleCompilationFactory.class)) { var jsr199Compiler = stub(JavaCompiler.class); var flagBuilder = stub(FlagBuilder.class); - var compiler = new StubbedCompiler("foobar", jsr199Compiler, flagBuilder); + var fileManagerTemplate = stub(SimpleFileManagerTemplate.class); + var compiler = new StubbedCompiler("foobar", fileManagerTemplate, jsr199Compiler, + flagBuilder); // When compiler.compile(); @@ -180,16 +208,19 @@ void compileCallsPerformEntireCompilation() { assertThat(compilationFactory.constructed()) .singleElement() .extracting(factory -> (SimpleCompilationFactory) factory) - .satisfies(factory -> verify(factory).compile(compiler, jsr199Compiler, flagBuilder)); + .satisfies( + factory -> verify(factory).compile(compiler, fileManagerTemplate, jsr199Compiler, + flagBuilder)); } } - @DisplayName("Applying a configurer invokes the configurer with the compiler") + @DisplayName("Applying a throwable configurer invokes the configurer with the compiler") @Test - void applyingConfigurerInvokesConfigurerWithCompiler() throws Exception { + void applyingThrowableConfigurerInvokesConfigurerWithCompiler() throws Exception { // Given var compiler = new StubbedCompiler(); - var configurer = mockCast(new TypeRef>() {}); + var type = new TypeRef>() {}; + var configurer = mockCast(type); // When var result = compiler.configure(configurer); @@ -199,93 +230,64 @@ void applyingConfigurerInvokesConfigurerWithCompiler() throws Exception { then(configurer).should().configure(compiler); } + @Disabled("fix me") @DisplayName("addPaths should pass the parameters to the file repository") @Test void addPathsDelegatesToFileRepository() { // Given var constructor = Mockito.mockConstruction( - PathLocationRepository.class, + SimpleFileManagerTemplate.class, withSettings().defaultAnswer(Answers.RETURNS_DEEP_STUBS) ); try (constructor) { - var compiler = new StubbedCompiler(); - var location = stub(Location.class); - var paths = stubCast(new TypeRef>() {}); + //var compiler = new StubbedCompiler(); + //var location = stub(Location.class); + + //var path1 = stub(Path.class); + //var path2 = stub(Path.class); + //var paths = List.of(path1, path2); // When - compiler.addPaths(location, paths); + //compiler.addPaths(location, paths); // Then - assertThat(constructor.constructed()) - .singleElement() - .satisfies( - repo -> verify(repo).getOrCreateManager(location), - repo -> verify(repo.getOrCreateManager(location)).addPaths(paths) - ); + //assertThat(constructor.constructed()) + // .singleElement() + // .satisfies( + // template -> verify(template).addPath(location, ), + // ); } } + @Disabled("fix me") @DisplayName("addRamPaths should pass the parameters to the file repository") @Test void addRamPathsDelegatesToFileRepository() { // Given var constructor = Mockito.mockConstruction( - PathLocationRepository.class, + SimpleFileManagerTemplate.class, withSettings().defaultAnswer(Answers.RETURNS_DEEP_STUBS) ); try (constructor) { - var compiler = new StubbedCompiler(); - var location = stub(Location.class); - var paths = stubCast(new TypeRef>() {}); + //var compiler = new StubbedCompiler(); + //var location = stub(Location.class); + //var paths = stubCast(new TypeRef>() {}); // When - compiler.addRamPaths(location, paths); + //compiler.addRamPaths(location, paths); // Then - assertThat(constructor.constructed()) - .singleElement() - .satisfies( - repo -> verify(repo).getOrCreateManager(location), - repo -> verify(repo.getOrCreateManager(location)).addRamPaths(paths) - ); + //assertThat(constructor.constructed()) + // .singleElement() + // .satisfies( + // repo -> verify(repo).getOrCreateManager(location), + // repo -> verify(repo.getOrCreateManager(location)).addRamPaths(paths) + // ); } } - @DisplayName("getPathLocationRepository() should get the PathLocationRepository") - @Test - void getPathLocationRepositoryShouldGetThePathLocationRepository() { - // Given - var compiler = new StubbedCompiler(); - var expectedPathLocationRepository = ReflectiveAccess.getField( - compiler, - "fileRepository", - PathLocationRepository.class - ); - - // When - var actualPathLocationRepository = compiler.getPathLocationRepository(); - - // Then - assertThat(actualPathLocationRepository).isSameAs(expectedPathLocationRepository); - } - - @DisplayName("getFileCharset and fileCharset tests") - @TestFactory - AttrTestPack fileCharsetWorksCorrectly() { - return new AttrTestPack<>( - "fileCharset", - SimpleCompiler::getFileCharset, - SimpleCompiler::fileCharset, - NullTests.EXPECT_DISALLOW, - StandardCharsets.UTF_8, - StandardCharsets.US_ASCII, - StandardCharsets.ISO_8859_1, - StandardCharsets.UTF_16 - ); - } - @DisplayName("isVerbose and verbose tests") @TestFactory AttrTestPack verboseWorksCorrectly() { @@ -555,25 +557,25 @@ AttrTestPack logCharsetWorksAsExpected() { @TestFactory AttrTestPack fileManagerLoggingWorksAsExpected() { return AttrTestPack.forEnum( - "fileManagerLogging", - StubbedCompiler::getFileManagerLogging, - StubbedCompiler::fileManagerLogging, + "fileManagerLoggingMode", + StubbedCompiler::getFileManagerLoggingMode, + StubbedCompiler::fileManagerLoggingMode, NullTests.EXPECT_DISALLOW, - StubbedCompiler.DEFAULT_FILE_MANAGER_LOGGING, - Logging.class + StubbedCompiler.DEFAULT_FILE_MANAGER_LOGGING_MODE, + LoggingMode.class ); } - @DisplayName("getDiagnosticLogging and diagnosticLogging tests") + @DisplayName("getDiagnosticLoggingMode and diagnosticLogging tests") @TestFactory - AttrTestPack diagnosticLoggingWorksAsExpected() { + AttrTestPack diagnosticLoggingModeWorksAsExpected() { return AttrTestPack.forEnum( - "diagnosticLogging", - StubbedCompiler::getDiagnosticLogging, - StubbedCompiler::diagnosticLogging, + "diagnosticLoggingMode", + StubbedCompiler::getDiagnosticLoggingMode, + StubbedCompiler::diagnosticLoggingMode, NullTests.EXPECT_DISALLOW, - StubbedCompiler.DEFAULT_DIAGNOSTIC_LOGGING, - Logging.class + StubbedCompiler.DEFAULT_DIAGNOSTIC_LOGGING_MODE, + LoggingMode.class ); } @@ -597,6 +599,7 @@ void toStringReturnsTheNameOfTheCompiler() { var expectedName = UUID.randomUUID().toString(); var compiler = new StubbedCompiler( expectedName, + stub(SimpleFileManagerTemplate.class), stub(JavaCompiler.class), stub(FlagBuilder.class) ); @@ -1036,11 +1039,26 @@ public Iterator iterator() { static class StubbedCompiler extends SimpleCompiler { StubbedCompiler() { - this("stubbed", stubCast(new TypeRef<>() {}), stubCast(new TypeRef<>() {})); + this( + "stubbed", + stub(SimpleFileManagerTemplate.class), + stubCast(new TypeRef<>() {}), + stubCast(new TypeRef<>() {}) + ); + } + + StubbedCompiler( + String name, + SimpleFileManagerTemplate template, + JavaCompiler jsr199Compiler, + FlagBuilder flagBuilder + ) { + super(name, template, jsr199Compiler, flagBuilder); } - StubbedCompiler(String name, JavaCompiler jsr199Compiler, FlagBuilder flagBuilder) { - super(name, jsr199Compiler, flagBuilder); + @Override + public String getDefaultRelease() { + return "1234"; } } } diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/ecj/EcjCompilerTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/ecj/EcjCompilerTest.java index a7897b97c..7a6e006e6 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/ecj/EcjCompilerTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/ecj/EcjCompilerTest.java @@ -34,11 +34,11 @@ @DisplayName("EcjCompiler tests") class EcjCompilerTest { - @DisplayName("compilers have the expected name") + @DisplayName("compilers have the expected default name") @Test - void compilersHaveTheExpectedName() { + void compilersHaveTheExpectedDefaultName() { Assertions.assertThat(new EcjCompiler(MoreMocks.stub(JavaCompiler.class)).getName()) - .isEqualTo("ecj"); + .isEqualTo("Eclipse Compiler for Java"); } @DisplayName("compilers have the expected JSR-199 compiler implementation") diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/javac/JavacCompilerTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/javac/JavacCompilerTest.java index ed14883b4..eaa7c9449 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/javac/JavacCompilerTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/javac/JavacCompilerTest.java @@ -33,11 +33,11 @@ @DisplayName("JavacCompiler tests") class JavacCompilerTest { - @DisplayName("compilers have the expected name") + @DisplayName("compilers have the expected default name") @Test - void compilersHaveTheExpectedName() { + void compilersHaveTheExpectedDefaultName() { assertThat(new JavacCompiler(MoreMocks.stub(JavaCompiler.class)).getName()) - .isEqualTo("javac"); + .isEqualTo("JDK Compiler"); } @DisplayName("compilers have the expected JSR-199 compiler implementation") diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/intern/PlatformLinkStrategyTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/intern/PlatformLinkStrategyTest.java deleted file mode 100644 index cf74deea6..000000000 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/intern/PlatformLinkStrategyTest.java +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Copyright (C) 2022 Ashley Scopes - * - * 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.testing.unit.intern; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.google.common.jimfs.Configuration; -import com.google.common.jimfs.Jimfs; -import com.google.common.jimfs.PathType; -import io.github.ascopes.jct.intern.PlatformLinkStrategy; -import java.io.IOException; -import java.nio.file.Files; -import java.util.Properties; -import java.util.UUID; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; - -/** - * {@link PlatformLinkStrategy} tests. - * - * @author Ashley Scopes - */ -@DisplayName("PlatformLinkStrategy tests") -class PlatformLinkStrategyTest { - - @DisplayName("Hard links are created on Windows") - @MethodSource("windowsOses") - @ParameterizedTest(name = "for os.name = \"{0}\"") - void hardLinksAreCreatedForWindows(String osName) throws IOException { - try (var fs = Jimfs.newFileSystem(Configuration.windows())) { - var root = fs.getRootDirectories().iterator().next(); - - // Given - var props = new Properties(); - props.put("os.name", osName); - - var target = Files.createFile(root.resolve("target")); - var content = UUID.randomUUID().toString(); - Files.writeString(target, content); - - var link = root.resolve("link"); - - // When - var result = new PlatformLinkStrategy(props).createLinkOrCopy(link, target); - - // Then - assertThat(result) - .isEqualTo(link) - .isRegularFile() - .content() - .isEqualTo(content); - - // Verify the file is a link and not a copy. - var moreContent = content + UUID.randomUUID(); - Files.writeString(target, moreContent); - - assertThat(result) - .content() - .withFailMessage("Expected result to be a link, but it was a copy") - .isEqualTo(moreContent); - } - } - - @DisplayName("Symbolic links are created on other OSes") - @MethodSource("otherOses") - @ParameterizedTest(name = "for os.name = \"{0}\"") - void symbolicLinksAreCreatedForWindows(String osName) throws IOException { - try (var fs = Jimfs.newFileSystem(Configuration.unix())) { - var root = fs.getRootDirectories().iterator().next(); - - // Given - var props = new Properties(); - props.put("os.name", osName); - - var target = Files.createFile(root.resolve("target")); - var content = UUID.randomUUID().toString(); - Files.writeString(target, content); - - var link = root.resolve("link"); - - // When - var result = new PlatformLinkStrategy(props).createLinkOrCopy(link, target); - - // Then - assertThat(result) - .isEqualTo(link) - .isSymbolicLink() - .content() - .isEqualTo(content); - - // Verify the file is a link and not a copy. - var moreContent = content + UUID.randomUUID(); - Files.writeString(target, moreContent); - - assertThat(result) - .content() - .withFailMessage("Expected result to be a link, but it was a copy") - .isEqualTo(moreContent); - } - } - - @DisplayName("Copies are created when links are not supported") - @MethodSource({"windowsOses", "otherOses"}) - @ParameterizedTest(name = "for os.name = \"{0}\"") - void copiesAreCreatedWhenLinksAreNotSupported(String osName) throws IOException { - var config = Configuration - .builder(PathType.unix()) - .setRoots("/") - .setWorkingDirectory("/work") - // No features! - .setSupportedFeatures() - .setAttributeViews("basic") - .build(); - - try (var fs = Jimfs.newFileSystem(config)) { - var root = fs.getRootDirectories().iterator().next(); - - // Given - var props = new Properties(); - props.put("os.name", osName); - - var target = Files.createFile(root.resolve("target")); - var content = UUID.randomUUID().toString(); - Files.writeString(target, content); - - var link = root.resolve("link"); - - // When - var result = new PlatformLinkStrategy(props).createLinkOrCopy(link, target); - - // Then - assertThat(result) - .isEqualTo(link) - .isRegularFile() - .content() - .isEqualTo(content); - - // Verify the file is a link and not a copy. - var moreContent = content + UUID.randomUUID(); - Files.writeString(target, moreContent); - - assertThat(result) - .content() - .withFailMessage("Expected result to be a copy, but it was a link") - .isNotEqualTo(moreContent); - } - } - - @DisplayName("Linking logic works for the current platform without exceptions") - @Test - void linkingLogicWorksForCurrentPlatform() throws IOException { - var dir = Files.createTempDirectory("PlatformLinkStrategy-platform"); - - try { - var target = Files.createFile(dir.resolve("target")); - var content = UUID.randomUUID().toString(); - Files.writeString(target, content); - - var link = dir.resolve("link"); - - // When - var result = new PlatformLinkStrategy(System.getProperties()) - .createLinkOrCopy(link, target); - - // Then - assertThat(result) - .isEqualTo(link) - .content() - .isEqualTo(content); - - } finally { - try (var list = Files.list(dir)) { - for (var file : list.collect(Collectors.toList())) { - Files.delete(file); - } - } - Files.delete(dir); - } - } - - static Stream windowsOses() { - return Stream.of( - "Windows", - "Windows 95", - "Windows 98", - "Windows ME", - "Windows NT", - "Windows 2000", - "Windows Server 2000", - "Windows 2003", - "Windows Server 2003", - "Windows XP", - "Windows Server 2008", - "Windows Server 2012", - "Windows Vista", - "Windows 7", - "Windows 8", - "Windows 8.1", - "Windows 10", - "Windows 11" - ); - } - - static Stream otherOses() { - return Stream.of( - // POSIX/UNIX-like OSes. - "AIX", - "FreeBSD", - "HP-UX", - "Irix", - "LINUX", - "Linux", - "Mac", - "Mac OS X", - "MINIX", - "Minix", - "OpenBSD", - "NetBSD", - "Solaris", - "SunOS", - - // Other non-UNIX OSes. - "OS/2", - "OS/400", - "TempleOS", - "z/OS" - ); - } -} diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/ForwardingDiagnosticTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/jsr199/diagnostics/ForwardingDiagnosticTest.java similarity index 98% rename from java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/ForwardingDiagnosticTest.java rename to java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/jsr199/diagnostics/ForwardingDiagnosticTest.java index 565a0579a..8bc10a2dd 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/ForwardingDiagnosticTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/jsr199/diagnostics/ForwardingDiagnosticTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.github.ascopes.jct.testing.unit.compilers; +package io.github.ascopes.jct.testing.unit.jsr199.diagnostics; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; @@ -22,7 +22,7 @@ import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; -import io.github.ascopes.jct.compilers.ForwardingDiagnostic; +import io.github.ascopes.jct.jsr199.diagnostics.ForwardingDiagnostic; import java.util.Locale; import java.util.Random; import java.util.UUID; diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/TeeWriterTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/jsr199/diagnostics/TeeWriterTest.java similarity index 96% rename from java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/TeeWriterTest.java rename to java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/jsr199/diagnostics/TeeWriterTest.java index d77a07d72..e01aaa33b 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/TeeWriterTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/jsr199/diagnostics/TeeWriterTest.java @@ -14,14 +14,14 @@ * limitations under the License. */ -package io.github.ascopes.jct.testing.unit.compilers; +package io.github.ascopes.jct.testing.unit.jsr199.diagnostics; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import io.github.ascopes.jct.compilers.TeeWriter; +import io.github.ascopes.jct.jsr199.diagnostics.TeeWriter; import io.github.ascopes.jct.testing.helpers.MoreMocks; import java.io.ByteArrayOutputStream; import java.io.IOException; diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/TraceDiagnosticTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/jsr199/diagnostics/TraceDiagnosticTest.java similarity index 97% rename from java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/TraceDiagnosticTest.java rename to java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/jsr199/diagnostics/TraceDiagnosticTest.java index 71dbac829..c485ebf07 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/TraceDiagnosticTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/jsr199/diagnostics/TraceDiagnosticTest.java @@ -14,12 +14,12 @@ * limitations under the License. */ -package io.github.ascopes.jct.testing.unit.compilers; +package io.github.ascopes.jct.testing.unit.jsr199.diagnostics; import static org.assertj.core.api.BDDAssertions.then; import static org.assertj.core.api.BDDAssertions.thenCode; -import io.github.ascopes.jct.compilers.TraceDiagnostic; +import io.github.ascopes.jct.jsr199.diagnostics.TraceDiagnostic; import io.github.ascopes.jct.testing.helpers.MoreMocks; import io.github.ascopes.jct.testing.helpers.TypeRef; import java.time.Instant; diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/TracingDiagnosticListenerTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/jsr199/diagnostics/TracingDiagnosticListenerTest.java similarity index 98% rename from java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/TracingDiagnosticListenerTest.java rename to java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/jsr199/diagnostics/TracingDiagnosticListenerTest.java index 426bf1486..eb63b11dd 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/compilers/TracingDiagnosticListenerTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/jsr199/diagnostics/TracingDiagnosticListenerTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.github.ascopes.jct.testing.unit.compilers; +package io.github.ascopes.jct.testing.unit.jsr199.diagnostics; import static io.github.ascopes.jct.testing.helpers.MoreMocks.hasToString; import static io.github.ascopes.jct.testing.helpers.MoreMocks.stub; @@ -33,8 +33,8 @@ import static org.mockito.Mockito.when; import static org.mockito.Mockito.withSettings; -import io.github.ascopes.jct.compilers.TraceDiagnostic; -import io.github.ascopes.jct.compilers.TracingDiagnosticListener; +import io.github.ascopes.jct.jsr199.diagnostics.TraceDiagnostic; +import io.github.ascopes.jct.jsr199.diagnostics.TracingDiagnosticListener; import io.github.ascopes.jct.testing.helpers.TypeRef; import java.time.Instant; import java.util.Arrays; @@ -228,7 +228,7 @@ void nothingIsLoggedIfLoggingIsDisabled(Kind kind, boolean stackTraces) { var listener = new AccessibleImpl<>(logger, false, stackTraces); // When - var originalDiagnostic = someDiagnostic(kind, "Logging disabled tests"); + var originalDiagnostic = someDiagnostic(kind, "LoggingMode disabled tests"); listener.report(originalDiagnostic); // Then diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/intern/AsyncResourceCloserTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/AsyncResourceCloserTest.java similarity index 97% rename from java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/intern/AsyncResourceCloserTest.java rename to java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/AsyncResourceCloserTest.java index f1cbe0b71..2abd740e7 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/intern/AsyncResourceCloserTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/AsyncResourceCloserTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.github.ascopes.jct.testing.unit.intern; +package io.github.ascopes.jct.testing.unit.utils; import static java.time.Duration.ofMillis; import static java.time.Duration.ofSeconds; @@ -23,9 +23,9 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.awaitility.Awaitility.await; -import io.github.ascopes.jct.intern.AsyncResourceCloser; import io.github.ascopes.jct.testing.helpers.ConcurrentRuns; import io.github.ascopes.jct.testing.helpers.MoreMocks; +import io.github.ascopes.jct.utils.AsyncResourceCloser; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.DisplayName; diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/intern/EnumerationAdapterTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/EnumerationAdapterTest.java similarity index 97% rename from java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/intern/EnumerationAdapterTest.java rename to java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/EnumerationAdapterTest.java index 18a4d6aea..dbf2d443e 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/intern/EnumerationAdapterTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/EnumerationAdapterTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.github.ascopes.jct.testing.unit.intern; +package io.github.ascopes.jct.testing.unit.utils; import static org.assertj.core.api.BDDAssertions.then; import static org.assertj.core.api.BDDAssertions.thenCode; @@ -22,9 +22,9 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import io.github.ascopes.jct.intern.EnumerationAdapter; import io.github.ascopes.jct.testing.helpers.MoreMocks; import io.github.ascopes.jct.testing.helpers.TypeRef; +import io.github.ascopes.jct.utils.EnumerationAdapter; import java.util.Iterator; import java.util.NoSuchElementException; import org.junit.jupiter.api.DisplayName; diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/intern/IoExceptionUtilsTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/IoExceptionUtilsTest.java similarity index 96% rename from java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/intern/IoExceptionUtilsTest.java rename to java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/IoExceptionUtilsTest.java index be2b41544..cd4ddadb9 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/intern/IoExceptionUtilsTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/IoExceptionUtilsTest.java @@ -14,17 +14,17 @@ * limitations under the License. */ -package io.github.ascopes.jct.testing.unit.intern; +package io.github.ascopes.jct.testing.unit.utils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.BDDAssertions.then; import static org.assertj.core.api.BDDAssertions.thenCode; import static org.assertj.core.api.InstanceOfAssertFactories.array; -import io.github.ascopes.jct.intern.IoExceptionUtils; -import io.github.ascopes.jct.intern.IoExceptionUtils.IoRunnable; -import io.github.ascopes.jct.intern.IoExceptionUtils.IoSupplier; import io.github.ascopes.jct.testing.helpers.StaticClassTestTemplate; +import io.github.ascopes.jct.utils.IoExceptionUtils; +import io.github.ascopes.jct.utils.IoExceptionUtils.IoRunnable; +import io.github.ascopes.jct.utils.IoExceptionUtils.IoSupplier; import java.io.IOException; import java.io.UncheckedIOException; import java.util.concurrent.atomic.AtomicBoolean; diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/intern/IterableUtilsTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/IterableUtilsTest.java similarity index 68% rename from java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/intern/IterableUtilsTest.java rename to java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/IterableUtilsTest.java index 81d494ed5..b4789aa3a 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/intern/IterableUtilsTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/IterableUtilsTest.java @@ -14,14 +14,14 @@ * limitations under the License. */ -package io.github.ascopes.jct.testing.unit.intern; +package io.github.ascopes.jct.testing.unit.utils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import io.github.ascopes.jct.intern.IterableUtils; import io.github.ascopes.jct.testing.helpers.StaticClassTestTemplate; +import io.github.ascopes.jct.utils.IterableUtils; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.List; @@ -165,9 +165,9 @@ void nonNullUnmodifiableSetFailsWhenMultipleElementsAreNull() { .hasMessage("pete[2]"); } - @DisplayName("requireNonNullValues(Collection) succeeds when no null elements are present") + @DisplayName("requireNonNullValues(Iterable) succeeds when no null elements are present") @Test - void requireNonNullValuesSucceedsWhenNoNullElementsArePresent() { + void requireNonNullValuesIterableSucceedsWhenNoNullElementsArePresent() { // Given var collection = List.of("foo", "bar", "", "baz", "bork"); @@ -176,18 +176,18 @@ void requireNonNullValuesSucceedsWhenNoNullElementsArePresent() { .isThrownBy(() -> IterableUtils.requireNonNullValues(collection, "dave")); } - @DisplayName("requireNonNullValues(Collection) fails when the collection is null") + @DisplayName("requireNonNullValues(Iterable) fails when the collection is null") @Test - void requireNonNullValuesFailsWhenCollectionIsNull() { + void requireNonNullValuesIterableFailsWhenCollectionIsNull() { // Then - assertThatThrownBy(() -> IterableUtils.requireNonNullValues(null, "dave")) + assertThatThrownBy(() -> IterableUtils.requireNonNullValues((Iterable) null, "dave")) .isExactlyInstanceOf(NullPointerException.class) .hasMessage("dave"); } - @DisplayName("requireNonNullValues(Collection) fails when a single null element is present") + @DisplayName("requireNonNullValues(Iterable) fails when a single null element is present") @Test - void requireNonNullValuesFailsWhenSingleNullElementIsPresent() { + void requireNonNullValuesIterableFailsWhenSingleNullElementIsPresent() { // Given var collection = Arrays.asList("foo", "bar", "", null, "baz", "bork"); @@ -197,9 +197,9 @@ void requireNonNullValuesFailsWhenSingleNullElementIsPresent() { .hasMessage("dave[3]"); } - @DisplayName("requireNonNullValues(Collection) fails when multiple null elements are present") + @DisplayName("requireNonNullValues(Iterable) fails when multiple null elements are present") @Test - void requireNonNullValuesFailsWhenMultipleNullElementsArePresent() { + void requireNonNullValuesIterableFailsWhenMultipleNullElementsArePresent() { // Given var collection = Arrays.asList("foo", "bar", null, "", null, null, "baz", "bork"); @@ -208,4 +208,68 @@ void requireNonNullValuesFailsWhenMultipleNullElementsArePresent() { .isExactlyInstanceOf(NullPointerException.class) .hasMessage("dave[2], dave[4], dave[5]"); } + + @DisplayName("requireNonNullValues(T[]) succeeds when no null elements are present") + @Test + void requireNonNullValuesArraySucceedsWhenNoNullElementsArePresent() { + // Given + var array = new String[]{"foo", "bar", "", "baz", "bork"}; + + // Then + assertThatNoException() + .isThrownBy(() -> IterableUtils.requireNonNullValues(array, "dave")); + } + + @DisplayName("requireNonNullValues(T[]) fails when the collection is null") + @Test + void requireNonNullValuesArrayFailsWhenCollectionIsNull() { + // Then + assertThatThrownBy(() -> IterableUtils.requireNonNullValues((String[]) null, "dave")) + .isExactlyInstanceOf(NullPointerException.class) + .hasMessage("dave"); + } + + @DisplayName("requireNonNullValues(T[]) fails when a single null element is present") + @Test + void requireNonNullValuesArrayFailsWhenSingleNullElementIsPresent() { + // Given + var array = new String[]{"foo", "bar", "", null, "baz", "bork"}; + + // Then + assertThatThrownBy(() -> IterableUtils.requireNonNullValues(array, "dave")) + .isExactlyInstanceOf(NullPointerException.class) + .hasMessage("dave[3]"); + } + + @DisplayName("requireNonNullValues(T[]) fails when multiple null elements are present") + @Test + void requireNonNullValuesArrayFailsWhenMultipleNullElementsArePresent() { + // Given + var array = new String[]{"foo", "bar", null, "", null, null, "baz", "bork"}; + + // Then + assertThatThrownBy(() -> IterableUtils.requireNonNullValues(array, "dave")) + .isExactlyInstanceOf(NullPointerException.class) + .hasMessage("dave[2], dave[4], dave[5]"); + } + + @DisplayName("asList produces the expected result for one item") + @Test + void asListProducesTheExpectedResultForOneItem() { + // When + var list = IterableUtils.asList("foo"); + + // Then + assertThat(list).singleElement().isEqualTo("foo"); + } + + @DisplayName("asList produces the expected result for multiple items") + @Test + void asListProducesTheExpectedResultForMultipleItems() { + // When + var list = IterableUtils.asList("foo", "bar", "baz", "bork"); + + // Then + assertThat(list).containsExactly("foo", "bar", "baz", "bork"); + } } diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/intern/LazyTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/LazyTest.java similarity index 98% rename from java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/intern/LazyTest.java rename to java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/LazyTest.java index eba8006be..78d1ef516 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/intern/LazyTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/LazyTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.github.ascopes.jct.testing.unit.intern; +package io.github.ascopes.jct.testing.unit.utils; import static org.assertj.core.api.BDDAssertions.then; import static org.assertj.core.api.BDDAssertions.thenCode; @@ -23,10 +23,10 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import io.github.ascopes.jct.intern.Lazy; import io.github.ascopes.jct.testing.helpers.ConcurrentRuns; import io.github.ascopes.jct.testing.helpers.ThreadPool; import io.github.ascopes.jct.testing.helpers.ThreadPool.RunTestsInIsolation; +import io.github.ascopes.jct.utils.Lazy; import java.util.concurrent.Callable; import java.util.function.Supplier; import java.util.stream.Collectors; diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/intern/RecursiveDeleterTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/RecursiveDeleterTest.java similarity index 97% rename from java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/intern/RecursiveDeleterTest.java rename to java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/RecursiveDeleterTest.java index 5a4394d8f..3536191f5 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/intern/RecursiveDeleterTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/RecursiveDeleterTest.java @@ -14,13 +14,13 @@ * limitations under the License. */ -package io.github.ascopes.jct.testing.unit.intern; +package io.github.ascopes.jct.testing.unit.utils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import com.google.common.jimfs.Jimfs; -import io.github.ascopes.jct.intern.RecursiveDeleter; +import io.github.ascopes.jct.utils.RecursiveDeleter; import java.io.IOException; import java.nio.file.FileSystem; import java.nio.file.Files; diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/intern/SpecialLocationsTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/SpecialLocationsTest.java similarity index 98% rename from java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/intern/SpecialLocationsTest.java rename to java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/SpecialLocationsTest.java index b410d0388..8922d324d 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/intern/SpecialLocationsTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/SpecialLocationsTest.java @@ -14,15 +14,15 @@ * limitations under the License. */ -package io.github.ascopes.jct.testing.unit.intern; +package io.github.ascopes.jct.testing.unit.utils; import static java.util.function.Predicate.not; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; -import io.github.ascopes.jct.intern.SpecialLocations; import io.github.ascopes.jct.testing.helpers.StaticClassTestTemplate; +import io.github.ascopes.jct.utils.SpecialLocations; import java.io.Closeable; import java.io.File; import java.io.IOException; diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/intern/StringSlicerTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/StringSlicerTest.java similarity index 97% rename from java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/intern/StringSlicerTest.java rename to java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/StringSlicerTest.java index 4f457ced8..f8a0925e4 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/intern/StringSlicerTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/StringSlicerTest.java @@ -14,12 +14,12 @@ * limitations under the License. */ -package io.github.ascopes.jct.testing.unit.intern; +package io.github.ascopes.jct.testing.unit.utils; import static org.assertj.core.api.BDDAssertions.then; import static org.assertj.core.api.BDDAssertions.thenCode; -import io.github.ascopes.jct.intern.StringSlicer; +import io.github.ascopes.jct.utils.StringSlicer; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/intern/StringUtilsTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/StringUtilsTest.java similarity index 87% rename from java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/intern/StringUtilsTest.java rename to java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/StringUtilsTest.java index 9be4a423c..8c96fb988 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/intern/StringUtilsTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/testing/unit/utils/StringUtilsTest.java @@ -14,14 +14,14 @@ * limitations under the License. */ -package io.github.ascopes.jct.testing.unit.intern; +package io.github.ascopes.jct.testing.unit.utils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.BDDAssertions.then; import static org.junit.jupiter.params.provider.Arguments.of; -import io.github.ascopes.jct.intern.StringUtils; import io.github.ascopes.jct.testing.helpers.StaticClassTestTemplate; +import io.github.ascopes.jct.utils.StringUtils; import java.lang.reflect.Type; import java.util.Arrays; import java.util.Collections; @@ -48,6 +48,37 @@ public Class getTypeBeingTested() { return StringUtils.class; } + @DisplayName("toWordedList() returns the expected results") + @MethodSource("toWordedListCases") + @ParameterizedTest(name = "expect {0} to produce <{3}> when separated by <{1}> and <{2}>") + void toWordedListReturnsTheExpectedResults( + List inputs, + String connector, + String lastConnector, + String expected + ) { + // When + var actual = StringUtils.toWordedList(inputs, connector, lastConnector); + + // Then + assertThat(actual).isEqualTo(expected); + } + + static Stream toWordedListCases() { + return Stream.of( + of(List.of(), ", ", ", and ", ""), + of(List.of("foo"), ", ", ", and ", "foo"), + of(List.of("foo", "bar"), ", ", ", and ", "foo, and bar"), + of(List.of("foo", "bar", "baz"), ", ", ", and ", "foo, bar, and baz"), + of(List.of("foo", "bar", "baz", "bork"), ", ", ", and ", "foo, bar, baz, and bork"), + + of(List.of(), ", or ", ", or even ", ""), + of(List.of("foo"), ", or ", ", or even ", "foo"), + of(List.of("foo", "bar"), ", or ", ", or even ", "foo, or even bar"), + of(List.of("foo", "bar", "baz"), ", or ", ", or even ", "foo, or bar, or even baz") + ); + } + @DisplayName("leftPad() pads the string on the left") @CsvSource({ "'foo', -1, 'x', 'foo'", diff --git a/java-compiler-testing/src/test/java/module-info.java b/java-compiler-testing/src/test/java/module-info.java index 8cb45d94f..c853ba041 100644 --- a/java-compiler-testing/src/test/java/module-info.java +++ b/java-compiler-testing/src/test/java/module-info.java @@ -21,10 +21,12 @@ requires java.compiler; requires java.management; requires jimfs; - requires transitive net.bytebuddy; // required for mockito to work with JPMS. - requires transitive net.bytebuddy.agent; // required for mockito to work with JPMS. - requires transitive org.assertj.core; - requires transitive org.junit.jupiter; + requires net.bytebuddy; // required for mockito to work with JPMS. + requires net.bytebuddy.agent; // required for mockito to work with JPMS. + requires org.assertj.core; + requires org.junit.jupiter.api; + requires org.junit.jupiter.engine; + requires org.junit.jupiter.params; requires org.mockito; requires org.mockito.junit.jupiter; requires org.slf4j; diff --git a/java-compiler-testing/src/test/resources/logback-test.xml b/java-compiler-testing/src/test/resources/logback-test.xml index ce02b2133..a4125bf5b 100644 --- a/java-compiler-testing/src/test/resources/logback-test.xml +++ b/java-compiler-testing/src/test/resources/logback-test.xml @@ -10,4 +10,4 @@ - \ No newline at end of file + diff --git a/pom.xml b/pom.xml index 5dbb301b7..7499f61ae 100644 --- a/pom.xml +++ b/pom.xml @@ -43,29 +43,8 @@ - 1.1.2 3.22.0 - 4.2.0 - 3.29.0 - 1.4.0 1.2 - 5.8.2 - 1.2.11 - 4.4.0 - 0.10.2 - 1.7.36 - - - 10.0 - 3.1.2 - 3.8.1 - 3.2.2 - 3.3.2 - 3.0.0-M6 - - - 0.8.8 - 0.1.0 true @@ -92,7 +71,7 @@ ch.qos.logback logback-classic - ${logback.version} + 1.2.11 @@ -106,14 +85,14 @@ me.xdrop fuzzywuzzy - ${fuzzywuzzy.version} + 1.4.0 org.apiguardian apiguardian-api - ${apiguardian.version} + 1.1.2 @@ -127,21 +106,21 @@ org.awaitility awaitility - ${awaitility.version} + 4.2.0 org.eclipse.jdt ecj - ${ecj.version} + 3.29.0 org.junit junit-bom - ${junit.version} + 5.8.2 import pom @@ -150,23 +129,33 @@ org.mockito mockito-bom - ${mockito.version} + 4.5.1 import pom + + + org.projectlombok + lombok + 1.18.24 + + org.reflections reflections - ${reflections.version} + 0.10.2 org.slf4j slf4j-api - ${slf4j.version} + 1.7.36 @@ -178,7 +167,7 @@ org.jacoco jacoco-maven-plugin - ${jacoco-maven-plugin.version} + 0.8.8 @@ -203,9 +192,10 @@ org.apache.maven.plugins maven-compiler-plugin - ${maven-compiler-plugin.version} + 3.8.1 + true true ${java-release.version} @@ -214,7 +204,7 @@ org.apache.maven.plugins maven-javadoc-plugin - ${maven-javadoc-plugin.version} + 3.3.2 @@ -232,7 +222,7 @@ org.apache.maven.plugins maven-jar-plugin - ${maven-jar-plugin.version} + 3.2.2 @@ -254,7 +244,7 @@ org.apache.maven.plugins maven-surefire-plugin - ${maven-surefire-plugin.version} + 3.0.0-M6 - org.apache.maven.plugins - maven-checkstyle-plugin - ${maven-checkstyle-plugin.version} - - - - checkstyle - compile - - check - - - - - ${maven.multiModuleProjectDirectory}/.mvn/checkstyle/checkstyle.xml - - true - UTF-8 - true - - - ${maven.multiModuleProjectDirectory}/.mvn/checkstyle/license-header.txt - - true - ${project.basedir}/src - + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.1.2 + + + + checkstyle + compile + + check + + + + + ${maven.multiModuleProjectDirectory}/.mvn/checkstyle/checkstyle.xml + + true + UTF-8 + true - ${maven.multiModuleProjectDirectory}/.mvn/checkstyle/suppressions.xml - - info - - - - - - - - com.puppycrawl.tools - checkstyle - ${checkstyle.version} - - - - + + ${maven.multiModuleProjectDirectory}/.mvn/checkstyle/license-header.txt + + true + ${project.basedir}/src + + + ${maven.multiModuleProjectDirectory}/.mvn/checkstyle/suppressions.xml + + info + + + + + + + + com.puppycrawl.tools + checkstyle + 10.0 + + + + + diff --git a/scripts/build-in-containers.sh b/scripts/build-in-containers.sh new file mode 100755 index 000000000..184fefb2d --- /dev/null +++ b/scripts/build-in-containers.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +### +### Copyright (C) 2022 Ashley Scopes +### +### Licensed under the Apache License, Version 2.0 (the "License"); +### you may not use this desired_jacoco_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. +### +### Script that will build this project in the given range of JDK versions. +### + +set -o errexit +set -o pipefail + +default_command="./mvnw -T8C clean package" + +if ! command -v docker >/dev/null 2>&1; then + echo "ERROR: docker is not installed on this system. Please ensure it is on your " + echo " \$PATH, and then try again." + exit 2 +fi + +function usage() { + echo "USAGE: ${1} [-c ] [-h] -v " + echo " -c The command to run. If unspecified, this will default to" + echo " ${default_command}" + echo " -h Show this message and exit." + echo " -v Set the version range to build with." + echo + echo " Version ranges can be a single digit (e.g. 17 for JDK 17), or a closed range, delimited" + echo " with a hyphen \`-' character (e.g. 11-17 for JDK 11, 12, 13, ..., 16, and 17)." + echo +} + +command="${default_command}" + +while getopts ":c:v:h" opt; do + case "${opt}" in + c) + command="${OPTARG}" + ;; + h) + usage "${0}" + exit 0 + ;; + v) + if ! echo "${OPTARG}" | grep -qE "^[0-9]+(-[0-9]+)?$"; then + echo "ERROR: Invalid range syntax for version." + usage "${0}" + exit 1 + else + first_version=$(echo "${OPTARG}" | cut -d- -f 1) + last_version=$(echo "${OPTARG}" | cut -d- -f 2) + fi + ;; + esac +done + +if [ -z ${first_version:+undef} ]; then + echo "ERROR: Missing parameter '-v '" + usage "${0}" + exit 1 +fi + +if [ -z ${M2_HOME+undef} ]; then + export M2_HOME="${HOME:-$HOMEDIR}/.m2" +fi + +workspace_dir="$(realpath $(dirname ${BASH_SOURCE[0]:-$0})/..)" +container_workspace_dir="/src" +m2_container_dir=/m2 + +mkdir target 2>&1 || true + +for version in $(seq ${first_version} ${last_version}); do + # AWS Elastic Container Registry will not rate-limit us like DockerHub will, so prefer them + # instead. + image="public.ecr.aws/docker/library/openjdk:${version}" + + if [ -z "$(docker image ls "${image}" --quiet)" ]; then + echo -e "\e[1;33mPulling \e[3;34m${image}\e[0;1;33m, as it was not found on this system.\e[0m" + docker pull "${image}" + fi + + echo -en "\e[0;1;33mRunning command \e[3;35m${command}\e[0;1;33m in " + echo -e "\e[3;34m${image}\e[0;1;33m container...\e[0m" + + docker run \ + -e "MAVEN_OPTS=-Dstyle.color=always" \ + -e "M2_HOME=${m2_container_dir}" \ + --name "jct-build-in-containers-jdk-${version}" \ + -t \ + -u "$(id -u "${USER}")" \ + -v "$(realpath "${M2_HOME}"):${m2_container_dir}" \ + -v "${workspace_dir}:${container_workspace_dir}" \ + -w "${container_workspace_dir}" \ + --rm \ + "${image}" \ + ${command} \ + 2>&1 | + while read line; do + printf "\e[1;33m[JDK %2d]\e[0m %s\n" "${version}" "${line}" + done +done diff --git a/.github/scripts/prepare-test-outputs-for-merge.sh b/scripts/prepare-test-outputs-for-merge.sh similarity index 94% rename from .github/scripts/prepare-test-outputs-for-merge.sh rename to scripts/prepare-test-outputs-for-merge.sh index 59a00698c..1bd3bd9aa 100755 --- a/.github/scripts/prepare-test-outputs-for-merge.sh +++ b/scripts/prepare-test-outputs-for-merge.sh @@ -19,12 +19,13 @@ ### that the test applies to, and to rename the jacoco.xml files to match the Java version in use. ### -set -e +set -o errexit +set -o pipefail CI_JAVA_VERSION=${1?Pass the Java version as the first argument to this script!} CI_OS=${2?Pass the OS name as the second argument to this script!} -if ! command -v xsltproc > /dev/null 2>&1; then +if ! command -v xsltproc >/dev/null 2>&1; then if [ -z ${CI+_} ]; then echo -e "\e[1;31mERROR\e[0m: xsltproc is not found -- make sure it is installed first." exit 2 @@ -37,7 +38,7 @@ fi echo -e "\e[1;35mUpdating Surefire reports...\e[0m" surefire_prefix_xslt=$(mktemp --suffix=.xslt) -sed 's/^ //g' > "${surefire_prefix_xslt}" <<'EOF' +sed 's/^ //g' >"${surefire_prefix_xslt}" <<'EOF'