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