blob: f8cb224234badf6323324dd3d485f18c51e7bde1 [file] [log] [blame]
package org.apache.maven.surefire.booter.spi;
/*
* 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.ConsoleLoggerUtils;
import org.apache.maven.surefire.api.booter.DumpErrorSingleton;
import org.apache.maven.surefire.api.booter.ForkedProcessEventType;
import org.apache.maven.surefire.api.booter.MasterProcessChannelEncoder;
import org.apache.maven.surefire.api.report.ReportEntry;
import org.apache.maven.surefire.api.report.RunMode;
import org.apache.maven.surefire.api.report.SafeThrowable;
import org.apache.maven.surefire.api.report.StackTraceWriter;
import org.apache.maven.surefire.api.util.internal.WritableBufferedByteChannel;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.charset.CharsetEncoder;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicBoolean;
import static java.lang.Math.ceil;
import static java.nio.CharBuffer.wrap;
import static java.util.Objects.requireNonNull;
import static org.apache.maven.surefire.api.booter.Constants.DEFAULT_STREAM_ENCODING;
import static org.apache.maven.surefire.api.booter.Constants.MAGIC_NUMBER_BYTES;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_BYE;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_CONSOLE_DEBUG;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_CONSOLE_ERROR;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_CONSOLE_INFO;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_CONSOLE_WARNING;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_JVM_EXIT_ERROR;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_NEXT_TEST;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_STDERR;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_STDERR_NEW_LINE;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_STDOUT;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_STDOUT_NEW_LINE;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_STOP_ON_NEXT_TEST;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_SYSPROPS;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_TESTSET_COMPLETED;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_TESTSET_STARTING;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_TEST_ASSUMPTIONFAILURE;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_TEST_ERROR;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_TEST_FAILED;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_TEST_SKIPPED;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_TEST_STARTING;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_TEST_SUCCEEDED;
import static org.apache.maven.surefire.api.report.RunMode.NORMAL_RUN;
import static org.apache.maven.surefire.api.report.RunMode.RERUN_TEST_AFTER_FAILURE;
/**
* magic number : opcode : run mode [: opcode specific data]*
* <br>
*
* @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
* @since 3.0.0-M4
*/
@SuppressWarnings( "checkstyle:linelength" )
public class LegacyMasterProcessChannelEncoder implements MasterProcessChannelEncoder
{
private static final byte[] INT_BINARY = new byte[] {0, 0, 0, 0};
private static final byte BOOLEAN_NON_NULL_OBJECT = (byte) 0xff;
private static final byte BOOLEAN_NULL_OBJECT = (byte) 0;
private final WritableBufferedByteChannel out;
private final RunMode runMode;
private final AtomicBoolean trouble = new AtomicBoolean();
private volatile boolean onExit;
public LegacyMasterProcessChannelEncoder( @Nonnull WritableBufferedByteChannel out )
{
this( out, NORMAL_RUN );
}
protected LegacyMasterProcessChannelEncoder( @Nonnull WritableBufferedByteChannel out, @Nonnull RunMode runMode )
{
this.out = requireNonNull( out );
this.runMode = requireNonNull( runMode );
}
@Override
public MasterProcessChannelEncoder asRerunMode() // todo apply this and rework providers
{
return new LegacyMasterProcessChannelEncoder( out, RERUN_TEST_AFTER_FAILURE );
}
@Override
public MasterProcessChannelEncoder asNormalMode()
{
return new LegacyMasterProcessChannelEncoder( out, NORMAL_RUN );
}
@Override
public boolean checkError()
{
return trouble.get();
}
@Override
public void onJvmExit()
{
onExit = true;
encodeAndPrintEvent( ByteBuffer.wrap( new byte[] {'\n'} ), true );
}
@Override
public void sendSystemProperties( Map<String, String> sysProps )
{
CharsetEncoder encoder = DEFAULT_STREAM_ENCODING.newEncoder();
ByteBuffer result = null;
for ( Iterator<Entry<String, String>> it = sysProps.entrySet().iterator(); it.hasNext(); )
{
Entry<String, String> entry = it.next();
String key = entry.getKey();
String value = entry.getValue();
int bufferLength = estimateBufferLength( BOOTERCODE_SYSPROPS, runMode, encoder, 0, key, value );
result = result != null && result.capacity() >= bufferLength ? result : ByteBuffer.allocate( bufferLength );
result.clear();
// :maven-surefire-event:sys-prop:rerun-test-after-failure:UTF-8:0000000000:<key>:0000000000:<value>:
encode( encoder, result, BOOTERCODE_SYSPROPS, runMode, key, value );
boolean sendImmediately = !it.hasNext();
encodeAndPrintEvent( result, sendImmediately );
}
}
@Override
public void testSetStarting( ReportEntry reportEntry, boolean trimStackTraces )
{
encode( BOOTERCODE_TESTSET_STARTING, runMode, reportEntry, trimStackTraces, true );
}
@Override
public void testSetCompleted( ReportEntry reportEntry, boolean trimStackTraces )
{
encode( BOOTERCODE_TESTSET_COMPLETED, runMode, reportEntry, trimStackTraces, true );
}
@Override
public void testStarting( ReportEntry reportEntry, boolean trimStackTraces )
{
encode( BOOTERCODE_TEST_STARTING, runMode, reportEntry, trimStackTraces, true );
}
@Override
public void testSucceeded( ReportEntry reportEntry, boolean trimStackTraces )
{
encode( BOOTERCODE_TEST_SUCCEEDED, runMode, reportEntry, trimStackTraces, true );
}
@Override
public void testFailed( ReportEntry reportEntry, boolean trimStackTraces )
{
encode( BOOTERCODE_TEST_FAILED, runMode, reportEntry, trimStackTraces, true );
}
@Override
public void testSkipped( ReportEntry reportEntry, boolean trimStackTraces )
{
encode( BOOTERCODE_TEST_SKIPPED, runMode, reportEntry, trimStackTraces, true );
}
@Override
public void testError( ReportEntry reportEntry, boolean trimStackTraces )
{
encode( BOOTERCODE_TEST_ERROR, runMode, reportEntry, trimStackTraces, true );
}
@Override
public void testAssumptionFailure( ReportEntry reportEntry, boolean trimStackTraces )
{
encode( BOOTERCODE_TEST_ASSUMPTIONFAILURE, runMode, reportEntry, trimStackTraces, true );
}
@Override
public void stdOut( String msg, boolean newLine )
{
ForkedProcessEventType event = newLine ? BOOTERCODE_STDOUT_NEW_LINE : BOOTERCODE_STDOUT;
setOutErr( event, msg );
}
@Override
public void stdErr( String msg, boolean newLine )
{
ForkedProcessEventType event = newLine ? BOOTERCODE_STDERR_NEW_LINE : BOOTERCODE_STDERR;
setOutErr( event, msg );
}
private void setOutErr( ForkedProcessEventType eventType, String message )
{
ByteBuffer result = encodeMessage( eventType, runMode, message );
encodeAndPrintEvent( result, false );
}
@Override
public void consoleInfoLog( String message )
{
ByteBuffer result = encodeMessage( BOOTERCODE_CONSOLE_INFO, null, message );
encodeAndPrintEvent( result, true );
}
@Override
public void consoleErrorLog( String message )
{
consoleErrorLog( message, null );
}
@Override
public void consoleErrorLog( Throwable t )
{
consoleErrorLog( t.getLocalizedMessage(), t );
}
@Override
public void consoleErrorLog( String message, Throwable t )
{
CharsetEncoder encoder = DEFAULT_STREAM_ENCODING.newEncoder();
String stackTrace = t == null ? null : ConsoleLoggerUtils.toString( t );
int bufferMaxLength = estimateBufferLength( BOOTERCODE_CONSOLE_ERROR, null, encoder, 0, message, stackTrace );
ByteBuffer result = ByteBuffer.allocate( bufferMaxLength );
encodeHeader( encoder, result, BOOTERCODE_CONSOLE_ERROR, null );
encode( encoder, result, message, null, stackTrace );
encodeAndPrintEvent( result, true );
}
@Override
public void consoleErrorLog( StackTraceWriter stackTraceWriter, boolean trimStackTraces )
{
error( stackTraceWriter, trimStackTraces, BOOTERCODE_CONSOLE_ERROR, true );
}
@Override
public void consoleDebugLog( String message )
{
ByteBuffer result = encodeMessage( BOOTERCODE_CONSOLE_DEBUG, null, message );
encodeAndPrintEvent( result, true );
}
@Override
public void consoleWarningLog( String message )
{
ByteBuffer result = encodeMessage( BOOTERCODE_CONSOLE_WARNING, null, message );
encodeAndPrintEvent( result, true );
}
@Override
public void bye()
{
encodeOpcode( BOOTERCODE_BYE, true );
}
@Override
public void stopOnNextTest()
{
encodeOpcode( BOOTERCODE_STOP_ON_NEXT_TEST, true );
}
@Override
public void acquireNextTest()
{
encodeOpcode( BOOTERCODE_NEXT_TEST, true );
}
@Override
public void sendExitError( StackTraceWriter stackTraceWriter, boolean trimStackTraces )
{
error( stackTraceWriter, trimStackTraces, BOOTERCODE_JVM_EXIT_ERROR, true );
}
private void error( StackTraceWriter stackTraceWriter, boolean trimStackTraces, ForkedProcessEventType eventType,
@SuppressWarnings( "SameParameterValue" ) boolean sendImmediately )
{
CharsetEncoder encoder = DEFAULT_STREAM_ENCODING.newEncoder();
StackTrace stackTraceWrapper = new StackTrace( stackTraceWriter, trimStackTraces );
int bufferMaxLength = estimateBufferLength( eventType, null, encoder, 0,
stackTraceWrapper.message, stackTraceWrapper.smartTrimmedStackTrace, stackTraceWrapper.stackTrace );
ByteBuffer result = ByteBuffer.allocate( bufferMaxLength );
encodeHeader( encoder, result, eventType, null );
encode( encoder, result, stackTraceWrapper );
encodeAndPrintEvent( result, sendImmediately );
}
/**
* :maven-surefire-event:testset-starting:rerun-test-after-failure:UTF-8:0000000000:SourceName:0000000000:SourceText:0000000000:Name:0000000000:NameText:0000000000:Group:0000000000:Message:0000000000:ElapsedTime:0000000000:LocalizedMessage:0000000000:SmartTrimmedStackTrace:0000000000:toStackTrace( stw, trimStackTraces ):0000000000:
*
*/
private void encode( ForkedProcessEventType operation, RunMode runMode, ReportEntry reportEntry,
boolean trimStackTraces, @SuppressWarnings( "SameParameterValue" ) boolean sendImmediately )
{
ByteBuffer result = encode( operation, runMode, reportEntry, trimStackTraces );
encodeAndPrintEvent( result, sendImmediately );
}
private void encodeOpcode( ForkedProcessEventType eventType, boolean sendImmediately )
{
CharsetEncoder encoder = DEFAULT_STREAM_ENCODING.newEncoder();
int bufferMaxLength = estimateBufferLength( eventType, null, null, 0 );
ByteBuffer result = ByteBuffer.allocate( bufferMaxLength );
encodeOpcode( encoder, result, eventType, null );
encodeAndPrintEvent( result, sendImmediately );
}
private void encodeAndPrintEvent( ByteBuffer frame, boolean sendImmediately )
{
final boolean wasInterrupted = Thread.interrupted();
try
{
if ( sendImmediately )
{
out.write( frame );
}
else
{
out.writeBuffered( frame );
}
}
catch ( ClosedChannelException e )
{
if ( !onExit )
{
String event = new String( frame.array(), frame.arrayOffset() + frame.position(), frame.remaining(),
DEFAULT_STREAM_ENCODING );
DumpErrorSingleton.getSingleton()
.dumpException( e, "Channel closed while writing the event '" + event + "'." );
}
}
catch ( IOException e )
{
if ( trouble.compareAndSet( false, true ) )
{
DumpErrorSingleton.getSingleton()
.dumpException( e );
}
}
finally
{
if ( wasInterrupted )
{
Thread.currentThread().interrupt();
}
}
}
static void encode( CharsetEncoder encoder, ByteBuffer result,
ForkedProcessEventType operation, RunMode runMode, String... messages )
{
encodeHeader( encoder, result, operation, runMode );
for ( String message : messages )
{
encodeString( encoder, result, message );
}
}
static void encode( CharsetEncoder encoder, ByteBuffer result, StackTrace stw )
{
encode( encoder, result, stw.message, stw.smartTrimmedStackTrace, stw.stackTrace );
}
private static void encode( CharsetEncoder encoder, ByteBuffer result,
String message, String smartStackTrace, String stackTrace )
{
encodeString( encoder, result, message );
encodeString( encoder, result, smartStackTrace );
encodeString( encoder, result, stackTrace );
}
/**
* Used operations:<br>
* <ul>
* <li>{@link ForkedProcessEventType#BOOTERCODE_TESTSET_STARTING},</li>
* <li>{@link ForkedProcessEventType#BOOTERCODE_TESTSET_COMPLETED},</li>
* <li>{@link ForkedProcessEventType#BOOTERCODE_TEST_STARTING},</li>
* <li>{@link ForkedProcessEventType#BOOTERCODE_TEST_SUCCEEDED},</li>
* <li>{@link ForkedProcessEventType#BOOTERCODE_TEST_FAILED},</li>
* <li>{@link ForkedProcessEventType#BOOTERCODE_TEST_ERROR},</li>
* <li>{@link ForkedProcessEventType#BOOTERCODE_TEST_SKIPPED},</li>
* <li>{@link ForkedProcessEventType#BOOTERCODE_TEST_ASSUMPTIONFAILURE}.</li>
* </ul>
*/
static ByteBuffer encode( ForkedProcessEventType operation, RunMode runMode, ReportEntry reportEntry,
boolean trimStackTraces )
{
StackTrace stackTraceWrapper = new StackTrace( reportEntry.getStackTraceWriter(), trimStackTraces );
CharsetEncoder encoder = DEFAULT_STREAM_ENCODING.newEncoder();
int bufferMaxLength = estimateBufferLength( operation, runMode, encoder, 1, reportEntry.getSourceName(),
reportEntry.getSourceText(), reportEntry.getName(), reportEntry.getNameText(), reportEntry.getGroup(),
reportEntry.getMessage(), stackTraceWrapper.message, stackTraceWrapper.smartTrimmedStackTrace,
stackTraceWrapper.stackTrace );
ByteBuffer result = ByteBuffer.allocate( bufferMaxLength );
encodeHeader( encoder, result, operation, runMode );
encodeString( encoder, result, reportEntry.getSourceName() );
encodeString( encoder, result, reportEntry.getSourceText() );
encodeString( encoder, result, reportEntry.getName() );
encodeString( encoder, result, reportEntry.getNameText() );
encodeString( encoder, result, reportEntry.getGroup() );
encodeString( encoder, result, reportEntry.getMessage() );
encodeInteger( encoder, result, reportEntry.getElapsed() );
encode( encoder, result, stackTraceWrapper );
return result;
}
static ByteBuffer encodeMessage( ForkedProcessEventType eventType, RunMode runMode, String message )
{
CharsetEncoder encoder = DEFAULT_STREAM_ENCODING.newEncoder();
int bufferMaxLength = estimateBufferLength( eventType, runMode, encoder, 0, message );
ByteBuffer result = ByteBuffer.allocate( bufferMaxLength );
encodeHeader( encoder, result, eventType, runMode );
encodeString( encoder, result, message );
return result;
}
private static void encodeString( CharsetEncoder encoder, ByteBuffer result, String string )
{
String nonNullString = nonNull( string );
int counterPosition = result.position();
result.put( INT_BINARY ).put( (byte) ':' );
int msgStart = result.position();
encoder.encode( wrap( nonNullString ), result, true );
int msgEnd = result.position();
int encodedMsgSize = msgEnd - msgStart;
result.putInt( counterPosition, encodedMsgSize );
result.position( msgEnd );
result.put( (byte) ':' );
}
private static void encodeInteger( CharsetEncoder encoder, ByteBuffer result, Integer i )
{
if ( i == null )
{
result.put( BOOLEAN_NULL_OBJECT );
}
else
{
result.put( BOOLEAN_NON_NULL_OBJECT ).putInt( i );
}
result.put( (byte) ':' );
}
static void encodeHeader( CharsetEncoder encoder, ByteBuffer result, ForkedProcessEventType operation,
RunMode runMode )
{
encodeOpcode( encoder, result, operation, runMode );
String charsetName = encoder.charset().name();
result.put( (byte) charsetName.length() );
result.put( (byte) ':' );
encoder.encode( wrap( charsetName ), result, true );
result.put( (byte) ':' );
}
/**
* Used in {@link #bye()}, {@link #stopOnNextTest()} and {@link #encodeOpcode(ForkedProcessEventType, boolean)}
* and private methods extending the buffer.
*
* @param operation opcode
* @param runMode run mode
*/
static void encodeOpcode( CharsetEncoder encoder, ByteBuffer result, ForkedProcessEventType operation,
RunMode runMode )
{
result.put( (byte) ':' );
result.put( MAGIC_NUMBER_BYTES );
result.put( (byte) ':' );
byte[] opcode = operation.getOpcodeBinary();
result.put( (byte) opcode.length );
result.put( (byte) ':' );
result.put( opcode );
result.put( (byte) ':' );
if ( runMode != null )
{
byte[] runmode = runMode.getRunmodeBinary();
result.put( (byte) runmode.length );
result.put( (byte) ':' );
result.put( runmode );
result.put( (byte) ':' );
}
}
private static String toStackTrace( StackTraceWriter stw, boolean trimStackTraces )
{
if ( stw == null )
{
return null;
}
return trimStackTraces ? stw.writeTrimmedTraceToString() : stw.writeTraceToString();
}
static String nonNull( String msg )
{
return msg == null ? "\u0000" : msg;
}
static int estimateBufferLength( ForkedProcessEventType eventType, RunMode runMode, CharsetEncoder encoder,
int integersCounter, String... strings )
{
assert !( encoder == null && strings.length != 0 );
// one delimiter character ':' + <string> + one delimiter character ':' +
// one byte + one delimiter character ':' + <string> + one delimiter character ':'
int lengthOfMetadata = 1 + MAGIC_NUMBER_BYTES.length + 1 + 1 + 1 + eventType.getOpcode().length() + 1;
if ( runMode != null )
{
// one byte of length + one delimiter character ':' + <string> + one delimiter character ':'
lengthOfMetadata += 1 + 1 + runMode.geRunmode().length() + 1;
}
if ( encoder != null )
{
// one byte of length + one delimiter character ':' + <string> + one delimiter character ':'
lengthOfMetadata += 1 + 1 + encoder.charset().name().length() + 1;
}
// one byte (0x00 if NULL) + 4 bytes for integer + one delimiter character ':'
int lengthOfData = ( 1 + 4 + 1 ) * integersCounter;
for ( String string : strings )
{
String s = string == null ? "\u0000" : string;
// 4 bytes of string length + one delimiter character ':' + <string> + one delimiter character ':'
lengthOfData += 4 + 1 + (int) ceil( encoder.maxBytesPerChar() * s.length() ) + 1;
}
return lengthOfMetadata + lengthOfData;
}
private static final class StackTrace
{
final String message;
final String smartTrimmedStackTrace;
final String stackTrace;
StackTrace( StackTraceWriter stackTraceWriter, boolean trimStackTraces )
{
SafeThrowable throwable = stackTraceWriter == null ? null : stackTraceWriter.getThrowable();
message = throwable == null ? null : throwable.getLocalizedMessage();
smartTrimmedStackTrace = stackTraceWriter == null ? null : stackTraceWriter.smartTrimmedStackTrace();
stackTrace = stackTraceWriter == null ? null : toStackTrace( stackTraceWriter, trimStackTraces );
}
}
}