package org.apache.maven.surefire.booter;
/*
* 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.log.api.ConsoleLogger;
import org.apache.maven.plugin.surefire.log.api.ConsoleLoggerUtils;
import org.apache.maven.surefire.report.ConsoleOutputReceiver;
import org.apache.maven.surefire.report.ConsoleStream;
import org.apache.maven.surefire.report.ReportEntry;
import org.apache.maven.surefire.report.RunListener;
import org.apache.maven.surefire.report.SafeThrowable;
import org.apache.maven.surefire.report.SimpleReportEntry;
import org.apache.maven.surefire.report.StackTraceWriter;
import org.apache.maven.surefire.report.TestSetReportEntry;
import java.io.PrintStream;
import java.util.Map.Entry;
import static java.lang.Integer.toHexString;
import static java.nio.charset.Charset.defaultCharset;
import static org.apache.maven.surefire.util.internal.ObjectUtils.systemProps;
import static org.apache.maven.surefire.util.internal.ObjectUtils.useNonNull;
import static org.apache.maven.surefire.util.internal.StringUtils.encodeStringForForkCommunication;
import static org.apache.maven.surefire.util.internal.StringUtils.escapeBytesToPrintable;
import static org.apache.maven.surefire.util.internal.StringUtils.escapeToPrintable;
/**
* Encodes the full output of the test run to the stdout stream.
* <br>
* This class and the ForkClient contain the full definition of the
* "wire-level" protocol used by the forked process. The protocol
* is *not* part of any public api and may change without further
* notice.
* <br>
* This class is threadsafe.
* <br>
* The synchronization in the underlying PrintStream (target instance)
* is used to preserve thread safety of the output stream. To perform
* multiple writes/prints for a single request, they must
* synchronize on "target" variable in this class.
*
* @author Kristian Rosenvold
*/
public class ForkingRunListener
implements RunListener, ConsoleLogger, ConsoleOutputReceiver, ConsoleStream
{
public static final byte BOOTERCODE_TESTSET_STARTING = (byte) '1';
public static final byte BOOTERCODE_TESTSET_COMPLETED = (byte) '2';
public static final byte BOOTERCODE_STDOUT = (byte) '3';
public static final byte BOOTERCODE_STDERR = (byte) '4';
public static final byte BOOTERCODE_TEST_STARTING = (byte) '5';
public static final byte BOOTERCODE_TEST_SUCCEEDED = (byte) '6';
public static final byte BOOTERCODE_TEST_ERROR = (byte) '7';
public static final byte BOOTERCODE_TEST_FAILED = (byte) '8';
public static final byte BOOTERCODE_TEST_SKIPPED = (byte) '9';
public static final byte BOOTERCODE_TEST_ASSUMPTIONFAILURE = (byte) 'G';
/**
* INFO logger
* @see ConsoleLogger#info(String)
*/
public static final byte BOOTERCODE_CONSOLE = (byte) 'H';
public static final byte BOOTERCODE_SYSPROPS = (byte) 'I';
public static final byte BOOTERCODE_NEXT_TEST = (byte) 'N';
public static final byte BOOTERCODE_STOP_ON_NEXT_TEST = (byte) 'S';
/**
* ERROR logger
* @see ConsoleLogger#error(String)
*/
public static final byte BOOTERCODE_ERROR = (byte) 'X';
public static final byte BOOTERCODE_BYE = (byte) 'Z';
/**
* DEBUG logger
* @see ConsoleLogger#debug(String)
*/
public static final byte BOOTERCODE_DEBUG = (byte) 'D';
/**
* WARNING logger
* @see ConsoleLogger#warning(String)
*/
public static final byte BOOTERCODE_WARNING = (byte) 'W';
private final PrintStream target;
private final int testSetChannelId;
private final boolean trimStackTraces;
private final byte[] stdOutHeader;
private final byte[] stdErrHeader;
public ForkingRunListener( PrintStream target, int testSetChannelId, boolean trimStackTraces )
{
this.target = target;
this.testSetChannelId = testSetChannelId;
this.trimStackTraces = trimStackTraces;
stdOutHeader = createHeader( BOOTERCODE_STDOUT, testSetChannelId );
stdErrHeader = createHeader( BOOTERCODE_STDERR, testSetChannelId );
sendProps();
}
@Override
public void testSetStarting( TestSetReportEntry report )
{
encodeAndWriteToTarget( toString( BOOTERCODE_TESTSET_STARTING, report, testSetChannelId ) );
}
@Override
public void testSetCompleted( TestSetReportEntry report )
{
encodeAndWriteToTarget( toString( BOOTERCODE_TESTSET_COMPLETED, report, testSetChannelId ) );
}
@Override
public void testStarting( ReportEntry report )
{
encodeAndWriteToTarget( toString( BOOTERCODE_TEST_STARTING, report, testSetChannelId ) );
}
@Override
public void testSucceeded( ReportEntry report )
{
encodeAndWriteToTarget( toString( BOOTERCODE_TEST_SUCCEEDED, report, testSetChannelId ) );
}
@Override
public void testAssumptionFailure( ReportEntry report )
{
encodeAndWriteToTarget( toString( BOOTERCODE_TEST_ASSUMPTIONFAILURE, report, testSetChannelId ) );
}
@Override
public void testError( ReportEntry report )
{
encodeAndWriteToTarget( toString( BOOTERCODE_TEST_ERROR, report, testSetChannelId ) );
}
@Override
public void testFailed( ReportEntry report )
{
encodeAndWriteToTarget( toString( BOOTERCODE_TEST_FAILED, report, testSetChannelId ) );
}
@Override
public void testSkipped( ReportEntry report )
{
encodeAndWriteToTarget( toString( BOOTERCODE_TEST_SKIPPED, report, testSetChannelId ) );
}
@Override
public void testExecutionSkippedByUser()
{
encodeAndWriteToTarget( toString( BOOTERCODE_STOP_ON_NEXT_TEST, new SimpleReportEntry(), testSetChannelId ) );
}
void sendProps()
{
for ( Entry<String, String> entry : systemProps().entrySet() )
{
String value = entry.getValue();
encodeAndWriteToTarget( toPropertyString( entry.getKey(), useNonNull( value, "null" ) ) );
}
}
@Override
public void writeTestOutput( byte[] buf, int off, int len, boolean stdout )
{
byte[] header = stdout ? stdOutHeader : stdErrHeader;
byte[] content =
new byte[buf.length * 3 + 1]; // Hex-escaping can be up to 3 times length of a regular byte.
int i = escapeBytesToPrintable( content, 0, buf, off, len );
content[i++] = (byte) '\n';
byte[] encodeBytes = new byte[header.length + i];
System.arraycopy( header, 0, encodeBytes, 0, header.length );
System.arraycopy( content, 0, encodeBytes, header.length, i );
synchronized ( target ) // See notes about synchronization/thread safety in class javadoc
{
target.write( encodeBytes, 0, encodeBytes.length );
target.flush();
if ( target.checkError() )
{
// We MUST NOT throw any exception from this method; otherwise we are in loop and CPU goes up:
// ForkingRunListener -> Exception -> JUnit Notifier and RunListener -> ForkingRunListener -> Exception
DumpErrorSingleton.getSingleton()
.dumpStreamText( "Unexpected IOException with stream: " + new String( buf, off, len ) );
}
}
}
public static byte[] createHeader( byte booterCode, int testSetChannel )
{
return encodeStringForForkCommunication( String.valueOf( (char) booterCode )
+ ','
+ Integer.toString( testSetChannel, 16 )
+ ',' + defaultCharset().name()
+ ',' );
}
private void log( byte bootCode, String message )
{
if ( message != null )
{
StringBuilder sb = new StringBuilder( 7 + message.length() * 5 );
append( sb, bootCode ); comma( sb );
append( sb, toHexString( testSetChannelId ) ); comma( sb );
escapeToPrintable( sb, message );
sb.append( '\n' );
encodeAndWriteToTarget( sb.toString() );
}
}
@Override
public void debug( String message )
{
log( BOOTERCODE_DEBUG, message );
}
@Override
public void info( String message )
{
log( BOOTERCODE_CONSOLE, message );
}
@Override
public void warning( String message )
{
log( BOOTERCODE_WARNING, message );
}
@Override
public void error( String message )
{
log( BOOTERCODE_ERROR, message );
}
@Override
public void error( String message, Throwable t )
{
error( ConsoleLoggerUtils.toString( message, t ) );
}
@Override
public void error( Throwable t )
{
error( null, t );
}
private void encodeAndWriteToTarget( String string )
{
byte[] encodeBytes = encodeStringForForkCommunication( string );
synchronized ( target ) // See notes about synchronization/thread safety in class javadoc
{
target.write( encodeBytes, 0, encodeBytes.length );
target.flush();
if ( target.checkError() )
{
// We MUST NOT throw any exception from this method; otherwise we are in loop and CPU goes up:
// ForkingRunListener -> Exception -> JUnit Notifier and RunListener -> ForkingRunListener -> Exception
DumpErrorSingleton.getSingleton().dumpStreamText( "Unexpected IOException: " + string );
}
}
}
private String toPropertyString( String key, String value )
{
StringBuilder stringBuilder = new StringBuilder();
append( stringBuilder, BOOTERCODE_SYSPROPS ); comma( stringBuilder );
append( stringBuilder, toHexString( testSetChannelId ) ); comma( stringBuilder );
escapeToPrintable( stringBuilder, key );
comma( stringBuilder );
escapeToPrintable( stringBuilder, value );
stringBuilder.append( "\n" );
return stringBuilder.toString();
}
private String toString( byte operationCode, ReportEntry reportEntry, int testSetChannelId )
{
StringBuilder stringBuilder = new StringBuilder();
append( stringBuilder, operationCode ); comma( stringBuilder );
append( stringBuilder, toHexString( testSetChannelId ) ); comma( stringBuilder );
nullableEncoding( stringBuilder, reportEntry.getSourceName() );
comma( stringBuilder );
nullableEncoding( stringBuilder, reportEntry.getName() );
comma( stringBuilder );
nullableEncoding( stringBuilder, reportEntry.getGroup() );
comma( stringBuilder );
nullableEncoding( stringBuilder, reportEntry.getMessage() );
comma( stringBuilder );
nullableEncoding( stringBuilder, reportEntry.getElapsed() );
encode( stringBuilder, reportEntry.getStackTraceWriter() );
stringBuilder.append( "\n" );
return stringBuilder.toString();
}
private static void comma( StringBuilder stringBuilder )
{
stringBuilder.append( "," );
}
private ForkingRunListener append( StringBuilder stringBuilder, String message )
{
stringBuilder.append( encode( message ) );
return this;
}
private ForkingRunListener append( StringBuilder stringBuilder, byte b )
{
stringBuilder.append( (char) b );
return this;
}
private void nullableEncoding( StringBuilder stringBuilder, Integer source )
{
if ( source == null )
{
stringBuilder.append( "null" );
}
else
{
stringBuilder.append( source.toString() );
}
}
private String encode( String source )
{
return source;
}
private static void nullableEncoding( StringBuilder stringBuilder, String source )
{
if ( source == null || source.length() == 0 )
{
stringBuilder.append( "null" );
}
else
{
escapeToPrintable( stringBuilder, source );
}
}
private void encode( StringBuilder stringBuilder, StackTraceWriter stackTraceWriter )
{
encode( stringBuilder, stackTraceWriter, trimStackTraces );
}
public static void encode( StringBuilder stringBuilder, StackTraceWriter stackTraceWriter, boolean trimStackTraces )
{
if ( stackTraceWriter != null )
{
comma( stringBuilder );
//noinspection ThrowableResultOfMethodCallIgnored
final SafeThrowable throwable = stackTraceWriter.getThrowable();
if ( throwable != null )
{
String message = throwable.getLocalizedMessage();
nullableEncoding( stringBuilder, message );
}
comma( stringBuilder );
nullableEncoding( stringBuilder, stackTraceWriter.smartTrimmedStackTrace() );
comma( stringBuilder );
nullableEncoding( stringBuilder, trimStackTraces
? stackTraceWriter.writeTrimmedTraceToString()
: stackTraceWriter.writeTraceToString() );
}
}
@Override
public void println( String message )
{
byte[] buf = message.getBytes();
println( buf, 0, buf.length );
}
@Override
public void println( byte[] buf, int off, int len )
{
writeTestOutput( buf, off, len, true );
}
}