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