blob: 1d5686b0120bfdeb84d112b8731162b27157eff1 [file] [log] [blame]
package org.apache.maven.plugin.surefire.extensions;
/*
* 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.output.DeserializedStacktraceWriter;
import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
import org.apache.maven.surefire.api.booter.ForkedProcessEventType;
import org.apache.maven.surefire.api.event.ConsoleDebugEvent;
import org.apache.maven.surefire.api.event.ConsoleErrorEvent;
import org.apache.maven.surefire.api.event.ConsoleInfoEvent;
import org.apache.maven.surefire.api.event.ConsoleWarningEvent;
import org.apache.maven.surefire.api.event.ControlByeEvent;
import org.apache.maven.surefire.api.event.ControlNextTestEvent;
import org.apache.maven.surefire.api.event.ControlStopOnNextTestEvent;
import org.apache.maven.surefire.api.event.Event;
import org.apache.maven.surefire.api.event.JvmExitErrorEvent;
import org.apache.maven.surefire.api.event.StandardStreamErrEvent;
import org.apache.maven.surefire.api.event.StandardStreamErrWithNewLineEvent;
import org.apache.maven.surefire.api.event.StandardStreamOutEvent;
import org.apache.maven.surefire.api.event.StandardStreamOutWithNewLineEvent;
import org.apache.maven.surefire.api.event.SystemPropertyEvent;
import org.apache.maven.surefire.api.event.TestAssumptionFailureEvent;
import org.apache.maven.surefire.api.event.TestErrorEvent;
import org.apache.maven.surefire.api.event.TestFailedEvent;
import org.apache.maven.surefire.api.event.TestSkippedEvent;
import org.apache.maven.surefire.api.event.TestStartingEvent;
import org.apache.maven.surefire.api.event.TestSucceededEvent;
import org.apache.maven.surefire.api.event.TestsetCompletedEvent;
import org.apache.maven.surefire.api.event.TestsetStartingEvent;
import org.apache.maven.surefire.extensions.CloseableDaemonThread;
import org.apache.maven.surefire.extensions.EventHandler;
import org.apache.maven.surefire.extensions.ForkNodeArguments;
import org.apache.maven.surefire.extensions.util.CountdownCloseable;
import org.apache.maven.surefire.api.report.RunMode;
import org.apache.maven.surefire.api.report.StackTraceWriter;
import org.apache.maven.surefire.api.report.TestSetReportEntry;
import org.apache.maven.surefire.shared.codec.binary.Base64;
import javax.annotation.Nonnull;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.MAGIC_NUMBER;
import static org.apache.maven.surefire.api.report.CategorizedReportEntry.reportEntry;
import static org.apache.maven.surefire.api.report.RunMode.MODES;
/**
*
*/
public class EventConsumerThread extends CloseableDaemonThread
{
private static final String[] JVM_ERROR_PATTERNS =
{
"could not create the java virtual machine",
"error occurred during initialization", // of VM, of boot layer
"error:", // general errors
"could not reserve enough space", "could not allocate", "unable to allocate", // memory errors
"java.lang.module.findexception" // JPMS errors
};
private static final String PRINTABLE_JVM_NATIVE_STREAM = "Listening for transport dt_socket at address:";
private static final Base64 BASE64 = new Base64();
private final ReadableByteChannel channel;
private final EventHandler<Event> eventHandler;
private final CountdownCloseable countdownCloseable;
private final ForkNodeArguments arguments;
private volatile boolean disabled;
public EventConsumerThread( @Nonnull String threadName,
@Nonnull ReadableByteChannel channel,
@Nonnull EventHandler<Event> eventHandler,
@Nonnull CountdownCloseable countdownCloseable,
@Nonnull ForkNodeArguments arguments )
{
super( threadName );
this.channel = channel;
this.eventHandler = eventHandler;
this.countdownCloseable = countdownCloseable;
this.arguments = arguments;
}
@Override
public void run()
{
try ( ReadableByteChannel stream = channel;
CountdownCloseable c = countdownCloseable; )
{
decode();
}
catch ( IOException e )
{
// not needed
}
}
@Override
public void disable()
{
disabled = true;
}
@Override
public void close() throws IOException
{
channel.close();
}
@SuppressWarnings( "checkstyle:innerassignment" )
private void decode() throws IOException
{
List<String> tokens = new ArrayList<>();
StringBuilder line = new StringBuilder();
StringBuilder token = new StringBuilder( MAGIC_NUMBER.length() );
ByteBuffer buffer = ByteBuffer.allocate( 1024 );
buffer.position( buffer.limit() );
boolean streamContinues;
start:
do
{
line.setLength( 0 );
tokens.clear();
token.setLength( 0 );
FrameCompletion completion = null;
for ( boolean frameStarted = false; streamContinues = read( buffer ); completion = null )
{
char c = (char) buffer.get();
if ( c == '\n' || c == '\r' )
{
printExistingLine( line );
continue start;
}
line.append( c );
if ( !frameStarted )
{
if ( c == ':' )
{
frameStarted = true;
token.setLength( 0 );
tokens.clear();
}
}
else
{
if ( c == ':' )
{
tokens.add( token.toString() );
token.setLength( 0 );
completion = frameCompleteness( tokens );
if ( completion == FrameCompletion.COMPLETE )
{
line.setLength( 0 );
break;
}
else if ( completion == FrameCompletion.MALFORMED )
{
printExistingLine( line );
continue start;
}
}
else
{
token.append( c );
}
}
}
if ( completion == FrameCompletion.COMPLETE )
{
Event event = toEvent( tokens );
if ( !disabled && event != null )
{
eventHandler.handleEvent( event );
}
}
if ( !streamContinues )
{
printExistingLine( line );
return;
}
}
while ( true );
}
private boolean read( ByteBuffer buffer ) throws IOException
{
if ( buffer.hasRemaining() && buffer.position() > 0 )
{
return true;
}
else
{
buffer.clear();
boolean isEndOfStream = channel.read( buffer ) == -1;
buffer.flip();
return !isEndOfStream;
}
}
private void printExistingLine( StringBuilder line )
{
if ( line.length() != 0 )
{
ConsoleLogger logger = arguments.getConsoleLogger();
String s = line.toString().trim();
if ( s.contains( PRINTABLE_JVM_NATIVE_STREAM ) )
{
if ( logger.isDebugEnabled() )
{
logger.debug( s );
}
else if ( logger.isInfoEnabled() )
{
logger.info( s );
}
else
{
// In case of debugging forked JVM, see PRINTABLE_JVM_NATIVE_STREAM.
System.out.println( s );
}
}
else
{
if ( isJvmError( s ) )
{
logger.error( s );
}
String msg = "Corrupted STDOUT by directly writing to native stream in forked JVM "
+ arguments.getForkChannelId() + ".";
File dumpFile = arguments.dumpStreamText( msg + " Stream '" + s + "'." );
arguments.logWarningAtEnd( msg + " See FAQ web page and the dump file " + dumpFile.getAbsolutePath() );
if ( logger.isDebugEnabled() )
{
logger.debug( s );
}
}
}
}
private Event toEvent( List<String> tokensInFrame )
{
Iterator<String> tokens = tokensInFrame.iterator();
String header = tokens.next();
assert header != null;
ForkedProcessEventType event = ForkedProcessEventType.byOpcode( tokens.next() );
if ( event == null )
{
return null;
}
if ( event.isControlCategory() )
{
switch ( event )
{
case BOOTERCODE_BYE:
return new ControlByeEvent();
case BOOTERCODE_STOP_ON_NEXT_TEST:
return new ControlStopOnNextTestEvent();
case BOOTERCODE_NEXT_TEST:
return new ControlNextTestEvent();
default:
throw new IllegalStateException( "Unknown enum " + event );
}
}
else if ( event.isConsoleErrorCategory() || event.isJvmExitError() )
{
Charset encoding = Charset.forName( tokens.next() );
StackTraceWriter stackTraceWriter = decodeTrace( encoding, tokens.next(), tokens.next(), tokens.next() );
return event.isConsoleErrorCategory()
? new ConsoleErrorEvent( stackTraceWriter )
: new JvmExitErrorEvent( stackTraceWriter );
}
else if ( event.isConsoleCategory() )
{
Charset encoding = Charset.forName( tokens.next() );
String msg = decode( tokens.next(), encoding );
switch ( event )
{
case BOOTERCODE_CONSOLE_INFO:
return new ConsoleInfoEvent( msg );
case BOOTERCODE_CONSOLE_DEBUG:
return new ConsoleDebugEvent( msg );
case BOOTERCODE_CONSOLE_WARNING:
return new ConsoleWarningEvent( msg );
default:
throw new IllegalStateException( "Unknown enum " + event );
}
}
else if ( event.isStandardStreamCategory() )
{
RunMode mode = MODES.get( tokens.next() );
Charset encoding = Charset.forName( tokens.next() );
String output = decode( tokens.next(), encoding );
switch ( event )
{
case BOOTERCODE_STDOUT:
return new StandardStreamOutEvent( mode, output );
case BOOTERCODE_STDOUT_NEW_LINE:
return new StandardStreamOutWithNewLineEvent( mode, output );
case BOOTERCODE_STDERR:
return new StandardStreamErrEvent( mode, output );
case BOOTERCODE_STDERR_NEW_LINE:
return new StandardStreamErrWithNewLineEvent( mode, output );
default:
throw new IllegalStateException( "Unknown enum " + event );
}
}
else if ( event.isSysPropCategory() )
{
RunMode mode = MODES.get( tokens.next() );
Charset encoding = Charset.forName( tokens.next() );
String key = decode( tokens.next(), encoding );
String value = decode( tokens.next(), encoding );
return new SystemPropertyEvent( mode, key, value );
}
else if ( event.isTestCategory() )
{
RunMode mode = MODES.get( tokens.next() );
Charset encoding = Charset.forName( tokens.next() );
TestSetReportEntry reportEntry =
decodeReportEntry( encoding, tokens.next(), tokens.next(), tokens.next(), tokens.next(),
tokens.next(), tokens.next(), tokens.next(), tokens.next(), tokens.next(), tokens.next() );
switch ( event )
{
case BOOTERCODE_TESTSET_STARTING:
return new TestsetStartingEvent( mode, reportEntry );
case BOOTERCODE_TESTSET_COMPLETED:
return new TestsetCompletedEvent( mode, reportEntry );
case BOOTERCODE_TEST_STARTING:
return new TestStartingEvent( mode, reportEntry );
case BOOTERCODE_TEST_SUCCEEDED:
return new TestSucceededEvent( mode, reportEntry );
case BOOTERCODE_TEST_FAILED:
return new TestFailedEvent( mode, reportEntry );
case BOOTERCODE_TEST_SKIPPED:
return new TestSkippedEvent( mode, reportEntry );
case BOOTERCODE_TEST_ERROR:
return new TestErrorEvent( mode, reportEntry );
case BOOTERCODE_TEST_ASSUMPTIONFAILURE:
return new TestAssumptionFailureEvent( mode, reportEntry );
default:
throw new IllegalStateException( "Unknown enum " + event );
}
}
throw new IllegalStateException( "Missing a branch for the event type " + event );
}
private static FrameCompletion frameCompleteness( List<String> tokens )
{
if ( !tokens.isEmpty() && !MAGIC_NUMBER.equals( tokens.get( 0 ) ) )
{
return FrameCompletion.MALFORMED;
}
if ( tokens.size() >= 2 )
{
String opcode = tokens.get( 1 );
ForkedProcessEventType event = ForkedProcessEventType.byOpcode( opcode );
if ( event == null )
{
return FrameCompletion.MALFORMED;
}
else if ( event.isControlCategory() )
{
return FrameCompletion.COMPLETE;
}
else if ( event.isConsoleErrorCategory() )
{
return tokens.size() == 6 ? FrameCompletion.COMPLETE : FrameCompletion.NOT_COMPLETE;
}
else if ( event.isConsoleCategory() )
{
return tokens.size() == 4 ? FrameCompletion.COMPLETE : FrameCompletion.NOT_COMPLETE;
}
else if ( event.isStandardStreamCategory() )
{
return tokens.size() == 5 ? FrameCompletion.COMPLETE : FrameCompletion.NOT_COMPLETE;
}
else if ( event.isSysPropCategory() )
{
return tokens.size() == 6 ? FrameCompletion.COMPLETE : FrameCompletion.NOT_COMPLETE;
}
else if ( event.isTestCategory() )
{
return tokens.size() == 14 ? FrameCompletion.COMPLETE : FrameCompletion.NOT_COMPLETE;
}
else if ( event.isJvmExitError() )
{
return tokens.size() == 6 ? FrameCompletion.COMPLETE : FrameCompletion.NOT_COMPLETE;
}
}
return FrameCompletion.NOT_COMPLETE;
}
static String decode( String line, Charset encoding )
{
// ForkedChannelEncoder is encoding the stream with US_ASCII
return line == null || "-".equals( line )
? null
: new String( BASE64.decode( line.getBytes( US_ASCII ) ), encoding );
}
private static StackTraceWriter decodeTrace( Charset encoding, String encTraceMessage,
String encSmartTrimmedStackTrace, String encStackTrace )
{
String traceMessage = decode( encTraceMessage, encoding );
String stackTrace = decode( encStackTrace, encoding );
String smartTrimmedStackTrace = decode( encSmartTrimmedStackTrace, encoding );
boolean exists = traceMessage != null || stackTrace != null || smartTrimmedStackTrace != null;
return exists ? new DeserializedStacktraceWriter( traceMessage, smartTrimmedStackTrace, stackTrace ) : null;
}
static TestSetReportEntry decodeReportEntry( Charset encoding,
// ReportEntry:
String encSource, String encSourceText, String encName,
String encNameText, String encGroup, String encMessage,
String encTimeElapsed,
// StackTraceWriter:
String encTraceMessage,
String encSmartTrimmedStackTrace, String encStackTrace )
throws NumberFormatException
{
if ( encoding == null )
{
// corrupted or incomplete stream
return null;
}
String source = decode( encSource, encoding );
String sourceText = decode( encSourceText, encoding );
String name = decode( encName, encoding );
String nameText = decode( encNameText, encoding );
String group = decode( encGroup, encoding );
StackTraceWriter stackTraceWriter =
decodeTrace( encoding, encTraceMessage, encSmartTrimmedStackTrace, encStackTrace );
Integer elapsed = decodeToInteger( encTimeElapsed );
String message = decode( encMessage, encoding );
return reportEntry( source, sourceText, name, nameText,
group, stackTraceWriter, elapsed, message, Collections.<String, String>emptyMap() );
}
static Integer decodeToInteger( String line )
{
return line == null || "-".equals( line ) ? null : Integer.decode( line );
}
private static boolean isJvmError( String line )
{
String lineLower = line.toLowerCase();
for ( String errorPattern : JVM_ERROR_PATTERNS )
{
if ( lineLower.contains( errorPattern ) )
{
return true;
}
}
return false;
}
/**
* Determines whether the frame is complete or malformed.
*/
private enum FrameCompletion
{
NOT_COMPLETE,
COMPLETE,
MALFORMED
}
}