blob: 744f7967629713a5b178e12328a93456c601d1b1 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.maven.plugin.compiler;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.maven.api.PathScope;
import org.apache.maven.api.Project;
import org.apache.maven.api.Session;
import org.apache.maven.api.di.Inject;
import org.apache.maven.api.di.Provides;
import org.apache.maven.api.di.Singleton;
import org.apache.maven.api.model.Build;
import org.apache.maven.api.model.Model;
import org.apache.maven.api.plugin.Log;
import org.apache.maven.api.plugin.testing.Basedir;
import org.apache.maven.api.plugin.testing.InjectMojo;
import org.apache.maven.api.plugin.testing.MojoExtension;
import org.apache.maven.api.plugin.testing.MojoParameter;
import org.apache.maven.api.plugin.testing.MojoTest;
import org.apache.maven.api.plugin.testing.stubs.ProducedArtifactStub;
import org.apache.maven.api.plugin.testing.stubs.ProjectStub;
import org.apache.maven.api.plugin.testing.stubs.SessionMock;
import org.apache.maven.api.services.ArtifactManager;
import org.apache.maven.api.services.MessageBuilderFactory;
import org.apache.maven.api.services.ToolchainManager;
import org.apache.maven.impl.DefaultMessageBuilderFactory;
import org.apache.maven.impl.InternalSession;
import org.apache.maven.plugin.compiler.stubs.CompilerStub;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.startsWith;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@MojoTest
public class CompilerMojoTestCase {
private static final String LOCAL_REPO = "/target/local-repo";
@Inject
private Session session;
/**
* Verifies that the {@value CompilerStub#OUTPUT_FILE} file exists, then deletes it.
* The deletion is necessary for preventing an {@link IndexOutOfBoundsException} in
* {@code maven-dependency-plugin} version 3.8.1, because the output file is empty.
*
* @param mojo the tested mojo
*/
private static void assertCompilerStubOutputFileExists(AbstractCompilerMojo mojo) {
try {
Files.delete(assertOutputFileExists(mojo, CompilerStub.OUTPUT_FILE));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
/**
* Verifies that the given output file exists.
*
* @param mojo the tested mojo
* @param first the first path element
* @param more the other path elements, if any
* @return the file
*/
private static Path assertOutputFileExists(AbstractCompilerMojo mojo, String first, String... more) {
Path file = mojo.getOutputDirectory().resolve(Path.of(first, more));
assertTrue(Files.isRegularFile(file), () -> "File not found: " + file);
return file;
}
/**
* Verifies that the given output file does not exist.
*
* @param mojo the tested mojo
* @param first the first path element
* @param more the other path elements, if any
*/
private static void assertOutputFileDoesNotExist(AbstractCompilerMojo mojo, String first, String... more) {
Path file = mojo.getOutputDirectory().resolve(Path.of(first, more));
assertFalse(Files.exists(file), () -> "File should not exist: " + file);
}
/**
* Tests the ability of the plugin to compile a basic file.
* This test does not declare a Java release version. Therefore, a warning should be emitted.
*/
@Test
@Basedir("${basedir}/target/test-classes/unit/compiler-basic-test")
public void testCompilerBasic(
@InjectMojo(goal = "compile", pom = "plugin-config.xml") CompilerMojo compileMojo,
@InjectMojo(goal = "testCompile", pom = "plugin-config.xml")
@MojoParameter(name = "compileSourceRoots", value = "${project.basedir}/src/test/java")
TestCompilerMojo testCompileMojo) {
Log log = mock(Log.class);
compileMojo.logger = log;
compileMojo.execute();
verify(log).warn(startsWith("No explicit value set for --release or --target."));
assertOutputFileExists(compileMojo, "foo", "TestCompile0.class");
assertTrue(
session.getArtifactPath(compileMojo.projectArtifact).isPresent(),
"MCOMPILER-94: artifact file should only be null if there is nothing to compile");
testCompileMojo.execute();
assertOutputFileExists(testCompileMojo, "foo", "TestCompile0Test.class");
assertOutputFileDoesNotExist(compileMojo, "foo", "TestCompile0Test.class");
}
/**
* A project with a source and target version specified.
* No warning should be logged.
*/
@Test
@Basedir("${basedir}/target/test-classes/unit/compiler-basic-sourcetarget")
public void testCompilerBasicSourceTarget(
@InjectMojo(goal = "compile", pom = "plugin-config.xml") CompilerMojo compileMojo) {
Log log = mock(Log.class);
compileMojo.logger = log;
compileMojo.execute();
verify(log, never()).warn(startsWith("No explicit value set for --release or --target."));
}
/**
* Tests the ability of the plugin to respond to empty source.
*/
@Test
@Basedir("${basedir}/target/test-classes/unit/compiler-empty-source-test")
public void testCompilerEmptySource(
@InjectMojo(goal = "compile", pom = "plugin-config.xml") CompilerMojo compileMojo,
@InjectMojo(goal = "testCompile", pom = "plugin-config.xml")
@MojoParameter(name = "compileSourceRoots", value = "${basedir}/src/test/java")
TestCompilerMojo testCompileMojo) {
compileMojo.execute();
assertFalse(Files.exists(compileMojo.getOutputDirectory()));
assertNull(
session.getArtifactPath(compileMojo.projectArtifact).orElse(null),
"MCOMPILER-94: artifact file should be null if there is nothing to compile");
testCompileMojo.execute();
assertFalse(Files.exists(testCompileMojo.getOutputDirectory()));
}
/**
* Tests the ability of the plugin to respond to includes and excludes correctly.
*/
@Test
@Basedir("${basedir}/target/test-classes/unit/compiler-includes-excludes-test")
public void testCompilerIncludesExcludes(
@InjectMojo(goal = "compile", pom = "plugin-config.xml") CompilerMojo compileMojo,
@InjectMojo(goal = "testCompile", pom = "plugin-config.xml")
@MojoParameter(name = "compileSourceRoots", value = "${project.basedir}/src/test/java")
TestCompilerMojo testCompileMojo) {
compileMojo.execute();
assertOutputFileDoesNotExist(compileMojo, "foo", "TestCompile2.class");
assertOutputFileDoesNotExist(compileMojo, "foo", "TestCompile3.class");
assertOutputFileExists(compileMojo, "foo", "TestCompile4.class");
testCompileMojo.execute();
assertOutputFileDoesNotExist(testCompileMojo, "foo", "TestCompile2TestCase.class");
assertOutputFileDoesNotExist(testCompileMojo, "foo", "TestCompile3TestCase.class");
assertOutputFileExists(testCompileMojo, "foo", "TestCompile4TestCase.class");
assertOutputFileDoesNotExist(compileMojo, "foo", "TestCompile4TestCase.class");
}
/**
* Tests the ability of the plugin to fork and successfully compile.
*/
@Test
@Basedir("${basedir}/target/test-classes/unit/compiler-fork-test")
public void testCompilerFork(
@InjectMojo(goal = "compile", pom = "plugin-config.xml") CompilerMojo compileMojo,
@InjectMojo(goal = "testCompile", pom = "plugin-config.xml")
@MojoParameter(name = "compileSourceRoots", value = "${project.basedir}/src/test/java")
TestCompilerMojo testCompileMojo) {
// JAVA_HOME doesn't have to be on the PATH.
String javaHome = System.getenv("JAVA_HOME");
if (javaHome != null) {
String command = new File(javaHome, "bin/javac").getPath();
compileMojo.executable = command;
testCompileMojo.executable = command;
}
compileMojo.execute();
assertOutputFileExists(compileMojo, "foo", "TestCompile1.class");
testCompileMojo.execute();
assertOutputFileExists(testCompileMojo, "foo", "TestCompile1TestCase.class");
assertOutputFileDoesNotExist(compileMojo, "foo", "TestCompile1TestCase.class");
}
/**
* Tests the use of a custom compiler.
* The dummy compiler used in this test generates only one file, despite having more than one source.
*/
@Test
@Basedir("${basedir}/target/test-classes/unit/compiler-one-output-file-test")
public void testOneOutputFileForAllInput(
@InjectMojo(goal = "compile", pom = "plugin-config.xml") CompilerMojo compileMojo,
@InjectMojo(goal = "testCompile", pom = "plugin-config.xml")
@MojoParameter(name = "compileSourceRoots", value = "${project.basedir}/src/test/java")
TestCompilerMojo testCompileMojo) {
assertEquals(CompilerStub.COMPILER_ID, compileMojo.compilerId);
compileMojo.execute();
assertCompilerStubOutputFileExists(compileMojo);
assertEquals(CompilerStub.COMPILER_ID, testCompileMojo.compilerId);
testCompileMojo.execute();
assertCompilerStubOutputFileExists(testCompileMojo);
}
/**
* Verifies that the options in the {@code <compilerArgs>} elements are given to the compiler.
*/
@Test
@Basedir("${basedir}/target/test-classes/unit/compiler-args-test")
public void testCompilerArgs(@InjectMojo(goal = "compile", pom = "plugin-config.xml") CompilerMojo compileMojo) {
assertEquals(CompilerStub.COMPILER_ID, compileMojo.compilerId);
compileMojo.execute();
assertCompilerStubOutputFileExists(compileMojo);
assertArrayEquals(
new String[] {"key1=value1", "-Xlint", "-my&special:param-with+chars/not>allowed_in_XML_element_names"},
compileMojo.compilerArgs.toArray(String[]::new));
List<String> options = CompilerStub.getOptions();
assertArrayEquals(
new String[] {
"--module-version", // Added by the plugin
"1.0-SNAPSHOT",
"key1=value1", // Specified in <compilerArgs>
"-Xlint",
"-my&special:param-with+chars/not>allowed_in_XML_element_names",
"param", // Specified in <compilerArgument>
"value"
},
options.toArray(String[]::new));
}
/**
* Tests the {@code <implicit>} option when set to "none".
*/
@Test
@Basedir("${basedir}/target/test-classes/unit/compiler-implicit-test")
public void testImplicitFlagNone(
@InjectMojo(goal = "compile", pom = "plugin-config-none.xml") CompilerMojo compileMojo) {
assertEquals("none", compileMojo.implicit);
compileMojo.execute();
}
/**
* Tests the {@code <implicit>} option when not set.
*/
@Test
@Basedir("${basedir}/target/test-classes/unit/compiler-implicit-test")
public void testImplicitFlagNotSet(
@InjectMojo(goal = "compile", pom = "plugin-config-not-set.xml") CompilerMojo compileMojo) {
assertNull(compileMojo.implicit);
compileMojo.execute();
}
/**
* Tests the compilation of a project having a {@code module-info.java} file, together with its tests.
* The compilation of tests requires a {@code --patch-module} option, otherwise compilation will fail.
*
* <h4>Requirements on Windows</h4>
* Executing the tests on Windows requires the developer mode.
* This is enabled with {@literal Settings > Update & Security > For Developers}.
*/
@Test
@Basedir("${basedir}/target/test-classes/unit/compiler-modular-project")
public void testModularProject(
@InjectMojo(goal = "compile", pom = "plugin-config.xml") CompilerMojo compileMojo,
@InjectMojo(goal = "testCompile", pom = "plugin-config.xml")
@MojoParameter(name = "compileSourceRoots", value = "${project.basedir}/src/test/java")
TestCompilerMojo testCompileMojo) {
compileMojo.execute();
assertOutputFileExists(compileMojo, SourceDirectory.MODULE_INFO + SourceDirectory.CLASS_FILE_SUFFIX);
assertOutputFileExists(compileMojo, "foo", "TestModular.class");
testCompileMojo.execute();
assertOutputFileExists(testCompileMojo, "foo", "TestModularTestCase.class");
assertOutputFileDoesNotExist(compileMojo, "foo", "TestModularTestCase.class");
}
/**
* Tests a compilation task which is expected to fail.
*/
@Test
@Basedir("${basedir}/target/test-classes/unit/compiler-fail-test")
public void testCompileFailure(@InjectMojo(goal = "compile", pom = "plugin-config.xml") CompilerMojo compileMojo) {
assertThrows(CompilationFailureException.class, compileMojo::execute, "Should throw an exception");
assertOutputFileExists(compileMojo, "..", "javac.args"); // Command-line that user can execute.
}
/**
* Tests a compilation task which is expected to fail, but where test failure are ignored.
*/
@Test
@Basedir("${basedir}/target/test-classes/unit/compiler-failonerror-test")
public void testCompileFailOnError(
@InjectMojo(goal = "compile", pom = "plugin-config.xml") CompilerMojo compileMojo) {
try {
compileMojo.execute();
} catch (CompilationFailureException e) {
fail("The compilation error should have been consumed because failOnError = false");
}
assertOutputFileExists(compileMojo, "..", "javac.args"); // Command-line that user can execute.
}
/**
* Tests that setting {@code skipMain} to true skips compilation of the main Java source files,
* but that test Java source files are still compiled.
*/
@Test
@Basedir("${basedir}/target/test-classes/unit/compiler-skip-main")
public void testCompileSkipMain(
@InjectMojo(goal = "compile", pom = "plugin-config.xml") CompilerMojo compileMojo,
@InjectMojo(goal = "testCompile", pom = "plugin-config.xml")
@MojoParameter(name = "compileSourceRoots", value = "${project.basedir}/src/test/java")
TestCompilerMojo testCompileMojo) {
compileMojo.skipMain = true;
compileMojo.execute();
assertOutputFileDoesNotExist(compileMojo, "foo", "TestSkipMainCompile0.class");
testCompileMojo.execute();
assertOutputFileExists(testCompileMojo, "foo", "TestSkipMainCompile0Test.class");
assertOutputFileDoesNotExist(compileMojo, "foo", "TestSkipMainCompile0Test.class");
}
/**
* Tests that setting {@code skip} to true skips compilation of the test Java source files,
* but that main Java source files are still compiled.
*/
@Test
@Basedir("${basedir}/target/test-classes/unit/compiler-skip-test")
public void testCompileSkipTest(
@InjectMojo(goal = "compile", pom = "plugin-config.xml") CompilerMojo compileMojo,
@InjectMojo(goal = "testCompile", pom = "plugin-config.xml")
@MojoParameter(name = "compileSourceRoots", value = "${project.basedir}/src/test/java")
TestCompilerMojo testCompileMojo) {
compileMojo.execute();
assertOutputFileExists(compileMojo, "foo/TestSkipTestCompile0.class");
testCompileMojo.skip = true;
testCompileMojo.execute();
assertOutputFileDoesNotExist(testCompileMojo, "foo", "TestSkipTestCompile0Test.class");
assertOutputFileDoesNotExist(compileMojo, "foo", "TestSkipTestCompile0Test.class");
}
@Provides
@Singleton
@SuppressWarnings("unused")
private static InternalSession createSession() {
InternalSession session = SessionMock.getMockSession(MojoExtension.getBasedir() + LOCAL_REPO);
ToolchainManager toolchainManager = mock(ToolchainManager.class);
doReturn(toolchainManager).when(session).getService(ToolchainManager.class);
doAnswer(iom -> Instant.now().minus(200, ChronoUnit.MILLIS))
.when(session)
.getStartTime();
var junit = new ProducedArtifactStub("junit", "junit", null, "3.8.1", "jar");
MessageBuilderFactory messageBuilderFactory = new DefaultMessageBuilderFactory();
doReturn(messageBuilderFactory).when(session).getService(MessageBuilderFactory.class);
Map<String, String> props = new HashMap<>();
props.put("basedir", MojoExtension.getBasedir());
doReturn(props).when(session).getUserProperties();
List<Path> artifacts = new ArrayList<>();
try {
Path artifactFile;
String localRepository = System.getProperty("localRepository");
if (localRepository != null) {
artifactFile = Path.of(
localRepository,
"org",
"junit",
"jupiter",
"junit-jupiter-api",
"5.10.2",
"junit-jupiter-api-5.10.2.jar");
} else {
// for IDE
String junitURI = Test.class.getResource("Test.class").toURI().toString();
junitURI = junitURI.substring("jar:".length(), junitURI.indexOf('!'));
artifactFile = new File(URI.create(junitURI)).toPath();
}
ArtifactManager artifactManager = session.getService(ArtifactManager.class);
artifactManager.setPath(junit, artifactFile);
artifacts.add(artifactFile);
} catch (Exception e) {
throw new RuntimeException("Unable to setup junit jar path", e);
}
doAnswer(iom -> List.of()).when(session).resolveDependencies(any(), eq(PathScope.MAIN_COMPILE));
doAnswer(iom -> artifacts).when(session).resolveDependencies(any(), eq(PathScope.TEST_COMPILE));
return session;
}
@Provides
@Singleton
@SuppressWarnings("unused")
private static Project createProject() {
ProjectStub stub = new ProjectStub();
var artifact = new ProducedArtifactStub("myGroupId", "myArtifactId", null, "1.0-SNAPSHOT", "jar");
stub.setMainArtifact(artifact);
stub.setModel(Model.newBuilder()
.groupId(artifact.getGroupId())
.artifactId(artifact.getArtifactId())
.version(artifact.getVersion().toString())
.build(Build.newBuilder()
.directory(MojoExtension.getBasedir() + "/target")
.outputDirectory(MojoExtension.getBasedir() + "/target/classes")
.sourceDirectory(MojoExtension.getBasedir() + "/src/main/java")
.testOutputDirectory(MojoExtension.getBasedir() + "/target/test-classes")
.build())
.build());
stub.setBasedir(Path.of(MojoExtension.getBasedir()));
return stub;
}
}