All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.maven.plugin.surefire.extensions.EventConsumerThread Maven / Gradle / Ivy

There is a newer version: 3.5.2
Show newest version
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 eventHandler;
    private final CountdownCloseable countdownCloseable;
    private final ForkNodeArguments arguments;
    private volatile boolean disabled;

    public EventConsumerThread( @Nonnull String threadName,
                                @Nonnull ReadableByteChannel channel,
                                @Nonnull EventHandler 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 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 tokensInFrame )
    {
        Iterator 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 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.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
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy