View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugin.compiler;
20  
21  import javax.tools.DiagnosticListener;
22  import javax.tools.JavaFileObject;
23  import javax.tools.OptionChecker;
24  
25  import java.io.IOException;
26  import java.nio.file.Files;
27  import java.nio.file.Path;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Set;
31  
32  import org.apache.maven.api.PathScope;
33  import org.apache.maven.api.annotations.Nonnull;
34  import org.apache.maven.api.annotations.Nullable;
35  import org.apache.maven.api.plugin.MojoException;
36  import org.apache.maven.api.plugin.annotations.Mojo;
37  import org.apache.maven.api.plugin.annotations.Parameter;
38  import org.apache.maven.api.services.MessageBuilder;
39  
40  import static org.apache.maven.plugin.compiler.SourceDirectory.CLASS_FILE_SUFFIX;
41  import static org.apache.maven.plugin.compiler.SourceDirectory.JAVA_FILE_SUFFIX;
42  import static org.apache.maven.plugin.compiler.SourceDirectory.MODULE_INFO;
43  
44  /**
45   * Compiles application test sources.
46   * Each instance shall be used only once, then discarded.
47   *
48   * @author <a href="mailto:jason@maven.org">Jason van Zyl</a>
49   * @author Martin Desruisseaux
50   * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html">javac Command</a>
51   * @since 2.0
52   */
53  @Mojo(name = "testCompile", defaultPhase = "test-compile")
54  public class TestCompilerMojo extends AbstractCompilerMojo {
55      /**
56       * Whether to bypass compilation of test sources.
57       * Its use is not recommended, but quite convenient on occasion.
58       *
59       * @see CompilerMojo#skipMain
60       */
61      @Parameter(property = "maven.test.skip")
62      protected boolean skip;
63  
64      /**
65       * Specify where to place generated source files created by annotation processing.
66       *
67       * @see CompilerMojo#generatedSourcesDirectory
68       * @since 2.2
69       */
70      @Parameter(defaultValue = "${project.build.directory}/generated-test-sources/test-annotations")
71      protected Path generatedTestSourcesDirectory;
72  
73      /**
74       * A set of inclusion filters for the compiler.
75       *
76       * @see CompilerMojo#includes
77       */
78      @Parameter
79      protected Set<String> testIncludes;
80  
81      /**
82       * A set of exclusion filters for the compiler.
83       *
84       * @see CompilerMojo#excludes
85       */
86      @Parameter
87      protected Set<String> testExcludes;
88  
89      /**
90       * A set of exclusion filters for the incremental calculation.
91       * Updated files, if excluded by this filter, will not cause the project to be rebuilt.
92       *
93       * @see CompilerMojo#incrementalExcludes
94       * @since 3.11
95       */
96      @Parameter
97      protected Set<String> testIncrementalExcludes;
98  
99      /**
100      * The {@code --source} argument for the test Java compiler.
101      *
102      * @see CompilerMojo#source
103      * @since 2.1
104      */
105     @Parameter(property = "maven.compiler.testSource")
106     protected String testSource;
107 
108     /**
109      * The {@code --target} argument for the test Java compiler.
110      *
111      * @see CompilerMojo#target
112      * @since 2.1
113      */
114     @Parameter(property = "maven.compiler.testTarget")
115     protected String testTarget;
116 
117     /**
118      * the {@code --release} argument for the test Java compiler
119      *
120      * @see CompilerMojo#release
121      * @since 3.6
122      */
123     @Parameter(property = "maven.compiler.testRelease")
124     protected String testRelease;
125 
126     /**
127      * The arguments to be passed to the test compiler.
128      * If this parameter is specified, it replaces {@link #compilerArgs}.
129      * Otherwise, the {@code compilerArgs} parameter is used.
130      *
131      * @see CompilerMojo#compilerArgs
132      * @since 4.0.0
133      */
134     @Parameter
135     protected List<String> testCompilerArgs;
136 
137     /**
138      * The arguments to be passed to test compiler.
139      *
140      * @deprecated Replaced by {@link #testCompilerArgs} for consistency with the main phase.
141      *
142      * @since 2.1
143      */
144     @Parameter
145     @Deprecated(since = "4.0.0")
146     protected Map<String, String> testCompilerArguments;
147 
148     /**
149      * The single argument string to be passed to the test compiler.
150      * If this parameter is specified, it replaces {@link #compilerArgument}.
151      * Otherwise, the {@code compilerArgument} parameter is used.
152      *
153      * @deprecated Use {@link #testCompilerArgs} instead.
154      *
155      * @see CompilerMojo#compilerArgument
156      * @since 2.1
157      */
158     @Parameter
159     @Deprecated(since = "4.0.0")
160     protected String testCompilerArgument;
161 
162     /**
163      * The directory where compiled test classes go.
164      * This parameter should only be modified in special cases.
165      * See the {@link CompilerMojo#outputDirectory} for more information.
166      *
167      * @see CompilerMojo#outputDirectory
168      * @see #getOutputDirectory()
169      */
170     @Parameter(defaultValue = "${project.build.testOutputDirectory}", required = true)
171     protected Path outputDirectory;
172 
173     /**
174      * The output directory of the main classes.
175      * This directory will be added to the class-path or module-path.
176      * Its value should be the same as {@link CompilerMojo#outputDirectory}.
177      *
178      * @see CompilerMojo#outputDirectory
179      */
180     @Parameter(defaultValue = "${project.build.outputDirectory}", required = true, readonly = true)
181     protected Path mainOutputDirectory;
182 
183     /**
184      * Whether to place the main classes on the module path when {@code module-info} is present.
185      * When {@code false}, always places the main classes on the class path.
186      * Dependencies are also placed on the class-path, unless their type is {@code module-jar}.
187      *
188      * @since 3.11
189      *
190      * @deprecated Use {@code "claspath-jar"} dependency type instead, and avoid {@code module-info.java} in tests.
191      */
192     @Deprecated(since = "4.0.0")
193     @Parameter(defaultValue = "true")
194     protected boolean useModulePath = true;
195 
196     /**
197      * Whether a {@code module-info.java} file is defined in the test sources.
198      * In such case, it has precedence over the {@code module-info.java} in main sources.
199      * This is defined for compatibility with Maven 3, but not recommended.
200      *
201      * <p>This field exists in this class only for transferring this information
202      * to {@link ToolExecutorForTest#hasTestModuleInfo}, which is the class that
203      * needs this information.</p>
204      *
205      * @deprecated Avoid {@code module-info.java} in tests.
206      */
207     @Deprecated(since = "4.0.0")
208     transient boolean hasTestModuleInfo;
209 
210     /**
211      * Whether a {@code module-info.java} file is defined in the main sources.
212      */
213     private transient boolean hasMainModuleInfo;
214 
215     /**
216      * The file where to dump the command-line when debug is activated or when the compilation failed.
217      * For example, if the value is {@code "javac-test"}, then the Java compiler can be launched
218      * from the command-line by typing {@code javac @target/javac-test.args}.
219      * The debug file will contain the compiler options together with the list of source files to compile.
220      *
221      * <p>By default, this debug file is written only if the compilation of test code failed.
222      * The writing of the debug files can be forced by setting the {@link #verbose} flag to {@code true}
223      * or by specifying the {@code --verbose} option to Maven on the command-line.</p>
224      *
225      * @see CompilerMojo#debugFileName
226      * @since 3.10.0
227      */
228     @Parameter(defaultValue = "javac-test.args")
229     protected String debugFileName;
230 
231     /**
232      * Creates a new compiler <abbr>MOJO</abbr> for the tests.
233      */
234     public TestCompilerMojo() {
235         super(PathScope.TEST_COMPILE);
236     }
237 
238     /**
239      * Runs the Java compiler on the test source code.
240      * If {@link #skip} is {@code true}, then this method logs a message and does nothing else.
241      * Otherwise, this method executes the steps described in the method of the parent class.
242      *
243      * @throws MojoException if the compiler cannot be run.
244      */
245     @Override
246     public void execute() throws MojoException {
247         if (skip) {
248             logger.info("Not compiling test sources");
249             return;
250         }
251         super.execute();
252     }
253 
254     /**
255      * Parses the parameters declared in the <abbr>MOJO</abbr>.
256      *
257      * @param  compiler  the tools to use for verifying the validity of options
258      * @return the options after validation
259      */
260     @Override
261     @SuppressWarnings("deprecation")
262     public Options parseParameters(final OptionChecker compiler) {
263         Options configuration = super.parseParameters(compiler);
264         configuration.addUnchecked(isAbsent(testCompilerArgs) ? compilerArgs : testCompilerArgs);
265         if (testCompilerArguments != null) {
266             for (Map.Entry<String, String> entry : testCompilerArguments.entrySet()) {
267                 configuration.addUnchecked(List.of(entry.getKey(), entry.getValue()));
268             }
269         }
270         configuration.addUnchecked(testCompilerArgument == null ? compilerArgument : testCompilerArgument);
271         return configuration;
272     }
273 
274     /**
275      * {@return the path where to place generated source files created by annotation processing on the test classes}
276      */
277     @Nullable
278     @Override
279     protected Path getGeneratedSourcesDirectory() {
280         return generatedTestSourcesDirectory;
281     }
282 
283     /**
284      * {@return the inclusion filters for the compiler, or an empty set for all Java source files}
285      */
286     @Override
287     protected Set<String> getIncludes() {
288         return (testIncludes != null) ? testIncludes : Set.of();
289     }
290 
291     /**
292      * {@return the exclusion filters for the compiler, or an empty set if none}
293      */
294     @Override
295     protected Set<String> getExcludes() {
296         return (testExcludes != null) ? testExcludes : Set.of();
297     }
298 
299     /**
300      * {@return the exclusion filters for the incremental calculation, or an empty set if none}
301      */
302     @Override
303     protected Set<String> getIncrementalExcludes() {
304         return (testIncrementalExcludes != null) ? testIncrementalExcludes : Set.of();
305     }
306 
307     /**
308      * If a different source version has been specified for the tests, returns that version.
309      * Otherwise returns the same source version as the main code.
310      *
311      * @return the {@code --source} argument for the Java compiler
312      */
313     @Nullable
314     @Override
315     protected String getSource() {
316         return testSource == null ? source : testSource;
317     }
318 
319     /**
320      * If a different target version has been specified for the tests, returns that version.
321      * Otherwise returns the same target version as the main code.
322      *
323      * @return the {@code --target} argument for the Java compiler
324      */
325     @Nullable
326     @Override
327     protected String getTarget() {
328         return testTarget == null ? target : testTarget;
329     }
330 
331     /**
332      * If a different release version has been specified for the tests, returns that version.
333      * Otherwise returns the same release version as the main code.
334      *
335      * @return the {@code --release} argument for the Java compiler
336      */
337     @Nullable
338     @Override
339     protected String getRelease() {
340         return testRelease == null ? release : testRelease;
341     }
342 
343     /**
344      * {@return the destination directory for test class files}
345      */
346     @Nonnull
347     @Override
348     protected Path getOutputDirectory() {
349         return outputDirectory;
350     }
351 
352     /**
353      * {@return the file where to dump the command-line when debug is activated or when the compilation failed}
354      *
355      * @see #debugFileName
356      */
357     @Nullable
358     @Override
359     protected String getDebugFileName() {
360         return debugFileName;
361     }
362 
363     /**
364      * {@return the module name found in the package hierarchy of given sources}
365      * We have to parse the source instead of the {@code module-info.class} file
366      * because the classes may not have been compiled yet. This is not reliable,
367      * but the use of package hierarchy for modular project should be avoided in
368      * Maven 4.
369      *
370      * @deprecated Declare modules in {@code <source>} elements instead.
371      */
372     @Deprecated(since = "4.0.0")
373     final String moduleNameFromPackageHierarchy(List<SourceDirectory> compileSourceRoots) throws IOException {
374         for (SourceDirectory directory : compileSourceRoots) {
375             if (directory.moduleName == null) {
376                 String name = parseModuleInfoName(directory.getModuleInfo().orElse(null));
377                 if (name != null) {
378                     return name;
379                 }
380             }
381         }
382         return null;
383     }
384 
385     /**
386      * {@return whether the project has at least one {@code module-info.class} file}
387      * The {@code module-info.class} should be located in the main source code.
388      * However, this method checks also in the test source code for compatibility with Maven 3,
389      * but this practice is deprecated.
390      *
391      * @param roots root directories of the source files of the test classes to compile
392      * @throws IOException if this method needed to read a module descriptor and failed
393      */
394     @Override
395     final boolean hasModuleDeclaration(final List<SourceDirectory> roots) throws IOException {
396         for (SourceDirectory root : roots) {
397             hasMainModuleInfo |= root.moduleName != null;
398             hasTestModuleInfo |= root.getModuleInfo().isPresent();
399             if (hasMainModuleInfo & hasTestModuleInfo) {
400                 break;
401             }
402         }
403         if (hasTestModuleInfo) {
404             MessageBuilder message = messageBuilderFactory.builder();
405             message.a("Overwriting the ")
406                     .warning(MODULE_INFO + JAVA_FILE_SUFFIX)
407                     .a(" file in the test directory is deprecated. Use ")
408                     .info(ModuleInfoPatch.FILENAME)
409                     .a(" instead.");
410             logger.warn(message.toString());
411             if (SUPPORT_LEGACY) {
412                 return useModulePath;
413             }
414         }
415         return useModulePath && hasMainModuleInfo;
416     }
417 
418     /**
419      * Creates a new task for compiling the test classes.
420      *
421      * @param listener where to send compilation warnings, or {@code null} for the Maven logger
422      * @throws MojoException if this method identifies an invalid parameter in this <abbr>MOJO</abbr>
423      * @return the task to execute for compiling the tests using the configuration in this <abbr>MOJO</abbr>
424      * @throws IOException if an error occurred while creating the output directory or scanning the source directories
425      */
426     @Override
427     public ToolExecutor createExecutor(DiagnosticListener<? super JavaFileObject> listener) throws IOException {
428         Path mainModulePath = mainOutputDirectory.resolve(MODULE_INFO + CLASS_FILE_SUFFIX);
429         if (Files.isRegularFile(mainModulePath)) {
430             hasMainModuleInfo = true;
431         } else {
432             mainModulePath = null;
433         }
434         return new ToolExecutorForTest(this, listener, mainModulePath);
435     }
436 }