package org.apache.maven.plugin.surefire.booterclient.output;
/*
* 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.booterclient.lazytestprovider.NotifiableTestStream;
import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
import org.apache.maven.plugin.surefire.report.DefaultReporterFactory;
import org.apache.maven.shared.utils.cli.StreamConsumer;
import org.apache.maven.surefire.report.ConsoleOutputReceiver;
import org.apache.maven.surefire.report.ReportEntry;
import org.apache.maven.surefire.report.RunListener;
import org.apache.maven.surefire.report.StackTraceWriter;
import org.apache.maven.surefire.report.TestSetReportEntry;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicLong;
import static java.lang.Integer.decode;
import static java.lang.System.currentTimeMillis;
import static java.util.Collections.unmodifiableMap;
import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_BYE;
import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_CONSOLE;
import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_DEBUG;
import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_ERROR;
import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_NEXT_TEST;
import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_STDERR;
import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_STDOUT;
import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_STOP_ON_NEXT_TEST;
import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_SYSPROPS;
import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_TESTSET_COMPLETED;
import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_TESTSET_STARTING;
import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_TEST_ASSUMPTIONFAILURE;
import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_TEST_ERROR;
import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_TEST_FAILED;
import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_TEST_SKIPPED;
import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_TEST_STARTING;
import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_TEST_SUCCEEDED;
import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_WARNING;
import static org.apache.maven.surefire.booter.Shutdown.KILL;
import static org.apache.maven.surefire.report.CategorizedReportEntry.reportEntry;
import static org.apache.maven.surefire.util.internal.StringUtils.isNotBlank;
import static org.apache.maven.surefire.util.internal.StringUtils.unescapeBytes;
import static org.apache.maven.surefire.util.internal.StringUtils.unescapeString;
// todo move to the same package with ForkStarter
/**
* Knows how to reconstruct *all* the state transmitted over stdout by the forked process.
*
* @author Kristian Rosenvold
*/
public class ForkClient
implements StreamConsumer
{
private static final String PRINTABLE_JVM_NATIVE_STREAM = "Listening for transport dt_socket at address:";
private static final long START_TIME_ZERO = 0L;
private static final long START_TIME_NEGATIVE_TIMEOUT = -1L;
private final DefaultReporterFactory defaultReporterFactory;
private final Map<String, String> testVmSystemProperties = new ConcurrentHashMap<String, String>();
private final NotifiableTestStream notifiableTestStream;
private final Queue<String> testsInProgress = new ConcurrentLinkedQueue<String>();
/**
* {@code testSetStartedAt} is set to non-zero after received
* {@link org.apache.maven.surefire.booter.ForkingRunListener#BOOTERCODE_TESTSET_STARTING test-set}.
*/
private final AtomicLong testSetStartedAt = new AtomicLong( START_TIME_ZERO );
private final ConsoleLogger log;
private RunListener testSetReporter;
private volatile boolean saidGoodBye;
private volatile StackTraceWriter errorInFork;
private volatile int forkNumber;
// prevents from printing same warning
private boolean printedErrorStream;
public ForkClient( DefaultReporterFactory defaultReporterFactory,
NotifiableTestStream notifiableTestStream, ConsoleLogger log )
{
this.defaultReporterFactory = defaultReporterFactory;
this.notifiableTestStream = notifiableTestStream;
this.log = log;
}
protected void stopOnNextTest()
{
}
public void kill()
{
if ( !saidGoodBye )
{
notifiableTestStream.shutdown( KILL );
}
}
/**
* Called in concurrent Thread.
* Will shutdown if timeout was reached.
*
* @param currentTimeMillis current time in millis seconds
* @param forkedProcessTimeoutInSeconds timeout in seconds given by MOJO
*/
public final void tryToTimeout( long currentTimeMillis, int forkedProcessTimeoutInSeconds )
{
if ( forkedProcessTimeoutInSeconds > 0 )
{
final long forkedProcessTimeoutInMillis = 1000 * forkedProcessTimeoutInSeconds;
final long startedAt = testSetStartedAt.get();
if ( startedAt > START_TIME_ZERO && currentTimeMillis - startedAt >= forkedProcessTimeoutInMillis )
{
testSetStartedAt.set( START_TIME_NEGATIVE_TIMEOUT );
notifiableTestStream.shutdown( KILL );
}
}
}
public final DefaultReporterFactory getDefaultReporterFactory()
{
return defaultReporterFactory;
}
@Override
public final void consumeLine( String s )
{
if ( isNotBlank( s ) )
{
processLine( s );
}
}
private void setCurrentStartTime()
{
if ( testSetStartedAt.get() == START_TIME_ZERO ) // JIT can optimize <= no JNI call
{
// Not necessary to call JNI library library #currentTimeMillis
// which may waste 10 - 30 machine cycles in callback. Callbacks should be fast.
testSetStartedAt.compareAndSet( START_TIME_ZERO, currentTimeMillis() );
}
}
public final boolean hadTimeout()
{
return testSetStartedAt.get() == START_TIME_NEGATIVE_TIMEOUT;
}
private RunListener getTestSetReporter()
{
if ( testSetReporter == null )
{
testSetReporter = defaultReporterFactory.createReporter();
}
return testSetReporter;
}
private void processLine( String event )
{
final OperationalData op;
try
{
op = new OperationalData( event );
}
catch ( RuntimeException e )
{
logStreamWarning( e, event );
return;
}
final String remaining = op.getData();
switch ( op.getOperationId() )
{
case BOOTERCODE_TESTSET_STARTING:
getTestSetReporter().testSetStarting( createReportEntry( remaining ) );
setCurrentStartTime();
break;
case BOOTERCODE_TESTSET_COMPLETED:
testsInProgress.clear();
getTestSetReporter().testSetCompleted( createReportEntry( remaining, testVmSystemProperties ) );
break;
case BOOTERCODE_TEST_STARTING:
ReportEntry reportEntry = createReportEntry( remaining );
testsInProgress.offer( reportEntry.getSourceName() );
getTestSetReporter().testStarting( createReportEntry( remaining ) );
break;
case BOOTERCODE_TEST_SUCCEEDED:
reportEntry = createReportEntry( remaining );
testsInProgress.remove( reportEntry.getSourceName() );
getTestSetReporter().testSucceeded( createReportEntry( remaining ) );
break;
case BOOTERCODE_TEST_FAILED:
reportEntry = createReportEntry( remaining );
testsInProgress.remove( reportEntry.getSourceName() );
getTestSetReporter().testFailed( createReportEntry( remaining ) );
break;
case BOOTERCODE_TEST_SKIPPED:
reportEntry = createReportEntry( remaining );
testsInProgress.remove( reportEntry.getSourceName() );
getTestSetReporter().testSkipped( createReportEntry( remaining ) );
break;
case BOOTERCODE_TEST_ERROR:
reportEntry = createReportEntry( remaining );
testsInProgress.remove( reportEntry.getSourceName() );
getTestSetReporter().testError( createReportEntry( remaining ) );
break;
case BOOTERCODE_TEST_ASSUMPTIONFAILURE:
reportEntry = createReportEntry( remaining );
testsInProgress.remove( reportEntry.getSourceName() );
getTestSetReporter().testAssumptionFailure( createReportEntry( remaining ) );
break;
case BOOTERCODE_SYSPROPS:
int keyEnd = remaining.indexOf( "," );
StringBuilder key = new StringBuilder();
StringBuilder value = new StringBuilder();
unescapeString( key, remaining.substring( 0, keyEnd ) );
unescapeString( value, remaining.substring( keyEnd + 1 ) );
testVmSystemProperties.put( key.toString(), value.toString() );
break;
case BOOTERCODE_STDOUT:
writeTestOutput( remaining, true );
break;
case BOOTERCODE_STDERR:
writeTestOutput( remaining, false );
break;
case BOOTERCODE_CONSOLE:
getOrCreateConsoleLogger()
.info( createConsoleMessage( remaining ) );
break;
case BOOTERCODE_NEXT_TEST:
notifiableTestStream.provideNewTest();
break;
case BOOTERCODE_ERROR:
errorInFork = deserializeStackTraceWriter( new StringTokenizer( remaining, "," ) );
break;
case BOOTERCODE_BYE:
saidGoodBye = true;
notifiableTestStream.acknowledgeByeEventReceived();
break;
case BOOTERCODE_STOP_ON_NEXT_TEST:
stopOnNextTest();
break;
case BOOTERCODE_DEBUG:
getOrCreateConsoleLogger()
.debug( createConsoleMessage( remaining ) );
break;
case BOOTERCODE_WARNING:
getOrCreateConsoleLogger()
.warning( createConsoleMessage( remaining ) );
break;
default:
logStreamWarning( event );
}
}
private void logStreamWarning( String event )
{
logStreamWarning( null, event );
}
private void logStreamWarning( Throwable e, String event )
{
if ( event == null || !event.contains( PRINTABLE_JVM_NATIVE_STREAM ) )
{
final String msg = "Corrupted stdin stream in forked JVM " + forkNumber + ".";
final InPluginProcessDumpSingleton util = InPluginProcessDumpSingleton.getSingleton();
final File dump =
e == null
? util.dumpText( msg + " Stream '" + event + "'.", defaultReporterFactory, forkNumber )
: util.dumpException( e, msg + " Stream '" + event + "'.", defaultReporterFactory, forkNumber );
if ( !printedErrorStream )
{
printedErrorStream = true;
log.warning( msg + " See the dump file " + dump.getAbsolutePath() );
}
}
else
{
log.info( event );
}
}
private void writeTestOutput( String remaining, boolean isStdout )
{
int csNameEnd = remaining.indexOf( ',' );
String charsetName = remaining.substring( 0, csNameEnd );
String byteEncoded = remaining.substring( csNameEnd + 1 );
ByteBuffer unescaped = unescapeBytes( byteEncoded, charsetName );
if ( unescaped.hasArray() )
{
byte[] convertedBytes = unescaped.array();
getOrCreateConsoleOutputReceiver()
.writeTestOutput( convertedBytes, unescaped.position(), unescaped.remaining(), isStdout );
}
else
{
byte[] convertedBytes = new byte[unescaped.remaining()];
unescaped.get( convertedBytes, 0, unescaped.remaining() );
getOrCreateConsoleOutputReceiver()
.writeTestOutput( convertedBytes, 0, convertedBytes.length, isStdout );
}
}
public final void consumeMultiLineContent( String s )
throws IOException
{
BufferedReader stringReader = new BufferedReader( new StringReader( s ) );
for ( String s1 = stringReader.readLine(); s1 != null; s1 = stringReader.readLine() )
{
consumeLine( s1 );
}
}
private String createConsoleMessage( String remaining )
{
return unescape( remaining );
}
private TestSetReportEntry createReportEntry( String untokenized )
{
return createReportEntry( untokenized, Collections.<String, String>emptyMap() );
}
private TestSetReportEntry createReportEntry( String untokenized, Map<String, String> systemProperties )
{
StringTokenizer tokens = new StringTokenizer( untokenized, "," );
try
{
String source = nullableCsv( tokens.nextToken() );
String name = nullableCsv( tokens.nextToken() );
String group = nullableCsv( tokens.nextToken() );
String message = nullableCsv( tokens.nextToken() );
String elapsedStr = tokens.nextToken();
Integer elapsed = "null".equals( elapsedStr ) ? null : decode( elapsedStr );
final StackTraceWriter stackTraceWriter =
tokens.hasMoreTokens() ? deserializeStackTraceWriter( tokens ) : null;
return reportEntry( source, name, group, stackTraceWriter, elapsed, message, systemProperties );
}
catch ( RuntimeException e )
{
throw new RuntimeException( untokenized, e );
}
}
private StackTraceWriter deserializeStackTraceWriter( StringTokenizer tokens )
{
String stackTraceMessage = nullableCsv( tokens.nextToken() );
String smartStackTrace = nullableCsv( tokens.nextToken() );
String stackTrace = tokens.hasMoreTokens() ? nullableCsv( tokens.nextToken() ) : null;
boolean hasTrace = stackTrace != null;
return hasTrace ? new DeserializedStacktraceWriter( stackTraceMessage, smartStackTrace, stackTrace ) : null;
}
private String nullableCsv( String source )
{
return "null".equals( source ) ? null : unescape( source );
}
private String unescape( String source )
{
StringBuilder stringBuffer = new StringBuilder( source.length() );
unescapeString( stringBuffer, source );
return stringBuffer.toString();
}
public final Map<String, String> getTestVmSystemProperties()
{
return unmodifiableMap( testVmSystemProperties );
}
/**
* Used when getting reporters on the plugin side of a fork.
* Used by testing purposes only. May not be volatile variable.
*
* @return A mock provider reporter
*/
public final RunListener getReporter()
{
return getTestSetReporter();
}
private ConsoleOutputReceiver getOrCreateConsoleOutputReceiver()
{
return (ConsoleOutputReceiver) getTestSetReporter();
}
private ConsoleLogger getOrCreateConsoleLogger()
{
return (ConsoleLogger) getTestSetReporter();
}
public void close( boolean hadTimeout )
{
// no op
}
public final boolean isSaidGoodBye()
{
return saidGoodBye;
}
public final StackTraceWriter getErrorInFork()
{
return errorInFork;
}
public final boolean isErrorInFork()
{
return errorInFork != null;
}
public Set<String> testsInProgress()
{
return new TreeSet<String>( testsInProgress );
}
public boolean hasTestsInProgress()
{
return !testsInProgress.isEmpty();
}
public void setForkNumber( int forkNumber )
{
assert this.forkNumber == 0;
this.forkNumber = forkNumber;
}
private static final class OperationalData
{
private final byte operationId;
private final String data;
OperationalData( String event )
{
operationId = (byte) event.charAt( 0 );
int comma = event.indexOf( ",", 3 );
if ( comma < 0 )
{
throw new IllegalArgumentException( "Stream stdin corrupted. Expected comma after third character "
+ "in command '" + event + "'." );
}
int rest = event.indexOf( ",", comma );
data = event.substring( rest + 1 );
}
byte getOperationId()
{
return operationId;
}
String getData()
{
return data;
}
}
}