| package org.apache.maven.surefire.stream; |
| |
| /* |
| * 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.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.api.fork.ForkNodeArguments; |
| 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.api.stream.AbstractStreamDecoder; |
| import org.apache.maven.surefire.api.stream.SegmentType; |
| |
| import javax.annotation.Nonnull; |
| import java.io.BufferedOutputStream; |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.nio.channels.ReadableByteChannel; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.concurrent.FutureTask; |
| |
| import static java.util.Collections.emptyMap; |
| import static org.apache.maven.surefire.api.booter.Constants.MAGIC_NUMBER_FOR_EVENTS_BYTES; |
| import static org.apache.maven.surefire.api.report.CategorizedReportEntry.reportEntry; |
| import static org.apache.maven.surefire.api.stream.SegmentType.DATA_INTEGER; |
| import static org.apache.maven.surefire.api.stream.SegmentType.DATA_STRING; |
| import static org.apache.maven.surefire.api.stream.SegmentType.END_OF_FRAME; |
| import static org.apache.maven.surefire.api.stream.SegmentType.RUN_MODE; |
| import static org.apache.maven.surefire.api.stream.SegmentType.STRING_ENCODING; |
| import static org.apache.maven.surefire.api.stream.SegmentType.TEST_RUN_ID; |
| import static org.apache.maven.surefire.shared.utils.cli.ShutdownHookUtils.addShutDownHook; |
| |
| /** |
| * |
| */ |
| public class EventDecoder extends AbstractStreamDecoder<Event, ForkedProcessEventType, SegmentType> |
| { |
| private static final int DEBUG_SINK_BUFFER_SIZE = 64 * 1024; |
| // due to have fast and thread-safe Map |
| private static final Map<Segment, ForkedProcessEventType> EVENT_TYPES = segmentsToEvents(); |
| private static final Map<Segment, RunMode> RUN_MODES = segmentsToRunModes(); |
| |
| private static final SegmentType[] EVENT_WITHOUT_DATA = new SegmentType[] { |
| END_OF_FRAME |
| }; |
| |
| private static final SegmentType[] EVENT_WITH_ERROR_TRACE = new SegmentType[] { |
| STRING_ENCODING, |
| DATA_STRING, |
| DATA_STRING, |
| DATA_STRING, |
| END_OF_FRAME |
| }; |
| |
| private static final SegmentType[] EVENT_WITH_ONE_STRING = new SegmentType[] { |
| STRING_ENCODING, |
| DATA_STRING, |
| END_OF_FRAME |
| }; |
| |
| private static final SegmentType[] EVENT_WITH_RUNMODE_TID_AND_ONE_STRING = new SegmentType[] { |
| RUN_MODE, |
| TEST_RUN_ID, |
| STRING_ENCODING, |
| DATA_STRING, |
| END_OF_FRAME |
| }; |
| |
| private static final SegmentType[] EVENT_WITH_RUNMODE_TID_AND_TWO_STRINGS = new SegmentType[] { |
| RUN_MODE, |
| TEST_RUN_ID, |
| STRING_ENCODING, |
| DATA_STRING, |
| DATA_STRING, |
| END_OF_FRAME |
| }; |
| |
| private static final SegmentType[] EVENT_TEST_CONTROL = new SegmentType[] { |
| RUN_MODE, |
| TEST_RUN_ID, |
| STRING_ENCODING, |
| DATA_STRING, |
| DATA_STRING, |
| DATA_STRING, |
| DATA_STRING, |
| DATA_STRING, |
| DATA_STRING, |
| DATA_INTEGER, |
| DATA_STRING, |
| DATA_STRING, |
| DATA_STRING, |
| END_OF_FRAME |
| }; |
| |
| private static final int NO_POSITION = -1; |
| |
| private final OutputStream debugSink; |
| |
| private Memento memento; |
| |
| public EventDecoder( @Nonnull ReadableByteChannel channel, |
| @Nonnull ForkNodeArguments arguments ) |
| { |
| super( channel, arguments, EVENT_TYPES ); |
| debugSink = newDebugSink( arguments ); |
| } |
| |
| @Override |
| public Event decode() throws IOException |
| { |
| if ( memento == null ) |
| { |
| // do not create memento in constructor because the constructor is called in another thread |
| // memento is the thread confinement object |
| memento = new Memento(); |
| } |
| |
| try |
| { |
| ForkedProcessEventType eventType = readMessageType( memento ); |
| if ( eventType == null ) |
| { |
| throw new MalformedFrameException( memento.getLine().getPositionByteBuffer(), |
| memento.getByteBuffer().position() ); |
| } |
| |
| for ( SegmentType segmentType : nextSegmentType( eventType ) ) |
| { |
| switch ( segmentType ) |
| { |
| case RUN_MODE: |
| memento.getData().add( RUN_MODES.get( readSegment( memento ) ) ); |
| break; |
| case TEST_RUN_ID: |
| memento.getData().add( readLong( memento ) ); |
| break; |
| case STRING_ENCODING: |
| memento.setCharset( readCharset( memento ) ); |
| break; |
| case DATA_STRING: |
| memento.getData().add( readString( memento ) ); |
| break; |
| case DATA_INTEGER: |
| memento.getData().add( readInteger( memento ) ); |
| break; |
| case END_OF_FRAME: |
| memento.getLine().setPositionByteBuffer( memento.getByteBuffer().position() ); |
| memento.getLine().clear(); |
| return toMessage( eventType, memento ); |
| default: |
| memento.getLine().setPositionByteBuffer( NO_POSITION ); |
| getArguments() |
| .dumpStreamText( "Unknown enum (" |
| + SegmentType.class.getSimpleName() |
| + ") " |
| + segmentType ); |
| } |
| } |
| } |
| catch ( MalformedFrameException e ) |
| { |
| if ( e.hasValidPositions() ) |
| { |
| int length = e.readTo() - e.readFrom(); |
| memento.getLine().write( memento.getByteBuffer(), e.readFrom(), length ); |
| } |
| return null; |
| } |
| catch ( RuntimeException e ) |
| { |
| getArguments().dumpStreamException( e ); |
| return null; |
| } |
| catch ( IOException e ) |
| { |
| if ( !( e.getCause() instanceof InterruptedException ) ) |
| { |
| printRemainingStream( memento ); |
| } |
| throw e; |
| } |
| finally |
| { |
| memento.reset(); |
| } |
| |
| throw new IOException( "unreachable statement" ); |
| } |
| |
| @Nonnull |
| @Override |
| protected final byte[] getEncodedMagicNumber() |
| { |
| return MAGIC_NUMBER_FOR_EVENTS_BYTES; |
| } |
| |
| @Override |
| @Nonnull |
| protected final SegmentType[] nextSegmentType( @Nonnull ForkedProcessEventType eventType ) |
| { |
| switch ( eventType ) |
| { |
| case BOOTERCODE_BYE: |
| case BOOTERCODE_STOP_ON_NEXT_TEST: |
| case BOOTERCODE_NEXT_TEST: |
| return EVENT_WITHOUT_DATA; |
| case BOOTERCODE_CONSOLE_ERROR: |
| case BOOTERCODE_JVM_EXIT_ERROR: |
| return EVENT_WITH_ERROR_TRACE; |
| case BOOTERCODE_CONSOLE_INFO: |
| case BOOTERCODE_CONSOLE_DEBUG: |
| case BOOTERCODE_CONSOLE_WARNING: |
| return EVENT_WITH_ONE_STRING; |
| case BOOTERCODE_STDOUT: |
| case BOOTERCODE_STDOUT_NEW_LINE: |
| case BOOTERCODE_STDERR: |
| case BOOTERCODE_STDERR_NEW_LINE: |
| return EVENT_WITH_RUNMODE_TID_AND_ONE_STRING; |
| case BOOTERCODE_SYSPROPS: |
| return EVENT_WITH_RUNMODE_TID_AND_TWO_STRINGS; |
| case BOOTERCODE_TESTSET_STARTING: |
| case BOOTERCODE_TESTSET_COMPLETED: |
| case BOOTERCODE_TEST_STARTING: |
| case BOOTERCODE_TEST_SUCCEEDED: |
| case BOOTERCODE_TEST_FAILED: |
| case BOOTERCODE_TEST_SKIPPED: |
| case BOOTERCODE_TEST_ERROR: |
| case BOOTERCODE_TEST_ASSUMPTIONFAILURE: |
| return EVENT_TEST_CONTROL; |
| default: |
| throw new IllegalArgumentException( "Unknown enum " + eventType ); |
| } |
| } |
| |
| @Override |
| @Nonnull |
| protected final Event toMessage( @Nonnull ForkedProcessEventType eventType, @Nonnull Memento memento ) |
| throws MalformedFrameException |
| { |
| switch ( eventType ) |
| { |
| case BOOTERCODE_BYE: |
| checkArguments( memento, 0 ); |
| return new ControlByeEvent(); |
| case BOOTERCODE_STOP_ON_NEXT_TEST: |
| checkArguments( memento, 0 ); |
| return new ControlStopOnNextTestEvent(); |
| case BOOTERCODE_NEXT_TEST: |
| checkArguments( memento, 0 ); |
| return new ControlNextTestEvent(); |
| case BOOTERCODE_JVM_EXIT_ERROR: |
| checkArguments( memento, 3 ); |
| return new JvmExitErrorEvent( toStackTraceWriter( memento.getData() ) ); |
| case BOOTERCODE_CONSOLE_ERROR: |
| checkArguments( memento, 3 ); |
| return new ConsoleErrorEvent( toStackTraceWriter( memento.getData() ) ); |
| case BOOTERCODE_CONSOLE_INFO: |
| checkArguments( memento, 1 ); |
| return new ConsoleInfoEvent( (String) memento.getData().get( 0 ) ); |
| case BOOTERCODE_CONSOLE_DEBUG: |
| checkArguments( memento, 1 ); |
| return new ConsoleDebugEvent( (String) memento.getData().get( 0 ) ); |
| case BOOTERCODE_CONSOLE_WARNING: |
| checkArguments( memento, 1 ); |
| return new ConsoleWarningEvent( (String) memento.getData().get( 0 ) ); |
| case BOOTERCODE_STDOUT: |
| checkArguments( memento, 3 ); |
| return new StandardStreamOutEvent( memento.ofDataAt( 0 ), memento.ofDataAt( 1 ), |
| memento.ofDataAt( 2 ) ); |
| case BOOTERCODE_STDOUT_NEW_LINE: |
| checkArguments( memento, 3 ); |
| return new StandardStreamOutWithNewLineEvent( memento.ofDataAt( 0 ), memento.ofDataAt( 1 ), |
| memento.ofDataAt( 2 ) ); |
| case BOOTERCODE_STDERR: |
| checkArguments( memento, 3 ); |
| return new StandardStreamErrEvent( memento.ofDataAt( 0 ), memento.ofDataAt( 1 ), |
| memento.ofDataAt( 2 ) ); |
| case BOOTERCODE_STDERR_NEW_LINE: |
| checkArguments( memento, 3 ); |
| return new StandardStreamErrWithNewLineEvent( memento.ofDataAt( 0 ), memento.ofDataAt( 1 ), |
| memento.ofDataAt( 2 ) ); |
| case BOOTERCODE_SYSPROPS: |
| checkArguments( memento, 4 ); |
| return new SystemPropertyEvent( memento.ofDataAt( 0 ), memento.ofDataAt( 1 ), |
| memento.ofDataAt( 2 ), memento.ofDataAt( 3 ) ); |
| case BOOTERCODE_TESTSET_STARTING: |
| checkArguments( memento, 12 ); |
| return new TestsetStartingEvent( toReportEntry( memento.getData() ) ); |
| case BOOTERCODE_TESTSET_COMPLETED: |
| checkArguments( memento, 12 ); |
| return new TestsetCompletedEvent( toReportEntry( memento.getData() ) ); |
| case BOOTERCODE_TEST_STARTING: |
| checkArguments( memento, 12 ); |
| return new TestStartingEvent( toReportEntry( memento.getData() ) ); |
| case BOOTERCODE_TEST_SUCCEEDED: |
| checkArguments( memento, 12 ); |
| return new TestSucceededEvent( toReportEntry( memento.getData() ) ); |
| case BOOTERCODE_TEST_FAILED: |
| checkArguments( memento, 12 ); |
| return new TestFailedEvent( toReportEntry( memento.getData() ) ); |
| case BOOTERCODE_TEST_SKIPPED: |
| checkArguments( memento, 12 ); |
| return new TestSkippedEvent( toReportEntry( memento.getData() ) ); |
| case BOOTERCODE_TEST_ERROR: |
| checkArguments( memento, 12 ); |
| return new TestErrorEvent( toReportEntry( memento.getData() ) ); |
| case BOOTERCODE_TEST_ASSUMPTIONFAILURE: |
| checkArguments( memento, 12 ); |
| return new TestAssumptionFailureEvent( toReportEntry( memento.getData() ) ); |
| default: |
| throw new IllegalArgumentException( "Missing a branch for the event type " + eventType ); |
| } |
| } |
| |
| @Nonnull |
| private static TestSetReportEntry toReportEntry( List<Object> args ) |
| { |
| RunMode runMode = (RunMode) args.get( 0 ); |
| long testRunId = (long) args.get( 1 ); |
| // ReportEntry: |
| String source = (String) args.get( 2 ); |
| String sourceText = (String) args.get( 3 ); |
| String name = (String) args.get( 4 ); |
| String nameText = (String) args.get( 5 ); |
| String group = (String) args.get( 6 ); |
| String message = (String) args.get( 7 ); |
| Integer timeElapsed = (Integer) args.get( 8 ); |
| // StackTraceWriter: |
| String traceMessage = (String) args.get( 9 ); |
| String smartTrimmedStackTrace = (String) args.get( 10 ); |
| String stackTrace = (String) args.get( 11 ); |
| return newReportEntry( runMode, testRunId, source, sourceText, name, nameText, group, message, timeElapsed, |
| traceMessage, smartTrimmedStackTrace, stackTrace ); |
| } |
| |
| private static StackTraceWriter toStackTraceWriter( List<Object> args ) |
| { |
| String traceMessage = (String) args.get( 0 ); |
| String smartTrimmedStackTrace = (String) args.get( 1 ); |
| String stackTrace = (String) args.get( 2 ); |
| return toTrace( traceMessage, smartTrimmedStackTrace, stackTrace ); |
| } |
| |
| private static StackTraceWriter toTrace( String traceMessage, String smartTrimmedStackTrace, String stackTrace ) |
| { |
| boolean exists = traceMessage != null || stackTrace != null || smartTrimmedStackTrace != null; |
| return exists ? new DeserializedStacktraceWriter( traceMessage, smartTrimmedStackTrace, stackTrace ) : null; |
| } |
| |
| static TestSetReportEntry newReportEntry( // ReportEntry: |
| RunMode runMode, long testRunId, String source, String sourceText, |
| String name, String nameText, String group, String message, |
| Integer timeElapsed, |
| // StackTraceWriter: |
| String traceMessage, |
| String smartTrimmedStackTrace, String stackTrace ) |
| throws NumberFormatException |
| { |
| StackTraceWriter stackTraceWriter = toTrace( traceMessage, smartTrimmedStackTrace, stackTrace ); |
| return reportEntry( runMode, testRunId, source, sourceText, name, nameText, group, stackTraceWriter, |
| timeElapsed, message, emptyMap() ); |
| } |
| |
| private static Map<Segment, ForkedProcessEventType> segmentsToEvents() |
| { |
| Map<Segment, ForkedProcessEventType> events = new HashMap<>(); |
| for ( ForkedProcessEventType event : ForkedProcessEventType.values() ) |
| { |
| byte[] array = event.getOpcodeBinary(); |
| events.put( new Segment( array, 0, array.length ), event ); |
| } |
| return events; |
| } |
| |
| private static Map<Segment, RunMode> segmentsToRunModes() |
| { |
| Map<Segment, RunMode> runModes = new HashMap<>(); |
| for ( RunMode runMode : RunMode.values() ) |
| { |
| byte[] array = runMode.getRunmodeBinary(); |
| runModes.put( new Segment( array, 0, array.length ), runMode ); |
| } |
| return runModes; |
| } |
| |
| @Override |
| protected void debugStream( byte[] array, int position, int remaining ) |
| { |
| if ( debugSink == null ) |
| { |
| return; |
| } |
| |
| try |
| { |
| debugSink.write( array, position, remaining ); |
| } |
| catch ( IOException e ) |
| { |
| // logger file was deleted |
| // System.out is already used by the stream in this decoder |
| } |
| } |
| |
| private OutputStream newDebugSink( ForkNodeArguments arguments ) |
| { |
| final File sink = arguments.getEventStreamBinaryFile(); |
| if ( sink == null ) |
| { |
| return null; |
| } |
| |
| try |
| { |
| OutputStream fos = new FileOutputStream( sink, true ); |
| final OutputStream os = new BufferedOutputStream( fos, DEBUG_SINK_BUFFER_SIZE ); |
| addShutDownHook( new Thread( new FutureTask<>( () -> |
| { |
| os.close(); |
| return null; |
| } ) ) ); |
| return os; |
| } |
| catch ( FileNotFoundException e ) |
| { |
| return null; |
| } |
| } |
| |
| @Override |
| public void close() throws IOException |
| { |
| // do NOT close the channel, it's std/out. |
| if ( debugSink != null ) |
| { |
| debugSink.close(); |
| } |
| } |
| } |