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