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