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