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 java.io.IOException;
22  import java.nio.file.Files;
23  import java.nio.file.Path;
24  import java.nio.file.Paths;
25  import java.util.ArrayList;
26  import java.util.Collections;
27  import java.util.HashSet;
28  import java.util.LinkedHashMap;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.Map.Entry;
32  import java.util.Optional;
33  import java.util.Set;
34  import java.util.stream.Collectors;
35  import java.util.stream.Stream;
36  
37  import org.apache.maven.api.JavaToolchain;
38  import org.apache.maven.api.PathScope;
39  import org.apache.maven.api.ProjectScope;
40  import org.apache.maven.api.Toolchain;
41  import org.apache.maven.api.plugin.MojoException;
42  import org.apache.maven.api.plugin.annotations.Mojo;
43  import org.apache.maven.api.plugin.annotations.Parameter;
44  import org.codehaus.plexus.compiler.util.scan.SimpleSourceInclusionScanner;
45  import org.codehaus.plexus.compiler.util.scan.SourceInclusionScanner;
46  import org.codehaus.plexus.compiler.util.scan.StaleSourceScanner;
47  import org.codehaus.plexus.languages.java.jpms.JavaModuleDescriptor;
48  import org.codehaus.plexus.languages.java.jpms.LocationManager;
49  import org.codehaus.plexus.languages.java.jpms.ResolvePathsRequest;
50  import org.codehaus.plexus.languages.java.jpms.ResolvePathsResult;
51  
52  /**
53   * Compiles application test sources.
54   * By default uses the <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html">javac</a> compiler
55   * of the JDK used to execute Maven. This can be overwritten through <a href="https://maven.apache.org/guides/mini/guide-using-toolchains.html">Toolchains</a>
56   * or parameter {@link AbstractCompilerMojo#compilerId}.
57   *
58   * @author <a href="mailto:jason@maven.org">Jason van Zyl</a>
59   * @since 2.0
60   * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html">javac Command</a>
61   */
62  @Mojo(name = "testCompile", defaultPhase = "test-compile")
63  public class TestCompilerMojo extends AbstractCompilerMojo {
64      /**
65       * Set this to 'true' to bypass compilation of test sources.
66       * Its use is NOT RECOMMENDED, but quite convenient on occasion.
67       */
68      @Parameter(property = "maven.test.skip")
69      private boolean skip;
70  
71      /**
72       * The source directories containing the test-source to be compiled.
73       */
74      @Parameter
75      private List<String> compileSourceRoots;
76  
77      /**
78       * The directory where compiled test classes go.
79       */
80      @Parameter(defaultValue = "${project.build.outputDirectory}", required = true)
81      private Path mainOutputDirectory;
82  
83      /**
84       * The directory where compiled test classes go.
85       * <p>
86       * This parameter should only be modified in special cases.
87       * See the {@link CompilerMojo#outputDirectory} for more information.
88       *
89       * @see CompilerMojo#outputDirectory
90       */
91      @Parameter(defaultValue = "${project.build.testOutputDirectory}", required = true)
92      private Path outputDirectory;
93  
94      /**
95       * A list of inclusion filters for the compiler.
96       */
97      @Parameter
98      private Set<String> testIncludes = new HashSet<>();
99  
100     /**
101      * A list of exclusion filters for the compiler.
102      */
103     @Parameter
104     private Set<String> testExcludes = new HashSet<>();
105 
106     /**
107      * A list of exclusion filters for the incremental calculation.
108      * @since 3.11
109      */
110     @Parameter
111     private Set<String> testIncrementalExcludes = new HashSet<>();
112 
113     /**
114      * The -source argument for the test Java compiler.
115      *
116      * @since 2.1
117      */
118     @Parameter(property = "maven.compiler.testSource")
119     private String testSource;
120 
121     /**
122      * The -target argument for the test Java compiler.
123      *
124      * @since 2.1
125      */
126     @Parameter(property = "maven.compiler.testTarget")
127     private String testTarget;
128 
129     /**
130      * the -release argument for the test Java compiler
131      *
132      * @since 3.6
133      */
134     @Parameter(property = "maven.compiler.testRelease")
135     private String testRelease;
136 
137     /**
138      * <p>
139      * Sets the arguments to be passed to test compiler (prepending a dash) if fork is set to true.
140      * </p>
141      * <p>
142      * This is because the list of valid arguments passed to a Java compiler
143      * varies based on the compiler version.
144      * </p>
145      *
146      * @since 2.1
147      */
148     @Parameter
149     private Map<String, String> testCompilerArguments;
150 
151     /**
152      * <p>
153      * Sets the unformatted argument string to be passed to test compiler if fork is set to true.
154      * </p>
155      * <p>
156      * This is because the list of valid arguments passed to a Java compiler
157      * varies based on the compiler version.
158      * </p>
159      *
160      * @since 2.1
161      */
162     @Parameter
163     private String testCompilerArgument;
164 
165     /**
166      * <p>
167      * Specify where to place generated source files created by annotation processing.
168      * Only applies to JDK 1.6+
169      * </p>
170      *
171      * @since 2.2
172      */
173     @Parameter(defaultValue = "${project.build.directory}/generated-test-sources/test-annotations")
174     private Path generatedTestSourcesDirectory;
175 
176     /**
177      * <p>
178      * When {@code true}, uses the module path when compiling with a release or target of 9+ and
179      * <em>module-info.java</em> or <em>module-info.class</em> is present.
180      * When {@code false}, always uses the class path.
181      * </p>
182      *
183      * @since 3.11
184      */
185     @Parameter(defaultValue = "true")
186     private boolean useModulePath;
187 
188     @Parameter
189     private List<String> testPath;
190 
191     /**
192      * when forking and debug activated the commandline used will be dumped in this file
193      * @since 3.10.0
194      */
195     @Parameter(defaultValue = "javac-test")
196     private String debugFileName;
197 
198     final LocationManager locationManager = new LocationManager();
199 
200     private Map<String, JavaModuleDescriptor> pathElements;
201 
202     private List<String> classpathElements;
203 
204     private List<String> modulepathElements;
205 
206     public void execute() throws MojoException {
207         if (skip) {
208             getLog().info("Not compiling test sources");
209             return;
210         }
211         super.execute();
212     }
213 
214     protected List<Path> getCompileSourceRoots() {
215         if (compileSourceRoots == null || compileSourceRoots.isEmpty()) {
216             return projectManager.getCompileSourceRoots(getProject(), ProjectScope.TEST);
217         } else {
218             return compileSourceRoots.stream().map(Paths::get).collect(Collectors.toList());
219         }
220     }
221 
222     @Override
223     protected Map<String, JavaModuleDescriptor> getPathElements() {
224         return pathElements;
225     }
226 
227     protected List<String> getClasspathElements() {
228         return classpathElements;
229     }
230 
231     @Override
232     protected List<String> getModulepathElements() {
233         return modulepathElements;
234     }
235 
236     protected Path getOutputDirectory() {
237         return outputDirectory;
238     }
239 
240     @Override
241     protected void preparePaths(Set<Path> sourceFiles) {
242         List<String> testPath = this.testPath;
243         if (testPath == null) {
244             Stream<String> s1 = Stream.of(outputDirectory.toString(), mainOutputDirectory.toString());
245             Stream<String> s2 = session.resolveDependencies(getProject(), PathScope.TEST_COMPILE).stream()
246                     .map(Path::toString);
247             testPath = Stream.concat(s1, s2).collect(Collectors.toList());
248         }
249 
250         Path mainOutputDirectory = Paths.get(getProject().getBuild().getOutputDirectory());
251 
252         Path mainModuleDescriptorClassFile = mainOutputDirectory.resolve("module-info.class");
253         JavaModuleDescriptor mainModuleDescriptor = null;
254 
255         Path testModuleDescriptorJavaFile = Paths.get("module-info.java");
256         JavaModuleDescriptor testModuleDescriptor = null;
257 
258         // Go through the source files to respect includes/excludes
259         for (Path sourceFile : sourceFiles) {
260             // @todo verify if it is the root of a sourcedirectory?
261             if ("module-info.java".equals(sourceFile.getFileName().toString())) {
262                 testModuleDescriptorJavaFile = sourceFile;
263                 break;
264             }
265         }
266 
267         // Get additional information from the main module descriptor, if available
268         if (Files.exists(mainModuleDescriptorClassFile)) {
269             ResolvePathsResult<String> result;
270 
271             try {
272                 ResolvePathsRequest<String> request = ResolvePathsRequest.ofStrings(testPath)
273                         .setIncludeStatic(true)
274                         .setMainModuleDescriptor(
275                                 mainModuleDescriptorClassFile.toAbsolutePath().toString());
276 
277                 Optional<Toolchain> toolchain = getToolchain();
278                 if (toolchain.isPresent() && toolchain.get() instanceof JavaToolchain) {
279                     request.setJdkHome(((JavaToolchain) toolchain.get()).getJavaHome());
280                 }
281 
282                 result = locationManager.resolvePaths(request);
283 
284                 for (Entry<String, Exception> pathException :
285                         result.getPathExceptions().entrySet()) {
286                     Throwable cause = pathException.getValue();
287                     while (cause.getCause() != null) {
288                         cause = cause.getCause();
289                     }
290                     String fileName =
291                             Paths.get(pathException.getKey()).getFileName().toString();
292                     getLog().warn("Can't extract module name from " + fileName + ": " + cause.getMessage());
293                 }
294             } catch (IOException e) {
295                 throw new RuntimeException(e);
296             }
297 
298             mainModuleDescriptor = result.getMainModuleDescriptor();
299 
300             pathElements = new LinkedHashMap<>(result.getPathElements().size());
301             pathElements.putAll(result.getPathElements());
302 
303             modulepathElements = new ArrayList<>(result.getModulepathElements().keySet());
304             classpathElements = new ArrayList<>(result.getClasspathElements());
305         }
306 
307         // Get additional information from the test module descriptor, if available
308         if (Files.exists(testModuleDescriptorJavaFile)) {
309             ResolvePathsResult<String> result;
310 
311             try {
312                 ResolvePathsRequest<String> request = ResolvePathsRequest.ofStrings(testPath)
313                         .setMainModuleDescriptor(
314                                 testModuleDescriptorJavaFile.toAbsolutePath().toString());
315 
316                 Optional<Toolchain> toolchain = getToolchain();
317                 if (toolchain.isPresent() && toolchain.get() instanceof JavaToolchain) {
318                     request.setJdkHome(((JavaToolchain) toolchain.get()).getJavaHome());
319                 }
320 
321                 result = locationManager.resolvePaths(request);
322             } catch (IOException e) {
323                 throw new RuntimeException(e);
324             }
325 
326             testModuleDescriptor = result.getMainModuleDescriptor();
327         }
328 
329         if (release != null && !release.isEmpty()) {
330             if (Integer.parseInt(release) < 9) {
331                 pathElements = Collections.emptyMap();
332                 modulepathElements = Collections.emptyList();
333                 classpathElements = testPath;
334                 return;
335             }
336         } else if (Double.parseDouble(getTarget()) < Double.parseDouble(MODULE_INFO_TARGET)) {
337             pathElements = Collections.emptyMap();
338             modulepathElements = Collections.emptyList();
339             classpathElements = testPath;
340             return;
341         }
342 
343         if (testModuleDescriptor != null) {
344             modulepathElements = testPath;
345             classpathElements = Collections.emptyList();
346 
347             if (mainModuleDescriptor != null) {
348                 if (getLog().isDebugEnabled()) {
349                     getLog().debug("Main and test module descriptors exist:");
350                     getLog().debug("  main module = " + mainModuleDescriptor.name());
351                     getLog().debug("  test module = " + testModuleDescriptor.name());
352                 }
353 
354                 if (testModuleDescriptor.name().equals(mainModuleDescriptor.name())) {
355                     if (compilerArgs == null) {
356                         compilerArgs = new ArrayList<>();
357                     }
358                     compilerArgs.add("--patch-module");
359 
360                     StringBuilder patchModuleValue = new StringBuilder();
361                     patchModuleValue.append(testModuleDescriptor.name());
362                     patchModuleValue.append('=');
363 
364                     for (Path root : projectManager.getCompileSourceRoots(getProject(), ProjectScope.MAIN)) {
365                         if (Files.exists(root)) {
366                             patchModuleValue.append(root).append(PS);
367                         }
368                     }
369 
370                     compilerArgs.add(patchModuleValue.toString());
371                 } else {
372                     getLog().debug("Black-box testing - all is ready to compile");
373                 }
374             } else {
375                 // No main binaries available? Means we're a test-only project.
376                 if (!Files.exists(mainOutputDirectory)) {
377                     return;
378                 }
379                 // very odd
380                 // Means that main sources must be compiled with -modulesource and -Xmodule:<moduleName>
381                 // However, this has a huge impact since you can't simply use it as a classpathEntry
382                 // due to extra folder in between
383                 throw new UnsupportedOperationException(
384                         "Can't compile test sources " + "when main sources are missing a module descriptor");
385             }
386         } else {
387             if (mainModuleDescriptor != null) {
388                 if (compilerArgs == null) {
389                     compilerArgs = new ArrayList<>();
390                 }
391                 compilerArgs.add("--patch-module");
392 
393                 StringBuilder patchModuleValue = new StringBuilder(mainModuleDescriptor.name())
394                         .append('=')
395                         .append(mainOutputDirectory)
396                         .append(PS);
397                 for (Path root : getCompileSourceRoots()) {
398                     patchModuleValue.append(root).append(PS);
399                 }
400 
401                 compilerArgs.add(patchModuleValue.toString());
402 
403                 compilerArgs.add("--add-reads");
404                 compilerArgs.add(mainModuleDescriptor.name() + "=ALL-UNNAMED");
405             } else {
406                 modulepathElements = Collections.emptyList();
407                 classpathElements = testPath;
408             }
409         }
410     }
411 
412     protected SourceInclusionScanner getSourceInclusionScanner(int staleMillis) {
413         SourceInclusionScanner scanner;
414 
415         if (testIncludes.isEmpty() && testExcludes.isEmpty() && testIncrementalExcludes.isEmpty()) {
416             scanner = new StaleSourceScanner(staleMillis);
417         } else {
418             if (testIncludes.isEmpty()) {
419                 testIncludes.add("**/*.java");
420             }
421             Set<String> excludesIncr = new HashSet<>(testExcludes);
422             excludesIncr.addAll(this.testIncrementalExcludes);
423             scanner = new StaleSourceScanner(staleMillis, testIncludes, excludesIncr);
424         }
425 
426         return scanner;
427     }
428 
429     protected SourceInclusionScanner getSourceInclusionScanner(String inputFileEnding) {
430         SourceInclusionScanner scanner;
431 
432         // it's not defined if we get the ending with or without the dot '.'
433         String defaultIncludePattern = "**/*" + (inputFileEnding.startsWith(".") ? "" : ".") + inputFileEnding;
434 
435         if (testIncludes.isEmpty() && testExcludes.isEmpty() && testIncrementalExcludes.isEmpty()) {
436             testIncludes = Collections.singleton(defaultIncludePattern);
437             scanner = new SimpleSourceInclusionScanner(testIncludes, Collections.emptySet());
438         } else {
439             if (testIncludes.isEmpty()) {
440                 testIncludes.add(defaultIncludePattern);
441             }
442             Set<String> excludesIncr = new HashSet<>(testExcludes);
443             excludesIncr.addAll(this.testIncrementalExcludes);
444             scanner = new SimpleSourceInclusionScanner(testIncludes, excludesIncr);
445         }
446 
447         return scanner;
448     }
449 
450     protected String getSource() {
451         return testSource == null ? source : testSource;
452     }
453 
454     protected String getTarget() {
455         return testTarget == null ? target : testTarget;
456     }
457 
458     @Override
459     protected String getRelease() {
460         return testRelease == null ? release : testRelease;
461     }
462 
463     protected String getCompilerArgument() {
464         return testCompilerArgument == null ? compilerArgument : testCompilerArgument;
465     }
466 
467     protected Path getGeneratedSourcesDirectory() {
468         return generatedTestSourcesDirectory;
469     }
470 
471     @Override
472     protected String getDebugFileName() {
473         return debugFileName;
474     }
475 
476     @Override
477     protected boolean isTestCompile() {
478         return true;
479     }
480 
481     @Override
482     protected Set<String> getIncludes() {
483         return testIncludes;
484     }
485 
486     @Override
487     protected Set<String> getExcludes() {
488         return testExcludes;
489     }
490 }