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      * Path to the {@code module-info.class} file of the main code, or {@code null} if that file does not exist.
217      * This field exists only for transferring this information to {@link ToolExecutorForTest#mainModulePath},
218      * and should be {@code null} the rest of the time.
219      */
220     transient Path mainModulePath;
221 
222     /**
223      * The file where to dump the command-line when debug is activated or when the compilation failed.
224      * For example, if the value is {@code "javac-test"}, then the Java compiler can be launched
225      * from the command-line by typing {@code javac @target/javac-test.args}.
226      * The debug file will contain the compiler options together with the list of source files to compile.
227      *
228      * <p>By default, this debug file is written only if the compilation of test code failed.
229      * The writing of the debug files can be forced by setting the {@link #verbose} flag to {@code true}
230      * or by specifying the {@code --verbose} option to Maven on the command-line.</p>
231      *
232      * @see CompilerMojo#debugFileName
233      * @since 3.10.0
234      */
235     @Parameter(defaultValue = "javac-test.args")
236     protected String debugFileName;
237 
238     /**
239      * Creates a new compiler <abbr>MOJO</abbr> for the tests.
240      */
241     public TestCompilerMojo() {
242         super(PathScope.TEST_COMPILE);
243     }
244 
245     /**
246      * Runs the Java compiler on the test source code.
247      * If {@link #skip} is {@code true}, then this method logs a message and does nothing else.
248      * Otherwise, this method executes the steps described in the method of the parent class.
249      *
250      * @throws MojoException if the compiler cannot be run.
251      */
252     @Override
253     public void execute() throws MojoException {
254         if (skip) {
255             logger.info("Not compiling test sources");
256             return;
257         }
258         super.execute();
259     }
260 
261     /**
262      * Parses the parameters declared in the <abbr>MOJO</abbr>.
263      *
264      * @param  compiler  the tools to use for verifying the validity of options
265      * @return the options after validation
266      */
267     @Override
268     @SuppressWarnings("deprecation")
269     public Options parseParameters(final OptionChecker compiler) {
270         Options configuration = super.parseParameters(compiler);
271         configuration.addUnchecked(
272                 testCompilerArgs == null || testCompilerArgs.isEmpty() ? compilerArgs : testCompilerArgs);
273         if (testCompilerArguments != null) {
274             for (Map.Entry<String, String> entry : testCompilerArguments.entrySet()) {
275                 configuration.addUnchecked(List.of(entry.getKey(), entry.getValue()));
276             }
277         }
278         configuration.addUnchecked(testCompilerArgument == null ? compilerArgument : testCompilerArgument);
279         return configuration;
280     }
281 
282     /**
283      * {@return the path where to place generated source files created by annotation processing on the test classes}
284      */
285     @Nullable
286     @Override
287     protected Path getGeneratedSourcesDirectory() {
288         return generatedTestSourcesDirectory;
289     }
290 
291     /**
292      * {@return the inclusion filters for the compiler, or an empty set for all Java source files}
293      */
294     @Override
295     protected Set<String> getIncludes() {
296         return (testIncludes != null) ? testIncludes : Set.of();
297     }
298 
299     /**
300      * {@return the exclusion filters for the compiler, or an empty set if none}
301      */
302     @Override
303     protected Set<String> getExcludes() {
304         return (testExcludes != null) ? testExcludes : Set.of();
305     }
306 
307     /**
308      * {@return the exclusion filters for the incremental calculation, or an empty set if none}
309      */
310     @Override
311     protected Set<String> getIncrementalExcludes() {
312         return (testIncrementalExcludes != null) ? testIncrementalExcludes : Set.of();
313     }
314 
315     /**
316      * If a different source version has been specified for the tests, returns that version.
317      * Otherwise returns the same source version as the main code.
318      *
319      * @return the {@code --source} argument for the Java compiler
320      */
321     @Nullable
322     @Override
323     protected String getSource() {
324         return testSource == null ? source : testSource;
325     }
326 
327     /**
328      * If a different target version has been specified for the tests, returns that version.
329      * Otherwise returns the same target version as the main code.
330      *
331      * @return the {@code --target} argument for the Java compiler
332      */
333     @Nullable
334     @Override
335     protected String getTarget() {
336         return testTarget == null ? target : testTarget;
337     }
338 
339     /**
340      * If a different release version has been specified for the tests, returns that version.
341      * Otherwise returns the same release version as the main code.
342      *
343      * @return the {@code --release} argument for the Java compiler
344      */
345     @Nullable
346     @Override
347     protected String getRelease() {
348         return testRelease == null ? release : testRelease;
349     }
350 
351     /**
352      * {@return the destination directory for test class files}
353      */
354     @Nonnull
355     @Override
356     protected Path getOutputDirectory() {
357         return outputDirectory;
358     }
359 
360     /**
361      * {@return the file where to dump the command-line when debug is activated or when the compilation failed}
362      *
363      * @see #debugFileName
364      */
365     @Nullable
366     @Override
367     protected String getDebugFileName() {
368         return debugFileName;
369     }
370 
371     /**
372      * {@return the module name declared in the test sources}
373      * We have to parse the source instead of the {@code module-info.class} file
374      * because the classes may not have been compiled yet.
375      * This is not very reliable, but putting a {@code module-info.java} file in the tests is deprecated anyway.
376      */
377     final String getTestModuleName(List<SourceDirectory> compileSourceRoots) throws IOException {
378         for (SourceDirectory directory : compileSourceRoots) {
379             if (directory.moduleName != null) {
380                 return directory.moduleName;
381             }
382             String name = parseModuleInfoName(directory.getModuleInfo().orElse(null));
383             if (name != null) {
384                 return name;
385             }
386         }
387         return null;
388     }
389 
390     /**
391      * {@return whether the project has at least one {@code module-info.class} file}
392      *
393      * @param roots root directories of the sources to compile
394      * @throws IOException if this method needed to read a module descriptor and failed
395      */
396     @Override
397     final boolean hasModuleDeclaration(final List<SourceDirectory> roots) throws IOException {
398         for (SourceDirectory root : roots) {
399             hasMainModuleInfo |= root.moduleName != null;
400             hasTestModuleInfo |= root.getModuleInfo().isPresent();
401             if (hasMainModuleInfo & hasTestModuleInfo) {
402                 break;
403             }
404         }
405         if (hasTestModuleInfo) {
406             MessageBuilder message = messageBuilderFactory.builder();
407             message.a("Overwriting the ")
408                     .warning(MODULE_INFO + JAVA_FILE_SUFFIX)
409                     .a(" file in the test directory is deprecated. Use ")
410                     .info(ModuleInfoPatch.FILENAME)
411                     .a(" instead.");
412             logger.warn(message.toString());
413             if (SUPPORT_LEGACY) {
414                 return useModulePath;
415             }
416         }
417         return useModulePath && hasMainModuleInfo;
418     }
419 
420     /**
421      * Creates a new task for compiling the test classes.
422      *
423      * @param listener where to send compilation warnings, or {@code null} for the Maven logger
424      * @throws MojoException if this method identifies an invalid parameter in this <abbr>MOJO</abbr>
425      * @return the task to execute for compiling the tests using the configuration in this <abbr>MOJO</abbr>
426      * @throws IOException if an error occurred while creating the output directory or scanning the source directories
427      */
428     @Override
429     public ToolExecutor createExecutor(DiagnosticListener<? super JavaFileObject> listener) throws IOException {
430         try {
431             Path file = mainOutputDirectory.resolve(MODULE_INFO + CLASS_FILE_SUFFIX);
432             if (Files.isRegularFile(file)) {
433                 mainModulePath = file;
434                 hasMainModuleInfo = true;
435             }
436             return new ToolExecutorForTest(this, listener);
437         } finally {
438             // Reset the fields that were used only for transfering information to `ToolExecutorForTest`.
439             hasTestModuleInfo = false;
440             hasMainModuleInfo = false;
441             mainModulePath = null;
442         }
443     }
444 }