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.JavaCompiler;
22  import javax.tools.OptionChecker;
23  
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.lang.module.ModuleDescriptor;
27  import java.nio.file.Files;
28  import java.nio.file.Path;
29  import java.nio.file.Paths;
30  import java.util.ArrayList;
31  import java.util.HashSet;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.Set;
35  import java.util.StringJoiner;
36  
37  import org.apache.maven.api.Dependency;
38  import org.apache.maven.api.JavaPathType;
39  import org.apache.maven.api.PathType;
40  import org.apache.maven.api.ProjectScope;
41  import org.apache.maven.api.annotations.Nonnull;
42  import org.apache.maven.api.annotations.Nullable;
43  import org.apache.maven.api.plugin.MojoException;
44  import org.apache.maven.api.plugin.annotations.Mojo;
45  import org.apache.maven.api.plugin.annotations.Parameter;
46  import org.apache.maven.api.services.DependencyResolverResult;
47  import org.apache.maven.api.services.MessageBuilder;
48  
49  import static org.apache.maven.plugin.compiler.SourceDirectory.CLASS_FILE_SUFFIX;
50  import static org.apache.maven.plugin.compiler.SourceDirectory.JAVA_FILE_SUFFIX;
51  import static org.apache.maven.plugin.compiler.SourceDirectory.MODULE_INFO;
52  
53  /**
54   * Compiles application test sources.
55   * Each instance shall be used only once, then discarded.
56   *
57   * @author <a href="mailto:jason@maven.org">Jason van Zyl</a>
58   * @author Martin Desruisseaux
59   * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html">javac Command</a>
60   * @since 2.0
61   */
62  @Mojo(name = "testCompile", defaultPhase = "test-compile")
63  public class TestCompilerMojo extends AbstractCompilerMojo {
64      /**
65       * Whether to bypass compilation of test sources.
66       * Its use is not recommended, but quite convenient on occasion.
67       *
68       * @see CompilerMojo#skipMain
69       */
70      @Parameter(property = "maven.test.skip")
71      protected boolean skip;
72  
73      /**
74       * The source directories containing the test-source to be compiled.
75       *
76       * @see CompilerMojo#compileSourceRoots
77       */
78      @Parameter
79      protected List<String> compileSourceRoots;
80  
81      /**
82       * Specify where to place generated source files created by annotation processing.
83       *
84       * @see CompilerMojo#generatedSourcesDirectory
85       * @since 2.2
86       */
87      @Parameter(defaultValue = "${project.build.directory}/generated-test-sources/test-annotations")
88      protected Path generatedTestSourcesDirectory;
89  
90      /**
91       * A set of inclusion filters for the compiler.
92       *
93       * @see CompilerMojo#includes
94       */
95      @Parameter
96      protected Set<String> testIncludes;
97  
98      /**
99       * A set of exclusion filters for the compiler.
100      *
101      * @see CompilerMojo#excludes
102      */
103     @Parameter
104     protected Set<String> testExcludes;
105 
106     /**
107      * A set of exclusion filters for the incremental calculation.
108      * Updated files, if excluded by this filter, will not cause the project to be rebuilt.
109      *
110      * @see CompilerMojo#incrementalExcludes
111      * @since 3.11
112      */
113     @Parameter
114     protected Set<String> testIncrementalExcludes;
115 
116     /**
117      * The {@code --source} argument for the test Java compiler.
118      *
119      * @see CompilerMojo#source
120      * @since 2.1
121      */
122     @Parameter(property = "maven.compiler.testSource")
123     protected String testSource;
124 
125     /**
126      * The {@code --target} argument for the test Java compiler.
127      *
128      * @see CompilerMojo#target
129      * @since 2.1
130      */
131     @Parameter(property = "maven.compiler.testTarget")
132     protected String testTarget;
133 
134     /**
135      * the {@code --release} argument for the test Java compiler
136      *
137      * @see CompilerMojo#release
138      * @since 3.6
139      */
140     @Parameter(property = "maven.compiler.testRelease")
141     protected String testRelease;
142 
143     /**
144      * The arguments to be passed to the test compiler.
145      * If this parameter is specified, it replaces {@link #compilerArgs}.
146      * Otherwise, the {@code compilerArgs} parameter is used.
147      *
148      * @see CompilerMojo#compilerArgs
149      * @since 4.0.0
150      */
151     @Parameter
152     protected List<String> testCompilerArgs;
153 
154     /**
155      * The arguments to be passed to test compiler.
156      *
157      * @deprecated Replaced by {@link #testCompilerArgs} for consistency with the main phase.
158      *
159      * @since 2.1
160      */
161     @Parameter
162     @Deprecated(since = "4.0.0")
163     protected Map<String, String> testCompilerArguments;
164 
165     /**
166      * The single argument string to be passed to the test compiler.
167      * If this parameter is specified, it replaces {@link #compilerArgument}.
168      * Otherwise, the {@code compilerArgument} parameter is used.
169      *
170      * @deprecated Use {@link #testCompilerArgs} instead.
171      *
172      * @see CompilerMojo#compilerArgument
173      * @since 2.1
174      */
175     @Parameter
176     @Deprecated(since = "4.0.0")
177     protected String testCompilerArgument;
178 
179     /**
180      * The directory where compiled test classes go.
181      * This parameter should only be modified in special cases.
182      * See the {@link CompilerMojo#outputDirectory} for more information.
183      *
184      * @see CompilerMojo#outputDirectory
185      */
186     @Parameter(defaultValue = "${project.build.testOutputDirectory}", required = true)
187     protected Path outputDirectory;
188 
189     /**
190      * The output directory of the main classes.
191      * This directory will be added to the class-path or module-path.
192      * Its value should be the same as {@link CompilerMojo#outputDirectory}.
193      *
194      * @see CompilerMojo#outputDirectory
195      * @see #addImplicitDependencies(Map, boolean)
196      */
197     @Parameter(defaultValue = "${project.build.outputDirectory}", required = true, readonly = true)
198     protected Path mainOutputDirectory;
199 
200     /**
201      * Whether to place the main classes on the module path when {@code module-info} is present.
202      * When {@code false}, always places the main classes on the class path.
203      * Dependencies are also placed on the class-path, unless their type is {@code module-jar}.
204      *
205      * @since 3.11
206      *
207      * @deprecated Use {@code "claspath-jar"} dependency type instead, and avoid {@code module-info.java} in tests.
208      */
209     @Deprecated(since = "4.0.0")
210     @Parameter(defaultValue = "true")
211     protected boolean useModulePath = true;
212 
213     /**
214      * Name of the main module to compile, or {@code null} if not yet determined.
215      * If the project is not modular, an empty string.
216      *
217      * TODO: use "*" as a sentinel value for modular source hierarchy.
218      *
219      * @see #getMainModuleName()
220      */
221     private String moduleName;
222 
223     /**
224      * Whether a {@code module-info.java} file is defined in the test sources.
225      * In such case, it has precedence over the {@code module-info.java} in main sources.
226      * This is defined for compatibility with Maven 3, but not recommended.
227      */
228     private boolean hasTestModuleInfo;
229 
230     /**
231      * Whether the {@code module-info} of the tests overwrites the main {@code module-info}.
232      * This is a deprecated practice, but is accepted if {@link #SUPPORT_LEGACY} is true.
233      */
234     private boolean overwriteMainModuleInfo;
235 
236     /**
237      * The file where to dump the command-line when debug is activated or when the compilation failed.
238      * For example, if the value is {@code "javac-test"}, then the Java compiler can be launched
239      * from the command-line by typing {@code javac @target/javac-test.args}.
240      * The debug file will contain the compiler options together with the list of source files to compile.
241      *
242      * @see CompilerMojo#debugFileName
243      * @since 3.10.0
244      */
245     @Parameter(defaultValue = "javac-test.args")
246     protected String debugFileName;
247 
248     /**
249      * Creates a new test compiler MOJO.
250      */
251     public TestCompilerMojo() {
252         super(true);
253     }
254 
255     /**
256      * Runs the Java compiler on the test source code.
257      *
258      * @throws MojoException if the compiler cannot be run.
259      */
260     @Override
261     public void execute() throws MojoException {
262         if (skip) {
263             logger.info("Not compiling test sources");
264             return;
265         }
266         super.execute();
267     }
268 
269     /**
270      * Parses the parameters declared in the MOJO.
271      *
272      * @param  compiler  the tools to use for verifying the validity of options
273      * @return the options after validation
274      */
275     @Override
276     @SuppressWarnings("deprecation")
277     protected Options acceptParameters(final OptionChecker compiler) {
278         Options compilerConfiguration = super.acceptParameters(compiler);
279         compilerConfiguration.addUnchecked(
280                 testCompilerArgs == null || testCompilerArgs.isEmpty() ? compilerArgs : testCompilerArgs);
281         if (testCompilerArguments != null) {
282             for (Map.Entry<String, String> entry : testCompilerArguments.entrySet()) {
283                 compilerConfiguration.addUnchecked(List.of(entry.getKey(), entry.getValue()));
284             }
285         }
286         compilerConfiguration.addUnchecked(testCompilerArgument == null ? compilerArgument : testCompilerArgument);
287         return compilerConfiguration;
288     }
289 
290     /**
291      * {@return the root directories of Java source files to compile for the tests}.
292      */
293     @Nonnull
294     @Override
295     protected List<Path> getCompileSourceRoots() {
296         if (compileSourceRoots == null || compileSourceRoots.isEmpty()) {
297             return projectManager.getCompileSourceRoots(project, ProjectScope.TEST);
298         } else {
299             return compileSourceRoots.stream().map(Paths::get).toList();
300         }
301     }
302 
303     /**
304      * {@return the path where to place generated source files created by annotation processing on the test classes}.
305      */
306     @Nullable
307     @Override
308     protected Path getGeneratedSourcesDirectory() {
309         return generatedTestSourcesDirectory;
310     }
311 
312     /**
313      * {@return the inclusion filters for the compiler, or an empty set for all Java source files}.
314      */
315     @Override
316     protected Set<String> getIncludes() {
317         return (testIncludes != null) ? testIncludes : Set.of();
318     }
319 
320     /**
321      * {@return the exclusion filters for the compiler, or an empty set if none}.
322      */
323     @Override
324     protected Set<String> getExcludes() {
325         return (testExcludes != null) ? testExcludes : Set.of();
326     }
327 
328     /**
329      * {@return the exclusion filters for the incremental calculation, or an empty set if none}.
330      */
331     @Override
332     protected Set<String> getIncrementalExcludes() {
333         return (testIncrementalExcludes != null) ? testIncrementalExcludes : Set.of();
334     }
335 
336     /**
337      * If a different source version has been specified for the tests, returns that version.
338      * Otherwise returns the same source version as the main code.
339      *
340      * @return the {@code --source} argument for the Java compiler
341      */
342     @Nullable
343     @Override
344     protected String getSource() {
345         return testSource == null ? source : testSource;
346     }
347 
348     /**
349      * If a different target version has been specified for the tests, returns that version.
350      * Otherwise returns the same target version as the main code.
351      *
352      * @return the {@code --target} argument for the Java compiler
353      */
354     @Nullable
355     @Override
356     protected String getTarget() {
357         return testTarget == null ? target : testTarget;
358     }
359 
360     /**
361      * If a different release version has been specified for the tests, returns that version.
362      * Otherwise returns the same release version as the main code.
363      *
364      * @return the {@code --release} argument for the Java compiler
365      */
366     @Nullable
367     @Override
368     protected String getRelease() {
369         return testRelease == null ? release : testRelease;
370     }
371 
372     /**
373      * {@return the destination directory for test class files}.
374      */
375     @Nonnull
376     @Override
377     protected Path getOutputDirectory() {
378         return outputDirectory;
379     }
380 
381     /**
382      * {@return the file where to dump the command-line when debug is activated or when the compilation failed}.
383      */
384     @Nullable
385     @Override
386     protected String getDebugFileName() {
387         return debugFileName;
388     }
389 
390     /**
391      * {@return the module name of the main code, or an empty string if none}.
392      * This method reads the module descriptor when first needed and caches the result.
393      *
394      * @throws IOException if the module descriptor cannot be read.
395      */
396     private String getMainModuleName() throws IOException {
397         if (moduleName == null) {
398             Path file = mainOutputDirectory.resolve(MODULE_INFO + CLASS_FILE_SUFFIX);
399             if (Files.isRegularFile(file)) {
400                 try (InputStream in = Files.newInputStream(file)) {
401                     moduleName = ModuleDescriptor.read(in).name();
402                 }
403             } else {
404                 moduleName = "";
405             }
406         }
407         return moduleName;
408     }
409 
410     /**
411      * {@return the module name declared in the test sources}. We have to parse the source instead
412      * of the {@code module-info.class} file because the classes may not have been compiled yet.
413      * This is not very reliable, but putting a {@code module-info.java} file in the tests is
414      * deprecated anyway.
415      */
416     private String getTestModuleName(List<SourceDirectory> compileSourceRoots) throws IOException {
417         for (SourceDirectory directory : compileSourceRoots) {
418             if (directory.moduleName != null) {
419                 return directory.moduleName;
420             }
421             String name = parseModuleInfoName(directory.getModuleInfo().orElse(null));
422             if (name != null) {
423                 return name;
424             }
425         }
426         return null;
427     }
428 
429     /**
430      * {@return whether the project has at least one {@code module-info.class} file}.
431      * This method opportunistically fetches the module name.
432      *
433      * @param roots root directories of the sources to compile
434      * @throws IOException if this method needed to read a module descriptor and failed
435      */
436     @Override
437     final boolean hasModuleDeclaration(final List<SourceDirectory> roots) throws IOException {
438         hasTestModuleInfo = super.hasModuleDeclaration(roots);
439         if (hasTestModuleInfo) {
440             MessageBuilder message = messageBuilderFactory.builder();
441             message.a("Overwriting the ")
442                     .warning(MODULE_INFO + JAVA_FILE_SUFFIX)
443                     .a(" file in the test directory is deprecated. Use ")
444                     .info("--add-reads")
445                     .a(", ")
446                     .info("--add-modules")
447                     .a(" and related options instead.");
448             logger.warn(message.toString());
449             if (SUPPORT_LEGACY) {
450                 return useModulePath;
451             }
452         }
453         return useModulePath && !getMainModuleName().isEmpty();
454     }
455 
456     /**
457      * Adds the main compilation output directories as test dependencies.
458      *
459      * @param addTo where to add dependencies
460      * @param hasModuleDeclaration whether the main sources have or should have a {@code module-info} file
461      */
462     @Override
463     protected void addImplicitDependencies(Map<PathType, List<Path>> addTo, boolean hasModuleDeclaration) {
464         var pathType = hasModuleDeclaration ? JavaPathType.MODULES : JavaPathType.CLASSES;
465         if (Files.exists(mainOutputDirectory)) {
466             addTo.computeIfAbsent(pathType, (key) -> new ArrayList<>()).add(mainOutputDirectory);
467         }
468     }
469 
470     /**
471      * Adds {@code --patch-module} options for the given source directories.
472      * In this case, the option values are directory of <em>source</em> files.
473      * Not to be confused with cases where a module is patched with compiled
474      * classes (it may happen in other parts of the compiler plugin).
475      *
476      * @param addTo the collection of source paths to augment
477      * @param compileSourceRoots the source paths to eventually adds to the {@code toAdd} map
478      * @throws IOException if this method needs to read a module descriptor and this operation failed
479      */
480     @Override
481     final void addSourceDirectories(Map<PathType, List<Path>> addTo, List<SourceDirectory> compileSourceRoots)
482             throws IOException {
483         for (SourceDirectory dir : compileSourceRoots) {
484             String moduleToPatch = dir.moduleName;
485             if (moduleToPatch == null) {
486                 moduleToPatch = getMainModuleName();
487                 if (moduleToPatch.isEmpty()) {
488                     continue; // No module-info found.
489                 }
490                 if (SUPPORT_LEGACY) {
491                     String testModuleName = getTestModuleName(compileSourceRoots);
492                     if (testModuleName != null) {
493                         overwriteMainModuleInfo = testModuleName.equals(getMainModuleName());
494                         if (!overwriteMainModuleInfo) {
495                             continue; // The test classes are in their own module.
496                         }
497                     }
498                 }
499             }
500             addTo.computeIfAbsent(JavaPathType.patchModule(moduleToPatch), (key) -> new ArrayList<>())
501                     .add(dir.root);
502         }
503     }
504 
505     /**
506      * Generates the {@code --add-modules} and {@code --add-reads} options for the dependencies that are not
507      * in the main compilation. This method is invoked only if {@code hasModuleDeclaration} is {@code true}.
508      *
509      * @param dependencies the project dependencies
510      * @param addTo where to add the options
511      * @throws IOException if the module information of a dependency cannot be read
512      */
513     @Override
514     @SuppressWarnings({"checkstyle:MissingSwitchDefault", "fallthrough"})
515     protected void addModuleOptions(DependencyResolverResult dependencies, Options addTo) throws IOException {
516         if (SUPPORT_LEGACY && useModulePath && hasTestModuleInfo) {
517             /*
518              * Do not add any `--add-reads` parameters. The developers should put
519              * everything needed in the `module-info`, including test dependencies.
520              */
521             return;
522         }
523         final var done = new HashSet<String>(); // Added modules and their dependencies.
524         final var addModules = new StringJoiner(",");
525         StringJoiner addReads = null;
526         boolean hasUnnamed = false;
527         for (Map.Entry<Dependency, Path> entry : dependencies.getDependencies().entrySet()) {
528             boolean compile = false;
529             switch (entry.getKey().getScope()) {
530                 case TEST:
531                 case TEST_ONLY:
532                     compile = true;
533                     // Fall through
534                 case TEST_RUNTIME:
535                     if (compile) {
536                         // Needs to be initialized even if `name` is null.
537                         if (addReads == null) {
538                             addReads = new StringJoiner(",", getMainModuleName() + "=", "");
539                         }
540                     }
541                     Path path = entry.getValue();
542                     String name = dependencies.getModuleName(path).orElse(null);
543                     if (name == null) {
544                         hasUnnamed = true;
545                     } else if (done.add(name)) {
546                         addModules.add(name);
547                         if (compile) {
548                             addReads.add(name);
549                         }
550                         /*
551                          * For making the options simpler, we do not add `--add-modules` or `--add-reads`
552                          * options for modules that are required by a module that we already added. This
553                          * simplification is not necessary, but makes the command-line easier to read.
554                          */
555                         dependencies.getModuleDescriptor(path).ifPresent((descriptor) -> {
556                             for (ModuleDescriptor.Requires r : descriptor.requires()) {
557                                 done.add(r.name());
558                             }
559                         });
560                     }
561                     break;
562             }
563         }
564         if (!done.isEmpty()) {
565             addTo.addIfNonBlank("--add-modules", addModules.toString());
566         }
567         if (addReads != null) {
568             if (hasUnnamed) {
569                 addReads.add("ALL-UNNAMED");
570             }
571             addTo.addIfNonBlank("--add-reads", addReads.toString());
572         }
573     }
574 
575     /**
576      * Separates the compilation of {@code module-info} from other classes. This is needed when the
577      * {@code module-info} of the test classes overwrite the {@code module-info} of the main classes.
578      * In the latter case, we need to compile the test {@code module-info} first in order to substitute
579      * the main module-info by the test one before to compile the remaining test classes.
580      */
581     @Override
582     final CompilationTaskSources[] toCompilationTasks(final SourcesForRelease unit) {
583         if (!(SUPPORT_LEGACY && useModulePath && hasTestModuleInfo && overwriteMainModuleInfo)) {
584             return super.toCompilationTasks(unit);
585         }
586         CompilationTaskSources moduleInfo = null;
587         final List<Path> files = unit.files;
588         for (int i = files.size(); --i >= 0; ) {
589             if (SourceDirectory.isModuleInfoSource(files.get(i))) {
590                 moduleInfo = new CompilationTaskSources(List.of(files.remove(i)));
591                 if (files.isEmpty()) {
592                     return new CompilationTaskSources[] {moduleInfo};
593                 }
594                 break;
595             }
596         }
597         var task = new CompilationTaskSources(files) {
598             /**
599              * Substitutes the main {@code module-info.class} by the test's one, compiles test classes,
600              * then restores the original {@code module-info.class}. The test {@code module-info.class}
601              * must have been compiled separately before this method is invoked.
602              */
603             @Override
604             boolean compile(JavaCompiler.CompilationTask task) throws IOException {
605                 try (unit) {
606                     unit.substituteModuleInfos(mainOutputDirectory, outputDirectory);
607                     return super.compile(task);
608                 }
609             }
610         };
611         if (moduleInfo != null) {
612             return new CompilationTaskSources[] {moduleInfo, task};
613         } else {
614             return new CompilationTaskSources[] {task};
615         }
616     }
617 }