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.booterclient.lazytestprovider.OutputStreamFlushableCommandline;
24  import org.apache.maven.plugin.surefire.util.Relocator;
25  import org.apache.maven.shared.utils.StringUtils;
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.UrlUtils;
31  
32  import java.io.File;
33  import java.io.FileOutputStream;
34  import java.io.IOException;
35  import java.util.Enumeration;
36  import java.util.List;
37  import java.util.Map;
38  import java.util.Properties;
39  import java.util.jar.JarEntry;
40  import java.util.jar.JarOutputStream;
41  import java.util.jar.Manifest;
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             for ( Map.Entry<String, String> entry : environmentVariables.entrySet() )
165             {
166                 String value = entry.getValue();
167                 cli.addEnvironment( entry.getKey(), value == null ? "" : value );
168             }
169         }
170 
171         if ( getDebugLine() != null && !"".equals( getDebugLine() ) )
172         {
173             cli.createArg().setLine( getDebugLine() );
174         }
175 
176         if ( useJar )
177         {
178             File jarFile;
179             try
180             {
181                 jarFile = createJar( classPath, providerThatHasMainMethod );
182             }
183             catch ( IOException e )
184             {
185                 throw new SurefireBooterForkException( "Error creating archive file", e );
186             }
187 
188             cli.createArg().setValue( "-jar" );
189 
190             cli.createArg().setValue( jarFile.getAbsolutePath() );
191         }
192         else
193         {
194             cli.addEnvironment( "CLASSPATH", StringUtils.join( classPath.iterator(), File.pathSeparator ) );
195 
196             final String forkedBooter =
197                 providerThatHasMainMethod != null ? providerThatHasMainMethod : ForkedBooter.class.getName();
198 
199             cli.createArg().setValue( shadefire ? new Relocator().relocate( forkedBooter ) : forkedBooter );
200         }
201 
202         cli.setWorkingDirectory( getWorkingDirectory( threadNumber ).getAbsolutePath() );
203 
204         return cli;
205     }
206 
207     private File getWorkingDirectory( int threadNumber )
208         throws SurefireBooterForkException
209     {
210         File cwd = new File( replaceThreadNumberPlaceholder( workingDirectory.getAbsolutePath(), threadNumber ) );
211         if ( !cwd.exists() && !cwd.mkdirs() )
212         {
213             throw new SurefireBooterForkException( "Cannot create workingDirectory " + cwd.getAbsolutePath() );
214         }
215         if ( !cwd.isDirectory() )
216         {
217             throw new SurefireBooterForkException(
218                 "WorkingDirectory " + cwd.getAbsolutePath() + " exists and is not a directory" );
219         }
220         return cwd;
221     }
222 
223     private String replaceThreadNumberPlaceholder( String argLine, int threadNumber )
224     {
225         return argLine.replace( AbstractSurefireMojo.THREAD_NUMBER_PLACEHOLDER,
226                                 String.valueOf( threadNumber ) ).replace( AbstractSurefireMojo.FORK_NUMBER_PLACEHOLDER,
227                                                                           String.valueOf( threadNumber ) );
228     }
229 
230     /**
231      * Replaces expressions <pre>@{property-name}</pre> with the corresponding properties
232      * from the model. This allows late evaluation of property values when the plugin is executed (as compared
233      * to evaluation when the pom is parsed as is done with <pre>${property-name}</pre> expressions).
234      *
235      * This allows other plugins to modify or set properties with the changes getting picked up by surefire.
236      */
237     private String replacePropertyExpressions( String argLine )
238     {
239         if ( argLine == null )
240         {
241             return null;
242         }
243 
244         for ( Enumeration<?> e = modelProperties.propertyNames(); e.hasMoreElements(); )
245         {
246             String key = e.nextElement().toString();
247             String field = "@{" + key + "}";
248             if ( argLine.contains( field ) )
249             {
250                 argLine = argLine.replace( field, modelProperties.getProperty( key, "" ) );
251             }
252         }
253 
254         return argLine;
255     }
256 
257     /**
258      * Create a jar with just a manifest containing a Main-Class entry for BooterConfiguration and a Class-Path entry
259      * for all classpath elements.
260      *
261      * @param classPath      List&lt;String> of all classpath elements.
262      * @param startClassName  The classname to start (main-class)
263      * @return The file pointint to the jar
264      * @throws java.io.IOException When a file operation fails.
265      */
266     private File createJar( List<String> classPath, String startClassName )
267         throws IOException
268     {
269         File file = File.createTempFile( "surefirebooter", ".jar", tempDirectory );
270         if ( !debug )
271         {
272             file.deleteOnExit();
273         }
274         FileOutputStream fos = new FileOutputStream( file );
275         JarOutputStream jos = new JarOutputStream( fos );
276         try
277         {
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             StringBuilder cp = new StringBuilder();
287             for ( String el : classPath )
288             {
289                 // NOTE: if File points to a directory, this entry MUST end in '/'.
290                 cp.append( UrlUtils.getURL( new File( el ) ).toExternalForm() )
291                         .append( " " );
292             }
293 
294             man.getMainAttributes().putValue( "Manifest-Version", "1.0" );
295             man.getMainAttributes().putValue( "Class-Path", cp.toString().trim() );
296             man.getMainAttributes().putValue( "Main-Class", startClassName );
297 
298             man.write( jos );
299         }
300         finally
301         {
302             jos.close();
303         }
304 
305         return file;
306     }
307 
308     public boolean isDebug()
309     {
310         return debug;
311     }
312 
313     public String stripNewLines( String argline )
314     {
315         return argline.replace( "\n", " " ).replace( "\r", " " );
316     }
317 
318     public String getDebugLine()
319     {
320         return debugLine;
321     }
322 
323     public File getTempDirectory()
324     {
325         return tempDirectory;
326     }
327 
328     public int getForkCount()
329     {
330         return forkCount;
331     }
332 
333 
334     public boolean isReuseForks()
335     {
336         return reuseForks;
337     }
338 }