diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/PackageContainerGroupAssert.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/PackageContainerGroupAssert.java index 96accac74..d480ff8f4 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/PackageContainerGroupAssert.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/assertions/PackageContainerGroupAssert.java @@ -59,31 +59,34 @@ public PackageContainerGroupAssert(@Nullable PackageContainerGroup containerGrou * * @param path the first path to check for * @param paths additional paths to check for. + * @return this object for further call chaining. * @throws AssertionError if the container group is null, or if any of the files do not * exist. * @throws NullPointerException if any of the paths are null. */ - public void allFilesExist(String path, String... paths) { + public PackageContainerGroupAssert allFilesExist(String path, String... paths) { requireNonNull(path, "path must not be null"); requireNonNullValues(paths, "paths"); - allFilesExist(combineOneOrMore(path, paths)); + return allFilesExist(combineOneOrMore(path, paths)); } /** * Assert that all given files exist. * * @param paths paths to check for. + * @return this object for further call chaining. * @throws AssertionError if the container group is null, or if any of the files do not * exist. * @throws NullPointerException if any of the paths are null. */ - public void allFilesExist(Iterable paths) { + public PackageContainerGroupAssert allFilesExist(Iterable paths) { requireNonNullValues(paths, "paths"); isNotNull(); assertThat(paths).allSatisfy(this::fileExists); + return this; } /** diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/JctCompilationFactory.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/JctCompilationFactory.java index 58de36bcf..bdf42cc63 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/JctCompilationFactory.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/JctCompilationFactory.java @@ -43,7 +43,9 @@ public interface JctCompilationFactory { * should be discovered automatically. * @return the compilation result that contains whether the compiler succeeded or failed, amongst * other information. - * @throws JctCompilerException if compiler raises an unhandled exception and cannot complete. + * @throws JctCompilerException if any prerequisites fail, such as no compilation units being + * found, or if the underlying JSR-199 compiler raises an unhandled exception and cannot + * complete when invoked. */ JctCompilation createCompilation( List flags, diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/impl/JctCompilationFactoryImpl.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/impl/JctCompilationFactoryImpl.java index 2a03ef5cf..39c47fb64 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/impl/JctCompilationFactoryImpl.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/impl/JctCompilationFactoryImpl.java @@ -26,19 +26,21 @@ import io.github.ascopes.jct.ex.JctCompilerException; import io.github.ascopes.jct.filemanagers.JctFileManager; import io.github.ascopes.jct.filemanagers.LoggingMode; +import io.github.ascopes.jct.filemanagers.PathFileObject; import io.github.ascopes.jct.utils.IterableUtils; import java.io.IOException; -import java.io.OutputStreamWriter; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import javax.tools.JavaCompiler; import javax.tools.JavaFileObject; import javax.tools.JavaFileObject.Kind; import javax.tools.StandardLocation; import org.apiguardian.api.API; import org.apiguardian.api.API.Status; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -69,9 +71,11 @@ public JctCompilation createCompilation( ) { try { return createCheckedCompilation(flags, fileManager, jsr199Compiler, classNames); + } catch (JctCompilerException ex) { - // Fall through, do not rewrap these. + // Rethrow JctCompilerExceptions -- we don't want to wrap these again. throw ex; + } catch (Exception ex) { throw new JctCompilerException( "Failed to perform compilation, an unexpected exception was raised", ex @@ -85,10 +89,14 @@ private JctCompilation createCheckedCompilation( JavaCompiler jsr199Compiler, Collection classNames ) throws Exception { - var compilationUnits = findCompilationUnits(fileManager); + var compilationUnits = findFilteredCompilationUnits(fileManager, classNames); + + if (compilationUnits.isEmpty()) { + throw new JctCompilerException("No compilation units were found in the given workspace"); + } // Do not close stdout, it breaks test engines, especially IntellIJ. - var writer = new TeeWriter(new OutputStreamWriter(System.out, compiler.getLogCharset())); + var writer = TeeWriter.wrapOutputStream(System.out, compiler.getLogCharset()); var diagnosticListener = new TracingDiagnosticListener<>( compiler.getDiagnosticLoggingMode() != LoggingMode.DISABLED, @@ -100,7 +108,7 @@ private JctCompilation createCheckedCompilation( fileManager, diagnosticListener, flags, - classNames, + null, compilationUnits ); @@ -109,17 +117,13 @@ private JctCompilation createCheckedCompilation( task.setProcessors(processors); } + task.setLocale(compiler.getLocale()); + LOGGER .atInfo() - .setMessage( - "Starting compilation with {} (found {} compilation units, {} user-provided class names)" - ) + .setMessage("Starting compilation with {} (found {} compilation units)") .addArgument(compiler::getName) .addArgument(compilationUnits::size) - .addArgument(classNames == null - ? () -> "no" - : classNames::size - ) .log(); var start = System.nanoTime(); @@ -129,6 +133,9 @@ private JctCompilation createCheckedCompilation( ); var delta = (System.nanoTime() - start) / 1_000_000L; + // Ensure we commit the writer contents to the wrapped output stream in full. + writer.flush(); + LOGGER .atInfo() .setMessage("Compilation with {} {} after approximately {}ms (roughly {} classes/sec)") @@ -145,16 +152,35 @@ private JctCompilation createCheckedCompilation( return JctCompilationImpl .builder() - .compilationUnits(compilationUnits) + .compilationUnits(Set.copyOf(compilationUnits)) .fileManager(fileManager) - .outputLines(writer.toString().lines().collect(toList())) + .outputLines(writer.getContent().lines().collect(toList())) .diagnostics(diagnosticListener.getDiagnostics()) .success(success) .failOnWarnings(compiler.isFailOnWarnings()) .build(); } - private Set findCompilationUnits(JctFileManager fileManager) throws IOException { + private Collection findFilteredCompilationUnits( + JctFileManager fileManager, + @Nullable Collection classNames + ) throws IOException { + var compilationUnits = findCompilationUnits(fileManager); + + if (classNames == null) { + return compilationUnits; + } + + if (classNames.isEmpty()) { + throw new JctCompilerException("The list of explicit class names to compile is empty"); + } + + return filterCompilationUnitsByBinaryNames(compilationUnits, classNames); + } + + private Collection findCompilationUnits( + JctFileManager fileManager + ) throws IOException { var locations = IterableUtils .flatten(fileManager.listLocationsForModules(StandardLocation.MODULE_SOURCE_PATH)); @@ -175,12 +201,40 @@ private Set findCompilationUnits(JctFileManager fileManager) thr objects.addAll(fileManager.list(location, "", Set.of(Kind.SOURCE), true)); } - if (objects.isEmpty()) { - throw new JctCompilerException( - "No compilation units were found. Did you forget to add something?" - ); + return objects; + } + + private Collection filterCompilationUnitsByBinaryNames( + Collection compilationUnits, + Collection classNames + ) { + var binaryNamesToCompilationUnits = compilationUnits + .stream() + // Assumption that we always use this class internally. Technically unsafe, but we don't + // care too much as the implementation should conform to this anyway. We just cannot enforce + // it due to covariance rules. + .map(PathFileObject.class::cast) + .filter(fo -> classNames.contains(fo.getBinaryName())) + .collect(Collectors.toMap( + PathFileObject::getBinaryName, + JavaFileObject.class::cast + )); + + for (var className : classNames) { + var compilationUnit = binaryNamesToCompilationUnits.get(className); + if (compilationUnit == null) { + throw new JctCompilerException( + "No compilation unit matching " + className + " found in the provided sources" + ); + } } - return objects; + LOGGER.atDebug() + .setMessage("Filtered {} candidate compilation units down to {} final compilation units") + .addArgument(compilationUnits::size) + .addArgument(binaryNamesToCompilationUnits::size) + .log(); + + return binaryNamesToCompilationUnits.values(); } } diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/diagnostics/TeeWriter.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/diagnostics/TeeWriter.java index 8618871ae..23a49ebd5 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/diagnostics/TeeWriter.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/diagnostics/TeeWriter.java @@ -16,9 +16,13 @@ package io.github.ascopes.jct.diagnostics; import static java.util.Objects.requireNonNull; +import static java.util.Objects.requireNonNullElse; import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; import java.io.Writer; +import java.nio.charset.Charset; import org.apiguardian.api.API; import org.apiguardian.api.API.Status; @@ -80,13 +84,31 @@ public void flush() throws IOException { } } - @Override - public String toString() { + /** + * Get the content of the internal buffer. + * + * @return the content. + * @since 0.2.1 + */ + @API(since = "0.2.1", status = Status.STABLE) + public String getContent() { synchronized (lock) { return builder.toString(); } } + /** + * Get the content of the internal buffer. + * + *

This calls {@link #getContent()} internally as of 0.2.1. + * + * @return the content. + */ + @Override + public String toString() { + return getContent(); + } + @Override public void write(char[] cbuf, int off, int len) throws IOException { synchronized (lock) { @@ -104,4 +126,23 @@ private void ensureOpen() { throw new IllegalStateException("TeeWriter is closed"); } } + + /** + * Create a tee writer for the given output stream. + * + *

Remember you may need to manually flush the tee writer for all contents to be committed to + * the output stream. + * + * @param outputStream the output stream. + * @param charset the charset. + * @return the Tee Writer. + * @since 0.2.1 + */ + @API(since = "0.2.1", status = Status.STABLE) + public static TeeWriter wrapOutputStream(OutputStream outputStream, Charset charset) { + requireNonNull(outputStream, "outputStream"); + requireNonNull(charset, "charset"); + var writer = new OutputStreamWriter(outputStream, charset); + return new TeeWriter(writer); + } } diff --git a/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/PathFileObject.java b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/PathFileObject.java index e0b3e02dc..d94fcad42 100644 --- a/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/PathFileObject.java +++ b/java-compiler-testing/src/main/java/io/github/ascopes/jct/filemanagers/PathFileObject.java @@ -150,6 +150,17 @@ public Modifier getAccessLevel() { return unknown(); } + /** + * Get the inferred binary name of the file object. + * + * @return the inferred binary name. + * @since 0.2.1 + */ + @API(since = "0.2.1", status = Status.STABLE) + public String getBinaryName() { + return FileUtils.pathToBinaryName(relativePath); + } + /** * Read the character content of the file into memory and decode it using the default character * set. diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/helpers/ExtraArgumentMatchers.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/helpers/ExtraArgumentMatchers.java new file mode 100644 index 000000000..f86fbb7a4 --- /dev/null +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/helpers/ExtraArgumentMatchers.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022 - 2023, the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.ascopes.jct.tests.helpers; + +import static org.mockito.ArgumentMatchers.argThat; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Set; +import org.mockito.ArgumentMatcher; + +/** + * Extra Mockito argument matchers. + * + * @author Ashley Scopes + */ +public final class ExtraArgumentMatchers { + + private ExtraArgumentMatchers() { + throw new UnsupportedOperationException("static-only class"); + } + + @SafeVarargs + public static > T containsExactlyElements(E... expected) { + return containsExactlyElements(Set.of(expected)); + } + + public static > T containsExactlyElements(Collection expected) { + return argThat(new ArgumentMatcher<>() { + @Override + public String toString() { + return "containsExactlyElements(" + expected + ")"; + } + + @Override + public boolean matches(T actualIterable) { + if (actualIterable == null) { + return false; + } + + var actualElements = new ArrayList(); + actualIterable.forEach(actualElements::add); + + // All expected are in actual + for (var expectedElement : expected) { + if (!actualElements.contains(expectedElement)) { + throw new IllegalArgumentException( + "Expected element " + expectedElement + " was not in the actual collection" + ); + } + } + + // All actual are in expected. + var expectedSet = Set.copyOf(expected); + for (var actualElement : actualElements) { + if (!expectedSet.contains(actualElement)) { + throw new IllegalArgumentException( + "Actual element " + actualElement + " was not in the expected elements array" + ); + } + } + + return true; + } + }); + } +} diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/integration/CompilingSpecificClassesIntegrationTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/integration/CompilingSpecificClassesIntegrationTest.java new file mode 100644 index 000000000..3a429ec13 --- /dev/null +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/integration/CompilingSpecificClassesIntegrationTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2022 - 2023, the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.ascopes.jct.tests.integration; + +import static io.github.ascopes.jct.assertions.JctAssertions.assertThat; + +import io.github.ascopes.jct.compilers.JctCompiler; +import io.github.ascopes.jct.junit.JavacCompilerTest; +import io.github.ascopes.jct.workspaces.Workspaces; +import org.junit.jupiter.api.DisplayName; + +/** + * Integration tests that test the compilation of specific classes only. + * + * @author Ashley Scopes + */ +@DisplayName("Compiling specific classes integration tests") +class CompilingSpecificClassesIntegrationTest { + @DisplayName("Only the classes that I specify get compiled") + @JavacCompilerTest + void onlyTheClassesSpecifiedGetCompiled(JctCompiler compiler) { + try (var workspace = Workspaces.newWorkspace()) { + workspace + .createSourcePathPackage() + .copyContentsFrom("src", "test", "resources", "integration", "specificclasses"); + + var compilation = compiler.compile(workspace, "Fibonacci", "HelloWorld"); + + assertThat(compilation) + .isSuccessfulWithoutWarnings() + .classOutput() + .packages() + .allFilesExist("Fibonacci.class", "HelloWorld.class") + .fileDoesNotExist("Sum.class"); + } + } +} diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/impl/JctCompilationFactoryImplTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/impl/JctCompilationFactoryImplTest.java new file mode 100644 index 000000000..e2dc9673c --- /dev/null +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/compilers/impl/JctCompilationFactoryImplTest.java @@ -0,0 +1,725 @@ +/* + * Copyright (C) 2022 - 2023, the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.ascopes.jct.tests.unit.compilers.impl; + +import static io.github.ascopes.jct.tests.helpers.ExtraArgumentMatchers.containsExactlyElements; +import static io.github.ascopes.jct.tests.helpers.Fixtures.oneOf; +import static io.github.ascopes.jct.tests.helpers.Fixtures.someBinaryName; +import static io.github.ascopes.jct.tests.helpers.Fixtures.someFlags; +import static io.github.ascopes.jct.tests.helpers.Fixtures.someLinesOfText; +import static io.github.ascopes.jct.tests.helpers.Fixtures.someText; +import static io.github.ascopes.jct.tests.helpers.Fixtures.someTraceDiagnostic; +import static io.github.ascopes.jct.utils.IterableUtils.flatten; +import static java.util.stream.Collectors.toList; +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.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; + +import io.github.ascopes.jct.compilers.JctCompilation; +import io.github.ascopes.jct.compilers.JctCompiler; +import io.github.ascopes.jct.compilers.impl.JctCompilationFactoryImpl; +import io.github.ascopes.jct.diagnostics.TeeWriter; +import io.github.ascopes.jct.diagnostics.TracingDiagnosticListener; +import io.github.ascopes.jct.ex.JctCompilerException; +import io.github.ascopes.jct.filemanagers.JctFileManager; +import io.github.ascopes.jct.filemanagers.LoggingMode; +import io.github.ascopes.jct.filemanagers.ModuleLocation; +import io.github.ascopes.jct.filemanagers.PathFileObject; +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import javax.annotation.processing.Processor; +import javax.tools.JavaCompiler; +import javax.tools.JavaCompiler.CompilationTask; +import javax.tools.JavaFileManager.Location; +import javax.tools.JavaFileObject; +import javax.tools.JavaFileObject.Kind; +import javax.tools.StandardLocation; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.BeforeEach; +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.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedConstruction.MockInitializer; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.quality.Strictness; + +/** + * {@link JctCompilationFactoryImpl} tests. + * + * @author Ashley Scopes + */ +@DisplayName("JctCompilationFactoryImpl tests") +@ExtendWith(MockitoExtension.class) +class JctCompilationFactoryImplTest { + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + JctFileManager fileManager; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + JavaCompiler javaCompiler; + + List flags; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + JctCompiler jctCompiler; + + @InjectMocks + JctCompilationFactoryImpl factory; + + @BeforeEach + void setUp() { + flags = someFlags(); + } + + JctCompilation doCompile(@Nullable Collection classNames) { + return factory.createCompilation(flags, fileManager, javaCompiler, classNames); + } + + @DisplayName("Multi-module sources are used when they exist") + @Test + void multiModuleSourcesAreUsedWhenTheyExist() throws IOException { + // Given + var apiLocation = new ModuleLocation(StandardLocation.MODULE_SOURCE_PATH, "org.example.api"); + var apiObjects = Set.of( + somePathFileObject(someBinaryName()), + somePathFileObject(someBinaryName()) + ); + + var implLocation = new ModuleLocation(StandardLocation.MODULE_SOURCE_PATH, "org.example.impl"); + var implObjects = Set.of( + somePathFileObject(someBinaryName()), + somePathFileObject(someBinaryName()) + ); + + var allObjects = flatten(Set.of(apiObjects, implObjects)); + + var multiModuleLocations = (Iterable>) Set.of( + Set.of(apiLocation, implLocation) + ); + + when(fileManager.listLocationsForModules(any())) + .thenReturn(multiModuleLocations); + + when(fileManager.list(eq(apiLocation), any(), any(), anyBoolean())) + .thenReturn(apiObjects); + + when(fileManager.list(eq(implLocation), any(), any(), anyBoolean())) + .thenReturn(implObjects); + + // When + doCompile(null); + + // Then + verify(fileManager).listLocationsForModules(StandardLocation.MODULE_SOURCE_PATH); + verify(fileManager).list(apiLocation, "", Set.of(Kind.SOURCE), true); + verify(fileManager).list(implLocation, "", Set.of(Kind.SOURCE), true); + verifyNoMoreInteractions(fileManager); + + verify(javaCompiler).getTask( + any(), + any(), + any(), + any(), + any(), + containsExactlyElements(allObjects) + ); + } + + @DisplayName("Legacy sources are used when multi-module sources do not exist") + @Test + void legacySourcesAreUsedWhenMultiModuleSourcesDoNotExist() throws IOException { + // Given + when(fileManager.listLocationsForModules(any())) + .thenReturn(Set.of()); + + var sourceObjects = Set.of( + somePathFileObject(someBinaryName()), + somePathFileObject(someBinaryName()), + somePathFileObject(someBinaryName()), + somePathFileObject(someBinaryName()) + ); + + when(fileManager.list(eq(StandardLocation.SOURCE_PATH), any(), any(), anyBoolean())) + .thenReturn(sourceObjects); + + // When + doCompile(null); + + // Then + verify(fileManager).listLocationsForModules(StandardLocation.MODULE_SOURCE_PATH); + verify(fileManager).list(StandardLocation.SOURCE_PATH, "", Set.of(Kind.SOURCE), true); + verifyNoMoreInteractions(fileManager); + + verify(javaCompiler).getTask( + any(), + any(), + any(), + any(), + any(), + containsExactlyElements(sourceObjects) + ); + } + + @DisplayName("All compilation units are used when null classNames collection is provided") + @Test + void allCompilationUnitsAreUsedWhenNullClassNamesCollectionIsProvided() throws IOException { + // Given + var compilationUnits = Set.of( + somePathFileObject("foo.bar.Baz"), + somePathFileObject("do.ray.Me"), + somePathFileObject("a.b.C") + ); + when(fileManager.list(any(), any(), any(), anyBoolean())) + .thenReturn(compilationUnits); + + // When + doCompile(null); + + // Then + verify(javaCompiler).getTask( + any(), + same(fileManager), + any(), + any(), + isNull(), + eq(compilationUnits) + ); + } + + @DisplayName("All compilation units are included in the compilation result when null " + + "classNames collection is provided") + @Test + void allCompilationUnitsAreIncludedInTheCompilationResultWhenNullClassNamesCollectionIsProvided() + throws IOException { + // Given + var compilationUnits = Set.of( + somePathFileObject("foo.bar.Baz"), + somePathFileObject("do.ray.Me"), + somePathFileObject("a.b.C") + ); + when(fileManager.list(any(), any(), any(), anyBoolean())) + .thenReturn(compilationUnits); + + // When + var result = doCompile(null); + + // Then + assertThat(result.getCompilationUnits()) + .isEqualTo(compilationUnits); + } + + @DisplayName("Filtering compilation units with an empty class names collection raises an error") + @Test + void filteringCompilationUnitsWithEmptyClassNamesCollectionRaisesAnError() { + // Then + assertThatThrownBy(() -> doCompile(List.of())) + .isInstanceOf(JctCompilerException.class) + .hasMessage("The list of explicit class names to compile is empty"); + + verifyNoInteractions(javaCompiler); + } + + @DisplayName("Filtered compilation units are used when class names are provided") + @Test + void filteredCompilationUnitsAreUsedWhenClassNamesAreProvided() throws IOException { + // Given + var fooBarBaz = somePathFileObject("foo.bar.Baz"); + var doRayMe = somePathFileObject("do.ray.Me"); + + var compilationUnits = Set.of(fooBarBaz, doRayMe, somePathFileObject("a.b.C")); + when(fileManager.list(any(), any(), any(), anyBoolean())) + .thenReturn(compilationUnits); + + // When + doCompile(Set.of("foo.bar.Baz", "do.ray.Me")); + + // Then + verify(javaCompiler).getTask( + any(), + same(fileManager), + any(), + any(), + isNull(), + containsExactlyElements(doRayMe, fooBarBaz) + ); + } + + @DisplayName("Filtered compilation units are included in the compilation result when " + + "class names are provided") + @Test + void filteredCompilationUnitsAreIncludedInTheCompilationResultWhenClassNamesAreProvided() + throws IOException { + // Given + var fooBarBaz = somePathFileObject("foo.bar.Baz"); + var doRayMe = somePathFileObject("do.ray.Me"); + + var compilationUnits = Set.of(fooBarBaz, doRayMe, somePathFileObject("a.b.C")); + when(fileManager.list(any(), any(), any(), anyBoolean())) + .thenReturn(compilationUnits); + + // When + var result = doCompile(Set.of("foo.bar.Baz", "do.ray.Me")); + + // Then + assertThat(result.getCompilationUnits()) + .containsExactlyInAnyOrder(fooBarBaz, doRayMe); + } + + @DisplayName("An error is raised when an explicit class name is not in the compilation units") + @Test + void anErrorIsRaisedWhenAnExplicitClassNameIsNotInTheCompilationUnits() throws IOException { + // Given + var fooBarBaz = somePathFileObject("foo.bar.Baz"); + var doRayMe = somePathFileObject("do.ray.Me"); + + var compilationUnits = Set.of(fooBarBaz, doRayMe, somePathFileObject("a.b.C")); + when(fileManager.list(any(), any(), any(), anyBoolean())) + .thenReturn(compilationUnits); + + // Then + assertThatThrownBy(() -> doCompile(Set.of("foo.bar.Baz", "this.clazz.does.not.Exist"))) + .isInstanceOf(JctCompilerException.class) + .hasMessage( + "No compilation unit matching this.clazz.does.not.Exist found in the provided sources" + ); + verifyNoInteractions(javaCompiler); + } + + @DisplayName("An error is raised if no compilation units are found") + @Test + void anErrorIsRaisedIfNoCompilationUnitsAreFound() throws IOException { + // Given + when(fileManager.list(any(), any(), any(), anyBoolean())) + .thenReturn(Set.of()); + + // Then + assertThatThrownBy(() -> doCompile(null)) + .isInstanceOf(JctCompilerException.class) + .hasMessage("No compilation units were found in the given workspace"); + verifyNoInteractions(javaCompiler); + } + + @DisplayName("The compiler should write logs to System.out") + @Test + void theCompilerShouldWriteLogsToSystemOut() throws IOException { + // Given + try (var teeWriterStatic = mockStatic(TeeWriter.class)) { + var teeWriter = mock(TeeWriter.class); + teeWriterStatic.when(() -> TeeWriter.wrapOutputStream(any(), any())) + .thenReturn(teeWriter); + when(teeWriter.getContent()) + .thenReturn(someLinesOfText()); + + // Do not inline this, it will break in Mockito's stubber backend. + var fileObjects = Set.of(somePathFileObject(someBinaryName())); + when(fileManager.list(any(), any(), any(), anyBoolean())) + .thenReturn(fileObjects); + + // When + doCompile(null); + + // Then + teeWriterStatic.verify(() -> TeeWriter.wrapOutputStream( + System.out, + jctCompiler.getLogCharset() + )); + teeWriterStatic.verifyNoMoreInteractions(); + + // Ensure the tee writer is used for the compiler task. + verify(javaCompiler).getTask(same(teeWriter), any(), any(), any(), any(), any()); + } + } + + @DisplayName("The tee writer logs should be placed in the compilation result") + @Test + @SuppressWarnings("ResultOfMethodCallIgnored") + void teeWriterLogsShouldBePlacedInTheCompilationResult() throws IOException { + // Given + try (var teeWriterStatic = mockStatic(TeeWriter.class)) { + var teeWriter = mock(TeeWriter.class); + var order = inOrder(teeWriter); + + teeWriterStatic.when(() -> TeeWriter.wrapOutputStream(any(), any())) + .thenReturn(teeWriter); + var lines = someLinesOfText(); + when(teeWriter.getContent()).thenReturn(lines); + + // Do not inline this, it will break in Mockito's stubber backend. + var fileObjects = Set.of(somePathFileObject(someBinaryName())); + when(fileManager.list(any(), any(), any(), anyBoolean())) + .thenReturn(fileObjects); + + // When + var result = doCompile(null); + + // Then + assertThat(result.getOutputLines()) + .containsExactlyElementsOf(lines.lines().collect(toList())); + + // Ensure we flushed before we called toString, otherwise data might be missing. + order.verify(teeWriter).flush(); + order.verify(teeWriter).getContent(); + } + } + + @DisplayName("A correctly configured diagnostic listener is used for compilation") + @CsvSource({ + "DISABLED, false, false", + "ENABLED, true, false", + "STACKTRACES, true, true" + }) + @ParameterizedTest(name = "- LoggingMode.{0} should use new TracingDiagnosticListener({1}, {2})") + @SuppressWarnings({"unchecked", "rawtypes"}) + void correctlyConfiguredDiagnosticListenerIsUsedForCompilation( + LoggingMode loggingMode, + boolean expectedEnabled, + boolean expectedStackTraces + ) throws IOException { + // Given + when(jctCompiler.getDiagnosticLoggingMode()) + .thenReturn(loggingMode); + + MockInitializer verifier = (mock, ctx) -> { + assertThat(ctx.arguments()) + .hasSize(2) + .satisfies( + args -> assertThat(args).element(0).isEqualTo(expectedEnabled), + args -> assertThat(args).element(1).isEqualTo(expectedStackTraces) + ); + }; + + try (var listenerCls = mockConstruction(TracingDiagnosticListener.class, verifier)) { + // Do not inline this, it will break in Mockito's stubber backend. + var fileObjects = Set.of(somePathFileObject(someBinaryName())); + when(fileManager.list(any(), any(), any(), anyBoolean())) + .thenReturn(fileObjects); + + // When + doCompile(null); + + // Then + verify(javaCompiler).getTask( + any(), + any(), + same(listenerCls.constructed().iterator().next()), + any(), + any(), + any() + ); + } + } + + @DisplayName("Diagnostics get placed in the compilation result") + @Test + @SuppressWarnings("rawtypes") + void correctlyConfiguredDiagnosticListenerIsUsedForCompilation() throws IOException { + // Given + var diagnostics = List.of( + someTraceDiagnostic(), + someTraceDiagnostic(), + someTraceDiagnostic(), + someTraceDiagnostic() + ); + + MockInitializer configurer = + (mock, ctx) -> when(mock.getDiagnostics()).thenReturn(diagnostics); + + try (var ignored = mockConstruction(TracingDiagnosticListener.class, configurer)) { + // Do not inline this, it will break in Mockito's stubber backend. + var fileObjects = Set.of(somePathFileObject(someBinaryName())); + when(fileManager.list(any(), any(), any(), anyBoolean())) + .thenReturn(fileObjects); + + // When + var result = doCompile(null); + + // Then + assertThat(result.getDiagnostics()) + .isEqualTo(diagnostics); + } + } + + @DisplayName("The file manager is passed to the compiler task") + @Test + void theFileManagerIsPassedToTheCompilationTask() throws IOException { + // Given + // Do not inline this, it will break in Mockito's stubber backend. + var fileObjects = Set.of(somePathFileObject(someBinaryName())); + when(fileManager.list(any(), any(), any(), anyBoolean())) + .thenReturn(fileObjects); + + // When + doCompile(null); + + // Then + verify(javaCompiler).getTask(any(), same(fileManager), any(), any(), any(), any()); + } + + @DisplayName("The file manager is included in the compilation result") + @Test + void theFileManagerIsIncludedInTheCompilationResult() throws IOException { + // Given + // Do not inline this, it will break in Mockito's stubber backend. + var fileObjects = Set.of(somePathFileObject(someBinaryName())); + when(fileManager.list(any(), any(), any(), anyBoolean())) + .thenReturn(fileObjects); + + // When + var result = doCompile(null); + + // Then + assertThat(result.getFileManager()) + .isSameAs(fileManager); + } + + @DisplayName("Flags are passed to the compilation task") + @Test + void flagsArePassedToTheCompilationTask() throws IOException { + // Given + // Do not inline this, it will break in Mockito's stubber backend. + var fileObjects = Set.of(somePathFileObject(someBinaryName())); + when(fileManager.list(any(), any(), any(), anyBoolean())) + .thenReturn(fileObjects); + + // When + doCompile(null); + + // Then + verify(javaCompiler).getTask(any(), any(), any(), same(flags), any(), any()); + } + + @DisplayName("Annotation processors are not registered if processors list is empty") + @Test + void annotationProcessorsAreNotRegisteredIfProcessorsListIsEmpty() throws IOException { + // Given + when(jctCompiler.getAnnotationProcessors()).thenReturn(List.of()); + + var task = mock(CompilationTask.class); + when(javaCompiler.getTask(any(), any(), any(), any(), any(), any())) + .thenReturn(task); + + // Do not inline this, it will break in Mockito's stubber backend. + var fileObjects = Set.of(somePathFileObject(someBinaryName())); + when(fileManager.list(any(), any(), any(), anyBoolean())) + .thenReturn(fileObjects); + + // When + doCompile(null); + + // Then + verify(task, never()).setProcessors(any()); + } + + @DisplayName("Annotation processors are registered if processors list is not empty") + @Test + void annotationProcessorsAreRegisteredIfProcessorsListIsNotEmpty() throws IOException { + // Given + var processors = List.of( + mock(Processor.class), + mock(Processor.class), + mock(Processor.class), + mock(Processor.class) + ); + when(jctCompiler.getAnnotationProcessors()).thenReturn(processors); + + var task = mock(CompilationTask.class); + when(javaCompiler.getTask(any(), any(), any(), any(), any(), any())) + .thenReturn(task); + + // Do not inline this, it will break in Mockito's stubber backend. + var fileObjects = Set.of(somePathFileObject(someBinaryName())); + when(fileManager.list(any(), any(), any(), anyBoolean())) + .thenReturn(fileObjects); + + // When + doCompile(null); + + // Then + verify(task).setProcessors(processors); + } + + @DisplayName("The locale is set on the compiler task") + @Test + void theLocaleIsSetOnTheCompilerTask() throws IOException { + // Given + var locale = someLocale(); + when(jctCompiler.getLocale()).thenReturn(locale); + + var task = mock(CompilationTask.class); + when(javaCompiler.getTask(any(), any(), any(), any(), any(), any())) + .thenReturn(task); + + // Do not inline this, it will break in Mockito's stubber backend. + var fileObjects = Set.of(somePathFileObject(someBinaryName())); + when(fileManager.list(any(), any(), any(), anyBoolean())) + .thenReturn(fileObjects); + + // When + doCompile(null); + + // Then + verify(task).setLocale(locale); + } + + @DisplayName("The compilation is invoked and the outcome is placed in the compilation result") + @ValueSource(booleans = {true, false}) + @ParameterizedTest(name = "when CompilationTask.call() returns {0}") + void theCompilationIsInvokedAndTheOutcomeIsPlacedInTheCompilationResult(boolean success) + throws IOException { + // Given + var task = mock(CompilationTask.class); + var order = inOrder(task); + when(javaCompiler.getTask(any(), any(), any(), any(), any(), any())) + .thenReturn(task); + when(task.call()) + .thenReturn(success); + + when(jctCompiler.getAnnotationProcessors()) + .thenReturn(mock()); + + // Do not inline this, it will break in Mockito's stubber backend. + var fileObjects = Set.of(somePathFileObject(someBinaryName())); + when(fileManager.list(any(), any(), any(), anyBoolean())) + .thenReturn(fileObjects); + + // When + var result = doCompile(null); + + // Then + assertThat(result.isSuccessful()) + .isEqualTo(success); + + // Verify that we call the .call() method as the last thing we call on the object. + order.verify(task).setProcessors(any()); + order.verify(task).setLocale(any()); + order.verify(task).call(); + order.verifyNoMoreInteractions(); + } + + @DisplayName("Exceptions during compilation get wrapped and rethrown") + @Test + void exceptionsDuringCompilationGetWrappedAndRethrown() throws IOException { + // Given + var task = mock(CompilationTask.class); + var cause = new IOException("Something is messed up"); + + when(javaCompiler.getTask(any(), any(), any(), any(), any(), any())) + .thenReturn(task); + when(task.call()) + .thenThrow(cause); + + // Do not inline this, it will break in Mockito's stubber backend. + var fileObjects = Set.of(somePathFileObject(someBinaryName())); + when(fileManager.list(any(), any(), any(), anyBoolean())) + .thenReturn(fileObjects); + + // Then + assertThatThrownBy(() -> doCompile(null)) + .isInstanceOf(JctCompilerException.class) + .hasMessage("Failed to perform compilation, an unexpected exception was raised") + .hasCause(cause); + } + + @DisplayName("Compilers returning null outcomes will be raised as an exception") + @Test + void compilersReturningNullOutcomesWillBeRaisedAsAnException() throws IOException { + // Given + var task = mock(CompilationTask.class); + var name = someText(); + + when(jctCompiler.getName()) + .thenReturn(name); + + when(javaCompiler.getTask(any(), any(), any(), any(), any(), any())) + .thenReturn(task); + when(task.call()) + .thenReturn(null); + + // Do not inline this, it will break in Mockito's stubber backend. + var fileObjects = Set.of(somePathFileObject(someBinaryName())); + when(fileManager.list(any(), any(), any(), anyBoolean())) + .thenReturn(fileObjects); + + // Then + assertThatThrownBy(() -> doCompile(null)) + .isInstanceOf(JctCompilerException.class) + .hasMessage("Failed to perform compilation, an unexpected exception was raised") + .cause() + .hasMessage("Compiler %s task .call() method returned null unexpectedly!", name) + .isInstanceOf(NullPointerException.class); + } + + @DisplayName("The compilation result holds the failOnWarnings flag from the compiler") + @ValueSource(booleans = {true, false}) + @ParameterizedTest(name = "for compiler.isFailOnWarnings() = {0}") + void theCompilationResultHoldsTheFailOnWarningsFlagFromTheCompiler(boolean failOnWarnings) + throws IOException { + // Given + when(jctCompiler.isFailOnWarnings()) + .thenReturn(failOnWarnings); + + // Do not inline this, it will break in Mockito's stubber backend. + var fileObjects = Set.of(somePathFileObject(someBinaryName())); + when(fileManager.list(any(), any(), any(), anyBoolean())) + .thenReturn(fileObjects); + + // When + var result = doCompile(null); + + // Then + assertThat(result.isFailOnWarnings()) + .isEqualTo(failOnWarnings); + } + + static JavaFileObject somePathFileObject(String binaryName) { + var pathFileObject = mock(PathFileObject.class, withSettings().strictness(Strictness.LENIENT)); + when(pathFileObject.getBinaryName()).thenReturn(binaryName); + return pathFileObject; + } + + static Locale someLocale() { + return oneOf( + Locale.ENGLISH, + Locale.CHINESE, + Locale.JAPAN, + Locale.CANADA, + Locale.GERMANY + ); + } +} diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/diagnostics/TeeWriterTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/diagnostics/TeeWriterTest.java index 498baa6ba..bf5588697 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/diagnostics/TeeWriterTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/diagnostics/TeeWriterTest.java @@ -16,6 +16,7 @@ package io.github.ascopes.jct.tests.unit.diagnostics; import static io.github.ascopes.jct.tests.helpers.Fixtures.someText; +import static java.nio.charset.Charset.defaultCharset; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -25,9 +26,11 @@ import static org.mockito.Mockito.times; import io.github.ascopes.jct.diagnostics.TeeWriter; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; +import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -47,7 +50,7 @@ void nullWritersAreDisallowed() { .isInstanceOf(NullPointerException.class); } - @DisplayName("write() fails if the writer is closed") + @DisplayName(".write() fails if the writer is closed") @Test void writeFailsIfWriterIsClosed() throws IOException { // Given @@ -67,7 +70,7 @@ void writeFailsIfWriterIsClosed() throws IOException { .isEmpty(); } - @DisplayName("write() delegates to the writer") + @DisplayName(".write() delegates to the writer") @Test void writeDelegatesToTheWriter() throws IOException { // Given @@ -82,7 +85,7 @@ void writeDelegatesToTheWriter() throws IOException { assertThat(writer).hasToString(text); } - @DisplayName("flush() fails if the writer is closed") + @DisplayName(".flush() fails if the writer is closed") @Test void flushFailsIfTheWriterIsClosed() throws IOException { // Given @@ -99,7 +102,7 @@ void flushFailsIfTheWriterIsClosed() throws IOException { then(writer).shouldHaveNoInteractions(); } - @DisplayName("flush() delegates to the writer") + @DisplayName(".flush() delegates to the writer") @Test void flushDelegatesToTheWriter() throws IOException { // Given @@ -114,7 +117,7 @@ void flushDelegatesToTheWriter() throws IOException { then(writer).shouldHaveNoMoreInteractions(); } - @DisplayName("close() delegates to the writer") + @DisplayName(".close() delegates to the writer") @SuppressWarnings("EmptyTryBlock") @Test void closeDelegatesToTheWriter() throws IOException { @@ -131,7 +134,7 @@ void closeDelegatesToTheWriter() throws IOException { then(writer).shouldHaveNoMoreInteractions(); } - @DisplayName("close() is idempotent") + @DisplayName(".close() is idempotent") @Test void closeIsIdempotent() throws IOException { var writer = mock(Writer.class); @@ -145,7 +148,23 @@ void closeIsIdempotent() throws IOException { then(writer).should(times(1)).close(); } - @DisplayName("toString() should return the buffer content") + @DisplayName(".getContent() should return the buffer content") + @Test + void getContentShouldReturnTheBufferContent() throws IOException { + // Given + var writer = mock(Writer.class); + var tee = new TeeWriter(writer); + + // When + tee.write("Hello, "); + tee.write("World"); + tee.write("!"); + + // Then + assertThat(tee.getContent()).isEqualTo("Hello, World!"); + } + + @DisplayName(".toString() should return the buffer content") @Test void toStringShouldReturnTheBufferContent() throws IOException { // Given @@ -160,4 +179,44 @@ void toStringShouldReturnTheBufferContent() throws IOException { // Then assertThat(tee).hasToString("Hello, World!"); } + + @DisplayName(".wrapOutputStream(null, Charset) throws a NullPointerException") + @Test + void wrapOutputStreamWithNullOutputStreamThrowsNullPointerException() { + // Then + assertThatThrownBy(() -> TeeWriter.wrapOutputStream(null, defaultCharset())) + .isInstanceOf(NullPointerException.class) + .hasMessage("outputStream"); + } + + @DisplayName(".wrapOutputStream(OutputStream, null) throws a NullPointerException") + @Test + void wrapOutputStreamWithNullCharsetThrowsNullPointerException() { + // Then + var os = new ByteArrayOutputStream(); + assertThatThrownBy(() -> TeeWriter.wrapOutputStream(os, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("charset"); + } + + @DisplayName(".wrapOutputStream(OutputStream, Charset) creates a writer to the output stream") + @Test + void wrapOutputStreamWritesToTheOutputStream() throws IOException { + // Given + var outputStream = new ByteArrayOutputStream(); + var charset = StandardCharsets.UTF_8; + var teeWriter = TeeWriter.wrapOutputStream(outputStream, charset); + + // When + teeWriter.write("Hello, World!"); + teeWriter.write("\n"); + teeWriter.write("blah blah blah €${¾½€đ¢æßŧ"); + teeWriter.flush(); + + // Then + assertThat(teeWriter.toString()) + .isEqualTo("Hello, World!\nblah blah blah €${¾½€đ¢æßŧ"); + assertThat(outputStream.toString(charset)) + .isEqualTo("Hello, World!\nblah blah blah €${¾½€đ¢æßŧ"); + } } diff --git a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/PathFileObjectTest.java b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/PathFileObjectTest.java index 1bd02c7a8..7f5e6d84e 100644 --- a/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/PathFileObjectTest.java +++ b/java-compiler-testing/src/test/java/io/github/ascopes/jct/tests/unit/filemanagers/PathFileObjectTest.java @@ -27,6 +27,7 @@ import static org.mockito.Mockito.mock; import io.github.ascopes.jct.filemanagers.PathFileObject; +import io.github.ascopes.jct.utils.FileUtils; import java.io.IOException; import java.io.StringWriter; import java.nio.charset.MalformedInputException; @@ -224,6 +225,19 @@ void getAccessLevelReturnsNull() { assertThat(fileObject.getAccessLevel()).isNull(); } + @DisplayName(".getBinaryName() gets the binary name of the relative path") + @Test + void getBinaryNameGetsTheBinaryNameOfTheRelativePath() { + // Given + var relativePath = someRelativePath().resolve("Cat.java"); + var expectedBinaryName = FileUtils.pathToBinaryName(relativePath); + var fileObject = new PathFileObject(someLocation(), someAbsolutePath(), relativePath); + + // Then + assertThat(fileObject.getBinaryName()) + .isEqualTo(expectedBinaryName); + } + @DisplayName(".getCharContent(...) returns the character content") @ValueSource(booleans = {true, false}) @ParameterizedTest(name = "for ignoreEncodingErrors={0}") diff --git a/java-compiler-testing/src/test/resources/integration/specificclasses/Fibonacci.java b/java-compiler-testing/src/test/resources/integration/specificclasses/Fibonacci.java new file mode 100644 index 000000000..1c6944782 --- /dev/null +++ b/java-compiler-testing/src/test/resources/integration/specificclasses/Fibonacci.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2022 - 2023, the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import static java.math.BigInteger.ONE; +import static java.math.BigInteger.ZERO; + +import java.math.BigInteger; + +/** + * Command line app to calculate the nth number of the fibonacci sequence. + */ +public class Fibonacci { + public static void main(String[] args) { + if (args.length != 1) { + System.out.println("USAGE: java Fibonacci "); + System.out.println(" Fibonacci number to generate"); + } + + BigInteger n = new BigInteger(args[0], 10); + BigInteger a = ZERO; + BigInteger b = ONE; + + for (BigInteger i = ZERO; i.compareTo(n) < 0; i = i.add(ONE)) { + BigInteger oldB = b; + b = a.add(b); + a = oldB; + } + + System.out.println(a); + } +} diff --git a/java-compiler-testing/src/test/resources/integration/specificclasses/HelloWorld.java b/java-compiler-testing/src/test/resources/integration/specificclasses/HelloWorld.java new file mode 100644 index 000000000..23753feb6 --- /dev/null +++ b/java-compiler-testing/src/test/resources/integration/specificclasses/HelloWorld.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2022 - 2023, the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Command line app that says hello to me. + */ +public class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello, World!"); + } +} diff --git a/java-compiler-testing/src/test/resources/integration/specificclasses/Sum.java b/java-compiler-testing/src/test/resources/integration/specificclasses/Sum.java new file mode 100644 index 000000000..613fc09e7 --- /dev/null +++ b/java-compiler-testing/src/test/resources/integration/specificclasses/Sum.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2022 - 2023, the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Command line app that sums together given integers. + */ +public class Sum { + public static void main(String[] args) { + long sum = 0; + for (String arg : args) { + sum += Integer.parseInt(arg); + } + System.out.println(sum); + } +} diff --git a/pom.xml b/pom.xml index 8dfdc650b..3c372ef89 100644 --- a/pom.xml +++ b/pom.xml @@ -143,7 +143,7 @@ --> -Dorg.slf4j.simpleLogger.log=INFO - -Dorg.slf4j.simpleLogger.log.io.github.ascopes.jct=DEBUG + -Dorg.slf4j.simpleLogger.log.io.github.ascopes.jct=TRACE -Xshare:off -XX:+TieredCompilation -XX:TieredStopAtLevel=1