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     final LocationManager locationManager = new LocationManager();
160 
161     private Map<String, JavaModuleDescriptor> pathElements;
162     
163     private Collection<String> classpathElements;
164 
165     private Collection<String> modulepathElements;
166 
167     public void execute()
168         throws MojoExecutionException, CompilationFailureException
169     {
170         if ( skip )
171         {
172             getLog().info( "Not compiling test sources" );
173             return;
174         }
175         super.execute();
176     }
177 
178     protected List<String> getCompileSourceRoots()
179     {
180         return compileSourceRoots;
181     }
182 
183     @Override
184     protected Map<String, JavaModuleDescriptor> getPathElements()
185     {
186         return pathElements;
187     }
188 
189     protected List<String> getClasspathElements()
190     {
191         return new ArrayList<>( classpathElements );
192     }
193 
194     @Override
195     protected List<String> getModulepathElements()
196     {
197         return new ArrayList<>( modulepathElements );
198     }
199 
200     protected File getOutputDirectory()
201     {
202         return outputDirectory;
203     }
204 
205     @Override
206     protected void preparePaths( Set<File> sourceFiles )
207     {
208         File mainOutputDirectory = new File( getProject().getBuild().getOutputDirectory() );
209 
210         File mainModuleDescriptorClassFile = new File( mainOutputDirectory, "module-info.class" );
211         JavaModuleDescriptor mainModuleDescriptor = null;
212 
213         File testModuleDescriptorJavaFile = new File( "module-info.java" );
214         JavaModuleDescriptor testModuleDescriptor = null;
215 
216         // Go through the source files to respect includes/excludes
217         for ( File sourceFile : sourceFiles )
218         {
219             // @todo verify if it is the root of a sourcedirectory?
220             if ( "module-info.java".equals( sourceFile.getName() ) ) 
221             {
222                 testModuleDescriptorJavaFile = sourceFile;
223                 break;
224             }
225         }
226 
227         // Get additional information from the main module descriptor, if available
228         if ( mainModuleDescriptorClassFile.exists() )
229         {
230             ResolvePathsResult<String> result;
231 
232             try
233             {
234                 ResolvePathsRequest<String> request =
235                         ResolvePathsRequest.ofStrings( testPath )
236                                 .setMainModuleDescriptor( mainModuleDescriptorClassFile.getAbsolutePath() );
237 
238                 Toolchain toolchain = getToolchain();
239                 if ( toolchain instanceof DefaultJavaToolChain )
240                 {
241                     request.setJdkHome( ( (DefaultJavaToolChain) toolchain ).getJavaHome() );
242                 }
243 
244                 result = locationManager.resolvePaths( request );
245                 
246                 for ( Entry<String, Exception> pathException : result.getPathExceptions().entrySet() )
247                 {
248                     Throwable cause = pathException.getValue();
249                     while ( cause.getCause() != null )
250                     {
251                         cause = cause.getCause();
252                     }
253                     String fileName = Paths.get( pathException.getKey() ).getFileName().toString();
254                     getLog().warn( "Can't extract module name from " + fileName + ": " + cause.getMessage() );
255                 }
256             }
257             catch ( IOException e )
258             {
259                 throw new RuntimeException( e );
260             }
261 
262             mainModuleDescriptor = result.getMainModuleDescriptor();
263 
264             pathElements = new LinkedHashMap<>( result.getPathElements().size() );
265             pathElements.putAll( result.getPathElements() );
266 
267             modulepathElements = result.getModulepathElements().keySet();
268             classpathElements = result.getClasspathElements();
269         }
270 
271         // Get additional information from the test module descriptor, if available
272         if ( testModuleDescriptorJavaFile.exists() )
273         {
274             ResolvePathsResult<String> result;
275 
276             try
277             {
278                 ResolvePathsRequest<String> request =
279                         ResolvePathsRequest.ofStrings( testPath )
280                                 .setMainModuleDescriptor( testModuleDescriptorJavaFile.getAbsolutePath() );
281 
282                 Toolchain toolchain = getToolchain();
283                 if ( toolchain instanceof DefaultJavaToolChain )
284                 {
285                     request.setJdkHome( ( (DefaultJavaToolChain) toolchain ).getJavaHome() );
286                 }
287 
288                 result = locationManager.resolvePaths( request );
289             }
290             catch ( IOException e )
291             {
292                 throw new RuntimeException( e );
293             }
294 
295             testModuleDescriptor = result.getMainModuleDescriptor();
296         }
297 
298         if ( release != null )
299         {
300             if ( Integer.valueOf( release ) < 9 )
301             {
302                 pathElements = Collections.emptyMap();
303                 modulepathElements = Collections.emptyList();
304                 classpathElements = testPath;
305                 return;
306             }
307         }
308         else if ( Double.valueOf( getTarget() ) < Double.valueOf( MODULE_INFO_TARGET ) )
309         {
310             pathElements = Collections.emptyMap();
311             modulepathElements = Collections.emptyList();
312             classpathElements = testPath;
313             return;
314         }
315             
316         if ( testModuleDescriptor != null )
317         {
318             modulepathElements = testPath;
319             classpathElements = Collections.emptyList();
320 
321             if ( mainModuleDescriptor != null )
322             {
323                 if ( getLog().isDebugEnabled() )
324                 {
325                     getLog().debug( "Main and test module descriptors exist:" );
326                     getLog().debug( "  main module = " + mainModuleDescriptor.name() );
327                     getLog().debug( "  test module = " + testModuleDescriptor.name() );
328                 }
329 
330                 if ( testModuleDescriptor.name().equals( mainModuleDescriptor.name() ) )
331                 {
332                     if ( compilerArgs == null )
333                     {
334                         compilerArgs = new ArrayList<>();
335                     }
336                     compilerArgs.add( "--patch-module" );
337 
338                     StringBuilder patchModuleValue = new StringBuilder();
339                     patchModuleValue.append( testModuleDescriptor.name() );
340                     patchModuleValue.append( '=' );
341 
342                     for ( String root : getProject().getCompileSourceRoots() )
343                     {
344                         if ( Files.exists( Paths.get( root ) ) )
345                         {
346                             patchModuleValue.append( root ).append( PS );
347                         }
348                     }
349 
350                     compilerArgs.add( patchModuleValue.toString() );
351                 }
352                 else
353                 {
354                     getLog().debug( "Black-box testing - all is ready to compile" );
355                 }
356             }
357             else
358             {
359                 // No main binaries available? Means we're a test-only project.
360                 if ( !mainOutputDirectory.exists() )
361                 {
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( "Can't compile test sources "
369                     + "when main sources are missing a module descriptor" );
370             }
371         }
372         else
373         {
374             if ( mainModuleDescriptor != null )
375             {
376                 if ( compilerArgs == null )
377                 {
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                 {
388                     patchModuleValue.append( root ).append( PS );
389                 }
390                 
391                 compilerArgs.add( patchModuleValue.toString() );
392                 
393                 compilerArgs.add( "--add-reads" );
394                 compilerArgs.add( mainModuleDescriptor.name() + "=ALL-UNNAMED" );
395             }
396             else
397             {
398                 modulepathElements = Collections.emptyList();
399                 classpathElements = testPath;
400             }
401         }
402     }
403 
404     protected SourceInclusionScanner getSourceInclusionScanner( int staleMillis )
405     {
406         SourceInclusionScanner scanner;
407 
408         if ( testIncludes.isEmpty() && testExcludes.isEmpty() )
409         {
410             scanner = new StaleSourceScanner( staleMillis );
411         }
412         else
413         {
414             if ( testIncludes.isEmpty() )
415             {
416                 testIncludes.add( "**/*.java" );
417             }
418             scanner = new StaleSourceScanner( staleMillis, testIncludes, testExcludes );
419         }
420 
421         return scanner;
422     }
423 
424     protected SourceInclusionScanner getSourceInclusionScanner( String inputFileEnding )
425     {
426         SourceInclusionScanner scanner;
427 
428         // it's not defined if we get the ending with or without the dot '.'
429         String defaultIncludePattern = "**/*" + ( inputFileEnding.startsWith( "." ) ? "" : "." ) + inputFileEnding;
430 
431         if ( testIncludes.isEmpty() && testExcludes.isEmpty() )
432         {
433             testIncludes = Collections.singleton( defaultIncludePattern );
434             scanner = new SimpleSourceInclusionScanner( testIncludes, Collections.<String>emptySet() );
435         }
436         else
437         {
438             if ( testIncludes.isEmpty() )
439             {
440                 testIncludes.add( defaultIncludePattern );
441             }
442             scanner = new SimpleSourceInclusionScanner( testIncludes, testExcludes );
443         }
444 
445         return scanner;
446     }
447 
448     protected String getSource()
449     {
450         return testSource == null ? source : testSource;
451     }
452 
453     protected String getTarget()
454     {
455         return testTarget == null ? target : testTarget;
456     }
457     
458     @Override
459     protected String getRelease()
460     {
461         return testRelease == null ? release : testRelease;
462     }
463 
464     protected String getCompilerArgument()
465     {
466         return testCompilerArgument == null ? compilerArgument : testCompilerArgument;
467     }
468 
469     protected Map<String, String> getCompilerArguments()
470     {
471         return testCompilerArguments == null ? compilerArguments : testCompilerArguments;
472     }
473 
474     protected File getGeneratedSourcesDirectory()
475     {
476         return generatedTestSourcesDirectory;
477     }
478 
479     @Override
480     protected boolean isTestCompile()
481     {
482         return true;
483     }
484 
485 }