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