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.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.Collections;
26  import java.util.HashSet;
27  import java.util.LinkedHashMap;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Set;
31  
32  import org.apache.maven.plugin.MojoExecutionException;
33  import org.apache.maven.plugin.compiler.ModuleInfoParser.Type;
34  import org.apache.maven.plugins.annotations.Component;
35  import org.apache.maven.plugins.annotations.LifecyclePhase;
36  import org.apache.maven.plugins.annotations.Mojo;
37  import org.apache.maven.plugins.annotations.Parameter;
38  import org.apache.maven.plugins.annotations.ResolutionScope;
39  import org.codehaus.plexus.compiler.util.scan.SimpleSourceInclusionScanner;
40  import org.codehaus.plexus.compiler.util.scan.SourceInclusionScanner;
41  import org.codehaus.plexus.compiler.util.scan.StaleSourceScanner;
42  
43  /**
44   * Compiles application test sources.
45   *
46   * @author <a href="mailto:jason@maven.org">Jason van Zyl</a>
47   * @version $Id: TestCompilerMojo.html 1004801 2017-01-13 11:24:25Z rfscholte $
48   * @since 2.0
49   */
50  @Mojo( name = "testCompile", defaultPhase = LifecyclePhase.TEST_COMPILE, threadSafe = true,
51                  requiresDependencyResolution = ResolutionScope.TEST )
52  public class TestCompilerMojo
53      extends AbstractCompilerMojo
54  {
55      /**
56       * Set this to 'true' to bypass compilation of test sources.
57       * Its use is NOT RECOMMENDED, but quite convenient on occasion.
58       */
59      @Parameter ( property = "maven.test.skip" )
60      private boolean skip;
61  
62      /**
63       * The source directories containing the test-source to be compiled.
64       */
65      @Parameter ( defaultValue = "${project.testCompileSourceRoots}", readonly = true, required = true )
66      private List<String> compileSourceRoots;
67  
68      /**
69       * The directory where compiled test classes go.
70       */
71      @Parameter ( defaultValue = "${project.build.testOutputDirectory}", required = true, readonly = true )
72      private File outputDirectory;
73  
74      /**
75       * A list of inclusion filters for the compiler.
76       */
77      @Parameter
78      private Set<String> testIncludes = new HashSet<String>();
79  
80      /**
81       * A list of exclusion filters for the compiler.
82       */
83      @Parameter
84      private Set<String> testExcludes = new HashSet<String>();
85  
86      /**
87       * The -source argument for the test Java compiler.
88       *
89       * @since 2.1
90       */
91      @Parameter ( property = "maven.compiler.testSource" )
92      private String testSource;
93  
94      /**
95       * The -target argument for the test Java compiler.
96       *
97       * @since 2.1
98       */
99      @Parameter ( property = "maven.compiler.testTarget" )
100     private String testTarget;
101 
102     /**
103      * the -release argument for the test Java compiler
104      * 
105      * @since 3.6
106      */
107     @Parameter ( property = "maven.compiler.testRelease" )
108     private String testRelease;
109 
110     /**
111      * <p>
112      * Sets the arguments to be passed to test compiler (prepending a dash) if fork is set to true.
113      * </p>
114      * <p>
115      * This is because the list of valid arguments passed to a Java compiler
116      * varies based on the compiler version.
117      * </p>
118      *
119      * @since 2.1
120      */
121     @Parameter
122     private Map<String, String> testCompilerArguments;
123 
124     /**
125      * <p>
126      * Sets the unformatted argument string to be passed to test compiler if fork is set to true.
127      * </p>
128      * <p>
129      * This is because the list of valid arguments passed to a Java compiler
130      * varies based on the compiler version.
131      * </p>
132      *
133      * @since 2.1
134      */
135     @Parameter
136     private String testCompilerArgument;
137 
138     /**
139      * <p>
140      * Specify where to place generated source files created by annotation processing.
141      * Only applies to JDK 1.6+
142      * </p>
143      *
144      * @since 2.2
145      */
146     @Parameter ( defaultValue = "${project.build.directory}/generated-test-sources/test-annotations" )
147     private File generatedTestSourcesDirectory;
148 
149     @Parameter( defaultValue = "${project.compileClasspathElements}", readonly = true )
150     private List<String> compilePath;
151 
152     @Parameter( defaultValue = "${project.testClasspathElements}", readonly = true )
153     private List<String> testPath;
154     
155     @Component
156     private Map<String, ModuleInfoParser> moduleInfoParsers;
157 
158     private List<String> classpathElements;
159 
160     private List<String> modulepathElements;
161 
162     public void execute()
163         throws MojoExecutionException, CompilationFailureException
164     {
165         if ( skip )
166         {
167             getLog().info( "Not compiling test sources" );
168             return;
169         }
170         super.execute();
171     }
172 
173     protected List<String> getCompileSourceRoots()
174     {
175         return compileSourceRoots;
176     }
177 
178     protected List<String> getClasspathElements()
179     {
180         return classpathElements;
181     }
182     
183     @Override
184     protected List<String> getModulepathElements()
185     {
186         return modulepathElements;
187     }
188 
189     protected File getOutputDirectory()
190     {
191         return outputDirectory;
192     }
193 
194     @Override
195     protected void preparePaths( Set<File> sourceFiles )
196     {
197         File mainOutputDirectory = new File( getProject().getBuild().getOutputDirectory() );
198 
199         boolean hasMainModuleDescriptor = new File( mainOutputDirectory, "module-info.class" ).exists();
200         
201         boolean hasTestModuleDescriptor = false;
202         
203         // Go through the source files to respect includes/excludes 
204         for ( File sourceFile : sourceFiles )
205         {
206             // @todo verify if it is the root of a sourcedirectory?
207             if ( "module-info.java".equals( sourceFile.getName() ) ) 
208             {
209                 hasTestModuleDescriptor = true;
210                 break;
211             }
212         }
213         
214         List<String> testScopedElements = new ArrayList<String>( testPath );
215         testScopedElements.removeAll( compilePath );
216         
217         if ( release != null )
218         {
219             if ( Integer.valueOf( release ) < 9 )
220             {
221                 modulepathElements = Collections.emptyList();
222                 classpathElements = testPath;
223                 return;
224             }
225         }
226         else if ( Double.valueOf( getTarget() ) < Double.valueOf( MODULE_INFO_TARGET ) )
227         {
228             modulepathElements = Collections.emptyList();
229             classpathElements = testPath;
230             return;
231         }
232             
233         if ( hasTestModuleDescriptor )
234         {
235             modulepathElements = testPath;
236             classpathElements = Collections.emptyList();
237 
238             if ( hasMainModuleDescriptor )
239             {
240                 // maybe some extra analysis required
241             }
242             else
243             {
244                 // very odd
245                 // Means that main sources must be compiled with -modulesource and -Xmodule:<moduleName>
246                 // However, this has a huge impact since you can't simply use it as a classpathEntry 
247                 // due to extra folder in between
248                 throw new UnsupportedOperationException( "Can't compile test sources "
249                     + "when main sources are missing a module descriptor" );
250             }
251         }
252         else
253         {
254             if ( hasMainModuleDescriptor )
255             {
256                 modulepathElements = compilePath;
257                 classpathElements = testScopedElements;
258                 if ( compilerArgs == null )
259                 {
260                     compilerArgs = new ArrayList<String>();
261                 }
262                 
263                 String moduleName = null;
264                 
265                 Map<String, Exception> exceptionMap = new LinkedHashMap<String, Exception>( moduleInfoParsers.size() ); 
266                 
267                 // Prefer ASM over QDox, since we're missing the info where the module-info.class is coming from. 
268                 // With QDox it is just the best possible guess 
269                 List<String> parserKeys = Arrays.asList( "asm", "qdox" );
270                 
271                 // The class format is still changing, for that reason provide multiple strategies to parse module-info 
272                 for ( String parserKey: parserKeys )
273                 {
274                     ModuleInfoParser parser = moduleInfoParsers.get( parserKey );
275 
276                     File modulePath = null;
277                     if ( Type.CLASS.equals( parser.getType() ) )
278                     {
279                         modulePath = mainOutputDirectory;
280                     }
281                     else if ( Type.SOURCE.equals( parser.getType() ) )
282                     {
283                         for ( String compileSourceRoot : getProject().getCompileSourceRoots() )
284                         {
285                             File sourceRoot = new File( compileSourceRoot );
286                             
287                             if ( new File( sourceRoot, "module-info.java" ).exists() )
288                             {
289                                 modulePath = sourceRoot;
290                             }
291                         }
292                     }
293                     else
294                     {
295                         throw new RuntimeException( "Unmapped type: " + parser.getType()  );
296                     }
297                     
298                     try
299                     {
300                         moduleName = parser.getModuleName( modulePath  );
301                         
302                         if ( moduleName != null )
303                         {
304                             break;
305                         }
306                     }
307                     catch ( Exception e )
308                     {
309                         exceptionMap.put( parserKey, e );
310                     }
311                 }
312                 
313                 if ( moduleName == null )
314                 {
315                     getLog().error( "Failed to parse module-info:" );
316                     
317                     for ( Map.Entry<String, Exception> exception : exceptionMap.entrySet() )
318                     {
319                         getLog().error( "With " + exception.getKey() + ": " + exception.getValue().getMessage() );
320                     }
321                     
322                     throw new RuntimeException( "Failed to parse module-info" );
323                 }
324                 
325                 compilerArgs.add( "-Xmodule:" + moduleName );
326                 compilerArgs.add( "--add-reads" );
327                 compilerArgs.add( moduleName + "=ALL-UNNAMED" );
328             }
329             else
330             {
331                 modulepathElements = Collections.emptyList();
332                 classpathElements = testPath;
333             }
334         }
335     }
336 
337     protected SourceInclusionScanner getSourceInclusionScanner( int staleMillis )
338     {
339         SourceInclusionScanner scanner;
340 
341         if ( testIncludes.isEmpty() && testExcludes.isEmpty() )
342         {
343             scanner = new StaleSourceScanner( staleMillis );
344         }
345         else
346         {
347             if ( testIncludes.isEmpty() )
348             {
349                 testIncludes.add( "**/*.java" );
350             }
351             scanner = new StaleSourceScanner( staleMillis, testIncludes, testExcludes );
352         }
353 
354         return scanner;
355     }
356 
357     protected SourceInclusionScanner getSourceInclusionScanner( String inputFileEnding )
358     {
359         SourceInclusionScanner scanner;
360 
361         // it's not defined if we get the ending with or without the dot '.'
362         String defaultIncludePattern = "**/*" + ( inputFileEnding.startsWith( "." ) ? "" : "." ) + inputFileEnding;
363 
364         if ( testIncludes.isEmpty() && testExcludes.isEmpty() )
365         {
366             testIncludes = Collections.singleton( defaultIncludePattern );
367             scanner = new SimpleSourceInclusionScanner( testIncludes, Collections.<String>emptySet() );
368         }
369         else
370         {
371             if ( testIncludes.isEmpty() )
372             {
373                 testIncludes.add( defaultIncludePattern );
374             }
375             scanner = new SimpleSourceInclusionScanner( testIncludes, testExcludes );
376         }
377 
378         return scanner;
379     }
380 
381     protected String getSource()
382     {
383         return testSource == null ? source : testSource;
384     }
385 
386     protected String getTarget()
387     {
388         return testTarget == null ? target : testTarget;
389     }
390     
391     @Override
392     protected String getRelease()
393     {
394         return testRelease == null ? release : testRelease;
395     }
396 
397     protected String getCompilerArgument()
398     {
399         return testCompilerArgument == null ? compilerArgument : testCompilerArgument;
400     }
401 
402     protected Map<String, String> getCompilerArguments()
403     {
404         return testCompilerArguments == null ? compilerArguments : testCompilerArguments;
405     }
406 
407     protected File getGeneratedSourcesDirectory()
408     {
409         return generatedTestSourcesDirectory;
410     }
411 
412     @Override
413     protected boolean isTestCompile()
414     {
415         return true;
416     }
417 
418 }