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 2020/04/08 08:22:09 UTC

[maven-surefire] 01/18: [SUREFIRE-1658] TCP/IP Channel for forked Surefire JVM. Extensions API and SPI. Polymorphism for remote and local process communication.

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

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

commit d0c9039e5faf2ff1939f52e7520324ae9d4886cf
Author: tibordigana <ti...@apache.org>
AuthorDate: Sat Jul 6 17:51:13 2019 +0200

    [SUREFIRE-1658] TCP/IP Channel for forked Surefire JVM. Extensions API and SPI. Polymorphism for remote and local process communication.
---
 .github/workflows/maven.yml                        |    2 +-
 Jenkinsfile                                        |    8 +-
 .../maven/plugin/failsafe/IntegrationTestMojo.java |   10 +
 maven-surefire-common/pom.xml                      |   16 -
 .../plugin/surefire/AbstractSurefireMojo.java      |   74 +-
 .../maven/plugin/surefire/CommonReflector.java     |   18 +-
 .../surefire/SurefireDependencyResolver.java       |    1 +
 .../AbstractClasspathForkConfiguration.java        |    7 +-
 .../surefire/booterclient/BooterSerializer.java    |    5 +-
 .../booterclient/ClasspathForkConfiguration.java   |    7 +-
 .../booterclient/DefaultForkConfiguration.java     |   13 +-
 .../surefire/booterclient/ForkConfiguration.java   |    2 +
 .../plugin/surefire/booterclient/ForkStarter.java  |  126 ++-
 .../booterclient/JarManifestForkConfiguration.java |    7 +-
 .../ModularClasspathForkConfiguration.java         |    7 +-
 ...InputStream.java => AbstractCommandReader.java} |   16 +-
 ...ommandStream.java => DefaultCommandReader.java} |   60 +-
 .../DefferedChannelCommandSender.java}             |   12 +-
 .../lazytestprovider/TestLessInputStream.java      |    7 +-
 .../lazytestprovider/TestProvidingInputStream.java |   14 +-
 .../surefire/booterclient/output/ForkClient.java   |  176 +--
 .../booterclient/output/ForkedChannelDecoder.java  |  352 ------
 .../output/ForkedProcessEventNotifier.java         |  248 ++++
 .../output/ForkedProcessExitErrorListener.java     |    4 +-
 .../ForkedProcessStackTraceEventListener.java      |    6 +-
 .../output/NativeStdErrStreamConsumer.java         |    8 +-
 ...stener.java => NativeStdOutStreamConsumer.java} |   24 +-
 .../output/ThreadedStreamConsumer.java             |   80 +-
 .../surefire/extensions/EventConsumerThread.java   |  441 ++++++++
 .../surefire/extensions/LegacyForkChannel.java     |   89 ++
 .../surefire/extensions/LegacyForkNodeFactory.java |   24 +-
 .../plugin/surefire/extensions/StreamFeeder.java   |  203 ++++
 .../surefire/extensions/SurefireForkChannel.java   |  140 +++
 .../extensions/SurefireForkNodeFactory.java        |   25 +-
 .../AbstractSurefireMojoJava7PlusTest.java         |   27 +-
 .../plugin/surefire/AbstractSurefireMojoTest.java  |   44 +-
 .../maven/plugin/surefire/CommonReflectorTest.java |   50 +
 .../maven/plugin/surefire/MojoMocklessTest.java    |    7 +
 .../plugin/surefire/SurefireReflectorTest.java     |   71 --
 ...ooterDeserializerProviderConfigurationTest.java |    8 +-
 ...BooterDeserializerStartupConfigurationTest.java |   23 +-
 .../booterclient/DefaultForkConfigurationTest.java |   48 +-
 .../booterclient/ForkConfigurationTest.java        |   17 +-
 .../surefire/booterclient/ForkStarterTest.java     |   17 +-
 .../booterclient/ForkingRunListenerTest.java       |  194 ++--
 .../plugin/surefire/booterclient/MainClass.java    |   14 +-
 .../ModularClasspathForkConfigurationTest.java     |   10 +-
 .../TestLessInputStreamBuilderTest.java            |   54 +-
 .../TestProvidingInputStreamTest.java              |  152 ++-
 .../booterclient/output/ForkClientTest.java        |  938 ++++------------
 .../output/ForkedChannelDecoderTest.java           |  901 ---------------
 .../extensions/ConsoleOutputReporterTest.java      |    8 +-
 .../extensions/ForkedProcessEventNotifierTest.java | 1183 ++++++++++++++++++++
 .../surefire/extensions/StatelessReporterTest.java |    5 +-
 .../surefire/extensions/StreamFeederTest.java      |  162 +++
 .../org/apache/maven/surefire/JUnit4SuiteTest.java |   14 +-
 .../maven/surefire/extensions/ForkChannelTest.java |  166 +++
 .../StatelessTestsetInfoReporterTest.java          |    2 +-
 .../maven/plugin/surefire/SurefirePlugin.java      |   10 +
 pom.xml                                            |    7 +-
 .../maven/surefire/booter/BaseProviderFactory.java |   79 +-
 .../org/apache/maven/surefire/booter/Command.java  |   21 +-
 ...ocessEvent.java => ForkedProcessEventType.java} |   20 +-
 .../surefire/booter/ForkingReporterFactory.java    |    5 +-
 .../maven/surefire/booter/ForkingRunListener.java  |    5 +-
 .../surefire/booter/MasterProcessCommand.java      |  147 +--
 .../surefire/booter/RunOrderParametersAware.java   |   30 -
 .../surefire/booter/TestArtifactInfoAware.java     |   30 -
 .../maven/surefire/booter/TestRequestAware.java    |   30 -
 .../surefire/eventapi/AbstractConsoleEvent.java    |   85 ++
 .../eventapi/AbstractStandardStreamEvent.java      |   93 ++
 .../eventapi/AbstractTestControlEvent.java         |   95 ++
 .../ConsoleDebugEvent.java}                        |   16 +-
 .../maven/surefire/eventapi/ConsoleErrorEvent.java |   87 ++
 .../ConsoleInfoEvent.java}                         |   18 +-
 .../ConsoleWarningEvent.java}                      |   16 +-
 .../ControlByeEvent.java}                          |   57 +-
 .../ControlNextTestEvent.java}                     |   57 +-
 .../eventapi/ControlStopOnNextTestEvent.java       |   77 ++
 .../Event.java}                                    |   32 +-
 .../maven/surefire/eventapi/JvmExitErrorEvent.java |   87 ++
 .../surefire/eventapi/StandardStreamErrEvent.java  |   19 +-
 .../StandardStreamErrWithNewLineEvent.java         |   19 +-
 .../surefire/eventapi/StandardStreamOutEvent.java  |   19 +-
 .../StandardStreamOutWithNewLineEvent.java         |   19 +-
 .../surefire/eventapi/SystemPropertyEvent.java     |  101 ++
 .../eventapi/TestAssumptionFailureEvent.java       |   20 +-
 .../maven/surefire/eventapi/TestErrorEvent.java    |   20 +-
 .../maven/surefire/eventapi/TestFailedEvent.java   |   20 +-
 .../maven/surefire/eventapi/TestSkippedEvent.java  |   20 +-
 .../maven/surefire/eventapi/TestStartingEvent.java |   20 +-
 .../surefire/eventapi/TestSucceededEvent.java      |   20 +-
 .../surefire/eventapi/TestsetCompletedEvent.java   |   20 +-
 .../surefire/eventapi/TestsetStartingEvent.java    |   20 +-
 .../CommandChainReader.java}                       |   19 +-
 .../{booter => providerapi}/CommandListener.java   |    4 +-
 .../providerapi/MasterProcessChannelDecoder.java   |   48 +
 .../providerapi/MasterProcessChannelEncoder.java   |   84 ++
 .../surefire/providerapi/ProviderParameters.java   |    8 +-
 .../apache/maven/surefire/report/ReportEntry.java  |    2 +-
 .../maven/surefire/report/SimpleReportEntry.java   |    2 +
 .../org/apache/maven/surefire/suite/RunResult.java |    2 +-
 .../maven/surefire/testset/TestListResolver.java   |    2 +-
 .../maven/surefire/util/ReflectionUtils.java       |   31 +-
 .../util/internal/DaemonThreadFactory.java         |   35 +-
 .../java/org/apache/maven/JUnit4SuiteTest.java     |   10 +-
 .../surefire/booter/ForkingRunListenerTest.java    |   25 +-
 .../surefire/booter/MasterProcessCommandTest.java  |  164 ---
 .../surefire/booter/SurefireReflectorTest.java     |  198 ----
 surefire-booter/pom.xml                            |   39 +-
 .../maven/surefire/booter/BooterConstants.java     |    1 +
 .../maven/surefire/booter/BooterDeserializer.java  |   14 +
 .../apache/maven/surefire/booter/Classpath.java    |   13 +-
 .../maven/surefire/booter/CommandReader.java       |  130 +--
 .../apache/maven/surefire/booter/ForkedBooter.java |   78 +-
 .../maven/surefire/booter/LazyTestsToRun.java      |   13 +-
 .../surefire/booter/ProviderConfiguration.java     |    2 -
 .../maven/surefire/booter/ProviderFactory.java     |    4 +-
 .../surefire/booter/StartupConfiguration.java      |   22 +-
 .../maven/surefire/booter/SurefireReflector.java   |  120 +-
 .../spi/LegacyMasterProcessChannelDecoder.java     |  190 ++++
 .../spi/LegacyMasterProcessChannelEncoder.java     |  180 +--
 ...LegacyMasterProcessChannelProcessorFactory.java |   72 ++
 ...refireMasterProcessChannelProcessorFactory.java |   90 ++
 ...refire.spi.MasterProcessChannelProcessorFactory |   32 +-
 .../surefire/booter/BooterDeserializerTest.java    |    2 +-
 .../maven/surefire/booter/ClasspathTest.java       |   88 +-
 .../maven/surefire/booter/CommandReaderTest.java   |   57 +-
 .../java/org/apache/maven/surefire/booter/Foo.java |   48 +-
 .../surefire/booter/ForkedBooterMockTest.java      |  232 +++-
 .../surefire/booter/IsolatedClassLoaderTest.java   |   66 ++
 .../maven/surefire/booter/JUnit4SuiteTest.java     |    7 +
 .../surefire/booter/NewClassLoaderRunner.java      |    0
 .../surefire/booter/SurefireReflectorTest.java     |  408 +++++++
 .../spi/LegacyMasterProcessChannelDecoderTest.java |  243 ++++
 .../spi/LegacyMasterProcessChannelEncoderTest.java |  223 ++--
 surefire-extensions-api/pom.xml                    |   38 +-
 .../surefire/extensions/CloseableDaemonThread.java |   17 +-
 .../maven/surefire/extensions/CommandReader.java   |   23 +-
 .../maven/surefire/extensions/EventHandler.java    |   12 +-
 .../maven/surefire/extensions/ForkChannel.java     |  108 ++
 .../maven/surefire/extensions/ForkNodeFactory.java |   24 +-
 .../surefire/extensions/StdOutStreamLine.java      |    8 +-
 .../extensions/util/LineConsumerThread.java        |   21 +-
 .../surefire/extensions/util/StreamFeeder.java     |   89 --
 .../extensions}/CommandlineExecutorTest.java       |   27 +-
 .../surefire/extensions}/JUnit4SuiteTest.java      |    2 +-
 surefire-extensions-spi/pom.xml                    |   42 +
 .../spi/MasterProcessChannelProcessorFactory.java  |   62 +
 .../surefire/its/fixture/SurefireLauncher.java     |    2 +
 surefire-logger-api/pom.xml                        |    2 +-
 .../maven/surefire/junit4/JUnit4Provider.java      |    9 +-
 .../maven/surefire/junit4/JUnit4ProviderTest.java  |    2 +-
 .../surefire/junitcore/JUnitCoreProvider.java      |    9 +-
 .../maven/surefire/junitcore/Surefire746Test.java  |    3 +-
 .../maven/surefire/testng/TestNGProvider.java      |    9 +-
 surefire-shadefire/pom.xml                         |    5 +
 157 files changed, 7117 insertions(+), 4193 deletions(-)

diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index 5340988..ccd2646 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -39,4 +39,4 @@ jobs:
           java-version: 1.8
 
       - name: Build with Maven
-        run: mvn install -e -B -V -nsu --no-transfer-progress -P run-its
+        run: mvn install -e -B -V -nsu --no-transfer-progress -P run-its
\ No newline at end of file
diff --git a/Jenkinsfile b/Jenkinsfile
index 24b32c7..4b29517 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -21,10 +21,10 @@
 
 properties(
     [
-        buildDiscarder(logRotator(artifactDaysToKeepStr: env.BRANCH_NAME == 'master' ? '1' : '2',
+        buildDiscarder(logRotator(artifactDaysToKeepStr: env.BRANCH_NAME == 'master' ? '14' : '7',
                                   artifactNumToKeepStr: '50',
-                                  daysToKeepStr: env.BRANCH_NAME == 'master' ? '10' : '5',
-                                  numToKeepStr: env.BRANCH_NAME == 'master' ? '5' : '3')
+                                  daysToKeepStr: env.BRANCH_NAME == 'master' ? '30' : '14',
+                                  numToKeepStr: env.BRANCH_NAME == 'master' ? '20' : '10')
         ),
         disableConcurrentBuilds()
     ]
@@ -213,6 +213,7 @@ static def sourcesPatternCsv() {
             '**/surefire-api/src/main/java,' +
             '**/surefire-booter/src/main/java,' +
             '**/surefire-extensions-api/src/main/java,' +
+            '**/surefire-extensions-spi/src/main/java,' +
             '**/surefire-grouper/src/main/java,' +
             '**/surefire-its/src/main/java,' +
             '**/surefire-logger-api/src/main/java,' +
@@ -229,6 +230,7 @@ static def classPatternCsv() {
             '**/surefire-api/target/classes,' +
             '**/surefire-booter/target/classes,' +
             '**/surefire-extensions-api/target/classes,' +
+            '**/surefire-extensions-spi/target/classes,' +
             '**/surefire-grouper/target/classes,' +
             '**/surefire-its/target/classes,' +
             '**/surefire-logger-api/target/classes,' +
diff --git a/maven-failsafe-plugin/src/main/java/org/apache/maven/plugin/failsafe/IntegrationTestMojo.java b/maven-failsafe-plugin/src/main/java/org/apache/maven/plugin/failsafe/IntegrationTestMojo.java
index e465bb2..a6771bc 100644
--- a/maven-failsafe-plugin/src/main/java/org/apache/maven/plugin/failsafe/IntegrationTestMojo.java
+++ b/maven-failsafe-plugin/src/main/java/org/apache/maven/plugin/failsafe/IntegrationTestMojo.java
@@ -27,6 +27,7 @@ import org.apache.maven.plugins.annotations.LifecyclePhase;
 import org.apache.maven.plugins.annotations.Mojo;
 import org.apache.maven.plugins.annotations.Parameter;
 import org.apache.maven.plugins.annotations.ResolutionScope;
+import org.apache.maven.surefire.extensions.ForkNodeFactory;
 import org.apache.maven.surefire.suite.RunResult;
 
 import java.io.File;
@@ -384,6 +385,9 @@ public class IntegrationTestMojo
     @Parameter( property = "failsafe.useModulePath", defaultValue = "true" )
     private boolean useModulePath;
 
+    @Parameter( property = "failsafe.forkNode" )
+    private ForkNodeFactory forkNode;
+
     /**
      * You can selectively exclude individual environment variables by enumerating their keys.
      * <br>
@@ -912,6 +916,12 @@ public class IntegrationTestMojo
     }
 
     @Override
+    protected final ForkNodeFactory getForkNode()
+    {
+        return forkNode;
+    }
+
+    @Override
     protected final String[] getExcludedEnvironmentVariables()
     {
         return excludedEnvironmentVariables == null ? new String[0] : excludedEnvironmentVariables;
diff --git a/maven-surefire-common/pom.xml b/maven-surefire-common/pom.xml
index fefe831..8ab9e98 100644
--- a/maven-surefire-common/pom.xml
+++ b/maven-surefire-common/pom.xml
@@ -120,22 +120,6 @@
     <build>
         <plugins>
             <plugin>
-                <artifactId>maven-dependency-plugin</artifactId>
-                <executions>
-                    <execution>
-                        <id>build-test-classpath</id>
-                        <phase>generate-sources</phase>
-                        <goals>
-                            <goal>build-classpath</goal>
-                        </goals>
-                        <configuration>
-                            <includeScope>test</includeScope>
-                            <outputFile>target/test-classpath/cp.txt</outputFile>
-                        </configuration>
-                    </execution>
-                </executions>
-            </plugin>
-            <plugin>
                 <groupId>org.jacoco</groupId>
                 <artifactId>jacoco-maven-plugin</artifactId>
                 <executions>
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/AbstractSurefireMojo.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/AbstractSurefireMojo.java
index a8c1927..7ba312a 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/AbstractSurefireMojo.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/AbstractSurefireMojo.java
@@ -25,6 +25,7 @@ import org.apache.maven.artifact.DefaultArtifact;
 import org.apache.maven.artifact.handler.ArtifactHandler;
 import org.apache.maven.artifact.repository.ArtifactRepository;
 import org.apache.maven.model.Plugin;
+import org.apache.maven.plugin.surefire.extensions.LegacyForkNodeFactory;
 import org.apache.maven.plugin.surefire.extensions.SurefireConsoleOutputReporter;
 import org.apache.maven.plugin.surefire.extensions.SurefireStatelessReporter;
 import org.apache.maven.plugin.surefire.extensions.SurefireStatelessTestsetInfoReporter;
@@ -73,6 +74,7 @@ import org.apache.maven.surefire.booter.StartupConfiguration;
 import org.apache.maven.surefire.booter.SurefireBooterForkException;
 import org.apache.maven.surefire.booter.SurefireExecutionException;
 import org.apache.maven.surefire.cli.CommandLineOption;
+import org.apache.maven.surefire.extensions.ForkNodeFactory;
 import org.apache.maven.surefire.providerapi.SurefireProvider;
 import org.apache.maven.surefire.report.ReporterConfiguration;
 import org.apache.maven.surefire.suite.RunResult;
@@ -777,8 +779,6 @@ public abstract class AbstractSurefireMojo
     @Component
     private DependencyResolver dependencyResolver;
 
-    private Artifact surefireBooterArtifact;
-
     private Toolchain toolchain;
 
     private int effectiveForkCount = -1;
@@ -835,6 +835,8 @@ public abstract class AbstractSurefireMojo
 
     protected abstract String getEnableProcessChecker();
 
+    protected abstract ForkNodeFactory getForkNode();
+
     /**
      * This plugin MOJO artifact.
      *
@@ -916,8 +918,7 @@ public abstract class AbstractSurefireMojo
                 getPluginName(), getDependencyResolver(),
                 getSession().isOffline() );
 
-        surefireBooterArtifact = getBooterArtifact();
-        if ( surefireBooterArtifact == null )
+        if ( getBooterArtifact() == null )
         {
             throw new RuntimeException( "Unable to locate surefire-booter in the list of plugin artifacts" );
         }
@@ -1751,7 +1752,7 @@ public abstract class AbstractSurefireMojo
         return new File( getBasedir(), ".surefire-" + configurationHash );
     }
 
-    private StartupConfiguration createStartupConfiguration( @Nonnull ProviderInfo provider, boolean isInprocess,
+    private StartupConfiguration createStartupConfiguration( @Nonnull ProviderInfo provider, boolean isForking,
                                                              @Nonnull ClassLoaderConfiguration classLoaderConfiguration,
                                                              @Nonnull DefaultScanResult scanResult,
                                                              @Nonnull Platform platform,
@@ -1762,7 +1763,7 @@ public abstract class AbstractSurefireMojo
         {
             Set<Artifact> providerArtifacts = provider.getProviderClasspath();
             String providerName = provider.getProviderName();
-            if ( canExecuteProviderWithModularPath( platform ) && !isInprocess )
+            if ( isForking && canExecuteProviderWithModularPath( platform ) )
             {
                 String jvmExecutable = platform.getJdkExecAttributesForTests().getJvmExecutable();
                 String javaHome = Paths.get( jvmExecutable )
@@ -1804,8 +1805,8 @@ public abstract class AbstractSurefireMojo
         getConsoleLogger().debug( testClasspath.getCompactLogMessage( "test(compact) classpath:" ) );
         getConsoleLogger().debug( providerClasspath.getCompactLogMessage( "provider(compact) classpath:" ) );
 
-        Artifact[] additionalInProcArtifacts =
-                { getCommonArtifact(), getExtensionsArtifact(), getApiArtifact(), getLoggerApiArtifact() };
+        Artifact[] additionalInProcArtifacts = { getCommonArtifact(), getBooterArtifact(), getExtensionsArtifact(),
+            getApiArtifact(), getSpiArtifact(), getLoggerApiArtifact(), getSurefireSharedUtilsArtifact() };
         Set<Artifact> inProcArtifacts = retainInProcArtifactsUnique( providerArtifacts, additionalInProcArtifacts );
         Classpath inProcClasspath = createInProcClasspath( providerClasspath, inProcArtifacts );
         getConsoleLogger().debug( inProcClasspath.getLogMessage( "in-process classpath:" ) );
@@ -1814,8 +1815,8 @@ public abstract class AbstractSurefireMojo
         ClasspathConfiguration classpathConfiguration = new ClasspathConfiguration( testClasspath, providerClasspath,
                 inProcClasspath, effectiveIsEnableAssertions(), isChildDelegation() );
 
-        return new StartupConfiguration( providerName, classpathConfiguration, classLoaderConfiguration, isForking(),
-                false, ProcessCheckerType.toEnum( getEnableProcessChecker() ) );
+        return new StartupConfiguration( providerName, classpathConfiguration, classLoaderConfiguration,
+            ProcessCheckerType.toEnum( getEnableProcessChecker() ) );
     }
 
     private static Set<Artifact> retainInProcArtifactsUnique( Set<Artifact> providerArtifacts,
@@ -1907,8 +1908,8 @@ public abstract class AbstractSurefireMojo
         ModularClasspath modularClasspath = new ModularClasspath( result.getMainModuleDescriptor().name(),
                 testModulepath.getClassPath(), packages, getTestClassesDirectory() );
 
-        Artifact[] additionalInProcArtifacts =
-                { getCommonArtifact(), getExtensionsArtifact(), getApiArtifact(), getLoggerApiArtifact() };
+        Artifact[] additionalInProcArtifacts = { getCommonArtifact(), getBooterArtifact(), getExtensionsArtifact(),
+            getApiArtifact(), getSpiArtifact(), getLoggerApiArtifact(), getSurefireSharedUtilsArtifact() };
         Set<Artifact> inProcArtifacts = retainInProcArtifactsUnique( providerArtifacts, additionalInProcArtifacts );
         Classpath inProcClasspath = createInProcClasspath( providerClasspath, inProcArtifacts );
 
@@ -1924,8 +1925,8 @@ public abstract class AbstractSurefireMojo
         getConsoleLogger().debug( inProcClasspath.getLogMessage( "in-process classpath:" ) );
         getConsoleLogger().debug( inProcClasspath.getCompactLogMessage( "in-process(compact) classpath:" ) );
 
-        return new StartupConfiguration( providerName, classpathConfiguration, classLoaderConfiguration, isForking(),
-                false, ProcessCheckerType.toEnum( getEnableProcessChecker() ) );
+        return new StartupConfiguration( providerName, classpathConfiguration, classLoaderConfiguration,
+            ProcessCheckerType.toEnum( getEnableProcessChecker() ) );
     }
 
     private Artifact getCommonArtifact()
@@ -1938,11 +1939,21 @@ public abstract class AbstractSurefireMojo
         return getPluginArtifactMap().get( "org.apache.maven.surefire:surefire-extensions-api" );
     }
 
+    private Artifact getSpiArtifact()
+    {
+        return getPluginArtifactMap().get( "org.apache.maven.surefire:surefire-extensions-spi" );
+    }
+
     private Artifact getApiArtifact()
     {
         return getPluginArtifactMap().get( "org.apache.maven.surefire:surefire-api" );
     }
 
+    private Artifact getSurefireSharedUtilsArtifact()
+    {
+        return getPluginArtifactMap().get( "org.apache.maven.surefire:surefire-shared-utils" );
+    }
+
     private Artifact getLoggerApiArtifact()
     {
         return getPluginArtifactMap().get( "org.apache.maven.surefire:surefire-logger-api" );
@@ -2232,7 +2243,7 @@ public abstract class AbstractSurefireMojo
                                            @Nonnull TestClassPath testClasspathWrapper )
         throws MojoExecutionException, MojoFailureException
     {
-        StartupConfiguration startupConfiguration = createStartupConfiguration( provider, false,
+        StartupConfiguration startupConfiguration = createStartupConfiguration( provider, true,
                 classLoaderConfiguration, scanResult, platform, testClasspathWrapper );
         String configChecksum = getConfigChecksum();
         StartupReportConfiguration startupReportConfiguration = getStartupReportConfiguration( configChecksum, true );
@@ -2249,7 +2260,7 @@ public abstract class AbstractSurefireMojo
                                                               @Nonnull TestClassPath testClasspathWrapper )
         throws MojoExecutionException, MojoFailureException
     {
-        StartupConfiguration startupConfiguration = createStartupConfiguration( provider, true, classLoaderConfig,
+        StartupConfiguration startupConfiguration = createStartupConfiguration( provider, false, classLoaderConfig,
                 scanResult, platform, testClasspathWrapper );
         String configChecksum = getConfigChecksum();
         StartupReportConfiguration startupReportConfiguration = getStartupReportConfiguration( configChecksum, false );
@@ -2258,6 +2269,14 @@ public abstract class AbstractSurefireMojo
                                               getConsoleLogger() );
     }
 
+    // todo this is in separate method and can be better tested than whole method createForkConfiguration()
+    @Nonnull
+    private ForkNodeFactory getForkNodeFactory()
+    {
+        ForkNodeFactory forkNode = getForkNode();
+        return forkNode == null ? new LegacyForkNodeFactory() : forkNode;
+    }
+
     @Nonnull
     private ForkConfiguration createForkConfiguration( Platform platform )
     {
@@ -2265,7 +2284,11 @@ public abstract class AbstractSurefireMojo
 
         Artifact shadeFire = getShadefireArtifact();
 
-        Classpath bootClasspath = getArtifactClasspath( shadeFire != null ? shadeFire : surefireBooterArtifact );
+        Classpath bootClasspath = getArtifactClasspath( shadeFire != null ? shadeFire : getBooterArtifact() );
+
+        ForkNodeFactory forkNode = getForkNodeFactory();
+
+        getConsoleLogger().debug( "Found implementation of fork node factory: " + forkNode.getClass() );
 
         if ( canExecuteProviderWithModularPath( platform ) )
         {
@@ -2281,7 +2304,8 @@ public abstract class AbstractSurefireMojo
                     getEffectiveForkCount(),
                     reuseForks,
                     platform,
-                    getConsoleLogger() );
+                    getConsoleLogger(),
+                    forkNode );
         }
         else if ( getClassLoaderConfiguration().isManifestOnlyJarRequestedAndUsable() )
         {
@@ -2297,7 +2321,8 @@ public abstract class AbstractSurefireMojo
                     getEffectiveForkCount(),
                     reuseForks,
                     platform,
-                    getConsoleLogger() );
+                    getConsoleLogger(),
+                    forkNode );
         }
         else
         {
@@ -2313,7 +2338,8 @@ public abstract class AbstractSurefireMojo
                     getEffectiveForkCount(),
                     reuseForks,
                     platform,
-                    getConsoleLogger() );
+                    getConsoleLogger(),
+                    forkNode );
         }
     }
 
@@ -2924,7 +2950,7 @@ public abstract class AbstractSurefireMojo
         {
             // add the JUnit provider as default - it doesn't require JUnit to be present,
             // since it supports POJO tests.
-            String version = surefireBooterArtifact.getBaseVersion();
+            String version = getBooterArtifact().getBaseVersion();
             return surefireDependencyResolver.getProviderClasspath( "surefire-junit3", version );
         }
     }
@@ -2963,7 +2989,7 @@ public abstract class AbstractSurefireMojo
         @Nonnull
         public Set<Artifact> getProviderClasspath()
         {
-            String version = surefireBooterArtifact.getBaseVersion();
+            String version = getBooterArtifact().getBaseVersion();
             return surefireDependencyResolver.getProviderClasspath( "surefire-junit4", version );
         }
     }
@@ -3006,7 +3032,7 @@ public abstract class AbstractSurefireMojo
         @Nonnull
         public Set<Artifact> getProviderClasspath() throws MojoExecutionException
         {
-            String surefireVersion = surefireBooterArtifact.getBaseVersion();
+            String surefireVersion = getBooterArtifact().getBaseVersion();
             Map<String, Artifact> providerArtifacts =
                     surefireDependencyResolver.getProviderClasspathAsMap( "surefire-junit-platform", surefireVersion );
             Map<String, Artifact> testDependencies = testClasspath.getTestDependencies();
@@ -3177,7 +3203,7 @@ public abstract class AbstractSurefireMojo
         @Nonnull
         public Set<Artifact> getProviderClasspath()
         {
-            String version = surefireBooterArtifact.getBaseVersion();
+            String version = getBooterArtifact().getBaseVersion();
             return surefireDependencyResolver.getProviderClasspath( "surefire-junit47", version );
         }
     }
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/CommonReflector.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/CommonReflector.java
index 835fb89..466148f 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/CommonReflector.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/CommonReflector.java
@@ -23,8 +23,8 @@ import org.apache.maven.plugin.surefire.extensions.SurefireConsoleOutputReporter
 import org.apache.maven.plugin.surefire.extensions.SurefireStatelessReporter;
 import org.apache.maven.plugin.surefire.extensions.SurefireStatelessTestsetInfoReporter;
 import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
+import org.apache.maven.plugin.surefire.log.api.ConsoleLoggerDecorator;
 import org.apache.maven.plugin.surefire.report.DefaultReporterFactory;
-import org.apache.maven.surefire.booter.SurefireReflector;
 import org.apache.maven.surefire.util.SurefireReflectionException;
 
 import javax.annotation.Nonnull;
@@ -71,7 +71,7 @@ public class CommonReflector
     {
         Class<?>[] args = { this.startupReportConfiguration, this.consoleLogger };
         Object src = createStartupReportConfiguration( startupReportConfiguration );
-        Object logger = SurefireReflector.createConsoleLogger( consoleLogger, surefireClassLoader );
+        Object logger = createConsoleLogger( consoleLogger, surefireClassLoader );
         Object[] params = { src, logger };
         return instantiateObject( DefaultReporterFactory.class.getName(), args, params, surefireClassLoader );
     }
@@ -84,7 +84,6 @@ public class CommonReflector
                                                      int.class, String.class, String.class, boolean.class,
                                                      statelessTestsetReporter, consoleOutputReporter,
                                                      statelessTestsetInfoReporter );
-        //noinspection BooleanConstructorCall
         Object[] params = { reporterConfiguration.isUseFile(), reporterConfiguration.isPrintSummary(),
             reporterConfiguration.getReportFormat(), reporterConfiguration.isRedirectTestOutputToFile(),
             reporterConfiguration.getReportsDirectory(),
@@ -98,4 +97,17 @@ public class CommonReflector
         };
         return newInstance( constructor, params );
     }
+
+    static Object createConsoleLogger( ConsoleLogger consoleLogger, ClassLoader cl )
+    {
+        try
+        {
+            Class<?> decoratorClass = cl.loadClass( ConsoleLoggerDecorator.class.getName() );
+            return getConstructor( decoratorClass, Object.class ).newInstance( consoleLogger );
+        }
+        catch ( Exception e )
+        {
+            throw new SurefireReflectionException( e );
+        }
+    }
 }
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/SurefireDependencyResolver.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/SurefireDependencyResolver.java
index 4684563..6da4a1a 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/SurefireDependencyResolver.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/SurefireDependencyResolver.java
@@ -74,6 +74,7 @@ final class SurefireDependencyResolver
             "surefire-junit-platform",
             "surefire-api",
             "surefire-logger-api",
+            "surefire-shared-utils",
             "common-java5",
             "common-junit3",
             "common-junit4",
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/AbstractClasspathForkConfiguration.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/AbstractClasspathForkConfiguration.java
index 692f486..f8e08ea 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/AbstractClasspathForkConfiguration.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/AbstractClasspathForkConfiguration.java
@@ -21,6 +21,7 @@ package org.apache.maven.plugin.surefire.booterclient;
 
 import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
 import org.apache.maven.surefire.booter.Classpath;
+import org.apache.maven.surefire.extensions.ForkNodeFactory;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -49,10 +50,12 @@ abstract class AbstractClasspathForkConfiguration
                                         int forkCount,
                                         boolean reuseForks,
                                         @Nonnull Platform pluginPlatform,
-                                        @Nonnull ConsoleLogger log )
+                                        @Nonnull ConsoleLogger log,
+                                        @Nonnull ForkNodeFactory forkNodeFactory )
     {
         super( bootClasspath, tempDirectory, debugLine, workingDirectory, modelProperties, argLine,
-                environmentVariables, excludedEnvironmentVariables, debug, forkCount, reuseForks, pluginPlatform, log );
+            environmentVariables, excludedEnvironmentVariables, debug, forkCount, reuseForks, pluginPlatform, log,
+            forkNodeFactory );
     }
 
     @Override
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 7eacb74..9a04326 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
@@ -71,6 +71,7 @@ import static org.apache.maven.surefire.booter.BooterConstants.TESTARTIFACT_CLAS
 import static org.apache.maven.surefire.booter.BooterConstants.TESTARTIFACT_VERSION;
 import static org.apache.maven.surefire.booter.BooterConstants.USEMANIFESTONLYJAR;
 import static org.apache.maven.surefire.booter.BooterConstants.USESYSTEMCLASSLOADER;
+import static org.apache.maven.surefire.booter.BooterConstants.FORK_NODE_CONNECTION_STRING;
 import static org.apache.maven.surefire.booter.SystemPropertyManager.writePropertiesFile;
 
 /**
@@ -102,11 +103,11 @@ class BooterSerializer
      */
     File serialize( KeyValueSource sourceProperties, ProviderConfiguration providerConfiguration,
                     StartupConfiguration startupConfiguration, Object testSet, boolean readTestsFromInStream,
-                    Long pid, int forkNumber )
+                    Long pid, int forkNumber, String forkNodeConnectionString )
         throws IOException
     {
         SurefireProperties properties = new SurefireProperties( sourceProperties );
-
+        properties.setNullableProperty( FORK_NODE_CONNECTION_STRING, forkNodeConnectionString );
         properties.setProperty( PLUGIN_PID, pid );
 
         AbstractPathConfiguration cp = startupConfiguration.getClasspathConfiguration();
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ClasspathForkConfiguration.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ClasspathForkConfiguration.java
index 1ca3932..9c906c4 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ClasspathForkConfiguration.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ClasspathForkConfiguration.java
@@ -24,6 +24,7 @@ import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
 import org.apache.maven.surefire.booter.Classpath;
 import org.apache.maven.surefire.booter.StartupConfiguration;
 import org.apache.maven.surefire.booter.SurefireBooterForkException;
+import org.apache.maven.surefire.extensions.ForkNodeFactory;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -48,10 +49,12 @@ public final class ClasspathForkConfiguration
                                        @Nonnull String[] excludedEnvironmentVariables,
                                        boolean debug, int forkCount,
                                        boolean reuseForks, @Nonnull Platform pluginPlatform,
-                                       @Nonnull ConsoleLogger log )
+                                       @Nonnull ConsoleLogger log,
+                                       @Nonnull ForkNodeFactory forkNodeFactory )
     {
         super( bootClasspath, tempDirectory, debugLine, workingDirectory, modelProperties, argLine,
-                environmentVariables, excludedEnvironmentVariables, debug, forkCount, reuseForks, pluginPlatform, log );
+            environmentVariables, excludedEnvironmentVariables, debug, forkCount, reuseForks, pluginPlatform, log,
+            forkNodeFactory );
     }
 
     @Override
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/DefaultForkConfiguration.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/DefaultForkConfiguration.java
index 4ab4435..443bf45 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/DefaultForkConfiguration.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/DefaultForkConfiguration.java
@@ -26,6 +26,7 @@ import org.apache.maven.surefire.booter.AbstractPathConfiguration;
 import org.apache.maven.surefire.booter.Classpath;
 import org.apache.maven.surefire.booter.StartupConfiguration;
 import org.apache.maven.surefire.booter.SurefireBooterForkException;
+import org.apache.maven.surefire.extensions.ForkNodeFactory;
 import org.apache.maven.surefire.util.internal.ImmutableMap;
 
 import javax.annotation.Nonnull;
@@ -65,6 +66,7 @@ public abstract class DefaultForkConfiguration
     private final boolean reuseForks;
     @Nonnull private final Platform pluginPlatform;
     @Nonnull private final ConsoleLogger log;
+    @Nonnull private final ForkNodeFactory forkNodeFactory;
 
     @SuppressWarnings( "checkstyle:parameternumber" )
     protected DefaultForkConfiguration( @Nonnull Classpath booterClasspath,
@@ -79,7 +81,8 @@ public abstract class DefaultForkConfiguration
                                      int forkCount,
                                      boolean reuseForks,
                                      @Nonnull Platform pluginPlatform,
-                                     @Nonnull ConsoleLogger log )
+                                     @Nonnull ConsoleLogger log,
+                                     @Nonnull ForkNodeFactory forkNodeFactory )
     {
         this.booterClasspath = booterClasspath;
         this.tempDirectory = tempDirectory;
@@ -94,6 +97,7 @@ public abstract class DefaultForkConfiguration
         this.reuseForks = reuseForks;
         this.pluginPlatform = pluginPlatform;
         this.log = log;
+        this.forkNodeFactory = forkNodeFactory;
     }
 
     protected abstract void resolveClasspath( @Nonnull OutputStreamFlushableCommandline cli,
@@ -108,6 +112,13 @@ public abstract class DefaultForkConfiguration
         return jvmArgLine;
     }
 
+    @Nonnull
+    @Override
+    public final ForkNodeFactory getForkNodeFactory()
+    {
+        return forkNodeFactory;
+    }
+
     /**
      * @param config       The startup configuration
      * @param forkNumber   index of forked JVM, to be the replacement in the argLine
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ForkConfiguration.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ForkConfiguration.java
index 92bebd0..9fddf96 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ForkConfiguration.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ForkConfiguration.java
@@ -25,6 +25,7 @@ import org.apache.maven.surefire.booter.Classpath;
 import org.apache.maven.surefire.booter.ForkedBooter;
 import org.apache.maven.surefire.booter.StartupConfiguration;
 import org.apache.maven.surefire.booter.SurefireBooterForkException;
+import org.apache.maven.surefire.extensions.ForkNodeFactory;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -39,6 +40,7 @@ public abstract class ForkConfiguration
 {
     static final String DEFAULT_PROVIDER_CLASS = ForkedBooter.class.getName();
 
+    @Nonnull public abstract ForkNodeFactory getForkNodeFactory();
     @Nonnull public abstract File getTempDirectory();
     @Nullable protected abstract String getDebugLine();
     @Nonnull protected abstract File getWorkingDirectory();
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 0ba2f46..a41384a 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
@@ -22,7 +22,7 @@ package org.apache.maven.plugin.surefire.booterclient;
 import org.apache.maven.plugin.surefire.CommonReflector;
 import org.apache.maven.plugin.surefire.StartupReportConfiguration;
 import org.apache.maven.plugin.surefire.SurefireProperties;
-import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.AbstractForkInputStream;
+import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.AbstractCommandReader;
 import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.NotifiableTestStream;
 import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.OutputStreamFlushableCommandline;
 import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.TestLessInputStream;
@@ -33,12 +33,6 @@ import org.apache.maven.plugin.surefire.booterclient.output.NativeStdErrStreamCo
 import org.apache.maven.plugin.surefire.booterclient.output.ThreadedStreamConsumer;
 import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
 import org.apache.maven.plugin.surefire.report.DefaultReporterFactory;
-import org.apache.maven.surefire.extensions.util.CommandlineExecutor;
-import org.apache.maven.surefire.extensions.util.CommandlineStreams;
-import org.apache.maven.surefire.extensions.util.CountdownCloseable;
-import org.apache.maven.surefire.extensions.util.LineConsumerThread;
-import org.apache.maven.surefire.extensions.util.StreamFeeder;
-import org.apache.maven.surefire.shared.utils.cli.CommandLineException;
 import org.apache.maven.surefire.booter.AbstractPathConfiguration;
 import org.apache.maven.surefire.booter.PropertiesWrapper;
 import org.apache.maven.surefire.booter.ProviderConfiguration;
@@ -47,6 +41,14 @@ import org.apache.maven.surefire.booter.Shutdown;
 import org.apache.maven.surefire.booter.StartupConfiguration;
 import org.apache.maven.surefire.booter.SurefireBooterForkException;
 import org.apache.maven.surefire.booter.SurefireExecutionException;
+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.util.CommandlineExecutor;
+import org.apache.maven.surefire.extensions.util.CommandlineStreams;
+import org.apache.maven.surefire.extensions.util.CountdownCloseable;
+import org.apache.maven.surefire.extensions.util.LineConsumerThread;
 import org.apache.maven.surefire.providerapi.SurefireProvider;
 import org.apache.maven.surefire.report.StackTraceWriter;
 import org.apache.maven.surefire.suite.RunResult;
@@ -72,14 +74,12 @@ import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import static java.lang.StrictMath.min;
 import static java.lang.System.currentTimeMillis;
 import static java.lang.Thread.currentThread;
 import static java.util.Collections.addAll;
-import static java.util.Objects.requireNonNull;
 import static java.util.concurrent.Executors.newScheduledThreadPool;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
@@ -88,12 +88,11 @@ import static org.apache.maven.plugin.surefire.SurefireHelper.DUMP_FILE_PREFIX;
 import static org.apache.maven.plugin.surefire.SurefireHelper.replaceForkThreadsInPath;
 import static org.apache.maven.plugin.surefire.booterclient.ForkNumberBucket.drawNumber;
 import static org.apache.maven.plugin.surefire.booterclient.ForkNumberBucket.returnNumber;
-import static org.apache.maven.plugin.surefire.booterclient.lazytestprovider.TestLessInputStream
-                      .TestLessInputStreamBuilder;
-import static org.apache.maven.surefire.shared.utils.cli.ShutdownHookUtils.addShutDownHook;
-import static org.apache.maven.surefire.shared.utils.cli.ShutdownHookUtils.removeShutdownHook;
+import static org.apache.maven.plugin.surefire.booterclient.lazytestprovider.TestLessInputStream.TestLessInputStreamBuilder;
 import static org.apache.maven.surefire.booter.SystemPropertyManager.writePropertiesFile;
 import static org.apache.maven.surefire.cli.CommandLineOption.SHOW_ERRORS;
+import static org.apache.maven.surefire.shared.utils.cli.ShutdownHookUtils.addShutDownHook;
+import static org.apache.maven.surefire.shared.utils.cli.ShutdownHookUtils.removeShutdownHook;
 import static org.apache.maven.surefire.suite.RunResult.SUCCESS;
 import static org.apache.maven.surefire.suite.RunResult.failure;
 import static org.apache.maven.surefire.suite.RunResult.timeout;
@@ -192,7 +191,7 @@ public class ForkStarter
                 {
                     closeable.close();
                 }
-                catch ( IOException e )
+                catch ( IOException | RuntimeException e )
                 {
                     // This error does not fail a test and does not necessarily mean that the forked JVM std/out stream
                     // was not closed, see ThreadedStreamConsumer. This error means that JVM wrote messages to a native
@@ -212,11 +211,17 @@ public class ForkStarter
         @Override
         public void close()
         {
-            run();
-            testProvidingInputStream.clear();
-            if ( inputStreamCloserHook != null )
+            try
             {
-                removeShutdownHook( inputStreamCloserHook );
+                run();
+            }
+            finally
+            {
+                testProvidingInputStream.clear();
+                if ( inputStreamCloserHook != null )
+                {
+                    removeShutdownHook( inputStreamCloserHook );
+                }
             }
         }
 
@@ -287,9 +292,9 @@ public class ForkStarter
             DefaultReporterFactory forkedReporterFactory =
                     new DefaultReporterFactory( startupReportConfiguration, log, forkNumber );
             defaultReporterFactories.add( forkedReporterFactory );
-            ForkClient forkClient =
-                    new ForkClient( forkedReporterFactory, stream, log, new AtomicBoolean(), forkNumber );
-            return fork( null, props, forkClient, effectiveSystemProperties, forkNumber, stream, false );
+            ForkClient forkClient = new ForkClient( forkedReporterFactory, stream, forkNumber );
+            return fork( null, props, forkClient, effectiveSystemProperties, forkNumber, stream,
+                    forkConfiguration.getForkNodeFactory(), false );
         }
         finally
         {
@@ -350,7 +355,6 @@ public class ForkStarter
             int failFastCount = providerConfiguration.getSkipAfterFailureCount();
             final AtomicInteger notifyStreamsToSkipTestsJustNow = new AtomicInteger( failFastCount );
             final Collection<Future<RunResult>> results = new ArrayList<>( forkCount );
-            final AtomicBoolean printedErrorStream = new AtomicBoolean();
             for ( final TestProvidingInputStream testProvidingInputStream : testStreams )
             {
                 Callable<RunResult> pf = new Callable<RunResult>()
@@ -363,8 +367,7 @@ public class ForkStarter
                         DefaultReporterFactory reporter =
                                 new DefaultReporterFactory( startupReportConfiguration, log, forkNumber );
                         defaultReporterFactories.add( reporter );
-                        ForkClient forkClient = new ForkClient( reporter, testProvidingInputStream, log,
-                                printedErrorStream, forkNumber )
+                        ForkClient forkClient = new ForkClient( reporter, testProvidingInputStream, forkNumber )
                         {
                             @Override
                             protected void stopOnNextTest()
@@ -379,7 +382,8 @@ public class ForkStarter
                         try
                         {
                             return fork( null, new PropertiesWrapper( providerProperties ), forkClient,
-                                    effectiveSystemProperties, forkNumber, testProvidingInputStream, true );
+                                    effectiveSystemProperties, forkNumber, testProvidingInputStream,
+                                    forkConfiguration.getForkNodeFactory(), true );
                         }
                         finally
                         {
@@ -423,7 +427,6 @@ public class ForkStarter
             addShutDownHook( shutdown );
             int failFastCount = providerConfiguration.getSkipAfterFailureCount();
             final AtomicInteger notifyStreamsToSkipTestsJustNow = new AtomicInteger( failFastCount );
-            final AtomicBoolean printedErrorStream = new AtomicBoolean();
             for ( final Object testSet : getSuitesIterator() )
             {
                 Callable<RunResult> pf = new Callable<RunResult>()
@@ -437,8 +440,7 @@ public class ForkStarter
                             new DefaultReporterFactory( startupReportConfiguration, log, forkNumber );
                         defaultReporterFactories.add( forkedReporterFactory );
                         TestLessInputStream stream = builder.build();
-                        ForkClient forkClient = new ForkClient( forkedReporterFactory, stream,
-                                log, printedErrorStream, forkNumber )
+                        ForkClient forkClient = new ForkClient( forkedReporterFactory, stream, forkNumber )
                         {
                             @Override
                             protected void stopOnNextTest()
@@ -453,7 +455,8 @@ public class ForkStarter
                         {
                             return fork( testSet,
                                          new PropertiesWrapper( providerConfiguration.getProviderProperties() ),
-                                         forkClient, effectiveSystemProperties, forkNumber, stream, false );
+                                         forkClient, effectiveSystemProperties, forkNumber, stream,
+                                         forkConfiguration.getForkNodeFactory(), false );
                         }
                         finally
                         {
@@ -549,21 +552,28 @@ public class ForkStarter
 
     private RunResult fork( Object testSet, PropertiesWrapper providerProperties, ForkClient forkClient,
                             SurefireProperties effectiveSystemProperties, int forkNumber,
-                            AbstractForkInputStream commandInputStream, boolean readTestsFromInStream )
+                            AbstractCommandReader commandReader, ForkNodeFactory forkNodeFactory,
+                            boolean readTestsFromInStream )
         throws SurefireBooterForkException
     {
+        CloseableCloser closer = new CloseableCloser( forkNumber, commandReader );
         final String tempDir;
         final File surefireProperties;
         final File systPropsFile;
+        final ForkChannel forkChannel;
         try
         {
+            forkChannel = forkNodeFactory.createForkChannel( forkNumber, log );
+            closer.addCloseable( forkChannel );
             tempDir = forkConfiguration.getTempDirectory().getCanonicalPath();
             BooterSerializer booterSerializer = new BooterSerializer( forkConfiguration );
             Long pluginPid = forkConfiguration.getPluginPlatform().getPluginPid();
-            surefireProperties = booterSerializer.serialize( providerProperties, providerConfiguration,
-                    startupConfiguration, testSet, readTestsFromInStream, pluginPid, forkNumber );
-
             log.debug( "Determined Maven Process ID " + pluginPid );
+            String connectionString = forkChannel.getForkNodeConnectionString();
+            log.debug( "Fork Channel [" + forkNumber + "] connection string '" + connectionString
+                + "' for the implementation " + forkChannel.getClass() );
+            surefireProperties = booterSerializer.serialize( providerProperties, providerConfiguration,
+                    startupConfiguration, testSet, readTestsFromInStream, pluginPid, forkNumber, connectionString );
 
             if ( effectiveSystemProperties != null )
             {
@@ -588,10 +598,7 @@ public class ForkStarter
         OutputStreamFlushableCommandline cli =
                 forkConfiguration.createCommandLine( startupConfiguration, forkNumber, dumpLogDir );
 
-        if ( commandInputStream != null )
-        {
-            commandInputStream.setFlushReceiverProvider( cli );
-        }
+        commandReader.setFlushReceiverProvider( cli );
 
         cli.createArg().setValue( tempDir );
         cli.createArg().setValue( DUMP_FILE_PREFIX + forkNumber );
@@ -602,38 +609,40 @@ public class ForkStarter
         }
 
         ThreadedStreamConsumer eventConsumer = new ThreadedStreamConsumer( forkClient );
-        CloseableCloser closer = new CloseableCloser( forkNumber, eventConsumer, requireNonNull( commandInputStream ) );
+        closer.addCloseable( eventConsumer );
 
         log.debug( "Forking command line: " + cli );
 
         Integer result = null;
         RunResult runResult = null;
         SurefireBooterForkException booterForkException = null;
-        StreamFeeder in = null;
-        LineConsumerThread out = null;
-        LineConsumerThread err = null;
+        CloseableDaemonThread in = null;
+        CloseableDaemonThread out = null;
+        CloseableDaemonThread err = null;
         DefaultReporterFactory reporter = forkClient.getDefaultReporterFactory();
         currentForkClients.add( forkClient );
-        CountdownCloseable countdownCloseable = new CountdownCloseable( eventConsumer, 2 );
+        CountdownCloseable countdownCloseable =
+            new CountdownCloseable( eventConsumer, 1 + ( forkChannel.useStdOut() ? 1 : 0 ) );
         try ( CommandlineExecutor exec = new CommandlineExecutor( cli, countdownCloseable ) )
         {
-            // default impl of the extension - solves everything including the encoder/decoder, Process starter,
-            // adaptation of the streams to pipes and sockets
-            // non-default impl may use another classes and not the LineConsumerThread, StreamFeeder - freedom
-            // BEGIN: beginning of the call of the extension
             CommandlineStreams streams = exec.execute();
             closer.addCloseable( streams );
-            in = new StreamFeeder( "std-in-fork-" + forkNumber, streams.getStdInChannel(), commandInputStream );
+
+            forkChannel.connectToClient();
+            log.debug( "Fork Channel [" + forkNumber + "] connected to the client." );
+
+            in = forkChannel.bindCommandReader( commandReader, streams.getStdInChannel() );
             in.start();
-            out = new LineConsumerThread( "std-out-fork-" + forkNumber, streams.getStdOutChannel(),
-                                          eventConsumer, countdownCloseable );
+
+            out = forkChannel.bindEventHandler( eventConsumer, countdownCloseable, streams.getStdOutChannel() );
             out.start();
-            NativeStdErrStreamConsumer stdErrConsumer = new NativeStdErrStreamConsumer( reporter );
-            err = new LineConsumerThread( "std-err-fork-" + forkNumber, streams.getStdErrChannel(),
-                                          stdErrConsumer, countdownCloseable );
+
+            EventHandler<String> stdErrConsumer = new NativeStdErrStreamConsumer( reporter );
+            err = new LineConsumerThread( "fork-" + forkNumber + "-err-thread-", streams.getStdErrChannel(),
+                stdErrConsumer, countdownCloseable );
             err.start();
+
             result = exec.awaitExit();
-            // END: end of the call of the extension
 
             if ( forkClient.hadTimeout() )
             {
@@ -647,13 +656,15 @@ public class ForkStarter
         }
         catch ( InterruptedException e )
         {
+            log.error( "Closing the streams after (InterruptedException) '" + e.getLocalizedMessage() + "'" );
             // maybe implement it in the Future.cancel() of the extension or similar
             in.disable();
             out.disable();
             err.disable();
         }
-        catch ( CommandLineException e )
+        catch ( Exception e )
         {
+            // CommandLineException from pipes and IOException from sockets
             runResult = failure( reporter.getGlobalRunStatistics().getRunResult(), e );
             String cliErr = e.getLocalizedMessage();
             Throwable cause = e.getCause();
@@ -662,10 +673,12 @@ public class ForkStarter
         }
         finally
         {
+            log.debug( "Closing the fork " + forkNumber + " after "
+                + ( forkClient.isSaidGoodBye() ? "saying GoodBye." : "not saying Good Bye." ) );
             currentForkClients.remove( forkClient );
             try
             {
-                Closeable c = forkClient.isSaidGoodBye() ? closer : commandInputStream;
+                Closeable c = forkClient.isSaidGoodBye() ? closer : commandReader;
                 c.close();
             }
             catch ( IOException e )
@@ -710,7 +723,7 @@ public class ForkStarter
                     //noinspection ThrowFromFinallyBlock
                     throw new SurefireBooterForkException( "There was an error in the forked process"
                                                         + detail
-                                                        + ( stackTrace == null ? "" : stackTrace ), cause );
+                                                        + ( stackTrace == null ? "" : "\n" + stackTrace ), cause );
                 }
                 if ( !forkClient.isSaidGoodBye() )
                 {
@@ -729,7 +742,6 @@ public class ForkStarter
 
             if ( booterForkException != null )
             {
-                // noinspection ThrowFromFinallyBlock
                 throw booterForkException;
             }
         }
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/JarManifestForkConfiguration.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/JarManifestForkConfiguration.java
index 02c275c..382bec6 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/JarManifestForkConfiguration.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/JarManifestForkConfiguration.java
@@ -28,6 +28,7 @@ import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
 import org.apache.maven.surefire.booter.Classpath;
 import org.apache.maven.surefire.booter.StartupConfiguration;
 import org.apache.maven.surefire.booter.SurefireBooterForkException;
+import org.apache.maven.surefire.extensions.ForkNodeFactory;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -70,10 +71,12 @@ public final class JarManifestForkConfiguration
                                          @Nonnull String[] excludedEnvironmentVariables,
                                          boolean debug,
                                          int forkCount, boolean reuseForks, @Nonnull Platform pluginPlatform,
-                                         @Nonnull ConsoleLogger log )
+                                         @Nonnull ConsoleLogger log,
+                                         @Nonnull ForkNodeFactory forkNodeFactory )
     {
         super( bootClasspath, tempDirectory, debugLine, workingDirectory, modelProperties, argLine,
-                environmentVariables, excludedEnvironmentVariables, debug, forkCount, reuseForks, pluginPlatform, log );
+            environmentVariables, excludedEnvironmentVariables, debug, forkCount, reuseForks, pluginPlatform, log,
+            forkNodeFactory );
     }
 
     @Override
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ModularClasspathForkConfiguration.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ModularClasspathForkConfiguration.java
index 8f5030b..d659a6c 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ModularClasspathForkConfiguration.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ModularClasspathForkConfiguration.java
@@ -28,6 +28,7 @@ import org.apache.maven.surefire.booter.ModularClasspath;
 import org.apache.maven.surefire.booter.ModularClasspathConfiguration;
 import org.apache.maven.surefire.booter.StartupConfiguration;
 import org.apache.maven.surefire.booter.SurefireBooterForkException;
+import org.apache.maven.surefire.extensions.ForkNodeFactory;
 
 import javax.annotation.Nonnegative;
 import javax.annotation.Nonnull;
@@ -67,10 +68,12 @@ public class ModularClasspathForkConfiguration
                                               @Nonnegative int forkCount,
                                               boolean reuseForks,
                                               @Nonnull Platform pluginPlatform,
-                                              @Nonnull ConsoleLogger log )
+                                              @Nonnull ConsoleLogger log,
+                                              @Nonnull ForkNodeFactory forkNodeFactory )
     {
         super( bootClasspath, tempDirectory, debugLine, workingDirectory, modelProperties, argLine,
-                environmentVariables, excludedEnvironmentVariables, debug, forkCount, reuseForks, pluginPlatform, log );
+            environmentVariables, excludedEnvironmentVariables, debug, forkCount, reuseForks, pluginPlatform, log,
+            forkNodeFactory );
     }
 
     @Override
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/AbstractForkInputStream.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/AbstractCommandReader.java
similarity index 84%
rename from maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/AbstractForkInputStream.java
rename to maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/AbstractCommandReader.java
index a884c15..a31e9f7 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/AbstractForkInputStream.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/AbstractCommandReader.java
@@ -19,32 +19,34 @@ package org.apache.maven.plugin.surefire.booterclient.lazytestprovider;
  * under the License.
  */
 
+import org.apache.maven.surefire.extensions.CommandReader;
+
 import java.io.IOException;
-import java.io.InputStream;
 
 import static java.util.Objects.requireNonNull;
 
 /**
- * Reader stream sends bytes to forked jvm std-{@link InputStream input-stream}.
+ * Stream reader returns bytes which ar finally sent to the forked jvm std-input-stream.
  *
  * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
  * @since 2.19
  */
-public abstract class AbstractForkInputStream
-    extends InputStream
-    implements NotifiableTestStream
+public abstract class AbstractCommandReader
+        implements CommandReader, DefferedChannelCommandSender
 {
     private volatile FlushReceiverProvider flushReceiverProvider;
 
     /**
      * @param flushReceiverProvider the provider for a flush receiver.
      */
+    @Override
     public void setFlushReceiverProvider( FlushReceiverProvider flushReceiverProvider )
     {
         this.flushReceiverProvider = requireNonNull( flushReceiverProvider );
     }
 
-    protected boolean tryFlush()
+    @Override
+    public void tryFlush()
         throws IOException
     {
         if ( flushReceiverProvider != null )
@@ -53,9 +55,7 @@ public abstract class AbstractForkInputStream
             if ( flushReceiver != null )
             {
                 flushReceiver.flush();
-                return true;
             }
         }
-        return false;
     }
 }
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/AbstractCommandStream.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/DefaultCommandReader.java
similarity index 63%
rename from maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/AbstractCommandStream.java
rename to maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/DefaultCommandReader.java
index 31b56c4..9aa19c3 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/AbstractCommandStream.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/DefaultCommandReader.java
@@ -20,7 +20,6 @@ package org.apache.maven.plugin.surefire.booterclient.lazytestprovider;
  */
 
 import org.apache.maven.surefire.booter.Command;
-import org.apache.maven.surefire.booter.MasterProcessCommand;
 
 import java.io.IOException;
 
@@ -31,14 +30,9 @@ import java.io.IOException;
  * @since 2.19
  * @see org.apache.maven.surefire.booter.Command
  */
-public abstract class AbstractCommandStream
-    extends AbstractForkInputStream
+public abstract class DefaultCommandReader
+        extends AbstractCommandReader
 {
-    private byte[] currentBuffer;
-    private int currentPos;
-
-    protected abstract boolean isClosed();
-
     /**
      * Opposite to {@link #isClosed()}.
      * @return {@code true} if not closed
@@ -62,59 +56,35 @@ public abstract class AbstractCommandStream
     protected abstract Command nextCommand();
 
     /**
-     * Returns quietly and immediately.
-     */
-    protected final void invalidateInternalBuffer()
-    {
-        currentBuffer = null;
-        currentPos = 0;
-    }
-
-    /**
      * Used by single thread in StreamFeeder class.
      *
      * @return {@inheritDoc}
      * @throws IOException {@inheritDoc}
      */
-    @SuppressWarnings( "checkstyle:magicnumber" )
     @Override
-    public int read()
+    public Command readNextCommand()
         throws IOException
     {
+        tryFlush();
+
         if ( isClosed() )
         {
-            tryFlush();
-            return -1;
+            return null;
         }
 
-        if ( currentBuffer == null )
+        if ( !canContinue() )
         {
-            tryFlush();
-
-            if ( !canContinue() )
-            {
-                close();
-                return -1;
-            }
-
-            beforeNextCommand();
-
-            if ( isClosed() )
-            {
-                return -1;
-            }
-
-            Command cmd = nextCommand();
-            MasterProcessCommand cmdType = cmd.getCommandType();
-            currentBuffer = cmdType.hasDataType() ? cmdType.encode( cmd.getData() ) : cmdType.encode();
+            close();
+            return null;
         }
 
-        int b =  currentBuffer[currentPos++] & 0xff;
-        if ( currentPos == currentBuffer.length )
+        beforeNextCommand();
+
+        if ( isClosed() )
         {
-            currentBuffer = null;
-            currentPos = 0;
+            return null;
         }
-        return b;
+
+        return nextCommand();
     }
 }
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedChannelDecoderErrorHandler.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/DefferedChannelCommandSender.java
similarity index 67%
rename from maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedChannelDecoderErrorHandler.java
rename to maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/DefferedChannelCommandSender.java
index 8faa803..e489caa 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedChannelDecoderErrorHandler.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/DefferedChannelCommandSender.java
@@ -1,4 +1,4 @@
-package org.apache.maven.plugin.surefire.booterclient.output;
+package org.apache.maven.plugin.surefire.booterclient.lazytestprovider;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,11 +19,17 @@ package org.apache.maven.plugin.surefire.booterclient.output;
  * under the License.
  */
 
+import java.io.Closeable;
+
 /**
+ * Physical implementation of command sender.<br>
+ * Instance of {@link AbstractCommandReader} (namely {@link TestLessInputStream} or {@link TestProvidingInputStream}).
+ *
  * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
  * @since 3.0.0-M4
  */
-public interface ForkedChannelDecoderErrorHandler
+public interface DefferedChannelCommandSender
+    extends NotifiableTestStream, Closeable
 {
-    void handledError( String line, Throwable e );
+    void setFlushReceiverProvider( FlushReceiverProvider flushReceiverProvider );
 }
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/TestLessInputStream.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/TestLessInputStream.java
index 372ce00..a1060a7 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/TestLessInputStream.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/TestLessInputStream.java
@@ -45,7 +45,7 @@ import static org.apache.maven.surefire.booter.Command.toShutdown;
  * @since 2.19
  */
 public final class TestLessInputStream
-    extends AbstractCommandStream
+        extends DefaultCommandReader
 {
     private final Semaphore barrier = new Semaphore( 0 );
 
@@ -108,7 +108,7 @@ public final class TestLessInputStream
     }
 
     @Override
-    protected boolean isClosed()
+    public boolean isClosed()
     {
         return closed.get();
     }
@@ -141,7 +141,6 @@ public final class TestLessInputStream
     {
         if ( closed.compareAndSet( false, true ) )
         {
-            invalidateInternalBuffer();
             barrier.drainPermits();
             barrier.release();
         }
@@ -166,8 +165,6 @@ public final class TestLessInputStream
         }
         catch ( InterruptedException e )
         {
-            // help GC to free this object because StreamFeeder Thread cannot read it anyway after IOE
-            invalidateInternalBuffer();
             throw new IOException( e.getLocalizedMessage() );
         }
     }
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/TestProvidingInputStream.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/TestProvidingInputStream.java
index a4255cc..6f3a4de 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/TestProvidingInputStream.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/lazytestprovider/TestProvidingInputStream.java
@@ -31,6 +31,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
 import static org.apache.maven.surefire.booter.Command.BYE_ACK;
 import static org.apache.maven.surefire.booter.Command.NOOP;
 import static org.apache.maven.surefire.booter.Command.SKIP_SINCE_NEXT_TEST;
+import static org.apache.maven.surefire.booter.Command.TEST_SET_FINISHED;
 import static org.apache.maven.surefire.booter.Command.toRunClass;
 import static org.apache.maven.surefire.booter.Command.toShutdown;
 
@@ -50,7 +51,7 @@ import static org.apache.maven.surefire.booter.Command.toShutdown;
  * @author Tibor Digana (tibor17)
  */
 public final class TestProvidingInputStream
-    extends AbstractCommandStream
+        extends DefaultCommandReader
 {
     private final Semaphore barrier = new Semaphore( 0 );
 
@@ -77,7 +78,7 @@ public final class TestProvidingInputStream
     {
         if ( canContinue() )
         {
-            commands.add( Command.TEST_SET_FINISHED );
+            commands.add( TEST_SET_FINISHED );
             barrier.release();
         }
     }
@@ -129,7 +130,7 @@ public final class TestProvidingInputStream
         if ( cmd == null )
         {
             String cmdData = testClassNames.poll();
-            return cmdData == null ? Command.TEST_SET_FINISHED : toRunClass( cmdData );
+            return cmdData == null ? TEST_SET_FINISHED : toRunClass( cmdData );
         }
         else
         {
@@ -145,7 +146,7 @@ public final class TestProvidingInputStream
     }
 
     @Override
-    protected boolean isClosed()
+    public boolean isClosed()
     {
         return closed.get();
     }
@@ -167,7 +168,6 @@ public final class TestProvidingInputStream
     {
         if ( closed.compareAndSet( false, true ) )
         {
-            invalidateInternalBuffer();
             barrier.drainPermits();
             barrier.release();
         }
@@ -182,9 +182,7 @@ public final class TestProvidingInputStream
         }
         catch ( InterruptedException e )
         {
-            // help GC to free this object because StreamFeeder Thread cannot read it anyway after IOE
-            invalidateInternalBuffer();
-            throw new IOException( e.getLocalizedMessage() );
+            throw new IOException( e.getLocalizedMessage(), e );
         }
     }
 }
\ No newline at end of file
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkClient.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkClient.java
index 18cfe28..b4de014 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkClient.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkClient.java
@@ -22,7 +22,9 @@ package org.apache.maven.plugin.surefire.booterclient.output;
 import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.NotifiableTestStream;
 import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
 import org.apache.maven.plugin.surefire.report.DefaultReporterFactory;
-import org.apache.maven.surefire.shared.utils.cli.StreamConsumer;
+import org.apache.maven.surefire.eventapi.Event;
+import org.apache.maven.surefire.extensions.EventHandler;
+import org.apache.maven.surefire.providerapi.MasterProcessChannelEncoder;
 import org.apache.maven.surefire.report.ConsoleOutputReceiver;
 import org.apache.maven.surefire.report.ReportEntry;
 import org.apache.maven.surefire.report.RunListener;
@@ -30,25 +32,20 @@ import org.apache.maven.surefire.report.RunMode;
 import org.apache.maven.surefire.report.StackTraceWriter;
 import org.apache.maven.surefire.report.TestSetReportEntry;
 
-import java.io.BufferedReader;
+import javax.annotation.Nonnull;
 import java.io.File;
-import java.io.IOException;
-import java.io.StringReader;
 import java.util.Map;
 import java.util.Queue;
 import java.util.Set;
 import java.util.TreeSet;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicLong;
 
 import static java.lang.System.currentTimeMillis;
 import static java.util.Collections.unmodifiableMap;
 import static org.apache.maven.surefire.booter.Shutdown.KILL;
 import static org.apache.maven.surefire.report.CategorizedReportEntry.reportEntry;
-import static org.apache.maven.surefire.shared.utils.StringUtils.isBlank;
-import static org.apache.maven.surefire.shared.utils.StringUtils.isNotBlank;
 
 // todo move to the same package with ForkStarter
 
@@ -58,9 +55,8 @@ import static org.apache.maven.surefire.shared.utils.StringUtils.isNotBlank;
  * @author Kristian Rosenvold
  */
 public class ForkClient
-     implements StreamConsumer
+    implements EventHandler<Event>
 {
-    private static final String PRINTABLE_JVM_NATIVE_STREAM = "Listening for transport dt_socket at address:";
     private static final long START_TIME_ZERO = 0L;
     private static final long START_TIME_NEGATIVE_TIMEOUT = -1L;
 
@@ -74,26 +70,14 @@ public class ForkClient
 
     /**
      * <em>testSetStartedAt</em> is set to non-zero after received
-     * {@link org.apache.maven.surefire.booter.ForkedChannelEncoder#testSetStarting(ReportEntry, boolean)}.
+     * {@link MasterProcessChannelEncoder#testSetStarting(ReportEntry, boolean)}.
      */
     private final AtomicLong testSetStartedAt = new AtomicLong( START_TIME_ZERO );
 
-    private final ForkedChannelDecoder decoder = new ForkedChannelDecoder();
-
-    private final ConsoleLogger log;
-
-    /**
-     * prevents from printing same warning
-     */
-    private final AtomicBoolean printedErrorStream;
+    private final ForkedProcessEventNotifier notifier = new ForkedProcessEventNotifier();
 
     private final int forkNumber;
 
-    /**
-     * Used by single Thread started by {@link ThreadedStreamConsumer} and therefore does not need to be volatile.
-     */
-    private final ForkedChannelDecoderErrorHandler errorHandler;
-
     private RunListener testSetReporter;
 
     /**
@@ -104,41 +88,29 @@ public class ForkClient
     private volatile StackTraceWriter errorInFork;
 
     public ForkClient( DefaultReporterFactory defaultReporterFactory, NotifiableTestStream notifiableTestStream,
-                       ConsoleLogger log, AtomicBoolean printedErrorStream, int forkNumber )
+                       int forkNumber )
     {
         this.defaultReporterFactory = defaultReporterFactory;
         this.notifiableTestStream = notifiableTestStream;
-        this.log = log;
-        this.printedErrorStream = printedErrorStream;
         this.forkNumber = forkNumber;
-        decoder.setTestSetStartingListener( new TestSetStartingListener() );
-        decoder.setTestSetCompletedListener( new TestSetCompletedListener() );
-        decoder.setTestStartingListener( new TestStartingListener() );
-        decoder.setTestSucceededListener( new TestSucceededListener() );
-        decoder.setTestFailedListener( new TestFailedListener() );
-        decoder.setTestSkippedListener( new TestSkippedListener() );
-        decoder.setTestErrorListener( new TestErrorListener() );
-        decoder.setTestAssumptionFailureListener( new TestAssumptionFailureListener() );
-        decoder.setSystemPropertiesListener( new SystemPropertiesListener() );
-        decoder.setStdOutListener( new StdOutListener() );
-        decoder.setStdErrListener( new StdErrListener() );
-        decoder.setConsoleInfoListener( new ConsoleListener() );
-        decoder.setAcquireNextTestListener( new AcquireNextTestListener() );
-        decoder.setConsoleErrorListener( new ErrorListener() );
-        decoder.setByeListener( new ByeListener() );
-        decoder.setStopOnNextTestListener( new StopOnNextTestListener() );
-        decoder.setConsoleDebugListener( new DebugListener() );
-        decoder.setConsoleWarningListener( new WarningListener() );
-        errorHandler = new ErrorHandler();
-    }
-
-    private final class ErrorHandler implements ForkedChannelDecoderErrorHandler
-    {
-        @Override
-        public void handledError( String line, Throwable e )
-        {
-            logStreamWarning( line, e );
-        }
+        notifier.setTestSetStartingListener( new TestSetStartingListener() );
+        notifier.setTestSetCompletedListener( new TestSetCompletedListener() );
+        notifier.setTestStartingListener( new TestStartingListener() );
+        notifier.setTestSucceededListener( new TestSucceededListener() );
+        notifier.setTestFailedListener( new TestFailedListener() );
+        notifier.setTestSkippedListener( new TestSkippedListener() );
+        notifier.setTestErrorListener( new TestErrorListener() );
+        notifier.setTestAssumptionFailureListener( new TestAssumptionFailureListener() );
+        notifier.setSystemPropertiesListener( new SystemPropertiesListener() );
+        notifier.setStdOutListener( new StdOutListener() );
+        notifier.setStdErrListener( new StdErrListener() );
+        notifier.setConsoleInfoListener( new ConsoleListener() );
+        notifier.setAcquireNextTestListener( new AcquireNextTestListener() );
+        notifier.setConsoleErrorListener( new ErrorListener() );
+        notifier.setByeListener( new ByeListener() );
+        notifier.setStopOnNextTestListener( new StopOnNextTestListener() );
+        notifier.setConsoleDebugListener( new DebugListener() );
+        notifier.setConsoleWarningListener( new WarningListener() );
     }
 
     private final class TestSetStartingListener
@@ -167,7 +139,7 @@ public class ForkClient
         }
     }
 
-    private final class TestStartingListener implements ForkedProcessReportEventListener
+    private final class TestStartingListener implements ForkedProcessReportEventListener<ReportEntry>
     {
         @Override
         public void handle( RunMode runMode, ReportEntry reportEntry )
@@ -177,7 +149,7 @@ public class ForkClient
         }
     }
 
-    private final class TestSucceededListener implements ForkedProcessReportEventListener
+    private final class TestSucceededListener implements ForkedProcessReportEventListener<ReportEntry>
     {
         @Override
         public void handle( RunMode runMode, ReportEntry reportEntry )
@@ -187,7 +159,7 @@ public class ForkClient
         }
     }
 
-    private final class TestFailedListener implements ForkedProcessReportEventListener
+    private final class TestFailedListener implements ForkedProcessReportEventListener<ReportEntry>
     {
         @Override
         public void handle( RunMode runMode, ReportEntry reportEntry )
@@ -197,7 +169,7 @@ public class ForkClient
         }
     }
 
-    private final class TestSkippedListener implements ForkedProcessReportEventListener
+    private final class TestSkippedListener implements ForkedProcessReportEventListener<ReportEntry>
     {
         @Override
         public void handle( RunMode runMode, ReportEntry reportEntry )
@@ -207,7 +179,7 @@ public class ForkClient
         }
     }
 
-    private final class TestErrorListener implements ForkedProcessReportEventListener
+    private final class TestErrorListener implements ForkedProcessReportEventListener<ReportEntry>
     {
         @Override
         public void handle( RunMode runMode, ReportEntry reportEntry )
@@ -217,7 +189,7 @@ public class ForkClient
         }
     }
 
-    private final class TestAssumptionFailureListener implements ForkedProcessReportEventListener
+    private final class TestAssumptionFailureListener implements ForkedProcessReportEventListener<ReportEntry>
     {
         @Override
         public void handle( RunMode runMode, ReportEntry reportEntry )
@@ -276,18 +248,19 @@ public class ForkClient
     private class ErrorListener implements ForkedProcessStackTraceEventListener
     {
         @Override
-        public void handle( String msg, String smartStackTrace, String stackTrace )
+        public void handle( @Nonnull StackTraceWriter stackTrace )
         {
+            String msg = stackTrace.getThrowable().getMessage();
             if ( errorInFork == null )
             {
-                errorInFork = deserializeStackTraceWriter( msg, smartStackTrace, stackTrace );
+                errorInFork = stackTrace.writeTraceToString() != null ? stackTrace : null;
                 if ( msg != null )
                 {
                     getOrCreateConsoleLogger()
                             .error( msg );
                 }
             }
-            dumpToLoFile( msg, null );
+            dumpToLoFile( msg );
         }
     }
 
@@ -372,12 +345,9 @@ public class ForkClient
     }
 
     @Override
-    public final void consumeLine( String s )
+    public final void handleEvent( @Nonnull Event event )
     {
-        if ( isNotBlank( s ) )
-        {
-            processLine( s );
-        }
+        notifier.notifyEvent( event );
     }
 
     private void setCurrentStartTime()
@@ -404,53 +374,11 @@ public class ForkClient
         return testSetReporter;
     }
 
-    private void processLine( String event )
-    {
-        decoder.handleEvent( event, errorHandler );
-    }
-
-    private File dumpToLoFile( String msg, Throwable e )
+    void dumpToLoFile( String msg )
     {
         File reportsDir = defaultReporterFactory.getReportsDirectory();
         InPluginProcessDumpSingleton util = InPluginProcessDumpSingleton.getSingleton();
-        return e == null
-                ? util.dumpStreamText( msg, reportsDir, forkNumber )
-                : util.dumpStreamException( e, msg, reportsDir, forkNumber );
-    }
-
-    private void logStreamWarning( String event, Throwable e )
-    {
-        if ( event == null || !event.contains( PRINTABLE_JVM_NATIVE_STREAM ) )
-        {
-            String msg = "Corrupted STDOUT by directly writing to native stream in forked JVM " + forkNumber + ".";
-            File dump = dumpToLoFile( msg + " Stream '" + event + "'.", e );
-
-            if ( printedErrorStream.compareAndSet( false, true ) )
-            {
-                log.warning( msg + " See FAQ web page and the dump file " + dump.getAbsolutePath() );
-            }
-
-            if ( log.isDebugEnabled() && event != null )
-            {
-                log.debug( event );
-            }
-        }
-        else
-        {
-            if ( log.isDebugEnabled() )
-            {
-                log.debug( event );
-            }
-            else if ( log.isInfoEnabled() )
-            {
-                log.info( event );
-            }
-            else
-            {
-                // In case of debugging forked JVM, see PRINTABLE_JVM_NATIVE_STREAM.
-                System.out.println( event );
-            }
-        }
+        util.dumpStreamText( msg, reportsDir, forkNumber );
     }
 
     private void writeTestOutput( String output, boolean newLine, boolean isStdout )
@@ -459,23 +387,6 @@ public class ForkClient
                 .writeTestOutput( output, newLine, isStdout );
     }
 
-    public final void consumeMultiLineContent( String s )
-            throws IOException
-    {
-        if ( isBlank( s ) )
-        {
-            logStreamWarning( s, null );
-        }
-        else
-        {
-            BufferedReader stringReader = new BufferedReader( new StringReader( s ) );
-            for ( String s1 = stringReader.readLine(); s1 != null; s1 = stringReader.readLine() )
-            {
-                consumeLine( s1 );
-            }
-        }
-    }
-
     public final Map<String, String> getTestVmSystemProperties()
     {
         return unmodifiableMap( testVmSystemProperties );
@@ -531,11 +442,4 @@ public class ForkClient
     {
         return !testsInProgress.isEmpty();
     }
-
-    private StackTraceWriter deserializeStackTraceWriter( String stackTraceMessage,
-                                                          String smartStackTrace, String stackTrace )
-    {
-        boolean hasTrace = stackTrace != null;
-        return hasTrace ? new DeserializedStacktraceWriter( stackTraceMessage, smartStackTrace, stackTrace ) : null;
-    }
 }
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedChannelDecoder.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedChannelDecoder.java
deleted file mode 100644
index 0c049d6..0000000
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedChannelDecoder.java
+++ /dev/null
@@ -1,352 +0,0 @@
-package org.apache.maven.plugin.surefire.booterclient.output;
-
-/*
- * 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.commons.codec.binary.Base64;
-import org.apache.maven.surefire.booter.ForkedProcessEvent;
-import org.apache.maven.surefire.report.ReportEntry;
-import org.apache.maven.surefire.report.RunMode;
-import org.apache.maven.surefire.report.StackTraceWriter;
-
-import java.nio.charset.Charset;
-import java.util.Collections;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-
-import static java.nio.charset.StandardCharsets.US_ASCII;
-import static org.apache.maven.surefire.booter.ForkedProcessEvent.MAGIC_NUMBER;
-import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_STDERR;
-import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_STDERR_NEW_LINE;
-import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_STDOUT;
-import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_STDOUT_NEW_LINE;
-import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_BYE;
-import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_CONSOLE_DEBUG;
-import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_CONSOLE_INFO;
-import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_CONSOLE_WARNING;
-import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_NEXT_TEST;
-import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_STOP_ON_NEXT_TEST;
-import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_TEST_ASSUMPTIONFAILURE;
-import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_TEST_ERROR;
-import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_TEST_FAILED;
-import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_TEST_SKIPPED;
-import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_TEST_STARTING;
-import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_TEST_SUCCEEDED;
-import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_TESTSET_COMPLETED;
-import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_TESTSET_STARTING;
-import static org.apache.maven.surefire.booter.ForkedProcessEvent.EVENTS;
-import static org.apache.maven.surefire.report.CategorizedReportEntry.reportEntry;
-import static org.apache.maven.surefire.report.RunMode.MODES;
-import static org.apache.maven.surefire.shared.utils.StringUtils.isBlank;
-import static org.apache.maven.surefire.shared.utils.StringUtils.isNotBlank;
-import static java.util.Objects.requireNonNull;
-
-/**
- * magic number : run mode : opcode [: opcode specific data]*
- *
- * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
- * @since 3.0.0-M4
- */
-public final class ForkedChannelDecoder
-{
-    private static final Base64 BASE64 = new Base64();
-
-    private volatile ForkedProcessPropertyEventListener propertyEventListener;
-    private volatile ForkedProcessStackTraceEventListener consoleErrorEventListener;
-    private volatile ForkedProcessExitErrorListener exitErrorEventListener;
-
-    private final ConcurrentMap<ForkedProcessEvent, ForkedProcessReportEventListener<?>> reportEventListeners =
-            new ConcurrentHashMap<>();
-
-    private final ConcurrentMap<ForkedProcessEvent, ForkedProcessStandardOutErrEventListener> stdOutErrEventListeners =
-            new ConcurrentHashMap<>();
-
-    private final ConcurrentMap<ForkedProcessEvent, ForkedProcessStringEventListener> consoleEventListeners =
-            new ConcurrentHashMap<>();
-
-    private final ConcurrentMap<ForkedProcessEvent, ForkedProcessEventListener> controlEventListeners =
-            new ConcurrentHashMap<>();
-
-    public void setSystemPropertiesListener( ForkedProcessPropertyEventListener listener )
-    {
-        propertyEventListener = requireNonNull( listener );
-    }
-
-    public <T extends ReportEntry> void setTestSetStartingListener( ForkedProcessReportEventListener<T> listener )
-    {
-        reportEventListeners.put( BOOTERCODE_TESTSET_STARTING, requireNonNull( listener ) );
-    }
-
-    public void setTestSetCompletedListener( ForkedProcessReportEventListener<?> listener )
-    {
-        reportEventListeners.put( BOOTERCODE_TESTSET_COMPLETED, requireNonNull( listener ) );
-    }
-
-    public void setTestStartingListener( ForkedProcessReportEventListener<?> listener )
-    {
-        reportEventListeners.put( BOOTERCODE_TEST_STARTING, requireNonNull( listener ) );
-    }
-
-    public void setTestSucceededListener( ForkedProcessReportEventListener<?> listener )
-    {
-        reportEventListeners.put( BOOTERCODE_TEST_SUCCEEDED, requireNonNull( listener ) );
-    }
-
-    public void setTestFailedListener( ForkedProcessReportEventListener<?> listener )
-    {
-        reportEventListeners.put( BOOTERCODE_TEST_FAILED, requireNonNull( listener ) );
-    }
-
-    public void setTestSkippedListener( ForkedProcessReportEventListener<?> listener )
-    {
-        reportEventListeners.put( BOOTERCODE_TEST_SKIPPED, requireNonNull( listener ) );
-    }
-
-    public void setTestErrorListener( ForkedProcessReportEventListener<?> listener )
-    {
-        reportEventListeners.put( BOOTERCODE_TEST_ERROR, requireNonNull( listener ) );
-    }
-
-    public void setTestAssumptionFailureListener( ForkedProcessReportEventListener<?> listener )
-    {
-        reportEventListeners.put( BOOTERCODE_TEST_ASSUMPTIONFAILURE, requireNonNull( listener ) );
-    }
-
-    public void setStdOutListener( ForkedProcessStandardOutErrEventListener listener )
-    {
-        stdOutErrEventListeners.put( BOOTERCODE_STDOUT, requireNonNull( listener ) );
-        stdOutErrEventListeners.put( BOOTERCODE_STDOUT_NEW_LINE, requireNonNull( listener ) );
-    }
-
-    public void setStdErrListener( ForkedProcessStandardOutErrEventListener listener )
-    {
-        stdOutErrEventListeners.put( BOOTERCODE_STDERR, requireNonNull( listener ) );
-        stdOutErrEventListeners.put( BOOTERCODE_STDERR_NEW_LINE, requireNonNull( listener ) );
-    }
-
-    public void setConsoleInfoListener( ForkedProcessStringEventListener listener )
-    {
-        consoleEventListeners.put( BOOTERCODE_CONSOLE_INFO, requireNonNull( listener ) );
-    }
-
-    public void setConsoleErrorListener( ForkedProcessStackTraceEventListener listener )
-    {
-        consoleErrorEventListener = requireNonNull( listener );
-    }
-
-    public void setConsoleDebugListener( ForkedProcessStringEventListener listener )
-    {
-        consoleEventListeners.put( BOOTERCODE_CONSOLE_DEBUG, requireNonNull( listener ) );
-    }
-
-    public void setConsoleWarningListener( ForkedProcessStringEventListener listener )
-    {
-        consoleEventListeners.put( BOOTERCODE_CONSOLE_WARNING, requireNonNull( listener ) );
-    }
-
-    public void setByeListener( ForkedProcessEventListener listener )
-    {
-        controlEventListeners.put( BOOTERCODE_BYE, requireNonNull( listener ) );
-    }
-
-    public void setStopOnNextTestListener( ForkedProcessEventListener listener )
-    {
-        controlEventListeners.put( BOOTERCODE_STOP_ON_NEXT_TEST, requireNonNull( listener ) );
-    }
-
-    public void setAcquireNextTestListener( ForkedProcessEventListener listener )
-    {
-        controlEventListeners.put( BOOTERCODE_NEXT_TEST, requireNonNull( listener ) );
-    }
-
-    public void setExitErrorEventListener( ForkedProcessExitErrorListener listener )
-    {
-        exitErrorEventListener = requireNonNull( listener );
-    }
-
-    public void handleEvent( String line, ForkedChannelDecoderErrorHandler errorHandler )
-    {
-        if ( line == null || !line.startsWith( MAGIC_NUMBER ) )
-        {
-            errorHandler.handledError( line, null );
-            return;
-        }
-
-        String[] tokens = line.substring( MAGIC_NUMBER.length() ).split( ":" );
-        int index = -1;
-        String opcode = tokens.length > ++index ? tokens[index] : null;
-        ForkedProcessEvent event = opcode == null ? null : EVENTS.get( opcode );
-        if ( event == null )
-        {
-            errorHandler.handledError( line, null );
-            return;
-        }
-
-        try
-        {
-            if ( event.isControlCategory() )
-            {
-                ForkedProcessEventListener listener = controlEventListeners.get( event );
-                if ( listener != null )
-                {
-                    listener.handle();
-                }
-            }
-            else if ( event.isConsoleCategory() )
-            {
-                ForkedProcessStringEventListener listener = consoleEventListeners.get( event );
-                Charset encoding = tokens.length > ++index ? Charset.forName( tokens[index] ) : null;
-                if ( listener != null && encoding != null )
-                {
-                    String msg = tokens.length > ++index ? decode( tokens[index], encoding ) : "";
-                    listener.handle( msg );
-                }
-            }
-            else if ( event.isConsoleErrorCategory() )
-            {
-                Charset encoding = tokens.length > ++index ? Charset.forName( tokens[index] ) : null;
-                if ( consoleErrorEventListener != null && encoding != null )
-                {
-                    String msg = tokens.length > ++index ? decode( tokens[index], encoding ) : null;
-                    String smartStackTrace =
-                            tokens.length > ++index ? decode( tokens[index], encoding ) : null;
-                    String stackTrace = tokens.length > ++index ? decode( tokens[index], encoding ) : null;
-                    consoleErrorEventListener.handle( msg, smartStackTrace, stackTrace );
-                }
-            }
-            else if ( event.isStandardStreamCategory() )
-            {
-                ForkedProcessStandardOutErrEventListener listener = stdOutErrEventListeners.get( event );
-                RunMode mode = tokens.length > ++index ? MODES.get( tokens[index] ) : null;
-                Charset encoding = tokens.length > ++index ? Charset.forName( tokens[index] ) : null;
-                if ( listener != null && encoding != null && mode != null )
-                {
-                    boolean newLine = event == BOOTERCODE_STDOUT_NEW_LINE || event == BOOTERCODE_STDERR_NEW_LINE;
-                    String output = tokens.length > ++index ? decode( tokens[index], encoding ) : "";
-                    listener.handle( mode, output, newLine );
-                }
-            }
-            else if ( event.isSysPropCategory() )
-            {
-                RunMode mode = tokens.length > ++index ? MODES.get( tokens[index] ) : null;
-                Charset encoding = tokens.length > ++index ? Charset.forName( tokens[index] ) : null;
-                String key = tokens.length > ++index ? decode( tokens[index], encoding ) : "";
-                if ( propertyEventListener != null && isNotBlank( key ) )
-                {
-                    String value = tokens.length > ++index ? decode( tokens[index], encoding ) : "";
-                    propertyEventListener.handle( mode, key, value );
-                }
-            }
-            else if ( event.isTestCategory() )
-            {
-                ForkedProcessReportEventListener listener = reportEventListeners.get( event );
-                RunMode mode = tokens.length > ++index ? MODES.get( tokens[index] ) : null;
-                Charset encoding = tokens.length > ++index ? Charset.forName( tokens[index] ) : null;
-                if ( listener != null && encoding != null && mode != null )
-                {
-                    String sourceName = tokens.length > ++index ? tokens[index] : null;
-                    String sourceText = tokens.length > ++index ? tokens[index] : null;
-                    String name = tokens.length > ++index ? tokens[index] : null;
-                    String nameText = tokens.length > ++index ? tokens[index] : null;
-                    String group = tokens.length > ++index ? tokens[index] : null;
-                    String message = tokens.length > ++index ? tokens[index] : null;
-                    String elapsed = tokens.length > ++index ? tokens[index] : null;
-                    String traceMessage = tokens.length > ++index ? tokens[index] : null;
-                    String smartTrimmedStackTrace = tokens.length > ++index ? tokens[index] : null;
-                    String stackTrace = tokens.length > ++index ? tokens[index] : null;
-                    ReportEntry reportEntry = toReportEntry( encoding, sourceName, sourceText, name, nameText,
-                            group, message, elapsed, traceMessage, smartTrimmedStackTrace, stackTrace );
-                    listener.handle( mode, reportEntry );
-                }
-            }
-            else if ( event.isJvmExitError() )
-            {
-                if ( exitErrorEventListener != null )
-                {
-                    Charset encoding = tokens.length > ++index ? Charset.forName( tokens[index] ) : null;
-                    String message = tokens.length > ++index ? decode( tokens[index], encoding ) : "";
-                    String smartTrimmedStackTrace =
-                            tokens.length > ++index ? decode( tokens[index], encoding ) : "";
-                    String stackTrace = tokens.length > ++index ? decode( tokens[index], encoding ) : "";
-                    exitErrorEventListener.handle( message, smartTrimmedStackTrace, stackTrace );
-                }
-            }
-        }
-        catch ( IllegalArgumentException e )
-        {
-            errorHandler.handledError( line, e );
-        }
-    }
-
-    static ReportEntry toReportEntry( Charset encoding,
-                   // ReportEntry:
-                   String encSource, String encSourceText, String encName, String encNameText,
-                                      String encGroup, String encMessage, String encTimeElapsed,
-                   // StackTraceWriter:
-                   String encTraceMessage, String encSmartTrimmedStackTrace, String encStackTrace )
-            throws NumberFormatException
-    {
-        if ( encoding == null )
-        {
-            // corrupted or incomplete stream
-            return null;
-        }
-
-        String source = decode( encSource, encoding );
-        String sourceText = decode( encSourceText, encoding );
-        String name = decode( encName, encoding );
-        String nameText = decode( encNameText, encoding );
-        String group = decode( encGroup, encoding );
-        StackTraceWriter stackTraceWriter =
-                decodeTrace( encoding, encTraceMessage, encSmartTrimmedStackTrace, encStackTrace );
-        Integer elapsed = decodeToInteger( encTimeElapsed );
-        String message = decode( encMessage, encoding );
-        return reportEntry( source, sourceText, name, nameText,
-                group, stackTraceWriter, elapsed, message, Collections.<String, String>emptyMap() );
-    }
-
-    static String decode( String line, Charset encoding )
-    {
-        // ForkedChannelEncoder is encoding the stream with US_ASCII
-        return line == null || "-".equals( line )
-                ? null
-                : new String( BASE64.decode( line.getBytes( US_ASCII ) ), encoding );
-    }
-
-    static Integer decodeToInteger( String line )
-    {
-        return line == null || "-".equals( line ) ? null : Integer.decode( line );
-    }
-
-    private static StackTraceWriter decodeTrace( Charset encoding, String encTraceMessage,
-                                                 String encSmartTrimmedStackTrace, String encStackTrace )
-    {
-        if ( isBlank( encStackTrace ) || "-".equals( encStackTrace ) )
-        {
-            return null;
-        }
-        else
-        {
-            String traceMessage = decode( encTraceMessage, encoding );
-            String stackTrace = decode( encStackTrace, encoding );
-            String smartTrimmedStackTrace = decode( encSmartTrimmedStackTrace, encoding );
-            return new DeserializedStacktraceWriter( traceMessage, smartTrimmedStackTrace, stackTrace );
-        }
-    }
-}
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessEventNotifier.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessEventNotifier.java
new file mode 100644
index 0000000..58f9344
--- /dev/null
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessEventNotifier.java
@@ -0,0 +1,248 @@
+package org.apache.maven.plugin.surefire.booterclient.output;
+
+/*
+ * 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.booter.ForkedProcessEventType;
+import org.apache.maven.surefire.eventapi.AbstractConsoleEvent;
+import org.apache.maven.surefire.eventapi.AbstractStandardStreamEvent;
+import org.apache.maven.surefire.eventapi.AbstractTestControlEvent;
+import org.apache.maven.surefire.eventapi.ConsoleErrorEvent;
+import org.apache.maven.surefire.eventapi.Event;
+import org.apache.maven.surefire.eventapi.JvmExitErrorEvent;
+import org.apache.maven.surefire.eventapi.SystemPropertyEvent;
+import org.apache.maven.surefire.report.ReportEntry;
+import org.apache.maven.surefire.report.RunMode;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import static java.util.Objects.requireNonNull;
+import static org.apache.maven.surefire.booter.ForkedProcessEventType.BOOTERCODE_BYE;
+import static org.apache.maven.surefire.booter.ForkedProcessEventType.BOOTERCODE_CONSOLE_DEBUG;
+import static org.apache.maven.surefire.booter.ForkedProcessEventType.BOOTERCODE_CONSOLE_INFO;
+import static org.apache.maven.surefire.booter.ForkedProcessEventType.BOOTERCODE_CONSOLE_WARNING;
+import static org.apache.maven.surefire.booter.ForkedProcessEventType.BOOTERCODE_NEXT_TEST;
+import static org.apache.maven.surefire.booter.ForkedProcessEventType.BOOTERCODE_STDERR;
+import static org.apache.maven.surefire.booter.ForkedProcessEventType.BOOTERCODE_STDERR_NEW_LINE;
+import static org.apache.maven.surefire.booter.ForkedProcessEventType.BOOTERCODE_STDOUT;
+import static org.apache.maven.surefire.booter.ForkedProcessEventType.BOOTERCODE_STDOUT_NEW_LINE;
+import static org.apache.maven.surefire.booter.ForkedProcessEventType.BOOTERCODE_STOP_ON_NEXT_TEST;
+import static org.apache.maven.surefire.booter.ForkedProcessEventType.BOOTERCODE_TESTSET_COMPLETED;
+import static org.apache.maven.surefire.booter.ForkedProcessEventType.BOOTERCODE_TESTSET_STARTING;
+import static org.apache.maven.surefire.booter.ForkedProcessEventType.BOOTERCODE_TEST_ASSUMPTIONFAILURE;
+import static org.apache.maven.surefire.booter.ForkedProcessEventType.BOOTERCODE_TEST_ERROR;
+import static org.apache.maven.surefire.booter.ForkedProcessEventType.BOOTERCODE_TEST_FAILED;
+import static org.apache.maven.surefire.booter.ForkedProcessEventType.BOOTERCODE_TEST_SKIPPED;
+import static org.apache.maven.surefire.booter.ForkedProcessEventType.BOOTERCODE_TEST_STARTING;
+import static org.apache.maven.surefire.booter.ForkedProcessEventType.BOOTERCODE_TEST_SUCCEEDED;
+
+/**
+ * magic number : run mode : opcode [: opcode specific data]*
+ *
+ * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
+ * @since 3.0.0-M4
+ */
+public final class ForkedProcessEventNotifier
+{
+    private volatile ForkedProcessPropertyEventListener propertyEventListener;
+    private volatile ForkedProcessStackTraceEventListener consoleErrorEventListener;
+    private volatile ForkedProcessExitErrorListener exitErrorEventListener;
+
+    private final ConcurrentMap<ForkedProcessEventType, ForkedProcessReportEventListener<?>> reportEventListeners =
+            new ConcurrentHashMap<>();
+
+    private final ConcurrentMap<ForkedProcessEventType, ForkedProcessStandardOutErrEventListener>
+        stdOutErrEventListeners = new ConcurrentHashMap<>();
+
+    private final ConcurrentMap<ForkedProcessEventType, ForkedProcessStringEventListener> consoleEventListeners =
+            new ConcurrentHashMap<>();
+
+    private final ConcurrentMap<ForkedProcessEventType, ForkedProcessEventListener> controlEventListeners =
+            new ConcurrentHashMap<>();
+
+    public void setSystemPropertiesListener( ForkedProcessPropertyEventListener listener )
+    {
+        propertyEventListener = requireNonNull( listener );
+    }
+
+    public <T extends ReportEntry> void setTestSetStartingListener( ForkedProcessReportEventListener<T> listener )
+    {
+        reportEventListeners.put( BOOTERCODE_TESTSET_STARTING, requireNonNull( listener ) );
+    }
+
+    public void setTestSetCompletedListener( ForkedProcessReportEventListener<?> listener )
+    {
+        reportEventListeners.put( BOOTERCODE_TESTSET_COMPLETED, requireNonNull( listener ) );
+    }
+
+    public void setTestStartingListener( ForkedProcessReportEventListener<?> listener )
+    {
+        reportEventListeners.put( BOOTERCODE_TEST_STARTING, requireNonNull( listener ) );
+    }
+
+    public void setTestSucceededListener( ForkedProcessReportEventListener<?> listener )
+    {
+        reportEventListeners.put( BOOTERCODE_TEST_SUCCEEDED, requireNonNull( listener ) );
+    }
+
+    public void setTestFailedListener( ForkedProcessReportEventListener<?> listener )
+    {
+        reportEventListeners.put( BOOTERCODE_TEST_FAILED, requireNonNull( listener ) );
+    }
+
+    public void setTestSkippedListener( ForkedProcessReportEventListener<?> listener )
+    {
+        reportEventListeners.put( BOOTERCODE_TEST_SKIPPED, requireNonNull( listener ) );
+    }
+
+    public void setTestErrorListener( ForkedProcessReportEventListener<?> listener )
+    {
+        reportEventListeners.put( BOOTERCODE_TEST_ERROR, requireNonNull( listener ) );
+    }
+
+    public void setTestAssumptionFailureListener( ForkedProcessReportEventListener<?> listener )
+    {
+        reportEventListeners.put( BOOTERCODE_TEST_ASSUMPTIONFAILURE, requireNonNull( listener ) );
+    }
+
+    public void setStdOutListener( ForkedProcessStandardOutErrEventListener listener )
+    {
+        stdOutErrEventListeners.put( BOOTERCODE_STDOUT, requireNonNull( listener ) );
+        stdOutErrEventListeners.put( BOOTERCODE_STDOUT_NEW_LINE, requireNonNull( listener ) );
+    }
+
+    public void setStdErrListener( ForkedProcessStandardOutErrEventListener listener )
+    {
+        stdOutErrEventListeners.put( BOOTERCODE_STDERR, requireNonNull( listener ) );
+        stdOutErrEventListeners.put( BOOTERCODE_STDERR_NEW_LINE, requireNonNull( listener ) );
+    }
+
+    public void setConsoleInfoListener( ForkedProcessStringEventListener listener )
+    {
+        consoleEventListeners.put( BOOTERCODE_CONSOLE_INFO, requireNonNull( listener ) );
+    }
+
+    public void setConsoleErrorListener( ForkedProcessStackTraceEventListener listener )
+    {
+        consoleErrorEventListener = requireNonNull( listener );
+    }
+
+    public void setConsoleDebugListener( ForkedProcessStringEventListener listener )
+    {
+        consoleEventListeners.put( BOOTERCODE_CONSOLE_DEBUG, requireNonNull( listener ) );
+    }
+
+    public void setConsoleWarningListener( ForkedProcessStringEventListener listener )
+    {
+        consoleEventListeners.put( BOOTERCODE_CONSOLE_WARNING, requireNonNull( listener ) );
+    }
+
+    public void setByeListener( ForkedProcessEventListener listener )
+    {
+        controlEventListeners.put( BOOTERCODE_BYE, requireNonNull( listener ) );
+    }
+
+    public void setStopOnNextTestListener( ForkedProcessEventListener listener )
+    {
+        controlEventListeners.put( BOOTERCODE_STOP_ON_NEXT_TEST, requireNonNull( listener ) );
+    }
+
+    public void setAcquireNextTestListener( ForkedProcessEventListener listener )
+    {
+        controlEventListeners.put( BOOTERCODE_NEXT_TEST, requireNonNull( listener ) );
+    }
+
+    public void setExitErrorEventListener( ForkedProcessExitErrorListener listener )
+    {
+        exitErrorEventListener = requireNonNull( listener );
+    }
+
+    public void notifyEvent( Event event )
+    {
+        ForkedProcessEventType eventType = event.getEventType();
+        if ( event.isControlCategory() )
+        {
+            ForkedProcessEventListener listener = controlEventListeners.get( eventType );
+            if ( listener != null )
+            {
+                listener.handle();
+            }
+        }
+        else if ( event.isConsoleErrorCategory() )
+        {
+            if ( consoleErrorEventListener != null )
+            {
+                consoleErrorEventListener.handle( ( ( ConsoleErrorEvent ) event ).getStackTraceWriter() );
+            }
+        }
+        else if ( event.isConsoleCategory() )
+        {
+            ForkedProcessStringEventListener listener = consoleEventListeners.get( eventType );
+            if ( listener != null )
+            {
+                listener.handle( ( (AbstractConsoleEvent) event ).getMessage() );
+            }
+        }
+        else if ( event.isStandardStreamCategory() )
+        {
+            boolean newLine = eventType == BOOTERCODE_STDOUT_NEW_LINE || eventType == BOOTERCODE_STDERR_NEW_LINE;
+            AbstractStandardStreamEvent standardStreamEvent = (AbstractStandardStreamEvent) event;
+            ForkedProcessStandardOutErrEventListener listener = stdOutErrEventListeners.get( eventType );
+            if ( listener != null )
+            {
+                listener.handle( standardStreamEvent.getRunMode(), standardStreamEvent.getMessage(), newLine );
+            }
+        }
+        else if ( event.isSysPropCategory() )
+        {
+            SystemPropertyEvent systemPropertyEvent = (SystemPropertyEvent) event;
+            RunMode runMode = systemPropertyEvent.getRunMode();
+            String key = systemPropertyEvent.getKey();
+            String value = systemPropertyEvent.getValue();
+            if ( propertyEventListener != null )
+            {
+                propertyEventListener.handle( runMode, key, value );
+            }
+        }
+        else if ( event.isTestCategory() )
+        {
+            ForkedProcessReportEventListener listener = reportEventListeners.get( eventType );
+            AbstractTestControlEvent testControlEvent = (AbstractTestControlEvent) event;
+            RunMode mode = testControlEvent.getRunMode();
+            ReportEntry reportEntry = testControlEvent.getReportEntry();
+            if ( listener != null )
+            {
+                listener.handle( mode, reportEntry );
+            }
+        }
+        else if ( event.isJvmExitError() )
+        {
+            JvmExitErrorEvent jvmExitErrorEvent = (JvmExitErrorEvent) event;
+            if ( exitErrorEventListener != null )
+            {
+                exitErrorEventListener.handle( jvmExitErrorEvent.getStackTraceWriter() );
+            }
+        }
+        else
+        {
+            throw new IllegalArgumentException( "Unknown event type " + eventType );
+        }
+    }
+}
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessExitErrorListener.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessExitErrorListener.java
index b14c38c..a8b4b0b 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessExitErrorListener.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessExitErrorListener.java
@@ -19,11 +19,13 @@ package org.apache.maven.plugin.surefire.booterclient.output;
  * under the License.
  */
 
+import org.apache.maven.surefire.report.StackTraceWriter;
+
 /**
  * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
  * @since 3.0.0-M4
  */
 public interface ForkedProcessExitErrorListener
 {
-    void handle( String exceptionMessage, String smartTrimmedStackTrace, String stackTrace );
+    void handle( StackTraceWriter stackTrace );
 }
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessStackTraceEventListener.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessStackTraceEventListener.java
index f54cc40..81bf53f 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessStackTraceEventListener.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessStackTraceEventListener.java
@@ -19,11 +19,15 @@ package org.apache.maven.plugin.surefire.booterclient.output;
  * under the License.
  */
 
+import org.apache.maven.surefire.report.StackTraceWriter;
+
+import javax.annotation.Nonnull;
+
 /**
  * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
  * @since 3.0.0-M4
  */
 public interface ForkedProcessStackTraceEventListener
 {
-    void handle( String msg, String smartStackTrace, String stackTrace );
+    void handle( @Nonnull StackTraceWriter stackTrace );
 }
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/NativeStdErrStreamConsumer.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/NativeStdErrStreamConsumer.java
index b17bfe4..bc0bc7c 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/NativeStdErrStreamConsumer.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/NativeStdErrStreamConsumer.java
@@ -20,7 +20,9 @@ package org.apache.maven.plugin.surefire.booterclient.output;
  */
 
 import org.apache.maven.plugin.surefire.report.DefaultReporterFactory;
-import org.apache.maven.surefire.shared.utils.cli.StreamConsumer;
+import org.apache.maven.surefire.extensions.EventHandler;
+
+import javax.annotation.Nonnull;
 
 /**
  * Used by forked JMV, see {@link org.apache.maven.plugin.surefire.booterclient.ForkStarter}.
@@ -30,7 +32,7 @@ import org.apache.maven.surefire.shared.utils.cli.StreamConsumer;
  * @see org.apache.maven.plugin.surefire.booterclient.ForkStarter
  */
 public final class NativeStdErrStreamConsumer
-    implements StreamConsumer
+    implements EventHandler<String>
 {
     private final DefaultReporterFactory defaultReporterFactory;
 
@@ -40,7 +42,7 @@ public final class NativeStdErrStreamConsumer
     }
 
     @Override
-    public void consumeLine( String line )
+    public void handleEvent( @Nonnull String line )
     {
         InPluginProcessDumpSingleton.getSingleton()
                 .dumpStreamText( line, defaultReporterFactory.getReportsDirectory() );
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessStackTraceEventListener.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/NativeStdOutStreamConsumer.java
similarity index 62%
copy from maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessStackTraceEventListener.java
copy to maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/NativeStdOutStreamConsumer.java
index f54cc40..1f915ae 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessStackTraceEventListener.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/NativeStdOutStreamConsumer.java
@@ -9,7 +9,7 @@ package org.apache.maven.plugin.surefire.booterclient.output;
  * "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
+ *   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
@@ -19,11 +19,25 @@ package org.apache.maven.plugin.surefire.booterclient.output;
  * under the License.
  */
 
+import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
+import org.apache.maven.surefire.extensions.StdOutStreamLine;
+
 /**
- * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
- * @since 3.0.0-M4
+ *
  */
-public interface ForkedProcessStackTraceEventListener
+public class NativeStdOutStreamConsumer
+        implements StdOutStreamLine
 {
-    void handle( String msg, String smartStackTrace, String stackTrace );
+    private final ConsoleLogger logger;
+
+    public NativeStdOutStreamConsumer( ConsoleLogger logger )
+    {
+        this.logger = logger;
+    }
+
+    @Override
+    public void handleLine( String line )
+    {
+        logger.info( line );
+    }
 }
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ThreadedStreamConsumer.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ThreadedStreamConsumer.java
index 853d35c..9c72a04 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ThreadedStreamConsumer.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ThreadedStreamConsumer.java
@@ -19,9 +19,12 @@ package org.apache.maven.plugin.surefire.booterclient.output;
  * under the License.
  */
 
+import org.apache.maven.surefire.eventapi.Event;
 import org.apache.maven.surefire.shared.utils.cli.StreamConsumer;
+import org.apache.maven.surefire.extensions.EventHandler;
 import org.apache.maven.surefire.util.internal.DaemonThreadFactory;
 
+import javax.annotation.Nonnull;
 import java.io.Closeable;
 import java.io.IOException;
 import java.util.concurrent.ArrayBlockingQueue;
@@ -36,13 +39,13 @@ import static java.lang.Thread.currentThread;
  * @author Kristian Rosenvold
  */
 public final class ThreadedStreamConsumer
-        implements StreamConsumer, Closeable
+        implements EventHandler<Event>, Closeable
 {
-    private static final String END_ITEM = "";
+    private static final Event END_ITEM = new FinalEvent();
 
     private static final int ITEM_LIMIT_BEFORE_SLEEP = 10_000;
 
-    private final BlockingQueue<String> items = new ArrayBlockingQueue<>( ITEM_LIMIT_BEFORE_SLEEP );
+    private final BlockingQueue<Event> items = new ArrayBlockingQueue<>( ITEM_LIMIT_BEFORE_SLEEP );
 
     private final AtomicBoolean stop = new AtomicBoolean();
 
@@ -53,17 +56,17 @@ public final class ThreadedStreamConsumer
     final class Pumper
             implements Runnable
     {
-        private final StreamConsumer target;
+        private final EventHandler<Event> target;
 
         private final MultipleFailureException errors = new MultipleFailureException();
 
-        Pumper( StreamConsumer target )
+        Pumper( EventHandler<Event> target )
         {
             this.target = target;
         }
 
         /**
-         * Calls {@link ForkClient#consumeLine(String)} which may throw any {@link RuntimeException}.<br>
+         * Calls {@link ForkClient#handleEvent(Event)} which may throw any {@link RuntimeException}.<br>
          * Even if {@link ForkClient} is not fault-tolerant, this method MUST be fault-tolerant and thus the
          * try-catch block must be inside of the loop which prevents from loosing events from {@link StreamConsumer}.
          * <br>
@@ -80,12 +83,12 @@ public final class ThreadedStreamConsumer
             {
                 try
                 {
-                    String item = ThreadedStreamConsumer.this.items.take();
+                    Event item = ThreadedStreamConsumer.this.items.take();
                     if ( shouldStopQueueing( item ) )
                     {
                         return;
                     }
-                    target.consumeLine( item );
+                    target.handleEvent( item );
                 }
                 catch ( Throwable t )
                 {
@@ -105,7 +108,7 @@ public final class ThreadedStreamConsumer
         }
     }
 
-    public ThreadedStreamConsumer( StreamConsumer target )
+    public ThreadedStreamConsumer( EventHandler<Event> target )
     {
         pumper = new Pumper( target );
         thread = DaemonThreadFactory.newDaemonThread( pumper, "ThreadedStreamConsumer" );
@@ -113,7 +116,7 @@ public final class ThreadedStreamConsumer
     }
 
     @Override
-    public void consumeLine( String s )
+    public void handleEvent( @Nonnull Event event )
     {
         if ( stop.get() )
         {
@@ -127,7 +130,7 @@ public final class ThreadedStreamConsumer
 
         try
         {
-            items.put( s );
+            items.put( event );
         }
         catch ( InterruptedException e )
         {
@@ -164,8 +167,61 @@ public final class ThreadedStreamConsumer
      * @param item    element from <code>items</code>
      * @return {@code true} if tail of the queue
      */
-    private boolean shouldStopQueueing( String item )
+    private boolean shouldStopQueueing( Event item )
     {
         return item == END_ITEM;
     }
+
+    /**
+     *
+     */
+    private static class FinalEvent extends Event
+    {
+        FinalEvent()
+        {
+            super( null );
+        }
+
+        @Override
+        public boolean isControlCategory()
+        {
+            return false;
+        }
+
+        @Override
+        public boolean isConsoleCategory()
+        {
+            return false;
+        }
+
+        @Override
+        public boolean isConsoleErrorCategory()
+        {
+            return false;
+        }
+
+        @Override
+        public boolean isStandardStreamCategory()
+        {
+            return false;
+        }
+
+        @Override
+        public boolean isSysPropCategory()
+        {
+            return false;
+        }
+
+        @Override
+        public boolean isTestCategory()
+        {
+            return false;
+        }
+
+        @Override
+        public boolean isJvmExitError()
+        {
+            return false;
+        }
+    }
 }
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
new file mode 100644
index 0000000..cc33c6c
--- /dev/null
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/EventConsumerThread.java
@@ -0,0 +1,441 @@
+package org.apache.maven.plugin.surefire.extensions;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.maven.plugin.surefire.booterclient.output.DeserializedStacktraceWriter;
+import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
+import org.apache.maven.surefire.booter.ForkedProcessEventType;
+import org.apache.maven.surefire.eventapi.ConsoleDebugEvent;
+import org.apache.maven.surefire.eventapi.ConsoleErrorEvent;
+import org.apache.maven.surefire.eventapi.ConsoleInfoEvent;
+import org.apache.maven.surefire.eventapi.ConsoleWarningEvent;
+import org.apache.maven.surefire.eventapi.ControlByeEvent;
+import org.apache.maven.surefire.eventapi.ControlNextTestEvent;
+import org.apache.maven.surefire.eventapi.ControlStopOnNextTestEvent;
+import org.apache.maven.surefire.eventapi.Event;
+import org.apache.maven.surefire.eventapi.JvmExitErrorEvent;
+import org.apache.maven.surefire.eventapi.StandardStreamErrEvent;
+import org.apache.maven.surefire.eventapi.StandardStreamErrWithNewLineEvent;
+import org.apache.maven.surefire.eventapi.StandardStreamOutEvent;
+import org.apache.maven.surefire.eventapi.StandardStreamOutWithNewLineEvent;
+import org.apache.maven.surefire.eventapi.SystemPropertyEvent;
+import org.apache.maven.surefire.eventapi.TestAssumptionFailureEvent;
+import org.apache.maven.surefire.eventapi.TestErrorEvent;
+import org.apache.maven.surefire.eventapi.TestFailedEvent;
+import org.apache.maven.surefire.eventapi.TestSkippedEvent;
+import org.apache.maven.surefire.eventapi.TestStartingEvent;
+import org.apache.maven.surefire.eventapi.TestSucceededEvent;
+import org.apache.maven.surefire.eventapi.TestsetCompletedEvent;
+import org.apache.maven.surefire.eventapi.TestsetStartingEvent;
+import org.apache.maven.surefire.extensions.CloseableDaemonThread;
+import org.apache.maven.surefire.extensions.EventHandler;
+import org.apache.maven.surefire.extensions.util.CountdownCloseable;
+import org.apache.maven.surefire.report.RunMode;
+import org.apache.maven.surefire.report.StackTraceWriter;
+import org.apache.maven.surefire.report.TestSetReportEntry;
+
+import javax.annotation.Nonnull;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static org.apache.maven.surefire.booter.ForkedProcessEventType.MAGIC_NUMBER;
+import static org.apache.maven.surefire.report.CategorizedReportEntry.reportEntry;
+import static org.apache.maven.surefire.report.RunMode.MODES;
+
+/**
+ *
+ */
+public class EventConsumerThread extends CloseableDaemonThread
+{
+    private static final String PRINTABLE_JVM_NATIVE_STREAM = "Listening for transport dt_socket at address:";
+    private static final Base64 BASE64 = new Base64();
+
+    private final ReadableByteChannel channel;
+    private final EventHandler<Event> eventHandler;
+    private final CountdownCloseable countdownCloseable;
+    private final ConsoleLogger logger;
+    private volatile boolean disabled;
+
+    public EventConsumerThread( @Nonnull String threadName,
+                                @Nonnull ReadableByteChannel channel,
+                                @Nonnull EventHandler<Event> eventHandler,
+                                @Nonnull CountdownCloseable countdownCloseable,
+                                @Nonnull ConsoleLogger logger )
+    {
+        super( threadName );
+        this.channel = channel;
+        this.eventHandler = eventHandler;
+        this.countdownCloseable = countdownCloseable;
+        this.logger = logger;
+    }
+
+    @Override
+    public void run()
+    {
+        try ( ReadableByteChannel stream = channel;
+              CountdownCloseable c = countdownCloseable; )
+        {
+            decode();
+        }
+        catch ( IOException e )
+        {
+            // not needed
+        }
+    }
+
+    @Override
+    public void disable()
+    {
+        disabled = true;
+    }
+
+    @Override
+    public void close() throws IOException
+    {
+        channel.close();
+    }
+
+    @SuppressWarnings( "checkstyle:innerassignment" )
+    private void decode() throws IOException
+    {
+        List<String> tokens = new ArrayList<>();
+        StringBuilder line = new StringBuilder();
+        StringBuilder token = new StringBuilder( MAGIC_NUMBER.length() );
+        ByteBuffer buffer = ByteBuffer.allocate( 1 );
+        boolean endOfStream;
+
+        start:
+        do
+        {
+            line.setLength( 0 );
+            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 ( c == '\n' || c == '\r' )
+                {
+                    printExistingLine( line );
+                    continue start;
+                }
+
+                line.append( c );
+
+                if ( !frameStarted )
+                {
+                    if ( c == ':' )
+                    {
+                        frameStarted = true;
+                        token.setLength( 0 );
+                        tokens.clear();
+                    }
+                }
+                else
+                {
+                    if ( c == ':' )
+                    {
+                        tokens.add( token.toString() );
+                        token.setLength( 0 );
+                        completion = frameCompleteness( tokens );
+                        if ( completion == FrameCompletion.COMPLETE )
+                        {
+                            line.setLength( 0 );
+                            break;
+                        }
+                        else if ( completion == FrameCompletion.MALFORMED )
+                        {
+                            printExistingLine( line );
+                            continue start;
+                        }
+                    }
+                    else
+                    {
+                        token.append( c );
+                    }
+                }
+            }
+
+            if ( completion == FrameCompletion.COMPLETE )
+            {
+                Event event = toEvent( tokens );
+                if ( !disabled && event != null )
+                {
+                    eventHandler.handleEvent( event );
+                }
+            }
+
+            if ( endOfStream )
+            {
+                printExistingLine( line );
+                return;
+            }
+        }
+        while ( true );
+    }
+
+    private void printExistingLine( StringBuilder line )
+    {
+        if ( line.length() != 0 )
+        {
+            String s = line.toString();
+            if ( s.contains( PRINTABLE_JVM_NATIVE_STREAM ) )
+            {
+                logger.info( s );
+            }
+            else
+            {
+                logger.error( s );
+            }
+        }
+    }
+
+    private Event toEvent( List<String> tokensInFrame )
+    {
+        Iterator<String> tokens = tokensInFrame.iterator();
+        String header = tokens.next();
+        assert header != null;
+
+        ForkedProcessEventType event = ForkedProcessEventType.byOpcode( tokens.next() );
+
+        if ( event == null )
+        {
+            return null;
+        }
+
+        if ( event.isControlCategory() )
+        {
+            switch ( event )
+            {
+                case BOOTERCODE_BYE:
+                    return new ControlByeEvent();
+                case BOOTERCODE_STOP_ON_NEXT_TEST:
+                    return new ControlStopOnNextTestEvent();
+                case BOOTERCODE_NEXT_TEST:
+                    return new ControlNextTestEvent();
+                default:
+                    throw new IllegalStateException( "Unknown enum " + event );
+            }
+        }
+        else if ( event.isConsoleErrorCategory() || event.isJvmExitError() )
+        {
+            Charset encoding = Charset.forName( tokens.next() );
+            StackTraceWriter stackTraceWriter = decodeTrace( encoding, tokens.next(), tokens.next(), tokens.next() );
+            return event.isConsoleErrorCategory()
+                ? new ConsoleErrorEvent( stackTraceWriter )
+                : new JvmExitErrorEvent( stackTraceWriter );
+        }
+        else if ( event.isConsoleCategory() )
+        {
+            Charset encoding = Charset.forName( tokens.next() );
+            String msg = decode( tokens.next(), encoding );
+            switch ( event )
+            {
+                case BOOTERCODE_CONSOLE_INFO:
+                    return new ConsoleInfoEvent( msg );
+                case BOOTERCODE_CONSOLE_DEBUG:
+                    return new ConsoleDebugEvent( msg );
+                case BOOTERCODE_CONSOLE_WARNING:
+                    return new ConsoleWarningEvent( msg );
+                default:
+                    throw new IllegalStateException( "Unknown enum " + event );
+            }
+        }
+        else if ( event.isStandardStreamCategory() )
+        {
+            RunMode mode = MODES.get( tokens.next() );
+            Charset encoding = Charset.forName( tokens.next() );
+            String output = decode( tokens.next(), encoding );
+            switch ( event )
+            {
+                case BOOTERCODE_STDOUT:
+                    return new StandardStreamOutEvent( mode, output );
+                case BOOTERCODE_STDOUT_NEW_LINE:
+                    return new StandardStreamOutWithNewLineEvent( mode, output );
+                case BOOTERCODE_STDERR:
+                    return new StandardStreamErrEvent( mode, output );
+                case BOOTERCODE_STDERR_NEW_LINE:
+                    return new StandardStreamErrWithNewLineEvent( mode, output );
+                default:
+                    throw new IllegalStateException( "Unknown enum " + event );
+            }
+        }
+        else if ( event.isSysPropCategory() )
+        {
+            RunMode mode = MODES.get( tokens.next() );
+            Charset encoding = Charset.forName( tokens.next() );
+            String key = decode( tokens.next(), encoding );
+            String value = decode( tokens.next(), encoding );
+            return new SystemPropertyEvent( mode, key, value );
+        }
+        else if ( event.isTestCategory() )
+        {
+            RunMode mode = MODES.get( tokens.next() );
+            Charset encoding = Charset.forName( tokens.next() );
+            TestSetReportEntry reportEntry =
+                decodeReportEntry( encoding, tokens.next(), tokens.next(), tokens.next(), tokens.next(),
+                    tokens.next(), tokens.next(), tokens.next(), tokens.next(), tokens.next(), tokens.next() );
+
+            switch ( event )
+            {
+                case BOOTERCODE_TESTSET_STARTING:
+                    return new TestsetStartingEvent( mode, reportEntry );
+                case BOOTERCODE_TESTSET_COMPLETED:
+                    return new TestsetCompletedEvent( mode, reportEntry );
+                case BOOTERCODE_TEST_STARTING:
+                    return new TestStartingEvent( mode, reportEntry );
+                case BOOTERCODE_TEST_SUCCEEDED:
+                    return new TestSucceededEvent( mode, reportEntry );
+                case BOOTERCODE_TEST_FAILED:
+                    return new TestFailedEvent( mode, reportEntry );
+                case BOOTERCODE_TEST_SKIPPED:
+                    return new TestSkippedEvent( mode, reportEntry );
+                case BOOTERCODE_TEST_ERROR:
+                    return new TestErrorEvent( mode, reportEntry );
+                case BOOTERCODE_TEST_ASSUMPTIONFAILURE:
+                    return new TestAssumptionFailureEvent( mode, reportEntry );
+                default:
+                    throw new IllegalStateException( "Unknown enum " + event );
+            }
+        }
+
+        throw new IllegalStateException( "Missing a branch for the event type " + event );
+    }
+
+    private static FrameCompletion frameCompleteness( List<String> tokens )
+    {
+        if ( !tokens.isEmpty() && !MAGIC_NUMBER.equals( tokens.get( 0 ) ) )
+        {
+            return FrameCompletion.MALFORMED;
+        }
+
+        if ( tokens.size() >= 2 )
+        {
+            String opcode = tokens.get( 1 );
+            ForkedProcessEventType event = ForkedProcessEventType.byOpcode( opcode );
+            if ( event == null )
+            {
+                return FrameCompletion.MALFORMED;
+            }
+            else if ( event.isControlCategory() )
+            {
+                return FrameCompletion.COMPLETE;
+            }
+            else if ( event.isConsoleErrorCategory() )
+            {
+                return tokens.size() == 6 ? FrameCompletion.COMPLETE : FrameCompletion.NOT_COMPLETE;
+            }
+            else if ( event.isConsoleCategory() )
+            {
+                return tokens.size() == 4 ? FrameCompletion.COMPLETE : FrameCompletion.NOT_COMPLETE;
+            }
+            else if ( event.isStandardStreamCategory() )
+            {
+                return tokens.size() == 5 ? FrameCompletion.COMPLETE : FrameCompletion.NOT_COMPLETE;
+            }
+            else if ( event.isSysPropCategory() )
+            {
+                return tokens.size() == 6 ? FrameCompletion.COMPLETE : FrameCompletion.NOT_COMPLETE;
+            }
+            else if ( event.isTestCategory() )
+            {
+                return tokens.size() == 14 ? FrameCompletion.COMPLETE : FrameCompletion.NOT_COMPLETE;
+            }
+            else if ( event.isJvmExitError() )
+            {
+                return tokens.size() == 6 ? FrameCompletion.COMPLETE : FrameCompletion.NOT_COMPLETE;
+            }
+        }
+        return FrameCompletion.NOT_COMPLETE;
+    }
+
+    static String decode( String line, Charset encoding )
+    {
+        // ForkedChannelEncoder is encoding the stream with US_ASCII
+        return line == null || "-".equals( line )
+            ? null
+            : new String( BASE64.decode( line.getBytes( US_ASCII ) ), encoding );
+    }
+
+    private static StackTraceWriter decodeTrace( Charset encoding, String encTraceMessage,
+                                                 String encSmartTrimmedStackTrace, String encStackTrace )
+    {
+        String traceMessage = decode( encTraceMessage, encoding );
+        String stackTrace = decode( encStackTrace, encoding );
+        String smartTrimmedStackTrace = decode( encSmartTrimmedStackTrace, encoding );
+        boolean exists = traceMessage != null || stackTrace != null || smartTrimmedStackTrace != null;
+        return exists ? new DeserializedStacktraceWriter( traceMessage, smartTrimmedStackTrace, stackTrace ) : null;
+    }
+
+    static TestSetReportEntry decodeReportEntry( Charset encoding,
+                                                 // ReportEntry:
+                                                 String encSource, String encSourceText, String encName,
+                                                 String encNameText, String encGroup, String encMessage,
+                                                 String encTimeElapsed,
+                                                 // StackTraceWriter:
+                                                 String encTraceMessage,
+                                                 String encSmartTrimmedStackTrace, String encStackTrace )
+        throws NumberFormatException
+    {
+        if ( encoding == null )
+        {
+            // corrupted or incomplete stream
+            return null;
+        }
+
+        String source = decode( encSource, encoding );
+        String sourceText = decode( encSourceText, encoding );
+        String name = decode( encName, encoding );
+        String nameText = decode( encNameText, encoding );
+        String group = decode( encGroup, encoding );
+        StackTraceWriter stackTraceWriter =
+            decodeTrace( encoding, encTraceMessage, encSmartTrimmedStackTrace, encStackTrace );
+        Integer elapsed = decodeToInteger( encTimeElapsed );
+        String message = decode( encMessage, encoding );
+        return reportEntry( source, sourceText, name, nameText,
+            group, stackTraceWriter, elapsed, message, Collections.<String, String>emptyMap() );
+    }
+
+    static Integer decodeToInteger( String line )
+    {
+        return line == null || "-".equals( line ) ? null : Integer.decode( line );
+    }
+
+    /**
+     * Determines whether the frame is complete or malformed.
+     */
+    private enum FrameCompletion
+    {
+        NOT_COMPLETE,
+        COMPLETE,
+        MALFORMED
+    }
+}
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
new file mode 100644
index 0000000..a554edf
--- /dev/null
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/LegacyForkChannel.java
@@ -0,0 +1,89 @@
+package org.apache.maven.plugin.surefire.extensions;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
+import org.apache.maven.surefire.eventapi.Event;
+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.util.CountdownCloseable;
+
+import javax.annotation.Nonnull;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.WritableByteChannel;
+
+/**
+ * The main purpose of this class is to bind the
+ * {@link #bindCommandReader(CommandReader, WritableByteChannel) command reader} reading the commands from
+ * {@link CommandReader}, serializing them and writing the stream to the
+ * {@link WritableByteChannel sub-process}. It binds the
+ * {@link #bindEventHandler(EventHandler, CountdownCloseable, ReadableByteChannel) event handler} deserializing
+ * a received event and sends the event object to the {@link EventHandler event handler}.
+ */
+final class LegacyForkChannel extends ForkChannel
+{
+    private final ConsoleLogger logger;
+
+    protected LegacyForkChannel( int forkChannelId, ConsoleLogger logger )
+    {
+        super( forkChannelId );
+        this.logger = logger;
+    }
+
+    @Override
+    public void connectToClient()
+    {
+    }
+
+    @Override
+    public String getForkNodeConnectionString()
+    {
+        return "pipe://" + getForkChannelId();
+    }
+
+    @Override
+    public boolean useStdOut()
+    {
+        return true;
+    }
+
+    @Override
+    public CloseableDaemonThread bindCommandReader( @Nonnull CommandReader commands,
+                                                    WritableByteChannel stdIn )
+    {
+        return new StreamFeeder( "std-in-fork-" + getForkChannelId(), stdIn, commands, logger );
+    }
+
+    @Override
+    public CloseableDaemonThread bindEventHandler( @Nonnull EventHandler<Event> eventHandler,
+                                                   @Nonnull CountdownCloseable countdownCloseable,
+                                                   ReadableByteChannel stdOut )
+    {
+        return new EventConsumerThread( "fork-" + getForkChannelId() + "-event-thread-", stdOut,
+            eventHandler, countdownCloseable, logger );
+    }
+
+    @Override
+    public void close()
+    {
+    }
+}
diff --git a/surefire-extensions-api/src/test/java/org/apache/maven/surefire/extensions/util/JUnit4SuiteTest.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/LegacyForkNodeFactory.java
similarity index 58%
copy from surefire-extensions-api/src/test/java/org/apache/maven/surefire/extensions/util/JUnit4SuiteTest.java
copy to maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/LegacyForkNodeFactory.java
index df9cca1..ca2010b 100644
--- a/surefire-extensions-api/src/test/java/org/apache/maven/surefire/extensions/util/JUnit4SuiteTest.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/LegacyForkNodeFactory.java
@@ -1,4 +1,4 @@
-package org.apache.maven.surefire.extensions.util;
+package org.apache.maven.plugin.surefire.extensions;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,20 +19,22 @@ package org.apache.maven.surefire.extensions.util;
  * under the License.
  */
 
-import junit.framework.JUnit4TestAdapter;
-import junit.framework.Test;
-import junit.framework.TestCase;
-import junit.framework.TestSuite;
+import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
+import org.apache.maven.surefire.extensions.ForkChannel;
+import org.apache.maven.surefire.extensions.ForkNodeFactory;
+
+import javax.annotation.Nonnegative;
+import javax.annotation.Nonnull;
 
 /**
- *
+ * The factory of {@link LegacyForkChannel}.
  */
-public class JUnit4SuiteTest extends TestCase
+public class LegacyForkNodeFactory implements ForkNodeFactory
 {
-    public static Test suite()
+    @Nonnull
+    @Override
+    public ForkChannel createForkChannel( @Nonnegative int forkChannelId, ConsoleLogger logger )
     {
-        TestSuite suite = new TestSuite();
-        suite.addTest( new JUnit4TestAdapter( CommandlineExecutorTest.class ) );
-        return suite;
+        return new LegacyForkChannel( forkChannelId, logger );
     }
 }
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
new file mode 100644
index 0000000..604ca33
--- /dev/null
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/StreamFeeder.java
@@ -0,0 +1,203 @@
+package org.apache.maven.plugin.surefire.extensions;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
+import org.apache.maven.surefire.booter.Command;
+import org.apache.maven.surefire.booter.MasterProcessCommand;
+import org.apache.maven.surefire.extensions.CloseableDaemonThread;
+import org.apache.maven.surefire.extensions.CommandReader;
+import org.apache.maven.surefire.util.internal.ImmutableMap;
+
+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.booter.MasterProcessCommand.BYE_ACK;
+import static org.apache.maven.surefire.booter.MasterProcessCommand.MAGIC_NUMBER;
+import static org.apache.maven.surefire.booter.MasterProcessCommand.NOOP;
+import static org.apache.maven.surefire.booter.MasterProcessCommand.RUN_CLASS;
+import static org.apache.maven.surefire.booter.MasterProcessCommand.SHUTDOWN;
+import static org.apache.maven.surefire.booter.MasterProcessCommand.SKIP_SINCE_NEXT_TEST;
+import static org.apache.maven.surefire.booter.MasterProcessCommand.TEST_SET_FINISHED;
+
+/**
+ * Commands which are sent from plugin to the forked jvm.
+ * <br>
+ *     <br>
+ * magic number : opcode [: opcode specific data]*
+ * <br>
+ *     or data encoded with Base64
+ * <br>
+ * magic number : opcode [: Base64(opcode specific data)]*
+ *
+ * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
+ * @since 3.0.0-M5
+ */
+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;
+
+    private volatile boolean disabled;
+    private volatile Throwable exception;
+
+    public StreamFeeder( @Nonnull String threadName, @Nonnull WritableByteChannel channel,
+                         @Nonnull CommandReader commandReader, @Nonnull ConsoleLogger logger )
+    {
+        super( threadName );
+        this.channel = channel;
+        this.commandReader = commandReader;
+        this.logger = logger;
+    }
+
+    @Override
+    @SuppressWarnings( "checkstyle:innerassignment" )
+    public void run()
+    {
+        try ( WritableByteChannel c = 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 ) );
+                }
+            }
+        }
+        catch ( ClosedChannelException e )
+        {
+            // closed externally
+        }
+        catch ( IOException | NonWritableChannelException e )
+        {
+            exception = e.getCause() == null ? e : e.getCause();
+        }
+        catch ( IllegalArgumentException e )
+        {
+            logger.error( e.getLocalizedMessage() );
+        }
+    }
+
+    public void disable()
+    {
+        disabled = true;
+    }
+
+    public Throwable getException()
+    {
+        return exception;
+    }
+
+    @Override
+    public void close() throws IOException
+    {
+        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
new file mode 100644
index 0000000..0aa790c
--- /dev/null
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/SurefireForkChannel.java
@@ -0,0 +1,140 @@
+package org.apache.maven.plugin.surefire.extensions;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
+import org.apache.maven.surefire.eventapi.Event;
+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.util.CountdownCloseable;
+
+import javax.annotation.Nonnull;
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketOption;
+import java.nio.channels.Channel;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+import java.nio.channels.WritableByteChannel;
+
+import static java.net.StandardSocketOptions.SO_KEEPALIVE;
+import static java.net.StandardSocketOptions.SO_REUSEADDR;
+import static java.net.StandardSocketOptions.TCP_NODELAY;
+import static java.nio.channels.ServerSocketChannel.open;
+
+/**
+ * The TCP/IP server accepting only one client connection. The forked JVM connects to the server using the
+ * {@link #getForkNodeConnectionString() connection string}.
+ * The main purpose of this class is to {@link #connectToClient() conect with tthe client}, bind the
+ * {@link #bindCommandReader(CommandReader, WritableByteChannel) command reader} to the internal socket's
+ * {@link java.io.InputStream}, and bind the
+ * {@link #bindEventHandler(EventHandler, CountdownCloseable, ReadableByteChannel) event handler} writing the event
+ * objects to the {@link EventHandler event handler}.
+ * <br>
+ * The objects {@link WritableByteChannel} and {@link ReadableByteChannel} are forked process streams
+ * (standard input and output). Both are ignored in this implementation but they are used in {@link LegacyForkChannel}.
+ * <br>
+ * The channel is closed after the forked JVM has finished normally or the shutdown hook is executed in the plugin.
+ */
+final class SurefireForkChannel extends ForkChannel
+{
+    private static final byte[] LOCAL_LOOPBACK_IP_ADDRESS = new byte[]{127, 0, 0, 1};
+
+    private final ConsoleLogger logger;
+    private final ServerSocketChannel server;
+    private final int localPort;
+    private volatile SocketChannel channel;
+
+    SurefireForkChannel( int forkChannelId, ConsoleLogger logger ) throws IOException
+    {
+        super( forkChannelId );
+        this.logger = logger;
+        server = open();
+        setTrueOptions( SO_REUSEADDR, TCP_NODELAY, SO_KEEPALIVE );
+        InetAddress ip = Inet4Address.getByAddress( LOCAL_LOOPBACK_IP_ADDRESS );
+        server.bind( new InetSocketAddress( ip, 0 ), 1 );
+        localPort = ( (InetSocketAddress) server.getLocalAddress() ).getPort();
+    }
+
+    @Override
+    public void connectToClient() throws IOException
+    {
+        if ( channel != null )
+        {
+            throw new IllegalStateException( "already accepted TCP client connection" );
+        }
+        channel = server.accept();
+    }
+
+    @SafeVarargs
+    private final void setTrueOptions( SocketOption<Boolean>... options ) throws IOException
+    {
+        for ( SocketOption<Boolean> option : options )
+        {
+            if ( server.supportedOptions().contains( option ) )
+            {
+                server.setOption( option, true );
+            }
+        }
+    }
+
+    @Override
+    public String getForkNodeConnectionString()
+    {
+        return "tcp://127.0.0.1:" + localPort;
+    }
+
+    @Override
+    public boolean useStdOut()
+    {
+        return false;
+    }
+
+    @Override
+    public CloseableDaemonThread bindCommandReader( @Nonnull CommandReader commands,
+                                                    WritableByteChannel stdIn )
+    {
+        return new StreamFeeder( "commands-fork-" + getForkChannelId(), channel, commands, logger );
+    }
+
+    @Override
+    public CloseableDaemonThread bindEventHandler( @Nonnull EventHandler<Event> eventHandler,
+                                                   @Nonnull CountdownCloseable countdownCloseable,
+                                                   ReadableByteChannel stdOut )
+    {
+        return new EventConsumerThread( "fork-" + getForkChannelId() + "-event-thread-", channel,
+            eventHandler, countdownCloseable, logger );
+    }
+
+    @Override
+    public void close() throws IOException
+    {
+        //noinspection EmptyTryBlock
+        try ( Channel c1 = channel; Channel c2 = server )
+        {
+            // only close all channels
+        }
+    }
+}
diff --git a/surefire-extensions-api/src/test/java/org/apache/maven/surefire/extensions/util/JUnit4SuiteTest.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/SurefireForkNodeFactory.java
similarity index 56%
copy from surefire-extensions-api/src/test/java/org/apache/maven/surefire/extensions/util/JUnit4SuiteTest.java
copy to maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/SurefireForkNodeFactory.java
index df9cca1..1c4aabc 100644
--- a/surefire-extensions-api/src/test/java/org/apache/maven/surefire/extensions/util/JUnit4SuiteTest.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/SurefireForkNodeFactory.java
@@ -1,4 +1,4 @@
-package org.apache.maven.surefire.extensions.util;
+package org.apache.maven.plugin.surefire.extensions;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,20 +19,23 @@ package org.apache.maven.surefire.extensions.util;
  * under the License.
  */
 
-import junit.framework.JUnit4TestAdapter;
-import junit.framework.Test;
-import junit.framework.TestCase;
-import junit.framework.TestSuite;
+import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
+import org.apache.maven.surefire.extensions.ForkChannel;
+import org.apache.maven.surefire.extensions.ForkNodeFactory;
+
+import javax.annotation.Nonnegative;
+import javax.annotation.Nonnull;
+import java.io.IOException;
 
 /**
- *
+ * The factory of {@link SurefireForkChannel}.
  */
-public class JUnit4SuiteTest extends TestCase
+public class SurefireForkNodeFactory implements ForkNodeFactory
 {
-    public static Test suite()
+    @Nonnull
+    @Override
+    public ForkChannel createForkChannel( @Nonnegative int forkChannelId, ConsoleLogger logger ) throws IOException
     {
-        TestSuite suite = new TestSuite();
-        suite.addTest( new JUnit4TestAdapter( CommandlineExecutorTest.class ) );
-        return suite;
+        return new SurefireForkChannel( forkChannelId, logger );
     }
 }
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/AbstractSurefireMojoJava7PlusTest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/AbstractSurefireMojoJava7PlusTest.java
index 9faa4eb..abd896e 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/AbstractSurefireMojoJava7PlusTest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/AbstractSurefireMojoJava7PlusTest.java
@@ -28,6 +28,7 @@ import org.apache.maven.surefire.booter.ClassLoaderConfiguration;
 import org.apache.maven.surefire.booter.Classpath;
 import org.apache.maven.surefire.booter.ModularClasspathConfiguration;
 import org.apache.maven.surefire.booter.StartupConfiguration;
+import org.apache.maven.surefire.extensions.ForkNodeFactory;
 import org.apache.maven.surefire.suite.RunResult;
 import org.apache.maven.surefire.util.DefaultScanResult;
 import org.codehaus.plexus.languages.java.jpms.JavaModuleDescriptor;
@@ -180,11 +181,26 @@ public class AbstractSurefireMojoJava7PlusTest
                 "jar", "", handler );
         loggerApi.setFile( mockFile( "surefire-logger-api.jar" ) );
 
+        Artifact spi = new DefaultArtifact( "org.apache.maven.surefire", "surefire-extensions-spi",
+            createFromVersion( "1" ), "runtime", "jar", "", handler );
+        spi.setFile( mockFile( "surefire-extensions-spi.jar" ) );
+
+        Artifact booter = new DefaultArtifact( "org.apache.maven.surefire", "surefire-booter",
+            createFromVersion( "1" ), "runtime", "jar", "", handler );
+        booter.setFile( mockFile( "surefire-booter.jar" ) );
+
+        Artifact utils = new DefaultArtifact( "org.apache.maven.surefire", "surefire-shared-utils",
+            createFromVersion( "1" ), "runtime", "jar", "", handler );
+        utils.setFile( mockFile( "surefire-shared-utils.jar" ) );
+
         Map<String, Artifact> artifacts = new HashMap<>();
         artifacts.put( "org.apache.maven.surefire:maven-surefire-common", common );
         artifacts.put( "org.apache.maven.surefire:surefire-extensions-api", ext );
         artifacts.put( "org.apache.maven.surefire:surefire-api", api );
         artifacts.put( "org.apache.maven.surefire:surefire-logger-api", loggerApi );
+        artifacts.put( "org.apache.maven.surefire:surefire-extensions-spi", spi );
+        artifacts.put( "org.apache.maven.surefire:surefire-booter", booter );
+        artifacts.put( "org.apache.maven.surefire:surefire-shared-utils", utils );
         when( mojo.getPluginArtifactMap() ).thenReturn( artifacts );
 
         StartupConfiguration conf = invokeMethod( mojo, "newStartupConfigWithModularPath",
@@ -193,7 +209,6 @@ public class AbstractSurefireMojoJava7PlusTest
 
         verify( mojo, times( 1 ) ).effectiveIsEnableAssertions();
         verify( mojo, times( 1 ) ).isChildDelegation();
-        verify( mojo, times( 1 ) ).getEffectiveForkCount();
         verify( mojo, times( 1 ) ).getTestClassesDirectory();
         verify( scanResult, times( 1 ) ).getClasses();
         verifyStatic( ResolvePathsRequest.class, times( 1 ) );
@@ -219,8 +234,8 @@ public class AbstractSurefireMojoJava7PlusTest
                         "test(compact) classpath:  non-modular.jar  junit.jar  hamcrest.jar",
                         "test(compact) modulepath:  modular.jar  classes",
                         "provider(compact) classpath:  surefire-provider.jar",
-                        "in-process classpath:  surefire-provider.jar  maven-surefire-common.jar  surefire-extensions-api.jar  surefire-api.jar  surefire-logger-api.jar",
-                        "in-process(compact) classpath:  surefire-provider.jar  maven-surefire-common.jar  surefire-extensions-api.jar  surefire-api.jar  surefire-logger-api.jar"
+                        "in-process classpath:  surefire-provider.jar  maven-surefire-common.jar  surefire-booter.jar  surefire-extensions-api.jar  surefire-api.jar  surefire-extensions-spi.jar  surefire-logger-api.jar  surefire-shared-utils.jar",
+                        "in-process(compact) classpath:  surefire-provider.jar  maven-surefire-common.jar  surefire-booter.jar  surefire-extensions-api.jar  surefire-api.jar  surefire-extensions-spi.jar  surefire-logger-api.jar  surefire-shared-utils.jar"
                 );
 
         assertThat( conf ).isNotNull();
@@ -632,6 +647,12 @@ public class AbstractSurefireMojoJava7PlusTest
         }
 
         @Override
+        protected ForkNodeFactory getForkNode()
+        {
+            return null;
+        }
+
+        @Override
         protected Artifact getMojoArtifact()
         {
             return null;
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/AbstractSurefireMojoTest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/AbstractSurefireMojoTest.java
index b1e7c1e..0553de7 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/AbstractSurefireMojoTest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/AbstractSurefireMojoTest.java
@@ -42,6 +42,7 @@ import org.apache.maven.shared.transfer.dependencies.resolve.DependencyResolver;
 import org.apache.maven.surefire.booter.ClassLoaderConfiguration;
 import org.apache.maven.surefire.booter.Classpath;
 import org.apache.maven.surefire.booter.StartupConfiguration;
+import org.apache.maven.surefire.extensions.ForkNodeFactory;
 import org.apache.maven.surefire.suite.RunResult;
 import org.codehaus.plexus.logging.Logger;
 import org.junit.Before;
@@ -312,11 +313,26 @@ public class AbstractSurefireMojoTest
                 createFromVersion( "1" ), "runtime", "jar", "", handler );
         loggerApi.setFile( mockFile( "surefire-logger-api.jar" ) );
 
+        Artifact spi = new DefaultArtifact( "org.apache.maven.surefire", "surefire-extensions-spi",
+            createFromVersion( "1" ), "runtime", "jar", "", handler );
+        spi.setFile( mockFile( "surefire-extensions-spi.jar" ) );
+
+        Artifact booter = new DefaultArtifact( "org.apache.maven.surefire", "surefire-booter",
+            createFromVersion( "1" ), "runtime", "jar", "", handler );
+        booter.setFile( mockFile( "surefire-booter.jar" ) );
+
+        Artifact utils = new DefaultArtifact( "org.apache.maven.surefire", "surefire-shared-utils",
+            createFromVersion( "1" ), "runtime", "jar", "", handler );
+        utils.setFile( mockFile( "surefire-shared-utils.jar" ) );
+
         Map<String, Artifact> providerArtifactsMap = new HashMap<>();
         providerArtifactsMap.put( "org.apache.maven.surefire:maven-surefire-common", common );
         providerArtifactsMap.put( "org.apache.maven.surefire:surefire-extensions-api", ext );
         providerArtifactsMap.put( "org.apache.maven.surefire:surefire-api", api );
         providerArtifactsMap.put( "org.apache.maven.surefire:surefire-logger-api", loggerApi );
+        providerArtifactsMap.put( "org.apache.maven.surefire:surefire-extensions-spi", spi );
+        providerArtifactsMap.put( "org.apache.maven.surefire:surefire-booter", booter );
+        providerArtifactsMap.put( "org.apache.maven.surefire:surefire-shared-utils", utils );
 
         when( mojo.getPluginArtifactMap() )
                 .thenReturn( providerArtifactsMap );
@@ -358,7 +374,6 @@ public class AbstractSurefireMojoTest
 
         verify( mojo, times( 1 ) ).effectiveIsEnableAssertions();
         verify( mojo, times( 1 ) ).isChildDelegation();
-        verify( mojo, times( 1 ) ).getEffectiveForkCount();
         ArgumentCaptor<String> argument = ArgumentCaptor.forClass( String.class );
         verify( logger, times( 6 ) ).debug( argument.capture() );
         assertThat( argument.getAllValues() )
@@ -366,8 +381,8 @@ public class AbstractSurefireMojoTest
                 "provider classpath:  surefire-provider.jar",
                 "test(compact) classpath:  test-classes  classes  junit.jar  hamcrest.jar",
                 "provider(compact) classpath:  surefire-provider.jar",
-                "in-process classpath:  surefire-provider.jar  maven-surefire-common.jar  surefire-extensions-api.jar  surefire-api.jar  surefire-logger-api.jar",
-                "in-process(compact) classpath:  surefire-provider.jar  maven-surefire-common.jar  surefire-extensions-api.jar  surefire-api.jar  surefire-logger-api.jar"
+                "in-process classpath:  surefire-provider.jar  maven-surefire-common.jar  surefire-booter.jar  surefire-extensions-api.jar  surefire-api.jar  surefire-extensions-spi.jar  surefire-logger-api.jar  surefire-shared-utils.jar",
+                "in-process(compact) classpath:  surefire-provider.jar  maven-surefire-common.jar  surefire-booter.jar  surefire-extensions-api.jar  surefire-api.jar  surefire-extensions-spi.jar  surefire-logger-api.jar  surefire-shared-utils.jar"
                 );
 
         assertThat( conf.getClassLoaderConfiguration() )
@@ -405,7 +420,7 @@ public class AbstractSurefireMojoTest
         Artifact provider = new DefaultArtifact( "com.example", "provider", createFromVersion( "1" ), "runtime",
                 "jar", "", handler );
         provider.setFile( mockFile( "original-test-provider.jar" ) );
-        HashSet<Artifact> providerClasspath = new HashSet<>( asList( provider ) );
+        Set<Artifact> providerClasspath = singleton( provider );
         when( providerInfo.getProviderClasspath() ).thenReturn( providerClasspath );
 
         StartupConfiguration startupConfiguration = startupConfigurationForProvider( providerInfo );
@@ -448,11 +463,26 @@ public class AbstractSurefireMojoTest
                 createFromVersion( "1" ), "runtime", "jar", "", handler );
         loggerApi.setFile( mockFile( "surefire-logger-api.jar" ) );
 
+        Artifact spi = new DefaultArtifact( "org.apache.maven.surefire", "surefire-extensions-spi",
+            createFromVersion( "1" ), "runtime", "jar", "", handler );
+        spi.setFile( mockFile( "surefire-extensions-spi.jar" ) );
+
+        Artifact booter = new DefaultArtifact( "org.apache.maven.surefire", "surefire-booter",
+            createFromVersion( "1" ), "runtime", "jar", "", handler );
+        booter.setFile( mockFile( "surefire-booter.jar" ) );
+
+        Artifact utils = new DefaultArtifact( "org.apache.maven.surefire", "surefire-shared-utils",
+            createFromVersion( "1" ), "runtime", "jar", "", handler );
+        utils.setFile( mockFile( "surefire-shared-utils.jar" ) );
+
         Map<String, Artifact> providerArtifactsMap = new HashMap<>();
         providerArtifactsMap.put( "org.apache.maven.surefire:maven-surefire-common", common );
         providerArtifactsMap.put( "org.apache.maven.surefire:surefire-extensions-api", ext );
         providerArtifactsMap.put( "org.apache.maven.surefire:surefire-api", api );
         providerArtifactsMap.put( "org.apache.maven.surefire:surefire-logger-api", loggerApi );
+        providerArtifactsMap.put( "org.apache.maven.surefire:surefire-extensions-spi", spi );
+        providerArtifactsMap.put( "org.apache.maven.surefire:surefire-booter", booter );
+        providerArtifactsMap.put( "org.apache.maven.surefire:surefire-shared-utils", utils );
 
         when( mojo.getPluginArtifactMap() ).thenReturn( providerArtifactsMap );
 
@@ -2121,6 +2151,12 @@ public class AbstractSurefireMojoTest
         }
 
         @Override
+        protected ForkNodeFactory getForkNode()
+        {
+            return null;
+        }
+
+        @Override
         protected Artifact getMojoArtifact()
         {
             return new DefaultArtifact( "org.apache.maven.surefire", "maven-surefire-plugin", createFromVersion( "1" ),
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/CommonReflectorTest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/CommonReflectorTest.java
index 1f94a75..4f954d4 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/CommonReflectorTest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/CommonReflectorTest.java
@@ -23,15 +23,30 @@ import org.apache.maven.plugin.surefire.extensions.SurefireConsoleOutputReporter
 import org.apache.maven.plugin.surefire.extensions.SurefireStatelessReporter;
 import org.apache.maven.plugin.surefire.extensions.SurefireStatelessTestsetInfoReporter;
 import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
+import org.apache.maven.plugin.surefire.log.api.ConsoleLoggerDecorator;
+import org.apache.maven.plugin.surefire.log.api.PrintStreamLogger;
 import org.apache.maven.plugin.surefire.report.DefaultReporterFactory;
+import org.hamcrest.MatcherAssert;
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
 
 import java.io.File;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.maven.surefire.util.ReflectionUtils.getMethod;
+import static org.apache.maven.surefire.util.ReflectionUtils.invokeMethodWithArray;
 import static org.fest.assertions.Assertions.assertThat;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.sameInstance;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 import static org.powermock.reflect.Whitebox.getInternalState;
 
 /**
@@ -96,4 +111,39 @@ public class CommonReflectorTest
         assertThat( reportConfiguration.getConsoleOutputReporter().toString() )
                 .isEqualTo( consoleOutputReporter.toString() );
     }
+
+    @Test
+    public void shouldProxyConsoleLogger()
+    {
+        ClassLoader cl = Thread.currentThread().getContextClassLoader();
+        ConsoleLogger logger = spy( new PrintStreamLogger( System.out ) );
+        Object mirror = CommonReflector.createConsoleLogger( logger, cl );
+        MatcherAssert.assertThat( mirror, is( notNullValue() ) );
+        MatcherAssert.assertThat( mirror.getClass().getInterfaces()[0].getName(), is( ConsoleLogger.class.getName() ) );
+        MatcherAssert.assertThat( mirror, is( not( sameInstance( (Object) logger ) ) ) );
+        MatcherAssert.assertThat( mirror, is( instanceOf( ConsoleLoggerDecorator.class ) ) );
+        invokeMethodWithArray( mirror, getMethod( mirror, "info", String.class ), "Hi There!" );
+        verify( logger, times( 1 ) ).info( "Hi There!" );
+    }
+
+    @Test
+    public void testCreateConsoleLogger()
+    {
+        ClassLoader cl = Thread.currentThread().getContextClassLoader();
+        ConsoleLogger consoleLogger = mock( ConsoleLogger.class );
+        ConsoleLogger decorator = (ConsoleLogger) CommonReflector.createConsoleLogger( consoleLogger, cl );
+        assertThat( decorator )
+                .isNotSameAs( consoleLogger );
+
+        assertThat( decorator.isDebugEnabled() ).isFalse();
+        when( consoleLogger.isDebugEnabled() ).thenReturn( true );
+        assertThat( decorator.isDebugEnabled() ).isTrue();
+        verify( consoleLogger, times( 2 ) ).isDebugEnabled();
+
+        decorator.info( "msg" );
+        ArgumentCaptor<String> argumentMsg = ArgumentCaptor.forClass( String.class );
+        verify( consoleLogger, times( 1 ) ).info( argumentMsg.capture() );
+        assertThat( argumentMsg.getAllValues() ).hasSize( 1 );
+        assertThat( argumentMsg.getAllValues().get( 0 ) ).isEqualTo( "msg" );
+    }
 }
\ No newline at end of file
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/MojoMocklessTest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/MojoMocklessTest.java
index ccf63f7..835ce8e 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/MojoMocklessTest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/MojoMocklessTest.java
@@ -28,6 +28,7 @@ import org.apache.maven.plugin.MojoFailureException;
 import org.apache.maven.plugin.surefire.extensions.SurefireConsoleOutputReporter;
 import org.apache.maven.plugin.surefire.extensions.SurefireStatelessReporter;
 import org.apache.maven.plugin.surefire.extensions.SurefireStatelessTestsetInfoReporter;
+import org.apache.maven.surefire.extensions.ForkNodeFactory;
 import org.apache.maven.surefire.suite.RunResult;
 import org.apache.maven.surefire.util.DefaultScanResult;
 import org.apache.maven.toolchain.Toolchain;
@@ -751,6 +752,12 @@ public class MojoMocklessTest
         }
 
         @Override
+        protected ForkNodeFactory getForkNode()
+        {
+            return null;
+        }
+
+        @Override
         protected String getEnableProcessChecker()
         {
             return null;
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/SurefireReflectorTest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/SurefireReflectorTest.java
deleted file mode 100644
index 2553617..0000000
--- a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/SurefireReflectorTest.java
+++ /dev/null
@@ -1,71 +0,0 @@
-package org.apache.maven.plugin.surefire;
-
-/*
- * 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.ConsoleLoggerDecorator;
-import org.apache.maven.plugin.surefire.log.api.PrintStreamLogger;
-import org.apache.maven.surefire.booter.IsolatedClassLoader;
-import org.apache.maven.surefire.booter.SurefireReflector;
-import org.junit.Before;
-import org.junit.Test;
-
-import static org.apache.maven.surefire.util.ReflectionUtils.getMethod;
-import static org.apache.maven.surefire.util.ReflectionUtils.invokeMethodWithArray;
-import static org.hamcrest.CoreMatchers.instanceOf;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.CoreMatchers.not;
-import static org.hamcrest.CoreMatchers.notNullValue;
-import static org.hamcrest.CoreMatchers.sameInstance;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-/**
- * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
- * @see ConsoleLogger
- * @see SurefireReflector
- * @since 2.20
- */
-public class SurefireReflectorTest
-{
-    private ConsoleLogger logger;
-    private ClassLoader cl;
-
-    @Before
-    public void prepareData()
-    {
-        logger = spy( new PrintStreamLogger( System.out ) );
-        cl = new IsolatedClassLoader( Thread.currentThread().getContextClassLoader(), false, "role" );
-    }
-
-    @Test
-    public void shouldProxyConsoleLogger()
-    {
-        Object mirror = SurefireReflector.createConsoleLogger( logger, cl );
-        assertThat( mirror, is( notNullValue() ) );
-        assertThat( mirror.getClass().getInterfaces()[0].getName(), is( ConsoleLogger.class.getName() ) );
-        assertThat( mirror, is( not( sameInstance( (Object) logger ) ) ) );
-        assertThat( mirror, is( instanceOf( ConsoleLoggerDecorator.class ) ) );
-        invokeMethodWithArray( mirror, getMethod( mirror, "info", String.class ), "Hi There!" );
-        verify( logger, times( 1 ) ).info( "Hi There!" );
-    }
-}
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/BooterDeserializerProviderConfigurationTest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/BooterDeserializerProviderConfigurationTest.java
index ca42c44..10563a8 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/BooterDeserializerProviderConfigurationTest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/BooterDeserializerProviderConfigurationTest.java
@@ -25,7 +25,6 @@ import org.apache.maven.surefire.shared.io.FileUtils;
 import org.apache.maven.surefire.booter.BooterDeserializer;
 import org.apache.maven.surefire.booter.ClassLoaderConfiguration;
 import org.apache.maven.surefire.booter.ClasspathConfiguration;
-import org.apache.maven.surefire.booter.ProcessCheckerType;
 import org.apache.maven.surefire.booter.PropertiesWrapper;
 import org.apache.maven.surefire.booter.ProviderConfiguration;
 import org.apache.maven.surefire.booter.Shutdown;
@@ -52,6 +51,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 
+import static org.apache.maven.surefire.booter.ProcessCheckerType.ALL;
 import static org.apache.maven.surefire.cli.CommandLineOption.LOGGING_LEVEL_DEBUG;
 import static org.apache.maven.surefire.cli.CommandLineOption.REACTOR_FAIL_FAST;
 import static org.apache.maven.surefire.cli.CommandLineOption.SHOW_ERRORS;
@@ -260,9 +260,10 @@ public class BooterDeserializerProviderConfigurationTest
             test = "aTest";
         }
         final File propsTest = booterSerializer.serialize( props, booterConfiguration, testProviderConfiguration, test,
-                                                           readTestsFromInStream, 51L, 1 );
+                                                           readTestsFromInStream, 51L, 1, "pipe://1" );
         BooterDeserializer booterDeserializer = new BooterDeserializer( new FileInputStream( propsTest ) );
         assertEquals( "51", (Object) booterDeserializer.getPluginPid() );
+        assertEquals( "pipe://1", booterDeserializer.getConnectionString() );
         return booterDeserializer.deserialize();
     }
 
@@ -285,8 +286,7 @@ public class BooterDeserializerProviderConfigurationTest
     {
         ClasspathConfiguration classpathConfiguration = new ClasspathConfiguration( true, true );
 
-        return new StartupConfiguration( "com.provider", classpathConfiguration, classLoaderConfiguration, false,
-                                         false, ProcessCheckerType.ALL );
+        return new StartupConfiguration( "com.provider", classpathConfiguration, classLoaderConfiguration, ALL );
     }
 
     private File getTestSourceDirectory()
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/BooterDeserializerStartupConfigurationTest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/BooterDeserializerStartupConfigurationTest.java
index 4e9bbc9..63f6162 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/BooterDeserializerStartupConfigurationTest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/BooterDeserializerStartupConfigurationTest.java
@@ -20,24 +20,23 @@ package org.apache.maven.plugin.surefire.booterclient;
  */
 
 import junit.framework.TestCase;
-import org.apache.maven.surefire.shared.io.FileUtils;
 import org.apache.maven.surefire.booter.AbstractPathConfiguration;
 import org.apache.maven.surefire.booter.BooterDeserializer;
+import org.apache.maven.surefire.booter.ClassLoaderConfiguration;
 import org.apache.maven.surefire.booter.Classpath;
 import org.apache.maven.surefire.booter.ClasspathConfiguration;
-import org.apache.maven.surefire.booter.ClassLoaderConfiguration;
-import org.apache.maven.surefire.booter.ProcessCheckerType;
 import org.apache.maven.surefire.booter.PropertiesWrapper;
 import org.apache.maven.surefire.booter.ProviderConfiguration;
-import org.apache.maven.surefire.booter.StartupConfiguration;
 import org.apache.maven.surefire.booter.Shutdown;
+import org.apache.maven.surefire.booter.StartupConfiguration;
 import org.apache.maven.surefire.cli.CommandLineOption;
 import org.apache.maven.surefire.report.ReporterConfiguration;
+import org.apache.maven.surefire.shared.io.FileUtils;
 import org.apache.maven.surefire.testset.DirectoryScannerParameters;
 import org.apache.maven.surefire.testset.RunOrderParameters;
 import org.apache.maven.surefire.testset.TestArtifactInfo;
-import org.apache.maven.surefire.testset.TestRequest;
 import org.apache.maven.surefire.testset.TestListResolver;
+import org.apache.maven.surefire.testset.TestRequest;
 import org.apache.maven.surefire.util.RunOrder;
 import org.junit.After;
 import org.junit.Before;
@@ -50,6 +49,7 @@ import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 
+import static org.apache.maven.surefire.booter.ProcessCheckerType.ALL;
 import static org.apache.maven.surefire.cli.CommandLineOption.LOGGING_LEVEL_DEBUG;
 import static org.apache.maven.surefire.cli.CommandLineOption.REACTOR_FAIL_FAST;
 import static org.apache.maven.surefire.cli.CommandLineOption.SHOW_ERRORS;
@@ -105,7 +105,7 @@ public class BooterDeserializerStartupConfigurationTest
 
     public void testProcessChecker() throws IOException
     {
-        assertEquals( ProcessCheckerType.ALL, getReloadedStartupConfiguration().getProcessChecker() );
+        assertEquals( ALL, getReloadedStartupConfiguration().getProcessChecker() );
     }
 
     private void assertCpConfigEquals( ClasspathConfiguration expectedConfiguration,
@@ -136,13 +136,13 @@ public class BooterDeserializerStartupConfigurationTest
 
     public void testProcessCheckerAll() throws IOException
     {
-        assertEquals( ProcessCheckerType.ALL, getReloadedStartupConfiguration().getProcessChecker() );
+        assertEquals( ALL, getReloadedStartupConfiguration().getProcessChecker() );
     }
 
     public void testProcessCheckerNull() throws IOException
     {
         StartupConfiguration startupConfiguration = new StartupConfiguration( "com.provider", classpathConfiguration,
-                getManifestOnlyJarForkConfiguration(), false, false, null );
+                getManifestOnlyJarForkConfiguration(), null );
         assertNull( saveAndReload( startupConfiguration ).getProcessChecker() );
     }
 
@@ -178,15 +178,15 @@ public class BooterDeserializerStartupConfigurationTest
         BooterSerializer booterSerializer = new BooterSerializer( forkConfiguration );
         String aTest = "aTest";
         File propsTest = booterSerializer.serialize( props, getProviderConfiguration(), startupConfiguration, aTest,
-                false, null, 1 );
+                false, null, 1, "tcp://127.0.0.1:63003" );
         BooterDeserializer booterDeserializer = new BooterDeserializer( new FileInputStream( propsTest ) );
         assertNull( booterDeserializer.getPluginPid() );
+        assertEquals( "tcp://127.0.0.1:63003", booterDeserializer.getConnectionString() );
         return booterDeserializer.getStartupConfiguration();
     }
 
     private ProviderConfiguration getProviderConfiguration()
     {
-
         File cwd = new File( "." );
         DirectoryScannerParameters directoryScannerParameters =
             new DirectoryScannerParameters( cwd, new ArrayList<String>(), new ArrayList<String>(),
@@ -204,8 +204,7 @@ public class BooterDeserializerStartupConfigurationTest
 
     private StartupConfiguration getTestStartupConfiguration( ClassLoaderConfiguration classLoaderConfiguration )
     {
-        return new StartupConfiguration( "com.provider", classpathConfiguration, classLoaderConfiguration, false,
-                                         false, ProcessCheckerType.ALL );
+        return new StartupConfiguration( "com.provider", classpathConfiguration, classLoaderConfiguration, ALL );
     }
 
     private File getTestSourceDirectory()
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/DefaultForkConfigurationTest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/DefaultForkConfigurationTest.java
index 06fe754..45a6b4a 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/DefaultForkConfigurationTest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/DefaultForkConfigurationTest.java
@@ -27,7 +27,7 @@ import org.apache.maven.surefire.booter.Classpath;
 import org.apache.maven.surefire.booter.ClasspathConfiguration;
 import org.apache.maven.surefire.booter.ForkedBooter;
 import org.apache.maven.surefire.booter.StartupConfiguration;
-import org.apache.maven.surefire.booter.SurefireBooterForkException;
+import org.apache.maven.surefire.extensions.ForkNodeFactory;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -80,6 +80,7 @@ public class DefaultForkConfigurationTest
     private boolean reuseForks;
     private Platform pluginPlatform;
     private ConsoleLogger log;
+    private ForkNodeFactory forkNodeFactory;
 
     @Before
     public void setup()
@@ -97,6 +98,7 @@ public class DefaultForkConfigurationTest
         reuseForks = true;
         pluginPlatform = new Platform();
         log = mock( ConsoleLogger.class );
+        forkNodeFactory = mock( ForkNodeFactory.class );
     }
 
     @Test
@@ -104,16 +106,15 @@ public class DefaultForkConfigurationTest
     {
         DefaultForkConfiguration config = new DefaultForkConfiguration( booterClasspath, tempDirectory, debugLine,
                 workingDirectory, modelProperties, argLine, environmentVariables, excludedEnvironmentVariables,
-                debug, forkCount, reuseForks, pluginPlatform, log )
+                debug, forkCount, reuseForks, pluginPlatform, log, forkNodeFactory )
         {
 
             @Override
             protected void resolveClasspath( @Nonnull OutputStreamFlushableCommandline cli,
                                              @Nonnull String booterThatHasMainMethod,
                                              @Nonnull StartupConfiguration config,
-                                             @Nonnull File dumpLogDirectory ) throws SurefireBooterForkException
+                                             @Nonnull File dumpLogDirectory )
             {
-
             }
         };
 
@@ -130,16 +131,15 @@ public class DefaultForkConfigurationTest
         argLine = "";
         DefaultForkConfiguration config = new DefaultForkConfiguration( booterClasspath, tempDirectory, debugLine,
                 workingDirectory, modelProperties, argLine, environmentVariables, excludedEnvironmentVariables,
-                debug, forkCount, reuseForks, pluginPlatform, log )
+                debug, forkCount, reuseForks, pluginPlatform, log, forkNodeFactory )
         {
 
             @Override
             protected void resolveClasspath( @Nonnull OutputStreamFlushableCommandline cli,
                                              @Nonnull String booterThatHasMainMethod,
                                              @Nonnull StartupConfiguration config,
-                                             @Nonnull File dumpLogDirectory ) throws SurefireBooterForkException
+                                             @Nonnull File dumpLogDirectory )
             {
-
             }
         };
 
@@ -156,16 +156,15 @@ public class DefaultForkConfigurationTest
         argLine = "\n\r";
         DefaultForkConfiguration config = new DefaultForkConfiguration( booterClasspath, tempDirectory, debugLine,
                 workingDirectory, modelProperties, argLine, environmentVariables, excludedEnvironmentVariables,
-                debug, forkCount, reuseForks, pluginPlatform, log )
+                debug, forkCount, reuseForks, pluginPlatform, log, forkNodeFactory )
         {
 
             @Override
             protected void resolveClasspath( @Nonnull OutputStreamFlushableCommandline cli,
                                              @Nonnull String booterThatHasMainMethod,
                                              @Nonnull StartupConfiguration config,
-                                             @Nonnull File dumpLogDirectory ) throws SurefireBooterForkException
+                                             @Nonnull File dumpLogDirectory )
             {
-
             }
         };
 
@@ -182,16 +181,15 @@ public class DefaultForkConfigurationTest
         argLine = "-Dfile.encoding=UTF-8";
         DefaultForkConfiguration config = new DefaultForkConfiguration( booterClasspath, tempDirectory, debugLine,
                 workingDirectory, modelProperties, argLine, environmentVariables, excludedEnvironmentVariables,
-                debug, forkCount, reuseForks, pluginPlatform, log )
+                debug, forkCount, reuseForks, pluginPlatform, log, forkNodeFactory )
         {
 
             @Override
             protected void resolveClasspath( @Nonnull OutputStreamFlushableCommandline cli,
                                              @Nonnull String booterThatHasMainMethod,
                                              @Nonnull StartupConfiguration config,
-                                             @Nonnull File dumpLogDirectory ) throws SurefireBooterForkException
+                                             @Nonnull File dumpLogDirectory )
             {
-
             }
         };
 
@@ -209,16 +207,15 @@ public class DefaultForkConfigurationTest
         argLine = "-Dfile.encoding=@{encoding}";
         DefaultForkConfiguration config = new DefaultForkConfiguration( booterClasspath, tempDirectory, debugLine,
                 workingDirectory, modelProperties, argLine, environmentVariables, excludedEnvironmentVariables,
-                debug, forkCount, reuseForks, pluginPlatform, log )
+                debug, forkCount, reuseForks, pluginPlatform, log, forkNodeFactory )
         {
 
             @Override
             protected void resolveClasspath( @Nonnull OutputStreamFlushableCommandline cli,
                                              @Nonnull String booterThatHasMainMethod,
                                              @Nonnull StartupConfiguration config,
-                                             @Nonnull File dumpLogDirectory ) throws SurefireBooterForkException
+                                             @Nonnull File dumpLogDirectory )
             {
-
             }
         };
 
@@ -235,16 +232,15 @@ public class DefaultForkConfigurationTest
         argLine = "a\n\rb";
         DefaultForkConfiguration config = new DefaultForkConfiguration( booterClasspath, tempDirectory, debugLine,
                 workingDirectory, modelProperties, argLine, environmentVariables, excludedEnvironmentVariables,
-                debug, forkCount, reuseForks, pluginPlatform, log )
+                debug, forkCount, reuseForks, pluginPlatform, log, forkNodeFactory )
         {
 
             @Override
             protected void resolveClasspath( @Nonnull OutputStreamFlushableCommandline cli,
                                              @Nonnull String booterThatHasMainMethod,
                                              @Nonnull StartupConfiguration config,
-                                             @Nonnull File dumpLogDirectory ) throws SurefireBooterForkException
+                                             @Nonnull File dumpLogDirectory )
             {
-
             }
         };
 
@@ -261,16 +257,15 @@ public class DefaultForkConfigurationTest
         argLine = "-Dthread=${surefire.threadNumber}";
         DefaultForkConfiguration config = new DefaultForkConfiguration( booterClasspath, tempDirectory, debugLine,
                 workingDirectory, modelProperties, argLine, environmentVariables, excludedEnvironmentVariables,
-                debug, forkCount, reuseForks, pluginPlatform, log )
+                debug, forkCount, reuseForks, pluginPlatform, log, forkNodeFactory )
         {
 
             @Override
             protected void resolveClasspath( @Nonnull OutputStreamFlushableCommandline cli,
                                              @Nonnull String booterThatHasMainMethod,
                                              @Nonnull StartupConfiguration config,
-                                             @Nonnull File dumpLogDirectory ) throws SurefireBooterForkException
+                                             @Nonnull File dumpLogDirectory )
             {
-
             }
         };
 
@@ -287,16 +282,15 @@ public class DefaultForkConfigurationTest
         argLine = "-Dthread=${surefire.forkNumber}";
         DefaultForkConfiguration config = new DefaultForkConfiguration( booterClasspath, tempDirectory, debugLine,
                 workingDirectory, modelProperties, argLine, environmentVariables, excludedEnvironmentVariables,
-                debug, forkCount, reuseForks, pluginPlatform, log )
+                debug, forkCount, reuseForks, pluginPlatform, log, forkNodeFactory )
         {
 
             @Override
             protected void resolveClasspath( @Nonnull OutputStreamFlushableCommandline cli,
                                              @Nonnull String booterThatHasMainMethod,
                                              @Nonnull StartupConfiguration config,
-                                             @Nonnull File dumpLogDirectory ) throws SurefireBooterForkException
+                                             @Nonnull File dumpLogDirectory )
             {
-
             }
         };
 
@@ -313,7 +307,7 @@ public class DefaultForkConfigurationTest
         ClassLoaderConfiguration clc = new ClassLoaderConfiguration( true, true );
         ClasspathConfiguration cc = new ClasspathConfiguration( true, true );
         StartupConfiguration conf = new StartupConfiguration( "org.apache.maven.shadefire.surefire.MyProvider",
-                cc, clc, false, false, null );
+                cc, clc, null );
         StartupConfiguration confMock = spy( conf );
         mockStatic( Relocator.class );
         when( Relocator.relocate( anyString() ) ).thenCallRealMethod();
@@ -334,7 +328,7 @@ public class DefaultForkConfigurationTest
         ClassLoaderConfiguration clc = new ClassLoaderConfiguration( true, true );
         ClasspathConfiguration cc = new ClasspathConfiguration( true, true );
         StartupConfiguration conf =
-                new StartupConfiguration( "org.apache.maven.surefire.MyProvider", cc, clc, false, false, null );
+                new StartupConfiguration( "org.apache.maven.surefire.MyProvider", cc, clc, null );
         StartupConfiguration confMock = spy( conf );
         mockStatic( Relocator.class );
         when( Relocator.relocate( anyString() ) ).thenCallRealMethod();
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/ForkConfigurationTest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/ForkConfigurationTest.java
index 72e5372..bc01ee8 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/ForkConfigurationTest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/ForkConfigurationTest.java
@@ -30,6 +30,7 @@ import org.apache.maven.surefire.booter.Classpath;
 import org.apache.maven.surefire.booter.ClasspathConfiguration;
 import org.apache.maven.surefire.booter.StartupConfiguration;
 import org.apache.maven.surefire.booter.SurefireBooterForkException;
+import org.apache.maven.surefire.extensions.ForkNodeFactory;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -47,6 +48,7 @@ import static org.fest.util.Files.temporaryFolder;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
 
 /**
  *
@@ -55,10 +57,7 @@ public class ForkConfigurationTest
 {
     private static final StartupConfiguration STARTUP_CONFIG = new StartupConfiguration( "",
             new ClasspathConfiguration( true, true ),
-            new ClassLoaderConfiguration( true, true ),
-            false,
-            false,
-            ALL );
+            new ClassLoaderConfiguration( true, true ), ALL );
 
     private static int idx = 0;
 
@@ -91,7 +90,7 @@ public class ForkConfigurationTest
         ClasspathConfiguration cpConfig = new ClasspathConfiguration( new Classpath( cp ), emptyClasspath(),
                 emptyClasspath(), true, true );
         ClassLoaderConfiguration clc = new ClassLoaderConfiguration( true, true );
-        StartupConfiguration startup = new StartupConfiguration( "", cpConfig, clc, false, false, ALL );
+        StartupConfiguration startup = new StartupConfiguration( "", cpConfig, clc, ALL );
 
         Commandline cli = config.createCommandLine( startup, 1, temporaryFolder() );
 
@@ -111,7 +110,7 @@ public class ForkConfigurationTest
         ClasspathConfiguration cpConfig = new ClasspathConfiguration( new Classpath( cp ), emptyClasspath(),
                 emptyClasspath(), true, true );
         ClassLoaderConfiguration clc = new ClassLoaderConfiguration( true, true );
-        StartupConfiguration startup = new StartupConfiguration( "", cpConfig, clc, false, false, ALL );
+        StartupConfiguration startup = new StartupConfiguration( "", cpConfig, clc, ALL );
 
         Commandline commandLine = config.createCommandLine( startup, 1, temporaryFolder() );
         assertTrue( commandLine.toString().contains( "abc def" ) );
@@ -126,7 +125,7 @@ public class ForkConfigurationTest
         ClasspathConfiguration cpConfig = new ClasspathConfiguration( emptyClasspath(), emptyClasspath(),
                 emptyClasspath(), true, true );
         ClassLoaderConfiguration clc = new ClassLoaderConfiguration( true, true );
-        StartupConfiguration startup = new StartupConfiguration( "", cpConfig, clc, false, false, ALL );
+        StartupConfiguration startup = new StartupConfiguration( "", cpConfig, clc, ALL );
         ForkConfiguration config = getForkConfiguration( cwd.getCanonicalFile() );
         Commandline commandLine = config.createCommandLine( startup, 1, temporaryFolder() );
 
@@ -225,7 +224,7 @@ public class ForkConfigurationTest
         return new JarManifestForkConfiguration( emptyClasspath(), tmpDir, null,
                 cwd, new Properties(), argLine,
                 Collections.<String, String>emptyMap(), new String[0], false, 1, false,
-                platform, new NullConsoleLogger() );
+                platform, new NullConsoleLogger(), mock( ForkNodeFactory.class ) );
     }
 
     // based on http://stackoverflow.com/questions/2591083/getting-version-of-java-in-runtime
@@ -233,6 +232,6 @@ public class ForkConfigurationTest
     private static boolean isJavaVersionAtLeast7u60()
     {
         String[] javaVersionElements = System.getProperty( "java.runtime.version" ).split( "\\.|_|-b" );
-        return Integer.valueOf( javaVersionElements[1] ) >= 7 && Integer.valueOf( javaVersionElements[3] ) >= 60;
+        return Integer.parseInt( javaVersionElements[1] ) >= 7 && Integer.parseInt( javaVersionElements[3] ) >= 60;
     }
 }
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/ForkStarterTest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/ForkStarterTest.java
index e0874f0..55f789f 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/ForkStarterTest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/ForkStarterTest.java
@@ -21,12 +21,13 @@ package org.apache.maven.plugin.surefire.booterclient;
 
 import org.apache.maven.plugin.surefire.StartupReportConfiguration;
 import org.apache.maven.plugin.surefire.SurefireProperties;
-import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.AbstractForkInputStream;
+import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.AbstractCommandReader;
 import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.OutputStreamFlushableCommandline;
 import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.TestLessInputStream;
 import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.TestLessInputStream.TestLessInputStreamBuilder;
 import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.TestProvidingInputStream;
 import org.apache.maven.plugin.surefire.booterclient.output.ForkClient;
+import org.apache.maven.plugin.surefire.extensions.LegacyForkNodeFactory;
 import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
 import org.apache.maven.plugin.surefire.report.DefaultReporterFactory;
 import org.apache.maven.surefire.booter.AbstractPathConfiguration;
@@ -37,6 +38,7 @@ import org.apache.maven.surefire.booter.ProviderConfiguration;
 import org.apache.maven.surefire.booter.Shutdown;
 import org.apache.maven.surefire.booter.StartupConfiguration;
 import org.apache.maven.surefire.booter.SurefireBooterForkException;
+import org.apache.maven.surefire.extensions.ForkNodeFactory;
 import org.apache.maven.surefire.report.ReporterConfiguration;
 import org.apache.maven.surefire.shared.compress.archivers.zip.Zip64Mode;
 import org.apache.maven.surefire.shared.compress.archivers.zip.ZipArchiveEntry;
@@ -55,7 +57,6 @@ import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayDeque;
 import java.util.Collections;
-import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.jar.Manifest;
 import java.util.zip.Deflater;
 
@@ -170,12 +171,12 @@ public class ForkStarterTest
         e.expectMessage( containsString( "VM crash or System.exit called?" ) );
 
         Class<?>[] types = {Object.class, PropertiesWrapper.class, ForkClient.class, SurefireProperties.class,
-            int.class, AbstractForkInputStream.class, boolean.class};
+            int.class, AbstractCommandReader.class, ForkNodeFactory.class, boolean.class};
         TestProvidingInputStream testProvidingInputStream = new TestProvidingInputStream( new ArrayDeque<String>() );
         invokeMethod( forkStarter, "fork", types, null,
             new PropertiesWrapper( Collections.<String, String>emptyMap() ),
-            new ForkClient( reporterFactory, null, logger, new AtomicBoolean(), 1 ),
-            new SurefireProperties(), 1, testProvidingInputStream, true );
+            new ForkClient( reporterFactory, null, 1 ),
+            new SurefireProperties(), 1, testProvidingInputStream, new LegacyForkNodeFactory(), true );
         testProvidingInputStream.close();
     }
 
@@ -224,12 +225,12 @@ public class ForkStarterTest
         DefaultReporterFactory reporterFactory = new DefaultReporterFactory( startupReportConfiguration, logger, 1 );
 
         Class<?>[] types = {Object.class, PropertiesWrapper.class, ForkClient.class, SurefireProperties.class,
-            int.class, AbstractForkInputStream.class, boolean.class};
+            int.class, AbstractCommandReader.class, ForkNodeFactory.class, boolean.class};
         TestLessInputStream testLessInputStream = new TestLessInputStreamBuilder().build();
         invokeMethod( forkStarter, "fork", types, null,
             new PropertiesWrapper( Collections.<String, String>emptyMap() ),
-            new ForkClient( reporterFactory, testLessInputStream, logger, new AtomicBoolean(), 1 ),
-            new SurefireProperties(), 1, testLessInputStream, true );
+            new ForkClient( reporterFactory, testLessInputStream, 1 ),
+            new SurefireProperties(), 1, testLessInputStream, new LegacyForkNodeFactory(), true );
         testLessInputStream.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 79c6f0e..4ca19ad 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
@@ -19,14 +19,16 @@ package org.apache.maven.plugin.surefire.booterclient;
  * under the License.
  */
 
-import junit.framework.Assert;
 import junit.framework.TestCase;
 import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.NotifiableTestStream;
 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.plugin.surefire.log.api.NullConsoleLogger;
-import org.apache.maven.surefire.booter.ForkedChannelEncoder;
 import org.apache.maven.surefire.booter.ForkingRunListener;
+import org.apache.maven.surefire.booter.spi.LegacyMasterProcessChannelEncoder;
+import org.apache.maven.surefire.eventapi.Event;
+import org.apache.maven.surefire.extensions.EventHandler;
+import org.apache.maven.surefire.extensions.util.CountdownCloseable;
 import org.apache.maven.surefire.report.CategorizedReportEntry;
 import org.apache.maven.surefire.report.ConsoleOutputReceiver;
 import org.apache.maven.surefire.report.LegacyPojoStackTraceWriter;
@@ -36,17 +38,23 @@ import org.apache.maven.surefire.report.RunListener;
 import org.apache.maven.surefire.report.SimpleReportEntry;
 import org.apache.maven.surefire.report.StackTraceWriter;
 import org.apache.maven.surefire.report.TestSetReportEntry;
-import org.hamcrest.MatcherAssert;
 
+import javax.annotation.Nonnull;
+import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
-import java.io.IOException;
+import java.io.Closeable;
 import java.io.PrintStream;
+import java.nio.channels.ReadableByteChannel;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
 
-import static org.hamcrest.Matchers.is;
+import static java.nio.channels.Channels.newChannel;
+import static org.mockito.Mockito.mock;
 
 /**
  * @author Kristian Rosenvold
@@ -74,8 +82,7 @@ public class ForkingRunListenerTest
         content.reset();
     }
 
-    public void testSetStarting()
-        throws ReporterException, IOException
+    public void testSetStarting() throws Exception
     {
         final StandardTestRun standardTestRun = new StandardTestRun();
         TestSetReportEntry expected = createDefaultReportEntry();
@@ -83,8 +90,7 @@ public class ForkingRunListenerTest
         standardTestRun.assertExpected( MockReporter.SET_STARTING, expected );
     }
 
-    public void testSetCompleted()
-        throws ReporterException, IOException
+    public void testSetCompleted() throws Exception
     {
         final StandardTestRun standardTestRun = new StandardTestRun();
         TestSetReportEntry expected = createDefaultReportEntry();
@@ -92,8 +98,7 @@ public class ForkingRunListenerTest
         standardTestRun.assertExpected( MockReporter.SET_COMPLETED, expected );
     }
 
-    public void testStarting()
-        throws ReporterException, IOException
+    public void testStarting() throws Exception
     {
         final StandardTestRun standardTestRun = new StandardTestRun();
         ReportEntry expected = createDefaultReportEntry();
@@ -101,8 +106,7 @@ public class ForkingRunListenerTest
         standardTestRun.assertExpected( MockReporter.TEST_STARTING, expected );
     }
 
-    public void testSucceded()
-        throws ReporterException, IOException
+    public void testSucceeded() throws Exception
     {
         final StandardTestRun standardTestRun = new StandardTestRun();
         ReportEntry expected = createDefaultReportEntry();
@@ -110,8 +114,7 @@ public class ForkingRunListenerTest
         standardTestRun.assertExpected( MockReporter.TEST_SUCCEEDED, expected );
     }
 
-    public void testFailed()
-        throws ReporterException, IOException
+    public void testFailed() throws Exception
     {
         final StandardTestRun standardTestRun = new StandardTestRun();
         ReportEntry expected = createReportEntryWithStackTrace();
@@ -119,8 +122,7 @@ public class ForkingRunListenerTest
         standardTestRun.assertExpected( MockReporter.TEST_FAILED, expected );
     }
 
-    public void testFailedWithCommaInMessage()
-        throws ReporterException, IOException
+    public void testFailedWithCommaInMessage() throws Exception
     {
         final StandardTestRun standardTestRun = new StandardTestRun();
         ReportEntry expected = createReportEntryWithSpecialMessage( "We, the people" );
@@ -128,8 +130,7 @@ public class ForkingRunListenerTest
         standardTestRun.assertExpected( MockReporter.TEST_FAILED, expected );
     }
 
-    public void testFailedWithUnicodeEscapeInMessage()
-        throws ReporterException, IOException
+    public void testFailedWithUnicodeEscapeInMessage() throws Exception
     {
         final StandardTestRun standardTestRun = new StandardTestRun();
         ReportEntry expected = createReportEntryWithSpecialMessage( "We, \\u0177 people" );
@@ -137,8 +138,7 @@ public class ForkingRunListenerTest
         standardTestRun.assertExpected( MockReporter.TEST_FAILED, expected );
     }
 
-    public void testFailure()
-        throws ReporterException, IOException
+    public void testFailure() throws Exception
     {
         final StandardTestRun standardTestRun = new StandardTestRun();
         ReportEntry expected = createDefaultReportEntry();
@@ -146,8 +146,7 @@ public class ForkingRunListenerTest
         standardTestRun.assertExpected( MockReporter.TEST_ERROR, expected );
     }
 
-    public void testSkipped()
-        throws ReporterException, IOException
+    public void testSkipped() throws Exception
     {
         final StandardTestRun standardTestRun = new StandardTestRun();
         ReportEntry expected = createDefaultReportEntry();
@@ -155,8 +154,7 @@ public class ForkingRunListenerTest
         standardTestRun.assertExpected( MockReporter.TEST_SKIPPED, expected );
     }
 
-    public void testAssumptionFailure()
-        throws ReporterException, IOException
+    public void testAssumptionFailure() throws Exception
     {
         final StandardTestRun standardTestRun = new StandardTestRun();
         ReportEntry expected = createDefaultReportEntry();
@@ -164,8 +162,7 @@ public class ForkingRunListenerTest
         standardTestRun.assertExpected( MockReporter.TEST_ASSUMPTION_FAIL, expected );
     }
 
-    public void testConsole()
-        throws ReporterException, IOException
+    public void testConsole() throws Exception
     {
         final StandardTestRun standardTestRun = new StandardTestRun();
         ConsoleLogger directConsoleReporter = (ConsoleLogger) standardTestRun.run();
@@ -173,8 +170,7 @@ public class ForkingRunListenerTest
         standardTestRun.assertExpected( MockReporter.CONSOLE_INFO, "HeyYou" );
     }
 
-    public void testConsoleOutput()
-        throws ReporterException, IOException
+    public void testConsoleOutput() throws Exception
     {
         final StandardTestRun standardTestRun = new StandardTestRun();
         ConsoleOutputReceiver directConsoleReporter = (ConsoleOutputReceiver) standardTestRun.run();
@@ -182,30 +178,36 @@ public class ForkingRunListenerTest
         standardTestRun.assertExpected( MockReporter.STDOUT, "HeyYou" );
     }
 
-    public void testSystemProperties()
-        throws ReporterException, IOException
+    public void testSystemProperties() throws Exception
     {
-        final StandardTestRun standardTestRun = new StandardTestRun();
+        StandardTestRun standardTestRun = new StandardTestRun();
         standardTestRun.run();
 
         reset();
         createForkingRunListener();
 
         TestSetMockReporterFactory providerReporterFactory = new TestSetMockReporterFactory();
-        NullConsoleLogger log = new NullConsoleLogger();
-        ForkClient forkStreamClient =
-                new ForkClient( providerReporterFactory, new MockNotifiableTestStream(), log, new AtomicBoolean(), 1 );
+        ForkClient forkStreamClient = new ForkClient( providerReporterFactory, new MockNotifiableTestStream(), 1 );
 
-        forkStreamClient.consumeMultiLineContent( ":maven:surefire:std:out:sys-prop:normal-run:UTF-8:azE=:djE="
-                + "\n:maven:surefire:std:out:sys-prop:normal-run:UTF-8:azI=:djI=" );
+        byte[] cmd = ":maven-surefire-event:sys-prop:normal-run:UTF-8:azE=:djE=:\n".getBytes();
+        for ( Event e : streamToEvent( cmd ) )
+        {
+            forkStreamClient.handleEvent( e );
+        }
+        cmd = "\n:maven-surefire-event:sys-prop:normal-run:UTF-8:azI=:djI=:\n".getBytes();
+        for ( Event e : streamToEvent( cmd ) )
+        {
+            forkStreamClient.handleEvent( e );
+        }
 
-        MatcherAssert.assertThat( forkStreamClient.getTestVmSystemProperties().size(), is( 2 ) );
+        assertTrue( forkStreamClient.getTestVmSystemProperties().size() == 2 );
+        assertTrue( forkStreamClient.getTestVmSystemProperties().containsKey( "k1" ) );
+        assertTrue( forkStreamClient.getTestVmSystemProperties().containsKey( "k2" ) );
     }
 
-    public void testMultipleEntries()
-        throws ReporterException, IOException
+    public void testMultipleEntries() throws Exception
     {
-        final StandardTestRun standardTestRun = new StandardTestRun();
+        StandardTestRun standardTestRun = new StandardTestRun();
         standardTestRun.run();
 
         reset();
@@ -218,11 +220,13 @@ public class ForkingRunListenerTest
         forkingReporter.testSetCompleted( reportEntry );
 
         TestSetMockReporterFactory providerReporterFactory = new TestSetMockReporterFactory();
-        NullConsoleLogger log = new NullConsoleLogger();
         ForkClient forkStreamClient =
-                new ForkClient( providerReporterFactory, new MockNotifiableTestStream(), log, null, 1 );
+                new ForkClient( providerReporterFactory, new MockNotifiableTestStream(), 1 );
 
-        forkStreamClient.consumeMultiLineContent( content.toString( "UTF-8" ) );
+        for ( Event e : streamToEvent( content.toByteArray() ) )
+        {
+            forkStreamClient.handleEvent( e );
+        }
 
         final MockReporter reporter = (MockReporter) forkStreamClient.getReporter();
         final List<String> events = reporter.getEvents();
@@ -233,36 +237,82 @@ public class ForkingRunListenerTest
     }
 
     public void test2DifferentChannels()
-        throws ReporterException, IOException
+        throws Exception
     {
         reset();
         ReportEntry expected = createDefaultReportEntry();
-        final SimpleReportEntry secondExpected = createAnotherDefaultReportEntry();
+        SimpleReportEntry secondExpected = createAnotherDefaultReportEntry();
 
-        new ForkingRunListener( new ForkedChannelEncoder( printStream ), false )
+        new ForkingRunListener( new LegacyMasterProcessChannelEncoder( newChannel( printStream ) ), false )
                 .testStarting( expected );
 
-        new ForkingRunListener( new ForkedChannelEncoder( anotherPrintStream ), false )
+        new ForkingRunListener( new LegacyMasterProcessChannelEncoder( newChannel( anotherPrintStream ) ), false )
                 .testSkipped( secondExpected );
 
         TestSetMockReporterFactory providerReporterFactory = new TestSetMockReporterFactory();
         NotifiableTestStream notifiableTestStream = new MockNotifiableTestStream();
-        NullConsoleLogger log = new NullConsoleLogger();
 
-        ForkClient forkStreamClient = new ForkClient( providerReporterFactory, notifiableTestStream, log, null, 1 );
-        forkStreamClient.consumeMultiLineContent( content.toString( "UTF-8" ) );
+        ForkClient forkStreamClient = new ForkClient( providerReporterFactory, notifiableTestStream, 1 );
+        for ( Event e : streamToEvent( content.toByteArray() ) )
+        {
+            forkStreamClient.handleEvent( e );
+        }
 
         MockReporter reporter = (MockReporter) forkStreamClient.getReporter();
-        Assert.assertEquals( MockReporter.TEST_STARTING, reporter.getFirstEvent() );
-        Assert.assertEquals( expected, reporter.getFirstData() );
-        Assert.assertEquals( 1, reporter.getEvents().size() );
+        assertEquals( MockReporter.TEST_STARTING, reporter.getFirstEvent() );
+        assertEquals( expected, reporter.getFirstData() );
+        assertEquals( 1, reporter.getEvents().size() );
 
-        forkStreamClient = new ForkClient( providerReporterFactory, notifiableTestStream, log, null, 2 );
-        forkStreamClient.consumeMultiLineContent( anotherContent.toString( "UTF-8" ) );
+        forkStreamClient = new ForkClient( providerReporterFactory, notifiableTestStream, 2 );
+        for ( Event e : streamToEvent( anotherContent.toByteArray() ) )
+        {
+            forkStreamClient.handleEvent( e );
+        }
         MockReporter reporter2 = (MockReporter) forkStreamClient.getReporter();
-        Assert.assertEquals( MockReporter.TEST_SKIPPED, reporter2.getFirstEvent() );
-        Assert.assertEquals( secondExpected, reporter2.getFirstData() );
-        Assert.assertEquals( 1, reporter2.getEvents().size() );
+        assertEquals( MockReporter.TEST_SKIPPED, reporter2.getFirstEvent() );
+        assertEquals( secondExpected, reporter2.getFirstData() );
+        assertEquals( 1, reporter2.getEvents().size() );
+    }
+
+    private static List<Event> streamToEvent( byte[] stream ) throws Exception
+    {
+        List<Event> events = new ArrayList<>();
+        EH handler = new EH();
+        CountdownCloseable countdown = new CountdownCloseable( mock( Closeable.class ), 1 );
+        ConsoleLogger logger = mock( ConsoleLogger.class );
+        ReadableByteChannel channel = newChannel( new ByteArrayInputStream( stream ) );
+        try ( EventConsumerThread t = new EventConsumerThread( "t", channel, handler, countdown, logger ) )
+        {
+            t.start();
+            countdown.awaitClosed();
+            for ( int i = 0, size = handler.countEventsInCache(); i < size; i++ )
+            {
+                events.add( handler.pullEvent() );
+            }
+            assertEquals( 0, handler.countEventsInCache() );
+            return events;
+        }
+    }
+
+    private static class EH implements EventHandler<Event>
+    {
+        private final BlockingQueue<Event> cache = new LinkedBlockingQueue<>();
+
+        Event pullEvent() throws InterruptedException
+        {
+            return cache.poll( 1, TimeUnit.MINUTES );
+        }
+
+        int countEventsInCache()
+        {
+            return cache.size();
+        }
+
+        @Override
+        public void handleEvent( @Nonnull Event event )
+        {
+            cache.add( event );
+        }
     }
 
     // Todo: Test weird characters
@@ -312,7 +362,7 @@ public class ForkingRunListenerTest
 
     private RunListener createForkingRunListener()
     {
-        return new ForkingRunListener( new ForkedChannelEncoder( printStream ), false );
+        return new ForkingRunListener( new LegacyMasterProcessChannelEncoder( newChannel( printStream ) ), false );
     }
 
     private class StandardTestRun
@@ -326,15 +376,15 @@ public class ForkingRunListenerTest
             return createForkingRunListener();
         }
 
-        public void clientReceiveContent()
-            throws ReporterException, IOException
+        public void clientReceiveContent() throws Exception
         {
             TestSetMockReporterFactory providerReporterFactory = new TestSetMockReporterFactory();
-            NullConsoleLogger log = new NullConsoleLogger();
-            final ForkClient forkStreamClient =
-                    new ForkClient( providerReporterFactory, new MockNotifiableTestStream(), log, null, 1 );
-            forkStreamClient.consumeMultiLineContent( content.toString( ) );
-            reporter = (MockReporter) forkStreamClient.getReporter();
+            ForkClient handler = new ForkClient( providerReporterFactory, new MockNotifiableTestStream(), 1 );
+            for ( Event e : streamToEvent( content.toByteArray() ) )
+            {
+                handler.handleEvent( e );
+            }
+            reporter = (MockReporter) handler.getReporter();
         }
 
         public String getFirstEvent()
@@ -347,8 +397,7 @@ public class ForkingRunListenerTest
             return (ReportEntry) reporter.getData().get( 0 );
         }
 
-        private void assertExpected( String actionCode, ReportEntry expected )
-            throws IOException, ReporterException
+        private void assertExpected( String actionCode, ReportEntry expected ) throws Exception
         {
             clientReceiveContent();
             assertEquals( actionCode, getFirstEvent() );
@@ -368,8 +417,7 @@ public class ForkingRunListenerTest
             }
         }
 
-        private void assertExpected( String actionCode, String expected )
-            throws IOException, ReporterException
+        private void assertExpected( String actionCode, String expected ) throws Exception
         {
             clientReceiveContent();
             assertEquals( actionCode, getFirstEvent() );
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 d90a128..dc435b5 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
@@ -34,15 +34,11 @@ public class MainClass
         }
         else
         {
-            System.out.println( ":maven:surefire:std:out:bye" );
-            if ( System.in.read() == 0
-                && System.in.read() == 0
-                && System.in.read() == 0
-                && System.in.read() == 5
-                && System.in.read() == 0
-                && System.in.read() == 0
-                && System.in.read() == 0
-                && System.in.read() == 0 )
+            System.out.println( ":maven-surefire-event:bye:" );
+            String byeAck = ":maven-surefire-command:bye-ack:";
+            byte[] cmd = new byte[byeAck.length()];
+            int len = System.in.read( cmd );
+            if ( len != -1 && new String( cmd, 0, len ).equals( byeAck ) )
             {
                 System.exit( 0 );
             }
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/ModularClasspathForkConfigurationTest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/ModularClasspathForkConfigurationTest.java
index 492c5c0..cfa7dce 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/ModularClasspathForkConfigurationTest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/ModularClasspathForkConfigurationTest.java
@@ -27,6 +27,7 @@ import org.apache.maven.surefire.booter.ForkedBooter;
 import org.apache.maven.surefire.booter.ModularClasspath;
 import org.apache.maven.surefire.booter.ModularClasspathConfiguration;
 import org.apache.maven.surefire.booter.StartupConfiguration;
+import org.apache.maven.surefire.extensions.ForkNodeFactory;
 import org.junit.Test;
 
 import java.io.File;
@@ -43,9 +44,10 @@ import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.nio.file.Files.readAllLines;
 import static java.util.Arrays.asList;
 import static java.util.Collections.singleton;
-import static org.apache.maven.surefire.shared.utils.StringUtils.replace;
 import static org.apache.maven.surefire.booter.Classpath.emptyClasspath;
+import static org.apache.maven.surefire.shared.utils.StringUtils.replace;
 import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
 
 /**
  * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
@@ -66,7 +68,7 @@ public class ModularClasspathForkConfigurationTest
         ModularClasspathForkConfiguration config = new ModularClasspathForkConfiguration( booter, tmp, "", pwd,
                 new Properties(), "",
                 Collections.<String, String>emptyMap(), new String[0], true, 1, true,
-                new Platform(), new NullConsoleLogger() );
+                new Platform(), new NullConsoleLogger(), mock( ForkNodeFactory.class ) );
 
         File patchFile = new File( "target" + separatorChar + "test-classes" );
         File descriptor = new File( tmp, "module-info.class" );
@@ -141,8 +143,8 @@ public class ModularClasspathForkConfigurationTest
                 new ModularClasspathConfiguration( modularClasspath, testClasspathUrls, surefireClasspathUrls,
                         emptyClasspath(), true, true );
         ClassLoaderConfiguration clc = new ClassLoaderConfiguration( true, true );
-        StartupConfiguration startupConfiguration =
-                new StartupConfiguration( "JUnitCoreProvider", modularClasspathConfiguration, clc, true, true, null );
+        StartupConfiguration startupConfiguration = new StartupConfiguration( "JUnitCoreProvider",
+            modularClasspathConfiguration, clc, null );
         OutputStreamFlushableCommandline cli = new OutputStreamFlushableCommandline();
         config.resolveClasspath( cli, ForkedBooter.class.getName(), startupConfiguration,
                 createTempFile( "surefire", "surefire-reports" ) );
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 bbc85d4..2aab4c2 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
@@ -21,22 +21,24 @@ package org.apache.maven.plugin.surefire.booterclient.lazytestprovider;
 
 import org.apache.maven.surefire.booter.Command;
 import org.apache.maven.surefire.booter.MasterProcessCommand;
+import org.apache.maven.surefire.booter.spi.LegacyMasterProcessChannelDecoder;
+import org.apache.maven.surefire.providerapi.MasterProcessChannelDecoder;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 
-import java.io.DataInputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.util.Iterator;
 import java.util.NoSuchElementException;
 
+import static java.nio.channels.Channels.newChannel;
 import static org.apache.maven.plugin.surefire.booterclient.lazytestprovider.TestLessInputStream.TestLessInputStreamBuilder;
-import static org.apache.maven.surefire.booter.Command.NOOP;
 import static org.apache.maven.surefire.booter.Command.SKIP_SINCE_NEXT_TEST;
 import static org.apache.maven.surefire.booter.MasterProcessCommand.SHUTDOWN;
-import static org.apache.maven.surefire.booter.MasterProcessCommand.decode;
 import static org.apache.maven.surefire.booter.Shutdown.EXIT;
 import static org.apache.maven.surefire.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;
@@ -87,7 +89,7 @@ public class TestLessInputStreamBuilderTest
         assertThat( is.availablePermits(), is( 1 ) );
         is.beforeNextCommand();
         assertThat( is.availablePermits(), is( 0 ) );
-        assertThat( is.nextCommand(), is( NOOP ) );
+        assertThat( is.nextCommand(), is( Command.NOOP ) );
         assertThat( is.availablePermits(), is( 0 ) );
         e.expect( NoSuchElementException.class );
         is.nextCommand();
@@ -105,7 +107,7 @@ public class TestLessInputStreamBuilderTest
         assertThat( is.availablePermits(), is( 2 ) );
         is.beforeNextCommand();
         assertThat( is.availablePermits(), is( 1 ) );
-        assertThat( is.nextCommand(), is( NOOP ) );
+        assertThat( is.nextCommand(), is( Command.NOOP ) );
         assertThat( is.availablePermits(), is( 1 ) );
         builder.getCachableCommands().skipSinceNextTest();
         assertThat( is.availablePermits(), is( 1 ) );
@@ -123,7 +125,7 @@ public class TestLessInputStreamBuilderTest
         builder.getCachableCommands().shutdown( EXIT );
         assertThat( is.availablePermits(), is( 2 ) );
         is.beforeNextCommand();
-        assertThat( is.nextCommand(), is( NOOP ) );
+        assertThat( is.nextCommand(), is( Command.NOOP ) );
         assertThat( is.availablePermits(), is( 1 ) );
         is.beforeNextCommand();
         assertThat( is.nextCommand().getCommandType(), is( SHUTDOWN ) );
@@ -137,15 +139,47 @@ public class TestLessInputStreamBuilderTest
             throws IOException
     {
         TestLessInputStreamBuilder builder = new TestLessInputStreamBuilder();
-        TestLessInputStream pluginIs = builder.build();
+        final TestLessInputStream pluginIs = builder.build();
+        InputStream is = new InputStream()
+        {
+            private byte[] buffer;
+            private int idx;
+
+            @Override
+            public int read() throws IOException
+            {
+                if ( buffer == null )
+                {
+                    idx = 0;
+                    Command cmd = pluginIs.readNextCommand();
+                    if ( cmd != null )
+                    {
+                        MasterProcessCommand cmdType = cmd.getCommandType();
+                        buffer = cmdType.hasDataType() ? encode( cmdType, cmd.getData() ) : encode( cmdType );
+                    }
+                }
+
+                if ( buffer != null )
+                {
+                    byte b = buffer[idx++];
+                    if ( idx == buffer.length )
+                    {
+                        buffer = null;
+                        idx = 0;
+                    }
+                    return b;
+                }
+                throw new IOException();
+            }
+        };
+        MasterProcessChannelDecoder decoder = new LegacyMasterProcessChannelDecoder( newChannel( is ) );
         builder.getImmediateCommands().shutdown( KILL );
         builder.getImmediateCommands().noop();
-        DataInputStream is = new DataInputStream( pluginIs );
-        Command bye = decode( is );
+        Command bye = decoder.decode();
         assertThat( bye, is( notNullValue() ) );
         assertThat( bye.getCommandType(), is( SHUTDOWN ) );
         assertThat( bye.getData(), is( KILL.name() ) );
-        Command noop = decode( is );
+        Command noop = decoder.decode();
         assertThat( noop, is( notNullValue() ) );
         assertThat( noop.getCommandType(), is( MasterProcessCommand.NOOP ) );
     }
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 21bc663..1d348ce 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,11 +20,14 @@ package org.apache.maven.plugin.surefire.booterclient.lazytestprovider;
  */
 
 import org.apache.maven.surefire.booter.Command;
-import org.apache.maven.surefire.booter.MasterProcessCommand;
+import org.apache.maven.surefire.booter.spi.LegacyMasterProcessChannelDecoder;
+import org.apache.maven.plugin.surefire.extensions.StreamFeeder;
+import org.apache.maven.surefire.providerapi.MasterProcessChannelDecoder;
 import org.junit.Test;
 
-import java.io.DataInputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.lang.Thread.State;
 import java.util.ArrayDeque;
 import java.util.Queue;
 import java.util.concurrent.Callable;
@@ -32,11 +35,15 @@ import java.util.concurrent.ConcurrentLinkedQueue;
 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 org.apache.maven.surefire.booter.MasterProcessCommand.BYE_ACK;
-import static org.apache.maven.surefire.booter.MasterProcessCommand.decode;
+import static org.apache.maven.surefire.booter.MasterProcessCommand.NOOP;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.Assert.assertTrue;
 
 /**
  * Asserts that this stream properly reads bytes from queue.
@@ -46,14 +53,15 @@ import static org.hamcrest.Matchers.notNullValue;
  */
 public class TestProvidingInputStreamTest
 {
+    private static final int WAIT_LOOPS = 100;
     @Test
-    public void closedStreamShouldReturnEndOfStream()
+    public void closedStreamShouldReturnNullAsEndOfStream()
         throws IOException
     {
         Queue<String> commands = new ArrayDeque<>();
         TestProvidingInputStream is = new TestProvidingInputStream( commands );
         is.close();
-        assertThat( is.read(), is( -1 ) );
+        assertThat( is.readNextCommand(), is( nullValue() ) );
     }
 
     @Test
@@ -63,22 +71,22 @@ public class TestProvidingInputStreamTest
         Queue<String> commands = new ArrayDeque<>();
         final TestProvidingInputStream is = new TestProvidingInputStream( commands );
         final Thread streamThread = Thread.currentThread();
-        FutureTask<Thread.State> futureTask = new FutureTask<>( new Callable<Thread.State>()
+        FutureTask<State> futureTask = new FutureTask<>( new Callable<State>()
         {
             @Override
-            public Thread.State call()
+            public State call()
             {
-                sleep( 1000 );
-                Thread.State state = streamThread.getState();
+                sleep( 1000L );
+                State state = streamThread.getState();
                 is.close();
                 return state;
             }
         } );
         Thread assertionThread = new Thread( futureTask );
         assertionThread.start();
-        assertThat( is.read(), is( -1 ) );
-        Thread.State state = futureTask.get();
-        assertThat( state, is( Thread.State.WAITING ) );
+        assertThat( is.readNextCommand(), is( nullValue() ) );
+        State state = futureTask.get();
+        assertThat( state, is( State.WAITING ) );
     }
 
     @Test
@@ -96,16 +104,23 @@ public class TestProvidingInputStreamTest
                 is.provideNewTest();
             }
         } ).start();
-        assertThat( is.read(), is( 0 ) );
-        assertThat( is.read(), is( 0 ) );
-        assertThat( is.read(), is( 0 ) );
-        assertThat( is.read(), is( 1 ) );
-        assertThat( is.read(), is( 0 ) );
-        assertThat( is.read(), is( 0 ) );
-        assertThat( is.read(), is( 0 ) );
-        assertThat( is.read(), is( 0 ) );
+
+        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:" ) );
+
+        boolean emptyStream = isInputStreamEmpty( is );
+
         is.close();
-        assertThat( is.read(), is( -1 ) );
+        assertTrue( emptyStream );
+        assertThat( is.readNextCommand(), is( nullValue() ) );
     }
 
     @Test
@@ -123,34 +138,55 @@ public class TestProvidingInputStreamTest
                 is.provideNewTest();
             }
         } ).start();
-        assertThat( is.read(), is( 0 ) );
-        assertThat( is.read(), is( 0 ) );
-        assertThat( is.read(), is( 0 ) );
-        assertThat( is.read(), is( 0 ) );
-        assertThat( is.read(), is( 0 ) );
-        assertThat( is.read(), is( 0 ) );
-        assertThat( is.read(), is( 0 ) );
-        assertThat( is.read(), is( 4 ) );
-        assertThat( is.read(), is( (int) 'T' ) );
-        assertThat( is.read(), is( (int) 'e' ) );
-        assertThat( is.read(), is( (int) 's' ) );
-        assertThat( is.read(), is( (int) 't' ) );
+
+        Command cmd = is.readNextCommand();
+        assertThat( cmd.getData(), is( "Test" ) );
+
+        is.close();
     }
 
     @Test
     public void shouldDecodeTwoCommands()
             throws IOException
     {
-        TestProvidingInputStream pluginIs = new TestProvidingInputStream( new ConcurrentLinkedQueue<String>() );
+        final TestProvidingInputStream pluginIs = new TestProvidingInputStream( new ConcurrentLinkedQueue<String>() );
+        InputStream is = new InputStream()
+        {
+            private byte[] buffer;
+            private int idx;
+
+            @Override
+            public int read() throws IOException
+            {
+                if ( buffer == null )
+                {
+                    idx = 0;
+                    Command cmd = pluginIs.readNextCommand();
+                    buffer = cmd == null ? null : StreamFeeder.encode( cmd.getCommandType() );
+                }
+
+                if ( buffer != null )
+                {
+                    byte b = buffer[idx++];
+                    if ( idx == buffer.length )
+                    {
+                        buffer = null;
+                        idx = 0;
+                    }
+                    return b;
+                }
+                throw new IOException();
+            }
+        };
+        MasterProcessChannelDecoder decoder = new LegacyMasterProcessChannelDecoder( newChannel( is ) );
         pluginIs.acknowledgeByeEventReceived();
         pluginIs.noop();
-        DataInputStream is = new DataInputStream( pluginIs );
-        Command bye = decode( is );
+        Command bye = decoder.decode();
         assertThat( bye, is( notNullValue() ) );
         assertThat( bye.getCommandType(), is( BYE_ACK ) );
-        Command noop = decode( is );
+        Command noop = decoder.decode();
         assertThat( noop, is( notNullValue() ) );
-        assertThat( noop.getCommandType(), is( MasterProcessCommand.NOOP ) );
+        assertThat( noop.getCommandType(), is( NOOP ) );
     }
 
     private static void sleep( long millis )
@@ -164,4 +200,44 @@ public class TestProvidingInputStreamTest
             // do nothing
         }
     }
+
+    /**
+     * Waiting (max of 20 seconds)
+     * @param is examined stream
+     * @return {@code true} if the {@link InputStream#read()} is waiting for a new byte.
+     */
+    private static boolean isInputStreamEmpty( final TestProvidingInputStream is )
+    {
+        Thread t = new Thread( new Runnable()
+        {
+            @Override
+            public void run()
+            {
+                try
+                {
+                    is.readNextCommand();
+                }
+                catch ( IOException e )
+                {
+                    Throwable cause = e.getCause();
+                    Throwable err = cause == null ? e : cause;
+                    if ( !( err instanceof InterruptedException ) )
+                    {
+                        System.err.println( err.toString() );
+                    }
+                }
+            }
+        } );
+        t.start();
+        State state;
+        int loops = 0;
+        do
+        {
+            sleep( 100L );
+            state = t.getState();
+        }
+        while ( state == State.NEW && loops++ < WAIT_LOOPS );
+        t.interrupt();
+        return state == State.WAITING || state == State.TIMED_WAITING;
+    }
 }
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 09230bb..60f6872 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
@@ -21,47 +21,79 @@ package org.apache.maven.plugin.surefire.booterclient.output;
 
 import org.apache.maven.plugin.surefire.booterclient.MockReporter;
 import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.NotifiableTestStream;
+import org.apache.maven.plugin.surefire.extensions.EventConsumerThread;
 import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
 import org.apache.maven.plugin.surefire.report.DefaultReporterFactory;
 import org.apache.maven.surefire.booter.Shutdown;
+import org.apache.maven.surefire.eventapi.ConsoleDebugEvent;
+import org.apache.maven.surefire.eventapi.ConsoleErrorEvent;
+import org.apache.maven.surefire.eventapi.ConsoleInfoEvent;
+import org.apache.maven.surefire.eventapi.ConsoleWarningEvent;
+import org.apache.maven.surefire.eventapi.ControlByeEvent;
+import org.apache.maven.surefire.eventapi.ControlNextTestEvent;
+import org.apache.maven.surefire.eventapi.ControlStopOnNextTestEvent;
+import org.apache.maven.surefire.eventapi.Event;
+import org.apache.maven.surefire.eventapi.StandardStreamErrEvent;
+import org.apache.maven.surefire.eventapi.StandardStreamErrWithNewLineEvent;
+import org.apache.maven.surefire.eventapi.StandardStreamOutEvent;
+import org.apache.maven.surefire.eventapi.StandardStreamOutWithNewLineEvent;
+import org.apache.maven.surefire.eventapi.SystemPropertyEvent;
+import org.apache.maven.surefire.eventapi.TestAssumptionFailureEvent;
+import org.apache.maven.surefire.eventapi.TestErrorEvent;
+import org.apache.maven.surefire.eventapi.TestFailedEvent;
+import org.apache.maven.surefire.eventapi.TestSkippedEvent;
+import org.apache.maven.surefire.eventapi.TestStartingEvent;
+import org.apache.maven.surefire.eventapi.TestSucceededEvent;
+import org.apache.maven.surefire.eventapi.TestsetCompletedEvent;
+import org.apache.maven.surefire.eventapi.TestsetStartingEvent;
+import org.apache.maven.surefire.extensions.EventHandler;
+import org.apache.maven.surefire.extensions.util.CountdownCloseable;
 import org.apache.maven.surefire.report.ReportEntry;
 import org.apache.maven.surefire.report.SafeThrowable;
+import org.apache.maven.surefire.report.SimpleReportEntry;
 import org.apache.maven.surefire.report.StackTraceWriter;
+import org.apache.maven.surefire.report.TestSetReportEntry;
 import org.junit.Test;
 
+import javax.annotation.Nonnull;
+import java.io.ByteArrayInputStream;
+import java.io.Closeable;
 import java.io.File;
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.util.concurrent.atomic.AtomicBoolean;
+import java.nio.channels.ReadableByteChannel;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
 
+import static java.nio.channels.Channels.newChannel;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.Arrays.copyOfRange;
 import static org.apache.commons.codec.binary.Base64.encodeBase64String;
 import static org.apache.maven.plugin.surefire.booterclient.MockReporter.CONSOLE_DEBUG;
 import static org.apache.maven.plugin.surefire.booterclient.MockReporter.CONSOLE_ERR;
-import static org.apache.maven.plugin.surefire.booterclient.MockReporter.CONSOLE_WARN;
 import static org.apache.maven.plugin.surefire.booterclient.MockReporter.CONSOLE_INFO;
+import static org.apache.maven.plugin.surefire.booterclient.MockReporter.CONSOLE_WARN;
 import static org.apache.maven.plugin.surefire.booterclient.MockReporter.SET_COMPLETED;
 import static org.apache.maven.plugin.surefire.booterclient.MockReporter.SET_STARTING;
-import static org.apache.maven.plugin.surefire.booterclient.MockReporter.STDOUT;
 import static org.apache.maven.plugin.surefire.booterclient.MockReporter.STDERR;
+import static org.apache.maven.plugin.surefire.booterclient.MockReporter.STDOUT;
 import static org.apache.maven.plugin.surefire.booterclient.MockReporter.TEST_ASSUMPTION_FAIL;
 import static org.apache.maven.plugin.surefire.booterclient.MockReporter.TEST_ERROR;
 import static org.apache.maven.plugin.surefire.booterclient.MockReporter.TEST_FAILED;
-import static org.apache.maven.plugin.surefire.booterclient.MockReporter.TEST_STARTING;
 import static org.apache.maven.plugin.surefire.booterclient.MockReporter.TEST_SKIPPED;
+import static org.apache.maven.plugin.surefire.booterclient.MockReporter.TEST_STARTING;
 import static org.apache.maven.plugin.surefire.booterclient.MockReporter.TEST_SUCCEEDED;
+import static org.apache.maven.surefire.booter.ForkedProcessEventType.BOOTERCODE_BYE;
+import static org.apache.maven.surefire.booter.ForkedProcessEventType.BOOTERCODE_CONSOLE_ERROR;
+import static org.apache.maven.surefire.report.RunMode.NORMAL_RUN;
 import static org.fest.assertions.Assertions.assertThat;
 import static org.fest.assertions.MapAssert.entry;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.startsWith;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
 /**
@@ -74,204 +106,74 @@ public class ForkClientTest
 {
     private static final int ELAPSED_TIME = 102;
 
-    @Test
-    public void shouldNotFailOnEmptyInput1()
+    @Test( expected = NullPointerException.class )
+    public void shouldFailOnNPE()
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
         DefaultReporterFactory factory = mock( DefaultReporterFactory.class );
         when( factory.getReportsDirectory() )
                 .thenReturn( new File( target, "surefire-reports" ) );
-        AtomicBoolean printedErrorStream = new AtomicBoolean();
-        ConsoleLogger logger = mock( ConsoleLogger.class );
-        ForkClient client = new ForkClient( factory, null, logger, printedErrorStream, 0 );
-        client.consumeLine( null );
-        assertThat( client.isSaidGoodBye() )
-                .isFalse();
-        assertThat( client.getErrorInFork() )
-                .isNull();
-        assertThat( client.isErrorInFork() )
-                .isFalse();
-        assertThat( client.hadTimeout() )
-                .isFalse();
-        assertThat( client.hasTestsInProgress() )
-                .isFalse();
-        assertThat( client.testsInProgress() )
-                .isEmpty();
-        assertThat( client.getTestVmSystemProperties() )
-                .isEmpty();
+        ForkClient client = new ForkClient( factory, null, 0 );
+        client.handleEvent( null );
     }
 
     @Test
-    public void shouldNotFailOnEmptyInput2()
+    public void shouldLogJvmMessage() throws Exception
     {
-        String cwd = System.getProperty( "user.dir" );
-        File target = new File( cwd, "target" );
-        DefaultReporterFactory factory = mock( DefaultReporterFactory.class );
-        when( factory.getReportsDirectory() )
-                .thenReturn( new File( target, "surefire-reports" ) );
-        AtomicBoolean printedErrorStream = new AtomicBoolean();
+        String nativeStream = "Listening for transport dt_socket at address: bla";
+        EH eventHandler = new EH();
+        CountdownCloseable countdown = new CountdownCloseable( mock( Closeable.class ), 1 );
         ConsoleLogger logger = mock( ConsoleLogger.class );
-        ForkClient client = new ForkClient( factory, null, logger, printedErrorStream, 0 );
-        client.consumeLine( "   " );
-        assertThat( client.isSaidGoodBye() )
-                .isFalse();
-        assertThat( client.getErrorInFork() )
-                .isNull();
-        assertThat( client.isErrorInFork() )
-                .isFalse();
-        assertThat( client.hadTimeout() )
-                .isFalse();
-        assertThat( client.hasTestsInProgress() )
-                .isFalse();
-        assertThat( client.testsInProgress() )
-                .isEmpty();
-        assertThat( client.getTestVmSystemProperties() )
-                .isEmpty();
-    }
+        ReadableByteChannel channel = newChannel( new ByteArrayInputStream( nativeStream.getBytes() ) );
+        try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, logger ) )
+        {
+            t.start();
 
-    @Test
-    public void shouldNotFailOnEmptyInput3()
-            throws IOException
-    {
-        String cwd = System.getProperty( "user.dir" );
-        File target = new File( cwd, "target" );
-        DefaultReporterFactory factory = mock( DefaultReporterFactory.class );
-        when( factory.getReportsDirectory() )
-                .thenReturn( new File( target, "surefire-reports" ) );
-        AtomicBoolean printedErrorStream = new AtomicBoolean();
-        ConsoleLogger logger = mock( ConsoleLogger.class );
-        ForkClient client = new ForkClient( factory, null, logger, printedErrorStream, 0 );
-        client.consumeMultiLineContent( null );
-        assertThat( client.isSaidGoodBye() )
-                .isFalse();
-        assertThat( client.getErrorInFork() )
-                .isNull();
-        assertThat( client.isErrorInFork() )
-                .isFalse();
-        assertThat( client.hadTimeout() )
-                .isFalse();
-        assertThat( client.hasTestsInProgress() )
-                .isFalse();
-        assertThat( client.testsInProgress() )
-                .isEmpty();
-        assertThat( client.getTestVmSystemProperties() )
-                .isEmpty();
-    }
+            countdown.awaitClosed();
 
-    @Test
-    public void shouldNotFailOnEmptyInput4()
-            throws IOException
-    {
-        String cwd = System.getProperty( "user.dir" );
-        File target = new File( cwd, "target" );
-        DefaultReporterFactory factory = mock( DefaultReporterFactory.class );
-        when( factory.getReportsDirectory() )
-                .thenReturn( new File( target, "surefire-reports" ) );
-        AtomicBoolean printedErrorStream = new AtomicBoolean();
-        ConsoleLogger logger = mock( ConsoleLogger.class );
-        when( logger.isDebugEnabled() )
-                .thenReturn( true );
-        ForkClient client = new ForkClient( factory, null, logger, printedErrorStream, 0 );
-        client.consumeMultiLineContent( "   " );
-        verify( logger )
-                .isDebugEnabled();
-        verify( logger )
-                .warning( startsWith( "Corrupted STDOUT by directly writing to native stream in forked JVM 0. "
-                        + "See FAQ web page and the dump file " ) );
-        verify( logger )
-                .debug( "   " );
-        verifyNoMoreInteractions( logger );
-        assertThat( client.isSaidGoodBye() )
-                .isFalse();
-        assertThat( client.getErrorInFork() )
-                .isNull();
-        assertThat( client.isErrorInFork() )
-                .isFalse();
-        assertThat( client.hadTimeout() )
-                .isFalse();
-        assertThat( client.hasTestsInProgress() )
-                .isFalse();
-        assertThat( client.testsInProgress() )
-                .isEmpty();
-        assertThat( client.getTestVmSystemProperties() )
-                .isEmpty();
-    }
+            verify( logger )
+                .info( "Listening for transport dt_socket at address: bla" );
+        }
+
+        assertThat( eventHandler.sizeOfEventCache() )
+            .isEqualTo( 0 );
 
-    @Test
-    public void shouldNotFailOnEmptyInput5()
-            throws IOException
-    {
-        String cwd = System.getProperty( "user.dir" );
-        File target = new File( cwd, "target" );
-        DefaultReporterFactory factory = mock( DefaultReporterFactory.class );
-        when( factory.getReportsDirectory() )
-                .thenReturn( new File( target, "surefire-reports" ) );
-        AtomicBoolean printedErrorStream = new AtomicBoolean();
-        ConsoleLogger logger = mock( ConsoleLogger.class );
-        when( logger.isDebugEnabled() )
-                .thenReturn( true );
-        ForkClient client = new ForkClient( factory, null, logger, printedErrorStream, 0 );
-        client.consumeMultiLineContent( "Listening for transport dt_socket at address: bla" );
-        verify( logger )
-                .isDebugEnabled();
-        verify( logger )
-                .debug( "Listening for transport dt_socket at address: bla" );
         verifyNoMoreInteractions( logger );
-        assertThat( client.isSaidGoodBye() )
-                .isFalse();
-        assertThat( client.getErrorInFork() )
-                .isNull();
-        assertThat( client.isErrorInFork() )
-                .isFalse();
-        assertThat( client.hadTimeout() )
-                .isFalse();
-        assertThat( client.hasTestsInProgress() )
-                .isFalse();
-        assertThat( client.testsInProgress() )
-                .isEmpty();
-        assertThat( client.getTestVmSystemProperties() )
-                .isEmpty();
     }
 
     @Test
-    public void shouldNotFailOnEmptyInput6()
-            throws IOException
+    public void shouldLogJvmMessageAndProcessEvent() throws Exception
     {
-        String cwd = System.getProperty( "user.dir" );
-        File target = new File( cwd, "target" );
-        DefaultReporterFactory factory = mock( DefaultReporterFactory.class );
-        when( factory.getReportsDirectory() )
-                .thenReturn( new File( target, "surefire-reports" ) );
-        AtomicBoolean printedErrorStream = new AtomicBoolean();
+        String nativeStream = "Listening for transport dt_socket at address: bla\n:maven-surefire-event:bye:\n";
+        EH eventHandler = new EH();
+        CountdownCloseable countdown = new CountdownCloseable( mock( Closeable.class ), 1 );
         ConsoleLogger logger = mock( ConsoleLogger.class );
         when( logger.isDebugEnabled() )
-                .thenReturn( false );
+            .thenReturn( false );
         when( logger.isInfoEnabled() )
-                .thenReturn( true );
-        ForkClient client = new ForkClient( factory, null, logger, printedErrorStream, 0 );
-        client.consumeMultiLineContent( "Listening for transport dt_socket at address: bla" );
-        verify( logger )
-                .isDebugEnabled();
-        verify( logger )
-                .isInfoEnabled();
-        verify( logger )
+            .thenReturn( true );
+        ReadableByteChannel channel = newChannel( new ByteArrayInputStream( nativeStream.getBytes() ) );
+        try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, logger ) )
+        {
+            t.start();
+
+            Event event = eventHandler.pullEvent();
+            assertThat( event.isControlCategory() )
+                .isTrue();
+            assertThat( event.getEventType() )
+                .isEqualTo( BOOTERCODE_BYE );
+
+            verify( logger )
                 .info( "Listening for transport dt_socket at address: bla" );
+
+            countdown.awaitClosed();
+        }
+
+        assertThat( eventHandler.sizeOfEventCache() )
+            .isEqualTo( 0 );
+
         verifyNoMoreInteractions( logger );
-        assertThat( client.isSaidGoodBye() )
-                .isFalse();
-        assertThat( client.getErrorInFork() )
-                .isNull();
-        assertThat( client.isErrorInFork() )
-                .isFalse();
-        assertThat( client.hadTimeout() )
-                .isFalse();
-        assertThat( client.hasTestsInProgress() )
-                .isFalse();
-        assertThat( client.testsInProgress() )
-                .isEmpty();
-        assertThat( client.getTestVmSystemProperties() )
-                .isEmpty();
     }
 
     @Test
@@ -279,7 +181,7 @@ public class ForkClientTest
     {
         NotifiableTestStream notifiableTestStream = mock( NotifiableTestStream.class );
 
-        ForkClient client = new ForkClient( null, notifiableTestStream, null, null, 0 );
+        ForkClient client = new ForkClient( null, notifiableTestStream, 0 );
         client.kill();
 
         verify( notifiableTestStream, times( 1 ) )
@@ -288,7 +190,6 @@ public class ForkClientTest
 
     @Test
     public void shouldAcquireNextTest()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -296,10 +197,8 @@ public class ForkClientTest
         when( factory.getReportsDirectory() )
                 .thenReturn( new File( target, "surefire-reports" ) );
         NotifiableTestStream notifiableTestStream = mock( NotifiableTestStream.class );
-        AtomicBoolean printedErrorStream = new AtomicBoolean();
-        ConsoleLogger logger = mock( ConsoleLogger.class );
-        ForkClient client = new ForkClient( factory, notifiableTestStream, logger, printedErrorStream, 0 );
-        client.consumeMultiLineContent( ":maven:surefire:std:out:next-test\n" );
+        ForkClient client = new ForkClient( factory, notifiableTestStream, 0 );
+        client.handleEvent( new ControlNextTestEvent() );
         verify( notifiableTestStream, times( 1 ) )
                 .provideNewTest();
         verifyNoMoreInteractions( notifiableTestStream );
@@ -322,7 +221,6 @@ public class ForkClientTest
 
     @Test
     public void shouldNotifyWithBye()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -330,11 +228,9 @@ public class ForkClientTest
         when( factory.getReportsDirectory() )
                 .thenReturn( new File( target, "surefire-reports" ) );
         NotifiableTestStream notifiableTestStream = mock( NotifiableTestStream.class );
-        AtomicBoolean printedErrorStream = new AtomicBoolean();
-        ConsoleLogger logger = mock( ConsoleLogger.class );
 
-        ForkClient client = new ForkClient( factory, notifiableTestStream, logger, printedErrorStream, 0 );
-        client.consumeMultiLineContent( ":maven:surefire:std:out:bye\n" );
+        ForkClient client = new ForkClient( factory, notifiableTestStream, 0 );
+        client.handleEvent( new ControlByeEvent() );
         client.kill();
 
         verify( notifiableTestStream, times( 1 ) )
@@ -361,7 +257,6 @@ public class ForkClientTest
 
     @Test
     public void shouldStopOnNextTest()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -369,10 +264,8 @@ public class ForkClientTest
         when( factory.getReportsDirectory() )
                 .thenReturn( new File( target, "surefire-reports" ) );
         NotifiableTestStream notifiableTestStream = mock( NotifiableTestStream.class );
-        AtomicBoolean printedErrorStream = new AtomicBoolean();
-        ConsoleLogger logger = mock( ConsoleLogger.class );
         final boolean[] verified = {false};
-        ForkClient client = new ForkClient( factory, notifiableTestStream, logger, printedErrorStream, 0 )
+        ForkClient client = new ForkClient( factory, notifiableTestStream, 0 )
         {
             @Override
             protected void stopOnNextTest()
@@ -381,7 +274,7 @@ public class ForkClientTest
                 verified[0] = true;
             }
         };
-        client.consumeMultiLineContent( ":maven:surefire:std:out:stop-on-next-test\n" );
+        client.handleEvent( new ControlStopOnNextTestEvent() );
         verifyZeroInteractions( notifiableTestStream );
         verifyZeroInteractions( factory );
         assertThat( verified[0] )
@@ -404,7 +297,6 @@ public class ForkClientTest
 
     @Test
     public void shouldReceiveStdOut()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -415,10 +307,8 @@ public class ForkClientTest
         when( factory.createReporter() )
                 .thenReturn( receiver );
         NotifiableTestStream notifiableTestStream = mock( NotifiableTestStream.class );
-        AtomicBoolean printedErrorStream = new AtomicBoolean();
-        ConsoleLogger logger = mock( ConsoleLogger.class );
-        ForkClient client = new ForkClient( factory, notifiableTestStream, logger, printedErrorStream, 0 );
-        client.consumeMultiLineContent( ":maven:surefire:std:out:std-out-stream:normal-run:UTF-8:bXNn\n" );
+        ForkClient client = new ForkClient( factory, notifiableTestStream, 0 );
+        client.handleEvent( new StandardStreamOutEvent( NORMAL_RUN, "msg" ) );
         verifyZeroInteractions( notifiableTestStream );
         verify( factory, times( 1 ) )
                 .createReporter();
@@ -449,7 +339,6 @@ public class ForkClientTest
 
     @Test
     public void shouldReceiveStdOutNewLine()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -460,10 +349,8 @@ public class ForkClientTest
         when( factory.createReporter() )
                 .thenReturn( receiver );
         NotifiableTestStream notifiableTestStream = mock( NotifiableTestStream.class );
-        AtomicBoolean printedErrorStream = new AtomicBoolean();
-        ConsoleLogger logger = mock( ConsoleLogger.class );
-        ForkClient client = new ForkClient( factory, notifiableTestStream, logger, printedErrorStream, 0 );
-        client.consumeMultiLineContent( ":maven:surefire:std:out:std-out-stream-new-line:normal-run:UTF-8:bXNn\n" );
+        ForkClient client = new ForkClient( factory, notifiableTestStream, 0 );
+        client.handleEvent( new StandardStreamOutWithNewLineEvent( NORMAL_RUN, "msg" ) );
         verifyZeroInteractions( notifiableTestStream );
         verify( factory, times( 1 ) )
                 .createReporter();
@@ -494,7 +381,6 @@ public class ForkClientTest
 
     @Test
     public void shouldReceiveStdErr()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -505,10 +391,8 @@ public class ForkClientTest
         when( factory.createReporter() )
                 .thenReturn( receiver );
         NotifiableTestStream notifiableTestStream = mock( NotifiableTestStream.class );
-        AtomicBoolean printedErrorStream = new AtomicBoolean();
-        ConsoleLogger logger = mock( ConsoleLogger.class );
-        ForkClient client = new ForkClient( factory, notifiableTestStream, logger, printedErrorStream, 0 );
-        client.consumeMultiLineContent( ":maven:surefire:std:out:std-err-stream:normal-run:UTF-8:bXNn\n" );
+        ForkClient client = new ForkClient( factory, notifiableTestStream, 0 );
+        client.handleEvent( new StandardStreamErrEvent( NORMAL_RUN, "msg" ) );
         verifyZeroInteractions( notifiableTestStream );
         verify( factory, times( 1 ) )
                 .createReporter();
@@ -539,7 +423,6 @@ public class ForkClientTest
 
     @Test
     public void shouldReceiveStdErrNewLine()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -550,10 +433,8 @@ public class ForkClientTest
         when( factory.createReporter() )
                 .thenReturn( receiver );
         NotifiableTestStream notifiableTestStream = mock( NotifiableTestStream.class );
-        AtomicBoolean printedErrorStream = new AtomicBoolean();
-        ConsoleLogger logger = mock( ConsoleLogger.class );
-        ForkClient client = new ForkClient( factory, notifiableTestStream, logger, printedErrorStream, 0 );
-        client.consumeMultiLineContent( ":maven:surefire:std:out:std-err-stream-new-line:normal-run:UTF-8:bXNn\n" );
+        ForkClient client = new ForkClient( factory, notifiableTestStream, 0 );
+        client.handleEvent( new StandardStreamErrWithNewLineEvent( NORMAL_RUN, "msg" ) );
         verifyZeroInteractions( notifiableTestStream );
         verify( factory, times( 1 ) )
                 .createReporter();
@@ -584,28 +465,22 @@ public class ForkClientTest
 
     @Test
     public void shouldLogConsoleError()
-            throws IOException
     {
-        String cwd = System.getProperty( "user.dir" );
-        File target = new File( cwd, "target" );
         DefaultReporterFactory factory = mock( DefaultReporterFactory.class );
-        when( factory.getReportsDirectory() )
-                .thenReturn( new File( target, "surefire-reports" ) );
         MockReporter receiver = new MockReporter();
         when( factory.createReporter() )
                 .thenReturn( receiver );
         NotifiableTestStream notifiableTestStream = mock( NotifiableTestStream.class );
-        AtomicBoolean printedErrorStream = new AtomicBoolean();
-        ConsoleLogger logger = mock( ConsoleLogger.class );
-        ForkClient client = new ForkClient( factory, notifiableTestStream, logger, printedErrorStream, 0 );
-        client.consumeMultiLineContent( ":maven:surefire:std:out:console-error-log:UTF-8:"
-                + encodeBase64String( "Listening for transport dt_socket at address:".getBytes( UTF_8 ) )
-                + ":-:-:-" );
+        ForkClient client = new ForkClient( factory, notifiableTestStream, 0 );
+        StackTraceWriter stackTrace =
+            new DeserializedStacktraceWriter( "Listening for transport dt_socket at address: 5005", null, null );
+        Event event = new ConsoleErrorEvent( stackTrace );
+        client.handleEvent( event );
         verifyZeroInteractions( notifiableTestStream );
         verify( factory, times( 1 ) )
                 .createReporter();
         verify( factory, times( 1 ) )
-                .getReportsDirectory();
+            .getReportsDirectory();
         verifyNoMoreInteractions( factory );
         assertThat( client.getReporter() )
                 .isNotNull();
@@ -616,7 +491,7 @@ public class ForkClientTest
         assertThat( receiver.getData() )
                 .isNotEmpty();
         assertThat( receiver.getData() )
-                .contains( "Listening for transport dt_socket at address:" );
+                .contains( "Listening for transport dt_socket at address: 5005" );
         assertThat( client.isSaidGoodBye() )
                 .isFalse();
         assertThat( client.isErrorInFork() )
@@ -634,66 +509,51 @@ public class ForkClientTest
     }
 
     @Test
-    public void shouldLogConsoleErrorWithStackTrace()
-            throws IOException
+    public void shouldLogConsoleErrorWithStackTrace() throws Exception
     {
-        String cwd = System.getProperty( "user.dir" );
-        File target = new File( cwd, "target" );
-        DefaultReporterFactory factory = mock( DefaultReporterFactory.class );
-        when( factory.getReportsDirectory() )
-                .thenReturn( new File( target, "surefire-reports" ) );
-        MockReporter receiver = new MockReporter();
-        when( factory.createReporter() )
-                .thenReturn( receiver );
-        NotifiableTestStream notifiableTestStream = mock( NotifiableTestStream.class );
-        AtomicBoolean printedErrorStream = new AtomicBoolean();
+        String nativeStream = ":maven-surefire-event:console-error-log:UTF-8"
+            + ":" + encodeBase64String( "Listening for transport dt_socket at address: 5005".getBytes( UTF_8 ) )
+            + ":" + encodeBase64String( "s1".getBytes( UTF_8 ) )
+            + ":" + encodeBase64String( "s2".getBytes( UTF_8 ) ) + ":";
+        EH eventHandler = new EH();
+        CountdownCloseable countdown = new CountdownCloseable( mock( Closeable.class ), 1 );
         ConsoleLogger logger = mock( ConsoleLogger.class );
-        ForkClient client = new ForkClient( factory, notifiableTestStream, logger, printedErrorStream, 0 );
-        client.consumeMultiLineContent( ":maven:surefire:std:out:console-error-log:UTF-8"
-                + ":" + encodeBase64String( "Listening for transport dt_socket at address:".getBytes( UTF_8 ) )
-                + ":" + encodeBase64String( "s1".getBytes( UTF_8 ) )
-                + ":" + encodeBase64String( "s2".getBytes( UTF_8 ) ) );
-        verifyZeroInteractions( notifiableTestStream );
-        verify( factory, times( 1 ) )
-                .createReporter();
-        verify( factory, times( 1 ) )
-                .getReportsDirectory();
-        verifyNoMoreInteractions( factory );
-        assertThat( client.getReporter() )
-                .isNotNull();
-        assertThat( receiver.getEvents() )
-                .isNotEmpty();
-        assertThat( receiver.getEvents() )
-                .contains( CONSOLE_ERR );
-        assertThat( receiver.getData() )
-                .isNotEmpty();
-        assertThat( receiver.getData() )
-                .contains( "Listening for transport dt_socket at address:" );
-        assertThat( client.isSaidGoodBye() )
-                .isFalse();
-        assertThat( client.isErrorInFork() )
+        ReadableByteChannel channel = newChannel( new ByteArrayInputStream( nativeStream.getBytes() ) );
+        try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, logger ) )
+        {
+            t.start();
+
+            Event event = eventHandler.pullEvent();
+            assertThat( event.isConsoleErrorCategory() )
                 .isTrue();
-        assertThat( client.getErrorInFork() )
+            assertThat( event.isConsoleCategory() )
+                .isTrue();
+            assertThat( event.getEventType() )
+                .isEqualTo( BOOTERCODE_CONSOLE_ERROR );
+
+            ConsoleErrorEvent consoleEvent = (ConsoleErrorEvent) event;
+            assertThat( consoleEvent.getStackTraceWriter() )
                 .isNotNull();
-        assertThat( client.getErrorInFork().getThrowable().getLocalizedMessage() )
-                .isEqualTo( "Listening for transport dt_socket at address:" );
-        assertThat( client.getErrorInFork().smartTrimmedStackTrace() )
+            assertThat( consoleEvent.getStackTraceWriter().getThrowable().getMessage() )
+                .isEqualTo( "Listening for transport dt_socket at address: 5005" );
+            assertThat( consoleEvent.getStackTraceWriter().smartTrimmedStackTrace() )
                 .isEqualTo( "s1" );
-        assertThat( client.getErrorInFork().writeTrimmedTraceToString() )
+            assertThat( consoleEvent.getStackTraceWriter().writeTraceToString() )
                 .isEqualTo( "s2" );
-        assertThat( client.hadTimeout() )
-                .isFalse();
-        assertThat( client.hasTestsInProgress() )
-                .isFalse();
-        assertThat( client.testsInProgress() )
-                .isEmpty();
-        assertThat( client.getTestVmSystemProperties() )
-                .isEmpty();
+
+            countdown.awaitClosed();
+
+            verifyZeroInteractions( logger );
+        }
+
+        assertThat( eventHandler.sizeOfEventCache() )
+            .isEqualTo( 0 );
+
+        verifyNoMoreInteractions( logger );
     }
 
     @Test
     public void shouldLogConsoleWarning()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -704,13 +564,8 @@ public class ForkClientTest
         when( factory.createReporter() )
                 .thenReturn( receiver );
         NotifiableTestStream notifiableTestStream = mock( NotifiableTestStream.class );
-        AtomicBoolean printedErrorStream = new AtomicBoolean();
-        ConsoleLogger logger = mock( ConsoleLogger.class );
-        when( logger.isWarnEnabled() )
-                .thenReturn( true );
-        ForkClient client = new ForkClient( factory, notifiableTestStream, logger, printedErrorStream, 0 );
-        client.consumeMultiLineContent( ":maven:surefire:std:out:console-warning-log:UTF-8:"
-                + encodeBase64String( "s1".getBytes( UTF_8 ) ) );
+        ForkClient client = new ForkClient( factory, notifiableTestStream, 0 );
+        client.handleEvent( new ConsoleWarningEvent( "s1" ) );
         verifyZeroInteractions( notifiableTestStream );
         verify( factory, times( 1 ) )
                 .createReporter();
@@ -741,7 +596,6 @@ public class ForkClientTest
 
     @Test
     public void shouldLogConsoleDebug()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -752,13 +606,8 @@ public class ForkClientTest
         when( factory.createReporter() )
                 .thenReturn( receiver );
         NotifiableTestStream notifiableTestStream = mock( NotifiableTestStream.class );
-        AtomicBoolean printedErrorStream = new AtomicBoolean();
-        ConsoleLogger logger = mock( ConsoleLogger.class );
-        when( logger.isDebugEnabled() )
-                .thenReturn( true );
-        ForkClient client = new ForkClient( factory, notifiableTestStream, logger, printedErrorStream, 0 );
-        client.consumeMultiLineContent( ":maven:surefire:std:out:console-debug-log:UTF-8:"
-                + encodeBase64String( "s1".getBytes( UTF_8 ) ) );
+        ForkClient client = new ForkClient( factory, notifiableTestStream, 0 );
+        client.handleEvent( new ConsoleDebugEvent( "s1" ) );
         verifyZeroInteractions( notifiableTestStream );
         verify( factory, times( 1 ) )
                 .createReporter();
@@ -789,7 +638,6 @@ public class ForkClientTest
 
     @Test
     public void shouldLogConsoleInfo()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -800,11 +648,8 @@ public class ForkClientTest
         when( factory.createReporter() )
                 .thenReturn( receiver );
         NotifiableTestStream notifiableTestStream = mock( NotifiableTestStream.class );
-        AtomicBoolean printedErrorStream = new AtomicBoolean();
-        ConsoleLogger logger = mock( ConsoleLogger.class );
-        ForkClient client = new ForkClient( factory, notifiableTestStream, logger, printedErrorStream, 0 );
-        client.consumeMultiLineContent( ":maven:surefire:std:out:console-info-log:UTF-8:"
-                + encodeBase64String( "s1".getBytes( UTF_8 ) ) );
+        ForkClient client = new ForkClient( factory, notifiableTestStream, 0 );
+        client.handleEvent( new ConsoleInfoEvent( "s1" ) );
         verifyZeroInteractions( notifiableTestStream );
         verify( factory, times( 1 ) )
                 .createReporter();
@@ -835,7 +680,6 @@ public class ForkClientTest
 
     @Test
     public void shouldSendSystemProperty()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -846,11 +690,8 @@ public class ForkClientTest
         when( factory.createReporter() )
                 .thenReturn( receiver );
         NotifiableTestStream notifiableTestStream = mock( NotifiableTestStream.class );
-        AtomicBoolean printedErrorStream = new AtomicBoolean();
-        ConsoleLogger logger = mock( ConsoleLogger.class );
-        ForkClient client = new ForkClient( factory, notifiableTestStream, logger, printedErrorStream, 0 );
-        client.consumeMultiLineContent( ":maven:surefire:std:out:sys-prop:normal-run:UTF-8:azE=:djE="
-                + encodeBase64String( "s1".getBytes( UTF_8 ) ) );
+        ForkClient client = new ForkClient( factory, notifiableTestStream, 0 );
+        client.handleEvent( new SystemPropertyEvent( NORMAL_RUN, "k1", "v1" ) );
         verifyZeroInteractions( notifiableTestStream );
         verifyZeroInteractions( factory );
         assertThat( client.getReporter() )
@@ -879,7 +720,6 @@ public class ForkClientTest
 
     @Test
     public void shouldSendTestsetStartingKilled()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -890,21 +730,11 @@ public class ForkClientTest
         when( factory.createReporter() )
                 .thenReturn( receiver );
         NotifiableTestStream notifiableTestStream = mock( NotifiableTestStream.class );
-        AtomicBoolean printedErrorStream = new AtomicBoolean();
-        ConsoleLogger logger = mock( ConsoleLogger.class );
-
 
         final String exceptionMessage = "msg";
-        final String encodedExceptionMsg = encodeBase64String( toArray( UTF_8.encode( exceptionMessage ) ) );
-
         final String smartStackTrace = "MyTest:86 >> Error";
-        final String encodedSmartStackTrace = encodeBase64String( toArray( UTF_8.encode( smartStackTrace ) ) );
-
         final String stackTrace = "trace line 1\ntrace line 2";
-        final String encodedStackTrace = encodeBase64String( toArray( UTF_8.encode( stackTrace ) ) );
-
         final String trimmedStackTrace = "trace line 1";
-        final String encodedTrimmedStackTrace = encodeBase64String( toArray( UTF_8.encode( trimmedStackTrace ) ) );
 
         SafeThrowable safeThrowable = new SafeThrowable( new Exception( exceptionMessage ) );
         StackTraceWriter stackTraceWriter = mock( StackTraceWriter.class );
@@ -913,7 +743,7 @@ public class ForkClientTest
         when( stackTraceWriter.writeTrimmedTraceToString() ).thenReturn( trimmedStackTrace );
         when( stackTraceWriter.writeTraceToString() ).thenReturn( stackTrace );
 
-        ReportEntry reportEntry = mock( ReportEntry.class );
+        TestSetReportEntry reportEntry = mock( TestSetReportEntry.class );
         when( reportEntry.getElapsed() ).thenReturn( ELAPSED_TIME );
         when( reportEntry.getGroup() ).thenReturn( "this group" );
         when( reportEntry.getMessage() ).thenReturn( "some test" );
@@ -922,33 +752,8 @@ public class ForkClientTest
         when( reportEntry.getSourceName() ).thenReturn( "pkg.MyTest" );
         when( reportEntry.getStackTraceWriter() ).thenReturn( stackTraceWriter );
 
-        String encodedSourceName = encodeBase64String( toArray( UTF_8.encode( reportEntry.getSourceName() ) ) );
-        String encodedName = encodeBase64String( toArray( UTF_8.encode( reportEntry.getName() ) ) );
-        String encodedGroup = encodeBase64String( toArray( UTF_8.encode( reportEntry.getGroup() ) ) );
-        String encodedMessage = encodeBase64String( toArray( UTF_8.encode( reportEntry.getMessage() ) ) );
-
-        ForkClient client = new ForkClient( factory, notifiableTestStream, logger, printedErrorStream, 0 );
-        client.consumeMultiLineContent( ":maven:surefire:std:out:testset-starting:normal-run:UTF-8:"
-                + encodedSourceName
-                + ":"
-                + "-"
-                + ":"
-                + encodedName
-                + ":"
-                + "-"
-                + ":"
-                + encodedGroup
-                + ":"
-                + encodedMessage
-                + ":"
-                + ELAPSED_TIME
-                + ":"
-
-                + encodedExceptionMsg
-                + ":"
-                + encodedSmartStackTrace
-                + ":"
-                + encodedTrimmedStackTrace );
+        ForkClient client = new ForkClient( factory, notifiableTestStream, 0 );
+        client.handleEvent( new TestsetStartingEvent( NORMAL_RUN, reportEntry ) );
 
         client.tryToTimeout( System.currentTimeMillis() + 1000L, 1 );
 
@@ -983,13 +788,13 @@ public class ForkClientTest
                 .isNotNull();
         assertThat( ( (ReportEntry) receiver.getData().get( 0 ) )
                 .getStackTraceWriter().getThrowable().getLocalizedMessage() )
-                .isEqualTo( "msg" );
+                .isEqualTo( exceptionMessage );
         assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getStackTraceWriter().smartTrimmedStackTrace() )
-                .isEqualTo( "MyTest:86 >> Error" );
+                .isEqualTo( smartStackTrace );
         assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getStackTraceWriter().writeTraceToString() )
-                .isEqualTo( "trace line 1" );
+                .isEqualTo( stackTrace );
         assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getStackTraceWriter().writeTrimmedTraceToString() )
-                .isEqualTo( "trace line 1" );
+                .isEqualTo( trimmedStackTrace );
         assertThat( client.isSaidGoodBye() )
                 .isFalse();
         assertThat( client.isErrorInFork() )
@@ -1010,7 +815,6 @@ public class ForkClientTest
 
     @Test
     public void shouldSendTestsetStarting()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -1021,21 +825,11 @@ public class ForkClientTest
         when( factory.createReporter() )
                 .thenReturn( receiver );
         NotifiableTestStream notifiableTestStream = mock( NotifiableTestStream.class );
-        AtomicBoolean printedErrorStream = new AtomicBoolean();
-        ConsoleLogger logger = mock( ConsoleLogger.class );
-
 
         final String exceptionMessage = "msg";
-        final String encodedExceptionMsg = encodeBase64String( toArray( UTF_8.encode( exceptionMessage ) ) );
-
         final String smartStackTrace = "MyTest:86 >> Error";
-        final String encodedSmartStackTrace = encodeBase64String( toArray( UTF_8.encode( smartStackTrace ) ) );
-
         final String stackTrace = "trace line 1\ntrace line 2";
-        final String encodedStackTrace = encodeBase64String( toArray( UTF_8.encode( stackTrace ) ) );
-
         final String trimmedStackTrace = "trace line 1";
-        final String encodedTrimmedStackTrace = encodeBase64String( toArray( UTF_8.encode( trimmedStackTrace ) ) );
 
         SafeThrowable safeThrowable = new SafeThrowable( new Exception( exceptionMessage ) );
         StackTraceWriter stackTraceWriter = mock( StackTraceWriter.class );
@@ -1044,7 +838,7 @@ public class ForkClientTest
         when( stackTraceWriter.writeTrimmedTraceToString() ).thenReturn( trimmedStackTrace );
         when( stackTraceWriter.writeTraceToString() ).thenReturn( stackTrace );
 
-        ReportEntry reportEntry = mock( ReportEntry.class );
+        TestSetReportEntry reportEntry = mock( TestSetReportEntry.class );
         when( reportEntry.getElapsed() ).thenReturn( ELAPSED_TIME );
         when( reportEntry.getGroup() ).thenReturn( "this group" );
         when( reportEntry.getMessage() ).thenReturn( "some test" );
@@ -1055,36 +849,8 @@ public class ForkClientTest
         when( reportEntry.getSourceText() ).thenReturn( "dn1" );
         when( reportEntry.getStackTraceWriter() ).thenReturn( stackTraceWriter );
 
-        String encodedSourceName = encodeBase64String( toArray( UTF_8.encode( reportEntry.getSourceName() ) ) );
-        String encodedSourceText = encodeBase64String( toArray( UTF_8.encode( reportEntry.getSourceText() ) ) );
-        String encodedName = encodeBase64String( toArray( UTF_8.encode( reportEntry.getName() ) ) );
-        String encodedNameText = encodeBase64String( toArray( UTF_8.encode( reportEntry.getNameText() ) ) );
-        String encodedGroup = encodeBase64String( toArray( UTF_8.encode( reportEntry.getGroup() ) ) );
-        String encodedMessage = encodeBase64String( toArray( UTF_8.encode( reportEntry.getMessage() ) ) );
-
-        ForkClient client = new ForkClient( factory, notifiableTestStream, logger, printedErrorStream, 0 );
-        client.consumeMultiLineContent( ":maven:surefire:std:out:testset-starting:normal-run:UTF-8:"
-                + encodedSourceName
-                + ":"
-                + encodedSourceText
-                + ":"
-                + encodedName
-                + ":"
-                + encodedNameText
-                + ":"
-                + encodedGroup
-                + ":"
-                + encodedMessage
-                + ":"
-                + ELAPSED_TIME
-                + ":"
-
-                + encodedExceptionMsg
-                + ":"
-                + encodedSmartStackTrace
-                + ":"
-                + encodedTrimmedStackTrace );
-
+        ForkClient client = new ForkClient( factory, notifiableTestStream, 0 );
+        client.handleEvent( new TestsetStartingEvent( NORMAL_RUN, reportEntry ) );
         client.tryToTimeout( System.currentTimeMillis(), 1 );
 
         verifyZeroInteractions( notifiableTestStream );
@@ -1118,11 +884,11 @@ public class ForkClientTest
                 .getStackTraceWriter().getThrowable().getLocalizedMessage() )
                 .isEqualTo( "msg" );
         assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getStackTraceWriter().smartTrimmedStackTrace() )
-                .isEqualTo( "MyTest:86 >> Error" );
+                .isEqualTo( smartStackTrace );
         assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getStackTraceWriter().writeTraceToString() )
-                .isEqualTo( "trace line 1" );
+                .isEqualTo( stackTrace );
         assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getStackTraceWriter().writeTrimmedTraceToString() )
-                .isEqualTo( "trace line 1" );
+                .isEqualTo( trimmedStackTrace );
         assertThat( client.isSaidGoodBye() )
                 .isFalse();
         assertThat( client.isErrorInFork() )
@@ -1145,7 +911,6 @@ public class ForkClientTest
 
     @Test
     public void shouldSendTestsetCompleted()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -1156,21 +921,11 @@ public class ForkClientTest
         when( factory.createReporter() )
                 .thenReturn( receiver );
         NotifiableTestStream notifiableTestStream = mock( NotifiableTestStream.class );
-        AtomicBoolean printedErrorStream = new AtomicBoolean();
-        ConsoleLogger logger = mock( ConsoleLogger.class );
-
 
         final String exceptionMessage = "msg";
-        final String encodedExceptionMsg = encodeBase64String( toArray( UTF_8.encode( exceptionMessage ) ) );
-
         final String smartStackTrace = "MyTest:86 >> Error";
-        final String encodedSmartStackTrace = encodeBase64String( toArray( UTF_8.encode( smartStackTrace ) ) );
-
         final String stackTrace = "trace line 1\ntrace line 2";
-        final String encodedStackTrace = encodeBase64String( toArray( UTF_8.encode( stackTrace ) ) );
-
         final String trimmedStackTrace = "trace line 1";
-        final String encodedTrimmedStackTrace = encodeBase64String( toArray( UTF_8.encode( trimmedStackTrace ) ) );
 
         SafeThrowable safeThrowable = new SafeThrowable( new Exception( exceptionMessage ) );
         StackTraceWriter stackTraceWriter = mock( StackTraceWriter.class );
@@ -1179,7 +934,7 @@ public class ForkClientTest
         when( stackTraceWriter.writeTrimmedTraceToString() ).thenReturn( trimmedStackTrace );
         when( stackTraceWriter.writeTraceToString() ).thenReturn( stackTrace );
 
-        ReportEntry reportEntry = mock( ReportEntry.class );
+        TestSetReportEntry reportEntry = mock( TestSetReportEntry.class );
         when( reportEntry.getElapsed() ).thenReturn( ELAPSED_TIME );
         when( reportEntry.getGroup() ).thenReturn( "this group" );
         when( reportEntry.getMessage() ).thenReturn( "some test" );
@@ -1188,33 +943,8 @@ public class ForkClientTest
         when( reportEntry.getSourceName() ).thenReturn( "pkg.MyTest" );
         when( reportEntry.getStackTraceWriter() ).thenReturn( stackTraceWriter );
 
-        String encodedSourceName = encodeBase64String( toArray( UTF_8.encode( reportEntry.getSourceName() ) ) );
-        String encodedName = encodeBase64String( toArray( UTF_8.encode( reportEntry.getName() ) ) );
-        String encodedGroup = encodeBase64String( toArray( UTF_8.encode( reportEntry.getGroup() ) ) );
-        String encodedMessage = encodeBase64String( toArray( UTF_8.encode( reportEntry.getMessage() ) ) );
-
-        ForkClient client = new ForkClient( factory, notifiableTestStream, logger, printedErrorStream, 0 );
-        client.consumeMultiLineContent( ":maven:surefire:std:out:testset-completed:normal-run:UTF-8:"
-                + encodedSourceName
-                + ":"
-                + "-"
-                + ":"
-                + encodedName
-                + ":"
-                + "-"
-                + ":"
-                + encodedGroup
-                + ":"
-                + encodedMessage
-                + ":"
-                + ELAPSED_TIME
-                + ":"
-
-                + encodedExceptionMsg
-                + ":"
-                + encodedSmartStackTrace
-                + ":"
-                + encodedTrimmedStackTrace );
+        ForkClient client = new ForkClient( factory, notifiableTestStream, 0 );
+        client.handleEvent( new TestsetCompletedEvent( NORMAL_RUN, reportEntry ) );
 
         verifyZeroInteractions( notifiableTestStream );
         verify( factory ).createReporter();
@@ -1249,9 +979,9 @@ public class ForkClientTest
         assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getStackTraceWriter().smartTrimmedStackTrace() )
                 .isEqualTo( "MyTest:86 >> Error" );
         assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getStackTraceWriter().writeTraceToString() )
-                .isEqualTo( "trace line 1" );
+                .isEqualTo( stackTrace );
         assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getStackTraceWriter().writeTrimmedTraceToString() )
-                .isEqualTo( "trace line 1" );
+                .isEqualTo( trimmedStackTrace );
         assertThat( client.isSaidGoodBye() )
                 .isFalse();
         assertThat( client.isErrorInFork() )
@@ -1274,7 +1004,6 @@ public class ForkClientTest
 
     @Test
     public void shouldSendTestStarting()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -1285,21 +1014,11 @@ public class ForkClientTest
         when( factory.createReporter() )
                 .thenReturn( receiver );
         NotifiableTestStream notifiableTestStream = mock( NotifiableTestStream.class );
-        AtomicBoolean printedErrorStream = new AtomicBoolean();
-        ConsoleLogger logger = mock( ConsoleLogger.class );
-
 
         final String exceptionMessage = "msg";
-        final String encodedExceptionMsg = encodeBase64String( toArray( UTF_8.encode( exceptionMessage ) ) );
-
         final String smartStackTrace = "MyTest:86 >> Error";
-        final String encodedSmartStackTrace = encodeBase64String( toArray( UTF_8.encode( smartStackTrace ) ) );
-
         final String stackTrace = "trace line 1\ntrace line 2";
-        final String encodedStackTrace = encodeBase64String( toArray( UTF_8.encode( stackTrace ) ) );
-
         final String trimmedStackTrace = "trace line 1";
-        final String encodedTrimmedStackTrace = encodeBase64String( toArray( UTF_8.encode( trimmedStackTrace ) ) );
 
         SafeThrowable safeThrowable = new SafeThrowable( new Exception( exceptionMessage ) );
         StackTraceWriter stackTraceWriter = mock( StackTraceWriter.class );
@@ -1317,33 +1036,8 @@ public class ForkClientTest
         when( reportEntry.getSourceName() ).thenReturn( "pkg.MyTest" );
         when( reportEntry.getStackTraceWriter() ).thenReturn( stackTraceWriter );
 
-        String encodedSourceName = encodeBase64String( toArray( UTF_8.encode( reportEntry.getSourceName() ) ) );
-        String encodedName = encodeBase64String( toArray( UTF_8.encode( reportEntry.getName() ) ) );
-        String encodedGroup = encodeBase64String( toArray( UTF_8.encode( reportEntry.getGroup() ) ) );
-        String encodedMessage = encodeBase64String( toArray( UTF_8.encode( reportEntry.getMessage() ) ) );
-
-        ForkClient client = new ForkClient( factory, notifiableTestStream, logger, printedErrorStream, 0 );
-        client.consumeMultiLineContent( ":maven:surefire:std:out:test-starting:normal-run:UTF-8:"
-                + encodedSourceName
-                + ":"
-                + "-"
-                + ":"
-                + encodedName
-                + ":"
-                + "-"
-                + ":"
-                + encodedGroup
-                + ":"
-                + encodedMessage
-                + ":"
-                + ELAPSED_TIME
-                + ":"
-
-                + encodedExceptionMsg
-                + ":"
-                + encodedSmartStackTrace
-                + ":"
-                + encodedTrimmedStackTrace );
+        ForkClient client = new ForkClient( factory, notifiableTestStream, 0 );
+        client.handleEvent( new TestStartingEvent( NORMAL_RUN, reportEntry ) );
 
         verifyZeroInteractions( notifiableTestStream );
         verify( factory ).createReporter();
@@ -1381,11 +1075,11 @@ public class ForkClientTest
                 .getStackTraceWriter().getThrowable().getLocalizedMessage() )
                 .isEqualTo( "msg" );
         assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getStackTraceWriter().smartTrimmedStackTrace() )
-                .isEqualTo( "MyTest:86 >> Error" );
+                .isEqualTo( smartStackTrace );
         assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getStackTraceWriter().writeTraceToString() )
-                .isEqualTo( "trace line 1" );
+                .isEqualTo( stackTrace );
         assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getStackTraceWriter().writeTrimmedTraceToString() )
-                .isEqualTo( "trace line 1" );
+                .isEqualTo( trimmedStackTrace );
         assertThat( client.isSaidGoodBye() )
                 .isFalse();
         assertThat( client.isErrorInFork() )
@@ -1404,7 +1098,6 @@ public class ForkClientTest
 
     @Test
     public void shouldSendTestSucceeded()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -1415,21 +1108,11 @@ public class ForkClientTest
         when( factory.createReporter() )
                 .thenReturn( receiver );
         NotifiableTestStream notifiableTestStream = mock( NotifiableTestStream.class );
-        AtomicBoolean printedErrorStream = new AtomicBoolean();
-        ConsoleLogger logger = mock( ConsoleLogger.class );
-
 
         final String exceptionMessage = "msg";
-        final String encodedExceptionMsg = encodeBase64String( toArray( UTF_8.encode( exceptionMessage ) ) );
-
         final String smartStackTrace = "MyTest:86 >> Error";
-        final String encodedSmartStackTrace = encodeBase64String( toArray( UTF_8.encode( smartStackTrace ) ) );
-
         final String stackTrace = "trace line 1\ntrace line 2";
-        final String encodedStackTrace = encodeBase64String( toArray( UTF_8.encode( stackTrace ) ) );
-
         final String trimmedStackTrace = "trace line 1";
-        final String encodedTrimmedStackTrace = encodeBase64String( toArray( UTF_8.encode( trimmedStackTrace ) ) );
 
         SafeThrowable safeThrowable = new SafeThrowable( new Exception( exceptionMessage ) );
         StackTraceWriter stackTraceWriter = mock( StackTraceWriter.class );
@@ -1447,42 +1130,15 @@ public class ForkClientTest
         when( reportEntry.getSourceName() ).thenReturn( "pkg.MyTest" );
         when( reportEntry.getStackTraceWriter() ).thenReturn( stackTraceWriter );
 
-        String encodedSourceName = encodeBase64String( toArray( UTF_8.encode( reportEntry.getSourceName() ) ) );
-        String encodedName = encodeBase64String( toArray( UTF_8.encode( reportEntry.getName() ) ) );
-        String encodedGroup = encodeBase64String( toArray( UTF_8.encode( reportEntry.getGroup() ) ) );
-        String encodedMessage = encodeBase64String( toArray( UTF_8.encode( reportEntry.getMessage() ) ) );
-
-        ForkClient client = new ForkClient( factory, notifiableTestStream, logger, printedErrorStream, 0 );
-
-        client.consumeMultiLineContent( ":maven:surefire:std:out:test-starting:normal-run:UTF-8:"
-                + encodedSourceName
-                + ":-:-:-:-:-:-:-:-:-" );
+        ForkClient client = new ForkClient( factory, notifiableTestStream,  0 );
+        SimpleReportEntry testStarted = new SimpleReportEntry( reportEntry.getSourceName(), null, null, null );
+        client.handleEvent( new TestStartingEvent( NORMAL_RUN, testStarted ) );
 
         assertThat( client.testsInProgress() )
                 .hasSize( 1 )
                 .contains( "pkg.MyTest" );
 
-        client.consumeMultiLineContent( ":maven:surefire:std:out:test-succeeded:normal-run:UTF-8:"
-                + encodedSourceName
-                + ":"
-                + "-"
-                + ":"
-                + encodedName
-                + ":"
-                + "-"
-                + ":"
-                + encodedGroup
-                + ":"
-                + encodedMessage
-                + ":"
-                + ELAPSED_TIME
-                + ":"
-
-                + encodedExceptionMsg
-                + ":"
-                + encodedSmartStackTrace
-                + ":"
-                + encodedStackTrace );
+        client.handleEvent( new TestSucceededEvent( NORMAL_RUN, reportEntry ) );
 
         verifyZeroInteractions( notifiableTestStream );
         verify( factory ).createReporter();
@@ -1519,11 +1175,11 @@ public class ForkClientTest
                 .getStackTraceWriter().getThrowable().getLocalizedMessage() )
                 .isEqualTo( "msg" );
         assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter().smartTrimmedStackTrace() )
-                .isEqualTo( "MyTest:86 >> Error" );
+                .isEqualTo( smartStackTrace );
         assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter().writeTraceToString() )
-                .isEqualTo( "trace line 1\ntrace line 2" );
+                .isEqualTo( stackTrace );
         assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter().writeTrimmedTraceToString() )
-                .isEqualTo( "trace line 1\ntrace line 2" );
+                .isEqualTo( trimmedStackTrace );
         assertThat( client.isSaidGoodBye() )
                 .isFalse();
         assertThat( client.isErrorInFork() )
@@ -1546,7 +1202,6 @@ public class ForkClientTest
 
     @Test
     public void shouldSendTestFailed()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -1557,21 +1212,11 @@ public class ForkClientTest
         when( factory.createReporter() )
                 .thenReturn( receiver );
         NotifiableTestStream notifiableTestStream = mock( NotifiableTestStream.class );
-        AtomicBoolean printedErrorStream = new AtomicBoolean();
-        ConsoleLogger logger = mock( ConsoleLogger.class );
-
 
         final String exceptionMessage = "msg";
-        final String encodedExceptionMsg = encodeBase64String( toArray( UTF_8.encode( exceptionMessage ) ) );
-
         final String smartStackTrace = "MyTest:86 >> Error";
-        final String encodedSmartStackTrace = encodeBase64String( toArray( UTF_8.encode( smartStackTrace ) ) );
-
         final String stackTrace = "trace line 1\ntrace line 2";
-        final String encodedStackTrace = encodeBase64String( toArray( UTF_8.encode( stackTrace ) ) );
-
         final String trimmedStackTrace = "trace line 1";
-        final String encodedTrimmedStackTrace = encodeBase64String( toArray( UTF_8.encode( trimmedStackTrace ) ) );
 
         SafeThrowable safeThrowable = new SafeThrowable( new Exception( exceptionMessage ) );
         StackTraceWriter stackTraceWriter = mock( StackTraceWriter.class );
@@ -1589,42 +1234,15 @@ public class ForkClientTest
         when( reportEntry.getSourceName() ).thenReturn( "pkg.MyTest" );
         when( reportEntry.getStackTraceWriter() ).thenReturn( stackTraceWriter );
 
-        String encodedSourceName = encodeBase64String( toArray( UTF_8.encode( reportEntry.getSourceName() ) ) );
-        String encodedName = encodeBase64String( toArray( UTF_8.encode( reportEntry.getName() ) ) );
-        String encodedGroup = encodeBase64String( toArray( UTF_8.encode( reportEntry.getGroup() ) ) );
-        String encodedMessage = encodeBase64String( toArray( UTF_8.encode( reportEntry.getMessage() ) ) );
-
-        ForkClient client = new ForkClient( factory, notifiableTestStream, logger, printedErrorStream, 0 );
-
-        client.consumeMultiLineContent( ":maven:surefire:std:out:test-starting:normal-run:UTF-8:"
-                + encodedSourceName
-                + ":-:-:-:-:-:-:-:-:-" );
+        ForkClient client = new ForkClient( factory, notifiableTestStream, 0 );
+        SimpleReportEntry testClass = new SimpleReportEntry( reportEntry.getSourceName(), null, null, null );
+        client.handleEvent( new TestStartingEvent( NORMAL_RUN, testClass ) );
 
         assertThat( client.testsInProgress() )
                 .hasSize( 1 )
                 .contains( "pkg.MyTest" );
 
-        client.consumeMultiLineContent( ":maven:surefire:std:out:test-failed:normal-run:UTF-8:"
-                + encodedSourceName
-                + ":"
-                + "-"
-                + ":"
-                + encodedName
-                + ":"
-                + "-"
-                + ":"
-                + encodedGroup
-                + ":"
-                + encodedMessage
-                + ":"
-                + ELAPSED_TIME
-                + ":"
-
-                + encodedExceptionMsg
-                + ":"
-                + encodedSmartStackTrace
-                + ":"
-                + encodedTrimmedStackTrace );
+        client.handleEvent( new TestFailedEvent( NORMAL_RUN, reportEntry ) );
 
         verifyZeroInteractions( notifiableTestStream );
         verify( factory ).createReporter();
@@ -1645,6 +1263,8 @@ public class ForkClientTest
                 .isNull();
         assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getNameText() )
                 .isNull();
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getStackTraceWriter() )
+                .isNull();
         assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getSourceName() )
                 .isEqualTo( "pkg.MyTest" );
         assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getSourceText() )
@@ -1667,9 +1287,9 @@ public class ForkClientTest
         assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter().smartTrimmedStackTrace() )
                 .isEqualTo( "MyTest:86 >> Error" );
         assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter().writeTraceToString() )
-                .isEqualTo( "trace line 1" );
+                .isEqualTo( stackTrace );
         assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter().writeTrimmedTraceToString() )
-                .isEqualTo( "trace line 1" );
+                .isEqualTo( trimmedStackTrace );
         assertThat( client.isSaidGoodBye() )
                 .isFalse();
         assertThat( client.isErrorInFork() )
@@ -1692,7 +1312,6 @@ public class ForkClientTest
 
     @Test
     public void shouldSendTestSkipped()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -1703,21 +1322,11 @@ public class ForkClientTest
         when( factory.createReporter() )
                 .thenReturn( receiver );
         NotifiableTestStream notifiableTestStream = mock( NotifiableTestStream.class );
-        AtomicBoolean printedErrorStream = new AtomicBoolean();
-        ConsoleLogger logger = mock( ConsoleLogger.class );
-
 
         final String exceptionMessage = "msg";
-        final String encodedExceptionMsg = encodeBase64String( toArray( UTF_8.encode( exceptionMessage ) ) );
-
         final String smartStackTrace = "MyTest:86 >> Error";
-        final String encodedSmartStackTrace = encodeBase64String( toArray( UTF_8.encode( smartStackTrace ) ) );
-
         final String stackTrace = "trace line 1\ntrace line 2";
-        final String encodedStackTrace = encodeBase64String( toArray( UTF_8.encode( stackTrace ) ) );
-
         final String trimmedStackTrace = "trace line 1";
-        final String encodedTrimmedStackTrace = encodeBase64String( toArray( UTF_8.encode( trimmedStackTrace ) ) );
 
         SafeThrowable safeThrowable = new SafeThrowable( new Exception( exceptionMessage ) );
         StackTraceWriter stackTraceWriter = mock( StackTraceWriter.class );
@@ -1735,42 +1344,15 @@ public class ForkClientTest
         when( reportEntry.getSourceName() ).thenReturn( "pkg.MyTest" );
         when( reportEntry.getStackTraceWriter() ).thenReturn( stackTraceWriter );
 
-        String encodedSourceName = encodeBase64String( toArray( UTF_8.encode( reportEntry.getSourceName() ) ) );
-        String encodedName = encodeBase64String( toArray( UTF_8.encode( reportEntry.getName() ) ) );
-        String encodedGroup = encodeBase64String( toArray( UTF_8.encode( reportEntry.getGroup() ) ) );
-        String encodedMessage = encodeBase64String( toArray( UTF_8.encode( reportEntry.getMessage() ) ) );
-
-        ForkClient client = new ForkClient( factory, notifiableTestStream, logger, printedErrorStream, 0 );
-
-        client.consumeMultiLineContent( ":maven:surefire:std:out:test-starting:normal-run:UTF-8:"
-                + encodedSourceName
-                + ":-:-:-:-:-:-:-:-:-" );
+        ForkClient client = new ForkClient( factory, notifiableTestStream, 0 );
+        SimpleReportEntry testStarted = new SimpleReportEntry( reportEntry.getSourceName(), null, null, null );
+        client.handleEvent( new TestStartingEvent( NORMAL_RUN, testStarted ) );
 
         assertThat( client.testsInProgress() )
                 .hasSize( 1 )
                 .contains( "pkg.MyTest" );
 
-        client.consumeMultiLineContent( ":maven:surefire:std:out:test-skipped:normal-run:UTF-8:"
-                + encodedSourceName
-                + ":"
-                + "-"
-                + ":"
-                + encodedName
-                + ":"
-                + "-"
-                + ":"
-                + encodedGroup
-                + ":"
-                + encodedMessage
-                + ":"
-                + ELAPSED_TIME
-                + ":"
-
-                + encodedExceptionMsg
-                + ":"
-                + encodedSmartStackTrace
-                + ":"
-                + encodedStackTrace );
+        client.handleEvent( new TestSkippedEvent( NORMAL_RUN, reportEntry ) );
 
         verifyZeroInteractions( notifiableTestStream );
         verify( factory ).createReporter();
@@ -1811,11 +1393,11 @@ public class ForkClientTest
                 .getStackTraceWriter().getThrowable().getLocalizedMessage() )
                 .isEqualTo( "msg" );
         assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter().smartTrimmedStackTrace() )
-                .isEqualTo( "MyTest:86 >> Error" );
+                .isEqualTo( smartStackTrace );
         assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter().writeTraceToString() )
-                .isEqualTo( "trace line 1\ntrace line 2" );
+                .isEqualTo( stackTrace );
         assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter().writeTrimmedTraceToString() )
-                .isEqualTo( "trace line 1\ntrace line 2" );
+                .isEqualTo( trimmedStackTrace );
         assertThat( client.isSaidGoodBye() )
                 .isFalse();
         assertThat( client.isErrorInFork() )
@@ -1838,7 +1420,6 @@ public class ForkClientTest
 
     @Test
     public void shouldSendTestError()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -1849,21 +1430,11 @@ public class ForkClientTest
         when( factory.createReporter() )
                 .thenReturn( receiver );
         NotifiableTestStream notifiableTestStream = mock( NotifiableTestStream.class );
-        AtomicBoolean printedErrorStream = new AtomicBoolean();
-        ConsoleLogger logger = mock( ConsoleLogger.class );
-
 
         final String exceptionMessage = "msg";
-        final String encodedExceptionMsg = encodeBase64String( toArray( UTF_8.encode( exceptionMessage ) ) );
-
         final String smartStackTrace = "MyTest:86 >> Error";
-        final String encodedSmartStackTrace = encodeBase64String( toArray( UTF_8.encode( smartStackTrace ) ) );
-
         final String stackTrace = "trace line 1\ntrace line 2";
-        final String encodedStackTrace = encodeBase64String( toArray( UTF_8.encode( stackTrace ) ) );
-
         final String trimmedStackTrace = "trace line 1";
-        final String encodedTrimmedStackTrace = encodeBase64String( toArray( UTF_8.encode( trimmedStackTrace ) ) );
 
         SafeThrowable safeThrowable = new SafeThrowable( new Exception( exceptionMessage ) );
         StackTraceWriter stackTraceWriter = mock( StackTraceWriter.class );
@@ -1882,45 +1453,16 @@ public class ForkClientTest
         when( reportEntry.getSourceText() ).thenReturn( "display name" );
         when( reportEntry.getStackTraceWriter() ).thenReturn( stackTraceWriter );
 
-        String encodedSourceName = encodeBase64String( toArray( UTF_8.encode( reportEntry.getSourceName() ) ) );
-        String encodedSourceText = encodeBase64String( toArray( UTF_8.encode( reportEntry.getSourceText() ) ) );
-        String encodedName = encodeBase64String( toArray( UTF_8.encode( reportEntry.getName() ) ) );
-        String encodedGroup = encodeBase64String( toArray( UTF_8.encode( reportEntry.getGroup() ) ) );
-        String encodedMessage = encodeBase64String( toArray( UTF_8.encode( reportEntry.getMessage() ) ) );
-
-        ForkClient client = new ForkClient( factory, notifiableTestStream, logger, printedErrorStream, 0 );
-
-        client.consumeMultiLineContent( ":maven:surefire:std:out:test-starting:normal-run:UTF-8:"
-                + encodedSourceName
-                + ":"
-                + encodedSourceText
-                + ":-:':-:-:-:-:-:-" );
+        ForkClient client = new ForkClient( factory, notifiableTestStream, 0 );
+        SimpleReportEntry testStarted =
+            new SimpleReportEntry( reportEntry.getSourceName(), reportEntry.getSourceText(), null, null );
+        client.handleEvent( new TestStartingEvent( NORMAL_RUN, testStarted ) );
 
         assertThat( client.testsInProgress() )
                 .hasSize( 1 )
                 .contains( "pkg.MyTest" );
 
-        client.consumeMultiLineContent( ":maven:surefire:std:out:test-error:normal-run:UTF-8:"
-                + encodedSourceName
-                + ":"
-                + encodedSourceText
-                + ":"
-                + encodedName
-                + ":"
-                + "-"
-                + ":"
-                + encodedGroup
-                + ":"
-                + encodedMessage
-                + ":"
-                + ELAPSED_TIME
-                + ":"
-
-                + encodedExceptionMsg
-                + ":"
-                + encodedSmartStackTrace
-                + ":"
-                + encodedTrimmedStackTrace );
+        client.handleEvent( new TestErrorEvent( NORMAL_RUN, reportEntry ) );
 
         verifyZeroInteractions( notifiableTestStream );
         verify( factory ).createReporter();
@@ -1957,11 +1499,11 @@ public class ForkClientTest
                 .getStackTraceWriter().getThrowable().getLocalizedMessage() )
                 .isEqualTo( "msg" );
         assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter().smartTrimmedStackTrace() )
-                .isEqualTo( "MyTest:86 >> Error" );
+                .isEqualTo( smartStackTrace );
         assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter().writeTraceToString() )
-                .isEqualTo( "trace line 1" );
+                .isEqualTo( stackTrace );
         assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter().writeTrimmedTraceToString() )
-                .isEqualTo( "trace line 1" );
+                .isEqualTo( trimmedStackTrace );
         assertThat( client.isSaidGoodBye() )
                 .isFalse();
         assertThat( client.isErrorInFork() )
@@ -1984,7 +1526,6 @@ public class ForkClientTest
 
     @Test
     public void shouldSendTestAssumptionFailure()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -1995,21 +1536,11 @@ public class ForkClientTest
         when( factory.createReporter() )
                 .thenReturn( receiver );
         NotifiableTestStream notifiableTestStream = mock( NotifiableTestStream.class );
-        AtomicBoolean printedErrorStream = new AtomicBoolean();
-        ConsoleLogger logger = mock( ConsoleLogger.class );
-
 
         final String exceptionMessage = "msg";
-        final String encodedExceptionMsg = encodeBase64String( toArray( UTF_8.encode( exceptionMessage ) ) );
-
         final String smartStackTrace = "MyTest:86 >> Error";
-        final String encodedSmartStackTrace = encodeBase64String( toArray( UTF_8.encode( smartStackTrace ) ) );
-
         final String stackTrace = "trace line 1\ntrace line 2";
-        final String encodedStackTrace = encodeBase64String( toArray( UTF_8.encode( stackTrace ) ) );
-
         final String trimmedStackTrace = "trace line 1";
-        final String encodedTrimmedStackTrace = encodeBase64String( toArray( UTF_8.encode( trimmedStackTrace ) ) );
 
         SafeThrowable safeThrowable = new SafeThrowable( new Exception( exceptionMessage ) );
         StackTraceWriter stackTraceWriter = mock( StackTraceWriter.class );
@@ -2028,43 +1559,15 @@ public class ForkClientTest
         when( reportEntry.getSourceName() ).thenReturn( "pkg.MyTest" );
         when( reportEntry.getStackTraceWriter() ).thenReturn( stackTraceWriter );
 
-        String encodedSourceName = encodeBase64String( toArray( UTF_8.encode( reportEntry.getSourceName() ) ) );
-        String encodedName = encodeBase64String( toArray( UTF_8.encode( reportEntry.getName() ) ) );
-        String encodedText = encodeBase64String( toArray( UTF_8.encode( reportEntry.getNameText() ) ) );
-        String encodedGroup = encodeBase64String( toArray( UTF_8.encode( reportEntry.getGroup() ) ) );
-        String encodedMessage = encodeBase64String( toArray( UTF_8.encode( reportEntry.getMessage() ) ) );
-
-        ForkClient client = new ForkClient( factory, notifiableTestStream, logger, printedErrorStream, 0 );
-
-        client.consumeMultiLineContent( ":maven:surefire:std:out:test-starting:normal-run:UTF-8:"
-                + encodedSourceName
-                + ":-:-:-:-:-:-:-:-:-" );
+        ForkClient client = new ForkClient( factory, notifiableTestStream, 0 );
+        SimpleReportEntry testStarted = new SimpleReportEntry( reportEntry.getSourceName(), null, null, null );
+        client.handleEvent( new TestStartingEvent( NORMAL_RUN, testStarted ) );
 
         assertThat( client.testsInProgress() )
                 .hasSize( 1 )
                 .contains( "pkg.MyTest" );
 
-        client.consumeMultiLineContent( ":maven:surefire:std:out:test-assumption-failure:normal-run:UTF-8:"
-                + encodedSourceName
-                + ":"
-                + "-"
-                + ":"
-                + encodedName
-                + ":"
-                + encodedText
-                + ":"
-                + encodedGroup
-                + ":"
-                + encodedMessage
-                + ":"
-                + ELAPSED_TIME
-                + ":"
-
-                + encodedExceptionMsg
-                + ":"
-                + encodedSmartStackTrace
-                + ":"
-                + encodedStackTrace );
+        client.handleEvent( new TestAssumptionFailureEvent( NORMAL_RUN, reportEntry ) );
 
         verifyZeroInteractions( notifiableTestStream );
         verify( factory ).createReporter();
@@ -2101,11 +1604,11 @@ public class ForkClientTest
                 .getStackTraceWriter().getThrowable().getLocalizedMessage() )
                 .isEqualTo( "msg" );
         assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter().smartTrimmedStackTrace() )
-                .isEqualTo( "MyTest:86 >> Error" );
+                .isEqualTo( smartStackTrace );
         assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter().writeTraceToString() )
-                .isEqualTo( "trace line 1\ntrace line 2" );
+                .isEqualTo( stackTrace );
         assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter().writeTrimmedTraceToString() )
-                .isEqualTo( "trace line 1\ntrace line 2" );
+                .isEqualTo( trimmedStackTrace );
         assertThat( client.isSaidGoodBye() )
                 .isFalse();
         assertThat( client.isErrorInFork() )
@@ -2126,9 +1629,24 @@ public class ForkClientTest
                 .isSameAs( factory );
     }
 
-    private static byte[] toArray( ByteBuffer buffer )
+    private static class EH implements EventHandler<Event>
     {
-        return copyOfRange( buffer.array(), buffer.arrayOffset(), buffer.arrayOffset() + buffer.remaining() );
-    }
+        private final BlockingQueue<Event> cache = new LinkedBlockingQueue<>( 1 );
+
+        Event pullEvent() throws InterruptedException
+        {
+            return cache.poll( 1, TimeUnit.MINUTES );
+        }
 
+        int sizeOfEventCache()
+        {
+            return cache.size();
+        }
+
+        @Override
+        public void handleEvent( @Nonnull Event event )
+        {
+            cache.add( event );
+        }
+    }
 }
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedChannelDecoderTest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedChannelDecoderTest.java
deleted file mode 100644
index 9b0d9c9..0000000
--- a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedChannelDecoderTest.java
+++ /dev/null
@@ -1,901 +0,0 @@
-package org.apache.maven.plugin.surefire.booterclient.output;
-
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import org.apache.maven.plugin.surefire.log.api.ConsoleLoggerUtils;
-import org.apache.maven.surefire.booter.ForkedChannelEncoder;
-import org.apache.maven.surefire.report.ReportEntry;
-import org.apache.maven.surefire.report.RunMode;
-import org.apache.maven.surefire.report.SafeThrowable;
-import org.apache.maven.surefire.report.StackTraceWriter;
-import org.apache.maven.surefire.util.internal.ObjectUtils;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.experimental.runners.Enclosed;
-import org.junit.experimental.theories.DataPoints;
-import org.junit.experimental.theories.FromDataPoints;
-import org.junit.experimental.theories.Theories;
-import org.junit.experimental.theories.Theory;
-import org.junit.rules.ExpectedException;
-import org.junit.runner.RunWith;
-
-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.util.Arrays;
-import java.util.Map;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.apache.commons.codec.binary.Base64.encodeBase64String;
-import static org.apache.maven.plugin.surefire.booterclient.output.ForkedChannelDecoder.toReportEntry;
-import static org.apache.maven.surefire.report.RunMode.NORMAL_RUN;
-import static org.fest.assertions.Assertions.assertThat;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-import static org.junit.rules.ExpectedException.none;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.nullable;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
-import static org.mockito.Mockito.when;
-
-/**
- * Test for {@link ForkedChannelDecoder}.
- *
- * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
- * @since 3.0.0-M4
- */
-@RunWith( Enclosed.class )
-public class ForkedChannelDecoderTest
-{
-    /**
-     *
-     */
-    public static class DecoderOperationsTest
-    {
-        @Rule
-        public final ExpectedException rule = none();
-
-        @Test
-        public void shouldBeFailSafe()
-        {
-            assertThat( ForkedChannelDecoder.decode( null, UTF_8 ) ).isNull();
-            assertThat( ForkedChannelDecoder.decode( "-", UTF_8 ) ).isNull();
-            assertThat( ForkedChannelDecoder.decodeToInteger( null ) ).isNull();
-            assertThat( ForkedChannelDecoder.decodeToInteger( "-" ) ).isNull();
-        }
-
-        @Test
-        @SuppressWarnings( "checkstyle:innerassignment" )
-        public void shouldHaveSystemProperty() throws IOException
-        {
-            Stream out = Stream.newStream();
-            ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
-            forkedChannelEncoder.sendSystemProperties( ObjectUtils.systemProps() );
-
-            ForkedChannelDecoder decoder = new ForkedChannelDecoder();
-            decoder.setSystemPropertiesListener( new PropertyEventAssertionListener() );
-            LineNumberReader reader = out.newReader( UTF_8 );
-            AssertionErrorHandler errorHandler = mock( AssertionErrorHandler.class );
-            for ( String line; ( line = reader.readLine() ) != null; )
-            {
-                decoder.handleEvent( line, errorHandler );
-            }
-            verifyZeroInteractions( errorHandler );
-            assertThat( reader.getLineNumber() ).isPositive();
-        }
-
-        @Test
-        public void shouldRecognizeEmptyStream4ReportEntry()
-        {
-            ReportEntry reportEntry = toReportEntry( null, null, null, "", "", null, null, "",
-                    "", "", null );
-            assertThat( reportEntry ).isNull();
-
-            reportEntry = toReportEntry( UTF_8, "", "", "", "", "", "", "-", "", "", "" );
-            assertThat( reportEntry ).isNotNull();
-            assertThat( reportEntry.getStackTraceWriter() ).isNull();
-            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();
-
-            rule.expect( NumberFormatException.class );
-            toReportEntry( UTF_8, "", "", "", "", "", "", "", "", "", "" );
-            fail();
-        }
-
-        @Test
-        @SuppressWarnings( "checkstyle:magicnumber" )
-        public void testCreatingReportEntry()
-        {
-            final String exceptionMessage = "msg";
-            final String encodedExceptionMsg = encodeBase64String( toArray( UTF_8.encode( exceptionMessage ) ) );
-
-            final String smartStackTrace = "MyTest:86 >> Error";
-            final String encodedSmartStackTrace = encodeBase64String( toArray( UTF_8.encode( smartStackTrace ) ) );
-
-            final String stackTrace = "Exception: msg\ntrace line 1\ntrace line 2";
-            final String encodedStackTrace = encodeBase64String( toArray( UTF_8.encode( stackTrace ) ) );
-
-            final String trimmedStackTrace = "trace line 1\ntrace line 2";
-            final String encodedTrimmedStackTrace = encodeBase64String( toArray( UTF_8.encode( trimmedStackTrace ) ) );
-
-            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 );
-
-            String encodedSourceName = encodeBase64String( toArray( UTF_8.encode( reportEntry.getSourceName() ) ) );
-            String encodedSourceText = encodeBase64String( toArray( UTF_8.encode( reportEntry.getSourceText() ) ) );
-            String encodedName = encodeBase64String( toArray( UTF_8.encode( reportEntry.getName() ) ) );
-            String encodedText = encodeBase64String( toArray( UTF_8.encode( reportEntry.getNameText() ) ) );
-            String encodedGroup = encodeBase64String( toArray( UTF_8.encode( reportEntry.getGroup() ) ) );
-            String encodedMessage = encodeBase64String( toArray( UTF_8.encode( reportEntry.getMessage() ) ) );
-
-            ReportEntry decodedReportEntry = toReportEntry( UTF_8, encodedSourceName, encodedSourceText,
-                    encodedName, encodedText, encodedGroup, encodedMessage, "-", 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 = toReportEntry( UTF_8, encodedSourceName, encodedSourceText, encodedName, encodedText,
-                    encodedGroup, encodedMessage, "-", encodedExceptionMsg, encodedSmartStackTrace, 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() ).isNull();
-
-            decodedReportEntry = toReportEntry( UTF_8, encodedSourceName, encodedSourceText, encodedName, encodedText,
-                    encodedGroup, encodedMessage, "1003", encodedExceptionMsg, encodedSmartStackTrace, 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() ).isNull();
-
-            decodedReportEntry = toReportEntry( UTF_8, encodedSourceName, encodedSourceText, encodedName, encodedText,
-                    encodedGroup, encodedMessage, "1003", encodedExceptionMsg, encodedSmartStackTrace,
-                    encodedStackTrace );
-
-            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 = toReportEntry( UTF_8, encodedSourceName, encodedSourceText, encodedName, encodedText,
-                    encodedGroup, encodedMessage, "1003", encodedExceptionMsg, encodedSmartStackTrace,
-                    encodedTrimmedStackTrace );
-
-            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 IOException
-        {
-            Stream out = Stream.newStream();
-            ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
-            forkedChannelEncoder.bye();
-            String read = new String( out.toByteArray(), UTF_8 );
-            assertThat( read )
-                    .isEqualTo( ":maven:surefire:std:out:bye\n" );
-            LineNumberReader lines = out.newReader( UTF_8 );
-            ForkedChannelDecoder decoder = new ForkedChannelDecoder();
-            decoder.setByeListener( new EventAssertionListener() );
-            AssertionErrorHandler errorHandler = mock( AssertionErrorHandler.class );
-            decoder.handleEvent( lines.readLine(), errorHandler );
-            verifyZeroInteractions( errorHandler );
-            assertThat( lines.readLine() )
-                    .isNull();
-        }
-
-        @Test
-        public void shouldSendStopOnNextTestEvent() throws IOException
-        {
-
-            Stream out = Stream.newStream();
-            ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
-            forkedChannelEncoder.stopOnNextTest();
-            String read = new String( out.toByteArray(), UTF_8 );
-            assertThat( read )
-                    .isEqualTo( ":maven:surefire:std:out:stop-on-next-test\n" );
-            LineNumberReader lines = out.newReader( UTF_8 );
-            ForkedChannelDecoder decoder = new ForkedChannelDecoder();
-            decoder.setStopOnNextTestListener( new EventAssertionListener() );
-            AssertionErrorHandler errorHandler = mock( AssertionErrorHandler.class );
-            decoder.handleEvent( lines.readLine(), errorHandler );
-            verifyZeroInteractions( errorHandler );
-            assertThat( lines.readLine() )
-                    .isNull();
-        }
-
-        @Test
-        public void shouldCorrectlyDecodeStackTracesWithEmptyStringTraceMessages()
-        {
-            String exceptionMessage = "";
-            String smartStackTrace = "JUnit5Test.failWithEmptyString:16";
-            String exceptionStackTrace = "org.opentest4j.AssertionFailedError: \n"
-                    + "\tat JUnit5Test.failWithEmptyString(JUnit5Test.java:16)\n";
-
-            StackTraceWriter stackTraceWriter = mock( StackTraceWriter.class );
-            SafeThrowable safeThrowable = new SafeThrowable( exceptionMessage );
-            when( stackTraceWriter.getThrowable() ).thenReturn( safeThrowable );
-            when( stackTraceWriter.smartTrimmedStackTrace() ).thenReturn( smartStackTrace );
-            when( stackTraceWriter.writeTrimmedTraceToString() ).thenReturn( exceptionStackTrace );
-            when( stackTraceWriter.writeTraceToString() ).thenReturn( exceptionStackTrace );
-
-            ReportEntry reportEntry = mock( ReportEntry.class );
-            when( reportEntry.getElapsed() ).thenReturn( 7 );
-            when( reportEntry.getGroup() ).thenReturn( null );
-            when( reportEntry.getMessage() ).thenReturn( null );
-            when( reportEntry.getName() ).thenReturn( "failWithEmptyString" );
-            when( reportEntry.getNameWithGroup() ).thenReturn( "JUnit5Test" );
-            when( reportEntry.getSourceName() ).thenReturn( "JUnit5Test" );
-            when( reportEntry.getSourceText() ).thenReturn( null );
-            when( reportEntry.getStackTraceWriter() ).thenReturn( stackTraceWriter );
-
-            Stream out = Stream.newStream();
-            ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
-            forkedChannelEncoder.testFailed( reportEntry, true );
-            String line = new String( out.toByteArray(), UTF_8 );
-
-            ForkedChannelDecoder decoder = new ForkedChannelDecoder();
-            decoder.setTestFailedListener( new ReportEventAssertionListener( reportEntry ) );
-            AssertionErrorHandler errorHandler = mock( AssertionErrorHandler.class );
-            decoder.handleEvent( line, errorHandler );
-            verifyZeroInteractions( errorHandler );
-        }
-
-        @Test
-        public void shouldSendNextTestEvent() throws IOException
-        {
-
-            Stream out = Stream.newStream();
-            ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
-            forkedChannelEncoder.acquireNextTest();
-            String read = new String( out.toByteArray(), UTF_8 );
-            assertThat( read )
-                    .isEqualTo( ":maven:surefire:std:out:next-test\n" );
-            LineNumberReader lines = out.newReader( UTF_8 );
-            ForkedChannelDecoder decoder = new ForkedChannelDecoder();
-            decoder.setAcquireNextTestListener( new EventAssertionListener() );
-            AssertionErrorHandler errorHandler = mock( AssertionErrorHandler.class );
-            decoder.handleEvent( lines.readLine(), errorHandler );
-            verifyZeroInteractions( errorHandler );
-            assertThat( lines.readLine() )
-                    .isNull();
-        }
-
-        @Test
-        public void testConsole() throws IOException
-        {
-            Stream out = Stream.newStream();
-
-            ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
-            forkedChannelEncoder.consoleInfoLog( "msg" );
-
-            LineNumberReader lines = out.newReader( UTF_8 );
-            ForkedChannelDecoder decoder = new ForkedChannelDecoder();
-            decoder.setConsoleInfoListener( new StringEventAssertionListener( "msg" ) );
-            AssertionErrorHandler errorHandler = mock( AssertionErrorHandler.class );
-            decoder.handleEvent( lines.readLine(), errorHandler );
-            verifyZeroInteractions( errorHandler );
-            assertThat( lines.readLine() )
-                    .isNull();
-        }
-
-        @Test
-        public void testError() throws IOException
-        {
-            Stream out = Stream.newStream();
-
-            ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
-            forkedChannelEncoder.consoleErrorLog( "msg" );
-
-            LineNumberReader lines = out.newReader( UTF_8 );
-            ForkedChannelDecoder decoder = new ForkedChannelDecoder();
-            decoder.setConsoleErrorListener( new StackTraceEventListener( "msg", null, null ) );
-            AssertionErrorHandler errorHandler = mock( AssertionErrorHandler.class );
-            decoder.handleEvent( lines.readLine(), errorHandler );
-            verifyZeroInteractions( errorHandler );
-            assertThat( lines.readLine() )
-                    .isNull();
-        }
-
-        @Test
-        public void testErrorWithException() throws IOException
-        {
-            Throwable t = new Throwable( "msg" );
-
-            Stream out = Stream.newStream();
-
-            ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
-            forkedChannelEncoder.consoleErrorLog( t );
-
-            LineNumberReader lines = out.newReader( UTF_8 );
-            ForkedChannelDecoder decoder = new ForkedChannelDecoder();
-            String stackTrace = ConsoleLoggerUtils.toString( t );
-            decoder.setConsoleErrorListener( new StackTraceEventListener( "msg", null, stackTrace ) );
-            AssertionErrorHandler errorHandler = mock( AssertionErrorHandler.class );
-            decoder.handleEvent( lines.readLine(), errorHandler );
-            verifyZeroInteractions( errorHandler );
-            assertThat( lines.readLine() )
-                    .isNull();
-        }
-
-        @Test
-        public void testErrorWithStackTraceWriter() throws IOException
-        {
-            Stream out = Stream.newStream();
-
-            ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
-            StackTraceWriter stackTraceWriter = new DeserializedStacktraceWriter( "1", "2", "3" );
-            forkedChannelEncoder.consoleErrorLog( stackTraceWriter, false );
-
-            LineNumberReader lines = out.newReader( UTF_8 );
-            ForkedChannelDecoder decoder = new ForkedChannelDecoder();
-            decoder.setConsoleErrorListener( new StackTraceEventListener( "1", "2", "3" ) );
-            AssertionErrorHandler errorHandler = mock( AssertionErrorHandler.class );
-            decoder.handleEvent( lines.readLine(), errorHandler );
-            verifyZeroInteractions( errorHandler );
-            assertThat( lines.readLine() )
-                    .isNull();
-        }
-
-        @Test
-        public void testDebug() throws IOException
-        {
-            Stream out = Stream.newStream();
-
-            ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
-            forkedChannelEncoder.consoleDebugLog( "msg" );
-
-            LineNumberReader lines = out.newReader( UTF_8 );
-            ForkedChannelDecoder decoder = new ForkedChannelDecoder();
-            decoder.setConsoleDebugListener( new StringEventAssertionListener( "msg" ) );
-            AssertionErrorHandler errorHandler = mock( AssertionErrorHandler.class );
-            decoder.handleEvent( lines.readLine(), errorHandler );
-            verifyZeroInteractions( errorHandler );
-            assertThat( lines.readLine() )
-                    .isNull();
-        }
-
-        @Test
-        public void testWarning() throws IOException
-        {
-            Stream out = Stream.newStream();
-
-            ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
-            forkedChannelEncoder.consoleWarningLog( "msg" );
-
-            LineNumberReader lines = out.newReader( UTF_8 );
-            ForkedChannelDecoder decoder = new ForkedChannelDecoder();
-            decoder.setConsoleWarningListener( new StringEventAssertionListener( "msg" ) );
-            AssertionErrorHandler errorHandler = mock( AssertionErrorHandler.class );
-            decoder.handleEvent( lines.readLine(), errorHandler );
-            verifyZeroInteractions( errorHandler );
-            assertThat( lines.readLine() )
-                    .isNull();
-        }
-
-        @Test
-        public void testStdOutStream() throws IOException
-        {
-            Stream out = Stream.newStream();
-            ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
-            forkedChannelEncoder.stdOut( "msg", false );
-
-            LineNumberReader lines = out.newReader( UTF_8 );
-            ForkedChannelDecoder decoder = new ForkedChannelDecoder();
-            decoder.setStdOutListener( new StandardOutErrEventAssertionListener( NORMAL_RUN, "msg", false ) );
-            AssertionErrorHandler errorHandler = mock( AssertionErrorHandler.class );
-            decoder.handleEvent( lines.readLine(), errorHandler );
-            verifyZeroInteractions( errorHandler );
-            assertThat( lines.readLine() )
-                    .isNull();
-        }
-
-        @Test
-        public void testStdOutStreamPrint() throws IOException
-        {
-            Stream out = Stream.newStream();
-            ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
-            forkedChannelEncoder.stdOut( "", false );
-
-            LineNumberReader lines = out.newReader( UTF_8 );
-            ForkedChannelDecoder decoder = new ForkedChannelDecoder();
-            decoder.setStdOutListener( new StandardOutErrEventAssertionListener( NORMAL_RUN, "", false ) );
-            AssertionErrorHandler errorHandler = mock( AssertionErrorHandler.class );
-            decoder.handleEvent( lines.readLine(), errorHandler );
-            verifyZeroInteractions( errorHandler );
-            assertThat( lines.readLine() )
-                    .isNull();
-        }
-
-        @Test
-        public void testStdOutStreamPrintWithNull() throws IOException
-        {
-            Stream out = Stream.newStream();
-            ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
-            forkedChannelEncoder.stdOut( null, false );
-
-            LineNumberReader lines = out.newReader( UTF_8 );
-            ForkedChannelDecoder decoder = new ForkedChannelDecoder();
-            decoder.setStdOutListener( new StandardOutErrEventAssertionListener( NORMAL_RUN, null, false ) );
-            AssertionErrorHandler errorHandler = mock( AssertionErrorHandler.class );
-            decoder.handleEvent( lines.readLine(), errorHandler );
-            verifyZeroInteractions( errorHandler );
-            assertThat( lines.readLine() )
-                    .isNull();
-        }
-
-        @Test
-        public void testStdOutStreamPrintln() throws IOException
-        {
-            Stream out = Stream.newStream();
-            ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
-            forkedChannelEncoder.stdOut( "", true );
-
-            LineNumberReader lines = out.newReader( UTF_8 );
-            ForkedChannelDecoder decoder = new ForkedChannelDecoder();
-            decoder.setStdOutListener( new StandardOutErrEventAssertionListener( NORMAL_RUN, "", true ) );
-            AssertionErrorHandler errorHandler = mock( AssertionErrorHandler.class );
-            decoder.handleEvent( lines.readLine(), errorHandler );
-            verifyZeroInteractions( errorHandler );
-            assertThat( lines.readLine() )
-                    .isNull();
-        }
-
-        @Test
-        public void testStdOutStreamPrintlnWithNull() throws IOException
-        {
-            Stream out = Stream.newStream();
-            ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
-            forkedChannelEncoder.stdOut( null, true );
-
-            LineNumberReader lines = out.newReader( UTF_8 );
-            ForkedChannelDecoder decoder = new ForkedChannelDecoder();
-            decoder.setStdOutListener( new StandardOutErrEventAssertionListener( NORMAL_RUN, null, true ) );
-            AssertionErrorHandler errorHandler = mock( AssertionErrorHandler.class );
-            decoder.handleEvent( lines.readLine(), errorHandler );
-            verifyZeroInteractions( errorHandler );
-            assertThat( lines.readLine() )
-                    .isNull();
-        }
-
-        @Test
-        public void testStdErrStream() throws IOException
-        {
-            Stream out = Stream.newStream();
-            ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
-            forkedChannelEncoder.stdErr( "msg", false );
-
-            LineNumberReader lines = out.newReader( UTF_8 );
-            ForkedChannelDecoder decoder = new ForkedChannelDecoder();
-            decoder.setStdErrListener( new StandardOutErrEventAssertionListener( NORMAL_RUN, "msg", false ) );
-            AssertionErrorHandler errorHandler = mock( AssertionErrorHandler.class );
-            decoder.handleEvent( lines.readLine(), errorHandler );
-            verifyZeroInteractions( errorHandler );
-            assertThat( lines.readLine() )
-                    .isNull();
-        }
-
-        @Test
-        public void shouldCountSameNumberOfSystemProperties() throws IOException
-        {
-            Stream out = Stream.newStream();
-            ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
-            forkedChannelEncoder.sendSystemProperties( ObjectUtils.systemProps() );
-
-            LineNumberReader lines = out.newReader( UTF_8 );
-            ForkedChannelDecoder decoder = new ForkedChannelDecoder();
-            decoder.setSystemPropertiesListener( new PropertyEventAssertionListener() );
-            AssertionErrorHandler errorHandler = mock( AssertionErrorHandler.class );
-            decoder.handleEvent( lines.readLine(), errorHandler );
-            verifyZeroInteractions( errorHandler );
-        }
-
-        @Test
-        public void shouldHandleErrorAfterNullLine()
-        {
-            ForkedChannelDecoder decoder = new ForkedChannelDecoder();
-            decoder.setSystemPropertiesListener( new PropertyEventAssertionListener() );
-            AssertionErrorHandler errorHandler = mock( AssertionErrorHandler.class );
-            decoder.handleEvent( null, errorHandler );
-            verify( errorHandler, times( 1 ) )
-                    .handledError( nullable( String.class ), nullable( Throwable.class ) );
-        }
-
-        @Test
-        public void shouldHandleErrorAfterUnknownOperation()
-        {
-            ForkedChannelDecoder decoder = new ForkedChannelDecoder();
-            decoder.setSystemPropertiesListener( new PropertyEventAssertionListener() );
-            AssertionErrorHandler errorHandler = mock( AssertionErrorHandler.class );
-            decoder.handleEvent( ":maven:surefire:std:out:abnormal-run:-", errorHandler );
-            verify( errorHandler, times( 1 ) )
-                    .handledError( eq( ":maven:surefire:std:out:abnormal-run:-" ), nullable( Throwable.class ) );
-        }
-
-        @Test
-        public void shouldHandleExit() throws IOException
-        {
-            Stream out = Stream.newStream();
-            ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
-            StackTraceWriter stackTraceWriter = mock( StackTraceWriter.class );
-            when( stackTraceWriter.getThrowable() ).thenReturn( new SafeThrowable( "1" ) );
-            when( stackTraceWriter.smartTrimmedStackTrace() ).thenReturn( "2" );
-            when( stackTraceWriter.writeTraceToString() ).thenReturn( "3" );
-            when( stackTraceWriter.writeTrimmedTraceToString() ).thenReturn( "4" );
-            forkedChannelEncoder.sendExitEvent( stackTraceWriter, false );
-
-            LineNumberReader lines = out.newReader( UTF_8 );
-            ForkedChannelDecoder decoder = new ForkedChannelDecoder();
-            decoder.setExitErrorEventListener( new ForkedProcessExitErrorListener()
-            {
-                @Override
-                public void handle( String exceptionMessage, String smartTrimmedStackTrace, String stackTrace )
-                {
-                    assertThat( exceptionMessage ).isEqualTo( "1" );
-                    assertThat( smartTrimmedStackTrace ).isEqualTo( "2" );
-                    assertThat( stackTrace ).isEqualTo( "3" );
-                }
-            } );
-            AssertionErrorHandler errorHandler = mock( AssertionErrorHandler.class );
-            decoder.handleEvent( lines.readLine(), errorHandler );
-            verifyZeroInteractions( errorHandler );
-        }
-    }
-
-    /**
-     *
-     */
-    @RunWith( Theories.class )
-    public static class ReportEntryTest
-    {
-        @DataPoints( value = "operation" )
-        @SuppressWarnings( "checkstyle:visibilitymodifier" )
-        public static String[][] operations = { { "testSetStarting", "setTestSetStartingListener" },
-                                                { "testSetCompleted", "setTestSetCompletedListener" },
-                                                { "testStarting", "setTestStartingListener" },
-                                                { "testSucceeded", "setTestSucceededListener" },
-                                                { "testFailed", "setTestFailedListener" },
-                                                { "testSkipped", "setTestSkippedListener" },
-                                                { "testError", "setTestErrorListener" },
-                                                { "testAssumptionFailure", "setTestAssumptionFailureListener" }
-        };
-
-        @DataPoints( value = "reportedMessage" )
-        @SuppressWarnings( "checkstyle:visibilitymodifier" )
-        public static String[] reportedMessage = { null, "skipped test" };
-
-        @DataPoints( value = "elapsed" )
-        @SuppressWarnings( { "checkstyle:visibilitymodifier", "checkstyle:magicnumber" } )
-        public static Integer[] elapsed = { null, 102 };
-
-        @DataPoints( value = "trim" )
-        @SuppressWarnings( "checkstyle:visibilitymodifier" )
-        public static boolean[] trim = { false, true };
-
-        @DataPoints( value = "msg" )
-        @SuppressWarnings( "checkstyle:visibilitymodifier" )
-        public static boolean[] msg = { false, true };
-
-        @DataPoints( value = "smart" )
-        @SuppressWarnings( "checkstyle:visibilitymodifier" )
-        public static boolean[] smart = { false, true };
-
-        @DataPoints( value = "trace" )
-        @SuppressWarnings( "checkstyle:visibilitymodifier" )
-        public static boolean[] trace = { false, true };
-
-        @Theory
-        public void testReportEntryOperations( @FromDataPoints( "operation" ) String[] operation,
-                                               @FromDataPoints( "reportedMessage" ) String reportedMessage,
-                                               @FromDataPoints( "elapsed" ) Integer elapsed,
-                                               @FromDataPoints( "trim" ) boolean trim,
-                                               @FromDataPoints( "msg" ) boolean msg,
-                                               @FromDataPoints( "smart" ) boolean smart,
-                                               @FromDataPoints( "trace" ) boolean trace )
-                throws Exception
-        {
-            String exceptionMessage = msg ? "msg" : null;
-            String smartStackTrace = smart ? "MyTest:86 >> Error" : null;
-            String exceptionStackTrace =
-                    trace ? ( trim ? "trace line 1\ntrace line 2" : "Exception: msg\ntrace line 1\ntrace line 2" )
-                            : null;
-
-            StackTraceWriter stackTraceWriter = null;
-            if ( exceptionStackTrace != null )
-            {
-                SafeThrowable safeThrowable = new SafeThrowable( exceptionMessage );
-                stackTraceWriter = mock( StackTraceWriter.class );
-                when( stackTraceWriter.getThrowable() ).thenReturn( safeThrowable );
-                when( stackTraceWriter.smartTrimmedStackTrace() ).thenReturn( smartStackTrace );
-                when( stackTraceWriter.writeTrimmedTraceToString() ).thenReturn( exceptionStackTrace );
-                when( stackTraceWriter.writeTraceToString() ).thenReturn( exceptionStackTrace );
-            }
-
-            ReportEntry reportEntry = mock( ReportEntry.class );
-            when( reportEntry.getElapsed() ).thenReturn( elapsed );
-            when( reportEntry.getGroup() ).thenReturn( "this group" );
-            when( reportEntry.getMessage() ).thenReturn( reportedMessage );
-            when( reportEntry.getName() ).thenReturn( "my test" );
-            when( reportEntry.getName() ).thenReturn( "display name of test" );
-            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 );
-
-            Stream out = Stream.newStream();
-
-            ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
-
-            ForkedChannelEncoder.class.getMethod( operation[0], ReportEntry.class, boolean.class )
-                    .invoke( forkedChannelEncoder, reportEntry, trim );
-
-            ForkedChannelDecoder decoder = new ForkedChannelDecoder();
-
-            ForkedChannelDecoder.class.getMethod( operation[1], ForkedProcessReportEventListener.class )
-                    .invoke( decoder, new ReportEventAssertionListener( reportEntry ) );
-
-            AssertionErrorHandler errorHandler = mock( AssertionErrorHandler.class );
-            decoder.handleEvent( out.newReader( UTF_8 ).readLine(), errorHandler );
-            verifyZeroInteractions( errorHandler );
-        }
-    }
-
-    private static class AssertionErrorHandler implements ForkedChannelDecoderErrorHandler
-    {
-        public void handledError( String line, Throwable e )
-        {
-            if ( e != null )
-            {
-                e.printStackTrace();
-            }
-            fail( line + ( e == null ? "" : "\n" + e.getLocalizedMessage() ) );
-        }
-    }
-
-    private static class PropertyEventAssertionListener implements ForkedProcessPropertyEventListener
-    {
-        private final Map sysProps = System.getProperties();
-
-        public void handle( RunMode runMode, String key, String value )
-        {
-            assertThat( runMode ).isEqualTo( NORMAL_RUN );
-            assertTrue( sysProps.containsKey( key ) );
-            assertThat( sysProps.get( key ) ).isEqualTo( value );
-        }
-    }
-
-    private static class EventAssertionListener implements ForkedProcessEventListener
-    {
-        public void handle()
-        {
-        }
-    }
-
-    private static class StringEventAssertionListener implements ForkedProcessStringEventListener
-    {
-        private final String msg;
-
-        StringEventAssertionListener( String msg )
-        {
-            this.msg = msg;
-        }
-
-        public void handle( String msg )
-        {
-            assertThat( msg )
-                    .isEqualTo( this.msg );
-        }
-    }
-
-    private static class StackTraceEventListener implements ForkedProcessStackTraceEventListener
-    {
-        private final String msg;
-        private final String smartStackTrace;
-        private final String stackTrace;
-
-        StackTraceEventListener( String msg, String smartStackTrace, String stackTrace )
-        {
-            this.msg = msg;
-            this.smartStackTrace = smartStackTrace;
-            this.stackTrace = stackTrace;
-        }
-
-        @Override
-        public void handle( String msg, String smartStackTrace, String stackTrace )
-        {
-            assertThat( msg )
-                    .isEqualTo( this.msg );
-
-            assertThat( smartStackTrace )
-                    .isEqualTo( this.smartStackTrace );
-
-            assertThat( stackTrace )
-                    .isEqualTo( this.stackTrace );
-        }
-    }
-
-    private static class StandardOutErrEventAssertionListener implements ForkedProcessStandardOutErrEventListener
-    {
-        private final RunMode runMode;
-        private final String output;
-        private final boolean newLine;
-
-        StandardOutErrEventAssertionListener( RunMode runMode, String output, boolean newLine )
-        {
-            this.runMode = runMode;
-            this.output = output;
-            this.newLine = newLine;
-        }
-
-        public void handle( RunMode runMode, String output, boolean newLine )
-        {
-            assertThat( runMode )
-                    .isEqualTo( this.runMode );
-
-            assertThat( output )
-                    .isEqualTo( this.output );
-
-            assertThat( newLine )
-                    .isEqualTo( this.newLine );
-        }
-    }
-
-    private static class ReportEventAssertionListener implements ForkedProcessReportEventListener
-    {
-        private final ReportEntry reportEntry;
-
-        ReportEventAssertionListener( ReportEntry reportEntry )
-        {
-            this.reportEntry = reportEntry;
-        }
-
-        public void handle( RunMode runMode, ReportEntry reportEntry )
-        {
-            assertThat( reportEntry.getSourceName() ).isEqualTo( this.reportEntry.getSourceName() );
-            assertThat( reportEntry.getSourceText() ).isEqualTo( this.reportEntry.getSourceText() );
-            assertThat( reportEntry.getName() ).isEqualTo( this.reportEntry.getName() );
-            assertThat( reportEntry.getNameText() ).isEqualTo( this.reportEntry.getNameText() );
-            assertThat( reportEntry.getGroup() ).isEqualTo( this.reportEntry.getGroup() );
-            assertThat( reportEntry.getMessage() ).isEqualTo( this.reportEntry.getMessage() );
-            assertThat( reportEntry.getElapsed() ).isEqualTo( this.reportEntry.getElapsed() );
-            if ( reportEntry.getStackTraceWriter() == null )
-            {
-                assertThat( this.reportEntry.getStackTraceWriter() ).isNull();
-            }
-            else
-            {
-                assertThat( this.reportEntry.getStackTraceWriter() ).isNotNull();
-
-                assertThat( reportEntry.getStackTraceWriter().getThrowable().getMessage() )
-                        .isEqualTo( this.reportEntry.getStackTraceWriter().getThrowable().getMessage() );
-
-                assertThat( reportEntry.getStackTraceWriter().getThrowable().getLocalizedMessage() )
-                        .isEqualTo( this.reportEntry.getStackTraceWriter().getThrowable().getLocalizedMessage() );
-
-                assertThat( reportEntry.getStackTraceWriter().smartTrimmedStackTrace() )
-                        .isEqualTo( this.reportEntry.getStackTraceWriter().smartTrimmedStackTrace() );
-            }
-        }
-    }
-
-    private static class Stream extends PrintStream
-    {
-        private final ByteArrayOutputStream out;
-
-        Stream( ByteArrayOutputStream out )
-        {
-            super( out, true );
-            this.out = out;
-        }
-
-        byte[] toByteArray()
-        {
-            return out.toByteArray();
-        }
-
-        LineNumberReader newReader( Charset streamCharset )
-        {
-            return new LineNumberReader( new StringReader( new String( toByteArray(), streamCharset ) ) );
-        }
-
-        static Stream newStream()
-        {
-            return new Stream( new ByteArrayOutputStream() );
-        }
-    }
-
-    private static byte[] toArray( ByteBuffer buffer )
-    {
-        return Arrays.copyOfRange( buffer.array(), buffer.arrayOffset(), buffer.arrayOffset() + buffer.remaining() );
-    }
-}
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/surefire/extensions/ConsoleOutputReporterTest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/ConsoleOutputReporterTest.java
similarity index 95%
rename from maven-surefire-common/src/test/java/org/apache/maven/surefire/extensions/ConsoleOutputReporterTest.java
rename to maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/ConsoleOutputReporterTest.java
index 546e554..372c9a8 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/surefire/extensions/ConsoleOutputReporterTest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/ConsoleOutputReporterTest.java
@@ -1,4 +1,4 @@
-package org.apache.maven.surefire.extensions;
+package org.apache.maven.plugin.surefire.extensions;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,10 +19,12 @@ package org.apache.maven.surefire.extensions;
  * under the License.
  */
 
-import org.apache.maven.plugin.surefire.extensions.SurefireConsoleOutputReporter;
+import org.apache.maven.surefire.extensions.ConsoleOutputReportEventListener;
+import org.apache.maven.surefire.extensions.ConsoleOutputReporter;
 import org.apache.maven.plugin.surefire.extensions.junit5.JUnit5ConsoleOutputReporter;
 import org.apache.maven.plugin.surefire.report.ConsoleOutputFileReporter;
 import org.apache.maven.plugin.surefire.report.DirectConsoleOutput;
+import org.fest.assertions.Assertions;
 import org.junit.Test;
 
 import java.io.File;
@@ -49,7 +51,7 @@ public class ConsoleOutputReporterTest
                 .isInstanceOf( SurefireConsoleOutputReporter.class );
         assertThat( clone.toString() )
                 .isEqualTo( "SurefireConsoleOutputReporter{disable=true, encoding=ISO-8859-1}" );
-        assertThat( ( (SurefireConsoleOutputReporter) clone ).isDisable() )
+        Assertions.assertThat( ( (SurefireConsoleOutputReporter) clone ).isDisable() )
                 .isTrue();
         assertThat( ( (SurefireConsoleOutputReporter) clone ).getEncoding() )
                 .isEqualTo( "ISO-8859-1" );
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
new file mode 100644
index 0000000..cb2e382
--- /dev/null
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/ForkedProcessEventNotifierTest.java
@@ -0,0 +1,1183 @@
+package org.apache.maven.plugin.surefire.extensions;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.apache.maven.plugin.surefire.booterclient.output.DeserializedStacktraceWriter;
+import org.apache.maven.plugin.surefire.booterclient.output.ForkedProcessEventListener;
+import org.apache.maven.plugin.surefire.booterclient.output.ForkedProcessEventNotifier;
+import org.apache.maven.plugin.surefire.booterclient.output.ForkedProcessExitErrorListener;
+import org.apache.maven.plugin.surefire.booterclient.output.ForkedProcessPropertyEventListener;
+import org.apache.maven.plugin.surefire.booterclient.output.ForkedProcessReportEventListener;
+import org.apache.maven.plugin.surefire.booterclient.output.ForkedProcessStackTraceEventListener;
+import org.apache.maven.plugin.surefire.booterclient.output.ForkedProcessStandardOutErrEventListener;
+import org.apache.maven.plugin.surefire.booterclient.output.ForkedProcessStringEventListener;
+import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
+import org.apache.maven.plugin.surefire.log.api.ConsoleLoggerUtils;
+import org.apache.maven.surefire.booter.spi.LegacyMasterProcessChannelEncoder;
+import org.apache.maven.surefire.eventapi.Event;
+import org.apache.maven.surefire.extensions.EventHandler;
+import org.apache.maven.surefire.extensions.util.CountdownCloseable;
+import org.apache.maven.surefire.report.ReportEntry;
+import org.apache.maven.surefire.report.RunMode;
+import org.apache.maven.surefire.report.SafeThrowable;
+import org.apache.maven.surefire.report.StackTraceWriter;
+import org.apache.maven.surefire.util.internal.ObjectUtils;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.experimental.runners.Enclosed;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.powermock.core.classloader.annotations.PowerMockIgnore;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import javax.annotation.Nonnull;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.LineNumberReader;
+import java.io.PrintStream;
+import java.io.StringReader;
+import java.nio.ByteBuffer;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.charset.Charset;
+import java.util.Map;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedTransferQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static java.nio.channels.Channels.newChannel;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Arrays.copyOfRange;
+import static org.apache.commons.codec.binary.Base64.encodeBase64String;
+import static org.apache.maven.surefire.report.RunMode.NORMAL_RUN;
+import static org.fest.assertions.Assertions.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.rules.ExpectedException.none;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test for {@link ForkedProcessEventNotifier}.
+ *
+ * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
+ * @since 3.0.0-M4
+ */
+@RunWith( Enclosed.class )
+public class ForkedProcessEventNotifierTest
+{
+    /**
+     *
+     */
+    @RunWith( PowerMockRunner.class )
+    @PowerMockIgnore( { "org.jacoco.agent.rt.*", "com.vladium.emma.rt.*" } )
+    public static class DecoderOperationsTest
+    {
+        @Rule
+        public final ExpectedException rule = none();
+
+        @Test
+        public void shouldBeFailSafe()
+        {
+            assertThat( EventConsumerThread.decode( null, UTF_8 ) ).isNull();
+            assertThat( EventConsumerThread.decode( "-", UTF_8 ) ).isNull();
+            assertThat( EventConsumerThread.decodeToInteger( null ) ).isNull();
+            assertThat( EventConsumerThread.decodeToInteger( "-" ) ).isNull();
+        }
+
+        @Test
+        public void shouldHaveSystemProperty() throws Exception
+        {
+            final Stream out = Stream.newStream();
+            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newChannel( out ) );
+            Map<String, String> props = ObjectUtils.systemProps();
+            encoder.sendSystemProperties( props );
+
+            ForkedProcessEventNotifier notifier = new ForkedProcessEventNotifier();
+            PropertyEventAssertionListener listener = new PropertyEventAssertionListener();
+            notifier.setSystemPropertiesListener( listener );
+
+            ReadableByteChannel channel = newChannel( new ByteArrayInputStream( out.toByteArray() ) );
+
+            EH eventHandler = new EH();
+            CountdownCloseable countdown = new CountdownCloseable( mock( Closeable.class ), 0 );
+            ConsoleLogger logger = mock( ConsoleLogger.class );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, logger ) )
+            {
+                t.start();
+                for ( int i = 0; i < props.size(); i++ )
+                {
+                    notifier.notifyEvent( eventHandler.pullEvent() );
+                }
+            }
+
+            assertThat( listener.counter.get() )
+                .isEqualTo( props.size() );
+        }
+
+        @Test
+        public void shouldRecognizeEmptyStream4ReportEntry()
+        {
+            ReportEntry reportEntry = EventConsumerThread.decodeReportEntry( null, null, null, "", "", null, null, "",
+                    "", "", null );
+            assertThat( reportEntry ).isNull();
+
+            reportEntry = EventConsumerThread.decodeReportEntry( UTF_8, "", "", "", "", "", "", "-", "", "", "" );
+            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();
+
+            rule.expect( NumberFormatException.class );
+            EventConsumerThread.decodeReportEntry( UTF_8, "", "", "", "", "", "", "", "", "", "" );
+        }
+
+        @Test
+        @SuppressWarnings( "checkstyle:magicnumber" )
+        public void testCreatingReportEntry()
+        {
+            final String exceptionMessage = "msg";
+            final String encodedExceptionMsg = encodeBase64String( toArray( UTF_8.encode( exceptionMessage ) ) );
+
+            final String smartStackTrace = "MyTest:86 >> Error";
+            final String encodedSmartStackTrace = encodeBase64String( toArray( UTF_8.encode( smartStackTrace ) ) );
+
+            final String stackTrace = "Exception: msg\ntrace line 1\ntrace line 2";
+            final String encodedStackTrace = encodeBase64String( toArray( UTF_8.encode( stackTrace ) ) );
+
+            final String trimmedStackTrace = "trace line 1\ntrace line 2";
+            final String encodedTrimmedStackTrace = encodeBase64String( toArray( UTF_8.encode( trimmedStackTrace ) ) );
+
+            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 );
+
+            String encodedSourceName = encodeBase64String( toArray( UTF_8.encode( reportEntry.getSourceName() ) ) );
+            String encodedSourceText = encodeBase64String( toArray( UTF_8.encode( reportEntry.getSourceText() ) ) );
+            String encodedName = encodeBase64String( toArray( UTF_8.encode( reportEntry.getName() ) ) );
+            String encodedText = encodeBase64String( toArray( UTF_8.encode( reportEntry.getNameText() ) ) );
+            String encodedGroup = encodeBase64String( toArray( UTF_8.encode( reportEntry.getGroup() ) ) );
+            String encodedMessage = encodeBase64String( toArray( UTF_8.encode( reportEntry.getMessage() ) ) );
+
+            ReportEntry decodedReportEntry = EventConsumerThread.decodeReportEntry( UTF_8, encodedSourceName,
+                encodedSourceText, encodedName, encodedText, encodedGroup, encodedMessage, "-", 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.decodeReportEntry( UTF_8, encodedSourceName, encodedSourceText,
+                encodedName, encodedText, encodedGroup, encodedMessage, "-", encodedExceptionMsg,
+                encodedSmartStackTrace, 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.decodeReportEntry( UTF_8, encodedSourceName, encodedSourceText,
+                encodedName, encodedText, encodedGroup, encodedMessage, "1003", encodedExceptionMsg,
+                encodedSmartStackTrace, 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.decodeReportEntry( UTF_8, encodedSourceName, encodedSourceText,
+                encodedName, encodedText, encodedGroup, encodedMessage, "1003", encodedExceptionMsg,
+                encodedSmartStackTrace, encodedStackTrace );
+
+            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.decodeReportEntry( UTF_8, encodedSourceName, encodedSourceText,
+                encodedName, encodedText, encodedGroup, encodedMessage, "1003", encodedExceptionMsg,
+                encodedSmartStackTrace, encodedTrimmedStackTrace );
+
+            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( newChannel( out ) );
+            encoder.bye();
+            String read = new String( out.toByteArray(), UTF_8 );
+
+            assertThat( read )
+                    .isEqualTo( ":maven-surefire-event:bye:\n" );
+
+            LineNumberReader lines = out.newReader( UTF_8 );
+
+            final String cmd = lines.readLine();
+            assertThat( cmd )
+                    .isNotNull();
+
+            ReadableByteChannel channel = newChannel( new ByteArrayInputStream( out.toByteArray() ) );
+
+            ForkedProcessEventNotifier notifier = new ForkedProcessEventNotifier();
+            EventAssertionListener listener = new EventAssertionListener();
+            notifier.setByeListener( listener );
+
+            EH eventHandler = new EH();
+            CountdownCloseable countdown = new CountdownCloseable( mock( Closeable.class ), 0 );
+            ConsoleLogger logger = mock( ConsoleLogger.class );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, logger ) )
+            {
+                t.start();
+                notifier.notifyEvent( eventHandler.pullEvent() );
+            }
+
+            assertThat( listener.called.get() )
+                .isTrue();
+        }
+
+        @Test
+        public void shouldSendStopOnNextTestEvent() throws Exception
+        {
+            Stream out = Stream.newStream();
+            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newChannel( out ) );
+            encoder.stopOnNextTest();
+            String read = new String( out.toByteArray(), UTF_8 );
+
+            assertThat( read )
+                    .isEqualTo( ":maven-surefire-event:stop-on-next-test:\n" );
+
+            LineNumberReader lines = out.newReader( UTF_8 );
+
+            final String cmd = lines.readLine();
+            assertThat( cmd )
+                .isNotNull();
+
+            ReadableByteChannel channel = newChannel( new ByteArrayInputStream( out.toByteArray() ) );
+
+            ForkedProcessEventNotifier notifier = new ForkedProcessEventNotifier();
+            EventAssertionListener listener = new EventAssertionListener();
+            notifier.setStopOnNextTestListener( listener );
+
+            EH eventHandler = new EH();
+            CountdownCloseable countdown = new CountdownCloseable( mock( Closeable.class ), 0 );
+            ConsoleLogger logger = mock( ConsoleLogger.class );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, logger ) )
+            {
+                t.start();
+                notifier.notifyEvent( eventHandler.pullEvent() );
+            }
+
+            assertThat( listener.called.get() )
+                .isTrue();
+        }
+
+        @Test
+        public void shouldCorrectlyDecodeStackTracesWithEmptyStringTraceMessages() throws Exception
+        {
+            String exceptionMessage = "";
+            String smartStackTrace = "JUnit5Test.failWithEmptyString:16";
+            String exceptionStackTrace = "org.opentest4j.AssertionFailedError: \n"
+                    + "\tat JUnit5Test.failWithEmptyString(JUnit5Test.java:16)\n";
+
+            StackTraceWriter stackTraceWriter = mock( StackTraceWriter.class );
+            SafeThrowable safeThrowable = new SafeThrowable( exceptionMessage );
+            when( stackTraceWriter.getThrowable() ).thenReturn( safeThrowable );
+            when( stackTraceWriter.smartTrimmedStackTrace() ).thenReturn( smartStackTrace );
+            when( stackTraceWriter.writeTrimmedTraceToString() ).thenReturn( exceptionStackTrace );
+            when( stackTraceWriter.writeTraceToString() ).thenReturn( exceptionStackTrace );
+
+            ReportEntry reportEntry = mock( ReportEntry.class );
+            when( reportEntry.getElapsed() ).thenReturn( 7 );
+            when( reportEntry.getGroup() ).thenReturn( null );
+            when( reportEntry.getMessage() ).thenReturn( null );
+            when( reportEntry.getName() ).thenReturn( "failWithEmptyString" );
+            when( reportEntry.getNameWithGroup() ).thenReturn( "JUnit5Test" );
+            when( reportEntry.getSourceName() ).thenReturn( "JUnit5Test" );
+            when( reportEntry.getSourceText() ).thenReturn( null );
+            when( reportEntry.getStackTraceWriter() ).thenReturn( stackTraceWriter );
+
+            final Stream out = Stream.newStream();
+            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newChannel( out ) );
+            encoder.testFailed( reportEntry, true );
+
+            ReadableByteChannel channel = newChannel( new ByteArrayInputStream( out.toByteArray() ) );
+
+            final ForkedProcessEventNotifier notifier = new ForkedProcessEventNotifier();
+            ReportEventAssertionListener listener = new ReportEventAssertionListener( reportEntry, true );
+            notifier.setTestFailedListener( listener );
+
+            EH eventHandler = new EH();
+            CountdownCloseable countdown = new CountdownCloseable( mock( Closeable.class ), 0 );
+            ConsoleLogger logger = mock( ConsoleLogger.class );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, logger ) )
+            {
+                t.start();
+                notifier.notifyEvent( eventHandler.pullEvent() );
+            }
+
+            assertThat( listener.called.get() )
+                .isTrue();
+        }
+
+        @Test
+        public void shouldSendNextTestEvent() throws Exception
+        {
+            final Stream out = Stream.newStream();
+            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newChannel( out ) );
+            encoder.acquireNextTest();
+            String read = new String( out.toByteArray(), UTF_8 );
+
+            assertThat( read )
+                    .isEqualTo( ":maven-surefire-event:next-test:\n" );
+
+            ReadableByteChannel channel = newChannel( new ByteArrayInputStream( out.toByteArray() ) );
+
+            ForkedProcessEventNotifier notifier = new ForkedProcessEventNotifier();
+            EventAssertionListener listener = new EventAssertionListener();
+            notifier.setAcquireNextTestListener( listener );
+
+            EH eventHandler = new EH();
+            CountdownCloseable countdown = new CountdownCloseable( mock( Closeable.class ), 0 );
+            ConsoleLogger logger = mock( ConsoleLogger.class );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, logger ) )
+            {
+                t.start();
+                notifier.notifyEvent( eventHandler.pullEvent() );
+            }
+
+            assertThat( listener.called.get() )
+                .isTrue();
+        }
+
+        @Test
+        public void testConsole() throws Exception
+        {
+            final Stream out = Stream.newStream();
+            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newChannel( out ) );
+            encoder.consoleInfoLog( "msg" );
+
+            ReadableByteChannel channel = newChannel( new ByteArrayInputStream( out.toByteArray() ) );
+
+            ForkedProcessEventNotifier notifier = new ForkedProcessEventNotifier();
+            StringEventAssertionListener listener = new StringEventAssertionListener( "msg" );
+            notifier.setConsoleInfoListener( listener );
+
+            EH eventHandler = new EH();
+            CountdownCloseable countdown = new CountdownCloseable( mock( Closeable.class ), 0 );
+            ConsoleLogger logger = mock( ConsoleLogger.class );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, logger ) )
+            {
+                t.start();
+                notifier.notifyEvent( eventHandler.pullEvent() );
+            }
+
+            assertThat( listener.called.get() )
+                .isTrue();
+        }
+
+        @Test
+        public void testError() throws Exception
+        {
+            final Stream out = Stream.newStream();
+            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newChannel( out ) );
+            encoder.consoleErrorLog( "msg" );
+
+            ReadableByteChannel channel = newChannel( new ByteArrayInputStream( out.toByteArray() ) );
+
+            ForkedProcessEventNotifier notifier = new ForkedProcessEventNotifier();
+            StackTraceEventListener listener = new StackTraceEventListener( "msg", null, null );
+            notifier.setConsoleErrorListener( listener );
+
+            EH eventHandler = new EH();
+            CountdownCloseable countdown = new CountdownCloseable( mock( Closeable.class ), 0 );
+            ConsoleLogger logger = mock( ConsoleLogger.class );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, logger ) )
+            {
+                t.start();
+                notifier.notifyEvent( eventHandler.pullEvent() );
+            }
+
+            assertThat( listener.called.get() )
+                .isTrue();
+        }
+
+        @Test
+        public void testErrorWithException() throws Exception
+        {
+            final Stream out = Stream.newStream();
+            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newChannel( out ) );
+            Throwable throwable = new Throwable( "msg" );
+            encoder.consoleErrorLog( throwable );
+
+            ReadableByteChannel channel = newChannel( new ByteArrayInputStream( out.toByteArray() ) );
+
+            ForkedProcessEventNotifier notifier = new ForkedProcessEventNotifier();
+            String stackTrace = ConsoleLoggerUtils.toString( throwable );
+            StackTraceEventListener listener = new StackTraceEventListener( "msg", null, stackTrace );
+            notifier.setConsoleErrorListener( listener );
+
+            EH eventHandler = new EH();
+            CountdownCloseable countdown = new CountdownCloseable( mock( Closeable.class ), 0 );
+            ConsoleLogger logger = mock( ConsoleLogger.class );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, logger ) )
+            {
+                t.start();
+                notifier.notifyEvent( eventHandler.pullEvent() );
+            }
+
+            assertThat( listener.called.get() )
+                .isTrue();
+        }
+
+        @Test
+        public void testErrorWithStackTraceWriter() throws Exception
+        {
+            final Stream out = Stream.newStream();
+
+            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newChannel( out ) );
+            StackTraceWriter stackTraceWriter = new DeserializedStacktraceWriter( "1", "2", "3" );
+            encoder.consoleErrorLog( stackTraceWriter, false );
+
+            ReadableByteChannel channel = newChannel( new ByteArrayInputStream( out.toByteArray() ) );
+
+            ForkedProcessEventNotifier notifier = new ForkedProcessEventNotifier();
+            StackTraceEventListener listener = new StackTraceEventListener( "1", "2", "3" );
+            notifier.setConsoleErrorListener( listener );
+
+            EH eventHandler = new EH();
+            CountdownCloseable countdown = new CountdownCloseable( mock( Closeable.class ), 0 );
+            ConsoleLogger logger = mock( ConsoleLogger.class );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, logger ) )
+            {
+                t.start();
+                notifier.notifyEvent( eventHandler.pullEvent() );
+            }
+
+            assertThat( listener.called.get() )
+                .isTrue();
+        }
+
+        @Test
+        public void testDebug() throws Exception
+        {
+            final Stream out = Stream.newStream();
+
+            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newChannel( out ) );
+            encoder.consoleDebugLog( "msg" );
+
+            ReadableByteChannel channel = newChannel( new ByteArrayInputStream( out.toByteArray() ) );
+
+            ForkedProcessEventNotifier notifier = new ForkedProcessEventNotifier();
+            StringEventAssertionListener listener = new StringEventAssertionListener( "msg" );
+            notifier.setConsoleDebugListener( listener );
+
+            EH eventHandler = new EH();
+            CountdownCloseable countdown = new CountdownCloseable( mock( Closeable.class ), 0 );
+            ConsoleLogger logger = mock( ConsoleLogger.class );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, logger ) )
+            {
+                t.start();
+                notifier.notifyEvent( eventHandler.pullEvent() );
+            }
+
+            assertThat( listener.called.get() )
+                .isTrue();
+
+            assertThat( listener.msg )
+                .isEqualTo( "msg" );
+        }
+
+        @Test
+        public void testWarning() throws Exception
+        {
+            final Stream out = Stream.newStream();
+
+            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newChannel( out ) );
+            encoder.consoleWarningLog( "msg" );
+
+            ReadableByteChannel channel = newChannel( new ByteArrayInputStream( out.toByteArray() ) );
+
+            ForkedProcessEventNotifier notifier = new ForkedProcessEventNotifier();
+            StringEventAssertionListener listener = new StringEventAssertionListener( "msg" );
+            notifier.setConsoleWarningListener( listener );
+
+            EH eventHandler = new EH();
+            CountdownCloseable countdown = new CountdownCloseable( mock( Closeable.class ), 0 );
+            ConsoleLogger logger = mock( ConsoleLogger.class );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, logger ) )
+            {
+                t.start();
+                notifier.notifyEvent( eventHandler.pullEvent() );
+            }
+
+            assertThat( listener.called.get() )
+                .isTrue();
+        }
+
+        @Test
+        public void testStdOutStream() throws Exception
+        {
+            final Stream out = Stream.newStream();
+            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newChannel( out ) );
+            encoder.stdOut( "msg", false );
+
+            ReadableByteChannel channel = newChannel( new ByteArrayInputStream( out.toByteArray() ) );
+
+            ForkedProcessEventNotifier notifier = new ForkedProcessEventNotifier();
+            StandardOutErrEventAssertionListener listener =
+                new StandardOutErrEventAssertionListener( NORMAL_RUN, "msg", false );
+            notifier.setStdOutListener( listener );
+
+            EH eventHandler = new EH();
+            CountdownCloseable countdown = new CountdownCloseable( mock( Closeable.class ), 0 );
+            ConsoleLogger logger = mock( ConsoleLogger.class );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, logger ) )
+            {
+                t.start();
+                notifier.notifyEvent( eventHandler.pullEvent() );
+            }
+
+            assertThat( listener.called.get() )
+                .isTrue();
+        }
+
+        @Test
+        public void testStdOutStreamPrint() throws Exception
+        {
+            final Stream out = Stream.newStream();
+            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newChannel( out ) );
+            encoder.stdOut( "", false );
+
+            ReadableByteChannel channel = newChannel( new ByteArrayInputStream( out.toByteArray() ) );
+
+            ForkedProcessEventNotifier notifier = new ForkedProcessEventNotifier();
+            StandardOutErrEventAssertionListener listener =
+                new StandardOutErrEventAssertionListener( NORMAL_RUN, "", false );
+            notifier.setStdOutListener( listener );
+
+            EH eventHandler = new EH();
+            CountdownCloseable countdown = new CountdownCloseable( mock( Closeable.class ), 0 );
+            ConsoleLogger logger = mock( ConsoleLogger.class );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, logger ) )
+            {
+                t.start();
+                notifier.notifyEvent( eventHandler.pullEvent() );
+            }
+
+            assertThat( listener.called.get() )
+                .isTrue();
+        }
+
+        @Test
+        public void testStdOutStreamPrintWithNull() throws Exception
+        {
+            final Stream out = Stream.newStream();
+            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newChannel( out ) );
+            encoder.stdOut( null, false );
+
+            ReadableByteChannel channel = newChannel( new ByteArrayInputStream( out.toByteArray() ) );
+
+            ForkedProcessEventNotifier notifier = new ForkedProcessEventNotifier();
+            StandardOutErrEventAssertionListener listener =
+                new StandardOutErrEventAssertionListener( NORMAL_RUN, null, false );
+            notifier.setStdOutListener( listener );
+
+            EH eventHandler = new EH();
+            CountdownCloseable countdown = new CountdownCloseable( mock( Closeable.class ), 0 );
+            ConsoleLogger logger = mock( ConsoleLogger.class );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, logger ) )
+            {
+                t.start();
+                notifier.notifyEvent( eventHandler.pullEvent() );
+            }
+
+            assertThat( listener.called.get() )
+                .isTrue();
+        }
+
+        @Test
+        public void testStdOutStreamPrintln() throws Exception
+        {
+            final Stream out = Stream.newStream();
+            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newChannel( out ) );
+            encoder.stdOut( "", true );
+
+            ReadableByteChannel channel = newChannel( new ByteArrayInputStream( out.toByteArray() ) );
+
+            ForkedProcessEventNotifier notifier = new ForkedProcessEventNotifier();
+            StandardOutErrEventAssertionListener listener =
+                new StandardOutErrEventAssertionListener( NORMAL_RUN, "", true );
+            notifier.setStdOutListener( listener );
+
+            EH eventHandler = new EH();
+            CountdownCloseable countdown = new CountdownCloseable( mock( Closeable.class ), 0 );
+            ConsoleLogger logger = mock( ConsoleLogger.class );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, logger ) )
+            {
+                t.start();
+                notifier.notifyEvent( eventHandler.pullEvent() );
+            }
+
+            assertThat( listener.called.get() )
+                .isTrue();
+        }
+
+        @Test
+        public void testStdOutStreamPrintlnWithNull() throws Exception
+        {
+            final Stream out = Stream.newStream();
+            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newChannel( out ) );
+            encoder.stdOut( null, true );
+
+            ReadableByteChannel channel = newChannel( new ByteArrayInputStream( out.toByteArray() ) );
+
+            ForkedProcessEventNotifier notifier = new ForkedProcessEventNotifier();
+            StandardOutErrEventAssertionListener listener =
+                new StandardOutErrEventAssertionListener( NORMAL_RUN, null, true );
+            notifier.setStdOutListener( listener );
+
+            EH eventHandler = new EH();
+            CountdownCloseable countdown = new CountdownCloseable( mock( Closeable.class ), 0 );
+            ConsoleLogger logger = mock( ConsoleLogger.class );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, logger ) )
+            {
+                t.start();
+                notifier.notifyEvent( eventHandler.pullEvent() );
+            }
+
+            assertThat( listener.called.get() )
+                .isTrue();
+        }
+
+        @Test
+        public void testStdErrStream() throws Exception
+        {
+            final Stream out = Stream.newStream();
+            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newChannel( out ) );
+            encoder.stdErr( "msg", false );
+
+            ReadableByteChannel channel = newChannel( new ByteArrayInputStream( out.toByteArray() ) );
+
+            ForkedProcessEventNotifier notifier = new ForkedProcessEventNotifier();
+            StandardOutErrEventAssertionListener listener =
+                new StandardOutErrEventAssertionListener( NORMAL_RUN, "msg", false );
+            notifier.setStdErrListener( listener );
+
+            EH eventHandler = new EH();
+            CountdownCloseable countdown = new CountdownCloseable( mock( Closeable.class ), 0 );
+            ConsoleLogger logger = mock( ConsoleLogger.class );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, logger ) )
+            {
+                t.start();
+                notifier.notifyEvent( eventHandler.pullEvent() );
+            }
+
+            assertThat( listener.called.get() )
+                .isTrue();
+        }
+
+        @Test
+        public void shouldCountSameNumberOfSystemProperties() throws Exception
+        {
+            final Stream out = Stream.newStream();
+            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newChannel( out ) );
+            encoder.sendSystemProperties( ObjectUtils.systemProps() );
+
+            ReadableByteChannel channel = newChannel( new ByteArrayInputStream( out.toByteArray() ) );
+
+            ForkedProcessEventNotifier notifier = new ForkedProcessEventNotifier();
+            PropertyEventAssertionListener listener = new PropertyEventAssertionListener();
+            notifier.setSystemPropertiesListener( listener );
+
+            EH eventHandler = new EH();
+            CountdownCloseable countdown = new CountdownCloseable( mock( Closeable.class ), 0 );
+            ConsoleLogger logger = mock( ConsoleLogger.class );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, logger ) )
+            {
+                t.start();
+                notifier.notifyEvent( eventHandler.pullEvent() );
+            }
+
+            assertThat( listener.called.get() )
+                .isTrue();
+        }
+
+        @Test
+        public void shouldHandleErrorAfterNullLine()
+        {
+            ForkedProcessEventNotifier decoder = new ForkedProcessEventNotifier();
+            decoder.setSystemPropertiesListener( new PropertyEventAssertionListener() );
+            rule.expect( NullPointerException.class );
+            decoder.notifyEvent( null );
+        }
+
+        @Test
+        public void shouldHandleErrorAfterUnknownOperation() throws Exception
+        {
+            String cmd = ":maven-surefire-event:abnormal-run:-:\n";
+
+            ReadableByteChannel channel = newChannel( new ByteArrayInputStream( cmd.getBytes() ) );
+
+            EH eventHandler = new EH();
+            CountdownCloseable countdown = new CountdownCloseable( mock( Closeable.class ), 1 );
+            ConsoleLogger logger = mock( ConsoleLogger.class );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, logger ) )
+            {
+                t.start();
+                countdown.awaitClosed();
+            }
+
+            verify( logger, times( 1 ) )
+                .error( eq( ":maven-surefire-event:abnormal-run:" ) );
+        }
+
+        @Test
+        public void shouldHandleExit() throws Exception
+        {
+            final Stream out = Stream.newStream();
+            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newChannel( out ) );
+
+            StackTraceWriter stackTraceWriter = mock( StackTraceWriter.class );
+            when( stackTraceWriter.getThrowable() ).thenReturn( new SafeThrowable( "1" ) );
+            when( stackTraceWriter.smartTrimmedStackTrace() ).thenReturn( "2" );
+            when( stackTraceWriter.writeTraceToString() ).thenReturn( "3" );
+            when( stackTraceWriter.writeTrimmedTraceToString() ).thenReturn( "4" );
+            encoder.sendExitEvent( stackTraceWriter, false );
+
+            ReadableByteChannel channel = newChannel( new ByteArrayInputStream( out.toByteArray() ) );
+
+            ForkedProcessEventNotifier notifier = new ForkedProcessEventNotifier();
+            ProcessExitErrorListener listener = new ProcessExitErrorListener();
+            notifier.setExitErrorEventListener( listener );
+
+            EH eventHandler = new EH();
+            CountdownCloseable countdown = new CountdownCloseable( mock( Closeable.class ), 0 );
+            ConsoleLogger logger = mock( ConsoleLogger.class );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, logger ) )
+            {
+                t.start();
+                notifier.notifyEvent( eventHandler.pullEvent() );
+            }
+
+            assertThat( listener.called.get() )
+                .isTrue();
+        }
+    }
+
+    /**
+     *
+     */
+    @RunWith( Theories.class )
+    public static class ReportEntryTest
+    {
+        @DataPoints( value = "operation" )
+        @SuppressWarnings( "checkstyle:visibilitymodifier" )
+        public static String[][] operations = { { "testSetStarting", "setTestSetStartingListener" },
+                                                { "testSetCompleted", "setTestSetCompletedListener" },
+                                                { "testStarting", "setTestStartingListener" },
+                                                { "testSucceeded", "setTestSucceededListener" },
+                                                { "testFailed", "setTestFailedListener" },
+                                                { "testSkipped", "setTestSkippedListener" },
+                                                { "testError", "setTestErrorListener" },
+                                                { "testAssumptionFailure", "setTestAssumptionFailureListener" }
+        };
+
+        @DataPoints( value = "reportedMessage" )
+        @SuppressWarnings( "checkstyle:visibilitymodifier" )
+        public static String[] reportedMessage = { null, "skipped test" };
+
+        @DataPoints( value = "elapsed" )
+        @SuppressWarnings( { "checkstyle:visibilitymodifier", "checkstyle:magicnumber" } )
+        public static Integer[] elapsed = { null, 102 };
+
+        @DataPoints( value = "trim" )
+        @SuppressWarnings( "checkstyle:visibilitymodifier" )
+        public static boolean[] trim = { false, true };
+
+        @DataPoints( value = "msg" )
+        @SuppressWarnings( "checkstyle:visibilitymodifier" )
+        public static boolean[] msg = { false, true };
+
+        @DataPoints( value = "smart" )
+        @SuppressWarnings( "checkstyle:visibilitymodifier" )
+        public static boolean[] smart = { false, true };
+
+        @DataPoints( value = "trace" )
+        @SuppressWarnings( "checkstyle:visibilitymodifier" )
+        public static boolean[] trace = { false, true };
+
+        @Theory
+        public void testReportEntryOperations( @FromDataPoints( "operation" ) String[] operation,
+                                               @FromDataPoints( "reportedMessage" ) String reportedMessage,
+                                               @FromDataPoints( "elapsed" ) Integer elapsed,
+                                               @FromDataPoints( "trim" ) boolean trim,
+                                               @FromDataPoints( "msg" ) boolean msg,
+                                               @FromDataPoints( "smart" ) boolean smart,
+                                               @FromDataPoints( "trace" ) boolean trace )
+                throws Exception
+        {
+            String exceptionMessage = msg ? "msg" : null;
+            String smartStackTrace = smart ? "MyTest:86 >> Error" : null;
+            String exceptionStackTrace =
+                    trace ? ( trim ? "trace line 1\ntrace line 2" : "Exception: msg\ntrace line 1\ntrace line 2" )
+                            : null;
+
+            StackTraceWriter stackTraceWriter = null;
+            if ( exceptionStackTrace != null )
+            {
+                SafeThrowable safeThrowable = new SafeThrowable( exceptionMessage );
+                stackTraceWriter = mock( StackTraceWriter.class );
+                when( stackTraceWriter.getThrowable() ).thenReturn( safeThrowable );
+                when( stackTraceWriter.smartTrimmedStackTrace() ).thenReturn( smartStackTrace );
+                when( stackTraceWriter.writeTrimmedTraceToString() ).thenReturn( exceptionStackTrace );
+                when( stackTraceWriter.writeTraceToString() ).thenReturn( exceptionStackTrace );
+            }
+
+            ReportEntry reportEntry = mock( ReportEntry.class );
+            when( reportEntry.getElapsed() ).thenReturn( elapsed );
+            when( reportEntry.getGroup() ).thenReturn( "this group" );
+            when( reportEntry.getMessage() ).thenReturn( reportedMessage );
+            when( reportEntry.getName() ).thenReturn( "my test" );
+            when( reportEntry.getName() ).thenReturn( "display name of test" );
+            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 );
+
+            final Stream out = Stream.newStream();
+
+            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( newChannel( out ) );
+
+            LegacyMasterProcessChannelEncoder.class.getMethod( operation[0], ReportEntry.class, boolean.class )
+                    .invoke( encoder, reportEntry, trim );
+
+            ForkedProcessEventNotifier notifier = new ForkedProcessEventNotifier();
+
+            ForkedProcessEventNotifier.class.getMethod( operation[1], ForkedProcessReportEventListener.class )
+                    .invoke( notifier, new ReportEventAssertionListener( reportEntry, stackTraceWriter != null ) );
+
+            ReadableByteChannel channel = newChannel( new ByteArrayInputStream( out.toByteArray() ) );
+
+            EH eventHandler = new EH();
+            CountdownCloseable countdown = new CountdownCloseable( mock( Closeable.class ), 0 );
+            ConsoleLogger logger = mock( ConsoleLogger.class );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, logger ) )
+            {
+                t.start();
+                notifier.notifyEvent( eventHandler.pullEvent() );
+            }
+        }
+    }
+
+    private static class ProcessExitErrorListener implements ForkedProcessExitErrorListener
+    {
+        final AtomicBoolean called = new AtomicBoolean();
+
+        @Override
+        public void handle( StackTraceWriter stackTrace )
+        {
+            called.set( true );
+            assertThat( stackTrace.getThrowable().getMessage() ).isEqualTo( "1" );
+            assertThat( stackTrace.smartTrimmedStackTrace() ).isEqualTo( "2" );
+            assertThat( stackTrace.writeTraceToString() ).isEqualTo( "3" );
+        }
+    }
+
+    private static class PropertyEventAssertionListener implements ForkedProcessPropertyEventListener
+    {
+        final AtomicBoolean called = new AtomicBoolean();
+        private final Map<?, ?> sysProps = System.getProperties();
+        private final AtomicInteger counter = new AtomicInteger();
+
+        public void handle( RunMode runMode, String key, String value )
+        {
+            called.set( true );
+            counter.incrementAndGet();
+            assertThat( runMode ).isEqualTo( NORMAL_RUN );
+            assertTrue( sysProps.containsKey( key ) );
+            assertThat( sysProps.get( key ) ).isEqualTo( value );
+        }
+    }
+
+    private static class EventAssertionListener implements ForkedProcessEventListener
+    {
+        final AtomicBoolean called = new AtomicBoolean();
+
+        public void handle()
+        {
+            called.set( true );
+        }
+    }
+
+    private static class StringEventAssertionListener implements ForkedProcessStringEventListener
+    {
+        final AtomicBoolean called = new AtomicBoolean();
+        private final String msg;
+
+        StringEventAssertionListener( String msg )
+        {
+            this.msg = msg;
+        }
+
+        public void handle( String msg )
+        {
+            called.set( true );
+            assertThat( msg )
+                    .isEqualTo( this.msg );
+        }
+    }
+
+    private static class StackTraceEventListener implements ForkedProcessStackTraceEventListener
+    {
+        final AtomicBoolean called = new AtomicBoolean();
+        private final String msg;
+        private final String smartStackTrace;
+        private final String stackTrace;
+
+        StackTraceEventListener( String msg, String smartStackTrace, String stackTrace )
+        {
+            this.msg = msg;
+            this.smartStackTrace = smartStackTrace;
+            this.stackTrace = stackTrace;
+        }
+
+        @Override
+        public void handle( @Nonnull StackTraceWriter stackTrace )
+        {
+            called.set( true );
+
+            assertThat( stackTrace.getThrowable().getMessage() )
+                    .isEqualTo( msg );
+
+            assertThat( stackTrace.smartTrimmedStackTrace() )
+                    .isEqualTo( smartStackTrace );
+
+            assertThat( stackTrace.writeTraceToString() )
+                    .isEqualTo( this.stackTrace );
+        }
+    }
+
+    private static class StandardOutErrEventAssertionListener implements ForkedProcessStandardOutErrEventListener
+    {
+        final AtomicBoolean called = new AtomicBoolean();
+        private final RunMode runMode;
+        private final String output;
+        private final boolean newLine;
+
+        StandardOutErrEventAssertionListener( RunMode runMode, String output, boolean newLine )
+        {
+            this.runMode = runMode;
+            this.output = output;
+            this.newLine = newLine;
+        }
+
+        public void handle( RunMode runMode, String output, boolean newLine )
+        {
+            called.set( true );
+
+            assertThat( runMode )
+                    .isEqualTo( this.runMode );
+
+            assertThat( output )
+                    .isEqualTo( this.output );
+
+            assertThat( newLine )
+                    .isEqualTo( this.newLine );
+        }
+    }
+
+    private static class ReportEventAssertionListener implements ForkedProcessReportEventListener<ReportEntry>
+    {
+        final AtomicBoolean called = new AtomicBoolean();
+        private final ReportEntry reportEntry;
+        private final boolean hasStackTrace;
+
+        ReportEventAssertionListener( ReportEntry reportEntry, boolean hasStackTrace )
+        {
+            this.reportEntry = reportEntry;
+            this.hasStackTrace = hasStackTrace;
+        }
+
+        public void handle( RunMode runMode, ReportEntry reportEntry )
+        {
+            called.set( true );
+            assertThat( reportEntry.getSourceName() ).isEqualTo( this.reportEntry.getSourceName() );
+            assertThat( reportEntry.getSourceText() ).isEqualTo( this.reportEntry.getSourceText() );
+            assertThat( reportEntry.getName() ).isEqualTo( this.reportEntry.getName() );
+            assertThat( reportEntry.getNameText() ).isEqualTo( this.reportEntry.getNameText() );
+            assertThat( reportEntry.getGroup() ).isEqualTo( this.reportEntry.getGroup() );
+            assertThat( reportEntry.getMessage() ).isEqualTo( this.reportEntry.getMessage() );
+            assertThat( reportEntry.getElapsed() ).isEqualTo( this.reportEntry.getElapsed() );
+            if ( reportEntry.getStackTraceWriter() == null )
+            {
+                assertThat( hasStackTrace ).isFalse();
+                assertThat( this.reportEntry.getStackTraceWriter() ).isNull();
+            }
+            else
+            {
+                assertThat( hasStackTrace ).isTrue();
+                assertThat( this.reportEntry.getStackTraceWriter() ).isNotNull();
+
+                assertThat( reportEntry.getStackTraceWriter().getThrowable().getMessage() )
+                        .isEqualTo( this.reportEntry.getStackTraceWriter().getThrowable().getMessage() );
+
+                assertThat( reportEntry.getStackTraceWriter().getThrowable().getLocalizedMessage() )
+                        .isEqualTo( this.reportEntry.getStackTraceWriter().getThrowable().getLocalizedMessage() );
+
+                assertThat( reportEntry.getStackTraceWriter().smartTrimmedStackTrace() )
+                        .isEqualTo( this.reportEntry.getStackTraceWriter().smartTrimmedStackTrace() );
+            }
+        }
+    }
+
+    private static class Stream extends PrintStream
+    {
+        private final ByteArrayOutputStream out;
+
+        Stream( ByteArrayOutputStream out )
+        {
+            super( out, true );
+            this.out = out;
+        }
+
+        byte[] toByteArray()
+        {
+            return out.toByteArray();
+        }
+
+        LineNumberReader newReader( Charset streamCharset )
+        {
+            return new LineNumberReader( new StringReader( new String( toByteArray(), streamCharset ) ) );
+        }
+
+        static Stream newStream()
+        {
+            return new Stream( new ByteArrayOutputStream() );
+        }
+    }
+
+    private static byte[] toArray( ByteBuffer buffer )
+    {
+        return copyOfRange( buffer.array(), buffer.arrayOffset(), buffer.arrayOffset() + buffer.remaining() );
+    }
+
+    private static class EH implements EventHandler<Event>
+    {
+        private final BlockingQueue<Event> cache = new LinkedTransferQueue<>();
+
+        Event pullEvent() throws InterruptedException
+        {
+            return cache.poll( 1, TimeUnit.MINUTES );
+        }
+
+        @Override
+        public void handleEvent( @Nonnull Event event )
+        {
+            cache.add( event );
+        }
+    }
+}
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/surefire/extensions/StatelessReporterTest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/StatelessReporterTest.java
similarity index 98%
rename from maven-surefire-common/src/test/java/org/apache/maven/surefire/extensions/StatelessReporterTest.java
rename to maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/StatelessReporterTest.java
index bb06eff..4aa048c 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/surefire/extensions/StatelessReporterTest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/StatelessReporterTest.java
@@ -1,4 +1,4 @@
-package org.apache.maven.surefire.extensions;
+package org.apache.maven.plugin.surefire.extensions;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,8 +19,7 @@ package org.apache.maven.surefire.extensions;
  * under the License.
  */
 
-import org.apache.maven.plugin.surefire.extensions.DefaultStatelessReportMojoConfiguration;
-import org.apache.maven.plugin.surefire.extensions.SurefireStatelessReporter;
+import org.apache.maven.surefire.extensions.StatelessReportEventListener;
 import org.apache.maven.plugin.surefire.extensions.junit5.JUnit5Xml30StatelessReporter;
 import org.apache.maven.plugin.surefire.report.StatelessXmlReporter;
 import org.apache.maven.plugin.surefire.report.TestSetStats;
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
new file mode 100644
index 0000000..7da6745
--- /dev/null
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/StreamFeederTest.java
@@ -0,0 +1,162 @@
+package org.apache.maven.plugin.surefire.extensions;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
+import org.apache.maven.surefire.booter.Command;
+import org.apache.maven.surefire.extensions.CommandReader;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.WritableByteChannel;
+import java.util.Iterator;
+
+import static java.util.Arrays.asList;
+import static org.apache.maven.surefire.booter.Command.TEST_SET_FINISHED;
+import static org.apache.maven.surefire.booter.MasterProcessCommand.NOOP;
+import static org.apache.maven.surefire.booter.MasterProcessCommand.RUN_CLASS;
+import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for {@link StreamFeeder}.
+ */
+public class StreamFeederTest
+{
+    private final ByteArrayOutputStream out = new ByteArrayOutputStream();
+    private final WritableByteChannel channel = mock( WritableByteChannel.class );
+    private final CommandReader commandReader = mock( CommandReader.class );
+    private StreamFeeder streamFeeder;
+
+    @Before
+    public void setup() throws IOException
+    {
+        final Iterator<Command> it = asList( new Command( RUN_CLASS, "pkg.ATest" ), TEST_SET_FINISHED ).iterator();
+        when( commandReader.readNextCommand() )
+            .thenAnswer( new Answer<Command>()
+            {
+                @Override
+                public Command answer( InvocationOnMock invocation )
+                {
+                    return it.hasNext() ? it.next() : null;
+                }
+            } );
+    }
+
+    @After
+    public void close() throws IOException
+    {
+        if ( streamFeeder != null )
+        {
+            streamFeeder.disable();
+            streamFeeder.close();
+        }
+    }
+
+    @Test
+    public void shouldEncodeCommandToStream() throws Exception
+    {
+        when( channel.write( any( ByteBuffer.class ) ) )
+            .thenAnswer( new Answer<Object>()
+            {
+                @Override
+                public Object answer( InvocationOnMock invocation ) throws IOException
+                {
+                    ByteBuffer bb = invocation.getArgument( 0 );
+                    bb.flip();
+                    out.write( bb.array() );
+                    return 0;
+                }
+            } );
+
+        ConsoleLogger logger = mock( ConsoleLogger.class );
+        streamFeeder = new StreamFeeder( "t", channel, commandReader, logger );
+        streamFeeder.start();
+
+        streamFeeder.join();
+        String commands = out.toString();
+
+        assertThat( commands )
+            .isEqualTo( ":maven-surefire-command:run-testclass:pkg.ATest::maven-surefire-command:testset-finished:" );
+
+        verify( channel, times( 1 ) )
+            .close();
+
+        assertThat( streamFeeder.getException() )
+            .isNull();
+
+        verifyZeroInteractions( logger );
+    }
+
+    @Test
+    public void shouldFailThread() throws Exception
+    {
+        when( channel.write( any( ByteBuffer.class ) ) )
+            .thenAnswer( new Answer<Object>()
+            {
+                @Override
+                public Object answer( InvocationOnMock invocation ) throws IOException
+                {
+                    throw new IOException();
+                }
+            } );
+
+        ConsoleLogger logger = mock( ConsoleLogger.class );
+        streamFeeder = new StreamFeeder( "t", channel, commandReader, logger );
+        streamFeeder.start();
+
+        streamFeeder.join();
+
+        assertThat( out.size() )
+            .isZero();
+
+        verify( channel, times( 1 ) )
+            .close();
+
+        assertThat( streamFeeder.getException() )
+            .isNotNull()
+            .isInstanceOf( IOException.class );
+
+        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 b1e3b22..a232544 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
@@ -28,7 +28,6 @@ import org.apache.maven.plugin.surefire.AbstractSurefireMojoTest;
 import org.apache.maven.plugin.surefire.CommonReflectorTest;
 import org.apache.maven.plugin.surefire.MojoMocklessTest;
 import org.apache.maven.plugin.surefire.SurefireHelperTest;
-import org.apache.maven.plugin.surefire.SurefireReflectorTest;
 import org.apache.maven.plugin.surefire.SurefirePropertiesTest;
 import org.apache.maven.plugin.surefire.booterclient.BooterDeserializerProviderConfigurationTest;
 import org.apache.maven.plugin.surefire.booterclient.BooterDeserializerStartupConfigurationTest;
@@ -41,7 +40,10 @@ import org.apache.maven.plugin.surefire.booterclient.ModularClasspathForkConfigu
 import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.TestLessInputStreamBuilderTest;
 import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.TestProvidingInputStreamTest;
 import org.apache.maven.plugin.surefire.booterclient.output.ForkClientTest;
-import org.apache.maven.plugin.surefire.booterclient.output.ForkedChannelDecoderTest;
+import org.apache.maven.plugin.surefire.extensions.ConsoleOutputReporterTest;
+import org.apache.maven.plugin.surefire.extensions.ForkedProcessEventNotifierTest;
+import org.apache.maven.plugin.surefire.extensions.StatelessReporterTest;
+import org.apache.maven.plugin.surefire.extensions.StreamFeederTest;
 import org.apache.maven.plugin.surefire.report.DefaultReporterFactoryTest;
 import org.apache.maven.plugin.surefire.report.StatelessXmlReporterTest;
 import org.apache.maven.plugin.surefire.report.TestSetStatsTest;
@@ -51,8 +53,7 @@ import org.apache.maven.plugin.surefire.util.DependenciesScannerTest;
 import org.apache.maven.plugin.surefire.util.DirectoryScannerTest;
 import org.apache.maven.plugin.surefire.util.ScannerUtilTest;
 import org.apache.maven.plugin.surefire.util.SpecificFileFilterTest;
-import org.apache.maven.surefire.extensions.ConsoleOutputReporterTest;
-import org.apache.maven.surefire.extensions.StatelessReporterTest;
+import org.apache.maven.surefire.extensions.ForkChannelTest;
 import org.apache.maven.surefire.extensions.StatelessTestsetInfoReporterTest;
 import org.apache.maven.surefire.report.FileReporterTest;
 import org.apache.maven.surefire.report.RunStatisticsTest;
@@ -89,7 +90,6 @@ public class JUnit4SuiteTest extends TestCase
         suite.addTest( new JUnit4TestAdapter( TestProvidingInputStreamTest.class ) );
         suite.addTest( new JUnit4TestAdapter( TestLessInputStreamBuilderTest.class ) );
         suite.addTest( new JUnit4TestAdapter( SPITest.class ) );
-        suite.addTest( new JUnit4TestAdapter( SurefireReflectorTest.class ) );
         suite.addTest( new JUnit4TestAdapter( SurefireHelperTest.class ) );
         suite.addTest( new JUnit4TestAdapter( AbstractSurefireMojoTest.class ) );
         suite.addTest( new JUnit4TestAdapter( DefaultForkConfigurationTest.class ) );
@@ -99,13 +99,15 @@ public class JUnit4SuiteTest extends TestCase
         suite.addTest( new JUnit4TestAdapter( ScannerUtilTest.class ) );
         suite.addTest( new JUnit4TestAdapter( MojoMocklessTest.class ) );
         suite.addTest( new JUnit4TestAdapter( ForkClientTest.class ) );
-        suite.addTest( new JUnit4TestAdapter( ForkedChannelDecoderTest.class ) );
+        suite.addTest( new JUnit4TestAdapter( ForkedProcessEventNotifierTest.class ) );
         suite.addTest( new JUnit4TestAdapter( ConsoleOutputReporterTest.class ) );
         suite.addTest( new JUnit4TestAdapter( StatelessReporterTest.class ) );
         suite.addTest( new JUnit4TestAdapter( TestSetStatsTest.class ) );
         suite.addTest( new JUnit4TestAdapter( StatelessTestsetInfoReporterTest.class ) );
         suite.addTest( new JUnit4TestAdapter( CommonReflectorTest.class ) );
         suite.addTest( new JUnit4TestAdapter( ForkStarterTest.class ) );
+        suite.addTest( new JUnit4TestAdapter( ForkChannelTest.class ) );
+        suite.addTest( new JUnit4TestAdapter( StreamFeederTest.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
new file mode 100644
index 0000000..68f2324
--- /dev/null
+++ b/maven-surefire-common/src/test/java/org/apache/maven/surefire/extensions/ForkChannelTest.java
@@ -0,0 +1,166 @@
+package org.apache.maven.surefire.extensions;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.apache.maven.plugin.surefire.booterclient.MockReporter;
+import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.TestLessInputStream;
+import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.TestLessInputStream.TestLessInputStreamBuilder;
+import org.apache.maven.plugin.surefire.extensions.SurefireForkNodeFactory;
+import org.apache.maven.surefire.eventapi.ControlByeEvent;
+import org.apache.maven.surefire.eventapi.Event;
+import org.apache.maven.surefire.extensions.util.CountdownCloseable;
+import org.junit.Test;
+
+import javax.annotation.Nonnull;
+import java.io.Closeable;
+import java.io.IOException;
+import java.net.Socket;
+import java.net.URI;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.fest.assertions.Assertions.assertThat;
+
+/**
+ *
+ */
+public class ForkChannelTest
+{
+    private static final long TESTCASE_TIMEOUT = 30_000L;
+
+    private final AtomicBoolean hasError = new AtomicBoolean();
+
+    @Test( timeout = TESTCASE_TIMEOUT )
+    public void shouldRequestReplyMessagesViaTCP() throws Exception
+    {
+        ForkNodeFactory factory = new SurefireForkNodeFactory();
+        try ( ForkChannel channel = factory.createForkChannel( 1, new MockReporter() ) )
+        {
+            assertThat( channel.getForkChannelId() )
+                .isEqualTo( 1 );
+
+            assertThat( channel.useStdOut() )
+                .isFalse();
+
+            assertThat( channel.getForkNodeConnectionString() )
+                .startsWith( "tcp://127.0.0.1:" )
+                .isNotEqualTo( "tcp://127.0.0.1:" );
+
+            URI uri = new URI( channel.getForkNodeConnectionString() );
+
+            assertThat( uri.getPort() )
+                .isPositive();
+
+            Consumer consumer = new Consumer();
+
+            Client client = new Client( uri.getPort() );
+            client.start();
+
+            channel.connectToClient();
+            SECONDS.sleep( 3L );
+
+            TestLessInputStreamBuilder builder = new TestLessInputStreamBuilder();
+            TestLessInputStream commandReader = builder.build();
+
+            channel.bindCommandReader( commandReader, null ).start();
+
+            final AtomicBoolean isCloseableCalled = new AtomicBoolean();
+            Closeable closeable = new Closeable()
+            {
+                @Override
+                public void close()
+                {
+                    isCloseableCalled.set( true );
+                }
+            };
+            CountdownCloseable cc = new CountdownCloseable( closeable, 1 );
+            channel.bindEventHandler( consumer, cc, null ).start();
+
+            SECONDS.sleep( 3L );
+
+            commandReader.noop();
+
+            SECONDS.sleep( 3L );
+
+            client.join( TESTCASE_TIMEOUT );
+
+            assertThat( hasError.get() )
+                .isFalse();
+
+            assertThat( isCloseableCalled.get() )
+                .isTrue();
+
+            assertThat( consumer.lines )
+                .hasSize( 1 );
+
+            assertThat( consumer.lines.element() )
+                .isInstanceOf( ControlByeEvent.class );
+        }
+    }
+
+    private static class Consumer implements EventHandler<Event>
+    {
+        final Queue<Event> lines = new ConcurrentLinkedQueue<>();
+
+        @Override
+        public void handleEvent( @Nonnull Event s )
+        {
+            lines.add( s );
+        }
+    }
+
+    private final class Client extends Thread
+    {
+        private final int port;
+
+        private Client( int port )
+        {
+            this.port = port;
+        }
+
+        @Override
+        public void run()
+        {
+            try ( Socket socket = new Socket( "127.0.0.1", port ) )
+            {
+                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:" );
+                socket.getOutputStream().write( ":maven-surefire-event:bye:".getBytes( US_ASCII ) );
+            }
+            catch ( IOException e )
+            {
+                hasError.set( true );
+                e.printStackTrace();
+                throw new IllegalStateException( e );
+            }
+            catch ( RuntimeException e )
+            {
+                hasError.set( true );
+                e.printStackTrace();
+                throw e;
+            }
+        }
+    }
+}
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/surefire/extensions/StatelessTestsetInfoReporterTest.java b/maven-surefire-common/src/test/java/org/apache/maven/surefire/extensions/StatelessTestsetInfoReporterTest.java
index a626180..577bb91 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/surefire/extensions/StatelessTestsetInfoReporterTest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/surefire/extensions/StatelessTestsetInfoReporterTest.java
@@ -26,8 +26,8 @@ import org.apache.maven.plugin.surefire.report.ConsoleReporter;
 import org.apache.maven.plugin.surefire.report.FileReporter;
 import org.apache.maven.plugin.surefire.report.TestSetStats;
 import org.apache.maven.plugin.surefire.report.WrappedReportEntry;
-import org.apache.maven.surefire.shared.utils.logging.MessageUtils;
 import org.apache.maven.surefire.report.TestSetReportEntry;
+import org.apache.maven.surefire.shared.utils.logging.MessageUtils;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
diff --git a/maven-surefire-plugin/src/main/java/org/apache/maven/plugin/surefire/SurefirePlugin.java b/maven-surefire-plugin/src/main/java/org/apache/maven/plugin/surefire/SurefirePlugin.java
index b39181a..5f6edea 100644
--- a/maven-surefire-plugin/src/main/java/org/apache/maven/plugin/surefire/SurefirePlugin.java
+++ b/maven-surefire-plugin/src/main/java/org/apache/maven/plugin/surefire/SurefirePlugin.java
@@ -29,6 +29,7 @@ import org.apache.maven.plugins.annotations.LifecyclePhase;
 import org.apache.maven.plugins.annotations.Mojo;
 import org.apache.maven.plugins.annotations.Parameter;
 import org.apache.maven.plugins.annotations.ResolutionScope;
+import org.apache.maven.surefire.extensions.ForkNodeFactory;
 import org.apache.maven.surefire.suite.RunResult;
 
 import static org.apache.maven.plugin.surefire.SurefireHelper.reportExecution;
@@ -365,6 +366,9 @@ public class SurefirePlugin
     @Parameter( property = "surefire.useModulePath", defaultValue = "true" )
     private boolean useModulePath;
 
+    @Parameter( property = "surefire.forkNode" )
+    private ForkNodeFactory forkNode;
+
     /**
      * You can selectively exclude individual environment variables by enumerating their keys.
      * <br>
@@ -831,4 +835,10 @@ public class SurefirePlugin
     {
         return enableProcessChecker;
     }
+
+    @Override
+    protected final ForkNodeFactory getForkNode()
+    {
+        return forkNode;
+    }
 }
diff --git a/pom.xml b/pom.xml
index f4f992e..ccf7c9e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -51,6 +51,7 @@
     <module>surefire-logger-api</module>
     <module>surefire-api</module>
     <module>surefire-extensions-api</module>
+    <module>surefire-extensions-spi</module>
     <module>surefire-booter</module>
     <module>surefire-grouper</module>
     <module>surefire-providers</module>
@@ -97,7 +98,7 @@
     <plexus-java-version>1.0.5</plexus-java-version>
     <!-- maven-shared-utils:3.2.0+ another behavior - broke Surefire performance - end of subprocess notification not arrived in ForkStarter -->
     <mavenSharedUtilsVersion>3.1.0</mavenSharedUtilsVersion>
-    <powermockVersion>2.0.4</powermockVersion>
+    <powermockVersion>2.0.5</powermockVersion>
     <jacocoVersion>0.8.5</jacocoVersion>
     <maven.surefire.scm.devConnection>scm:git:https://gitbox.apache.org/repos/asf/maven-surefire.git</maven.surefire.scm.devConnection>
     <maven.site.path>surefire-archives/surefire-LATEST</maven.site.path>
@@ -474,10 +475,8 @@
               </goals>
               <configuration>
                 <includes>
-                  <include>org/apache/maven/shared/utils/logging/*.java</include>
                   <include>HelpMojo.java</include>
                   <include>**/HelpMojo.java</include>
-                  <include>org/apache/maven/plugin/failsafe/xmlsummary/*.java</include>
                 </includes>
                 <compilerArgs>
                   <!-- FIXME: maven-plugin-plugin therefore used -syntax or none due to HelpMojo -->
@@ -493,10 +492,8 @@
               </goals>
               <configuration>
                 <excludes>
-                  <exclude>org/apache/maven/shared/utils/logging/*.java</exclude>
                   <exclude>HelpMojo.java</exclude>
                   <exclude>**/HelpMojo.java</exclude>
-                  <exclude>org/apache/maven/plugin/failsafe/xmlsummary/*.java</exclude>
                 </excludes>
                 <compilerArgs>
                   <arg>-Xdoclint:all</arg>
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/booter/BaseProviderFactory.java b/surefire-api/src/main/java/org/apache/maven/surefire/booter/BaseProviderFactory.java
index ec05580..6ac0ce2 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/booter/BaseProviderFactory.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/booter/BaseProviderFactory.java
@@ -20,6 +20,8 @@ package org.apache.maven.surefire.booter;
  */
 
 import org.apache.maven.surefire.cli.CommandLineOption;
+import org.apache.maven.surefire.providerapi.CommandChainReader;
+import org.apache.maven.surefire.providerapi.MasterProcessChannelEncoder;
 import org.apache.maven.surefire.providerapi.ProviderParameters;
 import org.apache.maven.surefire.report.ConsoleStream;
 import org.apache.maven.surefire.report.DefaultDirectConsoleReporter;
@@ -46,15 +48,13 @@ import static java.util.Collections.emptyList;
  * @author Kristian Rosenvold
  */
 public class BaseProviderFactory
-    implements DirectoryScannerParametersAware, ReporterConfigurationAware, SurefireClassLoadersAware, TestRequestAware,
-    ProviderPropertiesAware, ProviderParameters, TestArtifactInfoAware, RunOrderParametersAware, MainCliOptionsAware,
-    FailFastAware, ShutdownAware
+    implements ProviderParameters
 {
-    private final ReporterFactory reporterFactory;
-
     private final boolean insideFork;
 
-    private ForkedChannelEncoder forkedChannelEncoder;
+    private ReporterFactory reporterFactory;
+
+    private MasterProcessChannelEncoder masterProcessChannelEncoder;
 
     private List<CommandLineOption> mainCliOptions = emptyList();
 
@@ -74,17 +74,27 @@ public class BaseProviderFactory
 
     private int skipAfterFailureCount;
 
-    private Shutdown shutdown;
-
     private Integer systemExitTimeout;
 
-    public BaseProviderFactory( ReporterFactory reporterFactory, boolean insideFork )
+    private CommandChainReader commandReader;
+
+    public BaseProviderFactory( boolean insideFork )
     {
-        this.reporterFactory = reporterFactory;
         this.insideFork = insideFork;
     }
 
     @Override
+    public CommandChainReader getCommandReader()
+    {
+        return commandReader;
+    }
+
+    public void setCommandReader( CommandChainReader commandReader )
+    {
+        this.commandReader = commandReader;
+    }
+
+    @Override
     @Deprecated
     public DirectoryScanner getDirectoryScanner()
     {
@@ -114,25 +124,27 @@ public class BaseProviderFactory
                 ? null : new DefaultRunOrderCalculator( runOrderParameters, getThreadCount() );
     }
 
+    public void setReporterFactory( ReporterFactory reporterFactory )
+    {
+        this.reporterFactory = reporterFactory;
+    }
+
     @Override
     public ReporterFactory getReporterFactory()
     {
         return reporterFactory;
     }
 
-    @Override
     public void setDirectoryScannerParameters( DirectoryScannerParameters directoryScannerParameters )
     {
         this.directoryScannerParameters = directoryScannerParameters;
     }
 
-    @Override
     public void setReporterConfiguration( ReporterConfiguration reporterConfiguration )
     {
         this.reporterConfiguration = reporterConfiguration;
     }
 
-    @Override
     public void setClassLoaders( ClassLoader testClassLoader )
     {
         this.testClassLoader = testClassLoader;
@@ -141,11 +153,11 @@ public class BaseProviderFactory
     @Override
     public ConsoleStream getConsoleLogger()
     {
-        return insideFork ? new ForkingRunListener( forkedChannelEncoder, reporterConfiguration.isTrimStackTrace() )
-                       : new DefaultDirectConsoleReporter( reporterConfiguration.getOriginalSystemOut() );
+        return insideFork
+            ? new ForkingRunListener( masterProcessChannelEncoder, reporterConfiguration.isTrimStackTrace() )
+            : new DefaultDirectConsoleReporter( reporterConfiguration.getOriginalSystemOut() );
     }
 
-    @Override
     public void setTestRequest( TestRequest testRequest )
     {
         this.testRequest = testRequest;
@@ -175,7 +187,6 @@ public class BaseProviderFactory
         return testClassLoader;
     }
 
-    @Override
     public void setProviderProperties( Map<String, String> providerProperties )
     {
         this.providerProperties = providerProperties;
@@ -193,13 +204,11 @@ public class BaseProviderFactory
         return testArtifactInfo;
     }
 
-    @Override
     public void setTestArtifactInfo( TestArtifactInfo testArtifactInfo )
     {
         this.testArtifactInfo = testArtifactInfo;
     }
 
-    @Override
     public void setRunOrderParameters( RunOrderParameters runOrderParameters )
     {
         this.runOrderParameters = runOrderParameters;
@@ -211,7 +220,11 @@ public class BaseProviderFactory
         return mainCliOptions;
     }
 
-    @Override
+    /**
+     * CLI options in plugin (main) JVM process.
+     *
+     * @param mainCliOptions options
+     */
     public void setMainCliOptions( List<CommandLineOption> mainCliOptions )
     {
         this.mainCliOptions = mainCliOptions == null ? Collections.<CommandLineOption>emptyList() : mainCliOptions;
@@ -223,7 +236,11 @@ public class BaseProviderFactory
         return skipAfterFailureCount;
     }
 
-    @Override
+    /**
+     * See the plugin configuration parameter "skipAfterFailureCount".
+     *
+     * @param skipAfterFailureCount the value in config parameter "skipAfterFailureCount"
+     */
     public void setSkipAfterFailureCount( int skipAfterFailureCount )
     {
         this.skipAfterFailureCount = skipAfterFailureCount;
@@ -236,18 +253,6 @@ public class BaseProviderFactory
     }
 
     @Override
-    public Shutdown getShutdown()
-    {
-        return shutdown;
-    }
-
-    @Override
-    public void setShutdown( Shutdown shutdown )
-    {
-        this.shutdown = shutdown;
-    }
-
-    @Override
     public Integer getSystemExitTimeout()
     {
         return systemExitTimeout;
@@ -259,13 +264,13 @@ public class BaseProviderFactory
     }
 
     @Override
-    public ForkedChannelEncoder getForkedChannelEncoder()
+    public MasterProcessChannelEncoder getForkedChannelEncoder()
     {
-        return forkedChannelEncoder;
+        return masterProcessChannelEncoder;
     }
 
-    public void setForkedChannelEncoder( ForkedChannelEncoder forkedChannelEncoder )
+    public void setForkedChannelEncoder( MasterProcessChannelEncoder masterProcessChannelEncoder )
     {
-        this.forkedChannelEncoder = forkedChannelEncoder;
+        this.masterProcessChannelEncoder = masterProcessChannelEncoder;
     }
 }
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/booter/Command.java b/surefire-api/src/main/java/org/apache/maven/surefire/booter/Command.java
index f05c0f6..834317b 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/booter/Command.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/booter/Command.java
@@ -19,6 +19,8 @@ package org.apache.maven.surefire.booter;
  * under the License.
  */
 
+import java.util.Objects;
+
 import static java.util.Objects.requireNonNull;
 import static org.apache.maven.surefire.shared.utils.StringUtils.isBlank;
 import static org.apache.maven.surefire.booter.MasterProcessCommand.RUN_CLASS;
@@ -47,6 +49,11 @@ public final class Command
         this.data = data;
     }
 
+    public Command( MasterProcessCommand command )
+    {
+        this( command, null );
+    }
+
     public static Command toShutdown( Shutdown shutdownType )
     {
         return new Command( SHUTDOWN, shutdownType.name() );
@@ -57,11 +64,6 @@ public final class Command
         return new Command( RUN_CLASS, runClass );
     }
 
-    public Command( MasterProcessCommand command )
-    {
-        this( command, null );
-    }
-
     public MasterProcessCommand getCommandType()
     {
         return command;
@@ -78,18 +80,13 @@ public final class Command
      */
     public Shutdown toShutdownData()
     {
-        if ( !isType( SHUTDOWN ) )
+        if ( command != SHUTDOWN )
         {
             throw new IllegalStateException( "expected MasterProcessCommand.SHUTDOWN" );
         }
         return isBlank( data ) ? DEFAULT : Shutdown.valueOf( data );
     }
 
-    public boolean isType( MasterProcessCommand command )
-    {
-        return command == this.command;
-    }
-
     @Override
     public boolean equals( Object o )
     {
@@ -105,7 +102,7 @@ public final class Command
 
         Command arg = (Command) o;
 
-        return command == arg.command && ( data == null ? arg.data == null : data.equals( arg.data ) );
+        return command == arg.command && Objects.equals( data, arg.data );
     }
 
     @Override
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/booter/ForkedProcessEvent.java b/surefire-api/src/main/java/org/apache/maven/surefire/booter/ForkedProcessEventType.java
similarity index 86%
rename from surefire-api/src/main/java/org/apache/maven/surefire/booter/ForkedProcessEvent.java
rename to surefire-api/src/main/java/org/apache/maven/surefire/booter/ForkedProcessEventType.java
index 74b9eb9..c1087ac 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/booter/ForkedProcessEvent.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/booter/ForkedProcessEventType.java
@@ -19,6 +19,7 @@ package org.apache.maven.surefire.booter;
  * under the License.
  */
 
+import javax.annotation.Nonnull;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
@@ -30,7 +31,7 @@ import static java.util.Collections.unmodifiableMap;
  * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
  * @since 3.0.0-M4
  */
-public enum ForkedProcessEvent
+public enum ForkedProcessEventType
 {
     BOOTERCODE_SYSPROPS( "sys-prop" ),
 
@@ -59,14 +60,14 @@ public enum ForkedProcessEvent
 
     BOOTERCODE_JVM_EXIT_ERROR( "jvm-exit-error" );
 
-    public static final String MAGIC_NUMBER = ":maven:surefire:std:out:";
+    public static final String MAGIC_NUMBER = "maven-surefire-event";
 
-    public static final Map<String, ForkedProcessEvent> EVENTS = events();
+    private static final Map<String, ForkedProcessEventType> EVENTS = events();
 
-    private static Map<String, ForkedProcessEvent> events()
+    private static Map<String, ForkedProcessEventType> events()
     {
-        Map<String, ForkedProcessEvent> events = new ConcurrentHashMap<>();
-        for ( ForkedProcessEvent event : values() )
+        Map<String, ForkedProcessEventType> events = new ConcurrentHashMap<>();
+        for ( ForkedProcessEventType event : values() )
         {
             events.put( event.getOpcode(), event );
         }
@@ -75,7 +76,7 @@ public enum ForkedProcessEvent
 
     private final String opcode;
 
-    ForkedProcessEvent( String opcode )
+    ForkedProcessEventType( String opcode )
     {
         this.opcode = opcode;
     }
@@ -129,4 +130,9 @@ public enum ForkedProcessEvent
     {
         return this == BOOTERCODE_JVM_EXIT_ERROR;
     }
+
+    public static ForkedProcessEventType byOpcode( @Nonnull String opcode )
+    {
+        return EVENTS.get( opcode );
+    }
 }
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/booter/ForkingReporterFactory.java b/surefire-api/src/main/java/org/apache/maven/surefire/booter/ForkingReporterFactory.java
index 5bb16ee..1c6db50 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/booter/ForkingReporterFactory.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/booter/ForkingReporterFactory.java
@@ -19,6 +19,7 @@ package org.apache.maven.surefire.booter;
  * under the License.
  */
 
+import org.apache.maven.surefire.providerapi.MasterProcessChannelEncoder;
 import org.apache.maven.surefire.report.ReporterFactory;
 import org.apache.maven.surefire.report.RunListener;
 import org.apache.maven.surefire.suite.RunResult;
@@ -34,9 +35,9 @@ public class ForkingReporterFactory
 {
     private final boolean trimstackTrace;
 
-    private final ForkedChannelEncoder eventChannel;
+    private final MasterProcessChannelEncoder eventChannel;
 
-    public ForkingReporterFactory( boolean trimstackTrace, ForkedChannelEncoder eventChannel )
+    public ForkingReporterFactory( boolean trimstackTrace, MasterProcessChannelEncoder eventChannel )
     {
         this.trimstackTrace = trimstackTrace;
         this.eventChannel = eventChannel;
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/booter/ForkingRunListener.java b/surefire-api/src/main/java/org/apache/maven/surefire/booter/ForkingRunListener.java
index 528b607..6148149 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/booter/ForkingRunListener.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/booter/ForkingRunListener.java
@@ -20,6 +20,7 @@ package org.apache.maven.surefire.booter;
  */
 
 import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
+import org.apache.maven.surefire.providerapi.MasterProcessChannelEncoder;
 import org.apache.maven.surefire.report.ConsoleOutputReceiver;
 import org.apache.maven.surefire.report.ConsoleStream;
 import org.apache.maven.surefire.report.ReportEntry;
@@ -50,13 +51,13 @@ import static java.util.Objects.requireNonNull;
 public class ForkingRunListener
     implements RunListener, ConsoleLogger, ConsoleOutputReceiver, ConsoleStream
 {
-    private final ForkedChannelEncoder target;
+    private final MasterProcessChannelEncoder target;
 
     private final boolean trim;
 
     private volatile RunMode runMode = NORMAL_RUN;
 
-    public ForkingRunListener( ForkedChannelEncoder target, boolean trim )
+    public ForkingRunListener( MasterProcessChannelEncoder target, boolean trim )
     {
         this.target = target;
         this.trim = trim;
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/booter/MasterProcessCommand.java b/surefire-api/src/main/java/org/apache/maven/surefire/booter/MasterProcessCommand.java
index 7c4520f..b6ae644 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/booter/MasterProcessCommand.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/booter/MasterProcessCommand.java
@@ -19,46 +19,34 @@ package org.apache.maven.surefire.booter;
  * under the License.
  */
 
-import java.io.DataInputStream;
-import java.io.IOException;
-
-import static java.nio.charset.StandardCharsets.US_ASCII;
 import static java.util.Objects.requireNonNull;
-import static java.lang.String.format;
 
 /**
  * Commands which are sent from plugin to the forked jvm.
- * Support and methods related to the commands.
  *
  * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
  * @since 2.19
  */
 public enum MasterProcessCommand
 {
-    RUN_CLASS( 0, String.class ),
-    TEST_SET_FINISHED( 1, Void.class ),
-    SKIP_SINCE_NEXT_TEST( 2, Void.class ),
-    SHUTDOWN( 3, String.class ),
+    RUN_CLASS( String.class ),
+    TEST_SET_FINISHED( Void.class ),
+    SKIP_SINCE_NEXT_TEST( Void.class ),
+    SHUTDOWN( String.class ),
 
     /** To tell a forked process that the master process is still alive. Repeated after 10 seconds. */
-    NOOP( 4, Void.class ),
-    BYE_ACK( 5, Void.class );
+    NOOP( Void.class ),
+    BYE_ACK( Void.class );
 
-    private final int id;
+    public static final String MAGIC_NUMBER = "maven-surefire-command";
 
     private final Class<?> dataType;
 
-    MasterProcessCommand( int id, Class<?> dataType )
+    MasterProcessCommand( Class<?> dataType )
     {
-        this.id = id;
         this.dataType = requireNonNull( dataType, "dataType cannot be null" );
     }
 
-    public int getId()
-    {
-        return id;
-    }
-
     public Class<?> getDataType()
     {
         return dataType;
@@ -68,123 +56,4 @@ public enum MasterProcessCommand
     {
         return dataType != Void.class;
     }
-
-    @SuppressWarnings( "checkstyle:magicnumber" )
-    public byte[] encode( String data )
-    {
-        if ( !hasDataType() )
-        {
-            throw new IllegalArgumentException( "cannot use data without data type" );
-        }
-
-        if ( getDataType() != String.class )
-        {
-            throw new IllegalArgumentException( "Data type can be only " + String.class );
-        }
-
-        final byte[] dataBytes = fromDataType( data );
-        final int len = dataBytes.length;
-
-        final byte[] encoded = new byte[8 + len];
-
-        final int command = getId();
-        setCommandAndDataLength( command, len, encoded );
-        System.arraycopy( dataBytes, 0, encoded, 8, len );
-
-        return encoded;
-    }
-
-    @SuppressWarnings( "checkstyle:magicnumber" )
-    public byte[] encode()
-    {
-        if ( getDataType() != Void.class )
-        {
-            throw new IllegalArgumentException( "Data type can be only " + getDataType() );
-        }
-        byte[] encoded = new byte[8];
-        int command = getId();
-        setCommandAndDataLength( command, 0, encoded );
-        return encoded;
-    }
-
-    public static Command decode( DataInputStream is )
-        throws IOException
-    {
-        MasterProcessCommand command = resolve( is.readInt() );
-        if ( command == null )
-        {
-            return null;
-        }
-        else
-        {
-            int dataLength = is.readInt();
-            if ( dataLength > 0 )
-            {
-                byte[] buffer = new byte[ dataLength ];
-                is.readFully( buffer );
-
-                if ( command.getDataType() == Void.class )
-                {
-                    throw new IOException( format( "Command %s unexpectedly read Void data with length %d.",
-                                                   command, dataLength ) );
-                }
-
-                String data = command.toDataTypeAsString( buffer );
-                return new Command( command, data );
-            }
-            else
-            {
-                return new Command( command );
-            }
-        }
-    }
-
... 7551 lines suppressed ...