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     protected void preparePaths(Set<File> sourceFiles) {
235         File mainOutputDirectory = new File(getProject().getBuild().getOutputDirectory());
236 
237         File mainModuleDescriptorClassFile = new File(mainOutputDirectory, "module-info.class");
238         JavaModuleDescriptor mainModuleDescriptor = null;
239 
240         File testModuleDescriptorJavaFile = new File("module-info.java");
241         JavaModuleDescriptor testModuleDescriptor = null;
242 
243         // Go through the source files to respect includes/excludes
244         for (File sourceFile : sourceFiles) {
245             // @todo verify if it is the root of a sourcedirectory?
246             if ("module-info.java".equals(sourceFile.getName())) {
247                 testModuleDescriptorJavaFile = sourceFile;
248                 break;
249             }
250         }
251 
252         // Get additional information from the main module descriptor, if available
253         if (mainModuleDescriptorClassFile.exists()) {
254             ResolvePathsResult<String> result;
255 
256             try {
257                 ResolvePathsRequest<String> request = ResolvePathsRequest.ofStrings(testPath)
258                         .setIncludeStatic(true)
259                         .setMainModuleDescriptor(mainModuleDescriptorClassFile.getAbsolutePath());
260 
261                 Toolchain toolchain = getToolchain();
262                 if (toolchain instanceof DefaultJavaToolChain) {
263                     request.setJdkHome(((DefaultJavaToolChain) toolchain).getJavaHome());
264                 }
265 
266                 result = locationManager.resolvePaths(request);
267 
268                 for (Entry<String, Exception> pathException :
269                         result.getPathExceptions().entrySet()) {
270                     Throwable cause = pathException.getValue();
271                     while (cause.getCause() != null) {
272                         cause = cause.getCause();
273                     }
274                     String fileName =
275                             Paths.get(pathException.getKey()).getFileName().toString();
276                     getLog().warn("Can't extract module name from " + fileName + ": " + cause.getMessage());
277                 }
278             } catch (IOException e) {
279                 throw new RuntimeException(e);
280             }
281 
282             mainModuleDescriptor = result.getMainModuleDescriptor();
283 
284             pathElements = new LinkedHashMap<>(result.getPathElements().size());
285             pathElements.putAll(result.getPathElements());
286 
287             modulepathElements = result.getModulepathElements().keySet();
288             classpathElements = result.getClasspathElements();
289         }
290 
291         // Get additional information from the test module descriptor, if available
292         if (testModuleDescriptorJavaFile.exists()) {
293             ResolvePathsResult<String> result;
294 
295             try {
296                 ResolvePathsRequest<String> request = ResolvePathsRequest.ofStrings(testPath)
297                         .setMainModuleDescriptor(testModuleDescriptorJavaFile.getAbsolutePath());
298 
299                 Toolchain toolchain = getToolchain();
300                 if (toolchain instanceof DefaultJavaToolChain) {
301                     request.setJdkHome(((DefaultJavaToolChain) toolchain).getJavaHome());
302                 }
303 
304                 result = locationManager.resolvePaths(request);
305             } catch (IOException e) {
306                 throw new RuntimeException(e);
307             }
308 
309             testModuleDescriptor = result.getMainModuleDescriptor();
310         }
311 
312         if (!useModulePath) {
313             pathElements = Collections.emptyMap();
314             modulepathElements = Collections.emptyList();
315             classpathElements = testPath;
316             return;
317         }
318         if (StringUtils.isNotEmpty(getRelease())) {
319             if (Integer.parseInt(getRelease()) < 9) {
320                 pathElements = Collections.emptyMap();
321                 modulepathElements = Collections.emptyList();
322                 classpathElements = testPath;
323                 return;
324             }
325         } else if (Double.parseDouble(getTarget()) < Double.parseDouble(MODULE_INFO_TARGET)) {
326             pathElements = Collections.emptyMap();
327             modulepathElements = Collections.emptyList();
328             classpathElements = testPath;
329             return;
330         }
331 
332         if (testModuleDescriptor != null) {
333             modulepathElements = testPath;
334             classpathElements = Collections.emptyList();
335 
336             if (mainModuleDescriptor != null) {
337                 if (getLog().isDebugEnabled()) {
338                     getLog().debug("Main and test module descriptors exist:");
339                     getLog().debug("  main module = " + mainModuleDescriptor.name());
340                     getLog().debug("  test module = " + testModuleDescriptor.name());
341                 }
342 
343                 if (testModuleDescriptor.name().equals(mainModuleDescriptor.name())) {
344                     if (compilerArgs == null) {
345                         compilerArgs = new ArrayList<>();
346                     }
347                     compilerArgs.add("--patch-module");
348 
349                     StringBuilder patchModuleValue = new StringBuilder();
350                     patchModuleValue.append(testModuleDescriptor.name());
351                     patchModuleValue.append('=');
352 
353                     for (String root : getProject().getCompileSourceRoots()) {
354                         if (Files.exists(Paths.get(root))) {
355                             patchModuleValue.append(root).append(PS);
356                         }
357                     }
358 
359                     compilerArgs.add(patchModuleValue.toString());
360                 } else {
361                     getLog().debug("Black-box testing - all is ready to compile");
362                 }
363             } else {
364                 // No main binaries available? Means we're a test-only project.
365                 if (!mainOutputDirectory.exists()) {
366                     return;
367                 }
368                 // very odd
369                 // Means that main sources must be compiled with -modulesource and -Xmodule:<moduleName>
370                 // However, this has a huge impact since you can't simply use it as a classpathEntry
371                 // due to extra folder in between
372                 throw new UnsupportedOperationException(
373                         "Can't compile test sources " + "when main sources are missing a module descriptor");
374             }
375         } else {
376             if (mainModuleDescriptor != null) {
377                 if (compilerArgs == null) {
378                     compilerArgs = new ArrayList<>();
379                 }
380                 compilerArgs.add("--patch-module");
381 
382                 StringBuilder patchModuleValue = new StringBuilder(mainModuleDescriptor.name())
383                         .append('=')
384                         .append(mainOutputDirectory)
385                         .append(PS);
386                 for (String root : compileSourceRoots) {
387                     patchModuleValue.append(root).append(PS);
388                 }
389 
390                 compilerArgs.add(patchModuleValue.toString());
391 
392                 compilerArgs.add("--add-reads");
393                 compilerArgs.add(mainModuleDescriptor.name() + "=ALL-UNNAMED");
394             } else {
395                 modulepathElements = Collections.emptyList();
396                 classpathElements = testPath;
397             }
398         }
399     }
400 
401     protected SourceInclusionScanner getSourceInclusionScanner(int staleMillis) {
402         SourceInclusionScanner scanner;
403 
404         if (testIncludes.isEmpty() && testExcludes.isEmpty() && testIncrementalExcludes.isEmpty()) {
405             scanner = new StaleSourceScanner(staleMillis);
406         } else {
407             if (testIncludes.isEmpty()) {
408                 testIncludes.add("**/*.java");
409             }
410             Set<String> excludesIncr = new HashSet<>(testExcludes);
411             excludesIncr.addAll(this.testIncrementalExcludes);
412             scanner = new StaleSourceScanner(staleMillis, testIncludes, excludesIncr);
413         }
414 
415         return scanner;
416     }
417 
418     protected SourceInclusionScanner getSourceInclusionScanner(String inputFileEnding) {
419         SourceInclusionScanner scanner;
420 
421         // it's not defined if we get the ending with or without the dot '.'
422         String defaultIncludePattern = "**/*" + (inputFileEnding.startsWith(".") ? "" : ".") + inputFileEnding;
423 
424         if (testIncludes.isEmpty() && testExcludes.isEmpty() && testIncrementalExcludes.isEmpty()) {
425             testIncludes = Collections.singleton(defaultIncludePattern);
426             scanner = new SimpleSourceInclusionScanner(testIncludes, Collections.emptySet());
427         } else {
428             if (testIncludes.isEmpty()) {
429                 testIncludes.add(defaultIncludePattern);
430             }
431             Set<String> excludesIncr = new HashSet<>(testExcludes);
432             excludesIncr.addAll(this.testIncrementalExcludes);
433             scanner = new SimpleSourceInclusionScanner(testIncludes, excludesIncr);
434         }
435 
436         return scanner;
437     }
438 
439     protected String getSource() {
440         return testSource == null ? source : testSource;
441     }
442 
443     protected String getTarget() {
444         return testTarget == null ? target : testTarget;
445     }
446 
447     @Override
448     protected String getRelease() {
449         return testRelease == null ? release : testRelease;
450     }
451 
452     protected String getCompilerArgument() {
453         return testCompilerArgument == null ? compilerArgument : testCompilerArgument;
454     }
455 
456     protected Map<String, String> getCompilerArguments() {
457         return testCompilerArguments == null ? compilerArguments : testCompilerArguments;
458     }
459 
460     protected File getGeneratedSourcesDirectory() {
461         return generatedTestSourcesDirectory;
462     }
463 
464     @Override
465     protected String getDebugFileName() {
466         return debugFileName;
467     }
468 
469     @Override
470     protected boolean isTestCompile() {
471         return true;
472     }
473 
474     @Override
475     protected Set<String> getIncludes() {
476         return testIncludes;
477     }
478 
479     @Override
480     protected Set<String> getExcludes() {
481         return testExcludes;
482     }
483 }