package org.apache.maven.plugin.surefire.booterclient;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import org.apache.maven.plugin.surefire.AbstractSurefireMojo;
import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.OutputStreamFlushableCommandline;
import org.apache.maven.surefire.util.internal.ImmutableMap;
import org.apache.maven.plugin.surefire.util.Relocator;
import org.apache.maven.shared.utils.StringUtils;
import org.apache.maven.surefire.booter.Classpath;
import org.apache.maven.surefire.booter.ForkedBooter;
import org.apache.maven.surefire.booter.StartupConfiguration;
import org.apache.maven.surefire.booter.SurefireBooterForkException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
/**
* Configuration for forking tests.
*
* @author <a href="mailto:brett@apache.org">Brett Porter</a>
* @author <a href="mailto:kenney@apache.org">Kenney Westerhof</a>
* @author <a href="mailto:krosenvold@apache.org">Kristian Rosenvold</a>
*/
public class ForkConfiguration
{
public static final String FORK_ONCE = "once";
public static final String FORK_ALWAYS = "always";
public static final String FORK_NEVER = "never";
public static final String FORK_PERTHREAD = "perthread";
private final int forkCount;
private final boolean reuseForks;
private final Classpath bootClasspathConfiguration;
private final String jvmExecutable;
private final Properties modelProperties;
private final String argLine;
private final Map<String, String> environmentVariables;
private final File workingDirectory;
private final File tempDirectory;
private final boolean debug;
private final String debugLine;
@SuppressWarnings( "checkstyle:parameternumber" )
public ForkConfiguration( Classpath bootClasspathConfiguration, File tmpDir, String debugLine,
String jvmExecutable, File workingDirectory, Properties modelProperties, String argLine,
Map<String, String> environmentVariables, boolean debugEnabled, int forkCount,
boolean reuseForks )
{
this.bootClasspathConfiguration = bootClasspathConfiguration;
this.tempDirectory = tmpDir;
this.debugLine = debugLine;
this.jvmExecutable = jvmExecutable;
this.workingDirectory = workingDirectory;
this.modelProperties = modelProperties;
this.argLine = argLine;
this.environmentVariables = toImmutable( environmentVariables );
this.debug = debugEnabled;
this.forkCount = forkCount;
this.reuseForks = reuseForks;
}
public Classpath getBootClasspath()
{
return bootClasspathConfiguration;
}
public static String getEffectiveForkMode( String forkMode )
{
if ( "pertest".equalsIgnoreCase( forkMode ) )
{
return FORK_ALWAYS;
}
else if ( "none".equalsIgnoreCase( forkMode ) )
{
return FORK_NEVER;
}
else if ( forkMode.equals( FORK_NEVER ) || forkMode.equals( FORK_ONCE )
|| forkMode.equals( FORK_ALWAYS ) || forkMode.equals( FORK_PERTHREAD ) )
{
return forkMode;
}
else
{
throw new IllegalArgumentException( "Fork mode " + forkMode + " is not a legal value" );
}
}
/**
* @param classPath cla the classpath arguments
* @param startupConfiguration The startup configuration
* @param threadNumber the thread number, to be the replacement in the argLine @return A commandline
* @return CommandLine able to flush entire command going to be sent to forked JVM
* @throws org.apache.maven.surefire.booter.SurefireBooterForkException
* when unable to perform the fork
*/
public OutputStreamFlushableCommandline createCommandLine( List<String> classPath,
StartupConfiguration startupConfiguration,
int threadNumber )
throws SurefireBooterForkException
{
return createCommandLine( classPath,
startupConfiguration.getClassLoaderConfiguration()
.isManifestOnlyJarRequestedAndUsable(),
startupConfiguration.isShadefire(), startupConfiguration.isProviderMainClass()
? startupConfiguration.getActualClassName()
: ForkedBooter.class.getName(), threadNumber );
}
OutputStreamFlushableCommandline createCommandLine( List<String> classPath, boolean useJar, boolean shadefire,
String providerThatHasMainMethod, int threadNumber )
throws SurefireBooterForkException
{
OutputStreamFlushableCommandline cli = new OutputStreamFlushableCommandline();
cli.setExecutable( jvmExecutable );
if ( argLine != null )
{
cli.createArg().setLine(
replaceThreadNumberPlaceholder( stripNewLines( replacePropertyExpressions( argLine ) ),
threadNumber ) );
}
for ( Map.Entry<String, String> entry : environmentVariables.entrySet() )
{
String value = entry.getValue();
cli.addEnvironment( entry.getKey(), value == null ? "" : value );
}
if ( getDebugLine() != null && !getDebugLine().isEmpty() )
{
cli.createArg().setLine( getDebugLine() );
}
if ( useJar )
{
File jarFile;
try
{
jarFile = createJar( classPath, providerThatHasMainMethod );
}
catch ( IOException e )
{
throw new SurefireBooterForkException( "Error creating archive file", e );
}
cli.createArg().setValue( "-jar" );
cli.createArg().setValue( jarFile.getAbsolutePath() );
}
else
{
cli.addEnvironment( "CLASSPATH", StringUtils.join( classPath.iterator(), File.pathSeparator ) );
final String forkedBooter =
providerThatHasMainMethod != null ? providerThatHasMainMethod : ForkedBooter.class.getName();
cli.createArg().setValue( shadefire ? new Relocator().relocate( forkedBooter ) : forkedBooter );
}
cli.setWorkingDirectory( getWorkingDirectory( threadNumber ).getAbsolutePath() );
return cli;
}
private File getWorkingDirectory( int threadNumber )
throws SurefireBooterForkException
{
File cwd = new File( replaceThreadNumberPlaceholder( workingDirectory.getAbsolutePath(), threadNumber ) );
if ( !cwd.exists() && !cwd.mkdirs() )
{
throw new SurefireBooterForkException( "Cannot create workingDirectory " + cwd.getAbsolutePath() );
}
if ( !cwd.isDirectory() )
{
throw new SurefireBooterForkException(
"WorkingDirectory " + cwd.getAbsolutePath() + " exists and is not a directory" );
}
return cwd;
}
private String replaceThreadNumberPlaceholder( String argLine, int threadNumber )
{
return argLine.replace( AbstractSurefireMojo.THREAD_NUMBER_PLACEHOLDER,
String.valueOf( threadNumber ) ).replace( AbstractSurefireMojo.FORK_NUMBER_PLACEHOLDER,
String.valueOf( threadNumber ) );
}
/**
* Replaces expressions <pre>@{property-name}</pre> with the corresponding properties
* from the model. This allows late evaluation of property values when the plugin is executed (as compared
* to evaluation when the pom is parsed as is done with <pre>${property-name}</pre> expressions).
*
* This allows other plugins to modify or set properties with the changes getting picked up by surefire.
*/
private String replacePropertyExpressions( String argLine )
{
if ( argLine == null )
{
return null;
}
for ( final String key : modelProperties.stringPropertyNames() )
{
String field = "@{" + key + "}";
if ( argLine.contains( field ) )
{
argLine = argLine.replace( field, modelProperties.getProperty( key, "" ) );
}
}
return argLine;
}
/**
* Create a jar with just a manifest containing a Main-Class entry for BooterConfiguration and a Class-Path entry
* for all classpath elements.
*
* @param classPath List<String> of all classpath elements.
* @param startClassName The classname to start (main-class)
* @return The file pointint to the jar
* @throws java.io.IOException When a file operation fails.
*/
private File createJar( List<String> classPath, String startClassName )
throws IOException
{
File file = File.createTempFile( "surefirebooter", ".jar", tempDirectory );
if ( !debug )
{
file.deleteOnExit();
}
FileOutputStream fos = new FileOutputStream( file );
JarOutputStream jos = new JarOutputStream( fos );
try
{
jos.setLevel( JarOutputStream.STORED );
JarEntry je = new JarEntry( "META-INF/MANIFEST.MF" );
jos.putNextEntry( je );
Manifest man = new Manifest();
// we can't use StringUtils.join here since we need to add a '/' to
// the end of directory entries - otherwise the jvm will ignore them.
StringBuilder cp = new StringBuilder();
for ( Iterator<String> it = classPath.iterator(); it.hasNext(); )
{
File file1 = new File( it.next() );
String uri = file1.toURI().toASCIIString();
cp.append( uri );
if ( file1.isDirectory() && !uri.endsWith( "/" ) )
{
cp.append( '/' );
}
if ( it.hasNext() )
{
cp.append( ' ' );
}
}
man.getMainAttributes().putValue( "Manifest-Version", "1.0" );
man.getMainAttributes().putValue( "Class-Path", cp.toString().trim() );
man.getMainAttributes().putValue( "Main-Class", startClassName );
man.write( jos );
jos.closeEntry();
jos.flush();
return file;
}
finally
{
jos.close();
}
}
public boolean isDebug()
{
return debug;
}
public String getDebugLine()
{
return debugLine;
}
public File getTempDirectory()
{
return tempDirectory;
}
public int getForkCount()
{
return forkCount;
}
public boolean isReuseForks()
{
return reuseForks;
}
private static String stripNewLines( String argLine )
{
return argLine.replace( "\n", " " ).replace( "\r", " " );
}
/**
* Immutable map.
*
* @param map immutable map copies elements from <code>map</code>
* @param <K> key type
* @param <V> value type
* @return never returns null
*/
private static <K, V> Map<K, V> toImmutable( Map<K, V> map )
{
return map == null ? Collections.<K, V>emptyMap() : new ImmutableMap<K, V>( map );
}
}