blob: 1cdc33633fb3dbb67e6533348308f852d07d5a50 [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 javax.tools.DiagnosticListener;
import javax.tools.JavaFileObject;
import javax.tools.OptionChecker;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.maven.api.PathScope;
import org.apache.maven.api.annotations.Nonnull;
import org.apache.maven.api.annotations.Nullable;
import org.apache.maven.api.plugin.MojoException;
import org.apache.maven.api.plugin.annotations.Mojo;
import org.apache.maven.api.plugin.annotations.Parameter;
import org.apache.maven.api.services.MessageBuilder;
import static org.apache.maven.plugin.compiler.SourceDirectory.CLASS_FILE_SUFFIX;
import static org.apache.maven.plugin.compiler.SourceDirectory.JAVA_FILE_SUFFIX;
import static org.apache.maven.plugin.compiler.SourceDirectory.MODULE_INFO;
/**
* Compiles application test sources.
* Each instance shall be used only once, then discarded.
*
* @author <a href="mailto:jason@maven.org">Jason van Zyl</a>
* @author Martin Desruisseaux
* @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html">javac Command</a>
* @since 2.0
*/
@Mojo(name = "testCompile", defaultPhase = "test-compile")
public class TestCompilerMojo extends AbstractCompilerMojo {
/**
* Whether to bypass compilation of test sources.
* Its use is not recommended, but quite convenient on occasion.
*
* @see CompilerMojo#skipMain
*/
@Parameter(property = "maven.test.skip")
protected boolean skip;
/**
* Specify where to place generated source files created by annotation processing.
*
* @see CompilerMojo#generatedSourcesDirectory
* @since 2.2
*/
@Parameter(defaultValue = "${project.build.directory}/generated-test-sources/test-annotations")
protected Path generatedTestSourcesDirectory;
/**
* A set of inclusion filters for the compiler.
*
* @see CompilerMojo#includes
*/
@Parameter
protected Set<String> testIncludes;
/**
* A set of exclusion filters for the compiler.
*
* @see CompilerMojo#excludes
*/
@Parameter
protected Set<String> testExcludes;
/**
* A set of exclusion filters for the incremental calculation.
* Updated files, if excluded by this filter, will not cause the project to be rebuilt.
*
* @see CompilerMojo#incrementalExcludes
* @since 3.11
*/
@Parameter
protected Set<String> testIncrementalExcludes;
/**
* The {@code --source} argument for the test Java compiler.
*
* @see CompilerMojo#source
* @since 2.1
*/
@Parameter(property = "maven.compiler.testSource")
protected String testSource;
/**
* The {@code --target} argument for the test Java compiler.
*
* @see CompilerMojo#target
* @since 2.1
*/
@Parameter(property = "maven.compiler.testTarget")
protected String testTarget;
/**
* the {@code --release} argument for the test Java compiler
*
* @see CompilerMojo#release
* @since 3.6
*/
@Parameter(property = "maven.compiler.testRelease")
protected String testRelease;
/**
* The arguments to be passed to the test compiler.
* If this parameter is specified, it replaces {@link #compilerArgs}.
* Otherwise, the {@code compilerArgs} parameter is used.
*
* @see CompilerMojo#compilerArgs
* @since 4.0.0
*/
@Parameter
protected List<String> testCompilerArgs;
/**
* The arguments to be passed to test compiler.
*
* @deprecated Replaced by {@link #testCompilerArgs} for consistency with the main phase.
*
* @since 2.1
*/
@Parameter
@Deprecated(since = "4.0.0")
protected Map<String, String> testCompilerArguments;
/**
* The single argument string to be passed to the test compiler.
* If this parameter is specified, it replaces {@link #compilerArgument}.
* Otherwise, the {@code compilerArgument} parameter is used.
*
* @deprecated Use {@link #testCompilerArgs} instead.
*
* @see CompilerMojo#compilerArgument
* @since 2.1
*/
@Parameter
@Deprecated(since = "4.0.0")
protected String testCompilerArgument;
/**
* The directory where compiled test classes go.
* This parameter should only be modified in special cases.
* See the {@link CompilerMojo#outputDirectory} for more information.
*
* @see CompilerMojo#outputDirectory
* @see #getOutputDirectory()
*/
@Parameter(defaultValue = "${project.build.testOutputDirectory}", required = true)
protected Path outputDirectory;
/**
* The output directory of the main classes.
* This directory will be added to the class-path or module-path.
* Its value should be the same as {@link CompilerMojo#outputDirectory}.
*
* @see CompilerMojo#outputDirectory
*/
@Parameter(defaultValue = "${project.build.outputDirectory}", required = true, readonly = true)
protected Path mainOutputDirectory;
/**
* Whether to place the main classes on the module path when {@code module-info} is present.
* When {@code false}, always places the main classes on the class path.
* Dependencies are also placed on the class-path, unless their type is {@code module-jar}.
*
* @since 3.11
*
* @deprecated Use {@code "claspath-jar"} dependency type instead, and avoid {@code module-info.java} in tests.
*/
@Deprecated(since = "4.0.0")
@Parameter(defaultValue = "true")
protected boolean useModulePath = true;
/**
* Whether a {@code module-info.java} file is defined in the test sources.
* In such case, it has precedence over the {@code module-info.java} in main sources.
* This is defined for compatibility with Maven 3, but not recommended.
*
* <p>This field exists in this class only for transferring this information
* to {@link ToolExecutorForTest#hasTestModuleInfo}, which is the class that
* needs this information.</p>
*
* @deprecated Avoid {@code module-info.java} in tests.
*/
@Deprecated(since = "4.0.0")
transient boolean hasTestModuleInfo;
/**
* Whether a {@code module-info.java} file is defined in the main sources.
*/
private transient boolean hasMainModuleInfo;
/**
* The file where to dump the command-line when debug is activated or when the compilation failed.
* For example, if the value is {@code "javac-test"}, then the Java compiler can be launched
* from the command-line by typing {@code javac @target/javac-test.args}.
* The debug file will contain the compiler options together with the list of source files to compile.
*
* <p>By default, this debug file is written only if the compilation of test code failed.
* The writing of the debug files can be forced by setting the {@link #verbose} flag to {@code true}
* or by specifying the {@code --verbose} option to Maven on the command-line.</p>
*
* @see CompilerMojo#debugFileName
* @since 3.10.0
*/
@Parameter(defaultValue = "javac-test.args")
protected String debugFileName;
/**
* Creates a new compiler <abbr>MOJO</abbr> for the tests.
*/
public TestCompilerMojo() {
super(PathScope.TEST_COMPILE);
}
/**
* Runs the Java compiler on the test source code.
* If {@link #skip} is {@code true}, then this method logs a message and does nothing else.
* Otherwise, this method executes the steps described in the method of the parent class.
*
* @throws MojoException if the compiler cannot be run.
*/
@Override
public void execute() throws MojoException {
if (skip) {
logger.info("Not compiling test sources");
return;
}
super.execute();
}
/**
* Parses the parameters declared in the <abbr>MOJO</abbr>.
*
* @param compiler the tools to use for verifying the validity of options
* @return the options after validation
*/
@Override
@SuppressWarnings("deprecation")
public Options parseParameters(final OptionChecker compiler) {
Options configuration = super.parseParameters(compiler);
configuration.addUnchecked(isAbsent(testCompilerArgs) ? compilerArgs : testCompilerArgs);
if (testCompilerArguments != null) {
for (Map.Entry<String, String> entry : testCompilerArguments.entrySet()) {
configuration.addUnchecked(List.of(entry.getKey(), entry.getValue()));
}
}
configuration.addUnchecked(testCompilerArgument == null ? compilerArgument : testCompilerArgument);
return configuration;
}
/**
* {@return the path where to place generated source files created by annotation processing on the test classes}
*/
@Nullable
@Override
protected Path getGeneratedSourcesDirectory() {
return generatedTestSourcesDirectory;
}
/**
* {@return the inclusion filters for the compiler, or an empty set for all Java source files}
*/
@Override
protected Set<String> getIncludes() {
return (testIncludes != null) ? testIncludes : Set.of();
}
/**
* {@return the exclusion filters for the compiler, or an empty set if none}
*/
@Override
protected Set<String> getExcludes() {
return (testExcludes != null) ? testExcludes : Set.of();
}
/**
* {@return the exclusion filters for the incremental calculation, or an empty set if none}
*/
@Override
protected Set<String> getIncrementalExcludes() {
return (testIncrementalExcludes != null) ? testIncrementalExcludes : Set.of();
}
/**
* If a different source version has been specified for the tests, returns that version.
* Otherwise returns the same source version as the main code.
*
* @return the {@code --source} argument for the Java compiler
*/
@Nullable
@Override
protected String getSource() {
return testSource == null ? source : testSource;
}
/**
* If a different target version has been specified for the tests, returns that version.
* Otherwise returns the same target version as the main code.
*
* @return the {@code --target} argument for the Java compiler
*/
@Nullable
@Override
protected String getTarget() {
return testTarget == null ? target : testTarget;
}
/**
* If a different release version has been specified for the tests, returns that version.
* Otherwise returns the same release version as the main code.
*
* @return the {@code --release} argument for the Java compiler
*/
@Nullable
@Override
protected String getRelease() {
return testRelease == null ? release : testRelease;
}
/**
* {@return the destination directory for test class files}
*/
@Nonnull
@Override
protected Path getOutputDirectory() {
return outputDirectory;
}
/**
* {@return the file where to dump the command-line when debug is activated or when the compilation failed}
*
* @see #debugFileName
*/
@Nullable
@Override
protected String getDebugFileName() {
return debugFileName;
}
/**
* {@return the module name found in the package hierarchy of given sources}
* We have to parse the source instead of the {@code module-info.class} file
* because the classes may not have been compiled yet. This is not reliable,
* but the use of package hierarchy for modular project should be avoided in
* Maven 4.
*
* @deprecated Declare modules in {@code <source>} elements instead.
*/
@Deprecated(since = "4.0.0")
final String moduleNameFromPackageHierarchy(List<SourceDirectory> compileSourceRoots) throws IOException {
for (SourceDirectory directory : compileSourceRoots) {
if (directory.moduleName == null) {
String name = parseModuleInfoName(directory.getModuleInfo().orElse(null));
if (name != null) {
return name;
}
}
}
return null;
}
/**
* {@return whether the project has at least one {@code module-info.class} file}
* The {@code module-info.class} should be located in the main source code.
* However, this method checks also in the test source code for compatibility with Maven 3,
* but this practice is deprecated.
*
* @param roots root directories of the source files of the test classes to compile
* @throws IOException if this method needed to read a module descriptor and failed
*/
@Override
final boolean hasModuleDeclaration(final List<SourceDirectory> roots) throws IOException {
for (SourceDirectory root : roots) {
hasMainModuleInfo |= root.moduleName != null;
hasTestModuleInfo |= root.getModuleInfo().isPresent();
if (hasMainModuleInfo & hasTestModuleInfo) {
break;
}
}
if (hasTestModuleInfo) {
MessageBuilder message = messageBuilderFactory.builder();
message.a("Overwriting the ")
.warning(MODULE_INFO + JAVA_FILE_SUFFIX)
.a(" file in the test directory is deprecated. Use ")
.info(ModuleInfoPatch.FILENAME)
.a(" instead.");
logger.warn(message.toString());
if (SUPPORT_LEGACY) {
return useModulePath;
}
}
return useModulePath && hasMainModuleInfo;
}
/**
* Creates a new task for compiling the test classes.
*
* @param listener where to send compilation warnings, or {@code null} for the Maven logger
* @throws MojoException if this method identifies an invalid parameter in this <abbr>MOJO</abbr>
* @return the task to execute for compiling the tests using the configuration in this <abbr>MOJO</abbr>
* @throws IOException if an error occurred while creating the output directory or scanning the source directories
*/
@Override
public ToolExecutor createExecutor(DiagnosticListener<? super JavaFileObject> listener) throws IOException {
Path mainModulePath = mainOutputDirectory.resolve(MODULE_INFO + CLASS_FILE_SUFFIX);
if (Files.isRegularFile(mainModulePath)) {
hasMainModuleInfo = true;
} else {
mainModulePath = null;
}
return new ToolExecutorForTest(this, listener, mainModulePath);
}
}