View Javadoc
1   package org.apache.maven.plugin.surefire.booterclient;
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 org.apache.maven.plugin.surefire.AbstractSurefireMojo;
23  import org.apache.maven.plugin.surefire.JdkAttributes;
24  import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.OutputStreamFlushableCommandline;
25  import org.apache.maven.plugin.surefire.util.Relocator;
26  import org.apache.maven.surefire.booter.Classpath;
27  import org.apache.maven.surefire.booter.ForkedBooter;
28  import org.apache.maven.surefire.booter.StartupConfiguration;
29  import org.apache.maven.surefire.booter.SurefireBooterForkException;
30  import org.apache.maven.surefire.util.internal.ImmutableMap;
31  
32  import java.io.File;
33  import java.io.FileOutputStream;
34  import java.io.IOException;
35  import java.util.Collections;
36  import java.util.Iterator;
37  import java.util.List;
38  import java.util.Map;
39  import java.util.Properties;
40  import java.util.jar.JarEntry;
41  import java.util.jar.JarOutputStream;
42  import java.util.jar.Manifest;
43  
44  import static org.apache.maven.plugin.surefire.SurefireHelper.escapeToPlatformPath;
45  import static org.apache.maven.shared.utils.StringUtils.join;
46  
47  /**
48   * Configuration for forking tests.
49   *
50   * @author <a href="mailto:brett@apache.org">Brett Porter</a>
51   * @author <a href="mailto:kenney@apache.org">Kenney Westerhof</a>
52   * @author <a href="mailto:krosenvold@apache.org">Kristian Rosenvold</a>
53   */
54  public class ForkConfiguration
55  {
56      public static final String FORK_ONCE = "once";
57  
58      public static final String FORK_ALWAYS = "always";
59  
60      public static final String FORK_NEVER = "never";
61  
62      public static final String FORK_PERTHREAD = "perthread";
63  
64      private final int forkCount;
65  
66      private final boolean reuseForks;
67  
68      private final Classpath bootClasspathConfiguration;
69  
70      private final JdkAttributes jdk;
71  
72      private final Properties modelProperties;
73  
74      private final String argLine;
75  
76      private final Map<String, String> environmentVariables;
77  
78      private final File workingDirectory;
79  
80      private final File tempDirectory;
81  
82      private final boolean debug;
83  
84      private final String debugLine;
85  
86      private final Platform pluginPlatform;
87  
88      @SuppressWarnings( "checkstyle:parameternumber" )
89      public ForkConfiguration( Classpath bootClasspathConfiguration, File tmpDir, String debugLine,
90                                JdkAttributes jdk, File workingDirectory, Properties modelProperties, String argLine,
91                                Map<String, String> environmentVariables, boolean debugEnabled, int forkCount,
92                                boolean reuseForks, Platform pluginPlatform )
93      {
94          this.bootClasspathConfiguration = bootClasspathConfiguration;
95          this.tempDirectory = tmpDir;
96          this.debugLine = debugLine;
97          this.jdk = jdk;
98          this.workingDirectory = workingDirectory;
99          this.modelProperties = modelProperties;
100         this.argLine = argLine;
101         this.environmentVariables = toImmutable( environmentVariables );
102         this.debug = debugEnabled;
103         this.forkCount = forkCount;
104         this.reuseForks = reuseForks;
105         this.pluginPlatform = pluginPlatform;
106     }
107 
108     public Classpath getBootClasspath()
109     {
110         return bootClasspathConfiguration;
111     }
112 
113     public static String getEffectiveForkMode( String forkMode )
114     {
115         if ( "pertest".equalsIgnoreCase( forkMode ) )
116         {
117             return FORK_ALWAYS;
118         }
119         else if ( "none".equalsIgnoreCase( forkMode ) )
120         {
121             return FORK_NEVER;
122         }
123         else if ( forkMode.equals( FORK_NEVER ) || forkMode.equals( FORK_ONCE )
124                || forkMode.equals( FORK_ALWAYS ) || forkMode.equals( FORK_PERTHREAD ) )
125         {
126             return forkMode;
127         }
128         else
129         {
130             throw new IllegalArgumentException( "Fork mode " + forkMode + " is not a legal value" );
131         }
132     }
133 
134     /**
135      * @param classPath            cli the classpath arguments
136      * @param config               The startup configuration
137      * @param threadNumber         the thread number, to be the replacement in the argLine   @return A commandline
138      * @return CommandLine able to flush entire command going to be sent to forked JVM
139      * @throws org.apache.maven.surefire.booter.SurefireBooterForkException
140      *          when unable to perform the fork
141      */
142     public OutputStreamFlushableCommandline createCommandLine( List<String> classPath, StartupConfiguration config,
143                                                                int threadNumber )
144             throws SurefireBooterForkException
145     {
146         boolean useJar = config.getClassLoaderConfiguration().isManifestOnlyJarRequestedAndUsable();
147 
148         boolean shadefire = config.isShadefire();
149 
150         String providerThatHasMainMethod =
151                 config.isProviderMainClass() ? config.getActualClassName() : ForkedBooter.class.getName();
152 
153         return createCommandLine( classPath, useJar, shadefire, providerThatHasMainMethod, threadNumber );
154     }
155 
156     OutputStreamFlushableCommandline createCommandLine( List<String> classPath, boolean useJar, boolean shadefire,
157                                                         String providerThatHasMainMethod, int threadNumber )
158         throws SurefireBooterForkException
159     {
160         OutputStreamFlushableCommandline cli = new OutputStreamFlushableCommandline();
161 
162         cli.setExecutable( jdk.getJvmExecutable() );
163 
164         String jvmArgLine =
165                 replaceThreadNumberPlaceholder( stripNewLines( replacePropertyExpressions() ), threadNumber );
166 
167         if ( jdk.isJava9AtLeast() && !jvmArgLine.contains( "--add-modules" ) )
168         {
169             if ( jvmArgLine.isEmpty() )
170             {
171                 jvmArgLine = "--add-modules java.se.ee";
172             }
173             else
174             {
175                 jvmArgLine = "--add-modules java.se.ee " + jvmArgLine;
176             }
177         }
178 
179         if ( !jvmArgLine.isEmpty() )
180         {
181             cli.createArg().setLine( jvmArgLine );
182         }
183 
184         for ( Map.Entry<String, String> entry : environmentVariables.entrySet() )
185         {
186             String value = entry.getValue();
187             cli.addEnvironment( entry.getKey(), value == null ? "" : value );
188         }
189 
190         if ( getDebugLine() != null && !getDebugLine().isEmpty() )
191         {
192             cli.createArg().setLine( getDebugLine() );
193         }
194 
195         if ( useJar )
196         {
197             try
198             {
199                 File jarFile = createJar( classPath, providerThatHasMainMethod );
200                 cli.createArg().setValue( "-jar" );
201                 cli.createArg().setValue( escapeToPlatformPath( jarFile.getAbsolutePath() ) );
202             }
203             catch ( IOException e )
204             {
205                 throw new SurefireBooterForkException( "Error creating archive file", e );
206             }
207         }
208         else
209         {
210             cli.addEnvironment( "CLASSPATH", join( classPath.iterator(), File.pathSeparator ) );
211 
212             final String forkedBooter =
213                 providerThatHasMainMethod != null ? providerThatHasMainMethod : ForkedBooter.class.getName();
214 
215             cli.createArg().setValue( shadefire ? new Relocator().relocate( forkedBooter ) : forkedBooter );
216         }
217 
218         cli.setWorkingDirectory( getWorkingDirectory( threadNumber ).getAbsolutePath() );
219 
220         return cli;
221     }
222 
223     private File getWorkingDirectory( int threadNumber )
224         throws SurefireBooterForkException
225     {
226         File cwd = new File( replaceThreadNumberPlaceholder( workingDirectory.getAbsolutePath(), threadNumber ) );
227         if ( !cwd.exists() && !cwd.mkdirs() )
228         {
229             throw new SurefireBooterForkException( "Cannot create workingDirectory " + cwd.getAbsolutePath() );
230         }
231         if ( !cwd.isDirectory() )
232         {
233             throw new SurefireBooterForkException(
234                 "WorkingDirectory " + cwd.getAbsolutePath() + " exists and is not a directory" );
235         }
236         return cwd;
237     }
238 
239     private String replaceThreadNumberPlaceholder( String argLine, int threadNumber )
240     {
241         return argLine.replace( AbstractSurefireMojo.THREAD_NUMBER_PLACEHOLDER,
242                                 String.valueOf( threadNumber ) ).replace( AbstractSurefireMojo.FORK_NUMBER_PLACEHOLDER,
243                                                                           String.valueOf( threadNumber ) );
244     }
245 
246     /**
247      * Replaces expressions <pre>@{property-name}</pre> with the corresponding properties
248      * from the model. This allows late evaluation of property values when the plugin is executed (as compared
249      * to evaluation when the pom is parsed as is done with <pre>${property-name}</pre> expressions).
250      *
251      * This allows other plugins to modify or set properties with the changes getting picked up by surefire.
252      */
253     private String replacePropertyExpressions()
254     {
255         if ( argLine == null )
256         {
257             return "";
258         }
259 
260         String resolvedArgLine = argLine.trim();
261 
262         if ( resolvedArgLine.isEmpty() )
263         {
264             return "";
265         }
266 
267         for ( final String key : modelProperties.stringPropertyNames() )
268         {
269             String field = "@{" + key + "}";
270             if ( argLine.contains( field ) )
271             {
272                 resolvedArgLine = resolvedArgLine.replace( field, modelProperties.getProperty( key, "" ) );
273             }
274         }
275 
276         return resolvedArgLine;
277     }
278 
279     /**
280      * Create a jar with just a manifest containing a Main-Class entry for BooterConfiguration and a Class-Path entry
281      * for all classpath elements.
282      *
283      * @param classPath      List&lt;String&gt; of all classpath elements.
284      * @param startClassName  The classname to start (main-class)
285      * @return The file pointint to the jar
286      * @throws java.io.IOException When a file operation fails.
287      */
288     private File createJar( List<String> classPath, String startClassName )
289         throws IOException
290     {
291         File file = File.createTempFile( "surefirebooter", ".jar", tempDirectory );
292         if ( !debug )
293         {
294             file.deleteOnExit();
295         }
296         FileOutputStream fos = new FileOutputStream( file );
297         JarOutputStream jos = new JarOutputStream( fos );
298         try
299         {
300             jos.setLevel( JarOutputStream.STORED );
301             JarEntry je = new JarEntry( "META-INF/MANIFEST.MF" );
302             jos.putNextEntry( je );
303 
304             Manifest man = new Manifest();
305 
306             // we can't use StringUtils.join here since we need to add a '/' to
307             // the end of directory entries - otherwise the jvm will ignore them.
308             StringBuilder cp = new StringBuilder();
309             for ( Iterator<String> it = classPath.iterator(); it.hasNext(); )
310             {
311                 File file1 = new File( it.next() );
312                 String uri = file1.toURI().toASCIIString();
313                 cp.append( uri );
314                 if ( file1.isDirectory() && !uri.endsWith( "/" ) )
315                 {
316                     cp.append( '/' );
317                 }
318 
319                 if ( it.hasNext() )
320                 {
321                     cp.append( ' ' );
322                 }
323             }
324 
325             man.getMainAttributes().putValue( "Manifest-Version", "1.0" );
326             man.getMainAttributes().putValue( "Class-Path", cp.toString().trim() );
327             man.getMainAttributes().putValue( "Main-Class", startClassName );
328 
329             man.write( jos );
330 
331             jos.closeEntry();
332             jos.flush();
333 
334             return file;
335         }
336         finally
337         {
338             jos.close();
339         }
340     }
341 
342     public boolean isDebug()
343     {
344         return debug;
345     }
346 
347     public String getDebugLine()
348     {
349         return debugLine;
350     }
351 
352     public File getTempDirectory()
353     {
354         return tempDirectory;
355     }
356 
357     public int getForkCount()
358     {
359         return forkCount;
360     }
361 
362     public boolean isReuseForks()
363     {
364         return reuseForks;
365     }
366 
367     public Platform getPluginPlatform()
368     {
369         return pluginPlatform;
370     }
371 
372     private static String stripNewLines( String argLine )
373     {
374         return argLine.replace( "\n", " " ).replace( "\r", " " );
375     }
376 
377     /**
378      * Immutable map.
379      *
380      * @param map    immutable map copies elements from <code>map</code>
381      * @param <K>    key type
382      * @param <V>    value type
383      * @return never returns null
384      */
385     private static <K, V> Map<K, V> toImmutable( Map<K, V> map )
386     {
387         return map == null ? Collections.<K, V>emptyMap() : new ImmutableMap<K, V>( map );
388     }
389 }