blob: 443fdcd2c9a0ee4dbce8f6e8fd1aa8e301392267 [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.shared.codec.binary.Base64;
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.Charset;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicBoolean;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;
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.booter.ForkedProcessEventType.MAGIC_NUMBER;
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
*/
public class LegacyMasterProcessChannelEncoder implements MasterProcessChannelEncoder
{
private static final Base64 BASE64 = new Base64();
private static final Charset STREAM_ENCODING = US_ASCII;
private static final Charset STRING_ENCODING = UTF_8;
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( new StringBuilder( "\n" ), true );
}
@Override
public void sendSystemProperties( Map<String, String> sysProps )
{
for ( Entry<String, String> entry : sysProps.entrySet() )
{
String key = entry.getKey();
String value = entry.getValue();
StringBuilder event = encode( BOOTERCODE_SYSPROPS, runMode, key, value );
encodeAndPrintEvent( event, false );
}
}
@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.getOpcode(), msg );
}
@Override
public void stdErr( String msg, boolean newLine )
{
ForkedProcessEventType event = newLine ? BOOTERCODE_STDERR_NEW_LINE : BOOTERCODE_STDERR;
setOutErr( event.getOpcode(), msg );
}
private void setOutErr( String eventType, String message )
{
String base64Message = toBase64( message );
StringBuilder event = encodeMessage( eventType, runMode.geRunName(), base64Message );
encodeAndPrintEvent( event, false );
}
@Override
public void consoleInfoLog( String msg )
{
StringBuilder event = print( BOOTERCODE_CONSOLE_INFO.getOpcode(), msg );
encodeAndPrintEvent( event, true );
}
@Override
public void consoleErrorLog( String msg )
{
StringBuilder encoded = encodeHeader( BOOTERCODE_CONSOLE_ERROR.getOpcode(), null );
encode( encoded, msg, null, null );
encodeAndPrintEvent( encoded, true );
}
@Override
public void consoleErrorLog( Throwable t )
{
consoleErrorLog( t.getLocalizedMessage(), t );
}
@Override
public void consoleErrorLog( String msg, Throwable t )
{
StringBuilder encoded = encodeHeader( BOOTERCODE_CONSOLE_ERROR.getOpcode(), null );
encode( encoded, msg, null, ConsoleLoggerUtils.toString( t ) );
encodeAndPrintEvent( encoded, true );
}
@Override
public void consoleErrorLog( StackTraceWriter stackTraceWriter, boolean trimStackTraces )
{
error( stackTraceWriter, trimStackTraces, BOOTERCODE_CONSOLE_ERROR, true );
}
@Override
public void consoleDebugLog( String msg )
{
StringBuilder event = print( BOOTERCODE_CONSOLE_DEBUG.getOpcode(), msg );
encodeAndPrintEvent( event, true );
}
@Override
public void consoleWarningLog( String msg )
{
StringBuilder event = print( BOOTERCODE_CONSOLE_WARNING.getOpcode(), msg );
encodeAndPrintEvent( event, 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 event,
@SuppressWarnings( "SameParameterValue" ) boolean sendImmediately )
{
StringBuilder encoded = encodeHeader( event.getOpcode(), null );
encode( encoded, stackTraceWriter, trimStackTraces );
encodeAndPrintEvent( encoded, sendImmediately );
}
private void encode( ForkedProcessEventType operation, RunMode runMode, ReportEntry reportEntry,
boolean trimStackTraces, @SuppressWarnings( "SameParameterValue" ) boolean sendImmediately )
{
StringBuilder event = encode( operation.getOpcode(), runMode.geRunName(), reportEntry, trimStackTraces );
encodeAndPrintEvent( event, sendImmediately );
}
private void encodeOpcode( ForkedProcessEventType operation, boolean sendImmediately )
{
StringBuilder event = encodeOpcode( operation.getOpcode(), null );
encodeAndPrintEvent( event, sendImmediately );
}
private void encodeAndPrintEvent( StringBuilder event, boolean sendImmediately )
{
try
{
//noinspection ResultOfMethodCallIgnored
Thread.interrupted();
byte[] array = event.append( '\n' )
.toString()
.getBytes( STREAM_ENCODING );
ByteBuffer bb = ByteBuffer.wrap( array );
if ( sendImmediately )
{
out.write( bb );
}
else
{
out.writeBuffered( bb );
}
}
catch ( ClosedChannelException e )
{
if ( !onExit )
{
DumpErrorSingleton.getSingleton()
.dumpException( e, "Channel closed while writing the event '" + event + "'." );
}
}
catch ( IOException e )
{
if ( trouble.compareAndSet( false, true ) )
{
DumpErrorSingleton.getSingleton()
.dumpException( e );
}
}
}
static StringBuilder encode( ForkedProcessEventType operation, RunMode runMode, String... args )
{
StringBuilder encodedTo = encodeHeader( operation.getOpcode(), runMode.geRunName() );
for ( int i = 0; i < args.length; )
{
String arg = args[i++];
encodedTo.append( toBase64( arg ) )
.append( ':' );
}
return encodedTo;
}
static void encode( StringBuilder encoded, StackTraceWriter stw, boolean trimStackTraces )
{
SafeThrowable throwable = stw == null ? null : stw.getThrowable();
String message = throwable == null ? null : throwable.getLocalizedMessage();
String smartStackTrace = stw == null ? null : stw.smartTrimmedStackTrace();
String stackTrace = stw == null ? null : toStackTrace( stw, trimStackTraces );
encode( encoded, message, smartStackTrace, stackTrace );
}
private static void encode( StringBuilder encoded, String message, String smartStackTrace, String stackTrace )
{
encoded.append( toBase64( message ) )
.append( ':' )
.append( toBase64( smartStackTrace ) )
.append( ':' )
.append( toBase64( stackTrace ) )
.append( ':' );
}
/**
* 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 StringBuilder encode( String operation, String runMode, ReportEntry reportEntry, boolean trimStackTraces )
{
StringBuilder encodedTo = encodeHeader( operation, runMode )
.append( toBase64( reportEntry.getSourceName() ) )
.append( ':' )
.append( toBase64( reportEntry.getSourceText() ) )
.append( ':' )
.append( toBase64( reportEntry.getName() ) )
.append( ':' )
.append( toBase64( reportEntry.getNameText() ) )
.append( ':' )
.append( toBase64( reportEntry.getGroup() ) )
.append( ':' )
.append( toBase64( reportEntry.getMessage() ) )
.append( ':' )
.append( reportEntry.getElapsed() == null ? "-" : reportEntry.getElapsed().toString() )
.append( ':' );
encode( encodedTo, reportEntry.getStackTraceWriter(), trimStackTraces );
return encodedTo;
}
/**
* Used in {@link #consoleInfoLog(String)}, {@link #consoleErrorLog(String)}, {@link #consoleDebugLog(String)},
* {@link #consoleWarningLog(String)} and private methods extending the buffer.
*/
StringBuilder print( String operation, String... msgs )
{
String[] encodedMsgs = new String[msgs.length];
for ( int i = 0; i < encodedMsgs.length; i++ )
{
String msg = msgs[i];
encodedMsgs[i] = toBase64( msg );
}
return encodeMessage( operation, null, encodedMsgs );
}
static StringBuilder encodeMessage( String operation, String runMode, String... encodedMsgs )
{
StringBuilder builder = encodeHeader( operation, runMode );
for ( String encodedMsg : encodedMsgs )
{
builder.append( encodedMsg ).append( ':' );
}
return builder;
}
static StringBuilder encodeHeader( String operation, String runMode )
{
return encodeOpcode( operation, runMode )
.append( STRING_ENCODING.name() )
.append( ':' );
}
/**
* Used in {@link #bye()}, {@link #stopOnNextTest()} and {@link #encodeOpcode(ForkedProcessEventType, boolean)}
* and private methods extending the buffer.
*
* @param operation opcode
* @param runMode run mode
* @return encoded event
*/
static StringBuilder encodeOpcode( String operation, String runMode )
{
StringBuilder s = new StringBuilder( 128 )
.append( ':' )
.append( MAGIC_NUMBER )
.append( ':' )
.append( operation )
.append( ':' );
return runMode == null ? s : s.append( runMode ).append( ':' );
}
private static String toStackTrace( StackTraceWriter stw, boolean trimStackTraces )
{
return trimStackTraces ? stw.writeTrimmedTraceToString() : stw.writeTraceToString();
}
static String toBase64( String msg )
{
return msg == null ? "-" : new String( BASE64.encode( msg.getBytes( STRING_ENCODING ) ), STREAM_ENCODING );
}
}