You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by ti...@apache.org on 2021/01/16 02:08:14 UTC

[maven-surefire] 01/01: [SUREFIRE-1847] Remove Base64 in the Encoder/Decoder and gain the performance for the communication flow: Plugin to Fork

This is an automated email from the ASF dual-hosted git repository.

tibordigana pushed a commit to branch comm
in repository https://gitbox.apache.org/repos/asf/maven-surefire.git

commit bd04a9ead77bb962c716ad7bb224b4eec2888169
Author: tibordigana <ti...@gmail.com>
AuthorDate: Wed Nov 25 08:59:01 2020 +0100

    [SUREFIRE-1847] Remove Base64 in the Encoder/Decoder and gain the performance for the communication flow: Plugin to Fork
---
 .../maven/plugin/surefire/SurefireHelper.java      |    2 +
 .../maven/plugin/surefire/SurefireProperties.java  |    5 +
 .../surefire/booterclient/BooterSerializer.java    |    2 +
 .../plugin/surefire/booterclient/ForkStarter.java  |   22 +-
 .../output/InPluginProcessDumpSingleton.java       |    7 +
 .../surefire/extensions/EventConsumerThread.java   |  926 +----------------
 .../surefire/extensions/LegacyForkChannel.java     |    2 +-
 .../surefire/extensions/LegacyForkNodeFactory.java |    2 +-
 .../plugin/surefire/extensions/StreamFeeder.java   |  123 +--
 .../surefire/extensions/SurefireForkChannel.java   |    2 +-
 .../extensions/SurefireForkNodeFactory.java        |    2 +-
 .../maven/surefire/stream/CommandEncoder.java      |  148 +++
 .../apache/maven/surefire/stream/EventDecoder.java |  468 +++++++++
 .../booterclient/ForkingRunListenerTest.java       |   22 +-
 .../plugin/surefire/booterclient/MainClass.java    |    2 +-
 .../TestLessInputStreamBuilderTest.java            |   35 +-
 .../TestProvidingInputStreamTest.java              |   46 +-
 .../booterclient/output/ForkClientTest.java        |   19 +-
 .../maven/plugin/surefire/extensions/E2ETest.java  |   16 +-
 .../extensions/EventConsumerThreadTest.java        | 1060 +-------------------
 .../extensions/ForkedProcessEventNotifierTest.java |  223 +---
 .../surefire/extensions/StreamFeederTest.java      |   39 +-
 .../org/apache/maven/surefire/JUnit4SuiteTest.java |    2 +
 .../maven/surefire/extensions/ForkChannelTest.java |   15 +-
 .../maven/surefire/stream/EventDecoderTest.java    |  785 +++++++++++++++
 maven-surefire-plugin/src/site/fml/faq.fml         |    4 +-
 .../maven/surefire/api/booter/Constants.java       |    6 +-
 .../surefire/api/booter/DumpErrorSingleton.java    |   27 +-
 .../surefire/api/booter/ForkingRunListener.java    |    2 +-
 .../api/booter/MasterProcessChannelEncoder.java    |    6 +-
 .../surefire/api/booter/MasterProcessCommand.java  |   54 +-
 .../surefire/api/fork}/ForkNodeArguments.java      |    6 +-
 .../apache/maven/surefire/api/report/RunMode.java  |   19 +
 .../surefire/api/stream/AbstractStreamDecoder.java |  735 ++++++++++++++
 .../surefire/api/stream/AbstractStreamEncoder.java |  191 ++++
 .../MalformedChannelException.java}                |   13 +-
 .../Constants.java => stream/SegmentType.java}     |   21 +-
 .../surefire/api/util/internal/DumpFileUtils.java  |    1 +
 .../java/org/apache/maven/JUnit4SuiteTest.java     |    6 +-
 .../api/stream/AbstractStreamDecoderTest.java      |  692 +++++++++++++
 .../api/stream/AbstractStreamEncoderTest.java      |  331 ++++++
 .../maven/surefire/booter/BooterConstants.java     |    2 +-
 .../maven/surefire/booter/BooterDeserializer.java  |    5 +
 .../maven/surefire/booter/CommandReader.java       |    4 +-
 .../apache/maven/surefire/booter/ForkedBooter.java |   29 +-
 .../maven/surefire/booter/ForkedNodeArg.java       |   97 ++
 .../maven/surefire/booter/PropertiesWrapper.java   |    2 +-
 .../surefire/booter/spi/CommandChannelDecoder.java |   86 ++
 ...hannelEncoder.java => EventChannelEncoder.java} |  268 ++---
 .../spi/LegacyMasterProcessChannelDecoder.java     |  190 ----
 ...LegacyMasterProcessChannelProcessorFactory.java |   12 +-
 ...refireMasterProcessChannelProcessorFactory.java |   14 +-
 .../surefire/booter/stream/CommandDecoder.java     |  276 +++++
 .../maven/surefire/booter/stream/EventEncoder.java |   78 ++
 .../maven/surefire/booter/CommandReaderTest.java   |   44 +-
 .../surefire/booter/ForkedBooterMockTest.java      |   33 +-
 .../maven/surefire/booter/JUnit4SuiteTest.java     |    8 +-
 .../booter/spi/CommandChannelDecoderTest.java      |  519 ++++++++++
 ...coderTest.java => EventChannelEncoderTest.java} |  227 +----
 .../spi/LegacyMasterProcessChannelDecoderTest.java |  243 -----
 .../resources/binary-commands/75171711-encoder.bin |  Bin 0 -> 851 bytes
 .../maven/surefire/extensions/ForkChannel.java     |    1 +
 .../maven/surefire/extensions/ForkNodeFactory.java |    2 +
 surefire-extensions-spi/pom.xml                    |    5 +
 .../spi/MasterProcessChannelProcessorFactory.java  |   10 +-
 .../its/JUnitPlatformStreamCorruptionIT.java       |    2 +-
 .../plugin/surefire/log/api/PrintStreamLogger.java |    2 +-
 67 files changed, 5061 insertions(+), 3187 deletions(-)

diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/SurefireHelper.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/SurefireHelper.java
index d0cac4d..9149946 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/SurefireHelper.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/SurefireHelper.java
@@ -64,6 +64,8 @@ public final class SurefireHelper
 
     public static final String DUMP_FILENAME = DUMP_FILE_DATE + DUMP_FILE_EXT;
 
+    public static final String EVENTS_BINARY_DUMP_FILENAME_FORMATTER = DUMP_FILE_DATE + "-jvmRun%d-events.bin";
+
     /**
      * The maximum path that does not require long path prefix on Windows.<br>
      * See {@code sun/nio/fs/WindowsPath} in
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/SurefireProperties.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/SurefireProperties.java
index a880c0d..cd2f91f 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/SurefireProperties.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/SurefireProperties.java
@@ -201,6 +201,11 @@ public class SurefireProperties
         }
     }
 
+    public void setProperty( String key, int value )
+    {
+        setProperty( key, String.valueOf( value ) );
+    }
+
     public void setProperty( String key, Long value )
     {
         if ( value != null )
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/BooterSerializer.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/BooterSerializer.java
index 1e00f16..1335a82 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/BooterSerializer.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/BooterSerializer.java
@@ -50,6 +50,7 @@ import static org.apache.maven.surefire.booter.BooterConstants.FAIL_FAST_COUNT;
 import static org.apache.maven.surefire.booter.BooterConstants.FAILIFNOTESTS;
 import static org.apache.maven.surefire.booter.BooterConstants.FORKTESTSET;
 import static org.apache.maven.surefire.booter.BooterConstants.FORKTESTSET_PREFER_TESTS_FROM_IN_STREAM;
+import static org.apache.maven.surefire.booter.BooterConstants.FORK_NUMBER;
 import static org.apache.maven.surefire.booter.BooterConstants.INCLUDES_PROPERTY_PREFIX;
 import static org.apache.maven.surefire.booter.BooterConstants.ISTRIMSTACKTRACE;
 import static org.apache.maven.surefire.booter.BooterConstants.MAIN_CLI_OPTIONS;
@@ -168,6 +169,7 @@ class BooterSerializer
         ReporterConfiguration reporterConfiguration = providerConfiguration.getReporterConfiguration();
         boolean rep = reporterConfiguration.isTrimStackTrace();
         File reportsDirectory = replaceForkThreadsInPath( reporterConfiguration.getReportsDirectory(), forkNumber );
+        properties.setProperty( FORK_NUMBER, forkNumber );
         properties.setProperty( ISTRIMSTACKTRACE, rep );
         properties.setProperty( REPORTSDIRECTORY, reportsDirectory );
         ClassLoaderConfiguration classLoaderConfig = startupConfiguration.getClassLoaderConfiguration();
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ForkStarter.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ForkStarter.java
index 1ac3917..200d2a8 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ForkStarter.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ForkStarter.java
@@ -45,7 +45,7 @@ import org.apache.maven.surefire.extensions.CloseableDaemonThread;
 import org.apache.maven.surefire.extensions.EventHandler;
 import org.apache.maven.surefire.extensions.ForkChannel;
 import org.apache.maven.surefire.extensions.ForkNodeFactory;
-import org.apache.maven.surefire.extensions.ForkNodeArguments;
+import org.apache.maven.surefire.api.fork.ForkNodeArguments;
 import org.apache.maven.surefire.extensions.util.CommandlineExecutor;
 import org.apache.maven.surefire.extensions.util.CommandlineStreams;
 import org.apache.maven.surefire.extensions.util.CountdownCloseable;
@@ -574,7 +574,8 @@ public class ForkStarter
         File dumpLogDir = replaceForkThreadsInPath( startupReportConfiguration.getReportsDirectory(), forkNumber );
         try
         {
-            ForkNodeArguments forkNodeArguments = new ForkedNodeArg( forkNumber, dumpLogDir, randomUUID().toString() );
+            ForkNodeArguments forkNodeArguments =
+                new ForkedNodeArg( forkConfiguration.isDebug(), forkNumber, dumpLogDir, randomUUID().toString() );
             forkChannel = forkNodeFactory.createForkChannel( forkNodeArguments );
             closer.addCloseable( forkChannel );
             tempDir = forkConfiguration.getTempDirectory().getCanonicalPath();
@@ -884,9 +885,11 @@ public class ForkStarter
         private final int forkChannelId;
         private final File dumpLogDir;
         private final String sessionId;
+        private final boolean debug;
 
-        ForkedNodeArg( int forkChannelId, File dumpLogDir, String sessionId )
+        ForkedNodeArg( boolean debug, int forkChannelId, File dumpLogDir, String sessionId )
         {
+            this.debug = debug;
             this.forkChannelId = forkChannelId;
             this.dumpLogDir = dumpLogDir;
             this.sessionId = sessionId;
@@ -932,5 +935,18 @@ public class ForkStarter
         {
             return log;
         }
+
+        @Override
+        public File getEventStreamBinaryFile()
+        {
+            return debug ? InPluginProcessDumpSingleton.getSingleton()
+                .getEventStreamBinaryFile( dumpLogDir, forkChannelId ) : null;
+        }
+
+        @Override
+        public File getCommandStreamBinaryFile()
+        {
+            return null;
+        }
     }
 }
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/InPluginProcessDumpSingleton.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/InPluginProcessDumpSingleton.java
index 372c046..05c4235 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/InPluginProcessDumpSingleton.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/InPluginProcessDumpSingleton.java
@@ -28,6 +28,7 @@ import static org.apache.maven.plugin.surefire.SurefireHelper.DUMP_FILENAME;
 import static org.apache.maven.plugin.surefire.SurefireHelper.DUMP_FILENAME_FORMATTER;
 import static org.apache.maven.plugin.surefire.SurefireHelper.DUMPSTREAM_FILENAME;
 import static org.apache.maven.plugin.surefire.SurefireHelper.DUMPSTREAM_FILENAME_FORMATTER;
+import static org.apache.maven.plugin.surefire.SurefireHelper.EVENTS_BINARY_DUMP_FILENAME_FORMATTER;
 
 /**
  * Reports errors to dump file.
@@ -82,6 +83,12 @@ public final class InPluginProcessDumpSingleton
         DumpFileUtils.dumpException( t, msg == null ? "null" : msg, dump );
     }
 
+    public File getEventStreamBinaryFile( File reportsDirectory, int jvmRun )
+    {
+        reportsDirectory.mkdirs();
+        return new File( reportsDirectory, format( EVENTS_BINARY_DUMP_FILENAME_FORMATTER, jvmRun ) );
+    }
+
     private File newDumpStreamFile( File reportsDirectory )
     {
         return new File( reportsDirectory, DUMPSTREAM_FILENAME );
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/EventConsumerThread.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/EventConsumerThread.java
index 8b855d8..1371dbb 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/EventConsumerThread.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/EventConsumerThread.java
@@ -19,142 +19,28 @@ package org.apache.maven.plugin.surefire.extensions;
  * 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.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.fork.ForkNodeArguments;
+import org.apache.maven.surefire.api.stream.AbstractStreamDecoder.Memento;
 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.stream.EventDecoder;
 
-import javax.annotation.Nonnegative;
 import javax.annotation.Nonnull;
 import java.io.EOFException;
-import java.io.File;
 import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.CharBuffer;
 import java.nio.channels.ReadableByteChannel;
-import java.nio.charset.Charset;
-import java.nio.charset.CharsetDecoder;
-import java.nio.charset.CoderResult;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import static java.lang.Math.max;
-import static java.lang.Math.min;
-import static java.nio.charset.CodingErrorAction.REPLACE;
-import static java.nio.charset.StandardCharsets.US_ASCII;
-import static org.apache.maven.plugin.surefire.extensions.EventConsumerThread.StreamReadStatus.OVERFLOW;
-import static org.apache.maven.plugin.surefire.extensions.EventConsumerThread.StreamReadStatus.UNDERFLOW;
-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.report.CategorizedReportEntry.reportEntry;
 
 /**
  *
  */
 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 SegmentType[] EVENT_WITHOUT_DATA = new SegmentType[] {
-        SegmentType.END_OF_FRAME
-    };
-
-    private static final SegmentType[] EVENT_WITH_ERROR_TRACE = new SegmentType[] {
-        SegmentType.STRING_ENCODING,
-        SegmentType.DATA_STRING,
-        SegmentType.DATA_STRING,
-        SegmentType.DATA_STRING,
-        SegmentType.END_OF_FRAME
-    };
-
-    private static final SegmentType[] EVENT_WITH_ONE_STRING = new SegmentType[] {
-        SegmentType.STRING_ENCODING,
-        SegmentType.DATA_STRING,
-        SegmentType.END_OF_FRAME
-    };
-
-    private static final SegmentType[] EVENT_WITH_RUNMODE_AND_ONE_STRING = new SegmentType[] {
-        SegmentType.RUN_MODE,
-        SegmentType.STRING_ENCODING,
-        SegmentType.DATA_STRING,
-        SegmentType.END_OF_FRAME
-    };
-
-    private static final SegmentType[] EVENT_WITH_RUNMODE_AND_TWO_STRINGS = new SegmentType[] {
-        SegmentType.RUN_MODE,
-        SegmentType.STRING_ENCODING,
-        SegmentType.DATA_STRING,
-        SegmentType.DATA_STRING,
-        SegmentType.END_OF_FRAME
-    };
-
-    private static final SegmentType[] EVENT_TEST_CONTROL = new SegmentType[] {
-        SegmentType.RUN_MODE,
-        SegmentType.STRING_ENCODING,
-        SegmentType.DATA_STRING,
-        SegmentType.DATA_STRING,
-        SegmentType.DATA_STRING,
-        SegmentType.DATA_STRING,
-        SegmentType.DATA_STRING,
-        SegmentType.DATA_STRING,
-        SegmentType.DATA_INT,
-        SegmentType.DATA_STRING,
-        SegmentType.DATA_STRING,
-        SegmentType.DATA_STRING,
-        SegmentType.END_OF_FRAME
-    };
-
-    private static final int BUFFER_SIZE = 1024;
-    private static final byte[] DEFAULT_STREAM_ENCODING_BYTES = DEFAULT_STREAM_ENCODING.name().getBytes( US_ASCII );
-    private static final int DELIMITER_LENGTH = 1;
-    private static final int BYTE_LENGTH = 1;
-    private static final int INT_LENGTH = 4;
-    private static final int NO_POSITION = -1;
-
     private final ReadableByteChannel channel;
     private final EventHandler<Event> eventHandler;
     private final CountdownCloseable countdownCloseable;
+    private final EventDecoder decoder;
     private final ForkNodeArguments arguments;
     private volatile boolean disabled;
 
@@ -165,6 +51,7 @@ public class EventConsumerThread extends CloseableDaemonThread
                                 @Nonnull ForkNodeArguments arguments )
     {
         super( threadName );
+        decoder = new EventDecoder( channel, arguments );
         this.channel = channel;
         this.eventHandler = eventHandler;
         this.countdownCloseable = countdownCloseable;
@@ -175,809 +62,42 @@ public class EventConsumerThread extends CloseableDaemonThread
     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
-    {
-        Map<Segment, ForkedProcessEventType> eventTypes = mapEventTypes();
-        Map<Segment, RunMode> runModes = mapRunModes();
-        Memento memento = new Memento();
-        memento.bb.limit( 0 );
-
-        do
-        {
-            try
-            {
-                ForkedProcessEventType eventType = readEventType( eventTypes, memento );
-                if ( eventType == null )
-                {
-                    throw new MalformedFrameException( memento.line.positionByteBuffer, memento.bb.position() );
-                }
-                RunMode runMode = null;
-                for ( SegmentType segmentType : nextSegmentType( eventType ) )
-                {
-                    if ( segmentType == null )
-                    {
-                        break;
-                    }
-
-                    switch ( segmentType )
-                    {
-                        case RUN_MODE:
-                            runMode = runModes.get( readSegment( memento ) );
-                            break;
-                        case STRING_ENCODING:
-                            memento.setCharset( readCharset( memento ) );
-                            break;
-                        case DATA_STRING:
-                            memento.data.add( readString( memento ) );
-                            break;
-                        case DATA_INT:
-                            memento.data.add( readInteger( memento ) );
-                            break;
-                        case END_OF_FRAME:
-                            memento.line.positionByteBuffer = memento.bb.position();
-                            if ( !disabled )
-                            {
-                                eventHandler.handleEvent( toEvent( eventType, runMode, memento.data ) );
-                            }
-                            break;
-                        default:
-                            memento.line.positionByteBuffer = NO_POSITION;
-                            arguments.dumpStreamText( "Unknown enum ("
-                                + ForkedProcessEventType.class.getSimpleName()
-                                + ") "
-                                + segmentType );
-                    }
-                }
-            }
-            catch ( MalformedFrameException e )
-            {
-                if ( e.hasValidPositions() )
-                {
-                    int length = e.readTo - e.readFrom;
-                    memento.line.write( memento.bb, e.readFrom, length );
-                }
-            }
-            catch ( RuntimeException e )
-            {
-                arguments.dumpStreamException( e );
-            }
-            catch ( IOException e )
-            {
-                printRemainingStream( memento );
-                throw e;
-            }
-            finally
-            {
-                memento.reset();
-            }
-        }
-        while ( true );
-    }
-
-    protected ForkedProcessEventType readEventType( Map<Segment, ForkedProcessEventType> eventTypes, Memento memento )
-        throws IOException, MalformedFrameException
-    {
-        int readCount = DELIMITER_LENGTH + MAGIC_NUMBER_BYTES.length + DELIMITER_LENGTH
-            + BYTE_LENGTH + DELIMITER_LENGTH;
-        read( memento, readCount );
-        checkHeader( memento );
-        return eventTypes.get( readSegment( memento ) );
-    }
-
-    protected String readString( Memento memento ) throws IOException, MalformedFrameException
-    {
-        memento.cb.clear();
-        int readCount = readInt( memento );
-        read( memento, readCount + DELIMITER_LENGTH );
-
-        final String string;
-        if ( readCount == 0 )
-        {
-            string = "";
-        }
-        else if ( readCount == 1 )
-        {
-            read( memento, 1 );
-            byte oneChar = memento.bb.get();
-            string = oneChar == 0 ? null : String.valueOf( (char) oneChar );
-        }
-        else
-        {
-            string = readString( memento, readCount );
-        }
-
-        checkDelimiter( memento );
-        return string;
-    }
-
-    @Nonnull
-    @SuppressWarnings( "checkstyle:magicnumber" )
-    protected Segment readSegment( Memento memento ) throws IOException, MalformedFrameException
-    {
-        int readCount = readByte( memento ) & 0xff;
-        read( memento, readCount + DELIMITER_LENGTH );
-        ByteBuffer bb = memento.bb;
-        Segment segment = new Segment( bb.array(), bb.arrayOffset() + bb.position(), readCount );
-        bb.position( bb.position() + readCount );
-        checkDelimiter( memento );
-        return segment;
-    }
-
-    @Nonnull
-    @SuppressWarnings( "checkstyle:magicnumber" )
-    protected Charset readCharset( Memento memento ) throws IOException, MalformedFrameException
-    {
-        int length = readByte( memento ) & 0xff;
-        read( memento, length + DELIMITER_LENGTH );
-        ByteBuffer bb = memento.bb;
-        byte[] array = bb.array();
-        int offset = bb.arrayOffset() + bb.position();
-        bb.position( bb.position() + length );
-        boolean isDefaultEncoding = false;
-        if ( length == DEFAULT_STREAM_ENCODING_BYTES.length )
-        {
-            isDefaultEncoding = true;
-            for ( int i = 0; i < length; i++ )
-            {
-                isDefaultEncoding &= DEFAULT_STREAM_ENCODING_BYTES[i] == array[offset + i];
-            }
-        }
-
-        try
-        {
-            Charset charset =
-                isDefaultEncoding
-                    ? DEFAULT_STREAM_ENCODING
-                    : Charset.forName( new String( array, offset, length, US_ASCII ) );
-
-            checkDelimiter( memento );
-            return charset;
-        }
-        catch ( IllegalArgumentException e )
-        {
-            throw new MalformedFrameException( memento.line.positionByteBuffer, bb.position() );
-        }
-    }
-
-    private static void checkHeader( Memento memento ) throws MalformedFrameException
-    {
-        ByteBuffer bb = memento.bb;
-
-        checkDelimiter( memento );
-
-        int shift = 0;
-        try
-        {
-            for ( int start = bb.arrayOffset() + bb.position(), length = MAGIC_NUMBER_BYTES.length;
-                  shift < length; shift++ )
-            {
-                if ( bb.array()[shift + start] != MAGIC_NUMBER_BYTES[shift] )
-                {
-                    throw new MalformedFrameException( memento.line.positionByteBuffer, bb.position() + shift );
-                }
-            }
-        }
-        finally
-        {
-            bb.position( bb.position() + shift );
-        }
-
-        checkDelimiter( memento );
-    }
-
-    @SuppressWarnings( "checkstyle:magicnumber" )
-    private static void checkDelimiter( Memento memento ) throws MalformedFrameException
-    {
-        ByteBuffer bb = memento.bb;
-        if ( ( 0xff & bb.get() ) != ':' )
-        {
-            throw new MalformedFrameException( memento.line.positionByteBuffer, bb.position() );
-        }
-    }
-
-    static SegmentType[] nextSegmentType( 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_AND_ONE_STRING;
-            case BOOTERCODE_SYSPROPS:
-                return EVENT_WITH_RUNMODE_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 );
-        }
-    }
-
-    protected StreamReadStatus read( Memento memento, int recommendedCount ) throws IOException
-    {
-        ByteBuffer buffer = memento.bb;
-        if ( buffer.remaining() >= recommendedCount && buffer.position() != 0 )
-        {
-            return OVERFLOW;
-        }
-        else
-        {
-            if ( buffer.position() != 0 && recommendedCount > buffer.capacity() - buffer.position() )
-            {
-                buffer.compact().flip();
-                memento.line.positionByteBuffer = 0;
-            }
-            int mark = buffer.position();
-            buffer.position( buffer.limit() );
-            buffer.limit( buffer.capacity() );
-            boolean isEnd = false;
-            while ( !isEnd && buffer.position() - mark < recommendedCount && buffer.position() != buffer.limit() )
-            {
-                isEnd = channel.read( buffer ) == -1;
-            }
-
-            buffer.limit( buffer.position() );
-            buffer.position( mark );
-            int readBytes = buffer.remaining();
-
-            if ( isEnd && readBytes < recommendedCount )
-            {
-                throw new EOFException();
-            }
-            else
-            {
-                return readBytes >= recommendedCount ? OVERFLOW : UNDERFLOW;
-            }
-        }
-    }
-
-    static Event toEvent( ForkedProcessEventType eventType, RunMode runMode, List<Object> args )
-    {
-        switch ( eventType )
-        {
-            case BOOTERCODE_BYE:
-                return new ControlByeEvent();
-            case BOOTERCODE_STOP_ON_NEXT_TEST:
-                return new ControlStopOnNextTestEvent();
-            case BOOTERCODE_NEXT_TEST:
-                return new ControlNextTestEvent();
-            case BOOTERCODE_CONSOLE_ERROR:
-                return new ConsoleErrorEvent( toStackTraceWriter( args ) );
-            case BOOTERCODE_JVM_EXIT_ERROR:
-                return new JvmExitErrorEvent( toStackTraceWriter( args ) );
-            case BOOTERCODE_CONSOLE_INFO:
-                return new ConsoleInfoEvent( (String) args.get( 0 ) );
-            case BOOTERCODE_CONSOLE_DEBUG:
-                return new ConsoleDebugEvent( (String) args.get( 0 ) );
-            case BOOTERCODE_CONSOLE_WARNING:
-                return new ConsoleWarningEvent( (String) args.get( 0 ) );
-            case BOOTERCODE_STDOUT:
-                return new StandardStreamOutEvent( runMode, (String) args.get( 0 ) );
-            case BOOTERCODE_STDOUT_NEW_LINE:
-                return new StandardStreamOutWithNewLineEvent( runMode, (String) args.get( 0 ) );
-            case BOOTERCODE_STDERR:
-                return new StandardStreamErrEvent( runMode, (String) args.get( 0 ) );
-            case BOOTERCODE_STDERR_NEW_LINE:
-                return new StandardStreamErrWithNewLineEvent( runMode, (String) args.get( 0 ) );
-            case BOOTERCODE_SYSPROPS:
-                String key = (String) args.get( 0 );
-                String value = (String) args.get( 1 );
-                return new SystemPropertyEvent( runMode, key, value );
-            case BOOTERCODE_TESTSET_STARTING:
-                return new TestsetStartingEvent( runMode, toReportEntry( args ) );
-            case BOOTERCODE_TESTSET_COMPLETED:
-                return new TestsetCompletedEvent( runMode, toReportEntry( args ) );
-            case BOOTERCODE_TEST_STARTING:
-                return new TestStartingEvent( runMode, toReportEntry( args ) );
-            case BOOTERCODE_TEST_SUCCEEDED:
-                return new TestSucceededEvent( runMode, toReportEntry( args ) );
-            case BOOTERCODE_TEST_FAILED:
-                return new TestFailedEvent( runMode, toReportEntry( args ) );
-            case BOOTERCODE_TEST_SKIPPED:
-                return new TestSkippedEvent( runMode, toReportEntry( args ) );
-            case BOOTERCODE_TEST_ERROR:
-                return new TestErrorEvent( runMode, toReportEntry( args ) );
-            case BOOTERCODE_TEST_ASSUMPTIONFAILURE:
-                return new TestAssumptionFailureEvent( runMode, toReportEntry( args ) );
-            default:
-                throw new IllegalArgumentException( "Missing a branch for the event type " + eventType );
-        }
-    }
-
-    private static void printCorruptedStream( Memento memento )
-    {
-        ByteBuffer bb = memento.bb;
-        if ( bb.hasRemaining() )
-        {
-            int bytesToWrite = bb.remaining();
-            memento.line.write( bb, bb.position(), bytesToWrite );
-            bb.position( bb.position() + bytesToWrite );
-        }
-    }
-
-    /**
-     * Print the last string which has not been finished by a new line character.
-     *
-     * @param memento current memento object
-     */
-    private static void printRemainingStream( Memento memento )
-    {
-        printCorruptedStream( memento );
-        memento.line.printExistingLine();
-        memento.line.count = 0;
-    }
-
-    @Nonnull
-    private static TestSetReportEntry toReportEntry( List<Object> args )
-    {
-        // ReportEntry:
-        String source = (String) args.get( 0 );
-        String sourceText = (String) args.get( 1 );
-        String name = (String) args.get( 2 );
-        String nameText = (String) args.get( 3 );
-        String group = (String) args.get( 4 );
-        String message = (String) args.get( 5 );
-        Integer timeElapsed = (Integer) args.get( 6 );
-        // StackTraceWriter:
-        String traceMessage = (String) args.get( 7 );
-        String smartTrimmedStackTrace = (String) args.get( 8 );
-        String stackTrace = (String) args.get( 9 );
-        return newReportEntry( 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:
-                                              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( source, sourceText, name, nameText, group, stackTraceWriter, timeElapsed, message,
-            Collections.<String, String>emptyMap() );
-    }
-
-    private static boolean isJvmError( String line )
-    {
-        String lineLower = line.toLowerCase();
-        for ( String errorPattern : JVM_ERROR_PATTERNS )
+              CountdownCloseable c = countdownCloseable;
+              EventDecoder eventDecoder = decoder )
         {
-            if ( lineLower.contains( errorPattern ) )
-            {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    private static int decodeString( @Nonnull CharsetDecoder decoder, @Nonnull ByteBuffer input,
-                                     @Nonnull CharBuffer output, @Nonnegative int bytesToDecode,
-                                     boolean endOfInput, @Nonnegative int errorStreamFrom )
-        throws MalformedFrameException
-    {
-        int limit = input.limit();
-        input.limit( input.position() + bytesToDecode );
-
-        CoderResult result = decoder.decode( input, output, endOfInput );
-        if ( result.isError() || result.isMalformed() )
-        {
-            throw new MalformedFrameException( errorStreamFrom, input.position() );
-        }
-        
-        int decodedBytes = bytesToDecode - input.remaining();
-        input.limit( limit );
-        return decodedBytes;
-    }
-
-    String readString( @Nonnull final Memento memento, @Nonnegative final int totalBytes )
-        throws IOException, MalformedFrameException
-    {
-        memento.getDecoder().reset();
-        final CharBuffer output = memento.cb;
-        output.clear();
-        final ByteBuffer input = memento.bb;
-        final List<String> strings = new ArrayList<>();
-        int countDecodedBytes = 0;
-        for ( boolean endOfInput = false; !endOfInput; )
-        {
-            final int bytesToRead = totalBytes - countDecodedBytes;
-            read( memento, bytesToRead - input.remaining() );
-            int bytesToDecode = min( input.remaining(), bytesToRead );
-            final boolean isLastChunk = bytesToDecode == bytesToRead;
-            endOfInput = countDecodedBytes + bytesToDecode >= totalBytes;
+            Memento memento = eventDecoder.new Memento();
             do
             {
-                boolean endOfChunk = output.remaining() >= bytesToRead;
-                boolean endOfOutput = isLastChunk && endOfChunk;
-                int readInputBytes = decodeString( memento.getDecoder(), input, output, bytesToDecode, endOfOutput,
-                    memento.line.positionByteBuffer );
-                bytesToDecode -= readInputBytes;
-                countDecodedBytes += readInputBytes;
-            }
-            while ( isLastChunk && bytesToDecode > 0 && output.hasRemaining() );
-
-            if ( isLastChunk || !output.hasRemaining() )
-            {
-                strings.add( output.flip().toString() );
-                output.clear();
-            }
-        }
-
-        memento.getDecoder().reset();
-        output.clear();
-
-        return toString( strings );
-    }
-
-    protected byte readByte( Memento memento ) throws IOException, MalformedFrameException
-    {
-        read( memento, BYTE_LENGTH + DELIMITER_LENGTH );
-        byte b = memento.bb.get();
-        checkDelimiter( memento );
-        return b;
-    }
-
-    protected int readInt( Memento memento ) throws IOException, MalformedFrameException
-    {
-        read( memento, INT_LENGTH + DELIMITER_LENGTH );
-        int i = memento.bb.getInt();
-        checkDelimiter( memento );
-        return i;
-    }
-
-    protected Integer readInteger( Memento memento ) throws IOException, MalformedFrameException
-    {
-        read( memento, BYTE_LENGTH );
-        boolean isNullObject = memento.bb.get() == 0;
-        if ( isNullObject )
-        {
-            read( memento, DELIMITER_LENGTH );
-            checkDelimiter( memento );
-            return null;
-        }
-        return readInt( memento );
-    }
-
-    private static String toString( List<String> strings )
-    {
-        if ( strings.size() == 1 )
-        {
-            return strings.get( 0 );
-        }
-        StringBuilder concatenated = new StringBuilder( strings.size() * BUFFER_SIZE );
-        for ( String s : strings )
-        {
-            concatenated.append( s );
-        }
-        return concatenated.toString();
-    }
-
-    static Map<Segment, ForkedProcessEventType> mapEventTypes()
-    {
-        Map<Segment, ForkedProcessEventType> map = new HashMap<>();
-        for ( ForkedProcessEventType e : ForkedProcessEventType.values() )
-        {
-            byte[] array = e.getOpcode().getBytes( US_ASCII );
-            map.put( new Segment( array, 0, array.length ), e );
-        }
-        return map;
-    }
-
-    static Map<Segment, RunMode> mapRunModes()
-    {
-        Map<Segment, RunMode> map = new HashMap<>();
-        for ( RunMode e : RunMode.values() )
-        {
-            byte[] array = e.geRunmode().getBytes( US_ASCII );
-            map.put( new Segment( array, 0, array.length ), e );
-        }
-        return map;
-    }
-
-    enum StreamReadStatus
-    {
-        UNDERFLOW,
-        OVERFLOW,
-        EOF
-    }
-
-    enum SegmentType
-    {
-        RUN_MODE,
-        STRING_ENCODING,
-        DATA_STRING,
-        DATA_INT,
-        END_OF_FRAME
-    }
-
-    /**
-     * This class avoids locking which gains the performance of this decoder.
-     */
-    private class BufferedStream
-    {
-        private byte[] buffer;
-        private int count;
-        private int positionByteBuffer;
-        private boolean isNewLine;
-
-        BufferedStream( int capacity )
-        {
-            this.buffer = new byte[capacity];
-        }
-
-        void write( ByteBuffer bb, int position, int length )
-        {
-            ensureCapacity( length );
-            byte[] array = bb.array();
-            int pos = bb.arrayOffset() + position;
-            while ( length-- > 0 )
-            {
-                positionByteBuffer++;
-                byte b = array[pos++];
-                if ( b == '\r' || b == '\n' )
+                Event event = eventDecoder.decode( memento );
+                if ( event != null && !disabled )
                 {
-                    if ( !isNewLine )
-                    {
-                        printExistingLine();
-                        count = 0;
-                    }
-                    isNewLine = true;
-                }
-                else
-                {
-                    buffer[count++] = b;
-                    isNewLine = false;
+                    eventHandler.handleEvent( event );
                 }
             }
+            while ( true );
         }
-
-        private boolean isEmpty()
+        catch ( EOFException e )
         {
-            return count == 0;
-        }
-
-        @Override
-        public String toString()
-        {
-            return new String( buffer, 0, count, DEFAULT_STREAM_ENCODING );
-        }
-
-        private void ensureCapacity( int addCapacity )
-        {
-            int oldCapacity = buffer.length;
-            int exactCapacity = count + addCapacity;
-            if ( exactCapacity < 0 )
-            {
-                throw new OutOfMemoryError();
-            }
-
-            if ( oldCapacity < exactCapacity )
-            {
-                int newCapacity = oldCapacity << 1;
-                buffer = Arrays.copyOf( buffer, max( newCapacity, exactCapacity ) );
-            }
+            //
         }
-
-        void printExistingLine()
-        {
-            if ( isEmpty() )
-            {
-                return;
-            }
-
-            String s = toString();
-            if ( s == null )
-            {
-                return;
-            }
-
-            ConsoleLogger logger = arguments.getConsoleLogger();
-            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 );
-                }
-                else if ( logger.isDebugEnabled() )
-                {
-                    logger.debug( 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() );
-            }
-        }
-    }
-
-    class Memento
-    {
-        private final CharsetDecoder defaultDecoder;
-        private CharsetDecoder currentDecoder;
-        final BufferedStream line = new BufferedStream( 32 );
-        final List<Object> data = new ArrayList<>();
-        final CharBuffer cb = CharBuffer.allocate( BUFFER_SIZE );
-        final ByteBuffer bb = ByteBuffer.allocate( BUFFER_SIZE );
-
-        Memento()
-        {
-            defaultDecoder = DEFAULT_STREAM_ENCODING.newDecoder()
-                .onMalformedInput( REPLACE )
-                .onUnmappableCharacter( REPLACE );
-        }
-
-        void reset()
-        {
-            currentDecoder = null;
-            data.clear();
-        }
-
-        CharsetDecoder getDecoder()
-        {
-            return currentDecoder == null ? defaultDecoder : currentDecoder;
-        }
-
-        void setCharset( Charset charset )
+        catch ( IOException e )
         {
-            if ( charset.name().equals( defaultDecoder.charset().name() ) )
+            if ( !( e.getCause() instanceof InterruptedException ) )
             {
-                currentDecoder = defaultDecoder;
-            }
-            else
-            {
-                currentDecoder = charset.newDecoder()
-                    .onMalformedInput( REPLACE )
-                    .onUnmappableCharacter( REPLACE );
+                arguments.dumpStreamException( e );
             }
         }
     }
 
-    static class Segment
+    @Override
+    public void disable()
     {
-        private final byte[] array;
-        private final int fromIndex;
-        private final int length;
-        private final int hashCode;
-
-        Segment( byte[] array, int fromIndex, int length )
-        {
-            this.array = array;
-            this.fromIndex = fromIndex;
-            this.length = length;
-
-            int hashCode = 0;
-            int i = fromIndex;
-            for ( int loops = length >> 1; loops-- != 0; )
-            {
-                hashCode = 31 * hashCode + array[i++];
-                hashCode = 31 * hashCode + array[i++];
-            }
-            this.hashCode = i == fromIndex + length ? hashCode : 31 * hashCode + array[i];
-        }
-
-        @Override
-        public int hashCode()
-        {
-            return hashCode;
-        }
-
-        @Override
-        public boolean equals( Object obj )
-        {
-            if ( !( obj instanceof Segment ) )
-            {
-                return false;
-            }
-
-            Segment that = (Segment) obj;
-            if ( that.length != length )
-            {
-                return false;
-            }
-
-            for ( int i = 0; i < length; i++ )
-            {
-                if ( that.array[that.fromIndex + i] != array[fromIndex + i] )
-                {
-                    return false;
-                }
-            }
-            return true;
-        }
+        disabled = true;
     }
 
-    /**
-     *
-     */
-    static class MalformedFrameException extends Exception
+    @Override
+    public void close() throws IOException
     {
-        private final int readFrom;
-        private final int readTo;
-
-        MalformedFrameException( int readFrom, int readTo )
-        {
-            this.readFrom = readFrom;
-            this.readTo = readTo;
-        }
-
-        boolean hasValidPositions()
-        {
-            return readFrom != NO_POSITION && readTo != NO_POSITION && readTo - readFrom > 0;
-        }
+        channel.close();
     }
 }
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/LegacyForkChannel.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/LegacyForkChannel.java
index 2400f94..e18f77d 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/LegacyForkChannel.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/LegacyForkChannel.java
@@ -24,7 +24,7 @@ import org.apache.maven.surefire.extensions.CloseableDaemonThread;
 import org.apache.maven.surefire.extensions.CommandReader;
 import org.apache.maven.surefire.extensions.EventHandler;
 import org.apache.maven.surefire.extensions.ForkChannel;
-import org.apache.maven.surefire.extensions.ForkNodeArguments;
+import org.apache.maven.surefire.api.fork.ForkNodeArguments;
 import org.apache.maven.surefire.extensions.util.CountdownCloseable;
 
 import javax.annotation.Nonnull;
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/LegacyForkNodeFactory.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/LegacyForkNodeFactory.java
index 29d79af..b4d4788 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/LegacyForkNodeFactory.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/LegacyForkNodeFactory.java
@@ -20,7 +20,7 @@ package org.apache.maven.plugin.surefire.extensions;
  */
 
 import org.apache.maven.surefire.extensions.ForkChannel;
-import org.apache.maven.surefire.extensions.ForkNodeArguments;
+import org.apache.maven.surefire.api.fork.ForkNodeArguments;
 import org.apache.maven.surefire.extensions.ForkNodeFactory;
 
 import javax.annotation.Nonnull;
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/StreamFeeder.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/StreamFeeder.java
index 3269967..0dcf5ef 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/StreamFeeder.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/StreamFeeder.java
@@ -21,28 +21,15 @@ package org.apache.maven.plugin.surefire.extensions;
 
 import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
 import org.apache.maven.surefire.api.booter.Command;
-import org.apache.maven.surefire.api.booter.MasterProcessCommand;
 import org.apache.maven.surefire.extensions.CloseableDaemonThread;
 import org.apache.maven.surefire.extensions.CommandReader;
-import org.apache.maven.surefire.api.util.internal.ImmutableMap;
+import org.apache.maven.surefire.stream.CommandEncoder;
 
 import javax.annotation.Nonnull;
 import java.io.IOException;
-import java.nio.ByteBuffer;
 import java.nio.channels.ClosedChannelException;
 import java.nio.channels.NonWritableChannelException;
 import java.nio.channels.WritableByteChannel;
-import java.util.HashMap;
-import java.util.Map;
-
-import static java.nio.charset.StandardCharsets.US_ASCII;
-import static org.apache.maven.surefire.api.booter.MasterProcessCommand.BYE_ACK;
-import static org.apache.maven.surefire.api.booter.MasterProcessCommand.MAGIC_NUMBER;
-import static org.apache.maven.surefire.api.booter.MasterProcessCommand.NOOP;
-import static org.apache.maven.surefire.api.booter.MasterProcessCommand.RUN_CLASS;
-import static org.apache.maven.surefire.api.booter.MasterProcessCommand.SHUTDOWN;
-import static org.apache.maven.surefire.api.booter.MasterProcessCommand.SKIP_SINCE_NEXT_TEST;
-import static org.apache.maven.surefire.api.booter.MasterProcessCommand.TEST_SET_FINISHED;
 
 /**
  * Commands which are sent from plugin to the forked jvm.
@@ -59,8 +46,6 @@ import static org.apache.maven.surefire.api.booter.MasterProcessCommand.TEST_SET
  */
 public class StreamFeeder extends CloseableDaemonThread
 {
-    private static final Map<MasterProcessCommand, String> COMMAND_OPCODES = opcodesToStrings();
-
     private final WritableByteChannel channel;
     private final CommandReader commandReader;
     private final ConsoleLogger logger;
@@ -81,15 +66,35 @@ public class StreamFeeder extends CloseableDaemonThread
     @SuppressWarnings( "checkstyle:innerassignment" )
     public void run()
     {
-        try ( WritableByteChannel c = channel )
+        try ( CommandEncoder encoder = new CommandEncoder( channel ) )
         {
             for ( Command cmd; ( cmd = commandReader.readNextCommand() ) != null; )
             {
                 if ( !disabled )
                 {
-                    MasterProcessCommand cmdType = cmd.getCommandType();
-                    byte[] data = cmdType.hasDataType() ? encode( cmdType, cmd.getData() ) : encode( cmdType );
-                    c.write( ByteBuffer.wrap( data ) );
+                    switch ( cmd.getCommandType() )
+                    {
+                        case RUN_CLASS:
+                            encoder.sendRunClass( cmd.getData() );
+                            break;
+                        case TEST_SET_FINISHED:
+                            encoder.sendTestSetFinished();
+                            break;
+                        case SKIP_SINCE_NEXT_TEST:
+                            encoder.sendSkipSinceNextTest();
+                            break;
+                        case SHUTDOWN:
+                            encoder.sendShutdown( cmd.getData() );
+                            break;
+                        case NOOP:
+                            encoder.sendNoop();
+                            break;
+                        case BYE_ACK:
+                            encoder.sendByeAck();
+                            break;
+                        default:
+                            logger.error( "Unknown enum " + cmd.getCommandType().name() );
+                    }
                 }
             }
         }
@@ -122,82 +127,4 @@ public class StreamFeeder extends CloseableDaemonThread
     {
         channel.close();
     }
-
-    /**
-     * Public method for testing purposes.
-     *
-     * @param cmdType command type
-     * @param data data to encode
-     * @return command with data encoded to bytes
-     */
-    public static byte[] encode( MasterProcessCommand cmdType, String data )
-    {
-        if ( !cmdType.hasDataType() )
-        {
-            throw new IllegalArgumentException( "cannot use data without data type" );
-        }
-
-        if ( cmdType.getDataType() != String.class )
-        {
-            throw new IllegalArgumentException( "Data type can be only " + String.class );
-        }
-
-        return encode( COMMAND_OPCODES.get( cmdType ), data )
-            .toString()
-            .getBytes( US_ASCII );
-    }
-
-    /**
-     * Public method for testing purposes.
-     *
-     * @param cmdType command type
-     * @return command without data encoded to bytes
-     */
-    public static byte[] encode( MasterProcessCommand cmdType )
-    {
-        if ( cmdType.getDataType() != Void.class )
-        {
-            throw new IllegalArgumentException( "Data type can be only " + cmdType.getDataType() );
-        }
-
-        return encode( COMMAND_OPCODES.get( cmdType ), null )
-            .toString()
-            .getBytes( US_ASCII );
-    }
-
-    /**
-     * Encodes opcode and data.
-     *
-     * @param operation opcode
-     * @param data   data
-     * @return encoded command
-     */
-    private static StringBuilder encode( String operation, String data )
-    {
-        StringBuilder s = new StringBuilder( 128 )
-            .append( ':' )
-            .append( MAGIC_NUMBER )
-            .append( ':' )
-            .append( operation );
-
-        if ( data != null )
-        {
-            s.append( ':' )
-                .append( data );
-        }
-
-        return s.append( ':' );
-    }
-
-    private static Map<MasterProcessCommand, String> opcodesToStrings()
-    {
-        Map<MasterProcessCommand, String> opcodes = new HashMap<>();
-        opcodes.put( RUN_CLASS, "run-testclass" );
-        opcodes.put( TEST_SET_FINISHED, "testset-finished" );
-        opcodes.put( SKIP_SINCE_NEXT_TEST, "skip-since-next-test" );
-        opcodes.put( SHUTDOWN, "shutdown" );
-        opcodes.put( NOOP, "noop" );
-        opcodes.put( BYE_ACK, "bye-ack" );
-        return new ImmutableMap<>( opcodes );
-    }
 }
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/SurefireForkChannel.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/SurefireForkChannel.java
index 639944e..5c3381c 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/SurefireForkChannel.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/SurefireForkChannel.java
@@ -25,7 +25,7 @@ import org.apache.maven.surefire.extensions.CloseableDaemonThread;
 import org.apache.maven.surefire.extensions.CommandReader;
 import org.apache.maven.surefire.extensions.EventHandler;
 import org.apache.maven.surefire.extensions.ForkChannel;
-import org.apache.maven.surefire.extensions.ForkNodeArguments;
+import org.apache.maven.surefire.api.fork.ForkNodeArguments;
 import org.apache.maven.surefire.extensions.util.CountdownCloseable;
 import org.apache.maven.surefire.extensions.util.LineConsumerThread;
 
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/SurefireForkNodeFactory.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/SurefireForkNodeFactory.java
index 0b47784..f9c6d71 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/SurefireForkNodeFactory.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/SurefireForkNodeFactory.java
@@ -20,7 +20,7 @@ package org.apache.maven.plugin.surefire.extensions;
  */
 
 import org.apache.maven.surefire.extensions.ForkChannel;
-import org.apache.maven.surefire.extensions.ForkNodeArguments;
+import org.apache.maven.surefire.api.fork.ForkNodeArguments;
 import org.apache.maven.surefire.extensions.ForkNodeFactory;
 
 import javax.annotation.Nonnull;
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/surefire/stream/CommandEncoder.java b/maven-surefire-common/src/main/java/org/apache/maven/surefire/stream/CommandEncoder.java
new file mode 100644
index 0000000..81f9d77
--- /dev/null
+++ b/maven-surefire-common/src/main/java/org/apache/maven/surefire/stream/CommandEncoder.java
@@ -0,0 +1,148 @@
+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.surefire.api.booter.MasterProcessCommand;
+import org.apache.maven.surefire.api.stream.AbstractStreamEncoder;
+
+import javax.annotation.Nonnull;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.WritableByteChannel;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
+
+import static org.apache.maven.surefire.api.booter.Constants.DEFAULT_STREAM_ENCODING;
+import static org.apache.maven.surefire.api.booter.Constants.DEFAULT_STREAM_ENCODING_BYTES;
+import static org.apache.maven.surefire.api.booter.Constants.MAGIC_NUMBER_FOR_COMMANDS_BYTES;
+import static org.apache.maven.surefire.api.booter.MasterProcessCommand.BYE_ACK;
+import static org.apache.maven.surefire.api.booter.MasterProcessCommand.NOOP;
+import static org.apache.maven.surefire.api.booter.MasterProcessCommand.RUN_CLASS;
+import static org.apache.maven.surefire.api.booter.MasterProcessCommand.SHUTDOWN;
+import static org.apache.maven.surefire.api.booter.MasterProcessCommand.SKIP_SINCE_NEXT_TEST;
+import static org.apache.maven.surefire.api.booter.MasterProcessCommand.TEST_SET_FINISHED;
+import static org.apache.maven.surefire.api.report.RunMode.NORMAL_RUN;
+
+/**
+ *
+ */
+public class CommandEncoder extends AbstractStreamEncoder<MasterProcessCommand> implements AutoCloseable
+{
+    private final WritableByteChannel out;
+
+    public CommandEncoder( WritableByteChannel out )
+    {
+        super( out );
+        this.out = out;
+    }
+
+    public void sendRunClass( String testClassName ) throws IOException
+    {
+        CharsetEncoder encoder = newCharsetEncoder();
+        int bufferMaxLength =
+            estimateBufferLength( RUN_CLASS.getOpcodeLength(), NORMAL_RUN, encoder, 0, testClassName );
+        ByteBuffer result = ByteBuffer.allocate( bufferMaxLength );
+        encode( encoder, result, RUN_CLASS, NORMAL_RUN, testClassName );
+        write( result, true );
+    }
+
+    public void sendTestSetFinished() throws IOException
+    {
+        int bufferMaxLength = estimateBufferLength( TEST_SET_FINISHED.getOpcodeLength(), null, null, 0 );
+        ByteBuffer result = ByteBuffer.allocate( bufferMaxLength );
+        encodeHeader( result, TEST_SET_FINISHED, null );
+        write( result, true );
+    }
+
+    public void sendSkipSinceNextTest() throws IOException
+    {
+        int bufferMaxLength = estimateBufferLength( SKIP_SINCE_NEXT_TEST.getOpcodeLength(), null, null, 0 );
+        ByteBuffer result = ByteBuffer.allocate( bufferMaxLength );
+        encodeHeader( result, SKIP_SINCE_NEXT_TEST, null );
+        write( result, true );
+    }
+
+    public void sendShutdown( String shutdownData ) throws IOException
+    {
+        CharsetEncoder encoder = newCharsetEncoder();
+        int bufferMaxLength =
+            estimateBufferLength( SHUTDOWN.getOpcodeLength(), null, encoder, 0, shutdownData );
+        ByteBuffer result = ByteBuffer.allocate( bufferMaxLength );
+        encode( encoder, result, SHUTDOWN, null, shutdownData );
+        write( result, true );
+    }
+
+    public void sendNoop() throws IOException
+    {
+        int bufferMaxLength = estimateBufferLength( NOOP.getOpcodeLength(), null, null, 0 );
+        ByteBuffer result = ByteBuffer.allocate( bufferMaxLength );
+        encodeHeader( result, NOOP, null );
+        write( result, true );
+    }
+
+    public void sendByeAck() throws IOException
+    {
+        int bufferMaxLength = estimateBufferLength( BYE_ACK.getOpcodeLength(), null, null, 0 );
+        ByteBuffer result = ByteBuffer.allocate( bufferMaxLength );
+        encodeHeader( result, BYE_ACK, null );
+        write( result, true );
+    }
+
+    @Nonnull
+    @Override
+    protected final byte[] getEncodedMagicNumber()
+    {
+        return MAGIC_NUMBER_FOR_COMMANDS_BYTES;
+    }
+
+    @Nonnull
+    @Override
+    protected final byte[] enumToByteArray( MasterProcessCommand masterProcessCommand )
+    {
+        return masterProcessCommand.getOpcodeBinary();
+    }
+
+    @Nonnull
+    @Override
+    protected final byte[] getEncodedCharsetName()
+    {
+        return DEFAULT_STREAM_ENCODING_BYTES;
+    }
+
+    @Nonnull
+    @Override
+    protected final Charset getCharset()
+    {
+        return DEFAULT_STREAM_ENCODING;
+    }
+
+    @Nonnull
+    @Override
+    protected final CharsetEncoder newCharsetEncoder()
+    {
+        return getCharset().newEncoder();
+    }
+
+    @Override
+    public void close() throws IOException
+    {
+        out.close();
+    }
+}
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/surefire/stream/EventDecoder.java b/maven-surefire-common/src/main/java/org/apache/maven/surefire/stream/EventDecoder.java
new file mode 100644
index 0000000..3698511
--- /dev/null
+++ b/maven-surefire-common/src/main/java/org/apache/maven/surefire/stream/EventDecoder.java
@@ -0,0 +1,468 @@
+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.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.FutureTask;
+
+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;
+
+/**
+ *
+ */
+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_AND_ONE_STRING = new SegmentType[] {
+        RUN_MODE,
+        STRING_ENCODING,
+        DATA_STRING,
+        END_OF_FRAME
+    };
+
+    private static final SegmentType[] EVENT_WITH_RUNMODE_AND_TWO_STRINGS = new SegmentType[] {
+        RUN_MODE,
+        STRING_ENCODING,
+        DATA_STRING,
+        DATA_STRING,
+        END_OF_FRAME
+    };
+
+    private static final SegmentType[] EVENT_TEST_CONTROL = new SegmentType[] {
+        RUN_MODE,
+        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;
+
+    public EventDecoder( @Nonnull ReadableByteChannel channel,
+                         @Nonnull ForkNodeArguments arguments )
+    {
+        super( channel, arguments, EVENT_TYPES );
+        debugSink = newDebugSink( arguments );
+    }
+
+    @Override
+    public Event decode( @Nonnull Memento memento ) throws IOException
+    {
+        try
+        {
+            ForkedProcessEventType eventType = readMessageType( memento );
+            if ( eventType == null )
+            {
+                throw new MalformedFrameException( memento.getLine().getPositionByteBuffer(),
+                    memento.getByteBuffer().position() );
+            }
+            RunMode runMode = null;
+            for ( SegmentType segmentType : nextSegmentType( eventType ) )
+            {
+                switch ( segmentType )
+                {
+                    case RUN_MODE:
+                        runMode = RUN_MODES.get( readSegment( 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() );
+                        return toMessage( eventType, runMode, 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_AND_ONE_STRING;
+            case BOOTERCODE_SYSPROPS:
+                return EVENT_WITH_RUNMODE_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,
+                                     RunMode runMode,
+                                     @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_CONSOLE_ERROR:
+                checkArguments( memento, 3 );
+                return new ConsoleErrorEvent( toStackTraceWriter( memento.getData() ) );
+            case BOOTERCODE_JVM_EXIT_ERROR:
+                checkArguments( memento, 3 );
+                return new JvmExitErrorEvent( 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( runMode, memento, 1 );
+                return new StandardStreamOutEvent( runMode, (String) memento.getData().get( 0 ) );
+            case BOOTERCODE_STDOUT_NEW_LINE:
+                checkArguments( runMode, memento, 1 );
+                return new StandardStreamOutWithNewLineEvent( runMode, (String) memento.getData().get( 0 ) );
+            case BOOTERCODE_STDERR:
+                checkArguments( runMode, memento, 1 );
+                return new StandardStreamErrEvent( runMode, (String) memento.getData().get( 0 ) );
+            case BOOTERCODE_STDERR_NEW_LINE:
+                checkArguments( runMode, memento, 1 );
+                return new StandardStreamErrWithNewLineEvent( runMode, (String) memento.getData().get( 0 ) );
+            case BOOTERCODE_SYSPROPS:
+                checkArguments( runMode, memento, 2 );
+                String key = (String) memento.getData().get( 0 );
+                String value = (String) memento.getData().get( 1 );
+                return new SystemPropertyEvent( runMode, key, value );
+            case BOOTERCODE_TESTSET_STARTING:
+                checkArguments( runMode, memento, 10 );
+                return new TestsetStartingEvent( runMode, toReportEntry( memento.getData() ) );
+            case BOOTERCODE_TESTSET_COMPLETED:
+                checkArguments( runMode, memento, 10 );
+                return new TestsetCompletedEvent( runMode, toReportEntry( memento.getData() ) );
+            case BOOTERCODE_TEST_STARTING:
+                checkArguments( runMode, memento, 10 );
+                return new TestStartingEvent( runMode, toReportEntry( memento.getData() ) );
+            case BOOTERCODE_TEST_SUCCEEDED:
+                checkArguments( runMode, memento, 10 );
+                return new TestSucceededEvent( runMode, toReportEntry( memento.getData() ) );
+            case BOOTERCODE_TEST_FAILED:
+                checkArguments( runMode, memento, 10 );
+                return new TestFailedEvent( runMode, toReportEntry( memento.getData() ) );
+            case BOOTERCODE_TEST_SKIPPED:
+                checkArguments( runMode, memento, 10 );
+                return new TestSkippedEvent( runMode, toReportEntry( memento.getData() ) );
+            case BOOTERCODE_TEST_ERROR:
+                checkArguments( runMode, memento, 10 );
+                return new TestErrorEvent( runMode, toReportEntry( memento.getData() ) );
+            case BOOTERCODE_TEST_ASSUMPTIONFAILURE:
+                checkArguments( runMode, memento, 10 );
+                return new TestAssumptionFailureEvent( runMode, toReportEntry( memento.getData() ) );
+            default:
+                throw new IllegalArgumentException( "Missing a branch for the event type " + eventType );
+        }
+    }
+
+    @Nonnull
+    private static TestSetReportEntry toReportEntry( List<Object> args )
+    {
+        // ReportEntry:
+        String source = (String) args.get( 0 );
+        String sourceText = (String) args.get( 1 );
+        String name = (String) args.get( 2 );
+        String nameText = (String) args.get( 3 );
+        String group = (String) args.get( 4 );
+        String message = (String) args.get( 5 );
+        Integer timeElapsed = (Integer) args.get( 6 );
+        // StackTraceWriter:
+        String traceMessage = (String) args.get( 7 );
+        String smartTrimmedStackTrace = (String) args.get( 8 );
+        String stackTrace = (String) args.get( 9 );
+        return newReportEntry( 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:
+                                              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( source, sourceText, name, nameText, group, stackTraceWriter, timeElapsed, message,
+            Collections.<String, String>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 );
+            debugSink.flush();
+        }
+        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 );
+            Runtime.getRuntime().addShutdownHook( new Thread( new FutureTask<>( new Callable<Void>()
+            {
+                @Override
+                public Void call() throws Exception
+                {
+                    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();
+        }
+    }
+}
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/ForkingRunListenerTest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/ForkingRunListenerTest.java
index a8eb91f..48eb35f 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/ForkingRunListenerTest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/ForkingRunListenerTest.java
@@ -25,10 +25,10 @@ import org.apache.maven.plugin.surefire.booterclient.output.ForkClient;
 import org.apache.maven.plugin.surefire.extensions.EventConsumerThread;
 import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
 import org.apache.maven.surefire.api.booter.ForkingRunListener;
-import org.apache.maven.surefire.booter.spi.LegacyMasterProcessChannelEncoder;
+import org.apache.maven.surefire.booter.spi.EventChannelEncoder;
 import org.apache.maven.surefire.api.event.Event;
 import org.apache.maven.surefire.extensions.EventHandler;
-import org.apache.maven.surefire.extensions.ForkNodeArguments;
+import org.apache.maven.surefire.api.fork.ForkNodeArguments;
 import org.apache.maven.surefire.extensions.util.CountdownCloseable;
 import org.apache.maven.surefire.api.report.CategorizedReportEntry;
 import org.apache.maven.surefire.api.report.ConsoleOutputReceiver;
@@ -258,11 +258,11 @@ public class ForkingRunListenerTest
         ReportEntry expected = createDefaultReportEntry();
         SimpleReportEntry secondExpected = createAnotherDefaultReportEntry();
 
-        new ForkingRunListener( new LegacyMasterProcessChannelEncoder( newBufferedChannel( printStream ) ), false )
+        new ForkingRunListener( new EventChannelEncoder( newBufferedChannel( printStream ) ), false )
                 .testStarting( expected );
 
         new ForkingRunListener(
-            new LegacyMasterProcessChannelEncoder( newBufferedChannel( anotherPrintStream ) ), false )
+            new EventChannelEncoder( newBufferedChannel( anotherPrintStream ) ), false )
                 .testSkipped( secondExpected );
 
         TestSetMockReporterFactory providerReporterFactory = new TestSetMockReporterFactory();
@@ -375,6 +375,18 @@ public class ForkingRunListenerTest
         {
             return !dumpStreamText.isEmpty() || !logWarningAtEnd.isEmpty();
         }
+
+        @Override
+        public File getEventStreamBinaryFile()
+        {
+            return null;
+        }
+
+        @Override
+        public File getCommandStreamBinaryFile()
+        {
+            return null;
+        }
     }
 
     private static class EH implements EventHandler<Event>
@@ -446,7 +458,7 @@ public class ForkingRunListenerTest
     private RunListener createForkingRunListener()
     {
         WritableBufferedByteChannel channel = (WritableBufferedByteChannel) newChannel( printStream );
-        return new ForkingRunListener( new LegacyMasterProcessChannelEncoder( channel ), false );
+        return new ForkingRunListener( new EventChannelEncoder( channel ), false );
     }
 
     private class StandardTestRun
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/MainClass.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/MainClass.java
index 3d58ade..845897a 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/MainClass.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/MainClass.java
@@ -35,7 +35,7 @@ public class MainClass
         else
         {
             System.out.println( ":maven-surefire-event:\u0003:bye:" );
-            String byeAck = ":maven-surefire-command:bye-ack:";
+            String byeAck = ":maven-surefire-command:\u0007:bye-ack:";
             byte[] cmd = new byte[byeAck.length()];
             int len = System.in.read( cmd );
             if ( len != -1 && new String( cmd, 0, len ).equals( byeAck ) )
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/TestLessInputStreamBuilderTest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/TestLessInputStreamBuilderTest.java
index 3c68778..87bad45 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/TestLessInputStreamBuilderTest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/TestLessInputStreamBuilderTest.java
@@ -20,9 +20,9 @@ package org.apache.maven.plugin.surefire.booterclient.lazytestprovider;
  */
 
 import org.apache.maven.surefire.api.booter.Command;
-import org.apache.maven.surefire.api.booter.MasterProcessCommand;
-import org.apache.maven.surefire.booter.spi.LegacyMasterProcessChannelDecoder;
 import org.apache.maven.surefire.api.booter.MasterProcessChannelDecoder;
+import org.apache.maven.surefire.booter.ForkedNodeArg;
+import org.apache.maven.surefire.booter.spi.CommandChannelDecoder;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
@@ -33,17 +33,19 @@ import java.util.Iterator;
 import java.util.NoSuchElementException;
 
 import static java.nio.channels.Channels.newChannel;
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.apache.maven.plugin.surefire.booterclient.lazytestprovider.TestLessInputStream.TestLessInputStreamBuilder;
 import static org.apache.maven.surefire.api.booter.Command.SKIP_SINCE_NEXT_TEST;
+import static org.apache.maven.surefire.api.booter.MasterProcessCommand.NOOP;
 import static org.apache.maven.surefire.api.booter.MasterProcessCommand.SHUTDOWN;
 import static org.apache.maven.surefire.api.booter.Shutdown.EXIT;
 import static org.apache.maven.surefire.api.booter.Shutdown.KILL;
-import static org.apache.maven.plugin.surefire.extensions.StreamFeeder.encode;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.notNullValue;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 /**
  * Testing cached and immediate commands in {@link TestLessInputStream}.
@@ -144,6 +146,7 @@ public class TestLessInputStreamBuilderTest
         {
             private byte[] buffer;
             private int idx;
+            private boolean isLastBuffer;
 
             @Override
             public int read() throws IOException
@@ -154,8 +157,20 @@ public class TestLessInputStreamBuilderTest
                     Command cmd = pluginIs.readNextCommand();
                     if ( cmd != null )
                     {
-                        MasterProcessCommand cmdType = cmd.getCommandType();
-                        buffer = cmdType.hasDataType() ? encode( cmdType, cmd.getData() ) : encode( cmdType );
+                        if ( cmd.getCommandType() == SHUTDOWN )
+                        {
+                            buffer = ( ":maven-surefire-command:\u0008:shutdown:\u0005:UTF-8:\u0000\u0000\u0000\u0004:"
+                                + cmd.toShutdownData().getParam() + ":" ).getBytes( UTF_8 );
+                        }
+                        else if ( cmd.getCommandType() == NOOP )
+                        {
+                            buffer = ":maven-surefire-command:\u0004:noop:".getBytes( UTF_8 );
+                            isLastBuffer = true;
+                        }
+                        else
+                        {
+                            fail();
+                        }
                     }
                 }
 
@@ -169,10 +184,16 @@ public class TestLessInputStreamBuilderTest
                     }
                     return b;
                 }
+
+                if ( isLastBuffer )
+                {
+                    return -1;
+                }
                 throw new IOException();
             }
         };
-        MasterProcessChannelDecoder decoder = new LegacyMasterProcessChannelDecoder( newChannel( is ) );
+        MasterProcessChannelDecoder decoder =
+            new CommandChannelDecoder( newChannel( is ), new ForkedNodeArg( 1, false ) );
         builder.getImmediateCommands().shutdown( KILL );
         builder.getImmediateCommands().noop();
         Command bye = decoder.decode();
@@ -181,7 +202,7 @@ public class TestLessInputStreamBuilderTest
         assertThat( bye.getData(), is( KILL.name() ) );
         Command noop = decoder.decode();
         assertThat( noop, is( notNullValue() ) );
-        assertThat( noop.getCommandType(), is( MasterProcessCommand.NOOP ) );
+        assertThat( noop.getCommandType(), is( NOOP ) );
     }
 
     @Test( expected = UnsupportedOperationException.class )
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/TestProvidingInputStreamTest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/TestProvidingInputStreamTest.java
index e0acb9f..29a5745 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/TestProvidingInputStreamTest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/TestProvidingInputStreamTest.java
@@ -20,9 +20,9 @@ package org.apache.maven.plugin.surefire.booterclient.lazytestprovider;
  */
 
 import org.apache.maven.surefire.api.booter.Command;
-import org.apache.maven.surefire.booter.spi.LegacyMasterProcessChannelDecoder;
-import org.apache.maven.plugin.surefire.extensions.StreamFeeder;
 import org.apache.maven.surefire.api.booter.MasterProcessChannelDecoder;
+import org.apache.maven.surefire.booter.ForkedNodeArg;
+import org.apache.maven.surefire.booter.spi.CommandChannelDecoder;
 import org.junit.Test;
 
 import java.io.IOException;
@@ -36,7 +36,8 @@ import java.util.concurrent.FutureTask;
 import java.util.concurrent.TimeUnit;
 
 import static java.nio.channels.Channels.newChannel;
-import static java.nio.charset.StandardCharsets.US_ASCII;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.maven.surefire.api.booter.Command.TEST_SET_FINISHED;
 import static org.apache.maven.surefire.api.booter.MasterProcessCommand.BYE_ACK;
 import static org.apache.maven.surefire.api.booter.MasterProcessCommand.NOOP;
 import static org.hamcrest.MatcherAssert.assertThat;
@@ -44,6 +45,7 @@ import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.notNullValue;
 import static org.hamcrest.Matchers.nullValue;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 /**
  * Asserts that this stream properly reads bytes from queue.
@@ -93,8 +95,7 @@ public class TestProvidingInputStreamTest
     public void finishedTestsetShouldNotBlock()
         throws IOException
     {
-        Queue<String> commands = new ArrayDeque<>();
-        final TestProvidingInputStream is = new TestProvidingInputStream( commands );
+        final TestProvidingInputStream is = new TestProvidingInputStream( new ArrayDeque<String>() );
         is.testSetFinished();
         new Thread( new Runnable()
         {
@@ -105,16 +106,12 @@ public class TestProvidingInputStreamTest
             }
         } ).start();
 
-        Command cmd = is.readNextCommand();
-        assertThat( cmd.getData(), is( nullValue() ) );
-        String stream = new String( StreamFeeder.encode( cmd.getCommandType() ), US_ASCII );
-
-        cmd = is.readNextCommand();
-        assertThat( cmd.getData(), is( nullValue() ) );
-        stream += new String( StreamFeeder.encode( cmd.getCommandType() ), US_ASCII );
-
-        assertThat( stream,
-            is( ":maven-surefire-command:testset-finished::maven-surefire-command:testset-finished:" ) );
+        for ( int i = 0; i < 2; i++ )
+        {
+            Command cmd = is.readNextCommand();
+            assertThat( cmd.getData(), is( nullValue() ) );
+            assertThat( cmd, is( TEST_SET_FINISHED ) );
+        }
 
         boolean emptyStream = isInputStreamEmpty( is );
 
@@ -162,7 +159,21 @@ public class TestProvidingInputStreamTest
                 {
                     idx = 0;
                     Command cmd = pluginIs.readNextCommand();
-                    buffer = cmd == null ? null : StreamFeeder.encode( cmd.getCommandType() );
+                    if ( cmd != null )
+                    {
+                        if ( cmd.getCommandType() == BYE_ACK )
+                        {
+                            buffer = ":maven-surefire-command:\u0007:bye-ack:".getBytes( UTF_8 );
+                        }
+                        else if ( cmd.getCommandType() == NOOP )
+                        {
+                            buffer = ":maven-surefire-command:\u0004:noop:".getBytes( UTF_8 );
+                        }
+                        else
+                        {
+                            fail();
+                        }
+                    }
                 }
 
                 if ( buffer != null )
@@ -178,7 +189,8 @@ public class TestProvidingInputStreamTest
                 throw new IOException();
             }
         };
-        MasterProcessChannelDecoder decoder = new LegacyMasterProcessChannelDecoder( newChannel( is ) );
+        MasterProcessChannelDecoder decoder =
+            new CommandChannelDecoder( newChannel( is ), new ForkedNodeArg( 1, false ) );
         pluginIs.acknowledgeByeEventReceived();
         pluginIs.noop();
         Command bye = decoder.decode();
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/output/ForkClientTest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/output/ForkClientTest.java
index 5d871a3..531bafb 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/output/ForkClientTest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/output/ForkClientTest.java
@@ -52,7 +52,7 @@ import org.apache.maven.surefire.api.report.SimpleReportEntry;
 import org.apache.maven.surefire.api.report.StackTraceWriter;
 import org.apache.maven.surefire.api.report.TestSetReportEntry;
 import org.apache.maven.surefire.extensions.EventHandler;
-import org.apache.maven.surefire.extensions.ForkNodeArguments;
+import org.apache.maven.surefire.api.fork.ForkNodeArguments;
 import org.apache.maven.surefire.extensions.util.CountdownCloseable;
 import org.junit.Test;
 
@@ -302,7 +302,8 @@ public class ForkClientTest
         assertThat( logger.isDebugEnabledCalled )
             .isTrue();
 
-        String msg = "Corrupted STDOUT by directly writing to native stream in forked JVM 0. Stream 'unordered error'.";
+        String msg =
+            "Corrupted channel by directly writing to native stream in forked JVM 0. Stream 'unordered error'.";
         assertThat( arguments.dumpStreamText )
             .hasSize( 1 )
             .contains( msg );
@@ -310,7 +311,7 @@ public class ForkClientTest
         assertThat( arguments.logWarningAtEnd )
             .hasSize( 1 );
         assertThat( arguments.logWarningAtEnd.peek() )
-            .startsWith( "Corrupted STDOUT by directly writing to native stream in forked JVM 0. "
+            .startsWith( "Corrupted channel by directly writing to native stream in forked JVM 0. "
                 + "See FAQ web page and the dump file" );
     }
 
@@ -1883,6 +1884,18 @@ public class ForkClientTest
         {
             return !dumpStreamText.isEmpty() || !logWarningAtEnd.isEmpty();
         }
+
+        @Override
+        public File getEventStreamBinaryFile()
+        {
+            return null;
+        }
+
+        @Override
+        public File getCommandStreamBinaryFile()
+        {
+            return null;
+        }
     }
 
     /**
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/E2ETest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/E2ETest.java
index f3becd7..87bcca7 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/E2ETest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/E2ETest.java
@@ -27,7 +27,7 @@ import org.apache.maven.surefire.api.event.Event;
 import org.apache.maven.surefire.api.report.ConsoleOutputReceiver;
 import org.apache.maven.surefire.booter.spi.SurefireMasterProcessChannelProcessorFactory;
 import org.apache.maven.surefire.extensions.EventHandler;
-import org.apache.maven.surefire.extensions.ForkNodeArguments;
+import org.apache.maven.surefire.api.fork.ForkNodeArguments;
 import org.apache.maven.surefire.extensions.util.CountdownCloseable;
 import org.junit.Rule;
 import org.junit.Test;
@@ -74,7 +74,7 @@ public class E2ETest
 
         final SurefireMasterProcessChannelProcessorFactory factory = new SurefireMasterProcessChannelProcessorFactory();
         factory.connect( connection );
-        final MasterProcessChannelEncoder encoder = factory.createEncoder();
+        final MasterProcessChannelEncoder encoder = factory.createEncoder( arguments );
 
         System.gc();
 
@@ -333,5 +333,17 @@ public class E2ETest
         {
             return logger;
         }
+
+        @Override
+        public File getEventStreamBinaryFile()
+        {
+            return null;
+        }
+
+        @Override
+        public File getCommandStreamBinaryFile()
+        {
+            return null;
+        }
     }
 }
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/EventConsumerThreadTest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/EventConsumerThreadTest.java
index 63287df..9357080 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/EventConsumerThreadTest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/EventConsumerThreadTest.java
@@ -19,424 +19,32 @@ package org.apache.maven.plugin.surefire.extensions;
  * under the License.
  */
 
-import org.apache.maven.plugin.surefire.extensions.EventConsumerThread.Memento;
-import org.apache.maven.plugin.surefire.extensions.EventConsumerThread.Segment;
-import org.apache.maven.plugin.surefire.extensions.EventConsumerThread.SegmentType;
 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.api.report.RunMode;
+import org.apache.maven.surefire.api.fork.ForkNodeArguments;
 import org.apache.maven.surefire.extensions.EventHandler;
-import org.apache.maven.surefire.extensions.ForkNodeArguments;
 import org.apache.maven.surefire.extensions.util.CountdownCloseable;
-import org.fest.assertions.Condition;
 import org.junit.Test;
 
 import javax.annotation.Nonnull;
 import java.io.Closeable;
-import java.io.EOFException;
 import java.io.File;
-import java.math.BigInteger;
 import java.nio.ByteBuffer;
-import java.nio.CharBuffer;
 import java.nio.channels.ReadableByteChannel;
-import java.nio.charset.CharsetDecoder;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import static java.lang.Math.min;
-import static java.nio.charset.CodingErrorAction.REPLACE;
-import static java.nio.charset.StandardCharsets.ISO_8859_1;
-import static java.nio.charset.StandardCharsets.US_ASCII;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.Arrays.asList;
 import static java.util.Arrays.copyOfRange;
-import static java.util.Collections.emptyList;
-import static java.util.Collections.singletonList;
-import static org.apache.maven.plugin.surefire.extensions.EventConsumerThread.SegmentType.DATA_INT;
-import static org.apache.maven.plugin.surefire.extensions.EventConsumerThread.SegmentType.DATA_STRING;
-import static org.apache.maven.plugin.surefire.extensions.EventConsumerThread.SegmentType.END_OF_FRAME;
-import static org.apache.maven.plugin.surefire.extensions.EventConsumerThread.SegmentType.RUN_MODE;
-import static org.apache.maven.plugin.surefire.extensions.EventConsumerThread.SegmentType.STRING_ENCODING;
-import static org.apache.maven.plugin.surefire.extensions.EventConsumerThread.mapEventTypes;
-import static org.apache.maven.plugin.surefire.extensions.EventConsumerThread.mapRunModes;
-import static org.apache.maven.plugin.surefire.extensions.EventConsumerThread.nextSegmentType;
-import static org.apache.maven.plugin.surefire.extensions.EventConsumerThread.toEvent;
-import static org.apache.maven.surefire.api.booter.Constants.DEFAULT_STREAM_ENCODING;
-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;
+import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.fest.assertions.Assertions.assertThat;
-import static org.powermock.reflect.Whitebox.invokeMethod;
 
 /**
- * The performance of "get( Integer )" is 13.5 nano seconds on i5/2.6GHz:
- * <pre>
- *     {@code
- *     TreeMap<Integer, ForkedProcessEventType> map = new TreeMap<>();
- *     map.get( hash );
- *     }
- * </pre>
  *
- * <br> The performance of getting event type by Segment is 33.7 nano seconds:
- * <pre>
- *     {@code
- *     Map<Segment, ForkedProcessEventType> map = new HashMap<>();
- *     byte[] array = ForkedProcessEventType.BOOTERCODE_STDOUT.getOpcode().getBytes( UTF_8 );
- *     map.get( new Segment( array, 0, array.length ) );
- *     }
- * </pre>
- *
- * <br> The performance of decoder:
- * <pre>
- *     {@code
- *     CharsetDecoder decoder = STREAM_ENCODING.newDecoder()
- *             .onMalformedInput( REPLACE )
- *             .onUnmappableCharacter( REPLACE );
- *     ByteBuffer buffer = ByteBuffer.wrap( ForkedProcessEventType.BOOTERCODE_STDOUT.getOpcode().getBytes( UTF_8 ) );
- *     CharBuffer chars = CharBuffer.allocate( 100 );
- *     decoder.reset().decode( buffer, chars, true );
- *
- *     String s = chars.flip().toString(); // 37 nanos = CharsetDecoder + toString
- *
- *     buffer.clear();
- *     chars.clear();
- *
- *     ForkedProcessEventType.byOpcode( s ); // 65 nanos = CharsetDecoder + toString + byOpcode
- *     }
- * </pre>
- *
- * <br> The performance of decoding 100 bytes via CharacterDecoder - 71 nano seconds:
- * <pre>
- *     {@code
- *     decoder.reset()
- *         .decode( buffer, chars, true ); // CharsetDecoder 71 nanos
- *     chars.flip().toString(); // CharsetDecoder + toString = 91 nanos
- *     }
- * </pre>
- *
- * <br> The performance of a pure string creation (instead of decoder) - 31.5 nano seconds:
- * <pre>
- *     {@code
- *     byte[] b = {};
- *     new String( b, UTF_8 );
- *     }
- * </pre>
- *
- * <br> The performance of CharsetDecoder with empty ByteBuffer:
- * <pre>
- *     {@code
- *     CharsetDecoder + ByteBuffer.allocate( 0 ) makes 11.5 nanos
- *     CharsetDecoder + ByteBuffer.allocate( 0 ) + toString() makes 16.1 nanos
- *     }
- * </pre>
  */
-@SuppressWarnings( "checkstyle:magicnumber" )
 public class EventConsumerThreadTest
 {
-    private static final CountdownCloseable COUNTDOWN_CLOSEABLE = new CountdownCloseable( new MockCloseable(), 0 );
-
-    private static final String PATTERN1 =
-        "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789";
-
-    private static final String PATTERN2 = "€ab©c";
-
-    private static final byte[] PATTERN2_BYTES =
-        new byte[]{(byte) -30, (byte) -126, (byte) -84, 'a', 'b', (byte) 0xc2, (byte) 0xa9, 'c'};
-
-    @Test
-    public void shouldDecodeHappyCase() throws Exception
-    {
-        CharsetDecoder decoder = UTF_8.newDecoder()
-            .onMalformedInput( REPLACE )
-            .onUnmappableCharacter( REPLACE );
-        ByteBuffer input = ByteBuffer.allocate( 1024 );
-        input.put( PATTERN2_BYTES )
-            .flip();
-        int bytesToDecode = PATTERN2_BYTES.length;
-        CharBuffer output = CharBuffer.allocate( 1024 );
-        int readBytes =
-            invokeMethod( EventConsumerThread.class, "decodeString", decoder, input, output, bytesToDecode, true, 0 );
-
-        assertThat( readBytes )
-            .isEqualTo( bytesToDecode );
-
-        assertThat( output.flip().toString() )
-            .isEqualTo( PATTERN2 );
-    }
-
-    @Test
-    public void shouldDecodeShifted() throws Exception
-    {
-        CharsetDecoder decoder = UTF_8.newDecoder()
-            .onMalformedInput( REPLACE )
-            .onUnmappableCharacter( REPLACE );
-        ByteBuffer input = ByteBuffer.allocate( 1024 );
-        input.put( PATTERN1.getBytes( UTF_8 ) )
-            .put( 90, (byte) 'A' )
-            .put( 91, (byte) 'B' )
-            .put( 92, (byte) 'C' )
-            .position( 90 );
-        CharBuffer output = CharBuffer.allocate( 1024 );
-        int readBytes =
-            invokeMethod( EventConsumerThread.class, "decodeString", decoder, input, output, 2, true, 0 );
-
-        assertThat( readBytes )
-            .isEqualTo( 2 );
-
-        assertThat( output.flip().toString() )
-            .isEqualTo( "AB" );
-    }
-
-    @Test( expected = IllegalArgumentException.class )
-    public void shouldNotDecode() throws Exception
-    {
-        CharsetDecoder decoder = UTF_8.newDecoder();
-        ByteBuffer input = ByteBuffer.allocate( 100 );
-        int bytesToDecode = 101;
-        CharBuffer output = CharBuffer.allocate( 1000 );
-        invokeMethod( EventConsumerThread.class, "decodeString", decoder, input, output, bytesToDecode, true, 0 );
-    }
-
-    @Test
-    public void shouldReadInt() throws Exception
-    {
-        Channel channel = new Channel( new byte[] {0x01, 0x02, 0x03, 0x04, ':'}, 1 );
-
-        EventConsumerThread thread = new EventConsumerThread( "t", channel,
-            new MockEventHandler<Event>(), COUNTDOWN_CLOSEABLE, new MockForkNodeArguments() );
-
-        Memento memento = thread.new Memento();
-        memento.bb.position( 0 ).limit( 0 );
-        assertThat( thread.readInt( memento ) )
-            .isEqualTo( new BigInteger( new byte[] {0x01, 0x02, 0x03, 0x04} ).intValue() );
-    }
-
-    @Test
-    public void shouldReadInteger() throws Exception
-    {
-        Channel channel = new Channel( new byte[] {(byte) 0xff, 0x01, 0x02, 0x03, 0x04, ':'}, 1 );
-
-        EventConsumerThread thread = new EventConsumerThread( "t", channel,
-            new MockEventHandler<Event>(), COUNTDOWN_CLOSEABLE, new MockForkNodeArguments() );
-
-        Memento memento = thread.new Memento();
-        memento.bb.position( 0 ).limit( 0 );
-        assertThat( thread.readInteger( memento ) )
-            .isEqualTo( new BigInteger( new byte[] {0x01, 0x02, 0x03, 0x04} ).intValue() );
-    }
-
-    @Test
-    public void shouldReadNullInteger() throws Exception
-    {
-        Channel channel = new Channel( new byte[] {(byte) 0x00, ':'}, 1 );
-
-        EventConsumerThread thread = new EventConsumerThread( "t", channel,
-            new MockEventHandler<Event>(), COUNTDOWN_CLOSEABLE, new MockForkNodeArguments() );
-
-        Memento memento = thread.new Memento();
-        memento.bb.position( 0 ).limit( 0 );
-        assertThat( thread.readInteger( memento ) )
-            .isNull();
-    }
-
-    @Test( expected = EOFException.class )
-    public void shouldNotReadString() throws Exception
-    {
-        Channel channel = new Channel( PATTERN1.getBytes(), PATTERN1.length() );
-        channel.read( ByteBuffer.allocate( 100 ) );
-
-        EventConsumerThread thread = new EventConsumerThread( "t", channel,
-            new MockEventHandler<Event>(), COUNTDOWN_CLOSEABLE, new MockForkNodeArguments() );
-
-        Memento memento = thread.new Memento();
-        memento.bb.position( 0 ).limit( 0 );
-        thread.readString( memento, 10 );
-    }
-
-    @Test
-    public void shouldReadString() throws Exception
-    {
-        Channel channel = new Channel( PATTERN1.getBytes(), PATTERN1.length() );
-
-        EventConsumerThread thread = new EventConsumerThread( "t", channel,
-            new MockEventHandler<Event>(), COUNTDOWN_CLOSEABLE, new MockForkNodeArguments() );
-
-        Memento memento = thread.new Memento();
-        memento.bb.position( 0 ).limit( 0 );
-        String s = thread.readString( memento, 10 );
-        assertThat( s )
-            .isEqualTo( "0123456789" );
-    }
-
-    @Test
-    public void shouldReadStringShiftedBuffer() throws Exception
-    {
-        StringBuilder s = new StringBuilder( 1100 );
-        for ( int i = 0; i < 11; i++ )
-        {
-            s.append( PATTERN1 );
-        }
-
-        Channel channel = new Channel( s.toString().getBytes( UTF_8 ), s.length() );
-
-        EventConsumerThread thread = new EventConsumerThread( "t", channel,
-            new MockEventHandler<Event>(), COUNTDOWN_CLOSEABLE, new MockForkNodeArguments() );
-
-        Memento memento = thread.new Memento();
-        // whatever position will be compacted to 0
-        memento.bb.position( 974 ).limit( 974 );
-        assertThat( thread.readString( memento, PATTERN1.length() + 3 ) )
-            .isEqualTo( PATTERN1 + "012" );
-    }
-
-    @Test
-    public void shouldReadStringShiftedInput() throws Exception
-    {
-        StringBuilder s = new StringBuilder( 1100 );
-        for ( int i = 0; i < 11; i++ )
-        {
-            s.append( PATTERN1 );
-        }
-
-        Channel channel = new Channel( s.toString().getBytes( UTF_8 ), s.length() );
-        channel.read( ByteBuffer.allocate( 997 ) );
-
-        EventConsumerThread thread = new EventConsumerThread( "t", channel,
-            new MockEventHandler<Event>(), COUNTDOWN_CLOSEABLE, new MockForkNodeArguments() );
-
-        Memento memento = thread.new Memento();
-        memento.bb.limit( 0 );
-        assertThat( thread.readString( memento, PATTERN1.length() ) )
-            .isEqualTo( "789" + PATTERN1.substring( 0, 97 ) );
-    }
-
-    @Test
-    public void shouldReadMultipleStringsAndShiftedInput() throws Exception
-    {
-        StringBuilder s = new StringBuilder( 5000 );
-
-        for ( int i = 0; i < 50; i++ )
-        {
-            s.append( PATTERN1 );
-        }
-
-        Channel channel = new Channel( s.toString().getBytes( UTF_8 ), s.length() );
-        channel.read( ByteBuffer.allocate( 1997 ) );
-
-        EventConsumerThread thread = new EventConsumerThread( "t", channel,
-            new MockEventHandler<Event>(), COUNTDOWN_CLOSEABLE, new MockForkNodeArguments() );
-
-        Memento memento = thread.new Memento();
-        // whatever position will be compacted to 0
-        memento.bb.position( 974 ).limit( 974 );
-
-        StringBuilder expected = new StringBuilder( "789" );
-        for ( int i = 0; i < 11; i++ )
-        {
-            expected.append( PATTERN1 );
-        }
-        expected.setLength( 1100 );
-        assertThat( thread.readString( memento, 1100 ) )
-            .isEqualTo( expected.toString() );
-    }
-
-    @Test
-    public void shouldDecode3BytesEncodedSymbol() throws Exception
-    {
-        byte[] encodedSymbol = new byte[] {(byte) -30, (byte) -126, (byte) -84};
-        int countSymbols = 1024;
-        byte[] input = new byte[encodedSymbol.length * countSymbols];
-        for ( int i = 0; i < countSymbols; i++ )
-        {
-            System.arraycopy( encodedSymbol, 0, input, encodedSymbol.length * i, encodedSymbol.length );
-        }
-
-        Channel channel = new Channel( input, 2 );
-        EventConsumerThread thread = new EventConsumerThread( "t", channel,
-            new MockEventHandler<Event>(), COUNTDOWN_CLOSEABLE, new MockForkNodeArguments() );
-        Memento memento = thread.new Memento();
-        memento.bb.position( 0 ).limit( 0 );
-        String decodedOutput = thread.readString( memento, input.length );
-
-        assertThat( decodedOutput )
-            .isEqualTo( new String( input, 0, input.length, UTF_8 ) );
-    }
-
-    @Test
-    public void shouldDecode100Bytes()
-    {
-        CharsetDecoder decoder = DEFAULT_STREAM_ENCODING.newDecoder()
-            .onMalformedInput( REPLACE )
-            .onUnmappableCharacter( REPLACE );
-        // empty stream: CharsetDecoder + ByteBuffer.allocate( 0 ) makes 11.5 nanos
-        // empty stream: CharsetDecoder + ByteBuffer.allocate( 0 ) + toString() makes 16.1 nanos
-        ByteBuffer buffer = ByteBuffer.wrap( PATTERN1.getBytes( UTF_8 ) );
-        CharBuffer chars = CharBuffer.allocate( 100 );
-        /* uncomment this section for a proper measurement of the exec time
-        TimeUnit.SECONDS.sleep( 2 );
-        System.gc();
-        TimeUnit.SECONDS.sleep( 5 );
-        */
-        String s = null;
-        long l1 = System.currentTimeMillis();
-        for ( int i = 0; i < 10_000_000; i++ )
-        {
-            decoder.reset()
-                .decode( buffer, chars, true ); // CharsetDecoder 71 nanos
-            s = chars.flip().toString(); // CharsetDecoder + toString = 91 nanos
-            buffer.clear();
-            chars.clear();
-        }
-        long l2 = System.currentTimeMillis();
-        System.out.println( "decoded 100 bytes within " + ( l2 - l1 ) + " millis (10 million cycles)" );
-        assertThat( s )
-            .isEqualTo( PATTERN1 );
-    }
-
+    @SuppressWarnings( "checkstyle:magicnumber" )
     @Test( timeout = 60_000L )
     public void performanceTest() throws Exception
     {
@@ -477,32 +85,36 @@ public class EventConsumerThreadTest
 
         event.flip();
         byte[] frame = copyOfRange( event.array(), event.arrayOffset(), event.arrayOffset() + event.remaining() );
-        ReadableByteChannel channel = new Channel( frame, 1024 )
+        ReadableByteChannel channel = new Channel( frame, 100 )
         {
             private int countRounds;
 
             @Override
-            public int read( ByteBuffer dst )
+            public synchronized int read( ByteBuffer dst )
             {
-                int length = super.read( dst );
-                if ( length == -1 && countRounds < totalCalls )
+                if ( countRounds == totalCalls )
+                {
+                    return -1;
+                }
+
+                if ( remaining() == 0 )
                 {
-                    i = 0;
-                    length = super.read( dst );
                     countRounds++;
+                    i = 0;
                 }
-                return length;
+
+                return super.read( dst );
             }
         };
 
         EventConsumerThread thread = new EventConsumerThread( "t", channel, handler,
             new CountdownCloseable( new MockCloseable(), 1 ), new MockForkNodeArguments() );
 
-        TimeUnit.SECONDS.sleep( 2 );
+        SECONDS.sleep( 2 );
         System.gc();
-        TimeUnit.SECONDS.sleep( 5 );
+        SECONDS.sleep( 5 );
 
-        System.out.println( "Staring the event thread..." );
+        System.out.println( "Starting the event thread..." );
 
         thread.start();
         thread.join();
@@ -510,596 +122,12 @@ public class EventConsumerThreadTest
         long execTime = finishedAt[0] - staredAt[0];
         System.out.println( execTime );
 
-        // 0.6 seconds while using the encoder/decoder
+        // 0.6 seconds while using the encoder/decoder for 10 million messages
         assertThat( execTime )
-            .describedAs( "The performance test should assert 1.0s of read time. "
-                + "The limit 3.6s guarantees that the read time does not exceed this limit on overloaded CPU." )
+            .describedAs( "The performance test should assert 0.75s of read time. "
+                + "The limit 3.65s guarantees that the read time does not exceed this limit on overloaded CPU." )
             .isPositive()
-            .isLessThanOrEqualTo( 3_600L );
-    }
-
-    @Test
-    public void shouldReadEventType() throws Exception
-    {
-        Map<Segment, ForkedProcessEventType> eventTypes = mapEventTypes();
-        assertThat( eventTypes )
-            .hasSize( ForkedProcessEventType.values().length );
-
-        byte[] stream = ":maven-surefire-event:\u000E:std-out-stream:".getBytes( UTF_8 );
-        Channel channel = new Channel( stream, 1 );
-        EventConsumerThread thread = new EventConsumerThread( "t", channel,
-            new MockEventHandler<Event>(), COUNTDOWN_CLOSEABLE, new MockForkNodeArguments() );
-
-        Memento memento = thread.new Memento();
-        memento.bb.position( 0 ).limit( 0 );
-        memento.setCharset( UTF_8 );
-
-        ForkedProcessEventType eventType = thread.readEventType( eventTypes, memento );
-        assertThat( eventType )
-            .isEqualTo( BOOTERCODE_STDOUT );
-    }
-
-    @Test( expected = EOFException.class )
-    public void shouldEventTypeReachedEndOfStream() throws Exception
-    {
-        Map<Segment, ForkedProcessEventType> eventTypes = mapEventTypes();
-        assertThat( eventTypes )
-            .hasSize( ForkedProcessEventType.values().length );
-
-        byte[] stream = ":maven-surefire-event:\u000E:xxx".getBytes( UTF_8 );
-        Channel channel = new Channel( stream, 1 );
-        EventConsumerThread thread = new EventConsumerThread( "t", channel,
-            new MockEventHandler<Event>(), COUNTDOWN_CLOSEABLE, new MockForkNodeArguments() );
-
-        Memento memento = thread.new Memento();
-        memento.bb.position( 0 ).limit( 0 );
-        memento.setCharset( UTF_8 );
-        thread.readEventType( eventTypes, memento );
-    }
-
-    @Test( expected = EventConsumerThread.MalformedFrameException.class )
-    public void shouldEventTypeReachedMalformedHeader() throws Exception
-    {
-        Map<Segment, ForkedProcessEventType> eventTypes = mapEventTypes();
-        assertThat( eventTypes )
-            .hasSize( ForkedProcessEventType.values().length );
-
-        byte[] stream = ":xxxxx-xxxxxxxx-xxxxx:\u000E:xxx".getBytes( UTF_8 );
-        Channel channel = new Channel( stream, 1 );
-        EventConsumerThread thread = new EventConsumerThread( "t", channel,
-            new MockEventHandler<Event>(), COUNTDOWN_CLOSEABLE, new MockForkNodeArguments() );
-
-        Memento memento = thread.new Memento();
-        memento.bb.position( 0 ).limit( 0 );
-        memento.setCharset( UTF_8 );
-        thread.readEventType( eventTypes, memento );
-    }
-
-    @Test
-    public void shouldMapSegmentToEventType()
-    {
-        Map<Segment, RunMode> map = mapRunModes();
-
-        assertThat( map )
-            .hasSize( 2 );
-
-        byte[] stream = "normal-run".getBytes( US_ASCII );
-        Segment segment = new Segment( stream, 0, stream.length );
-        assertThat( map.get( segment ) )
-            .isEqualTo( NORMAL_RUN );
-
-        stream = "rerun-test-after-failure".getBytes( US_ASCII );
-        segment = new Segment( stream, 0, stream.length );
-        assertThat( map.get( segment ) )
-            .isEqualTo( RERUN_TEST_AFTER_FAILURE );
-    }
-
-    @Test
-    public void shouldReadEmptyString() throws Exception
-    {
-        byte[] stream = "\u0000\u0000\u0000\u0000::".getBytes( UTF_8 );
-        Channel channel = new Channel( stream, 1 );
-        EventConsumerThread thread = new EventConsumerThread( "t", channel,
-            new MockEventHandler<Event>(), COUNTDOWN_CLOSEABLE, new MockForkNodeArguments() );
-
-        Memento memento = thread.new Memento();
-        memento.bb.position( 0 ).limit( 0 );
-        memento.setCharset( UTF_8 );
-
-        assertThat( thread.readString( memento ) )
-            .isEmpty();
-    }
-
-    @Test
-    public void shouldReadNullString() throws Exception
-    {
-        byte[] stream = "\u0000\u0000\u0000\u0001:\u0000:".getBytes( UTF_8 );
-        Channel channel = new Channel( stream, 1 );
-        EventConsumerThread thread = new EventConsumerThread( "t", channel,
-            new MockEventHandler<Event>(), COUNTDOWN_CLOSEABLE, new MockForkNodeArguments() );
-
-        Memento memento = thread.new Memento();
-        memento.bb.position( 0 ).limit( 0 );
-        memento.setCharset( UTF_8 );
-
-        assertThat( thread.readString( memento ) )
-            .isNull();
-    }
-
-    @Test
-    public void shouldReadSingleCharString() throws Exception
-    {
-        byte[] stream = "\u0000\u0000\u0000\u0001:A:".getBytes( UTF_8 );
-        Channel channel = new Channel( stream, 1 );
-        EventConsumerThread thread = new EventConsumerThread( "t", channel,
-            new MockEventHandler<Event>(), COUNTDOWN_CLOSEABLE, new MockForkNodeArguments() );
-
-        Memento memento = thread.new Memento();
-        memento.bb.position( 0 ).limit( 0 );
-        memento.setCharset( UTF_8 );
-
-        assertThat( thread.readString( memento ) )
-            .isEqualTo( "A" );
-    }
-
-    @Test
-    public void shouldReadThreeCharactersString() throws Exception
-    {
-        byte[] stream = "\u0000\u0000\u0000\u0003:ABC:".getBytes( UTF_8 );
-        Channel channel = new Channel( stream, 1 );
-        EventConsumerThread thread = new EventConsumerThread( "t", channel,
-            new MockEventHandler<Event>(), COUNTDOWN_CLOSEABLE, new MockForkNodeArguments() );
-
-        Memento memento = thread.new Memento();
-        memento.bb.position( 0 ).limit( 0 );
-        memento.setCharset( UTF_8 );
-
-        assertThat( thread.readString( memento ) )
-            .isEqualTo( "ABC" );
-    }
-
-    @Test
-    public void shouldReadDefaultCharset() throws Exception
-    {
-        byte[] stream = "\u0005:UTF-8:".getBytes( US_ASCII );
-        Channel channel = new Channel( stream, 1 );
-        EventConsumerThread thread = new EventConsumerThread( "t", channel,
-            new MockEventHandler<Event>(), COUNTDOWN_CLOSEABLE, new MockForkNodeArguments() );
-
-        Memento memento = thread.new Memento();
-        memento.bb.position( 0 ).limit( 0 );
-        memento.setCharset( UTF_8 );
-
-        assertThat( thread.readCharset( memento ) )
-            .isNotNull()
-            .isEqualTo( UTF_8 );
-    }
-
-    @Test
-    public void shouldReadNonDefaultCharset() throws Exception
-    {
-        byte[] stream = ( (char) 10 + ":ISO_8859_1:" ).getBytes( US_ASCII );
-        Channel channel = new Channel( stream, 1 );
-        EventConsumerThread thread = new EventConsumerThread( "t", channel,
-            new MockEventHandler<Event>(), COUNTDOWN_CLOSEABLE, new MockForkNodeArguments() );
-
-        Memento memento = thread.new Memento();
-        memento.bb.position( 0 ).limit( 0 );
-        memento.setCharset( UTF_8 );
-
-        assertThat( thread.readCharset( memento ) )
-            .isNotNull()
-            .isEqualTo( ISO_8859_1 );
-    }
-
-    @Test
-    public void shouldSetNonDefaultCharset()
-    {
-        byte[] stream = {};
-        Channel channel = new Channel( stream, 1 );
-        EventConsumerThread thread = new EventConsumerThread( "t", channel,
-            new MockEventHandler<Event>(), COUNTDOWN_CLOSEABLE, new MockForkNodeArguments() );
-        Memento memento = thread.new Memento();
-
-        memento.setCharset( ISO_8859_1 );
-        assertThat( memento.getDecoder().charset() ).isEqualTo( ISO_8859_1 );
-
-        memento.setCharset( UTF_8 );
-        assertThat( memento.getDecoder().charset() ).isEqualTo( UTF_8 );
-
-        memento.reset();
-        assertThat( memento.getDecoder() ).isNotNull();
-        assertThat( memento.getDecoder().charset() ).isEqualTo( UTF_8 );
-    }
-
-    @Test( expected = EventConsumerThread.MalformedFrameException.class )
-    public void malformedCharset() throws Exception
-    {
-        byte[] stream = ( (char) 8 + ":ISO_8859:" ).getBytes( US_ASCII );
-        Channel channel = new Channel( stream, 1 );
-        EventConsumerThread thread = new EventConsumerThread( "t", channel,
-            new MockEventHandler<Event>(), COUNTDOWN_CLOSEABLE, new MockForkNodeArguments() );
-
-        Memento memento = thread.new Memento();
-        memento.bb.position( 0 ).limit( 0 );
-        memento.setCharset( UTF_8 );
-
-        thread.readCharset( memento );
-    }
-
-    @Test
-    public void shouldMapEventTypeToSegmentType()
-    {
-        SegmentType[] segmentTypes = nextSegmentType( BOOTERCODE_BYE );
-        assertThat( segmentTypes )
-            .hasSize( 1 )
-            .containsOnly( END_OF_FRAME );
-
-        segmentTypes = nextSegmentType( BOOTERCODE_STOP_ON_NEXT_TEST );
-        assertThat( segmentTypes )
-            .hasSize( 1 )
-            .containsOnly( END_OF_FRAME );
-
-        segmentTypes = nextSegmentType( BOOTERCODE_NEXT_TEST );
-        assertThat( segmentTypes )
-            .hasSize( 1 )
-            .containsOnly( END_OF_FRAME );
-
-        segmentTypes = nextSegmentType( BOOTERCODE_CONSOLE_ERROR );
-        assertThat( segmentTypes )
-            .hasSize( 5 )
-            .satisfies( new InOrder( STRING_ENCODING, DATA_STRING, DATA_STRING, DATA_STRING, END_OF_FRAME ) );
-
-        segmentTypes = nextSegmentType( ForkedProcessEventType.BOOTERCODE_JVM_EXIT_ERROR );
-        assertThat( segmentTypes )
-            .hasSize( 5 )
-            .satisfies( new InOrder( STRING_ENCODING, DATA_STRING, DATA_STRING, DATA_STRING, END_OF_FRAME ) );
-
-        segmentTypes = nextSegmentType( BOOTERCODE_CONSOLE_INFO );
-        assertThat( segmentTypes )
-            .hasSize( 3 )
-            .satisfies( new InOrder( STRING_ENCODING, DATA_STRING, END_OF_FRAME ) );
-
-        segmentTypes = nextSegmentType( ForkedProcessEventType.BOOTERCODE_CONSOLE_DEBUG );
-        assertThat( segmentTypes )
-            .hasSize( 3 )
-            .satisfies( new InOrder( STRING_ENCODING, DATA_STRING, END_OF_FRAME ) );
-
-        segmentTypes = nextSegmentType( ForkedProcessEventType.BOOTERCODE_CONSOLE_WARNING );
-        assertThat( segmentTypes )
-            .hasSize( 3 )
-            .satisfies( new InOrder( STRING_ENCODING, DATA_STRING, END_OF_FRAME ) );
-
-        segmentTypes = nextSegmentType( BOOTERCODE_STDOUT );
-        assertThat( segmentTypes )
-            .hasSize( 4 )
-            .satisfies( new InOrder( RUN_MODE, STRING_ENCODING, DATA_STRING, END_OF_FRAME ) );
-
-        segmentTypes = nextSegmentType( ForkedProcessEventType.BOOTERCODE_STDOUT_NEW_LINE );
-        assertThat( segmentTypes )
-            .hasSize( 4 )
-            .satisfies( new InOrder( RUN_MODE, STRING_ENCODING, DATA_STRING, END_OF_FRAME ) );
-
-        segmentTypes = nextSegmentType( ForkedProcessEventType.BOOTERCODE_STDERR );
-        assertThat( segmentTypes )
-            .hasSize( 4 )
-            .satisfies( new InOrder( RUN_MODE, STRING_ENCODING, DATA_STRING, END_OF_FRAME ) );
-
-        segmentTypes = nextSegmentType( ForkedProcessEventType.BOOTERCODE_STDERR_NEW_LINE );
-        assertThat( segmentTypes )
-            .hasSize( 4 )
-            .satisfies( new InOrder( RUN_MODE, STRING_ENCODING, DATA_STRING, END_OF_FRAME ) );
-
-        segmentTypes = nextSegmentType( BOOTERCODE_SYSPROPS );
-        assertThat( segmentTypes )
-            .hasSize( 5 )
-            .satisfies( new InOrder( RUN_MODE, STRING_ENCODING, DATA_STRING, DATA_STRING, END_OF_FRAME ) );
-
-        segmentTypes = nextSegmentType( BOOTERCODE_TESTSET_STARTING );
-        assertThat( segmentTypes )
-            .hasSize( 13 )
-            .satisfies( new InOrder( RUN_MODE, STRING_ENCODING, DATA_STRING, DATA_STRING, DATA_STRING, DATA_STRING,
-                DATA_STRING, DATA_STRING, DATA_INT, DATA_STRING, DATA_STRING, DATA_STRING, END_OF_FRAME ) );
-
-        segmentTypes = nextSegmentType( ForkedProcessEventType.BOOTERCODE_TESTSET_COMPLETED );
-        assertThat( segmentTypes )
-            .hasSize( 13 )
-            .satisfies( new InOrder( RUN_MODE, STRING_ENCODING, DATA_STRING, DATA_STRING, DATA_STRING, DATA_STRING,
-                DATA_STRING, DATA_STRING, DATA_INT, DATA_STRING, DATA_STRING, DATA_STRING, END_OF_FRAME ) );
-
-        segmentTypes = nextSegmentType( ForkedProcessEventType.BOOTERCODE_TEST_STARTING );
-        assertThat( segmentTypes )
-            .hasSize( 13 )
-            .satisfies( new InOrder( RUN_MODE, STRING_ENCODING, DATA_STRING, DATA_STRING, DATA_STRING, DATA_STRING,
-                DATA_STRING, DATA_STRING, DATA_INT, DATA_STRING, DATA_STRING, DATA_STRING, END_OF_FRAME ) );
-
-        segmentTypes = nextSegmentType( BOOTERCODE_TEST_SUCCEEDED );
-        assertThat( segmentTypes )
-            .hasSize( 13 )
-            .satisfies( new InOrder( RUN_MODE, STRING_ENCODING, DATA_STRING, DATA_STRING, DATA_STRING, DATA_STRING,
-                DATA_STRING, DATA_STRING, DATA_INT, DATA_STRING, DATA_STRING, DATA_STRING, END_OF_FRAME ) );
-
-        segmentTypes = nextSegmentType( BOOTERCODE_TEST_FAILED );
-        assertThat( segmentTypes )
-            .hasSize( 13 )
-            .satisfies( new InOrder( RUN_MODE, STRING_ENCODING, DATA_STRING, DATA_STRING, DATA_STRING, DATA_STRING,
-                DATA_STRING, DATA_STRING, DATA_INT, DATA_STRING, DATA_STRING, DATA_STRING, END_OF_FRAME ) );
-
-        segmentTypes = nextSegmentType( ForkedProcessEventType.BOOTERCODE_TEST_SKIPPED );
-        assertThat( segmentTypes )
-            .hasSize( 13 )
-            .satisfies( new InOrder( RUN_MODE, STRING_ENCODING, DATA_STRING, DATA_STRING, DATA_STRING, DATA_STRING,
-                DATA_STRING, DATA_STRING, DATA_INT, DATA_STRING, DATA_STRING, DATA_STRING, END_OF_FRAME ) );
-
-        segmentTypes = nextSegmentType( ForkedProcessEventType.BOOTERCODE_TEST_ERROR );
-        assertThat( segmentTypes )
-            .hasSize( 13 )
-            .satisfies( new InOrder( RUN_MODE, STRING_ENCODING, DATA_STRING, DATA_STRING, DATA_STRING, DATA_STRING,
-                DATA_STRING, DATA_STRING, DATA_INT, DATA_STRING, DATA_STRING, DATA_STRING, END_OF_FRAME ) );
-
-        segmentTypes = nextSegmentType( ForkedProcessEventType.BOOTERCODE_TEST_ASSUMPTIONFAILURE );
-        assertThat( segmentTypes )
-            .hasSize( 13 )
-            .satisfies( new InOrder( RUN_MODE, STRING_ENCODING, DATA_STRING, DATA_STRING, DATA_STRING, DATA_STRING,
-                DATA_STRING, DATA_STRING, DATA_INT, DATA_STRING, DATA_STRING, DATA_STRING, END_OF_FRAME ) );
-    }
-
-    @Test
-    public void shouldCreateEvent()
-    {
-        Event event = toEvent( BOOTERCODE_BYE, NORMAL_RUN, emptyList() );
-        assertThat( event )
-            .isInstanceOf( ControlByeEvent.class );
-
-        event = toEvent( BOOTERCODE_STOP_ON_NEXT_TEST, NORMAL_RUN, emptyList() );
-        assertThat( event )
-            .isInstanceOf( ControlStopOnNextTestEvent.class );
-
-        event = toEvent( BOOTERCODE_NEXT_TEST, NORMAL_RUN, emptyList() );
-        assertThat( event )
-            .isInstanceOf( ControlNextTestEvent.class );
-
-        List data = asList( "1", "2", "3" );
-        event = toEvent( BOOTERCODE_CONSOLE_ERROR, NORMAL_RUN, data );
-        assertThat( event )
-            .isInstanceOf( ConsoleErrorEvent.class );
-        ConsoleErrorEvent consoleErrorEvent = (ConsoleErrorEvent) event;
-        assertThat( consoleErrorEvent.getStackTraceWriter().getThrowable().getLocalizedMessage() )
-            .isEqualTo( "1" );
-        assertThat( consoleErrorEvent.getStackTraceWriter().smartTrimmedStackTrace() )
-            .isEqualTo( "2" );
-        assertThat( consoleErrorEvent.getStackTraceWriter().writeTraceToString() )
-            .isEqualTo( "3" );
-
-        data = asList( null, null, null );
-        event = toEvent( BOOTERCODE_CONSOLE_ERROR, NORMAL_RUN, data );
-        assertThat( event )
-            .isInstanceOf( ConsoleErrorEvent.class );
-        consoleErrorEvent = (ConsoleErrorEvent) event;
-        assertThat( consoleErrorEvent.getStackTraceWriter() )
-            .isNull();
-
-        data = asList( "1", "2", "3" );
-        event = toEvent( BOOTERCODE_JVM_EXIT_ERROR, NORMAL_RUN, data );
-        assertThat( event )
-            .isInstanceOf( JvmExitErrorEvent.class );
-        JvmExitErrorEvent jvmExitErrorEvent = (JvmExitErrorEvent) event;
-        assertThat( jvmExitErrorEvent.getStackTraceWriter().getThrowable().getLocalizedMessage() )
-            .isEqualTo( "1" );
-        assertThat( jvmExitErrorEvent.getStackTraceWriter().smartTrimmedStackTrace() )
-            .isEqualTo( "2" );
-        assertThat( jvmExitErrorEvent.getStackTraceWriter().writeTraceToString() )
-            .isEqualTo( "3" );
-
-        data = asList( null, null, null );
-        event = toEvent( BOOTERCODE_JVM_EXIT_ERROR, NORMAL_RUN, data );
-        assertThat( event )
-            .isInstanceOf( JvmExitErrorEvent.class );
-        jvmExitErrorEvent = (JvmExitErrorEvent) event;
-        assertThat( jvmExitErrorEvent.getStackTraceWriter() )
-            .isNull();
-
-        data = singletonList( "m" );
-        event = toEvent( BOOTERCODE_CONSOLE_INFO, NORMAL_RUN, data );
-        assertThat( event ).isInstanceOf( ConsoleInfoEvent.class );
-        assertThat( ( (ConsoleInfoEvent) event ).getMessage() ).isEqualTo( "m" );
-
-        data = singletonList( "" );
-        event = toEvent( BOOTERCODE_CONSOLE_WARNING, NORMAL_RUN, data );
-        assertThat( event ).isInstanceOf( ConsoleWarningEvent.class );
-        assertThat( ( (ConsoleWarningEvent) event ).getMessage() ).isEmpty();
-
-        data = singletonList( null );
-        event = toEvent( BOOTERCODE_CONSOLE_DEBUG, NORMAL_RUN, data );
-        assertThat( event ).isInstanceOf( ConsoleDebugEvent.class );
-        assertThat( ( (ConsoleDebugEvent) event ).getMessage() ).isNull();
-
-        data = singletonList( "m" );
-        event = toEvent( BOOTERCODE_STDOUT, NORMAL_RUN, data );
-        assertThat( event ).isInstanceOf( StandardStreamOutEvent.class );
-        assertThat( ( (StandardStreamOutEvent) event ).getMessage() ).isEqualTo( "m" );
-        assertThat( ( (StandardStreamOutEvent) event ).getRunMode() ).isEqualTo( NORMAL_RUN );
-
-        data = singletonList( null );
-        event = toEvent( BOOTERCODE_STDOUT_NEW_LINE, RERUN_TEST_AFTER_FAILURE, data );
-        assertThat( event ).isInstanceOf( StandardStreamOutWithNewLineEvent.class );
-        assertThat( ( (StandardStreamOutWithNewLineEvent) event ).getMessage() ).isNull();
-        assertThat( ( (StandardStreamOutWithNewLineEvent) event ).getRunMode() ).isEqualTo( RERUN_TEST_AFTER_FAILURE );
-
-        data = singletonList( null );
-        event = toEvent( BOOTERCODE_STDERR, RERUN_TEST_AFTER_FAILURE, data );
-        assertThat( event ).isInstanceOf( StandardStreamErrEvent.class );
-        assertThat( ( (StandardStreamErrEvent) event ).getMessage() ).isNull();
-        assertThat( ( (StandardStreamErrEvent) event ).getRunMode() ).isEqualTo( RERUN_TEST_AFTER_FAILURE );
-
-        data = singletonList( "abc" );
-        event = toEvent( BOOTERCODE_STDERR_NEW_LINE, NORMAL_RUN, data );
-        assertThat( event ).isInstanceOf( StandardStreamErrWithNewLineEvent.class );
-        assertThat( ( (StandardStreamErrWithNewLineEvent) event ).getMessage() ).isEqualTo( "abc" );
-        assertThat( ( (StandardStreamErrWithNewLineEvent) event ).getRunMode() ).isEqualTo( NORMAL_RUN );
-
-        data = asList( "key", "value" );
-        event = toEvent( BOOTERCODE_SYSPROPS, NORMAL_RUN, data );
-        assertThat( event ).isInstanceOf( SystemPropertyEvent.class );
-        assertThat( ( (SystemPropertyEvent) event ).getKey() ).isEqualTo( "key" );
-        assertThat( ( (SystemPropertyEvent) event ).getValue() ).isEqualTo( "value" );
-        assertThat( ( (SystemPropertyEvent) event ).getRunMode() ).isEqualTo( NORMAL_RUN );
-
-        data = asList( "source", "sourceText", "name", "nameText", "group", "message", 5,
-            "traceMessage", "smartTrimmedStackTrace", "stackTrace" );
-        event = toEvent( BOOTERCODE_TESTSET_STARTING, NORMAL_RUN, data );
-        assertThat( event ).isInstanceOf( TestsetStartingEvent.class );
-        assertThat( ( (TestsetStartingEvent) event ).getRunMode() ).isEqualTo( NORMAL_RUN );
-        assertThat( ( (TestsetStartingEvent) event ).getReportEntry().getSourceName() ).isEqualTo( "source" );
-        assertThat( ( (TestsetStartingEvent) event ).getReportEntry().getSourceText() ).isEqualTo( "sourceText" );
-        assertThat( ( (TestsetStartingEvent) event ).getReportEntry().getName() ).isEqualTo( "name" );
-        assertThat( ( (TestsetStartingEvent) event ).getReportEntry().getNameText() ).isEqualTo( "nameText" );
-        assertThat( ( (TestsetStartingEvent) event ).getReportEntry().getGroup() ).isEqualTo( "group" );
-        assertThat( ( (TestsetStartingEvent) event ).getReportEntry().getMessage() ).isEqualTo( "message" );
-        assertThat( ( (TestsetStartingEvent) event ).getReportEntry().getElapsed() ).isEqualTo( 5 );
-        assertThat( ( (TestsetStartingEvent) event )
-            .getReportEntry().getStackTraceWriter().getThrowable().getLocalizedMessage() )
-            .isEqualTo( "traceMessage" );
-        assertThat( ( (TestsetStartingEvent) event ).getReportEntry().getStackTraceWriter().smartTrimmedStackTrace() )
-            .isEqualTo( "smartTrimmedStackTrace" );
-        assertThat( ( (TestsetStartingEvent) event ).getReportEntry().getStackTraceWriter().writeTraceToString() )
-            .isEqualTo( "stackTrace" );
-
-        data = asList( "source", "sourceText", "name", "nameText", "group", null, 5,
-            "traceMessage", "smartTrimmedStackTrace", "stackTrace" );
-        event = toEvent( BOOTERCODE_TESTSET_COMPLETED, NORMAL_RUN, data );
-        assertThat( event ).isInstanceOf( TestsetCompletedEvent.class );
-        assertThat( ( (TestsetCompletedEvent) event ).getRunMode() ).isEqualTo( NORMAL_RUN );
-        assertThat( ( (TestsetCompletedEvent) event ).getReportEntry().getSourceName() ).isEqualTo( "source" );
-        assertThat( ( (TestsetCompletedEvent) event ).getReportEntry().getSourceText() ).isEqualTo( "sourceText" );
-        assertThat( ( (TestsetCompletedEvent) event ).getReportEntry().getName() ).isEqualTo( "name" );
-        assertThat( ( (TestsetCompletedEvent) event ).getReportEntry().getNameText() ).isEqualTo( "nameText" );
-        assertThat( ( (TestsetCompletedEvent) event ).getReportEntry().getGroup() ).isEqualTo( "group" );
-        assertThat( ( (TestsetCompletedEvent) event ).getReportEntry().getMessage() ).isNull();
-        assertThat( ( (TestsetCompletedEvent) event ).getReportEntry().getElapsed() ).isEqualTo( 5 );
-        assertThat( ( (TestsetCompletedEvent) event )
-            .getReportEntry().getStackTraceWriter().getThrowable().getLocalizedMessage() )
-            .isEqualTo( "traceMessage" );
-        assertThat( ( (TestsetCompletedEvent) event ).getReportEntry().getStackTraceWriter().smartTrimmedStackTrace() )
-            .isEqualTo( "smartTrimmedStackTrace" );
-        assertThat( ( (TestsetCompletedEvent) event ).getReportEntry().getStackTraceWriter().writeTraceToString() )
-            .isEqualTo( "stackTrace" );
-
-        data = asList( "source", "sourceText", "name", "nameText", "group", "message", 5,
-            null, "smartTrimmedStackTrace", "stackTrace" );
-        event = toEvent( BOOTERCODE_TEST_STARTING, NORMAL_RUN, data );
-        assertThat( event ).isInstanceOf( TestStartingEvent.class );
-        assertThat( ( (TestStartingEvent) event ).getRunMode() ).isEqualTo( NORMAL_RUN );
-        assertThat( ( (TestStartingEvent) event ).getReportEntry().getSourceName() ).isEqualTo( "source" );
-        assertThat( ( (TestStartingEvent) event ).getReportEntry().getSourceText() ).isEqualTo( "sourceText" );
-        assertThat( ( (TestStartingEvent) event ).getReportEntry().getName() ).isEqualTo( "name" );
-        assertThat( ( (TestStartingEvent) event ).getReportEntry().getNameText() ).isEqualTo( "nameText" );
-        assertThat( ( (TestStartingEvent) event ).getReportEntry().getGroup() ).isEqualTo( "group" );
-        assertThat( ( (TestStartingEvent) event ).getReportEntry().getMessage() ).isEqualTo( "message" );
-        assertThat( ( (TestStartingEvent) event ).getReportEntry().getElapsed() ).isEqualTo( 5 );
-        assertThat( ( (TestStartingEvent) event )
-            .getReportEntry().getStackTraceWriter().getThrowable().getLocalizedMessage() )
-            .isNull();
-        assertThat( ( (TestStartingEvent) event ).getReportEntry().getStackTraceWriter().smartTrimmedStackTrace() )
-            .isEqualTo( "smartTrimmedStackTrace" );
-        assertThat( ( (TestStartingEvent) event ).getReportEntry().getStackTraceWriter().writeTraceToString() )
-            .isEqualTo( "stackTrace" );
-
-        data = asList( "source", "sourceText", "name", "nameText", "group", "message", 5, null, null, null );
-        event = toEvent( BOOTERCODE_TEST_SUCCEEDED, NORMAL_RUN, data );
-        assertThat( event ).isInstanceOf( TestSucceededEvent.class );
-        assertThat( ( (TestSucceededEvent) event ).getRunMode() ).isEqualTo( NORMAL_RUN );
-        assertThat( ( (TestSucceededEvent) event ).getReportEntry().getSourceName() ).isEqualTo( "source" );
-        assertThat( ( (TestSucceededEvent) event ).getReportEntry().getSourceText() ).isEqualTo( "sourceText" );
-        assertThat( ( (TestSucceededEvent) event ).getReportEntry().getName() ).isEqualTo( "name" );
-        assertThat( ( (TestSucceededEvent) event ).getReportEntry().getNameText() ).isEqualTo( "nameText" );
-        assertThat( ( (TestSucceededEvent) event ).getReportEntry().getGroup() ).isEqualTo( "group" );
-        assertThat( ( (TestSucceededEvent) event ).getReportEntry().getMessage() ).isEqualTo( "message" );
-        assertThat( ( (TestSucceededEvent) event ).getReportEntry().getElapsed() ).isEqualTo( 5 );
-        assertThat( ( (TestSucceededEvent) event ).getReportEntry().getStackTraceWriter() ).isNull();
-
-        data = asList( "source", null, "name", null, "group", null, 5,
-            "traceMessage", "smartTrimmedStackTrace", "stackTrace" );
-        event = toEvent( BOOTERCODE_TEST_FAILED, RERUN_TEST_AFTER_FAILURE, data );
-        assertThat( event ).isInstanceOf( TestFailedEvent.class );
-        assertThat( ( (TestFailedEvent) event ).getRunMode() ).isEqualTo( RERUN_TEST_AFTER_FAILURE );
-        assertThat( ( (TestFailedEvent) event ).getReportEntry().getSourceName() ).isEqualTo( "source" );
-        assertThat( ( (TestFailedEvent) event ).getReportEntry().getSourceText() ).isNull();
-        assertThat( ( (TestFailedEvent) event ).getReportEntry().getName() ).isEqualTo( "name" );
-        assertThat( ( (TestFailedEvent) event ).getReportEntry().getNameText() ).isNull();
-        assertThat( ( (TestFailedEvent) event ).getReportEntry().getGroup() ).isEqualTo( "group" );
-        assertThat( ( (TestFailedEvent) event ).getReportEntry().getMessage() ).isNull();
-        assertThat( ( (TestFailedEvent) event ).getReportEntry().getElapsed() ).isEqualTo( 5 );
-        assertThat( ( (TestFailedEvent) event )
-            .getReportEntry().getStackTraceWriter().getThrowable().getLocalizedMessage() )
-            .isEqualTo( "traceMessage" );
-        assertThat( ( (TestFailedEvent) event ).getReportEntry().getStackTraceWriter().smartTrimmedStackTrace() )
-            .isEqualTo( "smartTrimmedStackTrace" );
-        assertThat( ( (TestFailedEvent) event ).getReportEntry().getStackTraceWriter().writeTraceToString() )
-            .isEqualTo( "stackTrace" );
-
-        data = asList( "source", null, "name", null, null, null, 5, null, null, "stackTrace" );
-        event = toEvent( BOOTERCODE_TEST_SKIPPED, NORMAL_RUN, data );
-        assertThat( event ).isInstanceOf( TestSkippedEvent.class );
-        assertThat( ( (TestSkippedEvent) event ).getRunMode() ).isEqualTo( NORMAL_RUN );
-        assertThat( ( (TestSkippedEvent) event ).getReportEntry().getSourceName() ).isEqualTo( "source" );
-        assertThat( ( (TestSkippedEvent) event ).getReportEntry().getSourceText() ).isNull();
-        assertThat( ( (TestSkippedEvent) event ).getReportEntry().getName() ).isEqualTo( "name" );
-        assertThat( ( (TestSkippedEvent) event ).getReportEntry().getNameText() ).isNull();
-        assertThat( ( (TestSkippedEvent) event ).getReportEntry().getGroup() ).isNull();
-        assertThat( ( (TestSkippedEvent) event ).getReportEntry().getMessage() ).isNull();
-        assertThat( ( (TestSkippedEvent) event ).getReportEntry().getElapsed() ).isEqualTo( 5 );
-        assertThat( ( (TestSkippedEvent) event )
-            .getReportEntry().getStackTraceWriter().getThrowable().getLocalizedMessage() )
-            .isNull();
-        assertThat( ( (TestSkippedEvent) event )
-            .getReportEntry().getStackTraceWriter().smartTrimmedStackTrace() )
-            .isNull();
-        assertThat( ( (TestSkippedEvent) event )
-            .getReportEntry().getStackTraceWriter().writeTraceToString() )
-            .isEqualTo( "stackTrace" );
-
-        data = asList( "source", null, "name", "nameText", null, null, 0, null, null, "stackTrace" );
-        event = toEvent( BOOTERCODE_TEST_ERROR, NORMAL_RUN, data );
-        assertThat( event ).isInstanceOf( TestErrorEvent.class );
-        assertThat( ( (TestErrorEvent) event ).getRunMode() ).isEqualTo( NORMAL_RUN );
-        assertThat( ( (TestErrorEvent) event ).getReportEntry().getSourceName() ).isEqualTo( "source" );
-        assertThat( ( (TestErrorEvent) event ).getReportEntry().getSourceText() ).isNull();
-        assertThat( ( (TestErrorEvent) event ).getReportEntry().getName() ).isEqualTo( "name" );
-        assertThat( ( (TestErrorEvent) event ).getReportEntry().getNameText() ).isEqualTo( "nameText" );
-        assertThat( ( (TestErrorEvent) event ).getReportEntry().getGroup() ).isNull();
-        assertThat( ( (TestErrorEvent) event ).getReportEntry().getMessage() ).isNull();
-        assertThat( ( (TestErrorEvent) event ).getReportEntry().getElapsed() ).isZero();
-        assertThat( ( (TestErrorEvent) event )
-            .getReportEntry().getStackTraceWriter().getThrowable().getLocalizedMessage() )
-            .isNull();
-        assertThat( ( (TestErrorEvent) event )
-            .getReportEntry().getStackTraceWriter().smartTrimmedStackTrace() )
-            .isNull();
-        assertThat( ( (TestErrorEvent) event )
-            .getReportEntry().getStackTraceWriter().writeTraceToString() )
-            .isEqualTo( "stackTrace" );
-
-        data = asList( "source", null, "name", null, "group", null, 5, null, null, "stackTrace" );
-        event = toEvent( BOOTERCODE_TEST_ASSUMPTIONFAILURE, NORMAL_RUN, data );
-        assertThat( event ).isInstanceOf( TestAssumptionFailureEvent.class );
-        assertThat( ( (TestAssumptionFailureEvent) event ).getRunMode() ).isEqualTo( NORMAL_RUN );
-        assertThat( ( (TestAssumptionFailureEvent) event ).getReportEntry().getSourceName() ).isEqualTo( "source" );
-        assertThat( ( (TestAssumptionFailureEvent) event ).getReportEntry().getSourceText() ).isNull();
-        assertThat( ( (TestAssumptionFailureEvent) event ).getReportEntry().getName() ).isEqualTo( "name" );
-        assertThat( ( (TestAssumptionFailureEvent) event ).getReportEntry().getNameText() ).isNull();
-        assertThat( ( (TestAssumptionFailureEvent) event ).getReportEntry().getGroup() ).isEqualTo( "group" );
-        assertThat( ( (TestAssumptionFailureEvent) event ).getReportEntry().getMessage() ).isNull();
-        assertThat( ( (TestAssumptionFailureEvent) event ).getReportEntry().getElapsed() ).isEqualTo( 5 );
-        assertThat( ( (TestAssumptionFailureEvent) event )
-            .getReportEntry().getStackTraceWriter().getThrowable().getLocalizedMessage() )
-            .isNull();
-        assertThat( ( (TestAssumptionFailureEvent) event )
-            .getReportEntry().getStackTraceWriter().smartTrimmedStackTrace() )
-            .isNull();
-        assertThat( ( (TestAssumptionFailureEvent) event )
-            .getReportEntry().getStackTraceWriter().writeTraceToString() )
-            .isEqualTo( "stackTrace" );
+            .isLessThanOrEqualTo( 3_650L );
     }
 
     private static class Channel implements ReadableByteChannel
@@ -1117,13 +145,13 @@ public class EventConsumerThreadTest
         @Override
         public int read( ByteBuffer dst )
         {
-            if ( i == bytes.length )
+            if ( remaining() == 0 )
             {
                 return -1;
             }
             else if ( dst.hasRemaining() )
             {
-                int length = min( min( chunkSize, bytes.length - i ), dst.remaining() ) ;
+                int length = min( min( chunkSize, remaining() ), dst.remaining() ) ;
                 dst.put( bytes, i, length );
                 i += length;
                 return length;
@@ -1134,6 +162,11 @@ public class EventConsumerThreadTest
             }
         }
 
+        protected final int remaining()
+        {
+            return bytes.length - i;
+        }
+
         @Override
         public boolean isOpen()
         {
@@ -1154,14 +187,6 @@ public class EventConsumerThreadTest
         }
     }
 
-    private static class MockEventHandler<T> implements EventHandler<T>
-    {
-        @Override
-        public void handleEvent( @Nonnull T event )
-        {
-        }
-    }
-
     private static class MockForkNodeArguments implements ForkNodeArguments
     {
         @Nonnull
@@ -1202,36 +227,17 @@ public class EventConsumerThreadTest
         {
             return null;
         }
-    }
-
-    private static class InOrder extends Condition<Object[]>
-    {
-        private final SegmentType[] expected;
 
-        InOrder( SegmentType... expected )
+        @Override
+        public File getEventStreamBinaryFile()
         {
-            this.expected = expected;
+            return null;
         }
 
         @Override
-        public boolean matches( Object[] values )
+        public File getCommandStreamBinaryFile()
         {
-            if ( values == null && expected == null )
-            {
-                return true;
-            }
-            else if ( values != null && expected != null && values.length == expected.length )
-            {
-                boolean matches = true;
-                for ( int i = 0; i < values.length; i++ )
-                {
-
-                    assertThat( values[i] ).isInstanceOf( SegmentType.class );
-                    matches &= values[i] == expected[i];
-                }
-                return matches;
-            }
-            return false;
+            return null;
         }
     }
 }
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/ForkedProcessEventNotifierTest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/ForkedProcessEventNotifierTest.java
index 3dbde1f..558c021 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/ForkedProcessEventNotifierTest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/ForkedProcessEventNotifierTest.java
@@ -31,15 +31,15 @@ import org.apache.maven.plugin.surefire.booterclient.output.ForkedProcessStringE
 import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
 import org.apache.maven.plugin.surefire.log.api.ConsoleLoggerUtils;
 import org.apache.maven.surefire.api.event.Event;
+import org.apache.maven.surefire.api.fork.ForkNodeArguments;
 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.ObjectUtils;
 import org.apache.maven.surefire.api.util.internal.WritableBufferedByteChannel;
-import org.apache.maven.surefire.booter.spi.LegacyMasterProcessChannelEncoder;
+import org.apache.maven.surefire.booter.spi.EventChannelEncoder;
 import org.apache.maven.surefire.extensions.EventHandler;
-import org.apache.maven.surefire.extensions.ForkNodeArguments;
 import org.apache.maven.surefire.extensions.util.CountdownCloseable;
 import org.junit.Rule;
 import org.junit.Test;
@@ -106,9 +106,9 @@ public class ForkedProcessEventNotifierTest
         {
             final Stream out = Stream.newStream();
             WritableBufferedByteChannel wChannel = newBufferedChannel( out );
-            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( wChannel );
+            EventChannelEncoder encoder = new EventChannelEncoder( wChannel );
             Map<String, String> props = ObjectUtils.systemProps();
-            encoder.sendSystemProperties( props );
+            encoder.systemProperties( props );
             wChannel.close();
 
             ForkedProcessEventNotifier notifier = new ForkedProcessEventNotifier();
@@ -138,154 +138,10 @@ public class ForkedProcessEventNotifierTest
         }
 
         @Test
-        public void shouldRecognizeEmptyStream4ReportEntry()
-        {
-            ReportEntry reportEntry = EventConsumerThread.newReportEntry( "", "", "", "", "", "", null, "", "", "" );
-            assertThat( reportEntry ).isNotNull();
-            assertThat( reportEntry.getStackTraceWriter() ).isNotNull();
-            assertThat( reportEntry.getStackTraceWriter().smartTrimmedStackTrace() ).isEmpty();
-            assertThat( reportEntry.getStackTraceWriter().writeTraceToString() ).isEmpty();
-            assertThat( reportEntry.getStackTraceWriter().writeTrimmedTraceToString() ).isEmpty();
-            assertThat( reportEntry.getSourceName() ).isEmpty();
-            assertThat( reportEntry.getSourceText() ).isEmpty();
-            assertThat( reportEntry.getName() ).isEmpty();
-            assertThat( reportEntry.getNameText() ).isEmpty();
-            assertThat( reportEntry.getGroup() ).isEmpty();
-            assertThat( reportEntry.getNameWithGroup() ).isEmpty();
-            assertThat( reportEntry.getMessage() ).isEmpty();
-            assertThat( reportEntry.getElapsed() ).isNull();
-        }
-
-        @Test
-        @SuppressWarnings( "checkstyle:magicnumber" )
-        public void testCreatingReportEntry()
-        {
-            final String exceptionMessage = "msg";
-            final String smartStackTrace = "MyTest:86 >> Error";
-            final String stackTrace = "Exception: msg\ntrace line 1\ntrace line 2";
-            final String trimmedStackTrace = "trace line 1\ntrace line 2";
-
-            SafeThrowable safeThrowable = new SafeThrowable( exceptionMessage );
-            StackTraceWriter stackTraceWriter = mock( StackTraceWriter.class );
-            when( stackTraceWriter.getThrowable() ).thenReturn( safeThrowable );
-            when( stackTraceWriter.smartTrimmedStackTrace() ).thenReturn( smartStackTrace );
-            when( stackTraceWriter.writeTrimmedTraceToString() ).thenReturn( trimmedStackTrace );
-            when( stackTraceWriter.writeTraceToString() ).thenReturn( stackTrace );
-
-            ReportEntry reportEntry = mock( ReportEntry.class );
-            when( reportEntry.getElapsed() ).thenReturn( 102 );
-            when( reportEntry.getGroup() ).thenReturn( "this group" );
-            when( reportEntry.getMessage() ).thenReturn( "skipped test" );
-            when( reportEntry.getName() ).thenReturn( "my test" );
-            when( reportEntry.getNameText() ).thenReturn( "my display name" );
-            when( reportEntry.getNameWithGroup() ).thenReturn( "name with group" );
-            when( reportEntry.getSourceName() ).thenReturn( "pkg.MyTest" );
-            when( reportEntry.getSourceText() ).thenReturn( "test class display name" );
-            when( reportEntry.getStackTraceWriter() ).thenReturn( stackTraceWriter );
-
-            ReportEntry decodedReportEntry = EventConsumerThread.newReportEntry( reportEntry.getSourceName(),
-                reportEntry.getSourceText(), reportEntry.getName(), reportEntry.getNameText(), reportEntry.getGroup(),
-                reportEntry.getMessage(), null, null, null, null );
-
-            assertThat( decodedReportEntry ).isNotNull();
-            assertThat( decodedReportEntry.getSourceName() ).isEqualTo( reportEntry.getSourceName() );
-            assertThat( decodedReportEntry.getSourceText() ).isEqualTo( reportEntry.getSourceText() );
-            assertThat( decodedReportEntry.getName() ).isEqualTo( reportEntry.getName() );
-            assertThat( decodedReportEntry.getNameText() ).isEqualTo( reportEntry.getNameText() );
-            assertThat( decodedReportEntry.getGroup() ).isEqualTo( reportEntry.getGroup() );
-            assertThat( decodedReportEntry.getMessage() ).isEqualTo( reportEntry.getMessage() );
-            assertThat( decodedReportEntry.getStackTraceWriter() ).isNull();
-
-            decodedReportEntry = EventConsumerThread.newReportEntry( reportEntry.getSourceName(),
-                reportEntry.getSourceText(), reportEntry.getName(), reportEntry.getNameText(), reportEntry.getGroup(),
-                reportEntry.getMessage(), null, exceptionMessage, smartStackTrace, null );
-
-            assertThat( decodedReportEntry ).isNotNull();
-            assertThat( decodedReportEntry.getSourceName() ).isEqualTo( reportEntry.getSourceName() );
-            assertThat( decodedReportEntry.getSourceText() ).isEqualTo( reportEntry.getSourceText() );
-            assertThat( decodedReportEntry.getName() ).isEqualTo( reportEntry.getName() );
-            assertThat( decodedReportEntry.getNameText() ).isEqualTo( reportEntry.getNameText() );
-            assertThat( decodedReportEntry.getGroup() ).isEqualTo( reportEntry.getGroup() );
-            assertThat( decodedReportEntry.getMessage() ).isEqualTo( reportEntry.getMessage() );
-            assertThat( decodedReportEntry.getElapsed() ).isNull();
-            assertThat( decodedReportEntry.getStackTraceWriter() ).isNotNull();
-            assertThat( decodedReportEntry.getStackTraceWriter().getThrowable().getMessage() )
-                .isEqualTo( exceptionMessage );
-            assertThat( decodedReportEntry.getStackTraceWriter().smartTrimmedStackTrace() )
-                .isEqualTo( smartStackTrace );
-            assertThat( decodedReportEntry.getStackTraceWriter().writeTraceToString() )
-                .isNull();
-
-            decodedReportEntry = EventConsumerThread.newReportEntry( reportEntry.getSourceName(),
-                reportEntry.getSourceText(), reportEntry.getName(), reportEntry.getNameText(), reportEntry.getGroup(),
-                reportEntry.getMessage(), 1003, exceptionMessage, smartStackTrace, null );
-
-            assertThat( decodedReportEntry ).isNotNull();
-            assertThat( decodedReportEntry.getSourceName() ).isEqualTo( reportEntry.getSourceName() );
-            assertThat( decodedReportEntry.getSourceText() ).isEqualTo( reportEntry.getSourceText() );
-            assertThat( decodedReportEntry.getName() ).isEqualTo( reportEntry.getName() );
-            assertThat( decodedReportEntry.getNameText() ).isEqualTo( reportEntry.getNameText() );
-            assertThat( decodedReportEntry.getGroup() ).isEqualTo( reportEntry.getGroup() );
-            assertThat( decodedReportEntry.getMessage() ).isEqualTo( reportEntry.getMessage() );
-            assertThat( decodedReportEntry.getElapsed() ).isEqualTo( 1003 );
-            assertThat( decodedReportEntry.getStackTraceWriter() ).isNotNull();
-            assertThat( decodedReportEntry.getStackTraceWriter().getThrowable().getMessage() )
-                .isEqualTo( exceptionMessage );
-            assertThat( decodedReportEntry.getStackTraceWriter().smartTrimmedStackTrace() )
-                .isEqualTo( smartStackTrace );
-            assertThat( decodedReportEntry.getStackTraceWriter().writeTraceToString() )
-                .isNull();
-
-            decodedReportEntry = EventConsumerThread.newReportEntry( reportEntry.getSourceName(),
-                reportEntry.getSourceText(), reportEntry.getName(), reportEntry.getNameText(), reportEntry.getGroup(),
-                reportEntry.getMessage(), 1003, exceptionMessage, smartStackTrace, stackTrace );
-
-            assertThat( decodedReportEntry ).isNotNull();
-            assertThat( decodedReportEntry.getSourceName() ).isEqualTo( reportEntry.getSourceName() );
-            assertThat( decodedReportEntry.getSourceText() ).isEqualTo( reportEntry.getSourceText() );
-            assertThat( decodedReportEntry.getName() ).isEqualTo( reportEntry.getName() );
-            assertThat( decodedReportEntry.getNameText() ).isEqualTo( reportEntry.getNameText() );
-            assertThat( decodedReportEntry.getGroup() ).isEqualTo( reportEntry.getGroup() );
-            assertThat( decodedReportEntry.getMessage() ).isEqualTo( reportEntry.getMessage() );
-            assertThat( decodedReportEntry.getElapsed() ).isEqualTo( 1003 );
-            assertThat( decodedReportEntry.getStackTraceWriter() ).isNotNull();
-            assertThat( decodedReportEntry.getStackTraceWriter().getThrowable().getMessage() ).isNotNull();
-            assertThat( decodedReportEntry.getStackTraceWriter().getThrowable().getMessage() )
-                    .isEqualTo( exceptionMessage );
-            assertThat( decodedReportEntry.getStackTraceWriter().smartTrimmedStackTrace() )
-                    .isEqualTo( smartStackTrace );
-            assertThat( decodedReportEntry.getStackTraceWriter().writeTraceToString() ).isEqualTo( stackTrace );
-            assertThat( decodedReportEntry.getStackTraceWriter().writeTrimmedTraceToString() ).isEqualTo( stackTrace );
-
-            decodedReportEntry = EventConsumerThread.newReportEntry( reportEntry.getSourceName(),
-                reportEntry.getSourceText(), reportEntry.getName(), reportEntry.getNameText(), reportEntry.getGroup(),
-                reportEntry.getMessage(), 1003, exceptionMessage, smartStackTrace, trimmedStackTrace );
-
-            assertThat( decodedReportEntry ).isNotNull();
-            assertThat( decodedReportEntry.getSourceName() ).isEqualTo( reportEntry.getSourceName() );
-            assertThat( decodedReportEntry.getSourceText() ).isEqualTo( reportEntry.getSourceText() );
-            assertThat( decodedReportEntry.getName() ).isEqualTo( reportEntry.getName() );
-            assertThat( decodedReportEntry.getNameText() ).isEqualTo( reportEntry.getNameText() );
-            assertThat( decodedReportEntry.getGroup() ).isEqualTo( reportEntry.getGroup() );
-            assertThat( decodedReportEntry.getMessage() ).isEqualTo( reportEntry.getMessage() );
-            assertThat( decodedReportEntry.getElapsed() ).isEqualTo( 1003 );
-            assertThat( decodedReportEntry.getStackTraceWriter() ).isNotNull();
-            assertThat( decodedReportEntry.getStackTraceWriter().getThrowable().getMessage() ).isNotNull();
-            assertThat( decodedReportEntry.getStackTraceWriter().getThrowable().getMessage() )
-                    .isEqualTo( exceptionMessage );
-            assertThat( decodedReportEntry.getStackTraceWriter().smartTrimmedStackTrace() )
-                    .isEqualTo( smartStackTrace );
-            assertThat( decodedReportEntry.getStackTraceWriter().writeTraceToString() ).isEqualTo( trimmedStackTrace );
-            assertThat( decodedReportEntry.getStackTraceWriter().writeTrimmedTraceToString() )
-                    .isEqualTo( trimmedStackTrace );
-        }
-
-        @Test
         public void shouldSendByeEvent() throws Exception
         {
             Stream out = Stream.newStream();
-            LegacyMasterProcessChannelEncoder encoder =
-                new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+            EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
             encoder.bye();
             String read = new String( out.toByteArray(), UTF_8 );
 
@@ -325,8 +181,7 @@ public class ForkedProcessEventNotifierTest
         public void shouldSendStopOnNextTestEvent() throws Exception
         {
             Stream out = Stream.newStream();
-            LegacyMasterProcessChannelEncoder encoder =
-                new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+            EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
             encoder.stopOnNextTest();
             String read = new String( out.toByteArray(), UTF_8 );
 
@@ -390,8 +245,7 @@ public class ForkedProcessEventNotifierTest
             when( reportEntry.getStackTraceWriter() ).thenReturn( stackTraceWriter );
 
             final Stream out = Stream.newStream();
-            LegacyMasterProcessChannelEncoder encoder =
-                new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+            EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
             encoder.testFailed( reportEntry, true );
 
             ReadableByteChannel channel = newChannel( new ByteArrayInputStream( out.toByteArray() ) );
@@ -421,8 +275,7 @@ public class ForkedProcessEventNotifierTest
         public void shouldSendNextTestEvent() throws Exception
         {
             final Stream out = Stream.newStream();
-            LegacyMasterProcessChannelEncoder encoder =
-                new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+            EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
             encoder.acquireNextTest();
             String read = new String( out.toByteArray(), UTF_8 );
 
@@ -456,8 +309,7 @@ public class ForkedProcessEventNotifierTest
         public void testConsole() throws Exception
         {
             final Stream out = Stream.newStream();
-            LegacyMasterProcessChannelEncoder encoder =
-                new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+            EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
             encoder.consoleInfoLog( "msg" );
 
             ReadableByteChannel channel = newChannel( new ByteArrayInputStream( out.toByteArray() ) );
@@ -487,8 +339,7 @@ public class ForkedProcessEventNotifierTest
         public void testError() throws Exception
         {
             final Stream out = Stream.newStream();
-            LegacyMasterProcessChannelEncoder encoder =
-                new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+            EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
             encoder.consoleErrorLog( "msg" );
 
             ReadableByteChannel channel = newChannel( new ByteArrayInputStream( out.toByteArray() ) );
@@ -518,8 +369,7 @@ public class ForkedProcessEventNotifierTest
         public void testErrorWithException() throws Exception
         {
             final Stream out = Stream.newStream();
-            LegacyMasterProcessChannelEncoder encoder =
-                new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+            EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
             Throwable throwable = new Throwable( "msg" );
             encoder.consoleErrorLog( throwable );
 
@@ -531,7 +381,7 @@ public class ForkedProcessEventNotifierTest
             notifier.setConsoleErrorListener( listener );
 
             EH eventHandler = new EH();
-            CountdownCloseable countdown = new CountdownCloseable( mock( Closeable.class ), 0 );
+            CountdownCloseable countdown = new CountdownCloseable( mock( Closeable.class ), 1 );
             ConsoleLoggerMock logger = new ConsoleLoggerMock( false, false, false, false );
             ForkNodeArgumentsMock arguments = new ForkNodeArgumentsMock( logger, new File( "" ) );
             try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, arguments ) )
@@ -552,8 +402,7 @@ public class ForkedProcessEventNotifierTest
         {
             final Stream out = Stream.newStream();
 
-            LegacyMasterProcessChannelEncoder encoder =
-                new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+            EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
             StackTraceWriter stackTraceWriter = new DeserializedStacktraceWriter( "1", "2", "3" );
             encoder.consoleErrorLog( stackTraceWriter, false );
 
@@ -585,8 +434,7 @@ public class ForkedProcessEventNotifierTest
         {
             final Stream out = Stream.newStream();
 
-            LegacyMasterProcessChannelEncoder encoder =
-                new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+            EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
             encoder.consoleDebugLog( "msg" );
 
             ReadableByteChannel channel = newChannel( new ByteArrayInputStream( out.toByteArray() ) );
@@ -623,8 +471,7 @@ public class ForkedProcessEventNotifierTest
         {
             final Stream out = Stream.newStream();
 
-            LegacyMasterProcessChannelEncoder encoder =
-                new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+            EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
             encoder.consoleWarningLog( "msg" );
 
             ReadableByteChannel channel = newChannel( new ByteArrayInputStream( out.toByteArray() ) );
@@ -655,7 +502,7 @@ public class ForkedProcessEventNotifierTest
         {
             final Stream out = Stream.newStream();
             WritableBufferedByteChannel wChannel = newBufferedChannel( out );
-            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( wChannel );
+            EventChannelEncoder encoder = new EventChannelEncoder( wChannel );
             encoder.stdOut( "msg", false );
             wChannel.close();
 
@@ -688,7 +535,7 @@ public class ForkedProcessEventNotifierTest
         {
             final Stream out = Stream.newStream();
             WritableBufferedByteChannel wChannel = newBufferedChannel( out );
-            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( wChannel );
+            EventChannelEncoder encoder = new EventChannelEncoder( wChannel );
             encoder.stdOut( "", false );
             wChannel.close();
 
@@ -721,7 +568,7 @@ public class ForkedProcessEventNotifierTest
         {
             final Stream out = Stream.newStream();
             WritableBufferedByteChannel wChannel = newBufferedChannel( out );
-            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( wChannel );
+            EventChannelEncoder encoder = new EventChannelEncoder( wChannel );
             encoder.stdOut( null, false );
             wChannel.close();
 
@@ -754,7 +601,7 @@ public class ForkedProcessEventNotifierTest
         {
             final Stream out = Stream.newStream();
             WritableBufferedByteChannel wChannel = newBufferedChannel( out );
-            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( wChannel );
+            EventChannelEncoder encoder = new EventChannelEncoder( wChannel );
             encoder.stdOut( "", true );
             wChannel.close();
 
@@ -787,7 +634,7 @@ public class ForkedProcessEventNotifierTest
         {
             final Stream out = Stream.newStream();
             WritableBufferedByteChannel wChannel = newBufferedChannel( out );
-            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( wChannel );
+            EventChannelEncoder encoder = new EventChannelEncoder( wChannel );
             encoder.stdOut( null, true );
             wChannel.close();
 
@@ -820,7 +667,7 @@ public class ForkedProcessEventNotifierTest
         {
             final Stream out = Stream.newStream();
             WritableBufferedByteChannel wChannel = newBufferedChannel( out );
-            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( wChannel );
+            EventChannelEncoder encoder = new EventChannelEncoder( wChannel );
             encoder.stdErr( "msg", false );
             wChannel.close();
 
@@ -853,8 +700,8 @@ public class ForkedProcessEventNotifierTest
         {
             final Stream out = Stream.newStream();
             WritableBufferedByteChannel wChannel = newBufferedChannel( out );
-            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( wChannel );
-            encoder.sendSystemProperties( ObjectUtils.systemProps() );
+            EventChannelEncoder encoder = new EventChannelEncoder( wChannel );
+            encoder.systemProperties( ObjectUtils.systemProps() );
             wChannel.close();
 
             ReadableByteChannel channel = newChannel( new ByteArrayInputStream( out.toByteArray() ) );
@@ -918,7 +765,7 @@ public class ForkedProcessEventNotifierTest
             assertThat( logger.debug.peek() )
                 .contains( ":maven-surefire-event:\u000c:abnormal-run:-:" );
 
-            String dump = "Corrupted STDOUT by directly writing to native stream in forked JVM 0.";
+            String dump = "Corrupted channel by directly writing to native stream in forked JVM 0.";
             assertThat( arguments.dumpStreamText )
                 .hasSize( 1 )
                 .contains( format( dump + " Stream '%s'.", ":maven-surefire-event:\u000c:abnormal-run:-:" ) );
@@ -934,8 +781,7 @@ public class ForkedProcessEventNotifierTest
         public void shouldHandleExit() throws Exception
         {
             final Stream out = Stream.newStream();
-            LegacyMasterProcessChannelEncoder encoder =
-                new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+            EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
 
             StackTraceWriter stackTraceWriter = mock( StackTraceWriter.class );
             when( stackTraceWriter.getThrowable() ).thenReturn( new SafeThrowable( "1" ) );
@@ -1050,10 +896,9 @@ public class ForkedProcessEventNotifierTest
 
             final Stream out = Stream.newStream();
 
-            LegacyMasterProcessChannelEncoder encoder =
-                new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+            EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
 
-            LegacyMasterProcessChannelEncoder.class.getMethod( operation[0], ReportEntry.class, boolean.class )
+            EventChannelEncoder.class.getMethod( operation[0], ReportEntry.class, boolean.class )
                     .invoke( encoder, reportEntry, trim );
 
             ForkedProcessEventNotifier notifier = new ForkedProcessEventNotifier();
@@ -1323,7 +1168,7 @@ public class ForkedProcessEventNotifierTest
         @Override
         public File dumpStreamException( @Nonnull Throwable t )
         {
-            throw new UnsupportedOperationException();
+            return dumpStreamTextFile;
         }
 
         @Override
@@ -1343,6 +1188,18 @@ public class ForkedProcessEventNotifierTest
         {
             return !dumpStreamText.isEmpty() || !logWarningAtEnd.isEmpty();
         }
+
+        @Override
+        public File getEventStreamBinaryFile()
+        {
+            return null;
+        }
+
+        @Override
+        public File getCommandStreamBinaryFile()
+        {
+            return null;
+        }
     }
 
     /**
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/StreamFeederTest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/StreamFeederTest.java
index 06cef43..adbb2a7 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/StreamFeederTest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/StreamFeederTest.java
@@ -36,7 +36,6 @@ import java.util.Iterator;
 
 import static java.util.Arrays.asList;
 import static org.apache.maven.surefire.api.booter.Command.TEST_SET_FINISHED;
-import static org.apache.maven.surefire.api.booter.MasterProcessCommand.NOOP;
 import static org.apache.maven.surefire.api.booter.MasterProcessCommand.RUN_CLASS;
 import static org.fest.assertions.Assertions.assertThat;
 import static org.mockito.ArgumentMatchers.any;
@@ -92,8 +91,8 @@ public class StreamFeederTest
                 {
                     ByteBuffer bb = invocation.getArgument( 0 );
                     bb.flip();
-                    out.write( bb.array() );
-                    return 0;
+                    out.write( bb.array(), 0, bb.limit() );
+                    return bb.limit();
                 }
             } );
 
@@ -104,8 +103,28 @@ public class StreamFeederTest
         streamFeeder.join();
         String commands = out.toString();
 
+        String expected = new StringBuilder()
+            .append( ":maven-surefire-command:" )
+            .append( (char) 13 )
+            .append( ":run-testclass:" )
+            .append( (char) 10 )
+            .append( ":normal-run:" )
+            .append( (char) 5 )
+            .append( ":UTF-8:" )
+            .append( (char) 0 )
+            .append( (char) 0 )
+            .append( (char) 0 )
+            .append( (char) 9 )
+            .append( ":" )
+            .append( "pkg.ATest" )
+            .append( ":" )
+            .append( ":maven-surefire-command:" )
+            .append( (char) 16 )
+            .append( ":testset-finished:" )
+            .toString();
+
         assertThat( commands )
-            .isEqualTo( ":maven-surefire-command:run-testclass:pkg.ATest::maven-surefire-command:testset-finished:" );
+            .isEqualTo( expected );
 
         verify( channel, times( 1 ) )
             .close();
@@ -147,16 +166,4 @@ public class StreamFeederTest
 
         verifyZeroInteractions( logger );
     }
-
-    @Test( expected = IllegalArgumentException.class )
-    public void shouldFailWithoutData()
-    {
-        StreamFeeder.encode( RUN_CLASS );
-    }
-
-    @Test( expected = IllegalArgumentException.class )
-    public void shouldFailWithData()
-    {
-        StreamFeeder.encode( NOOP, "" );
-    }
 }
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/surefire/JUnit4SuiteTest.java b/maven-surefire-common/src/test/java/org/apache/maven/surefire/JUnit4SuiteTest.java
index 7692a04..dd1f08d 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/surefire/JUnit4SuiteTest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/surefire/JUnit4SuiteTest.java
@@ -62,6 +62,7 @@ import org.apache.maven.surefire.extensions.StatelessTestsetInfoReporterTest;
 import org.apache.maven.surefire.report.FileReporterTest;
 import org.apache.maven.surefire.report.RunStatisticsTest;
 import org.apache.maven.surefire.spi.SPITest;
+import org.apache.maven.surefire.stream.EventDecoderTest;
 import org.apache.maven.surefire.util.RelocatorTest;
 
 /**
@@ -115,6 +116,7 @@ public class JUnit4SuiteTest extends TestCase
         suite.addTest( new JUnit4TestAdapter( StreamFeederTest.class ) );
         suite.addTest( new JUnit4TestAdapter( E2ETest.class ) );
         suite.addTest( new JUnit4TestAdapter( ThreadedStreamConsumerTest.class ) );
+        suite.addTest( new JUnit4TestAdapter( EventDecoderTest.class ) );
         suite.addTest( new JUnit4TestAdapter( EventConsumerThreadTest.class ) );
         return suite;
     }
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/surefire/extensions/ForkChannelTest.java b/maven-surefire-common/src/test/java/org/apache/maven/surefire/extensions/ForkChannelTest.java
index 8caa668..428ab10 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/surefire/extensions/ForkChannelTest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/surefire/extensions/ForkChannelTest.java
@@ -26,6 +26,7 @@ import org.apache.maven.plugin.surefire.extensions.SurefireForkNodeFactory;
 import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
 import org.apache.maven.surefire.api.event.ControlByeEvent;
 import org.apache.maven.surefire.api.event.Event;
+import org.apache.maven.surefire.api.fork.ForkNodeArguments;
 import org.apache.maven.surefire.extensions.util.CountdownCloseable;
 import org.junit.Test;
 
@@ -67,6 +68,18 @@ public class ForkChannelTest
         final String sessionId = UUID.randomUUID().toString();
         ForkNodeArguments forkNodeArguments = new ForkNodeArguments()
         {
+            @Override
+            public File getEventStreamBinaryFile()
+            {
+                return null;
+            }
+
+            @Override
+            public File getCommandStreamBinaryFile()
+            {
+                return null;
+            }
+
             @Nonnull
             @Override
             public String getSessionId()
@@ -203,7 +216,7 @@ public class ForkChannelTest
                 byte[] data = new byte[128];
                 int readLength = socket.getInputStream().read( data );
                 String token = new String( data, 0, readLength, US_ASCII );
-                assertThat( token ).isEqualTo( ":maven-surefire-command:noop:" );
+                assertThat( token ).isEqualTo( ":maven-surefire-command:\u0004:noop:" );
                 socket.getOutputStream().write( ":maven-surefire-event:\u0003:bye:".getBytes( US_ASCII ) );
             }
             catch ( IOException e )
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/surefire/stream/EventDecoderTest.java b/maven-surefire-common/src/test/java/org/apache/maven/surefire/stream/EventDecoderTest.java
new file mode 100644
index 0000000..ccfd3f0
--- /dev/null
+++ b/maven-surefire-common/src/test/java/org/apache/maven/surefire/stream/EventDecoderTest.java
@@ -0,0 +1,785 @@
+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.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.api.fork.ForkNodeArguments;
+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.stream.AbstractStreamDecoder.Memento;
+import org.apache.maven.surefire.api.stream.AbstractStreamDecoder.Segment;
+import org.apache.maven.surefire.api.stream.SegmentType;
+import org.junit.Test;
+
+import javax.annotation.Nonnull;
+import java.io.File;
+import java.nio.ByteBuffer;
+import java.nio.channels.ReadableByteChannel;
+import java.util.Map;
+
+import static java.lang.Math.min;
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+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;
+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.fest.assertions.Assertions.assertThat;
+import static org.powermock.api.mockito.PowerMockito.mock;
+import static org.powermock.api.mockito.PowerMockito.when;
+import static org.powermock.reflect.Whitebox.invokeMethod;
+
+/**
+ *
+ */
+public class EventDecoderTest
+{
+    @Test
+    public void shouldMapEventTypes() throws Exception
+    {
+        Map<Segment, ForkedProcessEventType> eventTypes = invokeMethod( EventDecoder.class, "segmentsToEvents" );
+        assertThat( eventTypes )
+            .hasSize( ForkedProcessEventType.values().length );
+    }
+
+    @Test
+    public void shouldMapRunModes() throws Exception
+    {
+        Map<Segment, RunMode> map = invokeMethod( EventDecoder.class, "segmentsToRunModes" );
+
+        assertThat( map )
+            .hasSize( 2 );
+
+        byte[] stream = "normal-run".getBytes( US_ASCII );
+        Segment segment = new Segment( stream, 0, stream.length );
+        assertThat( map.get( segment ) )
+            .isEqualTo( NORMAL_RUN );
+
+        stream = "rerun-test-after-failure".getBytes( US_ASCII );
+        segment = new Segment( stream, 0, stream.length );
+        assertThat( map.get( segment ) )
+            .isEqualTo( RERUN_TEST_AFTER_FAILURE );
+    }
+
+    @Test
+    public void shouldMapEventTypeToSegmentType()
+    {
+        byte[] stream = {};
+        Channel channel = new Channel( stream, 1 );
+        EventDecoder decoder = new EventDecoder( channel, new MockForkNodeArguments() );
+
+        SegmentType[] segmentTypes = decoder.nextSegmentType( BOOTERCODE_BYE );
+        assertThat( segmentTypes )
+            .hasSize( 1 )
+            .containsOnly( END_OF_FRAME );
+
+        segmentTypes = decoder.nextSegmentType( BOOTERCODE_STOP_ON_NEXT_TEST );
+        assertThat( segmentTypes )
+            .hasSize( 1 )
+            .containsOnly( END_OF_FRAME );
+
+        segmentTypes = decoder.nextSegmentType( BOOTERCODE_NEXT_TEST );
+        assertThat( segmentTypes )
+            .hasSize( 1 )
+            .containsOnly( END_OF_FRAME );
+
+        segmentTypes = decoder.nextSegmentType( BOOTERCODE_CONSOLE_ERROR );
+        assertThat( segmentTypes )
+            .hasSize( 5 )
+            .isEqualTo( new SegmentType[] { STRING_ENCODING, DATA_STRING, DATA_STRING, DATA_STRING, END_OF_FRAME } );
+
+        segmentTypes = decoder.nextSegmentType( ForkedProcessEventType.BOOTERCODE_JVM_EXIT_ERROR );
+        assertThat( segmentTypes )
+            .hasSize( 5 )
+            .isEqualTo( new SegmentType[] { STRING_ENCODING, DATA_STRING, DATA_STRING, DATA_STRING, END_OF_FRAME } );
+
+        segmentTypes = decoder.nextSegmentType( BOOTERCODE_CONSOLE_INFO );
+        assertThat( segmentTypes )
+            .hasSize( 3 )
+            .isEqualTo( new SegmentType[] { STRING_ENCODING, DATA_STRING, END_OF_FRAME } );
+
+        segmentTypes = decoder.nextSegmentType( ForkedProcessEventType.BOOTERCODE_CONSOLE_DEBUG );
+        assertThat( segmentTypes )
+            .hasSize( 3 )
+            .isEqualTo( new SegmentType[] { STRING_ENCODING, DATA_STRING, END_OF_FRAME } );
+
+        segmentTypes = decoder.nextSegmentType( ForkedProcessEventType.BOOTERCODE_CONSOLE_WARNING );
+        assertThat( segmentTypes )
+            .hasSize( 3 )
+            .isEqualTo( new SegmentType[] { STRING_ENCODING, DATA_STRING, END_OF_FRAME } );
+
+        segmentTypes = decoder.nextSegmentType( BOOTERCODE_STDOUT );
+        assertThat( segmentTypes )
+            .hasSize( 4 )
+            .isEqualTo( new SegmentType[] { RUN_MODE, STRING_ENCODING, DATA_STRING, END_OF_FRAME } );
+
+        segmentTypes = decoder.nextSegmentType( ForkedProcessEventType.BOOTERCODE_STDOUT_NEW_LINE );
+        assertThat( segmentTypes )
+            .hasSize( 4 )
+            .isEqualTo( new SegmentType[] { RUN_MODE, STRING_ENCODING, DATA_STRING, END_OF_FRAME } );
+
+        segmentTypes = decoder.nextSegmentType( ForkedProcessEventType.BOOTERCODE_STDERR );
+        assertThat( segmentTypes )
+            .hasSize( 4 )
+            .isEqualTo( new SegmentType[] { RUN_MODE, STRING_ENCODING, DATA_STRING, END_OF_FRAME } );
+
+        segmentTypes = decoder.nextSegmentType( ForkedProcessEventType.BOOTERCODE_STDERR_NEW_LINE );
+        assertThat( segmentTypes )
+            .hasSize( 4 )
+            .isEqualTo( new SegmentType[] { RUN_MODE, STRING_ENCODING, DATA_STRING, END_OF_FRAME } );
+
+        segmentTypes = decoder.nextSegmentType( BOOTERCODE_SYSPROPS );
+        assertThat( segmentTypes )
+            .hasSize( 5 )
+            .isEqualTo( new SegmentType[] { RUN_MODE, STRING_ENCODING, DATA_STRING, DATA_STRING, END_OF_FRAME } );
+
+        segmentTypes = decoder.nextSegmentType( BOOTERCODE_TESTSET_STARTING );
+        assertThat( segmentTypes )
+            .hasSize( 13 )
+            .isEqualTo( new SegmentType[] {
+                RUN_MODE, 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 } );
+
+        segmentTypes = decoder.nextSegmentType( ForkedProcessEventType.BOOTERCODE_TESTSET_COMPLETED );
+        assertThat( segmentTypes )
+            .hasSize( 13 )
+            .isEqualTo( new SegmentType[] { RUN_MODE, 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 } );
+
+        segmentTypes = decoder.nextSegmentType( ForkedProcessEventType.BOOTERCODE_TEST_STARTING );
+        assertThat( segmentTypes )
+            .hasSize( 13 )
+            .isEqualTo( new SegmentType[] { RUN_MODE, 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 } );
+
+        segmentTypes = decoder.nextSegmentType( BOOTERCODE_TEST_SUCCEEDED );
+        assertThat( segmentTypes )
+            .hasSize( 13 )
+            .isEqualTo( new SegmentType[] { RUN_MODE, 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 } );
+
+        segmentTypes = decoder.nextSegmentType( BOOTERCODE_TEST_FAILED );
+        assertThat( segmentTypes )
+            .hasSize( 13 )
+            .isEqualTo( new SegmentType[] { RUN_MODE, 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 } );
+
+        segmentTypes = decoder.nextSegmentType( ForkedProcessEventType.BOOTERCODE_TEST_SKIPPED );
+        assertThat( segmentTypes )
+            .hasSize( 13 )
+            .isEqualTo( new SegmentType[] { RUN_MODE, 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 } );
+
+        segmentTypes = decoder.nextSegmentType( ForkedProcessEventType.BOOTERCODE_TEST_ERROR );
+        assertThat( segmentTypes )
+            .hasSize( 13 )
+            .isEqualTo( new SegmentType[] { RUN_MODE, 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 } );
+
+        segmentTypes = decoder.nextSegmentType( ForkedProcessEventType.BOOTERCODE_TEST_ASSUMPTIONFAILURE );
+        assertThat( segmentTypes )
+            .hasSize( 13 )
+            .isEqualTo( new SegmentType[] { RUN_MODE, 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 } );
+    }
+
+    @Test
+    public void shouldCreateEvent() throws Exception
+    {
+        byte[] stream = {};
+        Channel channel = new Channel( stream, 1 );
+        EventDecoder decoder = new EventDecoder( channel, new MockForkNodeArguments() );
+
+        Event event = decoder.toMessage( BOOTERCODE_BYE, NORMAL_RUN, decoder.new Memento() );
+        assertThat( event )
+            .isInstanceOf( ControlByeEvent.class );
+
+        event = decoder.toMessage( BOOTERCODE_STOP_ON_NEXT_TEST, NORMAL_RUN, decoder.new Memento() );
+        assertThat( event )
+            .isInstanceOf( ControlStopOnNextTestEvent.class );
+
+        event = decoder.toMessage( BOOTERCODE_NEXT_TEST, NORMAL_RUN, decoder.new Memento() );
+        assertThat( event )
+            .isInstanceOf( ControlNextTestEvent.class );
+
+        Memento memento = decoder.new Memento();
+        memento.getData().addAll( asList( "1", "2", "3" ) );
+        event = decoder.toMessage( BOOTERCODE_CONSOLE_ERROR, NORMAL_RUN, memento );
+        assertThat( event )
+            .isInstanceOf( ConsoleErrorEvent.class );
+        ConsoleErrorEvent consoleErrorEvent = (ConsoleErrorEvent) event;
+        assertThat( consoleErrorEvent.getStackTraceWriter().getThrowable().getLocalizedMessage() )
+            .isEqualTo( "1" );
+        assertThat( consoleErrorEvent.getStackTraceWriter().smartTrimmedStackTrace() )
+            .isEqualTo( "2" );
+        assertThat( consoleErrorEvent.getStackTraceWriter().writeTraceToString() )
+            .isEqualTo( "3" );
+
+        memento = decoder.new Memento();
+        memento.getData().addAll( asList( null, null, null ) );
+        event = decoder.toMessage( BOOTERCODE_CONSOLE_ERROR, NORMAL_RUN, memento );
+        assertThat( event )
+            .isInstanceOf( ConsoleErrorEvent.class );
+        consoleErrorEvent = (ConsoleErrorEvent) event;
+        assertThat( consoleErrorEvent.getStackTraceWriter() )
+            .isNull();
+
+        memento = decoder.new Memento();
+        memento.getData().addAll( asList( "1", "2", "3" ) );
+        event = decoder.toMessage( BOOTERCODE_JVM_EXIT_ERROR, NORMAL_RUN, memento );
+        assertThat( event )
+            .isInstanceOf( JvmExitErrorEvent.class );
+        JvmExitErrorEvent jvmExitErrorEvent = (JvmExitErrorEvent) event;
+        assertThat( jvmExitErrorEvent.getStackTraceWriter().getThrowable().getLocalizedMessage() )
+            .isEqualTo( "1" );
+        assertThat( jvmExitErrorEvent.getStackTraceWriter().smartTrimmedStackTrace() )
+            .isEqualTo( "2" );
+        assertThat( jvmExitErrorEvent.getStackTraceWriter().writeTraceToString() )
+            .isEqualTo( "3" );
+
+        memento = decoder.new Memento();
+        memento.getData().addAll( asList( null, null, null ) );
+        event = decoder.toMessage( BOOTERCODE_JVM_EXIT_ERROR, NORMAL_RUN, memento );
+        assertThat( event )
+            .isInstanceOf( JvmExitErrorEvent.class );
+        jvmExitErrorEvent = (JvmExitErrorEvent) event;
+        assertThat( jvmExitErrorEvent.getStackTraceWriter() )
+            .isNull();
+
+        memento = decoder.new Memento();
+        memento.getData().addAll( singletonList( "m" ) );
+        event = decoder.toMessage( BOOTERCODE_CONSOLE_INFO, NORMAL_RUN, memento );
+        assertThat( event ).isInstanceOf( ConsoleInfoEvent.class );
+        assertThat( ( (ConsoleInfoEvent) event ).getMessage() ).isEqualTo( "m" );
+
+        memento = decoder.new Memento();
+        memento.getData().addAll( singletonList( "" ) );
+        event = decoder.toMessage( BOOTERCODE_CONSOLE_WARNING, NORMAL_RUN, memento );
+        assertThat( event ).isInstanceOf( ConsoleWarningEvent.class );
+        assertThat( ( (ConsoleWarningEvent) event ).getMessage() ).isEmpty();
+
+        memento = decoder.new Memento();
+        memento.getData().addAll( singletonList( null ) );
+        event = decoder.toMessage( BOOTERCODE_CONSOLE_DEBUG, NORMAL_RUN, memento );
+        assertThat( event ).isInstanceOf( ConsoleDebugEvent.class );
+        assertThat( ( (ConsoleDebugEvent) event ).getMessage() ).isNull();
+
+        memento = decoder.new Memento();
+        memento.getData().addAll( singletonList( "m" ) );
+        event = decoder.toMessage( BOOTERCODE_STDOUT, NORMAL_RUN, memento );
+        assertThat( event ).isInstanceOf( StandardStreamOutEvent.class );
+        assertThat( ( (StandardStreamOutEvent) event ).getMessage() ).isEqualTo( "m" );
+        assertThat( ( (StandardStreamOutEvent) event ).getRunMode() ).isEqualTo( NORMAL_RUN );
+
+        memento = decoder.new Memento();
+        memento.getData().addAll( singletonList( null ) );
+        event = decoder.toMessage( BOOTERCODE_STDOUT_NEW_LINE, RERUN_TEST_AFTER_FAILURE, memento );
+        assertThat( event ).isInstanceOf( StandardStreamOutWithNewLineEvent.class );
+        assertThat( ( (StandardStreamOutWithNewLineEvent) event ).getMessage() ).isNull();
+        assertThat( ( (StandardStreamOutWithNewLineEvent) event ).getRunMode() ).isEqualTo( RERUN_TEST_AFTER_FAILURE );
+
+        memento = decoder.new Memento();
+        memento.getData().addAll( singletonList( null ) );
+        event = decoder.toMessage( BOOTERCODE_STDERR, RERUN_TEST_AFTER_FAILURE, memento );
+        assertThat( event ).isInstanceOf( StandardStreamErrEvent.class );
+        assertThat( ( (StandardStreamErrEvent) event ).getMessage() ).isNull();
+        assertThat( ( (StandardStreamErrEvent) event ).getRunMode() ).isEqualTo( RERUN_TEST_AFTER_FAILURE );
+
+        memento = decoder.new Memento();
+        memento.getData().addAll( singletonList( "abc" ) );
+        event = decoder.toMessage( BOOTERCODE_STDERR_NEW_LINE, NORMAL_RUN, memento );
+        assertThat( event ).isInstanceOf( StandardStreamErrWithNewLineEvent.class );
+        assertThat( ( (StandardStreamErrWithNewLineEvent) event ).getMessage() ).isEqualTo( "abc" );
+        assertThat( ( (StandardStreamErrWithNewLineEvent) event ).getRunMode() ).isEqualTo( NORMAL_RUN );
+
+        memento = decoder.new Memento();
+        memento.getData().addAll( asList( "key", "value" ) );
+        event = decoder.toMessage( BOOTERCODE_SYSPROPS, NORMAL_RUN, memento );
+        assertThat( event ).isInstanceOf( SystemPropertyEvent.class );
+        assertThat( ( (SystemPropertyEvent) event ).getKey() ).isEqualTo( "key" );
+        assertThat( ( (SystemPropertyEvent) event ).getValue() ).isEqualTo( "value" );
+        assertThat( ( (SystemPropertyEvent) event ).getRunMode() ).isEqualTo( NORMAL_RUN );
+
+        memento = decoder.new Memento();
+        memento.getData().addAll( asList( "source", "sourceText", "name", "nameText", "group", "message", 5,
+            "traceMessage", "smartTrimmedStackTrace", "stackTrace" ) );
+        event = decoder.toMessage( BOOTERCODE_TESTSET_STARTING, NORMAL_RUN, memento );
+        assertThat( event ).isInstanceOf( TestsetStartingEvent.class );
+        assertThat( ( (TestsetStartingEvent) event ).getRunMode() ).isEqualTo( NORMAL_RUN );
+        assertThat( ( (TestsetStartingEvent) event ).getReportEntry().getSourceName() ).isEqualTo( "source" );
+        assertThat( ( (TestsetStartingEvent) event ).getReportEntry().getSourceText() ).isEqualTo( "sourceText" );
+        assertThat( ( (TestsetStartingEvent) event ).getReportEntry().getName() ).isEqualTo( "name" );
+        assertThat( ( (TestsetStartingEvent) event ).getReportEntry().getNameText() ).isEqualTo( "nameText" );
+        assertThat( ( (TestsetStartingEvent) event ).getReportEntry().getGroup() ).isEqualTo( "group" );
+        assertThat( ( (TestsetStartingEvent) event ).getReportEntry().getMessage() ).isEqualTo( "message" );
+        assertThat( ( (TestsetStartingEvent) event ).getReportEntry().getElapsed() ).isEqualTo( 5 );
+        assertThat( ( (TestsetStartingEvent) event )
+            .getReportEntry().getStackTraceWriter().getThrowable().getLocalizedMessage() )
+            .isEqualTo( "traceMessage" );
+        assertThat( ( (TestsetStartingEvent) event ).getReportEntry().getStackTraceWriter().smartTrimmedStackTrace() )
+            .isEqualTo( "smartTrimmedStackTrace" );
+        assertThat( ( (TestsetStartingEvent) event ).getReportEntry().getStackTraceWriter().writeTraceToString() )
+            .isEqualTo( "stackTrace" );
+
+        memento = decoder.new Memento();
+        memento.getData().addAll( asList( "source", "sourceText", "name", "nameText", "group", null, 5,
+            "traceMessage", "smartTrimmedStackTrace", "stackTrace" ) );
+        event = decoder.toMessage( BOOTERCODE_TESTSET_COMPLETED, NORMAL_RUN, memento );
+        assertThat( event ).isInstanceOf( TestsetCompletedEvent.class );
+        assertThat( ( (TestsetCompletedEvent) event ).getRunMode() ).isEqualTo( NORMAL_RUN );
+        assertThat( ( (TestsetCompletedEvent) event ).getReportEntry().getSourceName() ).isEqualTo( "source" );
+        assertThat( ( (TestsetCompletedEvent) event ).getReportEntry().getSourceText() ).isEqualTo( "sourceText" );
+        assertThat( ( (TestsetCompletedEvent) event ).getReportEntry().getName() ).isEqualTo( "name" );
+        assertThat( ( (TestsetCompletedEvent) event ).getReportEntry().getNameText() ).isEqualTo( "nameText" );
+        assertThat( ( (TestsetCompletedEvent) event ).getReportEntry().getGroup() ).isEqualTo( "group" );
+        assertThat( ( (TestsetCompletedEvent) event ).getReportEntry().getMessage() ).isNull();
+        assertThat( ( (TestsetCompletedEvent) event ).getReportEntry().getElapsed() ).isEqualTo( 5 );
+        assertThat( ( (TestsetCompletedEvent) event )
+            .getReportEntry().getStackTraceWriter().getThrowable().getLocalizedMessage() )
+            .isEqualTo( "traceMessage" );
+        assertThat( ( (TestsetCompletedEvent) event ).getReportEntry().getStackTraceWriter().smartTrimmedStackTrace() )
+            .isEqualTo( "smartTrimmedStackTrace" );
+        assertThat( ( (TestsetCompletedEvent) event ).getReportEntry().getStackTraceWriter().writeTraceToString() )
+            .isEqualTo( "stackTrace" );
+
+        memento = decoder.new Memento();
+        memento.getData().addAll( asList( "source", "sourceText", "name", "nameText", "group", "message", 5,
+            null, "smartTrimmedStackTrace", "stackTrace" ) );
+        event = decoder.toMessage( BOOTERCODE_TEST_STARTING, NORMAL_RUN, memento );
+        assertThat( event ).isInstanceOf( TestStartingEvent.class );
+        assertThat( ( (TestStartingEvent) event ).getRunMode() ).isEqualTo( NORMAL_RUN );
+        assertThat( ( (TestStartingEvent) event ).getReportEntry().getSourceName() ).isEqualTo( "source" );
+        assertThat( ( (TestStartingEvent) event ).getReportEntry().getSourceText() ).isEqualTo( "sourceText" );
+        assertThat( ( (TestStartingEvent) event ).getReportEntry().getName() ).isEqualTo( "name" );
+        assertThat( ( (TestStartingEvent) event ).getReportEntry().getNameText() ).isEqualTo( "nameText" );
+        assertThat( ( (TestStartingEvent) event ).getReportEntry().getGroup() ).isEqualTo( "group" );
+        assertThat( ( (TestStartingEvent) event ).getReportEntry().getMessage() ).isEqualTo( "message" );
+        assertThat( ( (TestStartingEvent) event ).getReportEntry().getElapsed() ).isEqualTo( 5 );
+        assertThat( ( (TestStartingEvent) event )
+            .getReportEntry().getStackTraceWriter().getThrowable().getLocalizedMessage() )
+            .isNull();
+        assertThat( ( (TestStartingEvent) event ).getReportEntry().getStackTraceWriter().smartTrimmedStackTrace() )
+            .isEqualTo( "smartTrimmedStackTrace" );
+        assertThat( ( (TestStartingEvent) event ).getReportEntry().getStackTraceWriter().writeTraceToString() )
+            .isEqualTo( "stackTrace" );
+
+        memento = decoder.new Memento();
+        memento.getData()
+            .addAll( asList( "source", "sourceText", "name", "nameText", "group", "message", 5, null, null, null ) );
+        event = decoder.toMessage( BOOTERCODE_TEST_SUCCEEDED, NORMAL_RUN, memento );
+        assertThat( event ).isInstanceOf( TestSucceededEvent.class );
+        assertThat( ( (TestSucceededEvent) event ).getRunMode() ).isEqualTo( NORMAL_RUN );
+        assertThat( ( (TestSucceededEvent) event ).getReportEntry().getSourceName() ).isEqualTo( "source" );
+        assertThat( ( (TestSucceededEvent) event ).getReportEntry().getSourceText() ).isEqualTo( "sourceText" );
+        assertThat( ( (TestSucceededEvent) event ).getReportEntry().getName() ).isEqualTo( "name" );
+        assertThat( ( (TestSucceededEvent) event ).getReportEntry().getNameText() ).isEqualTo( "nameText" );
+        assertThat( ( (TestSucceededEvent) event ).getReportEntry().getGroup() ).isEqualTo( "group" );
+        assertThat( ( (TestSucceededEvent) event ).getReportEntry().getMessage() ).isEqualTo( "message" );
+        assertThat( ( (TestSucceededEvent) event ).getReportEntry().getElapsed() ).isEqualTo( 5 );
+        assertThat( ( (TestSucceededEvent) event ).getReportEntry().getStackTraceWriter() ).isNull();
+
+        memento = decoder.new Memento();
+        memento.getData().addAll( asList( "source", null, "name", null, "group", null, 5,
+            "traceMessage", "smartTrimmedStackTrace", "stackTrace" ) );
+        event = decoder.toMessage( BOOTERCODE_TEST_FAILED, RERUN_TEST_AFTER_FAILURE, memento );
+        assertThat( event ).isInstanceOf( TestFailedEvent.class );
+        assertThat( ( (TestFailedEvent) event ).getRunMode() ).isEqualTo( RERUN_TEST_AFTER_FAILURE );
+        assertThat( ( (TestFailedEvent) event ).getReportEntry().getSourceName() ).isEqualTo( "source" );
+        assertThat( ( (TestFailedEvent) event ).getReportEntry().getSourceText() ).isNull();
+        assertThat( ( (TestFailedEvent) event ).getReportEntry().getName() ).isEqualTo( "name" );
+        assertThat( ( (TestFailedEvent) event ).getReportEntry().getNameText() ).isNull();
+        assertThat( ( (TestFailedEvent) event ).getReportEntry().getGroup() ).isEqualTo( "group" );
+        assertThat( ( (TestFailedEvent) event ).getReportEntry().getMessage() ).isNull();
+        assertThat( ( (TestFailedEvent) event ).getReportEntry().getElapsed() ).isEqualTo( 5 );
+        assertThat( ( (TestFailedEvent) event )
+            .getReportEntry().getStackTraceWriter().getThrowable().getLocalizedMessage() )
+            .isEqualTo( "traceMessage" );
+        assertThat( ( (TestFailedEvent) event ).getReportEntry().getStackTraceWriter().smartTrimmedStackTrace() )
+            .isEqualTo( "smartTrimmedStackTrace" );
+        assertThat( ( (TestFailedEvent) event ).getReportEntry().getStackTraceWriter().writeTraceToString() )
+            .isEqualTo( "stackTrace" );
+
+        memento = decoder.new Memento();
+        memento.getData().addAll( asList( "source", null, "name", null, null, null, 5, null, null, "stackTrace" ) );
+        event = decoder.toMessage( BOOTERCODE_TEST_SKIPPED, NORMAL_RUN, memento );
+        assertThat( event ).isInstanceOf( TestSkippedEvent.class );
+        assertThat( ( (TestSkippedEvent) event ).getRunMode() ).isEqualTo( NORMAL_RUN );
+        assertThat( ( (TestSkippedEvent) event ).getReportEntry().getSourceName() ).isEqualTo( "source" );
+        assertThat( ( (TestSkippedEvent) event ).getReportEntry().getSourceText() ).isNull();
+        assertThat( ( (TestSkippedEvent) event ).getReportEntry().getName() ).isEqualTo( "name" );
+        assertThat( ( (TestSkippedEvent) event ).getReportEntry().getNameText() ).isNull();
+        assertThat( ( (TestSkippedEvent) event ).getReportEntry().getGroup() ).isNull();
+        assertThat( ( (TestSkippedEvent) event ).getReportEntry().getMessage() ).isNull();
+        assertThat( ( (TestSkippedEvent) event ).getReportEntry().getElapsed() ).isEqualTo( 5 );
+        assertThat( ( (TestSkippedEvent) event )
+            .getReportEntry().getStackTraceWriter().getThrowable().getLocalizedMessage() )
+            .isNull();
+        assertThat( ( (TestSkippedEvent) event )
+            .getReportEntry().getStackTraceWriter().smartTrimmedStackTrace() )
+            .isNull();
+        assertThat( ( (TestSkippedEvent) event )
+            .getReportEntry().getStackTraceWriter().writeTraceToString() )
+            .isEqualTo( "stackTrace" );
+
+        memento = decoder.new Memento();
+        memento.getData()
+            .addAll( asList( "source", null, "name", "nameText", null, null, 0, null, null, "stackTrace" ) );
+        event = decoder.toMessage( BOOTERCODE_TEST_ERROR, NORMAL_RUN, memento );
+        assertThat( event ).isInstanceOf( TestErrorEvent.class );
+        assertThat( ( (TestErrorEvent) event ).getRunMode() ).isEqualTo( NORMAL_RUN );
+        assertThat( ( (TestErrorEvent) event ).getReportEntry().getSourceName() ).isEqualTo( "source" );
+        assertThat( ( (TestErrorEvent) event ).getReportEntry().getSourceText() ).isNull();
+        assertThat( ( (TestErrorEvent) event ).getReportEntry().getName() ).isEqualTo( "name" );
+        assertThat( ( (TestErrorEvent) event ).getReportEntry().getNameText() ).isEqualTo( "nameText" );
+        assertThat( ( (TestErrorEvent) event ).getReportEntry().getGroup() ).isNull();
+        assertThat( ( (TestErrorEvent) event ).getReportEntry().getMessage() ).isNull();
+        assertThat( ( (TestErrorEvent) event ).getReportEntry().getElapsed() ).isZero();
+        assertThat( ( (TestErrorEvent) event )
+            .getReportEntry().getStackTraceWriter().getThrowable().getLocalizedMessage() )
+            .isNull();
+        assertThat( ( (TestErrorEvent) event )
+            .getReportEntry().getStackTraceWriter().smartTrimmedStackTrace() )
+            .isNull();
+        assertThat( ( (TestErrorEvent) event )
+            .getReportEntry().getStackTraceWriter().writeTraceToString() )
+            .isEqualTo( "stackTrace" );
+
+        memento = decoder.new Memento();
+        memento.getData().addAll( asList( "source", null, "name", null, "group", null, 5, null, null, "stackTrace" ) );
+        event = decoder.toMessage( BOOTERCODE_TEST_ASSUMPTIONFAILURE, NORMAL_RUN, memento );
+        assertThat( event ).isInstanceOf( TestAssumptionFailureEvent.class );
+        assertThat( ( (TestAssumptionFailureEvent) event ).getRunMode() ).isEqualTo( NORMAL_RUN );
+        assertThat( ( (TestAssumptionFailureEvent) event ).getReportEntry().getSourceName() ).isEqualTo( "source" );
+        assertThat( ( (TestAssumptionFailureEvent) event ).getReportEntry().getSourceText() ).isNull();
+        assertThat( ( (TestAssumptionFailureEvent) event ).getReportEntry().getName() ).isEqualTo( "name" );
+        assertThat( ( (TestAssumptionFailureEvent) event ).getReportEntry().getNameText() ).isNull();
+        assertThat( ( (TestAssumptionFailureEvent) event ).getReportEntry().getGroup() ).isEqualTo( "group" );
+        assertThat( ( (TestAssumptionFailureEvent) event ).getReportEntry().getMessage() ).isNull();
+        assertThat( ( (TestAssumptionFailureEvent) event ).getReportEntry().getElapsed() ).isEqualTo( 5 );
+        assertThat( ( (TestAssumptionFailureEvent) event )
+            .getReportEntry().getStackTraceWriter().getThrowable().getLocalizedMessage() )
+            .isNull();
+        assertThat( ( (TestAssumptionFailureEvent) event )
+            .getReportEntry().getStackTraceWriter().smartTrimmedStackTrace() )
+            .isNull();
+        assertThat( ( (TestAssumptionFailureEvent) event )
+            .getReportEntry().getStackTraceWriter().writeTraceToString() )
+            .isEqualTo( "stackTrace" );
+    }
+
+    @Test
+    public void shouldRecognizeEmptyStream4ReportEntry()
+    {
+        ReportEntry reportEntry = EventDecoder.newReportEntry( "", "", "", "", "", "", null, "", "", "" );
+        assertThat( reportEntry ).isNotNull();
+        assertThat( reportEntry.getStackTraceWriter() ).isNotNull();
+        assertThat( reportEntry.getStackTraceWriter().smartTrimmedStackTrace() ).isEmpty();
+        assertThat( reportEntry.getStackTraceWriter().writeTraceToString() ).isEmpty();
+        assertThat( reportEntry.getStackTraceWriter().writeTrimmedTraceToString() ).isEmpty();
+        assertThat( reportEntry.getSourceName() ).isEmpty();
+        assertThat( reportEntry.getSourceText() ).isEmpty();
+        assertThat( reportEntry.getName() ).isEmpty();
+        assertThat( reportEntry.getNameText() ).isEmpty();
+        assertThat( reportEntry.getGroup() ).isEmpty();
+        assertThat( reportEntry.getNameWithGroup() ).isEmpty();
+        assertThat( reportEntry.getMessage() ).isEmpty();
+        assertThat( reportEntry.getElapsed() ).isNull();
+    }
+
+    @Test
+    @SuppressWarnings( "checkstyle:magicnumber" )
+    public void testCreatingReportEntry()
+    {
+        final String exceptionMessage = "msg";
+        final String smartStackTrace = "MyTest:86 >> Error";
+        final String stackTrace = "Exception: msg\ntrace line 1\ntrace line 2";
+        final String trimmedStackTrace = "trace line 1\ntrace line 2";
+
+        SafeThrowable safeThrowable = new SafeThrowable( exceptionMessage );
+        StackTraceWriter stackTraceWriter = mock( StackTraceWriter.class );
+        when( stackTraceWriter.getThrowable() ).thenReturn( safeThrowable );
+        when( stackTraceWriter.smartTrimmedStackTrace() ).thenReturn( smartStackTrace );
+        when( stackTraceWriter.writeTrimmedTraceToString() ).thenReturn( trimmedStackTrace );
+        when( stackTraceWriter.writeTraceToString() ).thenReturn( stackTrace );
+
+        ReportEntry reportEntry = mock( ReportEntry.class );
+        when( reportEntry.getElapsed() ).thenReturn( 102 );
+        when( reportEntry.getGroup() ).thenReturn( "this group" );
+        when( reportEntry.getMessage() ).thenReturn( "skipped test" );
+        when( reportEntry.getName() ).thenReturn( "my test" );
+        when( reportEntry.getNameText() ).thenReturn( "my display name" );
+        when( reportEntry.getNameWithGroup() ).thenReturn( "name with group" );
+        when( reportEntry.getSourceName() ).thenReturn( "pkg.MyTest" );
+        when( reportEntry.getSourceText() ).thenReturn( "test class display name" );
+        when( reportEntry.getStackTraceWriter() ).thenReturn( stackTraceWriter );
+
+        ReportEntry decodedReportEntry = EventDecoder.newReportEntry( reportEntry.getSourceName(),
+            reportEntry.getSourceText(), reportEntry.getName(), reportEntry.getNameText(), reportEntry.getGroup(),
+            reportEntry.getMessage(), null, null, null, null );
+
+        assertThat( decodedReportEntry ).isNotNull();
+        assertThat( decodedReportEntry.getSourceName() ).isEqualTo( reportEntry.getSourceName() );
+        assertThat( decodedReportEntry.getSourceText() ).isEqualTo( reportEntry.getSourceText() );
+        assertThat( decodedReportEntry.getName() ).isEqualTo( reportEntry.getName() );
+        assertThat( decodedReportEntry.getNameText() ).isEqualTo( reportEntry.getNameText() );
+        assertThat( decodedReportEntry.getGroup() ).isEqualTo( reportEntry.getGroup() );
+        assertThat( decodedReportEntry.getMessage() ).isEqualTo( reportEntry.getMessage() );
+        assertThat( decodedReportEntry.getStackTraceWriter() ).isNull();
+
+        decodedReportEntry = EventDecoder.newReportEntry( reportEntry.getSourceName(),
+            reportEntry.getSourceText(), reportEntry.getName(), reportEntry.getNameText(), reportEntry.getGroup(),
+            reportEntry.getMessage(), null, exceptionMessage, smartStackTrace, null );
+
+        assertThat( decodedReportEntry ).isNotNull();
+        assertThat( decodedReportEntry.getSourceName() ).isEqualTo( reportEntry.getSourceName() );
+        assertThat( decodedReportEntry.getSourceText() ).isEqualTo( reportEntry.getSourceText() );
+        assertThat( decodedReportEntry.getName() ).isEqualTo( reportEntry.getName() );
+        assertThat( decodedReportEntry.getNameText() ).isEqualTo( reportEntry.getNameText() );
+        assertThat( decodedReportEntry.getGroup() ).isEqualTo( reportEntry.getGroup() );
+        assertThat( decodedReportEntry.getMessage() ).isEqualTo( reportEntry.getMessage() );
+        assertThat( decodedReportEntry.getElapsed() ).isNull();
+        assertThat( decodedReportEntry.getStackTraceWriter() ).isNotNull();
+        assertThat( decodedReportEntry.getStackTraceWriter().getThrowable().getMessage() )
+            .isEqualTo( exceptionMessage );
+        assertThat( decodedReportEntry.getStackTraceWriter().smartTrimmedStackTrace() )
+            .isEqualTo( smartStackTrace );
+        assertThat( decodedReportEntry.getStackTraceWriter().writeTraceToString() )
+            .isNull();
+
+        decodedReportEntry = EventDecoder.newReportEntry( reportEntry.getSourceName(),
+            reportEntry.getSourceText(), reportEntry.getName(), reportEntry.getNameText(), reportEntry.getGroup(),
+            reportEntry.getMessage(), 1003, exceptionMessage, smartStackTrace, null );
+
+        assertThat( decodedReportEntry ).isNotNull();
+        assertThat( decodedReportEntry.getSourceName() ).isEqualTo( reportEntry.getSourceName() );
+        assertThat( decodedReportEntry.getSourceText() ).isEqualTo( reportEntry.getSourceText() );
+        assertThat( decodedReportEntry.getName() ).isEqualTo( reportEntry.getName() );
+        assertThat( decodedReportEntry.getNameText() ).isEqualTo( reportEntry.getNameText() );
+        assertThat( decodedReportEntry.getGroup() ).isEqualTo( reportEntry.getGroup() );
+        assertThat( decodedReportEntry.getMessage() ).isEqualTo( reportEntry.getMessage() );
+        assertThat( decodedReportEntry.getElapsed() ).isEqualTo( 1003 );
+        assertThat( decodedReportEntry.getStackTraceWriter() ).isNotNull();
+        assertThat( decodedReportEntry.getStackTraceWriter().getThrowable().getMessage() )
+            .isEqualTo( exceptionMessage );
+        assertThat( decodedReportEntry.getStackTraceWriter().smartTrimmedStackTrace() )
+            .isEqualTo( smartStackTrace );
+        assertThat( decodedReportEntry.getStackTraceWriter().writeTraceToString() )
+            .isNull();
+
+        decodedReportEntry = EventDecoder.newReportEntry( reportEntry.getSourceName(),
+            reportEntry.getSourceText(), reportEntry.getName(), reportEntry.getNameText(), reportEntry.getGroup(),
+            reportEntry.getMessage(), 1003, exceptionMessage, smartStackTrace, stackTrace );
+
+        assertThat( decodedReportEntry ).isNotNull();
+        assertThat( decodedReportEntry.getSourceName() ).isEqualTo( reportEntry.getSourceName() );
+        assertThat( decodedReportEntry.getSourceText() ).isEqualTo( reportEntry.getSourceText() );
+        assertThat( decodedReportEntry.getName() ).isEqualTo( reportEntry.getName() );
+        assertThat( decodedReportEntry.getNameText() ).isEqualTo( reportEntry.getNameText() );
+        assertThat( decodedReportEntry.getGroup() ).isEqualTo( reportEntry.getGroup() );
+        assertThat( decodedReportEntry.getMessage() ).isEqualTo( reportEntry.getMessage() );
+        assertThat( decodedReportEntry.getElapsed() ).isEqualTo( 1003 );
+        assertThat( decodedReportEntry.getStackTraceWriter() ).isNotNull();
+        assertThat( decodedReportEntry.getStackTraceWriter().getThrowable().getMessage() ).isNotNull();
+        assertThat( decodedReportEntry.getStackTraceWriter().getThrowable().getMessage() )
+            .isEqualTo( exceptionMessage );
+        assertThat( decodedReportEntry.getStackTraceWriter().smartTrimmedStackTrace() )
+            .isEqualTo( smartStackTrace );
+        assertThat( decodedReportEntry.getStackTraceWriter().writeTraceToString() ).isEqualTo( stackTrace );
+        assertThat( decodedReportEntry.getStackTraceWriter().writeTrimmedTraceToString() ).isEqualTo( stackTrace );
+
+        decodedReportEntry = EventDecoder.newReportEntry( reportEntry.getSourceName(),
+            reportEntry.getSourceText(), reportEntry.getName(), reportEntry.getNameText(), reportEntry.getGroup(),
+            reportEntry.getMessage(), 1003, exceptionMessage, smartStackTrace, trimmedStackTrace );
+
+        assertThat( decodedReportEntry ).isNotNull();
+        assertThat( decodedReportEntry.getSourceName() ).isEqualTo( reportEntry.getSourceName() );
+        assertThat( decodedReportEntry.getSourceText() ).isEqualTo( reportEntry.getSourceText() );
+        assertThat( decodedReportEntry.getName() ).isEqualTo( reportEntry.getName() );
+        assertThat( decodedReportEntry.getNameText() ).isEqualTo( reportEntry.getNameText() );
+        assertThat( decodedReportEntry.getGroup() ).isEqualTo( reportEntry.getGroup() );
+        assertThat( decodedReportEntry.getMessage() ).isEqualTo( reportEntry.getMessage() );
+        assertThat( decodedReportEntry.getElapsed() ).isEqualTo( 1003 );
+        assertThat( decodedReportEntry.getStackTraceWriter() ).isNotNull();
+        assertThat( decodedReportEntry.getStackTraceWriter().getThrowable().getMessage() ).isNotNull();
+        assertThat( decodedReportEntry.getStackTraceWriter().getThrowable().getMessage() )
+            .isEqualTo( exceptionMessage );
+        assertThat( decodedReportEntry.getStackTraceWriter().smartTrimmedStackTrace() )
+            .isEqualTo( smartStackTrace );
+        assertThat( decodedReportEntry.getStackTraceWriter().writeTraceToString() ).isEqualTo( trimmedStackTrace );
+        assertThat( decodedReportEntry.getStackTraceWriter().writeTrimmedTraceToString() )
+            .isEqualTo( trimmedStackTrace );
+    }
+
+    private static class Channel implements ReadableByteChannel
+    {
+        private final byte[] bytes;
+        private final int chunkSize;
+        protected int i;
+
+        Channel( byte[] bytes, int chunkSize )
+        {
+            this.bytes = bytes;
+            this.chunkSize = chunkSize;
+        }
+
+        @Override
+        public int read( ByteBuffer dst )
+        {
+            if ( i == bytes.length )
+            {
+                return -1;
+            }
+            else if ( dst.hasRemaining() )
+            {
+                int length = min( min( chunkSize, bytes.length - i ), dst.remaining() ) ;
+                dst.put( bytes, i, length );
+                i += length;
+                return length;
+            }
+            else
+            {
+                return 0;
+            }
+        }
+
+        @Override
+        public boolean isOpen()
+        {
+            return false;
+        }
+
+        @Override
+        public void close()
+        {
+        }
+    }
+
+    private static class MockForkNodeArguments implements ForkNodeArguments
+    {
+        @Nonnull
+        @Override
+        public String getSessionId()
+        {
+            return null;
+        }
+
+        @Override
+        public int getForkChannelId()
+        {
+            return 0;
+        }
+
+        @Nonnull
+        @Override
+        public File dumpStreamText( @Nonnull String text )
+        {
+            return null;
+        }
+
+        @Nonnull
+        @Override
+        public File dumpStreamException( @Nonnull Throwable t )
+        {
+            return null;
+        }
+
+        @Override
+        public void logWarningAtEnd( @Nonnull String text )
+        {
+        }
+
+        @Nonnull
+        @Override
+        public ConsoleLogger getConsoleLogger()
+        {
+            return null;
+        }
+
+        @Override
+        public File getEventStreamBinaryFile()
+        {
+            return null;
+        }
+
+        @Override
+        public File getCommandStreamBinaryFile()
+        {
+            return null;
+        }
+    }
+
+}
diff --git a/maven-surefire-plugin/src/site/fml/faq.fml b/maven-surefire-plugin/src/site/fml/faq.fml
index e5aaada..2464d08 100644
--- a/maven-surefire-plugin/src/site/fml/faq.fml
+++ b/maven-surefire-plugin/src/site/fml/faq.fml
@@ -138,7 +138,7 @@ under the License.
       </answer>
     </faq>
     <faq id="corruptedstream">
-      <question>Corrupted STDOUT by directly writing to native stream in forked JVM</question>
+      <question>Corrupted channel by directly writing to native stream in forked JVM</question>
       <answer>
         <p>
         If your tests use native library which prints to STDOUT this warning message appears because the library
@@ -149,7 +149,7 @@ under the License.
         This warning message appears if you use <em>FileDescriptor.out</em> or JVM prints GC summary.
         <br/>
         In that case the warning is printed
-        <em>"Corrupted STDOUT by directly writing to native stream in forked JVM"</em>, and a dump file can be found
+        <em>"Corrupted channel by directly writing to native stream in forked JVM"</em>, and a dump file can be found
         in Reports directory.
         <br/>
         If debug level is enabled then messages of corrupted stream appear in the console.
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/api/booter/Constants.java b/surefire-api/src/main/java/org/apache/maven/surefire/api/booter/Constants.java
index bcb7b17..639aba8 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/api/booter/Constants.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/api/booter/Constants.java
@@ -29,8 +29,10 @@ import static java.nio.charset.StandardCharsets.UTF_8;
  */
 public final class Constants
 {
-    public static final String MAGIC_NUMBER = "maven-surefire-event";
-    public static final byte[] MAGIC_NUMBER_BYTES = MAGIC_NUMBER.getBytes( US_ASCII );
+    private static final String MAGIC_NUMBER_FOR_EVENTS = "maven-surefire-event";
+    public static final String MAGIC_NUMBER_FOR_COMMANDS = "maven-surefire-command";
+    public static final byte[] MAGIC_NUMBER_FOR_EVENTS_BYTES = MAGIC_NUMBER_FOR_EVENTS.getBytes( US_ASCII );
+    public static final byte[] MAGIC_NUMBER_FOR_COMMANDS_BYTES = MAGIC_NUMBER_FOR_COMMANDS.getBytes( US_ASCII );
     public static final Charset DEFAULT_STREAM_ENCODING = UTF_8;
     public static final byte[] DEFAULT_STREAM_ENCODING_BYTES = UTF_8.name().getBytes( US_ASCII );
 }
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/api/booter/DumpErrorSingleton.java b/surefire-api/src/main/java/org/apache/maven/surefire/api/booter/DumpErrorSingleton.java
index b54bb2e..791dae5 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/api/booter/DumpErrorSingleton.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/api/booter/DumpErrorSingleton.java
@@ -40,6 +40,7 @@ public final class DumpErrorSingleton
 
     private File dumpFile;
     private File dumpStreamFile;
+    private File binaryDumpStreamFile;
 
     private DumpErrorSingleton()
     {
@@ -54,36 +55,50 @@ public final class DumpErrorSingleton
     {
         dumpFile = createDumpFile( reportsDir, dumpFileName );
         dumpStreamFile = createDumpStreamFile( reportsDir, dumpFileName );
+        String fileNameWithoutExtension =
+            dumpFileName.contains( "." ) ? dumpFileName.substring( 0, dumpFileName.lastIndexOf( '.' ) ) : dumpFileName;
+        binaryDumpStreamFile = newDumpFile( reportsDir, fileNameWithoutExtension + "-commands.bin" );
     }
 
-    public synchronized void dumpException( Throwable t, String msg )
+    public synchronized File dumpException( Throwable t, String msg )
     {
         DumpFileUtils.dumpException( t, msg == null ? "null" : msg, dumpFile );
+        return dumpFile;
     }
 
-    public synchronized void dumpException( Throwable t )
+    public synchronized File dumpException( Throwable t )
     {
         DumpFileUtils.dumpException( t, dumpFile );
+        return dumpFile;
     }
 
-    public synchronized void dumpText( String msg )
+    public synchronized File dumpText( String msg )
     {
         DumpFileUtils.dumpText( msg == null ? "null" : msg, dumpFile );
+        return dumpFile;
     }
 
-    public synchronized void dumpStreamException( Throwable t, String msg )
+    public synchronized File dumpStreamException( Throwable t, String msg )
     {
         DumpFileUtils.dumpException( t, msg == null ? "null" : msg, dumpStreamFile );
+        return dumpStreamFile;
     }
 
-    public synchronized void dumpStreamException( Throwable t )
+    public synchronized File dumpStreamException( Throwable t )
     {
         DumpFileUtils.dumpException( t, dumpStreamFile );
+        return dumpStreamFile;
     }
 
-    public synchronized void dumpStreamText( String msg )
+    public synchronized File dumpStreamText( String msg )
     {
         DumpFileUtils.dumpText( msg == null ? "null" : msg, dumpStreamFile );
+        return dumpStreamFile;
+    }
+
+    public File getCommandStreamBinaryFile()
+    {
+        return binaryDumpStreamFile;
     }
 
     private File createDumpFile( File reportsDir, String dumpFileName )
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/api/booter/ForkingRunListener.java b/surefire-api/src/main/java/org/apache/maven/surefire/api/booter/ForkingRunListener.java
index 7cf1dea..169f358 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/api/booter/ForkingRunListener.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/api/booter/ForkingRunListener.java
@@ -71,7 +71,7 @@ public class ForkingRunListener
     @Override
     public void testSetCompleted( TestSetReportEntry report )
     {
-        target.sendSystemProperties( report.getSystemProperties() );
+        target.systemProperties( report.getSystemProperties() );
         target.testSetCompleted( report, trim );
     }
 
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/api/booter/MasterProcessChannelEncoder.java b/surefire-api/src/main/java/org/apache/maven/surefire/api/booter/MasterProcessChannelEncoder.java
index e8ad45b..cb543f6 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/api/booter/MasterProcessChannelEncoder.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/api/booter/MasterProcessChannelEncoder.java
@@ -32,15 +32,11 @@ import java.util.Map;
  */
 public interface MasterProcessChannelEncoder
 {
-    MasterProcessChannelEncoder asRerunMode();
-
-    MasterProcessChannelEncoder asNormalMode();
-
     boolean checkError();
 
     void onJvmExit();
 
-    void sendSystemProperties( Map<String, String> sysProps );
+    void systemProperties( Map<String, String> sysProps );
 
     void testSetStarting( ReportEntry reportEntry, boolean trimStackTraces );
 
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/api/booter/MasterProcessCommand.java b/surefire-api/src/main/java/org/apache/maven/surefire/api/booter/MasterProcessCommand.java
index 64c392d..f492339 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/api/booter/MasterProcessCommand.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/api/booter/MasterProcessCommand.java
@@ -19,6 +19,12 @@ package org.apache.maven.surefire.api.booter;
  * under the License.
  */
 
+import org.apache.maven.surefire.api.stream.AbstractStreamDecoder.Segment;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
 import static java.util.Objects.requireNonNull;
 
 /**
@@ -29,24 +35,39 @@ import static java.util.Objects.requireNonNull;
  */
 public enum MasterProcessCommand
 {
-    RUN_CLASS( String.class ),
-    TEST_SET_FINISHED( Void.class ),
-    SKIP_SINCE_NEXT_TEST( Void.class ),
-    SHUTDOWN( String.class ),
+    RUN_CLASS( "run-testclass", String.class ),
+    TEST_SET_FINISHED( "testset-finished", Void.class ),
+    SKIP_SINCE_NEXT_TEST( "skip-since-next-test", Void.class ),
+    SHUTDOWN( "shutdown", String.class ),
 
     /** To tell a forked process that the master process is still alive. Repeated after 10 seconds. */
-    NOOP( Void.class ),
-    BYE_ACK( Void.class );
+    NOOP( "noop", Void.class ),
+    BYE_ACK( "bye-ack", Void.class );
 
-    public static final String MAGIC_NUMBER = "maven-surefire-command";
+    // due to have fast and thread-safe Map
+    public static final Map<Segment, MasterProcessCommand> COMMAND_TYPES = segmentsToCmds();
 
+    private final String opcode;
+    private final byte[] opcodeBinary;
     private final Class<?> dataType;
 
-    MasterProcessCommand( Class<?> dataType )
+    MasterProcessCommand( String opcode, Class<?> dataType )
     {
+        this.opcode = requireNonNull( opcode, "value cannot be null" );
+        opcodeBinary = opcode.getBytes( US_ASCII );
         this.dataType = requireNonNull( dataType, "dataType cannot be null" );
     }
 
+    public byte[] getOpcodeBinary()
+    {
+        return opcodeBinary;
+    }
+
+    public int getOpcodeLength()
+    {
+        return opcodeBinary.length;
+    }
+
     public Class<?> getDataType()
     {
         return dataType;
@@ -56,4 +77,21 @@ public enum MasterProcessCommand
     {
         return dataType != Void.class;
     }
+
+    @Override
+    public String toString()
+    {
+        return opcode;
+    }
+
+    private static Map<Segment, MasterProcessCommand> segmentsToCmds()
+    {
+        Map<Segment, MasterProcessCommand> commands = new HashMap<>();
+        for ( MasterProcessCommand command : MasterProcessCommand.values() )
+        {
+            byte[] array = command.toString().getBytes( US_ASCII );
+            commands.put( new Segment( array, 0, array.length ), command );
+        }
+        return commands;
+    }
 }
diff --git a/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/ForkNodeArguments.java b/surefire-api/src/main/java/org/apache/maven/surefire/api/fork/ForkNodeArguments.java
similarity index 92%
rename from surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/ForkNodeArguments.java
rename to surefire-api/src/main/java/org/apache/maven/surefire/api/fork/ForkNodeArguments.java
index 4ab6cc3..7d20e0e 100644
--- a/surefire-extensions-api/src/main/java/org/apache/maven/surefire/extensions/ForkNodeArguments.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/api/fork/ForkNodeArguments.java
@@ -1,4 +1,4 @@
-package org.apache.maven.surefire.extensions;
+package org.apache.maven.surefire.api.fork;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -54,4 +54,8 @@ public interface ForkNodeArguments
 
     @Nonnull
     ConsoleLogger getConsoleLogger();
+
+    File getEventStreamBinaryFile();
+
+    File getCommandStreamBinaryFile();
 }
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/api/report/RunMode.java b/surefire-api/src/main/java/org/apache/maven/surefire/api/report/RunMode.java
index 7329f9c..dbdd580 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/api/report/RunMode.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/api/report/RunMode.java
@@ -19,6 +19,11 @@ package org.apache.maven.surefire.api.report;
  * under the License.
  */
 
+import org.apache.maven.surefire.api.stream.AbstractStreamDecoder.Segment;
+
+import java.util.HashMap;
+import java.util.Map;
+
 import static java.nio.charset.StandardCharsets.US_ASCII;
 
 /**
@@ -35,6 +40,9 @@ public enum RunMode
     RERUN_TEST_AFTER_FAILURE( "rerun-test-after-failure" );
     //todo add here RERUN_TESTSET, see https://github.com/apache/maven-surefire/pull/221
 
+    // due to have fast and thread-safe Map
+    public static final Map<Segment, RunMode> RUN_MODES = segmentsToRunModes();
+
     private final String runmode;
     private final byte[] runmodeBinary;
 
@@ -53,4 +61,15 @@ public enum RunMode
     {
         return runmodeBinary;
     }
+
+    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;
+    }
 }
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/api/stream/AbstractStreamDecoder.java b/surefire-api/src/main/java/org/apache/maven/surefire/api/stream/AbstractStreamDecoder.java
new file mode 100644
index 0000000..862820c
--- /dev/null
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/api/stream/AbstractStreamDecoder.java
@@ -0,0 +1,735 @@
+package org.apache.maven.surefire.api.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.log.api.ConsoleLogger;
+import org.apache.maven.surefire.api.fork.ForkNodeArguments;
+import org.apache.maven.surefire.api.report.RunMode;
+
+import javax.annotation.Nonnegative;
+import javax.annotation.Nonnull;
+import java.io.EOFException;
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CoderResult;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+import static java.nio.charset.CodingErrorAction.REPLACE;
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static java.util.Arrays.copyOf;
+import static org.apache.maven.surefire.api.booter.Constants.DEFAULT_STREAM_ENCODING;
+import static org.apache.maven.surefire.api.stream.AbstractStreamDecoder.StreamReadStatus.OVERFLOW;
+import static org.apache.maven.surefire.api.stream.AbstractStreamDecoder.StreamReadStatus.UNDERFLOW;
+import static org.apache.maven.surefire.shared.lang3.StringUtils.isBlank;
+
+/**
+ * @param <M> message object
+ * @param <MT> enum describing the meaning of the message
+ * @param <ST> enum for segment type
+ */
+public abstract class AbstractStreamDecoder<M, MT extends Enum<MT>, ST extends Enum<ST>> implements AutoCloseable
+{
+    public static final int BUFFER_SIZE = 1024;
+
+    private static final String PRINTABLE_JVM_NATIVE_STREAM = "Listening for transport dt_socket at address:";
+
+    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 byte[] DEFAULT_STREAM_ENCODING_BYTES = DEFAULT_STREAM_ENCODING.name().getBytes( US_ASCII );
+
+    private static final int NO_POSITION = -1;
+    private static final int DELIMITER_LENGTH = 1;
+    private static final int BYTE_LENGTH = 1;
+    private static final int INT_LENGTH = 4;
+
+    private final ReadableByteChannel channel;
+    private final ForkNodeArguments arguments;
+    private final Map<Segment, MT> messageTypes;
+    private final ConsoleLogger logger;
+
+    protected AbstractStreamDecoder( @Nonnull ReadableByteChannel channel,
+                                     @Nonnull ForkNodeArguments arguments,
+                                     @Nonnull Map<Segment, MT> messageTypes )
+    {
+        this.channel = channel;
+        this.arguments = arguments;
+        this.messageTypes = messageTypes;
+        logger = arguments.getConsoleLogger();
+    }
+
+    public abstract M decode( @Nonnull Memento memento ) throws MalformedChannelException, IOException;
+
+    @Nonnull
+    protected abstract byte[] getEncodedMagicNumber();
+
+    @Nonnull
+    protected abstract ST[] nextSegmentType( @Nonnull MT messageType );
+
+    @Nonnull
+    protected abstract M toMessage( @Nonnull MT messageType, RunMode runMode, @Nonnull Memento memento )
+        throws MalformedFrameException;
+
+    @Nonnull
+    protected final ForkNodeArguments getArguments()
+    {
+        return arguments;
+    }
+
+    protected void debugStream( byte[] array, int position, int remaining )
+    {
+    }
+
+    protected MT readMessageType( @Nonnull Memento memento ) throws IOException, MalformedFrameException
+    {
+        byte[] header = getEncodedMagicNumber();
+        int readCount = DELIMITER_LENGTH + header.length + DELIMITER_LENGTH + BYTE_LENGTH + DELIMITER_LENGTH;
+        read( memento, readCount );
+        checkHeader( memento );
+        return messageTypes.get( readSegment( memento ) );
+    }
+
+    @Nonnull
+    @SuppressWarnings( "checkstyle:magicnumber" )
+    protected Segment readSegment( @Nonnull Memento memento ) throws IOException, MalformedFrameException
+    {
+        int readCount = readByte( memento ) & 0xff;
+        read( memento, readCount + DELIMITER_LENGTH );
+        ByteBuffer bb = memento.getByteBuffer();
+        Segment segment = new Segment( bb.array(), bb.arrayOffset() + bb.position(), readCount );
+        bb.position( bb.position() + readCount );
+        checkDelimiter( memento );
+        return segment;
+    }
+
+    @Nonnull
+    @SuppressWarnings( "checkstyle:magicnumber" )
+    protected Charset readCharset( @Nonnull Memento memento ) throws IOException, MalformedFrameException
+    {
+        int length = readByte( memento ) & 0xff;
+        read( memento, length + DELIMITER_LENGTH );
+        ByteBuffer bb = memento.getByteBuffer();
+        byte[] array = bb.array();
+        int offset = bb.arrayOffset() + bb.position();
+        bb.position( bb.position() + length );
+        boolean isDefaultEncoding = false;
+        if ( length == DEFAULT_STREAM_ENCODING_BYTES.length )
+        {
+            isDefaultEncoding = true;
+            for ( int i = 0; i < length; i++ )
+            {
+                isDefaultEncoding &= DEFAULT_STREAM_ENCODING_BYTES[i] == array[offset + i];
+            }
+        }
+
+        try
+        {
+            Charset charset =
+                isDefaultEncoding
+                    ? DEFAULT_STREAM_ENCODING
+                    : Charset.forName( new String( array, offset, length, US_ASCII ) );
+
+            checkDelimiter( memento );
+            return charset;
+        }
+        catch ( IllegalArgumentException e )
+        {
+            throw new MalformedFrameException( memento.getLine().getPositionByteBuffer(), bb.position() );
+        }
+    }
+
+    protected String readString( @Nonnull Memento memento ) throws IOException, MalformedFrameException
+    {
+        memento.getCharBuffer().clear();
+        int readCount = readInt( memento );
+        if ( readCount < 0 )
+        {
+            throw new MalformedFrameException( memento.getLine().getPositionByteBuffer(),
+                memento.getByteBuffer().position() );
+        }
+        read( memento, readCount + DELIMITER_LENGTH );
+
+        final String string;
+        if ( readCount == 0 )
+        {
+            string = "";
+        }
+        else if ( readCount == 1 )
+        {
+            read( memento, 1 );
+            byte oneChar = memento.getByteBuffer().get();
+            string = oneChar == 0 ? null : String.valueOf( (char) oneChar );
+        }
+        else
+        {
+            string = readString( memento, readCount );
+        }
+        read( memento, 1 );
+        checkDelimiter( memento );
+        return string;
+    }
+
+    protected Integer readInteger( @Nonnull Memento memento ) throws IOException, MalformedFrameException
+    {
+        read( memento, BYTE_LENGTH );
+        boolean isNullObject = memento.getByteBuffer().get() == 0;
+        if ( isNullObject )
+        {
+            read( memento, DELIMITER_LENGTH );
+            checkDelimiter( memento );
+            return null;
+        }
+        return readInt( memento );
+    }
+
+    protected byte readByte( @Nonnull Memento memento ) throws IOException, MalformedFrameException
+    {
+        read( memento, BYTE_LENGTH + DELIMITER_LENGTH );
+        byte b = memento.getByteBuffer().get();
+        checkDelimiter( memento );
+        return b;
+    }
+
+    protected int readInt( @Nonnull Memento memento ) throws IOException, MalformedFrameException
+    {
+        read( memento, INT_LENGTH + DELIMITER_LENGTH );
+        int i = memento.getByteBuffer().getInt();
+        checkDelimiter( memento );
+        return i;
+    }
+
+    @SuppressWarnings( "checkstyle:magicnumber" )
+    protected final void checkDelimiter( Memento memento ) throws MalformedFrameException
+    {
+        ByteBuffer bb = memento.bb;
+        if ( ( 0xff & bb.get() ) != ':' )
+        {
+            throw new MalformedFrameException( memento.getLine().getPositionByteBuffer(), bb.position() );
+        }
+    }
+
+    protected final void checkHeader( Memento memento ) throws MalformedFrameException
+    {
+        ByteBuffer bb = memento.bb;
+
+        checkDelimiter( memento );
+
+        int shift = 0;
+        try
+        {
+            byte[] header = getEncodedMagicNumber();
+            for ( int start = bb.arrayOffset() + bb.position(), length = header.length; shift < length; shift++ )
+            {
+                if ( bb.array()[shift + start] != header[shift] )
+                {
+                    throw new MalformedFrameException( memento.getLine().getPositionByteBuffer(),
+                        bb.position() + shift );
+                }
+            }
+        }
+        finally
+        {
+            bb.position( bb.position() + shift );
+        }
+
+        checkDelimiter( memento );
+    }
+
+    protected void checkArguments( RunMode runMode, Memento memento, int expectedDataElements )
+        throws MalformedFrameException
+    {
+        if ( runMode == null )
+        {
+            throw new MalformedFrameException( memento.getLine().getPositionByteBuffer(),
+                memento.getByteBuffer().position() );
+        }
+        checkArguments( memento, expectedDataElements );
+    }
+
+    protected void checkArguments( Memento memento, int expectedDataElements )
+        throws MalformedFrameException
+    {
+        if ( memento.getData().size() != expectedDataElements )
+        {
+            throw new MalformedFrameException( memento.getLine().getPositionByteBuffer(),
+                memento.getByteBuffer().position() );
+        }
+    }
+
+    private String readString( @Nonnull final Memento memento, @Nonnegative final int totalBytes )
+        throws IOException, MalformedFrameException
+    {
+        memento.getDecoder().reset();
+        final CharBuffer output = memento.getCharBuffer();
+        output.clear();
+        final ByteBuffer input = memento.getByteBuffer();
+        final List<String> strings = new ArrayList<>();
+        int countDecodedBytes = 0;
+        for ( boolean endOfInput = false; !endOfInput; )
+        {
+            final int bytesToRead = totalBytes - countDecodedBytes;
+            read( memento, bytesToRead );
+            int bytesToDecode = min( input.remaining(), bytesToRead );
+            final boolean isLastChunk = bytesToDecode == bytesToRead;
+            endOfInput = countDecodedBytes + bytesToDecode >= totalBytes;
+            do
+            {
+                boolean endOfChunk = output.remaining() >= bytesToRead;
+                boolean endOfOutput = isLastChunk && endOfChunk;
+                int readInputBytes = decodeString( memento.getDecoder(), input, output, bytesToDecode, endOfOutput,
+                    memento.getLine().getPositionByteBuffer() );
+                bytesToDecode -= readInputBytes;
+                countDecodedBytes += readInputBytes;
+            }
+            while ( isLastChunk && bytesToDecode > 0 && output.hasRemaining() );
+
+            if ( isLastChunk || !output.hasRemaining() )
+            {
+                strings.add( output.flip().toString() );
+                output.clear();
+            }
+        }
+
+        memento.getDecoder().reset();
+        output.clear();
+
+        return toString( strings );
+    }
+
+    private static int decodeString( @Nonnull CharsetDecoder decoder, @Nonnull ByteBuffer input,
+                                     @Nonnull CharBuffer output, @Nonnegative int bytesToDecode,
+                                     boolean endOfInput, @Nonnegative int errorStreamFrom )
+        throws MalformedFrameException
+    {
+        int limit = input.limit();
+        input.limit( input.position() + bytesToDecode );
+
+        CoderResult result = decoder.decode( input, output, endOfInput );
+        if ( result.isError() || result.isMalformed() )
+        {
+            throw new MalformedFrameException( errorStreamFrom, input.position() );
+        }
+
+        int decodedBytes = bytesToDecode - input.remaining();
+        input.limit( limit );
+        return decodedBytes;
+    }
+
+    private static String toString( List<String> strings )
+    {
+        if ( strings.size() == 1 )
+        {
+            return strings.get( 0 );
+        }
+        StringBuilder concatenated = new StringBuilder( strings.size() * BUFFER_SIZE );
+        for ( String s : strings )
+        {
+            concatenated.append( s );
+        }
+        return concatenated.toString();
+    }
+
+    private void printCorruptedStream( Memento memento )
+    {
+        ByteBuffer bb = memento.getByteBuffer();
+        if ( bb.hasRemaining() )
+        {
+            int bytesToWrite = bb.remaining();
+            memento.getLine().write( bb, bb.position(), bytesToWrite );
+            bb.position( bb.position() + bytesToWrite );
+        }
+    }
+
+    /**
+     * Print the last string which has not been finished by a new line character.
+     *
+     * @param memento current memento object
+     */
+    protected final void printRemainingStream( Memento memento )
+    {
+        printCorruptedStream( memento );
+        memento.getLine().printExistingLine();
+        memento.getLine().setCount( 0 );
+    }
+
+    /**
+     *
+     */
+    public static final class Segment
+    {
+        private final byte[] array;
+        private final int fromIndex;
+        private final int length;
+        private final int hashCode;
+
+        public Segment( byte[] array, int fromIndex, int length )
+        {
+            this.array = array;
+            this.fromIndex = fromIndex;
+            this.length = length;
+
+            int hashCode = 0;
+            int i = fromIndex;
+            for ( int loops = length >> 1; loops-- != 0; )
+            {
+                hashCode = 31 * hashCode + array[i++];
+                hashCode = 31 * hashCode + array[i++];
+            }
+            this.hashCode = i == fromIndex + length ? hashCode : 31 * hashCode + array[i];
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return hashCode;
+        }
+
+        @Override
+        public boolean equals( Object obj )
+        {
+            if ( !( obj instanceof Segment ) )
+            {
+                return false;
+            }
+
+            Segment that = (Segment) obj;
+            if ( that.length != length )
+            {
+                return false;
+            }
+
+            for ( int i = 0; i < length; i++ )
+            {
+                if ( that.array[that.fromIndex + i] != array[fromIndex + i] )
+                {
+                    return false;
+                }
+            }
+            return true;
+        }
+    }
+
+    protected @Nonnull StreamReadStatus read( @Nonnull Memento memento, int recommendedCount ) throws IOException
+    {
+        ByteBuffer buffer = memento.getByteBuffer();
+        if ( buffer.remaining() >= recommendedCount && buffer.limit() != 0 )
+        {
+            return OVERFLOW;
+        }
+        else
+        {
+            if ( buffer.position() != 0 && recommendedCount > buffer.capacity() - buffer.position() )
+            {
+                buffer.compact().flip();
+                memento.getLine().setPositionByteBuffer( 0 );
+            }
+            int mark = buffer.position();
+            buffer.position( buffer.limit() );
+            buffer.limit( min( buffer.position() + recommendedCount, buffer.capacity() ) );
+            boolean isEnd = false;
+            while ( !isEnd && buffer.position() - mark < recommendedCount && buffer.position() < buffer.limit() )
+            {
+                isEnd = channel.read( buffer ) == -1;
+            }
+
+            buffer.limit( buffer.position() );
+            buffer.position( mark );
+            int readBytes = buffer.remaining();
+
+            if ( isEnd && readBytes < recommendedCount )
+            {
+                throw new EOFException();
+            }
+            else
+            {
+                debugStream( buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining() );
+                return readBytes >= recommendedCount ? OVERFLOW : UNDERFLOW;
+            }
+        }
+    }
+
+    /**
+     *
+     */
+    public final class Memento
+    {
+        private CharsetDecoder currentDecoder;
+        private final CharsetDecoder defaultDecoder;
+        private final BufferedStream line = new BufferedStream( 32 );
+        private final List<Object> data = new ArrayList<>();
+        private final CharBuffer cb = CharBuffer.allocate( BUFFER_SIZE );
+        private final ByteBuffer bb = ByteBuffer.allocate( BUFFER_SIZE );
+
+        public Memento()
+        {
+            defaultDecoder = DEFAULT_STREAM_ENCODING.newDecoder()
+                .onMalformedInput( REPLACE )
+                .onUnmappableCharacter( REPLACE );
+            bb.limit( 0 );
+        }
+
+        public void reset()
+        {
+            currentDecoder = null;
+            data.clear();
+        }
+
+        public CharsetDecoder getDecoder()
+        {
+            return currentDecoder == null ? defaultDecoder : currentDecoder;
+        }
+
+        public void setCharset( Charset charset )
+        {
+            if ( charset.name().equals( defaultDecoder.charset().name() ) )
+            {
+                currentDecoder = defaultDecoder;
+            }
+            else
+            {
+                currentDecoder = charset.newDecoder()
+                    .onMalformedInput( REPLACE )
+                    .onUnmappableCharacter( REPLACE );
+            }
+        }
+
+        public BufferedStream getLine()
+        {
+            return line;
+        }
+
+        public List<Object> getData()
+        {
+            return data;
+        }
+
+        public CharBuffer getCharBuffer()
+        {
+            return cb;
+        }
+
+        public ByteBuffer getByteBuffer()
+        {
+            return bb;
+        }
+    }
+
+    /**
+     * This class avoids locking which gains the performance of this decoder.
+     */
+    public final class BufferedStream
+    {
+        private byte[] buffer;
+        private int count;
+        private int positionByteBuffer;
+        private boolean isNewLine;
+
+        BufferedStream( int capacity )
+        {
+            this.buffer = new byte[capacity];
+        }
+
+        public int getPositionByteBuffer()
+        {
+            return positionByteBuffer;
+        }
+
+        public void setPositionByteBuffer( int positionByteBuffer )
+        {
+            this.positionByteBuffer = positionByteBuffer;
+        }
+
+        public void write( ByteBuffer bb, int position, int length )
+        {
+            ensureCapacity( length );
+            byte[] array = bb.array();
+            int pos = bb.arrayOffset() + position;
+            while ( length-- > 0 )
+            {
+                positionByteBuffer++;
+                byte b = array[pos++];
+                if ( b == '\r' || b == '\n' )
+                {
+                    if ( !isNewLine )
+                    {
+                        printExistingLine();
+                        count = 0;
+                    }
+                    isNewLine = true;
+                }
+                else
+                {
+                    buffer[count++] = b;
+                    isNewLine = false;
+                }
+            }
+        }
+
+        public void setCount( int count )
+        {
+            this.count = count;
+        }
+
+        @Override
+        public String toString()
+        {
+            return new String( buffer, 0, count, DEFAULT_STREAM_ENCODING );
+        }
+
+        private boolean isEmpty()
+        {
+            return count == 0;
+        }
+
+        private void ensureCapacity( int addCapacity )
+        {
+            int oldCapacity = buffer.length;
+            int exactCapacity = count + addCapacity;
+            if ( exactCapacity < 0 )
+            {
+                throw new OutOfMemoryError();
+            }
+
+            if ( oldCapacity < exactCapacity )
+            {
+                int newCapacity = oldCapacity << 1;
+                buffer = copyOf( buffer, max( newCapacity, exactCapacity ) );
+            }
+        }
+
+        public void printExistingLine()
+        {
+            if ( isEmpty() )
+            {
+                return;
+            }
+
+            String s = toString();
+            if ( isBlank( s ) )
+            {
+                return;
+            }
+
+            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 );
+                }
+                else if ( logger.isDebugEnabled() )
+                {
+                    logger.debug( s );
+                }
+
+                String msg = "Corrupted channel by directly writing to native stream in forked JVM "
+                    + arguments.getForkChannelId() + ".";
+                File dumpFile = arguments.dumpStreamText( msg + " Stream '" + s + "'." );
+                String dumpPath = dumpFile.getAbsolutePath();
+                arguments.logWarningAtEnd( msg + " See FAQ web page and the dump file " + dumpPath );
+            }
+        }
+
+        private boolean isJvmError( String line )
+        {
+            String lineLower = line.toLowerCase();
+            for ( String errorPattern : JVM_ERROR_PATTERNS )
+            {
+                if ( lineLower.contains( errorPattern ) )
+                {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
+    /**
+     *
+     */
+    public static final class MalformedFrameException extends Exception
+    {
+        private final int readFrom;
+        private final int readTo;
+
+        public MalformedFrameException( int readFrom, int readTo )
+        {
+            this.readFrom = readFrom;
+            this.readTo = readTo;
+        }
+
+        public int readFrom()
+        {
+            return readFrom;
+        }
+
+        public int readTo()
+        {
+            return readTo;
+        }
+
+        public boolean hasValidPositions()
+        {
+            return readFrom != NO_POSITION && readTo != NO_POSITION && readTo - readFrom > 0;
+        }
+    }
+
+    /**
+     * Underflow - could not completely read out al bytes in one call.
+     * <br>
+     * Overflow - read all bytes or more
+     * <br>
+     * EOF - end of stream
+     */
+    public enum StreamReadStatus
+    {
+        UNDERFLOW,
+        OVERFLOW,
+        EOF
+    }
+}
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/api/stream/AbstractStreamEncoder.java b/surefire-api/src/main/java/org/apache/maven/surefire/api/stream/AbstractStreamEncoder.java
new file mode 100644
index 0000000..d033958
--- /dev/null
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/api/stream/AbstractStreamEncoder.java
@@ -0,0 +1,191 @@
+package org.apache.maven.surefire.api.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.surefire.api.report.RunMode;
+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.WritableByteChannel;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
+
+import static java.lang.Math.ceil;
+import static java.nio.CharBuffer.wrap;
+
+/**
+ * The base class of stream encoder.
+ * The type of message is expressed by opcode where the opcode object is described by the generic type {@link E}.
+ * @param <E> type of the message
+ */
+public abstract class AbstractStreamEncoder<E extends Enum<E>>
+{
+    private static final byte BOOLEAN_NON_NULL_OBJECT = (byte) 0xff;
+    private static final byte BOOLEAN_NULL_OBJECT = (byte) 0;
+    private static final byte[] INT_BINARY = new byte[] {0, 0, 0, 0};
+
+    private final WritableByteChannel out;
+
+    public AbstractStreamEncoder( WritableByteChannel out )
+    {
+        this.out = out;
+    }
+
+    @Nonnull
+    protected abstract byte[] getEncodedMagicNumber();
+
+    @Nonnull
+    protected abstract byte[] enumToByteArray( E e );
+
+    @Nonnull
+    protected abstract byte[] getEncodedCharsetName();
+
+    @Nonnull
+    protected abstract Charset getCharset();
+
+    @Nonnull
+    protected abstract CharsetEncoder newCharsetEncoder();
+
+    protected void write( ByteBuffer frame, boolean sendImmediately )
+        throws IOException
+    {
+        if ( !sendImmediately && out instanceof WritableBufferedByteChannel )
+        {
+            ( (WritableBufferedByteChannel) out ).writeBuffered( frame );
+        }
+        else
+        {
+            out.write( frame );
+        }
+    }
+
+    public void encodeHeader( ByteBuffer result, E operation, RunMode runMode )
+    {
+        result.put( (byte) ':' );
+        result.put( getEncodedMagicNumber() );
+        result.put( (byte) ':' );
+        byte[] opcode = enumToByteArray( operation );
+        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) ':' );
+        }
+    }
+
+    public void encodeCharset( ByteBuffer result )
+    {
+        byte[] charsetNameBinary = getEncodedCharsetName();
+        result.put( (byte) charsetNameBinary.length );
+        result.put( (byte) ':' );
+        result.put( charsetNameBinary );
+        result.put( (byte) ':' );
+    }
+
+    public 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) ':' );
+    }
+
+    public void encodeInteger( ByteBuffer result, Integer i )
+    {
+        if ( i == null )
+        {
+            result.put( BOOLEAN_NULL_OBJECT );
+        }
+        else
+        {
+            result.put( BOOLEAN_NON_NULL_OBJECT ).putInt( i );
+        }
+        result.put( (byte) ':' );
+    }
+
+    public void encode( CharsetEncoder encoder, ByteBuffer result, E operation, RunMode runMode, String... messages )
+    {
+        encodeHeader( result, operation, runMode );
+        encodeCharset( result );
+        for ( String message : messages )
+        {
+            encodeString( encoder, result, message );
+        }
+    }
+
+    public int estimateBufferLength( int opcodeLength, 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 + getEncodedMagicNumber().length + 1 + 1 + 1 + opcodeLength + 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 + 1 + 4 ) * integersCounter;
+
+        for ( String string : strings )
+        {
+            String s = nonNull( 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 String nonNull( String msg )
+    {
+        return msg == null ? "\u0000" : msg;
+    }
+}
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/api/booter/Constants.java b/surefire-api/src/main/java/org/apache/maven/surefire/api/stream/MalformedChannelException.java
similarity index 60%
copy from surefire-api/src/main/java/org/apache/maven/surefire/api/booter/Constants.java
copy to surefire-api/src/main/java/org/apache/maven/surefire/api/stream/MalformedChannelException.java
index bcb7b17..cf4468f 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/api/booter/Constants.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/api/stream/MalformedChannelException.java
@@ -1,4 +1,4 @@
-package org.apache.maven.surefire.api.booter;
+package org.apache.maven.surefire.api.stream;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,18 +19,9 @@ package org.apache.maven.surefire.api.booter;
  * under the License.
  */
 
-import java.nio.charset.Charset;
-
-import static java.nio.charset.StandardCharsets.US_ASCII;
-import static java.nio.charset.StandardCharsets.UTF_8;
-
 /**
  *
  */
-public final class Constants
+public class MalformedChannelException extends Exception
 {
-    public static final String MAGIC_NUMBER = "maven-surefire-event";
-    public static final byte[] MAGIC_NUMBER_BYTES = MAGIC_NUMBER.getBytes( US_ASCII );
-    public static final Charset DEFAULT_STREAM_ENCODING = UTF_8;
-    public static final byte[] DEFAULT_STREAM_ENCODING_BYTES = UTF_8.name().getBytes( US_ASCII );
 }
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/api/booter/Constants.java b/surefire-api/src/main/java/org/apache/maven/surefire/api/stream/SegmentType.java
similarity index 60%
copy from surefire-api/src/main/java/org/apache/maven/surefire/api/booter/Constants.java
copy to surefire-api/src/main/java/org/apache/maven/surefire/api/stream/SegmentType.java
index bcb7b17..f81d2ce 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/api/booter/Constants.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/api/stream/SegmentType.java
@@ -1,4 +1,4 @@
-package org.apache.maven.surefire.api.booter;
+package org.apache.maven.surefire.api.stream;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,18 +19,17 @@ package org.apache.maven.surefire.api.booter;
  * under the License.
  */
 
-import java.nio.charset.Charset;
-
-import static java.nio.charset.StandardCharsets.US_ASCII;
-import static java.nio.charset.StandardCharsets.UTF_8;
-
 /**
  *
  */
-public final class Constants
+public enum SegmentType
 {
-    public static final String MAGIC_NUMBER = "maven-surefire-event";
-    public static final byte[] MAGIC_NUMBER_BYTES = MAGIC_NUMBER.getBytes( US_ASCII );
-    public static final Charset DEFAULT_STREAM_ENCODING = UTF_8;
-    public static final byte[] DEFAULT_STREAM_ENCODING_BYTES = UTF_8.name().getBytes( US_ASCII );
+    RUN_MODE,
+    STRING_ENCODING,
+    DATA_STRING,
+    DATA_INTEGER,
+    DATA_INT,
+    DATA_BYTE,
+    DATA_STRING_ARRAY,
+    END_OF_FRAME
 }
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/api/util/internal/DumpFileUtils.java b/surefire-api/src/main/java/org/apache/maven/surefire/api/util/internal/DumpFileUtils.java
index bac68be..b552aa2 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/api/util/internal/DumpFileUtils.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/api/util/internal/DumpFileUtils.java
@@ -52,6 +52,7 @@ public final class DumpFileUtils
      */
     public static synchronized File newDumpFile( File reportsDir, String dumpFileName )
     {
+        reportsDir.mkdirs();
         return new File( reportsDir, dumpFileName );
     }
 
diff --git a/surefire-api/src/test/java/org/apache/maven/JUnit4SuiteTest.java b/surefire-api/src/test/java/org/apache/maven/JUnit4SuiteTest.java
index 7ac3ba2..c707ed6 100644
--- a/surefire-api/src/test/java/org/apache/maven/JUnit4SuiteTest.java
+++ b/surefire-api/src/test/java/org/apache/maven/JUnit4SuiteTest.java
@@ -25,6 +25,8 @@ import org.apache.maven.surefire.api.runorder.ThreadedExecutionSchedulerTest;
 import org.apache.maven.surefire.SpecificTestClassFilterTest;
 import org.apache.maven.surefire.api.booter.ForkingRunListenerTest;
 import org.apache.maven.surefire.api.report.LegacyPojoStackTraceWriterTest;
+import org.apache.maven.surefire.api.stream.AbstractStreamDecoderTest;
+import org.apache.maven.surefire.api.stream.AbstractStreamEncoderTest;
 import org.apache.maven.surefire.api.suite.RunResultTest;
 import org.apache.maven.surefire.api.testset.FundamentalFilterTest;
 import org.apache.maven.surefire.api.testset.ResolvedTestTest;
@@ -68,7 +70,9 @@ import org.junit.runners.Suite;
     ReflectionUtilsTest.class,
     ChannelsReaderTest.class,
     ChannelsWriterTest.class,
-    AsyncSocketTest.class
+    AsyncSocketTest.class,
+    AbstractStreamEncoderTest.class,
+    AbstractStreamDecoderTest.class
 } )
 @RunWith( Suite.class )
 public class JUnit4SuiteTest
diff --git a/surefire-api/src/test/java/org/apache/maven/surefire/api/stream/AbstractStreamDecoderTest.java b/surefire-api/src/test/java/org/apache/maven/surefire/api/stream/AbstractStreamDecoderTest.java
new file mode 100644
index 0000000..5ab7ae0
--- /dev/null
+++ b/surefire-api/src/test/java/org/apache/maven/surefire/api/stream/AbstractStreamDecoderTest.java
@@ -0,0 +1,692 @@
+package org.apache.maven.surefire.api.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.log.api.ConsoleLogger;
+import org.apache.maven.surefire.api.booter.Constants;
+import org.apache.maven.surefire.api.booter.ForkedProcessEventType;
+import org.apache.maven.surefire.api.event.Event;
+import org.apache.maven.surefire.api.fork.ForkNodeArguments;
+import org.apache.maven.surefire.api.report.RunMode;
+import org.apache.maven.surefire.api.stream.AbstractStreamDecoder.MalformedFrameException;
+import org.apache.maven.surefire.api.stream.AbstractStreamDecoder.Memento;
+import org.apache.maven.surefire.api.stream.AbstractStreamDecoder.Segment;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import javax.annotation.Nonnull;
+import java.io.EOFException;
+import java.io.File;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.charset.CharsetDecoder;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import static java.lang.Math.min;
+import static java.lang.System.arraycopy;
+import static java.nio.charset.CodingErrorAction.REPLACE;
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Collections.singletonMap;
+import static org.apache.maven.surefire.api.booter.Constants.DEFAULT_STREAM_ENCODING;
+import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_STDOUT;
+import static org.apache.maven.surefire.api.stream.SegmentType.END_OF_FRAME;
+import static org.fest.assertions.Assertions.assertThat;
+import static org.powermock.reflect.Whitebox.invokeMethod;
+
+/**
+ * The performance of "get( Integer )" is 13.5 nano seconds on i5/2.6GHz:
+ * <pre>
+ *     {@code
+ *     TreeMap<Integer, ForkedProcessEventType> map = new TreeMap<>();
+ *     map.get( hash );
+ *     }
+ * </pre>
+ *
+ * <br> The performance of getting event type by Segment is 33.7 nano seconds:
+ * <pre>
+ *     {@code
+ *     Map<Segment, ForkedProcessEventType> map = new HashMap<>();
+ *     byte[] array = ForkedProcessEventType.BOOTERCODE_STDOUT.getOpcode().getBytes( UTF_8 );
+ *     map.get( new Segment( array, 0, array.length ) );
+ *     }
+ * </pre>
+ *
+ * <br> The performance of decoder:
+ * <pre>
+ *     {@code
+ *     CharsetDecoder decoder = STREAM_ENCODING.newDecoder()
+ *             .onMalformedInput( REPLACE )
+ *             .onUnmappableCharacter( REPLACE );
+ *     ByteBuffer buffer = ByteBuffer.wrap( ForkedProcessEventType.BOOTERCODE_STDOUT.getOpcode().getBytes( UTF_8 ) );
+ *     CharBuffer chars = CharBuffer.allocate( 100 );
+ *     decoder.reset().decode( buffer, chars, true );
+ *
+ *     String s = chars.flip().toString(); // 37 nanos = CharsetDecoder + toString
+ *
+ *     buffer.clear();
+ *     chars.clear();
+ *
+ *     ForkedProcessEventType.byOpcode( s ); // 65 nanos = CharsetDecoder + toString + byOpcode
+ *     }
+ * </pre>
+ *
+ * <br> The performance of decoding 100 bytes via CharacterDecoder - 71 nano seconds:
+ * <pre>
+ *     {@code
+ *     decoder.reset()
+ *         .decode( buffer, chars, true ); // CharsetDecoder 71 nanos
+ *     chars.flip().toString(); // CharsetDecoder + toString = 91 nanos
+ *     }
+ * </pre>
+ *
+ * <br> The performance of a pure string creation (instead of decoder) - 31.5 nano seconds:
+ * <pre>
+ *     {@code
+ *     byte[] b = {};
+ *     new String( b, UTF_8 );
+ *     }
+ * </pre>
+ *
+ * <br> The performance of CharsetDecoder with empty ByteBuffer:
+ * <pre>
+ *     {@code
+ *     CharsetDecoder + ByteBuffer.allocate( 0 ) makes 11.5 nanos
+ *     CharsetDecoder + ByteBuffer.allocate( 0 ) + toString() makes 16.1 nanos
+ *     }
+ * </pre>
+ */
+@SuppressWarnings( "checkstyle:magicnumber" )
+public class AbstractStreamDecoderTest
+{
+    private static final Map<Segment, ForkedProcessEventType> EVENTS = new HashMap<>();
+
+    private static final String PATTERN1 =
+        "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789";
+
+    private static final String PATTERN2 = "€ab©c";
+
+    private static final byte[] PATTERN2_BYTES =
+        new byte[] {(byte) -30, (byte) -126, (byte) -84, 'a', 'b', (byte) 0xc2, (byte) 0xa9, 'c'};
+
+    @BeforeClass
+    public static void setup()
+    {
+        for ( ForkedProcessEventType event : ForkedProcessEventType.values() )
+        {
+            byte[] array = event.getOpcodeBinary();
+            EVENTS.put( new Segment( array, 0, array.length ), event );
+        }
+    }
+
+    @Test
+    public void shouldDecodeHappyCase() throws Exception
+    {
+        CharsetDecoder decoder = UTF_8.newDecoder().onMalformedInput( REPLACE ).onUnmappableCharacter( REPLACE );
+        ByteBuffer input = ByteBuffer.allocate( 1024 );
+        input.put( PATTERN2_BYTES ).flip();
+        int bytesToDecode = PATTERN2_BYTES.length;
+        CharBuffer output = CharBuffer.allocate( 1024 );
+        int readBytes = invokeMethod( AbstractStreamDecoder.class, "decodeString", decoder, input, output,
+            bytesToDecode, true, 0 );
+
+        assertThat( readBytes )
+            .isEqualTo( bytesToDecode );
+
+        assertThat( output.flip().toString() )
+            .isEqualTo( PATTERN2 );
+    }
+
+    @Test
+    public void shouldDecodeShifted() throws Exception
+    {
+        CharsetDecoder decoder = UTF_8.newDecoder().onMalformedInput( REPLACE ).onUnmappableCharacter( REPLACE );
+        ByteBuffer input = ByteBuffer.allocate( 1024 );
+        input.put( PATTERN1.getBytes( UTF_8 ) )
+            .put( 90, (byte) 'A' )
+            .put( 91, (byte) 'B' )
+            .put( 92, (byte) 'C' )
+            .position( 90 );
+        CharBuffer output = CharBuffer.allocate( 1024 );
+        int readBytes =
+            invokeMethod( AbstractStreamDecoder.class, "decodeString", decoder, input, output, 2, true, 0 );
+
+        assertThat( readBytes ).isEqualTo( 2 );
+
+        assertThat( output.flip().toString() )
+            .isEqualTo( "AB" );
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldNotDecode() throws Exception
+    {
+        CharsetDecoder decoder = UTF_8.newDecoder();
+        ByteBuffer input = ByteBuffer.allocate( 100 );
+        int bytesToDecode = 101;
+        CharBuffer output = CharBuffer.allocate( 1000 );
+        invokeMethod( AbstractStreamDecoder.class, "decodeString", decoder, input, output, bytesToDecode, true, 0 );
+    }
+
+    @Test
+    public void shouldReadInt() throws Exception
+    {
+        Channel channel = new Channel( new byte[] {0x01, 0x02, 0x03, 0x04, ':'}, 1 );
+
+        Mock thread = new Mock( channel, new MockForkNodeArguments(),
+            Collections.<Segment, ForkedProcessEventType>emptyMap() );
+
+        Memento memento = thread.new Memento();
+
+        assertThat( thread.readInt( memento ) )
+            .isEqualTo( new BigInteger( new byte[] {0x01, 0x02, 0x03, 0x04} ).intValue() );
+    }
+
+    @Test
+    public void shouldReadInteger() throws Exception
+    {
+        Channel channel = new Channel( new byte[] {(byte) 0xff, 0x01, 0x02, 0x03, 0x04, ':'}, 1 );
+
+        Mock thread = new Mock( channel, new MockForkNodeArguments(),
+            Collections.<Segment, ForkedProcessEventType>emptyMap() );
+
+        Memento memento = thread.new Memento();
+        assertThat( thread.readInteger( memento ) )
+            .isEqualTo( new BigInteger( new byte[] {0x01, 0x02, 0x03, 0x04} ).intValue() );
+    }
+
+    @Test
+    public void shouldReadNullInteger() throws Exception
+    {
+        Channel channel = new Channel( new byte[] {(byte) 0x00, ':'}, 1 );
+
+        Mock thread = new Mock( channel, new MockForkNodeArguments(),
+            Collections.<Segment, ForkedProcessEventType>emptyMap() );
+
+        Memento memento = thread.new Memento();
+        assertThat( thread.readInteger( memento ) )
+            .isNull();
+    }
+
+    @Test( expected = EOFException.class )
+    public void shouldNotReadString() throws Exception
+    {
+        Channel channel = new Channel( PATTERN1.getBytes(), PATTERN1.length() );
+        channel.read( ByteBuffer.allocate( 100 ) );
+
+        Mock thread = new Mock( channel, new MockForkNodeArguments(),
+            Collections.<Segment, ForkedProcessEventType>emptyMap() );
+
+        Memento memento = thread.new Memento();
+        invokeMethod( thread, "readString", memento, 10 );
+    }
+
+    @Test
+    public void shouldReadString() throws Exception
+    {
+        Channel channel = new Channel( PATTERN1.getBytes(), PATTERN1.length() );
+
+        Mock thread = new Mock( channel, new MockForkNodeArguments(),
+            Collections.<Segment, ForkedProcessEventType>emptyMap() );
+
+        Memento memento = thread.new Memento();
+        String s = invokeMethod( thread, "readString", memento, 10 );
+        assertThat( s )
+            .isEqualTo( "0123456789" );
+    }
+
+    @Test
+    public void shouldReadStringShiftedBuffer() throws Exception
+    {
+        StringBuilder s = new StringBuilder( 1100 );
+        for ( int i = 0; i < 11; i++ )
+        {
+            s.append( PATTERN1 );
+        }
+
+        Channel channel = new Channel( s.toString().getBytes( UTF_8 ), s.length() );
+
+        Mock thread = new Mock( channel, new MockForkNodeArguments(),
+            Collections.<Segment, ForkedProcessEventType>emptyMap() );
+
+        Memento memento = thread.new Memento();
+        // whatever position will be compacted to 0
+        memento.getByteBuffer().limit( 974 ).position( 974 );
+        assertThat( invokeMethod( thread, "readString", memento, PATTERN1.length() + 3 ) )
+            .isEqualTo( PATTERN1 + "012" );
+    }
+
+    @Test
+    public void shouldReadStringShiftedInput() throws Exception
+    {
+        StringBuilder s = new StringBuilder( 1100 );
+        for ( int i = 0; i < 11; i++ )
+        {
+            s.append( PATTERN1 );
+        }
+
+        Channel channel = new Channel( s.toString().getBytes( UTF_8 ), s.length() );
+        channel.read( ByteBuffer.allocate( 997 ) );
+
+        Mock thread = new Mock( channel, new MockForkNodeArguments(),
+            Collections.<Segment, ForkedProcessEventType>emptyMap() );
+
+        Memento memento = thread.new Memento();
+        assertThat( invokeMethod( thread, "readString", memento, PATTERN1.length() ) )
+            .isEqualTo( "789" + PATTERN1.substring( 0, 97 ) );
+    }
+
+    @Test
+    public void shouldReadMultipleStringsAndShiftedInput() throws Exception
+    {
+        StringBuilder s = new StringBuilder( 5000 );
+
+        for ( int i = 0; i < 50; i++ )
+        {
+            s.append( PATTERN1 );
+        }
+
+        Channel channel = new Channel( s.toString().getBytes( UTF_8 ), s.length() );
+        channel.read( ByteBuffer.allocate( 1997 ) );
+
+        Mock thread = new Mock( channel, new MockForkNodeArguments(),
+            Collections.<Segment, ForkedProcessEventType>emptyMap() );
+
+        Memento memento = thread.new Memento();
+        // whatever position will be compacted to 0
+        memento.getByteBuffer().limit( 974 ).position( 974 );
+
+        StringBuilder expected = new StringBuilder( "789" );
+        for ( int i = 0; i < 11; i++ )
+        {
+            expected.append( PATTERN1 );
+        }
+        expected.setLength( 1100 );
+        assertThat( invokeMethod( thread, "readString", memento, 1100 ) )
+            .isEqualTo( expected.toString() );
+    }
+
+    @Test
+    public void shouldDecode3BytesEncodedSymbol() throws Exception
+    {
+        byte[] encodedSymbol = new byte[] {(byte) -30, (byte) -126, (byte) -84};
+        int countSymbols = 1024;
+        byte[] input = new byte[encodedSymbol.length * countSymbols];
+        for ( int i = 0; i < countSymbols; i++ )
+        {
+            arraycopy( encodedSymbol, 0, input, encodedSymbol.length * i, encodedSymbol.length );
+        }
+
+        Channel channel = new Channel( input, 64 * 1024 );
+        Mock thread = new Mock( channel, new MockForkNodeArguments(),
+            Collections.<Segment, ForkedProcessEventType>emptyMap() );
+        Memento memento = thread.new Memento();
+        String decodedOutput = invokeMethod( thread, "readString", memento, input.length );
+
+        assertThat( decodedOutput )
+            .isEqualTo( new String( input, 0, input.length, UTF_8 ) );
+    }
+
+    @Test
+    public void shouldDecode100Bytes() throws Exception
+    {
+        CharsetDecoder decoder = DEFAULT_STREAM_ENCODING.newDecoder()
+            .onMalformedInput( REPLACE )
+            .onUnmappableCharacter( REPLACE );
+        // empty stream: CharsetDecoder + ByteBuffer.allocate( 0 ) makes 11.5 nanos
+        // empty stream: CharsetDecoder + ByteBuffer.allocate( 0 ) + toString() makes 16.1 nanos
+        ByteBuffer buffer = ByteBuffer.wrap( PATTERN1.getBytes( UTF_8 ) );
+        CharBuffer chars = CharBuffer.allocate( 100 );
+        // uncomment this section for a proper measurement of the exec time
+        TimeUnit.SECONDS.sleep( 2 );
+        System.gc();
+        TimeUnit.SECONDS.sleep( 5 );
+        String s = null;
+        long l1 = System.currentTimeMillis();
+        for ( int i = 0; i < 10_000_000; i++ )
+        {
+            decoder.reset()
+                .decode( buffer, chars, true ); // CharsetDecoder 71 nanos
+            s = chars.flip().toString(); // CharsetDecoder + toString = 91 nanos
+            buffer.clear();
+            chars.clear();
+        }
+        long l2 = System.currentTimeMillis();
+        System.out.println( "decoded 100 bytes within " + ( l2 - l1 ) + " millis (10 million cycles)" );
+        assertThat( s )
+            .isEqualTo( PATTERN1 );
+    }
+
+    @Test
+    public void shouldReadEventType() throws Exception
+    {
+        byte[] array = BOOTERCODE_STDOUT.getOpcodeBinary();
+        Map<Segment, ForkedProcessEventType> messageType =
+            singletonMap( new Segment( array, 0, array.length ), BOOTERCODE_STDOUT );
+
+        byte[] stream = ":maven-surefire-event:\u000E:std-out-stream:".getBytes( UTF_8 );
+        Channel channel = new Channel( stream, 1 );
+        Mock thread = new Mock( channel, new MockForkNodeArguments(), messageType );
+
+        Memento memento = thread.new Memento();
+        memento.setCharset( UTF_8 );
+
+        ForkedProcessEventType eventType = thread.readMessageType( memento );
+        assertThat( eventType )
+            .isEqualTo( BOOTERCODE_STDOUT );
+    }
+
+    @Test( expected = EOFException.class )
+    public void shouldEventTypeReachedEndOfStream() throws Exception
+    {
+        byte[] stream = ":maven-surefire-event:\u000E:xxx".getBytes( UTF_8 );
+        Channel channel = new Channel( stream, 1 );
+        Mock thread = new Mock( channel, new MockForkNodeArguments(), EVENTS );
+
+        Memento memento = thread.new Memento();
+        memento.setCharset( UTF_8 );
+        thread.readMessageType( memento );
+    }
+
+    @Test( expected = MalformedFrameException.class )
+    public void shouldEventTypeReachedMalformedHeader() throws Exception
+    {
+        byte[] stream = ":xxxxx-xxxxxxxx-xxxxx:\u000E:xxx".getBytes( UTF_8 );
+        Channel channel = new Channel( stream, 1 );
+        Mock thread = new Mock( channel, new MockForkNodeArguments(),
+            Collections.<Segment, ForkedProcessEventType>emptyMap() );
+
+        Memento memento = thread.new Memento();
+        memento.setCharset( UTF_8 );
+        thread.readMessageType( memento );
+    }
+
+    @Test
+    public void shouldReadEmptyString() throws Exception
+    {
+        byte[] stream = "\u0000\u0000\u0000\u0000::".getBytes( UTF_8 );
+        Channel channel = new Channel( stream, 1 );
+        Mock thread = new Mock( channel, new MockForkNodeArguments(),
+            Collections.<Segment, ForkedProcessEventType>emptyMap() );
+
+        Memento memento = thread.new Memento();
+        memento.setCharset( UTF_8 );
+
+        assertThat( thread.readString( memento ) )
+            .isEmpty();
+    }
+
+    @Test
+    public void shouldReadNullString() throws Exception
+    {
+        byte[] stream = "\u0000\u0000\u0000\u0001:\u0000:".getBytes( UTF_8 );
+        Channel channel = new Channel( stream, 1 );
+        Mock thread = new Mock( channel, new MockForkNodeArguments(),
+            Collections.<Segment, ForkedProcessEventType>emptyMap() );
+
+        Memento memento = thread.new Memento();
+        memento.setCharset( UTF_8 );
+
+        assertThat( thread.readString( memento ) )
+            .isNull();
+    }
+
+    @Test
+    public void shouldReadSingleCharString() throws Exception
+    {
+        byte[] stream = "\u0000\u0000\u0000\u0001:A:".getBytes( UTF_8 );
+        Channel channel = new Channel( stream, 1 );
+        Mock thread = new Mock( channel, new MockForkNodeArguments(),
+            Collections.<Segment, ForkedProcessEventType>emptyMap() );
+
+        Memento memento = thread.new Memento();
+        memento.setCharset( UTF_8 );
+
+        assertThat( thread.readString( memento ) )
+            .isEqualTo( "A" );
+    }
+
+    @Test
+    public void shouldReadThreeCharactersString() throws Exception
+    {
+        byte[] stream = "\u0000\u0000\u0000\u0003:ABC:".getBytes( UTF_8 );
+        Channel channel = new Channel( stream, 1 );
+        Mock thread = new Mock( channel, new MockForkNodeArguments(),
+            Collections.<Segment, ForkedProcessEventType>emptyMap() );
+
+        Memento memento = thread.new Memento();
+        memento.setCharset( UTF_8 );
+
+        assertThat( thread.readString( memento ) )
+            .isEqualTo( "ABC" );
+    }
+
+    @Test
+    public void shouldReadDefaultCharset() throws Exception
+    {
+        byte[] stream = "\u0005:UTF-8:".getBytes( US_ASCII );
+        Channel channel = new Channel( stream, 1 );
+        Mock thread = new Mock( channel, new MockForkNodeArguments(),
+            Collections.<Segment, ForkedProcessEventType>emptyMap() );
+
+        Memento memento = thread.new Memento();
+        memento.setCharset( UTF_8 );
+
+        assertThat( thread.readCharset( memento ) )
+            .isNotNull()
+            .isEqualTo( UTF_8 );
+    }
+
+    @Test
+    public void shouldReadNonDefaultCharset() throws Exception
+    {
+        byte[] stream = ( (char) 10 + ":ISO_8859_1:" ).getBytes( US_ASCII );
+        Channel channel = new Channel( stream, 1 );
+        Mock thread = new Mock( channel, new MockForkNodeArguments(),
+            Collections.<Segment, ForkedProcessEventType>emptyMap() );
+
+        Memento memento = thread.new Memento();
+        memento.setCharset( UTF_8 );
+
+        assertThat( thread.readCharset( memento ) )
+            .isNotNull()
+            .isEqualTo( ISO_8859_1 );
+    }
+
+    @Test
+    public void shouldSetNonDefaultCharset()
+    {
+        byte[] stream = {};
+        Channel channel = new Channel( stream, 1 );
+        Mock thread = new Mock( channel, new MockForkNodeArguments(),
+            Collections.<Segment, ForkedProcessEventType>emptyMap() );
+        Memento memento = thread.new Memento();
+
+        memento.setCharset( ISO_8859_1 );
+        assertThat( memento.getDecoder().charset() ).isEqualTo( ISO_8859_1 );
+
+        memento.setCharset( UTF_8 );
+        assertThat( memento.getDecoder().charset() ).isEqualTo( UTF_8 );
+
+        memento.reset();
+        assertThat( memento.getDecoder() ).isNotNull();
+        assertThat( memento.getDecoder().charset() ).isEqualTo( UTF_8 );
+    }
+
+    @Test( expected = MalformedFrameException.class )
+    public void malformedCharset() throws Exception
+    {
+        byte[] stream = ( (char) 8 + ":ISO_8859:" ).getBytes( US_ASCII );
+        Channel channel = new Channel( stream, 1 );
+        Mock thread = new Mock( channel, new MockForkNodeArguments(),
+            Collections.<Segment, ForkedProcessEventType>emptyMap() );
+
+        Memento memento = thread.new Memento();
+        memento.setCharset( UTF_8 );
+
+        thread.readCharset( memento );
+    }
+
+    private static class Channel implements ReadableByteChannel
+    {
+        private final byte[] bytes;
+        private final int chunkSize;
+        protected int i;
+
+        Channel( byte[] bytes, int chunkSize )
+        {
+            this.bytes = bytes;
+            this.chunkSize = chunkSize;
+        }
+
+        @Override
+        public int read( ByteBuffer dst )
+        {
+            if ( i == bytes.length )
+            {
+                return -1;
+            }
+            else if ( dst.hasRemaining() )
+            {
+                int length = min( min( chunkSize, bytes.length - i ), dst.remaining() ) ;
+                dst.put( bytes, i, length );
+                i += length;
+                return length;
+            }
+            else
+            {
+                return 0;
+            }
+        }
+
+        @Override
+        public boolean isOpen()
+        {
+            return false;
+        }
+
+        @Override
+        public void close()
+        {
+        }
+    }
+
+    private static class MockForkNodeArguments implements ForkNodeArguments
+    {
+        @Nonnull
+        @Override
+        public String getSessionId()
+        {
+            return null;
+        }
+
+        @Override
+        public int getForkChannelId()
+        {
+            return 0;
+        }
+
+        @Nonnull
+        @Override
+        public File dumpStreamText( @Nonnull String text )
+        {
+            return null;
+        }
+
+        @Nonnull
+        @Override
+        public File dumpStreamException( @Nonnull Throwable t )
+        {
+            return null;
+        }
+
+        @Override
+        public void logWarningAtEnd( @Nonnull String text )
+        {
+        }
+
+        @Nonnull
+        @Override
+        public ConsoleLogger getConsoleLogger()
+        {
+            return null;
+        }
+
+        @Override
+        public File getEventStreamBinaryFile()
+        {
+            return null;
+        }
+
+        @Override
+        public File getCommandStreamBinaryFile()
+        {
+            return null;
+        }
+    }
+
+    private static class Mock extends AbstractStreamDecoder<Event, ForkedProcessEventType, SegmentType>
+    {
+        protected Mock( @Nonnull ReadableByteChannel channel, @Nonnull ForkNodeArguments arguments,
+                        @Nonnull Map<Segment, ForkedProcessEventType> messageTypes )
+        {
+            super( channel, arguments, messageTypes );
+        }
+
+        @Override
+        public Event decode( @Nonnull Memento memento ) throws MalformedChannelException
+        {
+            throw new MalformedChannelException();
+        }
+
+        @Nonnull
+        @Override
+        protected byte[] getEncodedMagicNumber()
+        {
+            return Constants.MAGIC_NUMBER_FOR_EVENTS_BYTES;
+        }
+
+        @Nonnull
+        @Override
+        protected SegmentType[] nextSegmentType( @Nonnull ForkedProcessEventType messageType )
+        {
+            return new SegmentType[] {END_OF_FRAME};
+        }
+
+        @Nonnull
+        @Override
+        protected Event toMessage(
+            @Nonnull ForkedProcessEventType messageType, RunMode runMode,
+            @Nonnull Memento memento ) throws MalformedFrameException
+        {
+            return null;
+        }
+
+        @Override
+        public void close() throws Exception
+        {
+        }
+    }
+}
diff --git a/surefire-api/src/test/java/org/apache/maven/surefire/api/stream/AbstractStreamEncoderTest.java b/surefire-api/src/test/java/org/apache/maven/surefire/api/stream/AbstractStreamEncoderTest.java
new file mode 100644
index 0000000..e36373c
--- /dev/null
+++ b/surefire-api/src/test/java/org/apache/maven/surefire/api/stream/AbstractStreamEncoderTest.java
@@ -0,0 +1,331 @@
+package org.apache.maven.surefire.api.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.surefire.api.booter.ForkedProcessEventType;
+import org.apache.maven.surefire.api.util.internal.WritableBufferedByteChannel;
+import org.junit.Test;
+
+import javax.annotation.Nonnull;
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.maven.surefire.api.booter.Constants.MAGIC_NUMBER_FOR_EVENTS_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.fest.assertions.Assertions.assertThat;
+
+/**
+ *
+ */
+@SuppressWarnings( { "checkstyle:linelength", "checkstyle:magicnumber" } )
+public class AbstractStreamEncoderTest
+{
+    @Test
+    public void shouldComputeStreamPreemptiveLength()
+    {
+        Encoder streamEncoder = new Encoder( new DummyChannel() );
+        CharsetEncoder encoder = streamEncoder.newCharsetEncoder();
+
+        // :maven-surefire-event:8:sys-prop:10:normal-run:5:UTF-8:0001:kkk:0001:vvv:
+        assertThat( streamEncoder.estimateBufferLength( BOOTERCODE_SYSPROPS.getOpcodeBinary().length, NORMAL_RUN,
+            encoder, 0, "k", "v" ) )
+            .isEqualTo( 72 );
+
+        // :maven-surefire-event:16:testset-starting:10:normal-run:5:UTF-8:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:X0001:0001:sss:0001:sss:0001:sss:
+        assertThat( streamEncoder.estimateBufferLength( BOOTERCODE_TESTSET_STARTING.getOpcodeBinary().length,
+            NORMAL_RUN, encoder, 1, "s", "s", "s", "s", "s", "s", "s", "s", "s" ) )
+            .isEqualTo( 149 );
+
+        // :maven-surefire-event:17:testset-completed:10:normal-run:5:UTF-8:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:X0001:0001:sss:0001:sss:0001:sss:
+        assertThat( streamEncoder.estimateBufferLength( BOOTERCODE_TESTSET_COMPLETED.getOpcodeBinary().length,
+            NORMAL_RUN, encoder, 1, "s", "s", "s", "s", "s", "s", "s", "s", "s" ) )
+            .isEqualTo( 150 );
+
+        // :maven-surefire-event:13:test-starting:10:normal-run:5:UTF-8:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:X0001:0001:sss:0001:sss:0001:sss:
+        assertThat( streamEncoder.estimateBufferLength( BOOTERCODE_TEST_STARTING.getOpcodeBinary().length,
+            NORMAL_RUN, encoder, 1, "s", "s", "s", "s", "s", "s", "s", "s", "s" ) )
+            .isEqualTo( 146 );
+
+        // :maven-surefire-event:14:test-succeeded:10:normal-run:5:UTF-8:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:X0001:0001:sss:0001:sss:0001:sss:
+        assertThat( streamEncoder.estimateBufferLength( BOOTERCODE_TEST_SUCCEEDED.getOpcodeBinary().length,
+            NORMAL_RUN, encoder, 1, "s", "s", "s", "s", "s", "s", "s", "s", "s" ) )
+            .isEqualTo( 147 );
+
+        // :maven-surefire-event:11:test-failed:10:normal-run:5:UTF-8:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:X0001:0001:sss:0001:sss:0001:sss:
+        assertThat( streamEncoder.estimateBufferLength( BOOTERCODE_TEST_FAILED.getOpcodeBinary().length,
+            NORMAL_RUN, encoder, 1, "s", "s", "s", "s", "s", "s", "s", "s", "s" ) )
+            .isEqualTo( 144 );
+
+        // :maven-surefire-event:12:test-skipped:10:normal-run:5:UTF-8:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:X0001:0001:sss:0001:sss:0001:sss:
+        assertThat( streamEncoder.estimateBufferLength( BOOTERCODE_TEST_SKIPPED.getOpcodeBinary().length,
+            NORMAL_RUN, encoder, 1, "s", "s", "s", "s", "s", "s", "s", "s", "s" ) )
+            .isEqualTo( 145 );
+
+        // :maven-surefire-event:10:test-error:10:normal-run:5:UTF-8:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:X0001:0001:sss:0001:sss:0001:sss:
+        assertThat( streamEncoder.estimateBufferLength( BOOTERCODE_TEST_ERROR.getOpcodeBinary().length,
+            NORMAL_RUN, encoder, 1, "s", "s", "s", "s", "s", "s", "s", "s", "s" ) )
+            .isEqualTo( 143 );
+
+        // :maven-surefire-event:23:test-assumption-failure:10:normal-run:5:UTF-8:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:X0001:0001:sss:0001:sss:0001:sss:
+        assertThat( streamEncoder.estimateBufferLength( BOOTERCODE_TEST_ASSUMPTIONFAILURE.getOpcodeBinary().length,
+            NORMAL_RUN, encoder, 1, "s", "s", "s", "s", "s", "s", "s", "s", "s" ) )
+            .isEqualTo( 156 );
+
+        // :maven-surefire-event:14:std-out-stream:10:normal-run:5:UTF-8:0001:sss:
+        assertThat( streamEncoder.estimateBufferLength( BOOTERCODE_STDOUT.getOpcodeBinary().length,
+            NORMAL_RUN, encoder, 0, "s" ) )
+            .isEqualTo( 69 );
+
+        // :maven-surefire-event:23:std-out-stream-new-line:10:normal-run:5:UTF-8:0001:sss:
+        assertThat( streamEncoder.estimateBufferLength( BOOTERCODE_STDOUT_NEW_LINE.getOpcodeBinary().length,
+            NORMAL_RUN, encoder, 0, "s" ) )
+            .isEqualTo( 78 );
+
+        // :maven-surefire-event:14:std-err-stream:10:normal-run:5:UTF-8:0001:sss:
+        assertThat( streamEncoder.estimateBufferLength( BOOTERCODE_STDERR.getOpcodeBinary().length,
+            NORMAL_RUN, encoder, 0, "s" ) )
+            .isEqualTo( 69 );
+
+        // :maven-surefire-event:23:std-err-stream-new-line:10:normal-run:5:UTF-8:0001:sss:
+        assertThat( streamEncoder.estimateBufferLength( BOOTERCODE_STDERR_NEW_LINE.getOpcodeBinary().length,
+            NORMAL_RUN, encoder, 0, "s" ) )
+            .isEqualTo( 78 );
+
+        // :maven-surefire-event:16:console-info-log:5:UTF-8:0001:sss:
+        assertThat( streamEncoder.estimateBufferLength( BOOTERCODE_CONSOLE_INFO.getOpcodeBinary().length,
+            null, encoder, 0, "s" ) )
+            .isEqualTo( 58 );
+
+        // :maven-surefire-event:17:console-debug-log:5:UTF-8:0001:sss:
+        assertThat( streamEncoder.estimateBufferLength( BOOTERCODE_CONSOLE_DEBUG.getOpcodeBinary().length,
+            null, encoder, 0, "s" ) )
+            .isEqualTo( 59 );
+
+        // :maven-surefire-event:19:console-warning-log:5:UTF-8:0001:sss:
+        assertThat( streamEncoder.estimateBufferLength( BOOTERCODE_CONSOLE_WARNING.getOpcodeBinary().length,
+            null, encoder, 0, "s" ) )
+            .isEqualTo( 61 );
+
+        // :maven-surefire-event:17:console-error-log:5:UTF-8:0001:sss:
+        assertThat( streamEncoder.estimateBufferLength( BOOTERCODE_CONSOLE_ERROR.getOpcodeBinary().length,
+            null, encoder, 0, "s" ) )
+            .isEqualTo( 59 );
+
+        // :maven-surefire-event:3:bye:
+        assertThat( streamEncoder.estimateBufferLength( BOOTERCODE_BYE.getOpcodeBinary().length,
+            null, null, 0 ) )
+            .isEqualTo( 28 );
+
+        // :maven-surefire-event:17:stop-on-next-test:
+        assertThat( streamEncoder.estimateBufferLength( BOOTERCODE_STOP_ON_NEXT_TEST.getOpcodeBinary().length,
+            null, null, 0 ) )
+            .isEqualTo( 42 );
+
+        // :maven-surefire-event:9:next-test:
+        assertThat( streamEncoder.estimateBufferLength( BOOTERCODE_NEXT_TEST.getOpcodeBinary().length,
+            null, null, 0 ) )
+            .isEqualTo( 34 );
+
+        // :maven-surefire-event:14:jvm-exit-error:
+        assertThat( streamEncoder.estimateBufferLength( BOOTERCODE_JVM_EXIT_ERROR.getOpcodeBinary().length,
+            null, null, 0 ) )
+            .isEqualTo( 39 );
+    }
+
+    @Test
+    public void testSendOpcode()
+    {
+        Encoder streamEncoder = new Encoder( new DummyChannel() );
+        ByteBuffer result = ByteBuffer.allocate( 128 );
+        streamEncoder.encodeHeader( result, BOOTERCODE_TEST_ERROR, NORMAL_RUN );
+        assertThat( toString( result ) )
+            .isEqualTo( ":maven-surefire-event:" + (char) 10 + ":test-error:" + (char) 10 + ":normal-run:" );
+
+        result = ByteBuffer.allocate( 1024 );
+        streamEncoder.encodeHeader( result, BOOTERCODE_CONSOLE_ERROR, null );
+        streamEncoder.encodeCharset( result );
+        assertThat( toString( result ) )
+            .isEqualTo( ":maven-surefire-event:" + (char) 17 + ":console-error-log:" + (char) 5 + ":UTF-8:" );
+    }
+
+    @Test
+    public void testEncodedString()
+    {
+        Encoder streamEncoder = new Encoder( new DummyChannel() );
+        ByteBuffer result = ByteBuffer.allocate( 128 );
+        streamEncoder.encode( streamEncoder.newCharsetEncoder(), result, BOOTERCODE_STDOUT, NORMAL_RUN, null, "msg" );
+        assertThat( toString( result ) )
+            .isEqualTo( ":maven-surefire-event:\u000e:std-out-stream:"
+                + (char) 10 + ":normal-run:\u0005:UTF-8:\u0000\u0000\u0000\u0001:\u0000:\u0000\u0000\u0000\u0003:msg:" );
+    }
+
+    @Test
+    public void testIntegerAsNull()
+    {
+        Encoder streamEncoder = new Encoder( new DummyChannel() );
+        ByteBuffer result = ByteBuffer.allocate( 4 );
+        streamEncoder.encodeInteger( result, null );
+        assertThat( result.position() ).isEqualTo( 2 );
+        assertThat( result.get( 0 ) ).isEqualTo( (byte) 0 );
+        assertThat( result.get( 1 ) ).isEqualTo( (byte) ':' );
+    }
+
+    @Test
+    public void testInteger()
+    {
+        Encoder streamEncoder = new Encoder( new DummyChannel() );
+        ByteBuffer result = ByteBuffer.allocate( 8 );
+        streamEncoder.encodeInteger( result, 5 );
+        assertThat( result.position() ).isEqualTo( 6 );
+        result.position( 0 );
+        assertThat( result.get() ).isEqualTo( (byte) 0xff );
+        assertThat( result.getInt() ).isEqualTo( (byte) 5 );
+        assertThat( result.get( 5 ) ).isEqualTo( (byte) ':' );
+    }
+
+    @Test
+    public void testWrite() throws Exception
+    {
+        DummyChannel channel = new DummyChannel();
+        Encoder streamEncoder = new Encoder( channel );
+        streamEncoder.write( ByteBuffer.allocate( 0 ), false );
+        assertThat( channel.writeBuffered ).isTrue();
+        assertThat( channel.write ).isFalse();
+        channel.writeBuffered = false;
+        channel.write = false;
+        streamEncoder.write( ByteBuffer.allocate( 0 ), true );
+        assertThat( channel.writeBuffered ).isFalse();
+        assertThat( channel.write ).isTrue();
+    }
+
+    private static String toString( ByteBuffer frame )
+    {
+        ByteArrayOutputStream os = new ByteArrayOutputStream();
+        frame.flip();
+        os.write( frame.array(), frame.arrayOffset() + frame.position(), frame.remaining() );
+        return new String( os.toByteArray(), UTF_8 );
+    }
+
+    private static class Encoder extends AbstractStreamEncoder<ForkedProcessEventType>
+    {
+
+        Encoder( WritableBufferedByteChannel out )
+        {
+            super( out );
+        }
+
+        @Nonnull
+        @Override
+        public byte[] getEncodedMagicNumber()
+        {
+            return MAGIC_NUMBER_FOR_EVENTS_BYTES;
+        }
+
+        @Nonnull
+        @Override
+        protected byte[] enumToByteArray( ForkedProcessEventType forkedProcessEventType )
+        {
+            return forkedProcessEventType.getOpcodeBinary();
+        }
+
+        @Nonnull
+        @Override
+        protected byte[] getEncodedCharsetName()
+        {
+            return getCharset().name().getBytes( US_ASCII );
+        }
+
+        @Nonnull
+        @Override
+        public Charset getCharset()
+        {
+            return UTF_8;
+        }
+
+        @Nonnull
+        @Override
+        public CharsetEncoder newCharsetEncoder()
+        {
+            return getCharset().newEncoder();
+        }
+    }
+
+    private static class DummyChannel implements WritableBufferedByteChannel
+    {
+        boolean writeBuffered;
+        boolean write;
+
+        @Override
+        public void writeBuffered( ByteBuffer src )
+        {
+            writeBuffered = true;
+        }
+
+        @Override
+        public long countBufferOverflows()
+        {
+            return 0;
+        }
+
+        @Override
+        public int write( ByteBuffer src )
+        {
+            write = true;
+            return 0;
+        }
+
+        @Override
+        public boolean isOpen()
+        {
+            return false;
+        }
+
+        @Override
+        public void close()
+        {
+        }
+    }
+  
+}
diff --git a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/BooterConstants.java b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/BooterConstants.java
index ae950df..dcf7361 100644
--- a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/BooterConstants.java
+++ b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/BooterConstants.java
@@ -42,7 +42,6 @@ public final class BooterConstants
     public static final String TESTARTIFACT_VERSION = "testFwJarVersion";
     public static final String TESTARTIFACT_CLASSIFIER = "testFwJarClassifier";
     public static final String REQUESTEDTEST = "requestedTest";
-    public static final String REQUESTEDTESTMETHOD = "requestedTestMethod";
     public static final String SOURCE_DIRECTORY = "testSuiteDefinitionTestSourceDirectory";
     public static final String TEST_CLASSES_DIRECTORY = "testClassesDirectory";
     public static final String RUN_ORDER = "runOrder";
@@ -60,4 +59,5 @@ public final class BooterConstants
     public static final String PLUGIN_PID = "pluginPid";
     public static final String PROCESS_CHECKER = "processChecker";
     public static final String FORK_NODE_CONNECTION_STRING = "forkNodeConnectionString";
+    public static final String FORK_NUMBER = "forkNumber";
 }
diff --git a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/BooterDeserializer.java b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/BooterDeserializer.java
index 6a6cfae..ca5328e 100644
--- a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/BooterDeserializer.java
+++ b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/BooterDeserializer.java
@@ -62,6 +62,11 @@ public class BooterDeserializer
         properties = SystemPropertyManager.loadProperties( inputStream );
     }
 
+    public int getForkNumber()
+    {
+        return properties.getIntProperty( FORK_NUMBER );
+    }
+
     /**
      * Describes the current connection channel used by the client in the forked JVM
      * in order to connect to the plugin process.
diff --git a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/CommandReader.java b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/CommandReader.java
index ebe72a9..6599433 100644
--- a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/CommandReader.java
+++ b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/CommandReader.java
@@ -336,11 +336,11 @@ public final class CommandReader implements CommandChainReader
         {
             CommandReader.this.startMonitor.countDown();
             boolean isTestSetFinished = false;
-            try
+            try ( MasterProcessChannelDecoder commandReader = CommandReader.this.decoder )
             {
                 while ( CommandReader.this.state.get() == RUNNABLE )
                 {
-                    Command command = CommandReader.this.decoder.decode();
+                    Command command = commandReader.decode();
                     switch ( command.getCommandType() )
                     {
                         case RUN_CLASS:
diff --git a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ForkedBooter.java b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ForkedBooter.java
index d8e3bfe..f00cb6a 100644
--- a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ForkedBooter.java
+++ b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ForkedBooter.java
@@ -27,16 +27,17 @@ import org.apache.maven.surefire.api.booter.ForkingReporterFactory;
 import org.apache.maven.surefire.api.booter.MasterProcessChannelDecoder;
 import org.apache.maven.surefire.api.booter.MasterProcessChannelEncoder;
 import org.apache.maven.surefire.api.booter.Shutdown;
-import org.apache.maven.surefire.booter.spi.LegacyMasterProcessChannelProcessorFactory;
-import org.apache.maven.surefire.booter.spi.SurefireMasterProcessChannelProcessorFactory;
+import org.apache.maven.surefire.api.fork.ForkNodeArguments;
 import org.apache.maven.surefire.api.provider.CommandListener;
 import org.apache.maven.surefire.api.provider.ProviderParameters;
 import org.apache.maven.surefire.api.provider.SurefireProvider;
 import org.apache.maven.surefire.api.report.LegacyPojoStackTraceWriter;
 import org.apache.maven.surefire.api.report.StackTraceWriter;
+import org.apache.maven.surefire.api.testset.TestSetFailedException;
+import org.apache.maven.surefire.booter.spi.LegacyMasterProcessChannelProcessorFactory;
+import org.apache.maven.surefire.booter.spi.SurefireMasterProcessChannelProcessorFactory;
 import org.apache.maven.surefire.shared.utils.cli.ShutdownHookUtils;
 import org.apache.maven.surefire.spi.MasterProcessChannelProcessorFactory;
-import org.apache.maven.surefire.api.testset.TestSetFailedException;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -61,13 +62,14 @@ import static java.lang.Thread.currentThread;
 import static java.util.ServiceLoader.load;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.apache.maven.surefire.api.cli.CommandLineOption.LOGGING_LEVEL_DEBUG;
+import static org.apache.maven.surefire.api.util.ReflectionUtils.instantiateOneArg;
+import static org.apache.maven.surefire.api.util.internal.DaemonThreadFactory.newDaemonThreadFactory;
+import static org.apache.maven.surefire.api.util.internal.StringUtils.NL;
 import static org.apache.maven.surefire.booter.ProcessCheckerType.ALL;
 import static org.apache.maven.surefire.booter.ProcessCheckerType.NATIVE;
 import static org.apache.maven.surefire.booter.ProcessCheckerType.PING;
 import static org.apache.maven.surefire.booter.SystemPropertyManager.setSystemProperties;
-import static org.apache.maven.surefire.api.util.ReflectionUtils.instantiateOneArg;
-import static org.apache.maven.surefire.api.util.internal.DaemonThreadFactory.newDaemonThreadFactory;
-import static org.apache.maven.surefire.api.util.internal.StringUtils.NL;
 
 /**
  * The part of the booter that is unique to a forked vm.
@@ -113,10 +115,13 @@ public final class ForkedBooter
         DumpErrorSingleton.getSingleton()
                 .init( providerConfiguration.getReporterConfiguration().getReportsDirectory(), dumpFileName );
 
+        int forkNumber = booterDeserializer.getForkNumber();
+
         if ( isDebugging() )
         {
             DumpErrorSingleton.getSingleton()
-                    .dumpText( "Found Maven process ID " + booterDeserializer.getPluginPid() );
+                    .dumpText( "Found Maven process ID " + booterDeserializer.getPluginPid()
+                        + " for the fork " + forkNumber + "." );
         }
 
         startupConfiguration = booterDeserializer.getStartupConfiguration();
@@ -124,8 +129,11 @@ public final class ForkedBooter
         String channelConfig = booterDeserializer.getConnectionString();
         channelProcessorFactory = lookupDecoderFactory( channelConfig );
         channelProcessorFactory.connect( channelConfig );
-        eventChannel = channelProcessorFactory.createEncoder();
-        MasterProcessChannelDecoder decoder = channelProcessorFactory.createDecoder();
+        boolean isDebugging = isDebugging();
+        boolean debug = isDebugging || providerConfiguration.getMainCliOptions().contains( LOGGING_LEVEL_DEBUG );
+        ForkNodeArguments args = new ForkedNodeArg( forkNumber, debug );
+        eventChannel = channelProcessorFactory.createEncoder( args );
+        MasterProcessChannelDecoder decoder = channelProcessorFactory.createDecoder( args );
 
         flushEventChannelOnExit();
 
@@ -133,7 +141,7 @@ public final class ForkedBooter
         ConsoleLogger logger = (ConsoleLogger) forkingReporterFactory.createReporter();
         commandReader = new CommandReader( decoder, providerConfiguration.getShutdown(), logger );
 
-        pingScheduler = isDebugging() ? null : listenToShutdownCommands( booterDeserializer.getPluginPid(), logger );
+        pingScheduler = isDebugging ? null : listenToShutdownCommands( booterDeserializer.getPluginPid(), logger );
 
         systemExitTimeoutInSeconds = providerConfiguration.systemExitTimeout( DEFAULT_SYSTEM_EXIT_TIMEOUT_IN_SECONDS );
 
@@ -671,4 +679,5 @@ public final class ForkedBooter
         return ManagementFactory.getRuntimeMXBean()
                 .getName();
     }
+
 }
diff --git a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ForkedNodeArg.java b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ForkedNodeArg.java
new file mode 100644
index 0000000..4bed5ca
--- /dev/null
+++ b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ForkedNodeArg.java
@@ -0,0 +1,97 @@
+package org.apache.maven.surefire.booter;
+
+/*
+ * 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.ConsoleLogger;
+import org.apache.maven.plugin.surefire.log.api.NullConsoleLogger;
+import org.apache.maven.surefire.api.booter.DumpErrorSingleton;
+import org.apache.maven.surefire.api.fork.ForkNodeArguments;
+
+import javax.annotation.Nonnull;
+import java.io.File;
+
+/**
+ *
+ */
+public final class ForkedNodeArg implements ForkNodeArguments
+{
+    private final int forkChannelId;
+    private final ConsoleLogger logger;
+    private final boolean isDebug;
+
+    public ForkedNodeArg( int forkChannelId, boolean isDebug )
+    {
+        this.forkChannelId = forkChannelId;
+        logger = new NullConsoleLogger();
+        this.isDebug = isDebug;
+    }
+
+    @Nonnull
+    @Override
+    public String getSessionId()
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int getForkChannelId()
+    {
+        return forkChannelId;
+    }
+
+    @Override
+    @Nonnull
+    public File dumpStreamText( @Nonnull String text )
+    {
+        return DumpErrorSingleton.getSingleton().dumpStreamText( text );
+    }
+
+    @Nonnull
+    @Override
+    public File dumpStreamException( @Nonnull Throwable t )
+    {
+        return DumpErrorSingleton.getSingleton().dumpStreamException( t, t.getLocalizedMessage() );
+    }
+
+    @Override
+    public void logWarningAtEnd( @Nonnull String text )
+    {
+        // do nothing - the log message of forked VM already goes to the dump file
+    }
+
+    @Nonnull
+    @Override
+    public ConsoleLogger getConsoleLogger()
+    {
+        return logger;
+    }
+
+    @Override
+    public File getEventStreamBinaryFile()
+    {
+        return null;
+    }
+
+    @Override
+    public File getCommandStreamBinaryFile()
+    {
+        return isDebug ? DumpErrorSingleton.getSingleton().getCommandStreamBinaryFile() : null;
+    }
+}
diff --git a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/PropertiesWrapper.java b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/PropertiesWrapper.java
index c0004e5..b5cccf4 100644
--- a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/PropertiesWrapper.java
+++ b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/PropertiesWrapper.java
@@ -62,7 +62,7 @@ public class PropertiesWrapper
 
     public boolean getBooleanProperty( String propertyName )
     {
-        return Boolean.valueOf( properties.get( propertyName ) );
+        return Boolean.parseBoolean( properties.get( propertyName ) );
     }
 
     public int getIntProperty( String propertyName )
diff --git a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/CommandChannelDecoder.java b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/CommandChannelDecoder.java
new file mode 100644
index 0000000..85bda73
--- /dev/null
+++ b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/CommandChannelDecoder.java
@@ -0,0 +1,86 @@
+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.surefire.api.booter.Command;
+import org.apache.maven.surefire.api.booter.MasterProcessChannelDecoder;
+import org.apache.maven.surefire.api.fork.ForkNodeArguments;
+import org.apache.maven.surefire.api.stream.AbstractStreamDecoder.Memento;
+import org.apache.maven.surefire.api.stream.MalformedChannelException;
+import org.apache.maven.surefire.booter.stream.CommandDecoder;
+
+import javax.annotation.Nonnull;
+import java.io.IOException;
+import java.nio.channels.ReadableByteChannel;
+
+/**
+ * magic number : opcode [: opcode specific data]*
+ * <br>
+ *
+ * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
+ * @since 3.0.0-M5
+ */
+public class CommandChannelDecoder implements MasterProcessChannelDecoder
+{
+    private final CommandDecoder decoder;
+    private Memento memento;
+
+    public CommandChannelDecoder( @Nonnull ReadableByteChannel channel,
+                                  @Nonnull ForkNodeArguments arguments )
+    {
+        decoder = new CommandDecoder( channel, arguments );
+    }
+
+    @Override
+    @Nonnull
+    @SuppressWarnings( "checkstyle:innerassignment" )
+    public Command 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 = decoder.new Memento();
+        }
+
+        do
+        {
+            try
+            {
+                Command command = decoder.decode( memento );
+                if ( command != null )
+                {
+                    return command;
+                }
+            }
+            catch ( MalformedChannelException e )
+            {
+                // a bad stream, already logged the stream down to a dump file or console, and continue till OEF
+            }
+        }
+        while ( true );
+    }
+
+    @Override
+    public void close() throws IOException
+    {
+        decoder.close();
+    }
+}
diff --git a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/LegacyMasterProcessChannelEncoder.java b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/EventChannelEncoder.java
similarity index 61%
rename from surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/LegacyMasterProcessChannelEncoder.java
rename to surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/EventChannelEncoder.java
index 0d8f787..8f78072 100644
--- a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/LegacyMasterProcessChannelEncoder.java
+++ b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/EventChannelEncoder.java
@@ -28,6 +28,7 @@ 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 org.apache.maven.surefire.booter.stream.EventEncoder;
 
 import javax.annotation.Nonnull;
 import java.io.IOException;
@@ -39,12 +40,7 @@ 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.DEFAULT_STREAM_ENCODING_BYTES;
-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;
@@ -67,7 +63,6 @@ import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTER
 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]*
@@ -77,39 +72,45 @@ import static org.apache.maven.surefire.api.report.RunMode.RERUN_TEST_AFTER_FAIL
  * @since 3.0.0-M4
  */
 @SuppressWarnings( "checkstyle:linelength" )
-public class LegacyMasterProcessChannelEncoder implements MasterProcessChannelEncoder
+public class EventChannelEncoder extends EventEncoder 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 )
+    /**
+     * The encoder for events and normal test mode.
+     *
+     * @param out     the channel available for writing the events
+     */
+    public EventChannelEncoder( @Nonnull WritableBufferedByteChannel out )
     {
         this( out, NORMAL_RUN );
     }
 
-    protected LegacyMasterProcessChannelEncoder( @Nonnull WritableBufferedByteChannel out, @Nonnull RunMode runMode )
+    /**
+     * The encoder for events and any test mode.
+     *
+     * @param out     the channel available for writing the events
+     * @param runMode run mode
+     */
+    public EventChannelEncoder( @Nonnull WritableBufferedByteChannel out, @Nonnull RunMode runMode )
     {
-        this.out = requireNonNull( out );
+        super( out );
         this.runMode = requireNonNull( runMode );
     }
 
-    @Override
+    /*@Override
     public MasterProcessChannelEncoder asRerunMode() // todo apply this and rework providers
     {
-        return new LegacyMasterProcessChannelEncoder( out, RERUN_TEST_AFTER_FAILURE );
+        return new EventChannelEncoder( streamEncoder, RERUN_TEST_AFTER_FAILURE );
     }
 
     @Override
     public MasterProcessChannelEncoder asNormalMode()
     {
-        return new LegacyMasterProcessChannelEncoder( out, NORMAL_RUN );
-    }
+        return new EventChannelEncoder( streamEncoder, NORMAL_RUN );
+    }*/
 
     @Override
     public boolean checkError()
@@ -121,13 +122,13 @@ public class LegacyMasterProcessChannelEncoder implements MasterProcessChannelEn
     public void onJvmExit()
     {
         onExit = true;
-        encodeAndPrintEvent( ByteBuffer.wrap( new byte[] {'\n'} ), true );
+        write( ByteBuffer.wrap( new byte[] {'\n'} ), true );
     }
 
     @Override
-    public void sendSystemProperties( Map<String, String> sysProps )
+    public void systemProperties( Map<String, String> sysProps )
     {
-        CharsetEncoder encoder = DEFAULT_STREAM_ENCODING.newEncoder();
+        CharsetEncoder encoder = newCharsetEncoder();
         ByteBuffer result = null;
         for ( Iterator<Entry<String, String>> it = sysProps.entrySet().iterator(); it.hasNext(); )
         {
@@ -135,13 +136,14 @@ public class LegacyMasterProcessChannelEncoder implements MasterProcessChannelEn
             String key = entry.getKey();
             String value = entry.getValue();
 
-            int bufferLength = estimateBufferLength( BOOTERCODE_SYSPROPS, runMode, encoder, 0, key, value );
+            int bufferLength =
+                estimateBufferLength( BOOTERCODE_SYSPROPS.getOpcode().length(), 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>:
+            // :maven-surefire-event:sys-prop:rerun-test-after-failure:UTF-8:<integer>:<key>:<integer>:<value>:
             encode( encoder, result, BOOTERCODE_SYSPROPS, runMode, key, value );
-            boolean sendImmediately = !it.hasNext();
-            encodeAndPrintEvent( result, sendImmediately );
+            boolean sync = !it.hasNext();
+            write( result, sync );
         }
     }
 
@@ -210,14 +212,14 @@ public class LegacyMasterProcessChannelEncoder implements MasterProcessChannelEn
     private void setOutErr( ForkedProcessEventType eventType, String message )
     {
         ByteBuffer result = encodeMessage( eventType, runMode, message );
-        encodeAndPrintEvent( result, false );
+        write( result, false );
     }
 
     @Override
     public void consoleInfoLog( String message )
     {
         ByteBuffer result = encodeMessage( BOOTERCODE_CONSOLE_INFO, null, message );
-        encodeAndPrintEvent( result, true );
+        write( result, true );
     }
 
     @Override
@@ -235,13 +237,15 @@ public class LegacyMasterProcessChannelEncoder implements MasterProcessChannelEn
     @Override
     public void consoleErrorLog( String message, Throwable t )
     {
-        CharsetEncoder encoder = DEFAULT_STREAM_ENCODING.newEncoder();
+        CharsetEncoder encoder = newCharsetEncoder();
         String stackTrace = t == null ? null : ConsoleLoggerUtils.toString( t );
-        int bufferMaxLength = estimateBufferLength( BOOTERCODE_CONSOLE_ERROR, null, encoder, 0, message, stackTrace );
+        int bufferMaxLength = estimateBufferLength( BOOTERCODE_CONSOLE_ERROR.getOpcode().length(), null, encoder, 0,
+            message, stackTrace );
         ByteBuffer result = ByteBuffer.allocate( bufferMaxLength );
-        encodeHeader( encoder, result, BOOTERCODE_CONSOLE_ERROR, null );
+        encodeHeader( result, BOOTERCODE_CONSOLE_ERROR, null );
+        encodeCharset( result );
         encode( encoder, result, message, null, stackTrace );
-        encodeAndPrintEvent( result, true );
+        write( result, true );
     }
 
     @Override
@@ -254,14 +258,14 @@ public class LegacyMasterProcessChannelEncoder implements MasterProcessChannelEn
     public void consoleDebugLog( String message )
     {
         ByteBuffer result = encodeMessage( BOOTERCODE_CONSOLE_DEBUG, null, message );
-        encodeAndPrintEvent( result, true );
+        write( result, true );
     }
 
     @Override
     public void consoleWarningLog( String message )
     {
         ByteBuffer result = encodeMessage( BOOTERCODE_CONSOLE_WARNING, null, message );
-        encodeAndPrintEvent( result, true );
+        write( result, true );
     }
 
     @Override
@@ -289,58 +293,51 @@ public class LegacyMasterProcessChannelEncoder implements MasterProcessChannelEn
     }
 
     private void error( StackTraceWriter stackTraceWriter, boolean trimStackTraces, ForkedProcessEventType eventType,
-                        @SuppressWarnings( "SameParameterValue" ) boolean sendImmediately )
+                        @SuppressWarnings( "SameParameterValue" ) boolean sync )
     {
-        CharsetEncoder encoder = DEFAULT_STREAM_ENCODING.newEncoder();
+        CharsetEncoder encoder = newCharsetEncoder();
         StackTrace stackTraceWrapper = new StackTrace( stackTraceWriter, trimStackTraces );
-        int bufferMaxLength = estimateBufferLength( eventType, null, encoder, 0,
+        int bufferMaxLength = estimateBufferLength( eventType.getOpcode().length(), null, encoder, 0,
             stackTraceWrapper.message, stackTraceWrapper.smartTrimmedStackTrace, stackTraceWrapper.stackTrace );
         ByteBuffer result = ByteBuffer.allocate( bufferMaxLength );
 
-        encodeHeader( encoder, result, eventType, null );
+        encodeHeader( result, eventType, null );
+        encodeCharset( result );
         encode( encoder, result, stackTraceWrapper );
-        encodeAndPrintEvent( result, sendImmediately );
+        write( result, sync );
     }
 
-    /**
-     * :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:
-     *
-     */
+    // example
+    // :maven-surefire-event:testset-starting:rerun-test-after-failure:UTF-8:<integer>:SourceName:<integer>:SourceText:<integer>:Name:<integer>:NameText:<integer>:Group:<integer>:Message:<integer>:ElapsedTime:<integer>:LocalizedMessage:<integer>:SmartTrimmedStackTrace:<integer>:toStackTrace( stw, trimStackTraces ):<integer>:
     private void encode( ForkedProcessEventType operation, RunMode runMode, ReportEntry reportEntry,
-                         boolean trimStackTraces, @SuppressWarnings( "SameParameterValue" ) boolean sendImmediately )
+                         boolean trimStackTraces, @SuppressWarnings( "SameParameterValue" ) boolean sync )
     {
         ByteBuffer result = encode( operation, runMode, reportEntry, trimStackTraces );
-        encodeAndPrintEvent( result, sendImmediately );
+        write( result, sync );
     }
 
-    private void encodeOpcode( ForkedProcessEventType eventType, boolean sendImmediately )
+    private void encodeOpcode( ForkedProcessEventType eventType, boolean sync )
     {
-        int bufferMaxLength = estimateBufferLength( eventType, null, null, 0 );
+        int bufferMaxLength = estimateBufferLength( eventType.getOpcode().length(), null, null, 0 );
         ByteBuffer result = ByteBuffer.allocate( bufferMaxLength );
-        encodeOpcode( result, eventType, null );
-        encodeAndPrintEvent( result, sendImmediately );
+        encodeHeader( result, eventType, null );
+        write( result, sync );
     }
 
-    private void encodeAndPrintEvent( ByteBuffer frame, boolean sendImmediately )
+    @Override
+    protected void write( ByteBuffer frame, boolean sync )
     {
         final boolean wasInterrupted = Thread.interrupted();
         try
         {
-            if ( sendImmediately )
-            {
-                out.write( frame );
-            }
-            else
-            {
-                out.writeBuffered( frame );
-            }
+            super.write( frame, sync );
         }
         catch ( ClosedChannelException e )
         {
             if ( !onExit )
             {
                 String event = new String( frame.array(), frame.arrayOffset() + frame.position(), frame.remaining(),
-                    DEFAULT_STREAM_ENCODING );
+                    getCharset() );
 
                 DumpErrorSingleton.getSingleton()
                     .dumpException( e, "Channel closed while writing the event '" + event + "'." );
@@ -363,23 +360,13 @@ public class LegacyMasterProcessChannelEncoder implements MasterProcessChannelEn
         }
     }
 
-    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 )
+    private 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 )
+    private void encode( CharsetEncoder encoder, ByteBuffer result,
+                         String message, String smartStackTrace, String stackTrace )
     {
         encodeString( encoder, result, message );
         encodeString( encoder, result, smartStackTrace );
@@ -399,21 +386,22 @@ public class LegacyMasterProcessChannelEncoder implements MasterProcessChannelEn
      * <li>{@link ForkedProcessEventType#BOOTERCODE_TEST_ASSUMPTIONFAILURE}.</li>
      * </ul>
      */
-    static ByteBuffer encode( ForkedProcessEventType operation, RunMode runMode, ReportEntry reportEntry,
-                              boolean trimStackTraces )
+    ByteBuffer encode( ForkedProcessEventType operation, RunMode runMode, ReportEntry reportEntry,
+                               boolean trimStackTraces )
     {
         StackTrace stackTraceWrapper = new StackTrace( reportEntry.getStackTraceWriter(), trimStackTraces );
 
-        CharsetEncoder encoder = DEFAULT_STREAM_ENCODING.newEncoder();
+        CharsetEncoder encoder = newCharsetEncoder();
 
-        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 );
+        int bufferMaxLength = estimateBufferLength( operation.getOpcode().length(), 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 );
+        encodeHeader( result, operation, runMode );
+        encodeCharset( result );
 
         encodeString( encoder, result, reportEntry.getSourceName() );
         encodeString( encoder, result, reportEntry.getSourceText() );
@@ -428,87 +416,15 @@ public class LegacyMasterProcessChannelEncoder implements MasterProcessChannelEn
         return result;
     }
 
-    static ByteBuffer encodeMessage( ForkedProcessEventType eventType, RunMode runMode, String message )
+    ByteBuffer encodeMessage( ForkedProcessEventType eventType, RunMode runMode, String message )
     {
-        CharsetEncoder encoder = DEFAULT_STREAM_ENCODING.newEncoder();
-        int bufferMaxLength = estimateBufferLength( eventType, runMode, encoder, 0, message );
+        CharsetEncoder encoder = newCharsetEncoder();
+        int bufferMaxLength = estimateBufferLength( eventType.getOpcode().length(), runMode, encoder, 0, message );
         ByteBuffer result = ByteBuffer.allocate( bufferMaxLength );
-        encodeHeader( encoder, result, eventType, runMode );
-        encodeString( encoder, result, message );
+        encode( encoder, result, eventType, runMode, 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( 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( result, operation, runMode );
-        String charsetName = encoder.charset().name();
-        result.put( (byte) charsetName.length() );
-        result.put( (byte) ':' );
-        result.put( DEFAULT_STREAM_ENCODING_BYTES );
-        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( 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 )
@@ -519,46 +435,6 @@ public class LegacyMasterProcessChannelEncoder implements MasterProcessChannelEn
         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;
diff --git a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/LegacyMasterProcessChannelDecoder.java b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/LegacyMasterProcessChannelDecoder.java
deleted file mode 100644
index 9d29495..0000000
--- a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/LegacyMasterProcessChannelDecoder.java
+++ /dev/null
@@ -1,190 +0,0 @@
-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.surefire.api.booter.Command;
-import org.apache.maven.surefire.api.booter.DumpErrorSingleton;
-import org.apache.maven.surefire.api.booter.MasterProcessCommand;
-import org.apache.maven.surefire.api.booter.MasterProcessChannelDecoder;
-import org.apache.maven.surefire.api.util.internal.ImmutableMap;
-
-import javax.annotation.Nonnull;
-import java.io.EOFException;
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.channels.ReadableByteChannel;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import static org.apache.maven.surefire.api.booter.MasterProcessCommand.BYE_ACK;
-import static org.apache.maven.surefire.api.booter.MasterProcessCommand.MAGIC_NUMBER;
-import static org.apache.maven.surefire.api.booter.MasterProcessCommand.NOOP;
-import static org.apache.maven.surefire.api.booter.MasterProcessCommand.RUN_CLASS;
-import static org.apache.maven.surefire.api.booter.MasterProcessCommand.SHUTDOWN;
-import static org.apache.maven.surefire.api.booter.MasterProcessCommand.SKIP_SINCE_NEXT_TEST;
-import static org.apache.maven.surefire.api.booter.MasterProcessCommand.TEST_SET_FINISHED;
-
-/**
- * magic number : opcode [: opcode specific data]*
- * <br>
- *
- * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
- * @since 3.0.0-M5
- */
-public class LegacyMasterProcessChannelDecoder implements MasterProcessChannelDecoder
-{
-    private static final Map<String, MasterProcessCommand> COMMAND_OPCODES = stringsToOpcodes();
-
-    private final ReadableByteChannel channel;
-
-    public LegacyMasterProcessChannelDecoder( @Nonnull ReadableByteChannel channel )
-    {
-        this.channel = channel;
-    }
-
-    @Override
-    @Nonnull
-    @SuppressWarnings( "checkstyle:innerassignment" )
-    public Command decode() throws IOException
-    {
-        List<String> tokens = new ArrayList<>( 3 );
-        StringBuilder token = new StringBuilder( MAGIC_NUMBER.length() );
-        ByteBuffer buffer = ByteBuffer.allocate( 1 );
-
-        start:
-        do
-        {
-            boolean endOfStream;
-            tokens.clear();
-            token.setLength( 0 );
-            FrameCompletion completion = null;
-            for ( boolean frameStarted = false; !( endOfStream = channel.read( buffer ) == -1 ); completion = null )
-            {
-                buffer.flip();
-                char c = (char) buffer.get();
-                buffer.clear();
-
-                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 )
-                        {
-                            break;
-                        }
-                        else if ( completion == FrameCompletion.MALFORMED )
-                        {
-                            DumpErrorSingleton.getSingleton()
-                                .dumpStreamText( "Malformed frame with tokens " + tokens );
-                            continue start;
-                        }
-                    }
-                    else
-                    {
-                        token.append( c );
-                    }
-                }
-            }
-
-            if ( completion == FrameCompletion.COMPLETE )
-            {
-                MasterProcessCommand cmd = COMMAND_OPCODES.get( tokens.get( 1 ) );
-                if ( tokens.size() == 2 )
-                {
-                    return new Command( cmd );
-                }
-                else if ( tokens.size() == 3 )
-                {
-                    return new Command( cmd, tokens.get( 2 ) );
-                }
-            }
-
-            if ( endOfStream )
-            {
-                throw new EOFException();
-            }
-        }
-        while ( true );
-    }
-
-    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 );
-            MasterProcessCommand cmd = COMMAND_OPCODES.get( opcode );
-            if ( cmd == null )
-            {
-                return FrameCompletion.MALFORMED;
-            }
-            else if ( cmd.hasDataType() == ( tokens.size() == 3 ) )
-            {
-                return FrameCompletion.COMPLETE;
-            }
-        }
-        return FrameCompletion.NOT_COMPLETE;
-    }
-
-    @Override
-    public void close()
-    {
-    }
-
-    /**
-     * Determines whether the frame is complete or malformed.
-     */
-    private enum FrameCompletion
-    {
-        NOT_COMPLETE,
-        COMPLETE,
-        MALFORMED
-    }
-
-    private static Map<String, MasterProcessCommand> stringsToOpcodes()
-    {
-        Map<String, MasterProcessCommand> opcodes = new HashMap<>();
-        opcodes.put( "run-testclass", RUN_CLASS );
-        opcodes.put( "testset-finished", TEST_SET_FINISHED );
-        opcodes.put( "skip-since-next-test", SKIP_SINCE_NEXT_TEST );
-        opcodes.put( "shutdown", SHUTDOWN );
-        opcodes.put( "noop", NOOP );
-        opcodes.put( "bye-ack", BYE_ACK );
-        return new ImmutableMap<>( opcodes );
-    }
-}
diff --git a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/LegacyMasterProcessChannelProcessorFactory.java b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/LegacyMasterProcessChannelProcessorFactory.java
index 6e28764..3d96f06 100644
--- a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/LegacyMasterProcessChannelProcessorFactory.java
+++ b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/LegacyMasterProcessChannelProcessorFactory.java
@@ -21,8 +21,10 @@ package org.apache.maven.surefire.booter.spi;
 
 import org.apache.maven.surefire.api.booter.MasterProcessChannelDecoder;
 import org.apache.maven.surefire.api.booter.MasterProcessChannelEncoder;
+import org.apache.maven.surefire.api.fork.ForkNodeArguments;
 import org.apache.maven.surefire.api.util.internal.WritableBufferedByteChannel;
 
+import javax.annotation.Nonnull;
 import java.io.IOException;
 import java.net.MalformedURLException;
 
@@ -51,21 +53,21 @@ public class LegacyMasterProcessChannelProcessorFactory
     {
         if ( !canUse( channelConfig ) )
         {
-            throw new MalformedURLException( "Unknown chanel string " + channelConfig );
+            throw new MalformedURLException( "Unknown channel string " + channelConfig );
         }
     }
 
     @Override
-    public MasterProcessChannelDecoder createDecoder()
+    public MasterProcessChannelDecoder createDecoder( @Nonnull ForkNodeArguments forkingArguments )
     {
-        return new LegacyMasterProcessChannelDecoder( newBufferedChannel( System.in ) );
+        return new CommandChannelDecoder( newBufferedChannel( System.in ), forkingArguments );
     }
 
     @Override
-    public MasterProcessChannelEncoder createEncoder()
+    public MasterProcessChannelEncoder createEncoder( @Nonnull ForkNodeArguments forkingArguments )
     {
         WritableBufferedByteChannel channel = newBufferedChannel( System.out );
         schedulePeriodicFlusher( FLUSH_PERIOD_MILLIS, channel );
-        return new LegacyMasterProcessChannelEncoder( channel );
+        return new EventChannelEncoder( channel );
     }
 }
diff --git a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/SurefireMasterProcessChannelProcessorFactory.java b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/SurefireMasterProcessChannelProcessorFactory.java
index 0bebeb4..6232209 100644
--- a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/SurefireMasterProcessChannelProcessorFactory.java
+++ b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/SurefireMasterProcessChannelProcessorFactory.java
@@ -21,8 +21,10 @@ package org.apache.maven.surefire.booter.spi;
 
 import org.apache.maven.surefire.api.booter.MasterProcessChannelDecoder;
 import org.apache.maven.surefire.api.booter.MasterProcessChannelEncoder;
+import org.apache.maven.surefire.api.fork.ForkNodeArguments;
 import org.apache.maven.surefire.api.util.internal.WritableBufferedByteChannel;
 
+import javax.annotation.Nonnull;
 import java.io.IOException;
 import java.net.InetSocketAddress;
 import java.net.MalformedURLException;
@@ -31,6 +33,7 @@ import java.net.URI;
 import java.net.URISyntaxException;
 import java.nio.ByteBuffer;
 import java.nio.channels.AsynchronousSocketChannel;
+import java.nio.channels.ReadableByteChannel;
 import java.util.StringTokenizer;
 import java.util.concurrent.ExecutionException;
 
@@ -69,7 +72,7 @@ public class SurefireMasterProcessChannelProcessorFactory
     {
         if ( !canUse( channelConfig ) )
         {
-            throw new MalformedURLException( "Unknown chanel string " + channelConfig );
+            throw new MalformedURLException( "Unknown channel string " + channelConfig );
         }
 
         try
@@ -97,17 +100,18 @@ public class SurefireMasterProcessChannelProcessorFactory
     }
 
     @Override
-    public MasterProcessChannelDecoder createDecoder()
+    public MasterProcessChannelDecoder createDecoder( @Nonnull ForkNodeArguments forkingArguments )
     {
-        return new LegacyMasterProcessChannelDecoder( newBufferedChannel( newInputStream( clientSocketChannel ) ) );
+        ReadableByteChannel bufferedChannel = newBufferedChannel( newInputStream( clientSocketChannel ) );
+        return new CommandChannelDecoder( bufferedChannel, forkingArguments );
     }
 
     @Override
-    public MasterProcessChannelEncoder createEncoder()
+    public MasterProcessChannelEncoder createEncoder( @Nonnull ForkNodeArguments forkingArguments )
     {
         WritableBufferedByteChannel channel = newBufferedChannel( newOutputStream( clientSocketChannel ) );
         schedulePeriodicFlusher( FLUSH_PERIOD_MILLIS, channel );
-        return new LegacyMasterProcessChannelEncoder( channel );
+        return new EventChannelEncoder( channel );
     }
 
     @Override
diff --git a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/stream/CommandDecoder.java b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/stream/CommandDecoder.java
new file mode 100644
index 0000000..b660147
--- /dev/null
+++ b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/stream/CommandDecoder.java
@@ -0,0 +1,276 @@
+package org.apache.maven.surefire.booter.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.surefire.api.booter.Command;
+import org.apache.maven.surefire.api.booter.MasterProcessCommand;
+import org.apache.maven.surefire.api.booter.Shutdown;
+import org.apache.maven.surefire.api.fork.ForkNodeArguments;
+import org.apache.maven.surefire.api.report.RunMode;
+import org.apache.maven.surefire.api.stream.AbstractStreamDecoder;
+import org.apache.maven.surefire.api.stream.MalformedChannelException;
+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.concurrent.Callable;
+import java.util.concurrent.FutureTask;
+
+import static org.apache.maven.surefire.api.booter.Command.BYE_ACK;
+import static org.apache.maven.surefire.api.booter.Command.NOOP;
+import static org.apache.maven.surefire.api.booter.Command.SKIP_SINCE_NEXT_TEST;
+import static org.apache.maven.surefire.api.booter.Command.TEST_SET_FINISHED;
+import static org.apache.maven.surefire.api.booter.Command.toRunClass;
+import static org.apache.maven.surefire.api.booter.Command.toShutdown;
+import static org.apache.maven.surefire.api.booter.Constants.MAGIC_NUMBER_FOR_COMMANDS_BYTES;
+import static org.apache.maven.surefire.api.booter.MasterProcessCommand.COMMAND_TYPES;
+import static org.apache.maven.surefire.api.report.RunMode.RUN_MODES;
+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;
+
+/**
+ *
+ */
+public class CommandDecoder extends AbstractStreamDecoder<Command, MasterProcessCommand, SegmentType>
+{
+    private static final int DEBUG_SINK_BUFFER_SIZE = 64 * 1024;
+    private static final int NO_POSITION = -1;
+
+    private static final SegmentType[] COMMAND_WITHOUT_DATA = new SegmentType[] {
+        END_OF_FRAME
+    };
+
+    private static final SegmentType[] COMMAND_WITH_RUNNABLE_STRING = new SegmentType[] {
+        RUN_MODE,
+        STRING_ENCODING,
+        DATA_STRING,
+        END_OF_FRAME
+    };
+
+    private static final SegmentType[] COMMAND_WITH_ONE_STRING = new SegmentType[] {
+        STRING_ENCODING,
+        DATA_STRING,
+        END_OF_FRAME
+    };
+
+    private final ForkNodeArguments arguments;
+    private final OutputStream debugSink;
+
+    public CommandDecoder( @Nonnull ReadableByteChannel channel,
+                           @Nonnull ForkNodeArguments arguments )
+    {
+        super( channel, arguments, COMMAND_TYPES );
+        this.arguments = arguments;
+        debugSink = newDebugSink();
+    }
+
+    @Override
+    public Command decode( @Nonnull Memento memento ) throws IOException, MalformedChannelException
+    {
+        try
+        {
+            MasterProcessCommand commandType = readMessageType( memento );
+            if ( commandType == null )
+            {
+                throw new MalformedFrameException( memento.getLine().getPositionByteBuffer(),
+                    memento.getByteBuffer().position() );
+            }
+            RunMode runMode = null;
+            for ( SegmentType segmentType : nextSegmentType( commandType ) )
+            {
+                switch ( segmentType )
+                {
+                    case RUN_MODE:
+                        runMode = RUN_MODES.get( readSegment( 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() );
+                        return toMessage( commandType, runMode, memento );
+                    default:
+                        memento.getLine().setPositionByteBuffer( NO_POSITION );
+                        arguments.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 MalformedChannelException();
+    }
+
+    @Nonnull
+    @Override
+    protected final byte[] getEncodedMagicNumber()
+    {
+        return MAGIC_NUMBER_FOR_COMMANDS_BYTES;
+    }
+
+    @Nonnull
+    @Override
+    protected SegmentType[] nextSegmentType( @Nonnull MasterProcessCommand commandType )
+    {
+        switch ( commandType )
+        {
+            case NOOP:
+            case BYE_ACK:
+            case SKIP_SINCE_NEXT_TEST:
+            case TEST_SET_FINISHED:
+                return COMMAND_WITHOUT_DATA;
+            case RUN_CLASS:
+                return COMMAND_WITH_RUNNABLE_STRING;
+            case SHUTDOWN:
+                return COMMAND_WITH_ONE_STRING;
+            default:
+                throw new IllegalArgumentException( "Unknown enum " + commandType );
+        }
+    }
+
+    @Nonnull
+    @Override
+    protected Command toMessage( @Nonnull MasterProcessCommand commandType, RunMode runMode, @Nonnull Memento memento )
+        throws MalformedFrameException
+    {
+        switch ( commandType )
+        {
+            case NOOP:
+                checkArguments( memento, 0 );
+                return NOOP;
+            case BYE_ACK:
+                checkArguments( memento, 0 );
+                return BYE_ACK;
+            case SKIP_SINCE_NEXT_TEST:
+                checkArguments( memento, 0 );
+                return SKIP_SINCE_NEXT_TEST;
+            case TEST_SET_FINISHED:
+                checkArguments( memento, 0 );
+                return TEST_SET_FINISHED;
+            case RUN_CLASS:
+                checkArguments( memento, 1 );
+                return toRunClass( (String) memento.getData().get( 0 ) );
+            case SHUTDOWN:
+                checkArguments( memento, 1 );
+                return toShutdown( Shutdown.parameterOf( (String) memento.getData().get( 0 ) ) );
+            default:
+                throw new IllegalArgumentException( "Missing a branch for the event type " + commandType );
+        }
+    }
+
+    @Override
+    protected void debugStream( byte[] array, int position, int remaining )
+    {
+        if ( debugSink == null )
+        {
+            return;
+        }
+
+        try
+        {
+            debugSink.write( array, position, remaining );
+            debugSink.flush();
+        }
+        catch ( IOException e )
+        {
+            // logger file was deleted
+            // System.out is already used by the stream in this decoder
+        }
+    }
+
+    private OutputStream newDebugSink()
+    {
+        final File sink = arguments.getCommandStreamBinaryFile();
+        if ( sink == null )
+        {
+            return null;
+        }
+
+        try
+        {
+            OutputStream fos = new FileOutputStream( sink, true );
+            final OutputStream os = new BufferedOutputStream( fos, DEBUG_SINK_BUFFER_SIZE );
+            Runtime.getRuntime().addShutdownHook( new Thread( new FutureTask<>( new Callable<Void>()
+            {
+                @Override
+                public Void call() throws Exception
+                {
+                    os.close();
+                    return null;
+                }
+            } ) ) );
+            return os;
+        }
+        catch ( FileNotFoundException e )
+        {
+            return null;
+        }
+    }
+
+    @Override
+    public void close() throws IOException
+    {
+        if ( debugSink != null )
+        {
+            debugSink.close();
+        }
+    }
+}
diff --git a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/stream/EventEncoder.java b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/stream/EventEncoder.java
new file mode 100644
index 0000000..f982017
--- /dev/null
+++ b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/stream/EventEncoder.java
@@ -0,0 +1,78 @@
+package org.apache.maven.surefire.booter.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.surefire.api.booter.ForkedProcessEventType;
+import org.apache.maven.surefire.api.stream.AbstractStreamEncoder;
+import org.apache.maven.surefire.api.util.internal.WritableBufferedByteChannel;
+
+import javax.annotation.Nonnull;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
+
+import static org.apache.maven.surefire.api.booter.Constants.DEFAULT_STREAM_ENCODING;
+import static org.apache.maven.surefire.api.booter.Constants.DEFAULT_STREAM_ENCODING_BYTES;
+import static org.apache.maven.surefire.api.booter.Constants.MAGIC_NUMBER_FOR_EVENTS_BYTES;
+
+/**
+ *
+ */
+public class EventEncoder extends AbstractStreamEncoder<ForkedProcessEventType>
+{
+    public EventEncoder( WritableBufferedByteChannel out )
+    {
+        super( out );
+    }
+
+    @Nonnull
+    @Override
+    protected final byte[] getEncodedMagicNumber()
+    {
+        return MAGIC_NUMBER_FOR_EVENTS_BYTES;
+    }
+
+    @Nonnull
+    @Override
+    protected final byte[] enumToByteArray( @Nonnull ForkedProcessEventType e )
+    {
+        return e.getOpcodeBinary();
+    }
+
+    @Nonnull
+    @Override
+    protected final byte[] getEncodedCharsetName()
+    {
+        return DEFAULT_STREAM_ENCODING_BYTES;
+    }
+
+    @Nonnull
+    @Override
+    protected final Charset getCharset()
+    {
+        return DEFAULT_STREAM_ENCODING;
+    }
+
+    @Nonnull
+    @Override
+    protected final CharsetEncoder newCharsetEncoder()
+    {
+        return getCharset().newEncoder();
+    }
+}
diff --git a/surefire-booter/src/test/java/org/apache/maven/surefire/booter/CommandReaderTest.java b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/CommandReaderTest.java
index 5bc5ffe..2cb7267 100644
--- a/surefire-booter/src/test/java/org/apache/maven/surefire/booter/CommandReaderTest.java
+++ b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/CommandReaderTest.java
@@ -23,8 +23,9 @@ import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
 import org.apache.maven.plugin.surefire.log.api.NullConsoleLogger;
 import org.apache.maven.surefire.api.booter.MasterProcessChannelDecoder;
 import org.apache.maven.surefire.api.booter.Shutdown;
-import org.apache.maven.surefire.booter.spi.LegacyMasterProcessChannelDecoder;
-import org.apache.maven.surefire.booter.spi.LegacyMasterProcessChannelEncoder;
+import org.apache.maven.surefire.api.fork.ForkNodeArguments;
+import org.apache.maven.surefire.booter.spi.CommandChannelDecoder;
+import org.apache.maven.surefire.booter.spi.EventChannelEncoder;
 import org.apache.maven.surefire.api.testset.TestSetFailedException;
 import org.apache.maven.surefire.api.util.internal.WritableBufferedByteChannel;
 import org.junit.After;
@@ -45,6 +46,7 @@ import java.util.concurrent.FutureTask;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
 
+import static java.nio.charset.StandardCharsets.US_ASCII;
 import static org.apache.maven.surefire.api.util.internal.Channels.newBufferedChannel;
 import static org.apache.maven.surefire.api.util.internal.Channels.newChannel;
 import static org.fest.assertions.Assertions.assertThat;
@@ -61,6 +63,7 @@ import static org.junit.Assert.fail;
  * @since 2.19
  */
 @RunWith( NewClassLoaderRunner.class )
+@SuppressWarnings( "checkstyle:magicnumber" )
 public class CommandReaderTest
 {
     private static final long DELAY = 200L;
@@ -93,7 +96,9 @@ public class CommandReaderTest
         InputStream realInputStream = new SystemInputStream();
         addTestToPipeline( getClass().getName() );
         ConsoleLogger logger = new NullConsoleLogger();
-        MasterProcessChannelDecoder decoder = new LegacyMasterProcessChannelDecoder( newChannel( realInputStream ) );
+        ForkNodeArguments args = new ForkedNodeArg( 1, false );
+        MasterProcessChannelDecoder decoder =
+            new CommandChannelDecoder( newChannel( realInputStream ), args );
         reader = new CommandReader( decoder, Shutdown.DEFAULT, logger );
     }
 
@@ -106,7 +111,7 @@ public class CommandReaderTest
     @Test
     public void readJustOneClass()
     {
-        Iterator<String> it = reader.getIterableClasses( new LegacyMasterProcessChannelEncoder( nul() ) ).iterator();
+        Iterator<String> it = reader.getIterableClasses( new EventChannelEncoder( nul() ) ).iterator();
         assertTrue( it.hasNext() );
         assertThat( it.next(), is( getClass().getName() ) );
         reader.stop();
@@ -125,7 +130,7 @@ public class CommandReaderTest
     @Test
     public void manyClasses()
     {
-        Iterator<String> it1 = reader.getIterableClasses( new LegacyMasterProcessChannelEncoder( nul() ) ).iterator();
+        Iterator<String> it1 = reader.getIterableClasses( new EventChannelEncoder( nul() ) ).iterator();
         assertThat( it1.next(), is( getClass().getName() ) );
         addTestToPipeline( A.class.getName() );
         assertThat( it1.next(), is( A.class.getName() ) );
@@ -141,7 +146,7 @@ public class CommandReaderTest
     @Test
     public void twoIterators() throws Exception
     {
-        Iterator<String> it1 = reader.getIterableClasses( new LegacyMasterProcessChannelEncoder( nul() ) ).iterator();
+        Iterator<String> it1 = reader.getIterableClasses( new EventChannelEncoder( nul() ) ).iterator();
 
         assertThat( it1.next(), is( getClass().getName() ) );
         addTestToPipeline( A.class.getName() );
@@ -174,8 +179,7 @@ public class CommandReaderTest
             @Override
             public void run()
             {
-                Iterator<String> it =
-                    reader.getIterableClasses( new LegacyMasterProcessChannelEncoder( nul() ) ).iterator();
+                Iterator<String> it = reader.getIterableClasses( new EventChannelEncoder( nul() ) ).iterator();
                 assertThat( it.next(), is( CommandReaderTest.class.getName() ) );
             }
         };
@@ -203,7 +207,7 @@ public class CommandReaderTest
             public void run()
             {
                 Iterator<String> it =
-                    reader.getIterableClasses( new LegacyMasterProcessChannelEncoder( nul() ) ).iterator();
+                    reader.getIterableClasses( new EventChannelEncoder( nul() ) ).iterator();
                 assertThat( it.next(), is( CommandReaderTest.class.getName() ) );
                 counter.countDown();
                 assertThat( it.next(), is( Foo.class.getName() ) );
@@ -250,7 +254,25 @@ public class CommandReaderTest
 
     private void addTestToPipeline( String cls )
     {
-        for ( byte cmdByte : ( ":maven-surefire-command:run-testclass:" + cls + ":" ).getBytes() )
+        int clsLength = cls.length();
+        String cmd = new StringBuilder( 512 )
+            .append( ":maven-surefire-command:" )
+            .append( (char) 13 )
+            .append( ":run-testclass:" )
+            .append( (char) 10 )
+            .append( ":normal-run:" )
+            .append( (char) 5 )
+            .append( ":UTF-8:" )
+            .append( (char) ( clsLength >> 24 ) )
+            .append( (char) ( ( clsLength >> 16 ) & 0xff ) )
+            .append( (char) ( ( clsLength >> 8 ) & 0xff ) )
+            .append( (char) ( clsLength & 0xff ) )
+            .append( ":" )
+            .append( cls )
+            .append( ":" )
+            .toString();
+
+        for ( byte cmdByte : cmd.getBytes( US_ASCII ) )
         {
             blockingStream.add( cmdByte );
         }
@@ -258,7 +280,7 @@ public class CommandReaderTest
 
     private void addEndOfPipeline()
     {
-        for ( byte cmdByte : ":maven-surefire-command:testset-finished:".getBytes() )
+        for ( byte cmdByte : ( ":maven-surefire-command:" + (char) 16 + ":testset-finished:" ).getBytes( US_ASCII ) )
         {
             blockingStream.add( cmdByte );
         }
diff --git a/surefire-booter/src/test/java/org/apache/maven/surefire/booter/ForkedBooterMockTest.java b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/ForkedBooterMockTest.java
index c1140c8..0f3d8db 100644
--- a/surefire-booter/src/test/java/org/apache/maven/surefire/booter/ForkedBooterMockTest.java
+++ b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/ForkedBooterMockTest.java
@@ -21,11 +21,12 @@ package org.apache.maven.surefire.booter;
 
 import org.apache.maven.surefire.api.booter.MasterProcessChannelDecoder;
 import org.apache.maven.surefire.api.booter.MasterProcessChannelEncoder;
+import org.apache.maven.surefire.api.fork.ForkNodeArguments;
 import org.apache.maven.surefire.api.report.StackTraceWriter;
 import org.apache.maven.surefire.api.util.internal.WritableBufferedByteChannel;
 import org.apache.maven.surefire.booter.spi.AbstractMasterProcessChannelProcessorFactory;
-import org.apache.maven.surefire.booter.spi.LegacyMasterProcessChannelDecoder;
-import org.apache.maven.surefire.booter.spi.LegacyMasterProcessChannelEncoder;
+import org.apache.maven.surefire.booter.spi.CommandChannelDecoder;
+import org.apache.maven.surefire.booter.spi.EventChannelEncoder;
 import org.apache.maven.surefire.booter.spi.LegacyMasterProcessChannelProcessorFactory;
 import org.apache.maven.surefire.booter.spi.SurefireMasterProcessChannelProcessorFactory;
 import org.apache.maven.surefire.shared.utils.cli.ShutdownHookUtils;
@@ -44,6 +45,7 @@ import org.powermock.core.classloader.annotations.PowerMockIgnore;
 import org.powermock.core.classloader.annotations.PrepareForTest;
 import org.powermock.modules.junit4.PowerMockRunner;
 
+import javax.annotation.Nonnull;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.net.InetSocketAddress;
@@ -86,7 +88,7 @@ import static org.powermock.reflect.Whitebox.setInternalState;
 @PrepareForTest( {
                      PpidChecker.class,
                      ForkedBooter.class,
-                     LegacyMasterProcessChannelEncoder.class,
+                     EventChannelEncoder.class,
                      ShutdownHookUtils.class
 } )
 @PowerMockIgnore( { "org.jacoco.agent.rt.*", "com.vladium.emma.rt.*" } )
@@ -105,7 +107,7 @@ public class ForkedBooterMockTest
     private MasterProcessChannelProcessorFactory channelProcessorFactory;
 
     @Mock
-    private LegacyMasterProcessChannelEncoder eventChannel;
+    private EventChannelEncoder eventChannel;
 
     @Captor
     private ArgumentCaptor<StackTraceWriter> capturedStackTraceWriter;
@@ -292,10 +294,11 @@ public class ForkedBooterMockTest
 
             factory.connect( "pipe://3" );
 
-            MasterProcessChannelDecoder decoder = factory.createDecoder();
-            assertThat( decoder ).isInstanceOf( LegacyMasterProcessChannelDecoder.class );
-            MasterProcessChannelEncoder encoder = factory.createEncoder();
-            assertThat( encoder ).isInstanceOf( LegacyMasterProcessChannelEncoder.class );
+            ForkNodeArguments args = new ForkedNodeArg( 1, false );
+            MasterProcessChannelDecoder decoder = factory.createDecoder( args );
+            assertThat( decoder ).isInstanceOf( CommandChannelDecoder.class );
+            MasterProcessChannelEncoder encoder = factory.createEncoder( args );
+            assertThat( encoder ).isInstanceOf( EventChannelEncoder.class );
         }
     }
 
@@ -318,13 +321,13 @@ public class ForkedBooterMockTest
             }
 
             @Override
-            public MasterProcessChannelDecoder createDecoder()
+            public MasterProcessChannelDecoder createDecoder( @Nonnull  ForkNodeArguments args )
             {
                 return null;
             }
 
             @Override
-            public MasterProcessChannelEncoder createEncoder()
+            public MasterProcessChannelEncoder createEncoder( @Nonnull ForkNodeArguments args )
             {
                 return null;
             }
@@ -411,13 +414,13 @@ public class ForkedBooterMockTest
                 } );
 
                 factory.connect( "tcp://localhost:" + serverPort );
-
-                MasterProcessChannelDecoder decoder = factory.createDecoder();
+                ForkNodeArguments args = new ForkedNodeArg( 1, false );
+                MasterProcessChannelDecoder decoder = factory.createDecoder( args );
                 assertThat( decoder )
-                    .isInstanceOf( LegacyMasterProcessChannelDecoder.class );
-                MasterProcessChannelEncoder encoder = factory.createEncoder();
+                    .isInstanceOf( CommandChannelDecoder.class );
+                MasterProcessChannelEncoder encoder = factory.createEncoder( args );
                 assertThat( encoder )
-                    .isInstanceOf( LegacyMasterProcessChannelEncoder.class );
+                    .isInstanceOf( EventChannelEncoder.class );
             }
         }
     }
diff --git a/surefire-booter/src/test/java/org/apache/maven/surefire/booter/JUnit4SuiteTest.java b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/JUnit4SuiteTest.java
index 280774e..6c14e0e 100644
--- a/surefire-booter/src/test/java/org/apache/maven/surefire/booter/JUnit4SuiteTest.java
+++ b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/JUnit4SuiteTest.java
@@ -23,8 +23,8 @@ import junit.framework.JUnit4TestAdapter;
 import junit.framework.Test;
 import junit.framework.TestCase;
 import junit.framework.TestSuite;
-import org.apache.maven.surefire.booter.spi.LegacyMasterProcessChannelDecoderTest;
-import org.apache.maven.surefire.booter.spi.LegacyMasterProcessChannelEncoderTest;
+import org.apache.maven.surefire.booter.spi.CommandChannelDecoderTest;
+import org.apache.maven.surefire.booter.spi.EventChannelEncoderTest;
 
 /**
  * Adapt the JUnit4 tests which use only annotations to the JUnit3 test suite.
@@ -46,8 +46,8 @@ public class JUnit4SuiteTest extends TestCase
         suite.addTest( new JUnit4TestAdapter( BooterDeserializerTest.class ) );
         suite.addTestSuite( ClasspathTest.class );
         suite.addTestSuite( PropertiesWrapperTest.class );
-        suite.addTest( new JUnit4TestAdapter( LegacyMasterProcessChannelDecoderTest.class ) );
-        suite.addTest( new JUnit4TestAdapter( LegacyMasterProcessChannelEncoderTest.class ) );
+        suite.addTest( new JUnit4TestAdapter( CommandChannelDecoderTest.class ) );
+        suite.addTest( new JUnit4TestAdapter( EventChannelEncoderTest.class ) );
         suite.addTestSuite( SurefireReflectorTest.class );
         return suite;
     }
diff --git a/surefire-booter/src/test/java/org/apache/maven/surefire/booter/spi/CommandChannelDecoderTest.java b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/spi/CommandChannelDecoderTest.java
new file mode 100644
index 0000000..f31d9d8
--- /dev/null
+++ b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/spi/CommandChannelDecoderTest.java
@@ -0,0 +1,519 @@
+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.ConsoleLogger;
+import org.apache.maven.surefire.api.booter.Command;
+import org.apache.maven.surefire.api.booter.DumpErrorSingleton;
+import org.apache.maven.surefire.api.booter.Shutdown;
+import org.apache.maven.surefire.api.fork.ForkNodeArguments;
+import org.apache.maven.surefire.booter.ForkedNodeArg;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import javax.annotation.Nonnull;
+import java.io.ByteArrayInputStream;
+import java.io.EOFException;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import static java.nio.channels.Channels.newChannel;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.maven.surefire.api.booter.MasterProcessCommand.BYE_ACK;
+import static org.apache.maven.surefire.api.booter.MasterProcessCommand.NOOP;
+import static org.apache.maven.surefire.api.booter.MasterProcessCommand.RUN_CLASS;
+import static org.apache.maven.surefire.api.booter.MasterProcessCommand.SHUTDOWN;
+import static org.apache.maven.surefire.api.booter.MasterProcessCommand.SKIP_SINCE_NEXT_TEST;
+import static org.apache.maven.surefire.api.booter.MasterProcessCommand.TEST_SET_FINISHED;
+import static org.apache.maven.surefire.api.booter.Shutdown.DEFAULT;
+import static org.apache.maven.surefire.api.booter.Shutdown.EXIT;
+import static org.apache.maven.surefire.api.booter.Shutdown.KILL;
+import static org.fest.assertions.Assertions.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+/**
+ * Tests for {@link CommandChannelDecoder}.
+ */
+@SuppressWarnings( "checkstyle:magicnumber" )
+public class CommandChannelDecoderTest
+{
+    @Rule
+    public final TemporaryFolder tempFolder = new TemporaryFolder();
+
+    private File reportsDir;
+    private String dumpFileName;
+
+    @Before
+    public void initTmpFile() throws IOException
+    {
+        File tmp = tempFolder.newFile();
+        reportsDir = tmp.getParentFile();
+        dumpFileName = tmp.getName();
+    }
+
+    @Test
+    public void testDecoderRunClass() throws IOException
+    {
+        assertEquals( String.class, RUN_CLASS.getDataType() );
+        byte[] encoded = new StringBuilder( 512 )
+            .append( ":maven-surefire-command:" )
+            .append( (char) 13 )
+            .append( ":run-testclass:" )
+            .append( (char) 10 )
+            .append( ":normal-run:" )
+            .append( (char) 5 )
+            .append( ":UTF-8:" )
+            .append( (char) 0 )
+            .append( (char) 0 )
+            .append( (char) 0 )
+            .append( (char) 8 )
+            .append( ":" )
+            .append( "pkg.Test" )
+            .append( ":" )
+            .toString()
+            .getBytes( UTF_8 );
+        InputStream is = new ByteArrayInputStream( encoded );
+        DumpErrorSingleton.getSingleton().init( reportsDir, dumpFileName );
+        ForkNodeArguments args = new ForkedNodeArg( 1, false );
+        CommandChannelDecoder decoder = new CommandChannelDecoder( newChannel( is ), args );
+        Command command = decoder.decode();
+        assertThat( command.getCommandType() ).isSameAs( RUN_CLASS );
+        assertThat( command.getData() ).isEqualTo( "pkg.Test" );
+    }
+
+    @Test
+    public void testDecoderTestsetFinished() throws IOException
+    {
+        Command command = Command.TEST_SET_FINISHED;
+        assertThat( command.getCommandType() ).isSameAs( TEST_SET_FINISHED );
+        assertEquals( Void.class, TEST_SET_FINISHED.getDataType() );
+        byte[] encoded = ":maven-surefire-command:\u0010:testset-finished:".getBytes();
+        ByteArrayInputStream is = new ByteArrayInputStream( encoded );
+        DumpErrorSingleton.getSingleton().init( reportsDir, dumpFileName );
+        ForkNodeArguments args = new ForkedNodeArg( 1, false );
+        CommandChannelDecoder decoder = new CommandChannelDecoder( newChannel( is ), args );
+        command = decoder.decode();
+        assertThat( command.getCommandType() ).isSameAs( TEST_SET_FINISHED );
+        assertNull( command.getData() );
+    }
+
+    @Test
+    public void testDecoderSkipSinceNextTest() throws IOException
+    {
+        Command command = Command.SKIP_SINCE_NEXT_TEST;
+        assertThat( command.getCommandType() ).isSameAs( SKIP_SINCE_NEXT_TEST );
+        assertEquals( Void.class, SKIP_SINCE_NEXT_TEST.getDataType() );
+        byte[] encoded = ":maven-surefire-command:\u0014:skip-since-next-test:".getBytes();
+        ByteArrayInputStream is = new ByteArrayInputStream( encoded );
+        DumpErrorSingleton.getSingleton().init( reportsDir, dumpFileName );
+        ForkNodeArguments args = new ForkedNodeArg( 1, false );
+        CommandChannelDecoder decoder = new CommandChannelDecoder( newChannel( is ), args );
+        command = decoder.decode();
+        assertThat( command.getCommandType() ).isSameAs( SKIP_SINCE_NEXT_TEST );
+        assertNull( command.getData() );
+    }
+
+    @Test
+    public void testDecoderShutdownWithExit() throws IOException
+    {
+        Shutdown shutdownType = EXIT;
+        assertEquals( String.class, SHUTDOWN.getDataType() );
+        byte[] encoded = ( ":maven-surefire-command:\u0008:shutdown:\u0005:UTF-8:\u0000\u0000\u0000\u0004:"
+            + shutdownType.getParam() + ":" ).getBytes();
+        ByteArrayInputStream is = new ByteArrayInputStream( encoded );
+        DumpErrorSingleton.getSingleton().init( reportsDir, dumpFileName );
+        ForkNodeArguments args = new ForkedNodeArg( 1, false );
+        CommandChannelDecoder decoder = new CommandChannelDecoder( newChannel( is ), args );
+        Command command = decoder.decode();
+        assertThat( command.getCommandType() ).isSameAs( SHUTDOWN );
+        assertThat( command.getData() ).isEqualTo( shutdownType.name() );
+    }
+
+    @Test
+    public void testDecoderShutdownWithKill() throws IOException
+    {
+        Shutdown shutdownType = KILL;
+        assertEquals( String.class, SHUTDOWN.getDataType() );
+        byte[] encoded = ( ":maven-surefire-command:\u0008:shutdown:\u0005:UTF-8:\u0000\u0000\u0000\u0004:"
+            + shutdownType.getParam() + ":" ).getBytes();
+        ByteArrayInputStream is = new ByteArrayInputStream( encoded );
+        DumpErrorSingleton.getSingleton().init( reportsDir, dumpFileName );
+        ForkNodeArguments args = new ForkedNodeArg( 1, false );
+        CommandChannelDecoder decoder = new CommandChannelDecoder( newChannel( is ), args );
+        Command command = decoder.decode();
+        assertThat( command.getCommandType() ).isSameAs( SHUTDOWN );
+        assertThat( command.getData() ).isEqualTo( shutdownType.name() );
+    }
+
+    @Test
+    public void testDecoderShutdownWithDefault() throws IOException
+    {
+        Shutdown shutdownType = DEFAULT;
+        assertEquals( String.class, SHUTDOWN.getDataType() );
+        byte[] encoded = ( ":maven-surefire-command:\u0008:shutdown:\u0005:UTF-8:\u0000\u0000\u0000\u0007:"
+            + shutdownType.getParam() + ":" ).getBytes();
+        ByteArrayInputStream is = new ByteArrayInputStream( encoded );
+        DumpErrorSingleton.getSingleton().init( reportsDir, dumpFileName );
+        ForkNodeArguments args = new ForkedNodeArg( 1, false );
+        CommandChannelDecoder decoder = new CommandChannelDecoder( newChannel( is ), args );
+        Command command = decoder.decode();
+        assertThat( command.getCommandType() ).isSameAs( SHUTDOWN );
+        assertThat( command.getData() ).isEqualTo( shutdownType.name() );
+    }
+
+    @Test
+    public void testDecoderNoop() throws IOException
+    {
+        assertThat( NOOP ).isSameAs( Command.NOOP.getCommandType() );
+        assertEquals( Void.class, NOOP.getDataType() );
+        byte[] encoded = ":maven-surefire-command:\u0004:noop:".getBytes();
+        ByteArrayInputStream is = new ByteArrayInputStream( encoded );
+        DumpErrorSingleton.getSingleton().init( reportsDir, dumpFileName );
+        ForkNodeArguments args = new ForkedNodeArg( 1, false );
+        CommandChannelDecoder decoder = new CommandChannelDecoder( newChannel( is ), args );
+        Command command = decoder.decode();
+        assertThat( command.getCommandType() ).isSameAs( NOOP );
+        assertNull( command.getData() );
+    }
+
+    @Test
+    public void shouldIgnoreDamagedStream() throws IOException
+    {
+        assertThat( BYE_ACK ).isSameAs( Command.BYE_ACK.getCommandType() );
+        assertEquals( Void.class, BYE_ACK.getDataType() );
+        byte[] encoded = ":maven-surefire-command:\u0007:bye-ack:".getBytes();
+        byte[] streamContent = ( "<something>" + new String( encoded ) + "<damaged>" ).getBytes();
+        ByteArrayInputStream is = new ByteArrayInputStream( streamContent );
+        DumpErrorSingleton.getSingleton().init( reportsDir, dumpFileName );
+        ForkNodeArguments args = new ForkedNodeArg( 1, false );
+        CommandChannelDecoder decoder = new CommandChannelDecoder( newChannel( is ), args );
+        Command command = decoder.decode();
+        assertThat( command.getCommandType() ).isSameAs( BYE_ACK );
+        assertNull( command.getData() );
+    }
+
+    @Test
+    public void shouldIgnoreDamagedHeader() throws IOException
+    {
+        assertThat( BYE_ACK ).isSameAs( Command.BYE_ACK.getCommandType() );
+        assertEquals( Void.class, BYE_ACK.getDataType() );
+        byte[] encoded = ":maven-surefire-command:\u0007:bye-ack:".getBytes();
+        byte[] streamContent = ( ":<damaged>:" + new String( encoded ) ).getBytes();
+        ByteArrayInputStream is = new ByteArrayInputStream( streamContent );
+        DumpErrorSingleton.getSingleton().init( reportsDir, dumpFileName );
+        ForkNodeArguments args = new ForkedNodeArg( 1, false );
+        CommandChannelDecoder decoder = new CommandChannelDecoder( newChannel( is ), args );
+        Command command = decoder.decode();
+        assertThat( command.getCommandType() ).isSameAs( BYE_ACK );
+        assertNull( command.getData() );
+    }
+
+    @Test
+    public void testDecoderByeAck() throws IOException
+    {
+        assertThat( BYE_ACK ).isSameAs( Command.BYE_ACK.getCommandType() );
+        assertEquals( Void.class, BYE_ACK.getDataType() );
+        byte[] encoded = ":maven-surefire-command:\u0007:bye-ack:".getBytes();
+        ByteArrayInputStream is = new ByteArrayInputStream( encoded );
+        DumpErrorSingleton.getSingleton().init( reportsDir, dumpFileName );
+        ForkNodeArguments args = new ForkedNodeArg( 1, false );
+        CommandChannelDecoder decoder = new CommandChannelDecoder( newChannel( is ), args );
+        Command command = decoder.decode();
+        assertThat( command.getCommandType() ).isSameAs( BYE_ACK );
+        assertNull( command.getData() );
+    }
+
+    @Test
+    public void shouldDecodeTwoCommands() throws IOException
+    {
+        String cmd = ":maven-surefire-command:\u0007:bye-ack:\r\n:maven-surefire-command:\u0007:bye-ack:";
+        InputStream is = new ByteArrayInputStream( cmd.getBytes() );
+        DumpErrorSingleton.getSingleton().init( reportsDir, dumpFileName );
+        ForkNodeArguments args = new ForkedNodeArg( 1, false );
+        CommandChannelDecoder decoder = new CommandChannelDecoder( newChannel( is ), args );
+
+        Command command = decoder.decode();
+        assertThat( command.getCommandType() ).isEqualTo( BYE_ACK );
+        assertThat( command.getData() ).isNull();
+
+        command = decoder.decode();
+        assertThat( command.getCommandType() ).isEqualTo( BYE_ACK );
+        assertThat( command.getData() ).isNull();
+
+        decoder.close();
+    }
+
+    @Test( expected = EOFException.class )
+    public void testIncompleteCommand() throws IOException
+    {
+
+        ByteArrayInputStream is = new ByteArrayInputStream( ":maven-surefire-command:".getBytes() );
+        DumpErrorSingleton.getSingleton().init( reportsDir, dumpFileName );
+        ForkNodeArguments args = new ForkedNodeArg( 1, false );
+        CommandChannelDecoder decoder = new CommandChannelDecoder( newChannel( is ), args );
+        decoder.decode();
+        fail();
+    }
+
+    @Test( expected = EOFException.class )
+    public void testIncompleteCommandStart() throws IOException
+    {
+
+        ByteArrayInputStream is = new ByteArrayInputStream( new byte[] {':', '\r'} );
+        DumpErrorSingleton.getSingleton().init( reportsDir, dumpFileName );
+        ForkNodeArguments args = new ForkedNodeArg( 1, false );
+        CommandChannelDecoder decoder = new CommandChannelDecoder( newChannel( is ), args );
+        decoder.decode();
+        fail();
+    }
+
+    @Test( expected = EOFException.class )
+    public void shouldNotDecodeCorruptedCommand() throws IOException
+    {
+        String cmd = ":maven-surefire-command:\u0007:bye-ack ::maven-surefire-command:";
+        InputStream is = new ByteArrayInputStream( cmd.getBytes() );
+        DumpErrorSingleton.getSingleton().init( reportsDir, dumpFileName );
+        ForkNodeArguments args = new ForkedNodeArg( 1, false );
+        CommandChannelDecoder decoder = new CommandChannelDecoder( newChannel( is ), args );
+
+        decoder.decode();
+    }
+
+    @Test
+    public void shouldSkipCorruptedCommand() throws IOException
+    {
+        String cmd = ":maven-surefire-command:\0007:bye-ack\r\n::maven-surefire-command:\u0004:noop:";
+        InputStream is = new ByteArrayInputStream( cmd.getBytes() );
+        DumpErrorSingleton.getSingleton().init( reportsDir, dumpFileName );
+        ForkNodeArguments args = new ForkedNodeArg( 1, false );
+        CommandChannelDecoder decoder = new CommandChannelDecoder( newChannel( is ), args );
+
+        Command command = decoder.decode();
+        assertThat( command.getCommandType() ).isSameAs( NOOP );
+        assertNull( command.getData() );
+    }
+
+    @Test
+    public void testBinaryCommandStream() throws Exception
+    {
+        InputStream commands = getClass().getResourceAsStream( "/binary-commands/75171711-encoder.bin" );
+        ConsoleLoggerMock logger = new ConsoleLoggerMock( true, true, true, true );
+        ForkNodeArguments args = new ForkNodeArgumentsMock( logger, new File( "" ) );
+        CommandChannelDecoder decoder = new CommandChannelDecoder( newChannel( commands ), args );
+
+        Command command = decoder.decode();
+        assertThat( command ).isNotNull();
+        assertThat( command.getCommandType() ).isEqualTo( NOOP );
+        assertThat( command.getData() ).isNull();
+
+        command = decoder.decode();
+        assertThat( command ).isNotNull();
+        assertThat( command.getCommandType() ).isEqualTo( RUN_CLASS );
+        assertThat( command.getData() ).isEqualTo( "pkg.ATest" );
+
+        for ( int i = 0; i < 24; i++ )
+        {
+            command = decoder.decode();
+            assertThat( command ).isNotNull();
+            assertThat( command.getCommandType() ).isEqualTo( NOOP );
+            assertThat( command.getData() ).isNull();
+        }
+    }
+
+    /**
+     * Threadsafe impl. Mockito and Powermock are not thread-safe.
+     */
+    private static class ForkNodeArgumentsMock implements ForkNodeArguments
+    {
+        private final ConcurrentLinkedQueue<String> dumpStreamText = new ConcurrentLinkedQueue<>();
+        private final ConcurrentLinkedQueue<String> logWarningAtEnd = new ConcurrentLinkedQueue<>();
+        private final ConsoleLogger logger;
+        private final File dumpStreamTextFile;
+
+        ForkNodeArgumentsMock( ConsoleLogger logger, File dumpStreamTextFile )
+        {
+            this.logger = logger;
+            this.dumpStreamTextFile = dumpStreamTextFile;
+        }
+
+        @Nonnull
+        @Override
+        public String getSessionId()
+        {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public int getForkChannelId()
+        {
+            return 0;
+        }
+
+        @Nonnull
+        @Override
+        public File dumpStreamText( @Nonnull String text )
+        {
+            dumpStreamText.add( text );
+            return dumpStreamTextFile;
+        }
+
+        @Nonnull
+        @Override
+        public File dumpStreamException( @Nonnull Throwable t )
+        {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void logWarningAtEnd( @Nonnull String text )
+        {
+            logWarningAtEnd.add( text );
+        }
+
+        @Nonnull
+        @Override
+        public ConsoleLogger getConsoleLogger()
+        {
+            return logger;
+        }
+
+        boolean isCalled()
+        {
+            return !dumpStreamText.isEmpty() || !logWarningAtEnd.isEmpty();
+        }
+
+        @Override
+        public File getEventStreamBinaryFile()
+        {
+            return null;
+        }
+
+        @Override
+        public File getCommandStreamBinaryFile()
+        {
+            return null;
+        }
+    }
+
+    /**
+     * Threadsafe impl. Mockito and Powermock are not thread-safe.
+     */
+    private static class ConsoleLoggerMock implements ConsoleLogger
+    {
+        final ConcurrentLinkedQueue<String> debug = new ConcurrentLinkedQueue<>();
+        final ConcurrentLinkedQueue<String> info = new ConcurrentLinkedQueue<>();
+        final ConcurrentLinkedQueue<String> error = new ConcurrentLinkedQueue<>();
+        final boolean isDebug;
+        final boolean isInfo;
+        final boolean isWarning;
+        final boolean isError;
+        boolean called;
+        boolean isDebugEnabledCalled;
+        boolean isInfoEnabledCalled;
+
+        ConsoleLoggerMock( boolean isDebug, boolean isInfo, boolean isWarning, boolean isError )
+        {
+            this.isDebug = isDebug;
+            this.isInfo = isInfo;
+            this.isWarning = isWarning;
+            this.isError = isError;
+        }
+
+        @Override
+        public boolean isDebugEnabled()
+        {
+            isDebugEnabledCalled = true;
+            called = true;
+            return isDebug;
+        }
+
+        @Override
+        public void debug( String message )
+        {
+            debug.add( message );
+            called = true;
+        }
+
+        @Override
+        public boolean isInfoEnabled()
+        {
+            isInfoEnabledCalled = true;
+            called = true;
+            return isInfo;
+        }
+
+        @Override
+        public void info( String message )
+        {
+            info.add( message );
+            called = true;
+        }
+
+        @Override
+        public boolean isWarnEnabled()
+        {
+            called = true;
+            return isWarning;
+        }
+
+        @Override
+        public void warning( String message )
+        {
+            called = true;
+        }
+
+        @Override
+        public boolean isErrorEnabled()
+        {
+            called = true;
+            return isError;
+        }
+
+        @Override
+        public void error( String message )
+        {
+            error.add( message );
+            called = true;
+        }
+
+        @Override
+        public void error( String message, Throwable t )
+        {
+            called = true;
+        }
+
+        @Override
+        public void error( Throwable t )
+        {
+            called = true;
+        }
+
+        boolean isCalled()
+        {
+            return called;
+        }
+    }
+}
diff --git a/surefire-booter/src/test/java/org/apache/maven/surefire/booter/spi/LegacyMasterProcessChannelEncoderTest.java b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/spi/EventChannelEncoderTest.java
similarity index 81%
rename from surefire-booter/src/test/java/org/apache/maven/surefire/booter/spi/LegacyMasterProcessChannelEncoderTest.java
rename to surefire-booter/src/test/java/org/apache/maven/surefire/booter/spi/EventChannelEncoderTest.java
index 11f2d16..44abbf9 100644
--- a/surefire-booter/src/test/java/org/apache/maven/surefire/booter/spi/LegacyMasterProcessChannelEncoderTest.java
+++ b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/spi/EventChannelEncoderTest.java
@@ -29,152 +29,33 @@ import org.junit.Test;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.io.LineNumberReader;
 import java.io.PrintStream;
-import java.io.StringReader;
 import java.nio.ByteBuffer;
-import java.nio.charset.Charset;
-import java.nio.charset.CharsetEncoder;
 import java.util.Map;
 import java.util.Map.Entry;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Arrays.copyOfRange;
-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.util.internal.Channels.newBufferedChannel;
-import static org.apache.maven.surefire.booter.spi.LegacyMasterProcessChannelEncoder.encode;
-import static org.apache.maven.surefire.booter.spi.LegacyMasterProcessChannelEncoder.encodeHeader;
-import static org.apache.maven.surefire.booter.spi.LegacyMasterProcessChannelEncoder.encodeMessage;
-import static org.apache.maven.surefire.booter.spi.LegacyMasterProcessChannelEncoder.encodeOpcode;
-import static org.apache.maven.surefire.booter.spi.LegacyMasterProcessChannelEncoder.estimateBufferLength;
 import static org.fest.assertions.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 /**
- * Test for {@link LegacyMasterProcessChannelEncoder}.
+ * Test for {@link EventChannelEncoder}.
  *
  * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
  * @since 3.0.0-M4
  */
 @SuppressWarnings( { "checkstyle:linelength", "checkstyle:magicnumber" } )
-public class LegacyMasterProcessChannelEncoderTest
+public class EventChannelEncoderTest
 {
     private static final int ELAPSED_TIME = 102;
     private static final byte[] ELAPSED_TIME_HEXA = new byte[] {0, 0, 0, 0x66};
 
     @Test
-    public void shouldComputeStreamPreemptiveLength()
-    {
-        CharsetEncoder encoder = UTF_8.newEncoder();
-
-        // :maven-surefire-event:8:sys-prop:10:normal-run:5:UTF-8:0001:kkk:0001:vvv:
-        assertThat( estimateBufferLength( BOOTERCODE_SYSPROPS, NORMAL_RUN, encoder, 0, "k", "v" ) )
-            .isEqualTo( 72 );
-
-        // :maven-surefire-event:16:testset-starting:10:normal-run:5:UTF-8:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:X0001:0001:sss:0001:sss:0001:sss:
-        assertThat( estimateBufferLength( BOOTERCODE_TESTSET_STARTING, NORMAL_RUN, encoder, 1, "s", "s", "s", "s", "s", "s", "s", "s", "s" ) )
-            .isEqualTo( 149 );
-
-        // :maven-surefire-event:17:testset-completed:10:normal-run:5:UTF-8:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:X0001:0001:sss:0001:sss:0001:sss:
-        assertThat( estimateBufferLength( BOOTERCODE_TESTSET_COMPLETED, NORMAL_RUN, encoder, 1, "s", "s", "s", "s", "s", "s", "s", "s", "s" ) )
-            .isEqualTo( 150 );
-
-        // :maven-surefire-event:13:test-starting:10:normal-run:5:UTF-8:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:X0001:0001:sss:0001:sss:0001:sss:
-        assertThat( estimateBufferLength( BOOTERCODE_TEST_STARTING, NORMAL_RUN, encoder, 1, "s", "s", "s", "s", "s", "s", "s", "s", "s" ) )
-            .isEqualTo( 146 );
-
-        // :maven-surefire-event:14:test-succeeded:10:normal-run:5:UTF-8:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:X0001:0001:sss:0001:sss:0001:sss:
-        assertThat( estimateBufferLength( BOOTERCODE_TEST_SUCCEEDED, NORMAL_RUN, encoder, 1, "s", "s", "s", "s", "s", "s", "s", "s", "s" ) )
-            .isEqualTo( 147 );
-
-        // :maven-surefire-event:11:test-failed:10:normal-run:5:UTF-8:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:X0001:0001:sss:0001:sss:0001:sss:
-        assertThat( estimateBufferLength( BOOTERCODE_TEST_FAILED, NORMAL_RUN, encoder, 1, "s", "s", "s", "s", "s", "s", "s", "s", "s" ) )
-            .isEqualTo( 144 );
-
-        // :maven-surefire-event:12:test-skipped:10:normal-run:5:UTF-8:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:X0001:0001:sss:0001:sss:0001:sss:
-        assertThat( estimateBufferLength( BOOTERCODE_TEST_SKIPPED, NORMAL_RUN, encoder, 1, "s", "s", "s", "s", "s", "s", "s", "s", "s" ) )
-            .isEqualTo( 145 );
-
-        // :maven-surefire-event:10:test-error:10:normal-run:5:UTF-8:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:X0001:0001:sss:0001:sss:0001:sss:
-        assertThat( estimateBufferLength( BOOTERCODE_TEST_ERROR, NORMAL_RUN, encoder, 1, "s", "s", "s", "s", "s", "s", "s", "s", "s" ) )
-            .isEqualTo( 143 );
-
-        // :maven-surefire-event:23:test-assumption-failure:10:normal-run:5:UTF-8:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:0001:sss:X0001:0001:sss:0001:sss:0001:sss:
-        assertThat( estimateBufferLength( BOOTERCODE_TEST_ASSUMPTIONFAILURE, NORMAL_RUN, encoder, 1, "s", "s", "s", "s", "s", "s", "s", "s", "s" ) )
-            .isEqualTo( 156 );
-
-        // :maven-surefire-event:14:std-out-stream:10:normal-run:5:UTF-8:0001:sss:
-        assertThat( estimateBufferLength( BOOTERCODE_STDOUT, NORMAL_RUN, encoder, 0, "s" ) )
-            .isEqualTo( 69 );
-
-        // :maven-surefire-event:23:std-out-stream-new-line:10:normal-run:5:UTF-8:0001:sss:
-        assertThat( estimateBufferLength( BOOTERCODE_STDOUT_NEW_LINE, NORMAL_RUN, encoder, 0, "s" ) )
-            .isEqualTo( 78 );
-
-        // :maven-surefire-event:14:std-err-stream:10:normal-run:5:UTF-8:0001:sss:
-        assertThat( estimateBufferLength( BOOTERCODE_STDERR, NORMAL_RUN, encoder, 0, "s" ) )
-            .isEqualTo( 69 );
-
-        // :maven-surefire-event:23:std-err-stream-new-line:10:normal-run:5:UTF-8:0001:sss:
-        assertThat( estimateBufferLength( BOOTERCODE_STDERR_NEW_LINE, NORMAL_RUN, encoder, 0, "s" ) )
-            .isEqualTo( 78 );
-
-        // :maven-surefire-event:16:console-info-log:5:UTF-8:0001:sss:
-        assertThat( estimateBufferLength( BOOTERCODE_CONSOLE_INFO, null, encoder, 0, "s" ) )
-            .isEqualTo( 58 );
-
-        // :maven-surefire-event:17:console-debug-log:5:UTF-8:0001:sss:
-        assertThat( estimateBufferLength( BOOTERCODE_CONSOLE_DEBUG, null, encoder, 0, "s" ) )
-            .isEqualTo( 59 );
-
-        // :maven-surefire-event:19:console-warning-log:5:UTF-8:0001:sss:
-        assertThat( estimateBufferLength( BOOTERCODE_CONSOLE_WARNING, null, encoder, 0, "s" ) )
-            .isEqualTo( 61 );
-
-        // :maven-surefire-event:17:console-error-log:5:UTF-8:0001:sss:
-        assertThat( estimateBufferLength( BOOTERCODE_CONSOLE_ERROR, null, encoder, 0, "s" ) )
-            .isEqualTo( 59 );
-
-        // :maven-surefire-event:3:bye:
-        assertThat( estimateBufferLength( BOOTERCODE_BYE, null, null, 0 ) )
-            .isEqualTo( 28 );
-
-        // :maven-surefire-event:17:stop-on-next-test:
-        assertThat( estimateBufferLength( BOOTERCODE_STOP_ON_NEXT_TEST, null, null, 0 ) )
-            .isEqualTo( 42 );
-
-        // :maven-surefire-event:9:next-test:
-        assertThat( estimateBufferLength( BOOTERCODE_NEXT_TEST, null, null, 0 ) )
-            .isEqualTo( 34 );
-
-        // :maven-surefire-event:14:jvm-exit-error:
-        assertThat( estimateBufferLength( BOOTERCODE_JVM_EXIT_ERROR, null, null, 0 ) )
-            .isEqualTo( 39 );
-    }
-
-    @Test
     public void reportEntry() throws IOException
     {
         final String exceptionMessage = "msg";
@@ -198,7 +79,9 @@ public class LegacyMasterProcessChannelEncoderTest
         when( reportEntry.getSourceName() ).thenReturn( "pkg.MyTest" );
         when( reportEntry.getStackTraceWriter() ).thenReturn( stackTraceWriter );
 
-        ByteBuffer encoded = encode( BOOTERCODE_TEST_ERROR, NORMAL_RUN, reportEntry, false );
+        Stream out = Stream.newStream();
+        EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
+        ByteBuffer encoded = encoder.encode( BOOTERCODE_TEST_ERROR, NORMAL_RUN, reportEntry, false );
         ByteArrayOutputStream expectedFrame = new ByteArrayOutputStream();
         expectedFrame.write( ":maven-surefire-event:".getBytes( UTF_8 ) );
         expectedFrame.write( (byte) 10 );
@@ -251,7 +134,9 @@ public class LegacyMasterProcessChannelEncoderTest
         assertThat( toArray( encoded ) )
             .isEqualTo( expectedFrame.toByteArray() );
 
-        encoded = encode( BOOTERCODE_TEST_ERROR, NORMAL_RUN, reportEntry, true );
+        out = Stream.newStream();
+        encoder = new EventChannelEncoder( newBufferedChannel( out ) );
+        encoded = encoder.encode( BOOTERCODE_TEST_ERROR, NORMAL_RUN, reportEntry, true );
         expectedFrame = new ByteArrayOutputStream();
         expectedFrame.write( ":maven-surefire-event:".getBytes( UTF_8 ) );
         expectedFrame.write( (byte) 10 );
@@ -304,9 +189,8 @@ public class LegacyMasterProcessChannelEncoderTest
         assertThat( toArray( encoded ) )
             .isEqualTo( expectedFrame.toByteArray() );
 
-        Stream out = Stream.newStream();
-        LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
-
+        out = Stream.newStream();
+        encoder = new EventChannelEncoder( newBufferedChannel( out ) );
         encoder.testSetStarting( reportEntry, true );
         expectedFrame = new ByteArrayOutputStream();
         expectedFrame.write( ":maven-surefire-event:".getBytes( UTF_8 ) );
@@ -359,7 +243,7 @@ public class LegacyMasterProcessChannelEncoderTest
             .isEqualTo( expectedFrame.toByteArray() );
 
         out = Stream.newStream();
-        encoder = new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+        encoder = new EventChannelEncoder( newBufferedChannel( out ) );
 
         encoder.testSetStarting( reportEntry, false );
         expectedFrame = new ByteArrayOutputStream();
@@ -438,7 +322,7 @@ public class LegacyMasterProcessChannelEncoderTest
         when( reportEntry.getStackTraceWriter() ).thenReturn( stackTraceWriter );
 
         Stream out = Stream.newStream();
-        LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+        EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
 
         encoder.testSetCompleted( reportEntry, false );
         ByteArrayOutputStream expectedFrame = new ByteArrayOutputStream();
@@ -517,7 +401,7 @@ public class LegacyMasterProcessChannelEncoderTest
         when( reportEntry.getStackTraceWriter() ).thenReturn( stackTraceWriter );
 
         Stream out = Stream.newStream();
-        LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+        EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
 
         encoder.testStarting( reportEntry, true );
 
@@ -597,7 +481,7 @@ public class LegacyMasterProcessChannelEncoderTest
         when( reportEntry.getStackTraceWriter() ).thenReturn( stackTraceWriter );
 
         Stream out = Stream.newStream();
-        LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+        EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
 
         encoder.testSucceeded( reportEntry, true );
         ByteArrayOutputStream expectedFrame = new ByteArrayOutputStream();
@@ -676,7 +560,7 @@ public class LegacyMasterProcessChannelEncoderTest
         when( reportEntry.getStackTraceWriter() ).thenReturn( stackTraceWriter );
 
         Stream out = Stream.newStream();
-        LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+        EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
 
         encoder.testFailed( reportEntry, false );
         ByteArrayOutputStream expectedFrame = new ByteArrayOutputStream();
@@ -754,7 +638,7 @@ public class LegacyMasterProcessChannelEncoderTest
         when( reportEntry.getStackTraceWriter() ).thenReturn( stackTraceWriter );
 
         Stream out = Stream.newStream();
-        LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+        EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
 
         encoder.testSkipped( reportEntry, false );
         ByteArrayOutputStream expectedFrame = new ByteArrayOutputStream();
@@ -832,7 +716,7 @@ public class LegacyMasterProcessChannelEncoderTest
         when( reportEntry.getStackTraceWriter() ).thenReturn( stackTraceWriter );
 
         Stream out = Stream.newStream();
-        LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+        EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
         encoder.testError( reportEntry, false );
         ByteArrayOutputStream expectedFrame = new ByteArrayOutputStream();
         expectedFrame.write( ":maven-surefire-event:".getBytes( UTF_8 ) );
@@ -909,7 +793,7 @@ public class LegacyMasterProcessChannelEncoderTest
         when( reportEntry.getStackTraceWriter() ).thenReturn( stackTraceWriter );
 
         Stream out = Stream.newStream();
-        LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+        EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
 
         encoder.testAssumptionFailure( reportEntry, false );
         ByteArrayOutputStream expectedFrame = new ByteArrayOutputStream();
@@ -966,7 +850,7 @@ public class LegacyMasterProcessChannelEncoderTest
     public void testBye()
     {
         Stream out = Stream.newStream();
-        LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+        EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
 
         encoder.bye();
 
@@ -980,7 +864,7 @@ public class LegacyMasterProcessChannelEncoderTest
     public void testStopOnNextTest()
     {
         Stream out = Stream.newStream();
-        LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+        EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
 
         encoder.stopOnNextTest();
 
@@ -993,7 +877,7 @@ public class LegacyMasterProcessChannelEncoderTest
     public void testAcquireNextTest()
     {
         Stream out = Stream.newStream();
-        LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+        EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
 
         encoder.acquireNextTest();
 
@@ -1005,41 +889,30 @@ public class LegacyMasterProcessChannelEncoderTest
     @Test
     public void testSendOpcode()
     {
-        CharsetEncoder encoder = UTF_8.newEncoder();
-        ByteBuffer result = ByteBuffer.allocate( 128 );
-        encodeOpcode( result, BOOTERCODE_TEST_ERROR, NORMAL_RUN );
-        assertThat( toString( result ) )
-                .isEqualTo( ":maven-surefire-event:" + (char) 10 + ":test-error:" + (char) 10 + ":normal-run:" );
-
-        result = ByteBuffer.allocate( 1024 );
-        encodeHeader( encoder, result, BOOTERCODE_TEST_ERROR, NORMAL_RUN );
-        assertThat( toString( result ) )
-                .isEqualTo( ":maven-surefire-event:" + (char) 10 + ":test-error:" + (char) 10 + ":normal-run:"
-                    + (char) 5 + ":UTF-8:" );
-
-        result = encodeMessage( BOOTERCODE_TEST_ERROR, NORMAL_RUN, "msg" );
-        assertThat( toString( result ) )
-                .isEqualTo( ":maven-surefire-event:" + (char) 10 + ":test-error:" + (char) 10 + ":normal-run:"
-                    + (char) 5 + ":UTF-8:\u0000\u0000\u0000\u0003:msg:" );
-
         Channel channel = new Channel();
-        new LegacyMasterProcessChannelEncoder( channel ).stdOut( "msg", false );
+        new EventChannelEncoder( channel ).stdOut( "msg", false );
         assertThat( toString( channel.src ) )
                 .isEqualTo( ":maven-surefire-event:" + (char) 14 + ":std-out-stream:" + (char) 10 + ":normal-run:"
                     + (char) 5 + ":UTF-8:\u0000\u0000\u0000\u0003:msg:" );
 
         channel = new Channel();
-        new LegacyMasterProcessChannelEncoder( channel ).stdErr( null, false );
+        new EventChannelEncoder( channel ).stdErr( null, false );
         assertThat( toString( channel.src ) )
                 .isEqualTo( ":maven-surefire-event:" + (char) 14 + ":std-err-stream:" + (char) 10 + ":normal-run:"
                     + (char) 5 + ":UTF-8:\u0000\u0000\u0000\u0001:\u0000:" );
+
+        ByteBuffer result = new EventChannelEncoder( new Channel() )
+            .encodeMessage( BOOTERCODE_TEST_ERROR, NORMAL_RUN, "msg" );
+        assertThat( toString( result ) )
+            .isEqualTo( ":maven-surefire-event:" + (char) 10 + ":test-error:" + (char) 10 + ":normal-run:"
+                + (char) 5 + ":UTF-8:\u0000\u0000\u0000\u0003:msg:" );
     }
 
     @Test
     public void testConsoleInfo()
     {
         Stream out = Stream.newStream();
-        LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+        EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
 
         encoder.consoleInfoLog( "msg" );
 
@@ -1055,7 +928,7 @@ public class LegacyMasterProcessChannelEncoderTest
     public void testConsoleError()
     {
         Stream out = Stream.newStream();
-        LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+        EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
 
         encoder.consoleErrorLog( "msg" );
 
@@ -1074,7 +947,7 @@ public class LegacyMasterProcessChannelEncoderTest
     public void testConsoleErrorLog1() throws IOException
     {
         Stream out = Stream.newStream();
-        LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+        EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
 
         Exception e = new Exception( "msg" );
         encoder.consoleErrorLog( e );
@@ -1112,7 +985,7 @@ public class LegacyMasterProcessChannelEncoderTest
     public void testConsoleErrorLog2() throws IOException
     {
         Stream out = Stream.newStream();
-        LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+        EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
 
         Exception e = new Exception( "msg" );
         encoder.consoleErrorLog( "msg2", e );
@@ -1150,7 +1023,7 @@ public class LegacyMasterProcessChannelEncoderTest
     public void testConsoleErrorLog3()
     {
         Stream out = Stream.newStream();
-        LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+        EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
 
         StackTraceWriter stackTraceWriter = mock( StackTraceWriter.class );
         when( stackTraceWriter.getThrowable() ).thenReturn( new SafeThrowable( "1" ) );
@@ -1168,7 +1041,7 @@ public class LegacyMasterProcessChannelEncoderTest
     public void testConsoleDebug()
     {
         Stream out = Stream.newStream();
-        LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+        EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
 
         encoder.consoleDebugLog( "msg" );
 
@@ -1184,7 +1057,7 @@ public class LegacyMasterProcessChannelEncoderTest
     public void testConsoleWarning()
     {
         Stream out = Stream.newStream();
-        LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+        EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
 
         encoder.consoleWarningLog( "msg" );
 
@@ -1201,7 +1074,7 @@ public class LegacyMasterProcessChannelEncoderTest
     {
         Stream out = Stream.newStream();
         WritableBufferedByteChannel channel = newBufferedChannel( out );
-        LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( channel );
+        EventChannelEncoder encoder = new EventChannelEncoder( channel );
 
         encoder.stdOut( "msg", false );
         channel.close();
@@ -1218,7 +1091,7 @@ public class LegacyMasterProcessChannelEncoderTest
     {
         Stream out = Stream.newStream();
         WritableBufferedByteChannel channel = newBufferedChannel( out );
-        LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( channel );
+        EventChannelEncoder encoder = new EventChannelEncoder( channel );
 
         encoder.stdOut( "msg", true );
         channel.close();
@@ -1235,7 +1108,7 @@ public class LegacyMasterProcessChannelEncoderTest
     {
         Stream out = Stream.newStream();
         WritableBufferedByteChannel channel = newBufferedChannel( out );
-        LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( channel );
+        EventChannelEncoder encoder = new EventChannelEncoder( channel );
 
         encoder.stdErr( "msg", false );
         channel.close();
@@ -1252,7 +1125,7 @@ public class LegacyMasterProcessChannelEncoderTest
     {
         Stream out = Stream.newStream();
         WritableBufferedByteChannel channel = newBufferedChannel( out );
-        LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( channel );
+        EventChannelEncoder encoder = new EventChannelEncoder( channel );
 
         encoder.stdErr( "msg", true );
         channel.close();
@@ -1270,10 +1143,10 @@ public class LegacyMasterProcessChannelEncoderTest
     {
         Stream stream = Stream.newStream();
         WritableBufferedByteChannel channel = newBufferedChannel( stream );
-        LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( channel );
+        EventChannelEncoder encoder = new EventChannelEncoder( channel );
 
         Map<String, String> sysProps = ObjectUtils.systemProps();
-        encoder.sendSystemProperties( sysProps );
+        encoder.systemProperties( sysProps );
         channel.close();
 
         for ( Entry<String, String> entry : sysProps.entrySet() )
@@ -1304,7 +1177,7 @@ public class LegacyMasterProcessChannelEncoderTest
     {
         Stream out = Stream.newStream();
 
-        LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+        EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
         StackTraceWriter stackTraceWriter = mock( StackTraceWriter.class );
         when( stackTraceWriter.getThrowable() ).thenReturn( new SafeThrowable( "1" ) );
         when( stackTraceWriter.smartTrimmedStackTrace() ).thenReturn( "2" );
@@ -1321,7 +1194,7 @@ public class LegacyMasterProcessChannelEncoderTest
     {
         Stream out = Stream.newStream();
 
-        LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newBufferedChannel( out ) );
+        EventChannelEncoder encoder = new EventChannelEncoder( newBufferedChannel( out ) );
         StackTraceWriter stackTraceWriter = mock( StackTraceWriter.class );
         when( stackTraceWriter.getThrowable() ).thenReturn( new SafeThrowable( "1" ) );
         when( stackTraceWriter.smartTrimmedStackTrace() ).thenReturn( "2" );
@@ -1338,7 +1211,7 @@ public class LegacyMasterProcessChannelEncoderTest
     {
         Stream out = Stream.newStream();
         WritableBufferedByteChannel channel = newBufferedChannel( out );
-        LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( channel );
+        EventChannelEncoder encoder = new EventChannelEncoder( channel );
 
         Thread.currentThread().interrupt();
         try
@@ -1373,11 +1246,6 @@ public class LegacyMasterProcessChannelEncoderTest
             return out.toByteArray();
         }
 
-        LineNumberReader newReader( Charset streamCharset )
-        {
-            return new LineNumberReader( new StringReader( new String( toByteArray(), streamCharset ) ) );
-        }
-
         static Stream newStream()
         {
             return new Stream( new ByteArrayOutputStream() );
@@ -1412,7 +1280,7 @@ public class LegacyMasterProcessChannelEncoderTest
         ByteBuffer src;
 
         @Override
... 376 lines suppressed ...