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/14 00:20:48 UTC

[maven-surefire] branch master updated: [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 master
in repository https://gitbox.apache.org/repos/asf/maven-surefire.git


The following commit(s) were added to refs/heads/master by this push:
     new 5e3348c  [SUREFIRE-1658] TCP/IP Channel for forked Surefire JVM. Extensions API and SPI. Polymorphism for remote and local process communication.
5e3348c is described below

commit 5e3348cdf29d1d49ca615a893b9174563f62815c
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.
    
    added TCP alternative in ConsoleOutputIT
    
    extended few tests with a new alternative of forkNode: TCP
    
    documentation and Javadoc
    
    fix after Enrico's findings in external project
    
    fixed the IT 735
    
    fixed the IT 735
    
    improved performance from 320s to 54s.
    
    investigated tests
    
    E2E test for TCP
    
    fixed performance problem in TCP/Pipes communication (we do NOT flush every time, used buffered channels, used Async Sockets instead of blocking NIO Sockets)
    
    sendExitError
    
    fixed Surefire817SystemExitIT
    
    E2ETest performance test
    
    improved coverage in new code
    
    removed unused methods in CommandReader.java
    
    name of the thread ends with dash "-"
    
    don't print ClosedChannelException in dump file on exit
    
    keeping backwards compatibility - printing corrupted stream
---
 .github/workflows/maven.yml                        |    2 +-
 Jenkinsfile                                        |    8 +-
 .../maven/plugin/failsafe/IntegrationTestMojo.java |   22 +
 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  |  175 ++-
 .../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   |  187 +--
 .../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   |  478 ++++++++
 .../surefire/extensions/LegacyForkChannel.java     |   87 ++
 .../surefire/extensions/LegacyForkNodeFactory.java |   23 +-
 .../plugin/surefire/extensions/StreamFeeder.java   |  203 ++++
 .../surefire/extensions/SurefireForkChannel.java   |  169 +++
 .../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       |  202 +--
 .../plugin/surefire/booterclient/MainClass.java    |   14 +-
 .../ModularClasspathForkConfigurationTest.java     |   10 +-
 .../TestLessInputStreamBuilderTest.java            |   54 +-
 .../TestProvidingInputStreamTest.java              |  152 ++-
 .../booterclient/output/ForkClientTest.java        |  959 ++++-----------
 .../output/ForkedChannelDecoderTest.java           |  901 --------------
 .../extensions/ConsoleOutputReporterTest.java      |    8 +-
 .../maven/plugin/surefire/extensions/E2ETest.java  |  182 +++
 .../extensions/ForkedProcessEventNotifierTest.java | 1284 ++++++++++++++++++++
 .../surefire/extensions/StatelessReporterTest.java |    5 +-
 .../surefire/extensions/StreamFeederTest.java      |  162 +++
 .../org/apache/maven/surefire/JUnit4SuiteTest.java |   16 +-
 .../maven/surefire/extensions/ForkChannelTest.java |  189 +++
 .../StatelessTestsetInfoReporterTest.java          |    2 +-
 .../maven/plugin/surefire/SurefirePlugin.java      |   22 +
 .../src/site/apt/examples/process-communication.vm |  153 +++
 maven-surefire-plugin/src/site/site.xml            |    1 +
 pom.xml                                            |    7 +-
 .../maven/surefire/booter/BaseProviderFactory.java |   80 +-
 .../org/apache/maven/surefire/booter/Command.java  |   21 +-
 ...ocessEvent.java => ForkedProcessEventType.java} |   20 +-
 .../surefire/booter/ForkingReporterFactory.java    |    4 +-
 .../maven/surefire/booter/ForkingRunListener.java  |    4 +-
 .../booter/MasterProcessChannelDecoder.java        |   46 +
 .../booter/MasterProcessChannelEncoder.java        |   86 ++
 .../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}                       |   15 +-
 .../{booter => providerapi}/CommandListener.java   |    4 +-
 .../surefire/providerapi/ProviderParameters.java   |    6 +-
 .../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 +-
 .../AbstractNoninterruptibleReadableChannel.java   |   69 ++
 .../AbstractNoninterruptibleWritableChannel.java   |   97 ++
 .../maven/surefire/util/internal/Channels.java     |  256 ++++
 .../util/internal/DaemonThreadFactory.java         |   35 +-
 .../internal/WritableBufferedByteChannel.java}     |   16 +-
 .../java/org/apache/maven/JUnit4SuiteTest.java     |   14 +-
 .../surefire/booter/ForkingRunListenerTest.java    |   24 +-
 .../surefire/booter/MasterProcessCommandTest.java  |  164 ---
 .../surefire/booter/SurefireReflectorTest.java     |  198 ---
 .../surefire/util/internal/AsyncSocketTest.java    |  227 ++++
 .../surefire/util/internal/ChannelsReaderTest.java |  545 +++++++++
 .../surefire/util/internal/ChannelsWriterTest.java |  453 +++++++
 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       |  165 +--
 .../apache/maven/surefire/booter/ForkedBooter.java |  117 +-
 .../maven/surefire/booter/LazyTestsToRun.java      |   12 +-
 .../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     |  283 +++--
 ...LegacyMasterProcessChannelProcessorFactory.java |   72 ++
 ...refireMasterProcessChannelProcessorFactory.java |  122 ++
 ...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      |  264 +++-
 .../maven/surefire/booter/ForkedBooterTest.java    |   23 +-
 .../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 |  234 ++--
 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     |  105 ++
 .../surefire/extensions/ForkNodeArguments.java     |   30 +-
 .../maven/surefire/extensions/ForkNodeFactory.java |   23 +-
 .../surefire/extensions/StdOutStreamLine.java      |    8 +-
 .../extensions/util/CommandlineStreams.java        |   16 +-
 .../util/FlushableWritableByteChannel.java         |   68 --
 .../extensions/util/LineConsumerThread.java        |   21 +-
 .../surefire/extensions/util/StreamFeeder.java     |   89 --
 .../extensions}/CommandlineExecutorTest.java       |   27 +-
 .../surefire/extensions}/JUnit4SuiteTest.java      |    2 +-
 .../pom.xml                                        |   48 +-
 .../spi/MasterProcessChannelProcessorFactory.java  |   62 +
 surefire-its/pom.xml                               |    2 +
 .../maven/surefire/its/AbstractFailFastIT.java     |   28 +-
 .../apache/maven/surefire/its/ConsoleOutputIT.java |  106 +-
 .../apache/maven/surefire/its/FailFastJUnitIT.java |   31 +-
 .../maven/surefire/its/FailFastTestNgIT.java       |   18 +-
 .../its/JUnit47RerunFailingTestWithCucumberIT.java |   83 +-
 .../maven/surefire/its/TestMethodPatternIT.java    |   84 +-
 .../surefire/its/fixture/SurefireLauncher.java     |    2 +
 ...fire735ForkFailWithRedirectConsoleOutputIT.java |    3 +-
 .../src/test/resources/consoleOutput/pom.xml       |   17 +
 .../src/test/java/consoleOutput/Test1.java         |   26 +-
 .../src/test/resources/consoleoutput-noisy/pom.xml |   17 +
 .../src/test/java/consoleoutput_noisy/Test1.java   |    2 +
 .../src/test/java/consoleoutput_noisy/Test3.java   |   46 +-
 .../src/test/resources/fail-fast-junit/pom.xml     |   14 +
 .../src/test/resources/fail-fast-testng/pom.xml    |   17 +
 .../test/resources/junit44-method-pattern/pom.xml  |   17 +
 .../pom.xml                                        |   20 +
 .../test/resources/junit48-method-pattern/pom.xml  |   14 +
 .../resources/testng-method-pattern-after/pom.xml  |   14 +
 .../resources/testng-method-pattern-before/pom.xml |   14 +
 .../test/resources/testng-method-pattern/pom.xml   |   14 +
 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 +
 192 files changed, 10090 insertions(+), 4494 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..0e6e26d 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;
@@ -385,6 +386,21 @@ public class IntegrationTestMojo
     private boolean useModulePath;
 
     /**
+     * This parameter configures the forked node. Currently, you can select the communication protocol, i.e. process
+     * pipes or TCP/IP sockets.
+     * The plugin uses process pipes by default which will be turned to TCP/IP in the version 3.0.0.
+     * Alternatively, you can implement your own factory and SPI.
+     * <br>
+     * See the documentation for more details:<br>
+     * <a href="https://maven.apache.org/plugins/maven-surefire-plugin/examples/process-communication.html">
+     *     https://maven.apache.org/plugins/maven-surefire-plugin/examples/process-communication.html</a>
+     *
+     * @since 3.0.0-M5
+     */
+    @Parameter( property = "failsafe.forkNode" )
+    private ForkNodeFactory forkNode;
+
+    /**
      * You can selectively exclude individual environment variables by enumerating their keys.
      * <br>
      * The environment is a system-dependent mapping from keys to values which is inherited from the Maven process
@@ -912,6 +928,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..b26e981 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().getName() );
 
         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..44c9f99 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,15 @@ 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.ForkNodeArguments;
+import org.apache.maven.surefire.extensions.util.CommandlineExecutor;
+import org.apache.maven.surefire.extensions.util.CommandlineStreams;
+import org.apache.maven.surefire.extensions.util.CountdownCloseable;
+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;
@@ -61,9 +64,11 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Map;
 import java.util.Queue;
+import java.util.Set;
 import java.util.concurrent.ArrayBlockingQueue;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ConcurrentSkipListSet;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
@@ -72,14 +77,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 +91,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;
@@ -132,6 +134,8 @@ public class ForkStarter
 
     private static final AtomicInteger SYSTEM_PROPERTIES_FILE_COUNTER = new AtomicInteger();
 
+    private final Set<String> logsAtEnd = new ConcurrentSkipListSet<>();
+
     private final ScheduledExecutorService pingThreadScheduler = createPingScheduler();
 
     private final ScheduledExecutorService timeoutCheckScheduler;
@@ -192,7 +196,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 +216,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 );
+                }
             }
         }
 
@@ -261,6 +271,10 @@ public class ForkStarter
             defaultReporterFactory.close();
             pingThreadScheduler.shutdownNow();
             timeoutCheckScheduler.shutdownNow();
+            for ( String line : logsAtEnd )
+            {
+                log.warning( line );
+            }
         }
     }
 
@@ -287,9 +301,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 +364,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 +376,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 +391,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 +436,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 +449,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 +464,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 +561,29 @@ 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;
+        File dumpLogDir = replaceForkThreadsInPath( startupReportConfiguration.getReportsDirectory(), forkNumber );
         try
         {
+            forkChannel = forkNodeFactory.createForkChannel( new ForkedNodeArg( forkNumber, dumpLogDir ) );
+            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 )
             {
@@ -584,14 +604,10 @@ public class ForkStarter
             throw new SurefireBooterForkException( "Error creating properties files for forking", e );
         }
 
-        File dumpLogDir = replaceForkThreadsInPath( startupReportConfiguration.getReportsDirectory(), forkNumber );
         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 +618,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> errConsumer = new NativeStdErrStreamConsumer( reporter );
+            err = new LineConsumerThread( "fork-" + forkNumber + "-err-thread-", streams.getStdErrChannel(),
+                errConsumer, countdownCloseable );
             err.start();
+
             result = exec.awaitExit();
-            // END: end of the call of the extension
 
             if ( forkClient.hadTimeout() )
             {
@@ -647,13 +665,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 +682,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 +732,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 +751,6 @@ public class ForkStarter
 
             if ( booterForkException != null )
             {
-                // noinspection ThrowFromFinallyBlock
                 throw booterForkException;
             }
         }
@@ -855,4 +876,42 @@ public class ForkStarter
             }
         }, 0, TIMEOUT_CHECK_PERIOD_MILLIS, MILLISECONDS );
     }
+
+    private final class ForkedNodeArg implements ForkNodeArguments
+    {
+        private final int forkChannelId;
+        private final File dumpLogDir;
+
+        ForkedNodeArg( int forkChannelId, File dumpLogDir )
+        {
+            this.forkChannelId = forkChannelId;
+            this.dumpLogDir = dumpLogDir;
+        }
+
+        @Override
+        public int getForkChannelId()
+        {
+            return forkChannelId;
+        }
+
+        @Override
+        @Nonnull
+        public File dumpStreamText( @Nonnull String text )
+        {
+            return InPluginProcessDumpSingleton.getSingleton().dumpStreamText( text, dumpLogDir, forkChannelId );
+        }
+
+        @Override
+        public void logWarningAtEnd( @Nonnull String text )
+        {
+            logsAtEnd.add( text );
+        }
+
+        @Nonnull
+        @Override
+        public ConsoleLogger getConsoleLogger()
+        {
+            return log;
+        }
+    }
 }
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..bc76dfe 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.booter.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,30 @@ 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() );
+        notifier.setExitErrorEventListener( new ExitErrorEventListener() );
     }
 
     private final class TestSetStartingListener
@@ -167,7 +140,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 +150,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 +160,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 +170,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 +180,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 +190,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 +249,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 );
         }
     }
 
@@ -330,6 +304,16 @@ public class ForkClient
         }
     }
 
+    private final class ExitErrorEventListener implements ForkedProcessExitErrorListener
+    {
+        @Override
+        public void handle( StackTraceWriter stackTrace )
+        {
+            getOrCreateConsoleLogger()
+                .error( "System Exit has timed out in the forked process " + forkNumber );
+        }
+    }
+
     /**
      * Overridden by a subclass, see {@link org.apache.maven.plugin.surefire.booterclient.ForkStarter}.
      */
@@ -372,12 +356,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 +385,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 +398,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 +453,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..35fef0e
--- /dev/null
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/EventConsumerThread.java
@@ -0,0 +1,478 @@
+package org.apache.maven.plugin.surefire.extensions;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.apache.maven.plugin.surefire.booterclient.output.DeserializedStacktraceWriter;
+import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
+import org.apache.maven.surefire.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.ForkNodeArguments;
+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 org.apache.maven.surefire.shared.codec.binary.Base64;
+
+import javax.annotation.Nonnull;
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static org.apache.maven.surefire.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 ForkNodeArguments arguments;
+    private volatile boolean disabled;
+
+    public EventConsumerThread( @Nonnull String threadName,
+                                @Nonnull ReadableByteChannel channel,
+                                @Nonnull EventHandler<Event> eventHandler,
+                                @Nonnull CountdownCloseable countdownCloseable,
+                                @Nonnull ForkNodeArguments arguments )
+    {
+        super( threadName );
+        this.channel = channel;
+        this.eventHandler = eventHandler;
+        this.countdownCloseable = countdownCloseable;
+        this.arguments = arguments;
+    }
+
+    @Override
+    public void run()
+    {
+        try ( ReadableByteChannel stream = channel;
+              CountdownCloseable c = countdownCloseable; )
+        {
+            decode();
+        }
+        catch ( IOException e )
+        {
+            // not needed
+        }
+    }
+
+    @Override
+    public void disable()
+    {
+        disabled = true;
+    }
+
+    @Override
+    public void close() throws IOException
+    {
+        channel.close();
+    }
+
+    @SuppressWarnings( "checkstyle:innerassignment" )
+    private void decode() throws IOException
+    {
+        List<String> tokens = new ArrayList<>();
+        StringBuilder line = new StringBuilder();
+        StringBuilder token = new StringBuilder( MAGIC_NUMBER.length() );
+        ByteBuffer buffer = ByteBuffer.allocate( 1024 );
+        buffer.position( buffer.limit() );
+        boolean streamContinues;
+
+        start:
+        do
+        {
+            line.setLength( 0 );
+            tokens.clear();
+            token.setLength( 0 );
+            FrameCompletion completion = null;
+            for ( boolean frameStarted = false; streamContinues = read( buffer ); completion = null )
+            {
+                char c = (char) buffer.get();
+
+                if ( c == '\n' || c == '\r' )
+                {
+                    printExistingLine( line );
+                    continue start;
+                }
+
+                line.append( c );
+
+                if ( !frameStarted )
+                {
+                    if ( c == ':' )
+                    {
+                        frameStarted = true;
+                        token.setLength( 0 );
+                        tokens.clear();
+                    }
+                }
+                else
+                {
+                    if ( c == ':' )
+                    {
+                        tokens.add( token.toString() );
+                        token.setLength( 0 );
+                        completion = frameCompleteness( tokens );
+                        if ( completion == FrameCompletion.COMPLETE )
+                        {
+                            line.setLength( 0 );
+                            break;
+                        }
+                        else if ( completion == FrameCompletion.MALFORMED )
+                        {
+                            printExistingLine( line );
+                            continue start;
+                        }
+                    }
+                    else
+                    {
+                        token.append( c );
+                    }
+                }
+            }
+
+            if ( completion == FrameCompletion.COMPLETE )
+            {
+                Event event = toEvent( tokens );
+                if ( !disabled && event != null )
+                {
+                    eventHandler.handleEvent( event );
+                }
+            }
+
+            if ( !streamContinues )
+            {
+                printExistingLine( line );
+                return;
+            }
+        }
+        while ( true );
+    }
+
+    private boolean read( ByteBuffer buffer ) throws IOException
+    {
+        if ( buffer.hasRemaining() && buffer.position() > 0 )
+        {
+            return true;
+        }
+        else
+        {
+            buffer.clear();
+            boolean isEndOfStream = channel.read( buffer ) == -1;
+            buffer.flip();
+            return !isEndOfStream;
+        }
+    }
+
+    private void printExistingLine( StringBuilder line )
+    {
+        if ( line.length() != 0 )
+        {
+            ConsoleLogger logger = arguments.getConsoleLogger();
+            String s = line.toString().trim();
+            if ( s.contains( PRINTABLE_JVM_NATIVE_STREAM ) )
+            {
+                if ( logger.isDebugEnabled() )
+                {
+                    logger.debug( s );
+                }
+                else if ( logger.isInfoEnabled() )
+                {
+                    logger.info( s );
+                }
+                else
+                {
+                    // In case of debugging forked JVM, see PRINTABLE_JVM_NATIVE_STREAM.
+                    System.out.println( s );
+                }
+            }
+            else
+            {
+                String msg = "Corrupted STDOUT by directly writing to native stream in forked JVM "
+                    + arguments.getForkChannelId() + ".";
+                File dumpFile = arguments.dumpStreamText( msg + " Stream '" + s + "'." );
+                arguments.logWarningAtEnd( msg + " See FAQ web page and the dump file " + dumpFile.getAbsolutePath() );
+
+                if ( logger.isDebugEnabled() )
+                {
+                    logger.debug( s );
+                }
+            }
+        }
+    }
+
+    private Event toEvent( List<String> tokensInFrame )
+    {
+        Iterator<String> tokens = tokensInFrame.iterator();
+        String header = tokens.next();
+        assert header != null;
+
+        ForkedProcessEventType event = ForkedProcessEventType.byOpcode( tokens.next() );
+
+        if ( event == null )
+        {
+            return null;
+        }
+
+        if ( event.isControlCategory() )
+        {
+            switch ( event )
+            {
+                case BOOTERCODE_BYE:
+                    return new ControlByeEvent();
+                case BOOTERCODE_STOP_ON_NEXT_TEST:
+                    return new ControlStopOnNextTestEvent();
+                case BOOTERCODE_NEXT_TEST:
+                    return new ControlNextTestEvent();
+                default:
+                    throw new IllegalStateException( "Unknown enum " + event );
+            }
+        }
+        else if ( event.isConsoleErrorCategory() || event.isJvmExitError() )
+        {
+            Charset encoding = Charset.forName( tokens.next() );
+            StackTraceWriter stackTraceWriter = decodeTrace( encoding, tokens.next(), tokens.next(), tokens.next() );
+            return event.isConsoleErrorCategory()
+                ? new ConsoleErrorEvent( stackTraceWriter )
+                : new JvmExitErrorEvent( stackTraceWriter );
+        }
+        else if ( event.isConsoleCategory() )
+        {
+            Charset encoding = Charset.forName( tokens.next() );
+            String msg = decode( tokens.next(), encoding );
+            switch ( event )
+            {
+                case BOOTERCODE_CONSOLE_INFO:
+                    return new ConsoleInfoEvent( msg );
+                case BOOTERCODE_CONSOLE_DEBUG:
+                    return new ConsoleDebugEvent( msg );
+                case BOOTERCODE_CONSOLE_WARNING:
+                    return new ConsoleWarningEvent( msg );
+                default:
+                    throw new IllegalStateException( "Unknown enum " + event );
+            }
+        }
+        else if ( event.isStandardStreamCategory() )
+        {
+            RunMode mode = MODES.get( tokens.next() );
+            Charset encoding = Charset.forName( tokens.next() );
+            String output = decode( tokens.next(), encoding );
+            switch ( event )
+            {
+                case BOOTERCODE_STDOUT:
+                    return new StandardStreamOutEvent( mode, output );
+                case BOOTERCODE_STDOUT_NEW_LINE:
+                    return new StandardStreamOutWithNewLineEvent( mode, output );
+                case BOOTERCODE_STDERR:
+                    return new StandardStreamErrEvent( mode, output );
+                case BOOTERCODE_STDERR_NEW_LINE:
+                    return new StandardStreamErrWithNewLineEvent( mode, output );
+                default:
+                    throw new IllegalStateException( "Unknown enum " + event );
+            }
+        }
+        else if ( event.isSysPropCategory() )
+        {
+            RunMode mode = MODES.get( tokens.next() );
+            Charset encoding = Charset.forName( tokens.next() );
+            String key = decode( tokens.next(), encoding );
+            String value = decode( tokens.next(), encoding );
+            return new SystemPropertyEvent( mode, key, value );
+        }
+        else if ( event.isTestCategory() )
+        {
+            RunMode mode = MODES.get( tokens.next() );
+            Charset encoding = Charset.forName( tokens.next() );
+            TestSetReportEntry reportEntry =
+                decodeReportEntry( encoding, tokens.next(), tokens.next(), tokens.next(), tokens.next(),
+                    tokens.next(), tokens.next(), tokens.next(), tokens.next(), tokens.next(), tokens.next() );
+
+            switch ( event )
+            {
+                case BOOTERCODE_TESTSET_STARTING:
+                    return new TestsetStartingEvent( mode, reportEntry );
+                case BOOTERCODE_TESTSET_COMPLETED:
+                    return new TestsetCompletedEvent( mode, reportEntry );
+                case BOOTERCODE_TEST_STARTING:
+                    return new TestStartingEvent( mode, reportEntry );
+                case BOOTERCODE_TEST_SUCCEEDED:
+                    return new TestSucceededEvent( mode, reportEntry );
+                case BOOTERCODE_TEST_FAILED:
+                    return new TestFailedEvent( mode, reportEntry );
+                case BOOTERCODE_TEST_SKIPPED:
+                    return new TestSkippedEvent( mode, reportEntry );
+                case BOOTERCODE_TEST_ERROR:
+                    return new TestErrorEvent( mode, reportEntry );
+                case BOOTERCODE_TEST_ASSUMPTIONFAILURE:
+                    return new TestAssumptionFailureEvent( mode, reportEntry );
+                default:
+                    throw new IllegalStateException( "Unknown enum " + event );
+            }
+        }
+
+        throw new IllegalStateException( "Missing a branch for the event type " + event );
+    }
+
+    private static FrameCompletion frameCompleteness( List<String> tokens )
+    {
+        if ( !tokens.isEmpty() && !MAGIC_NUMBER.equals( tokens.get( 0 ) ) )
+        {
+            return FrameCompletion.MALFORMED;
+        }
+
+        if ( tokens.size() >= 2 )
+        {
+            String opcode = tokens.get( 1 );
+            ForkedProcessEventType event = ForkedProcessEventType.byOpcode( opcode );
+            if ( event == null )
+            {
+                return FrameCompletion.MALFORMED;
+            }
+            else if ( event.isControlCategory() )
+            {
+                return FrameCompletion.COMPLETE;
+            }
+            else if ( event.isConsoleErrorCategory() )
+            {
+                return tokens.size() == 6 ? FrameCompletion.COMPLETE : FrameCompletion.NOT_COMPLETE;
+            }
+            else if ( event.isConsoleCategory() )
+            {
+                return tokens.size() == 4 ? FrameCompletion.COMPLETE : FrameCompletion.NOT_COMPLETE;
+            }
+            else if ( event.isStandardStreamCategory() )
+            {
+                return tokens.size() == 5 ? FrameCompletion.COMPLETE : FrameCompletion.NOT_COMPLETE;
+            }
+            else if ( event.isSysPropCategory() )
+            {
+                return tokens.size() == 6 ? FrameCompletion.COMPLETE : FrameCompletion.NOT_COMPLETE;
+            }
+            else if ( event.isTestCategory() )
+            {
+                return tokens.size() == 14 ? FrameCompletion.COMPLETE : FrameCompletion.NOT_COMPLETE;
+            }
+            else if ( event.isJvmExitError() )
+            {
+                return tokens.size() == 6 ? FrameCompletion.COMPLETE : FrameCompletion.NOT_COMPLETE;
+            }
+        }
+        return FrameCompletion.NOT_COMPLETE;
+    }
+
+    static String decode( String line, Charset encoding )
+    {
+        // ForkedChannelEncoder is encoding the stream with US_ASCII
+        return line == null || "-".equals( line )
+            ? null
+            : new String( BASE64.decode( line.getBytes( US_ASCII ) ), encoding );
+    }
+
+    private static StackTraceWriter decodeTrace( Charset encoding, String encTraceMessage,
+                                                 String encSmartTrimmedStackTrace, String encStackTrace )
+    {
+        String traceMessage = decode( encTraceMessage, encoding );
+        String stackTrace = decode( encStackTrace, encoding );
+        String smartTrimmedStackTrace = decode( encSmartTrimmedStackTrace, encoding );
+        boolean exists = traceMessage != null || stackTrace != null || smartTrimmedStackTrace != null;
+        return exists ? new DeserializedStacktraceWriter( traceMessage, smartTrimmedStackTrace, stackTrace ) : null;
+    }
+
+    static TestSetReportEntry decodeReportEntry( Charset encoding,
+                                                 // ReportEntry:
+                                                 String encSource, String encSourceText, String encName,
+                                                 String encNameText, String encGroup, String encMessage,
+                                                 String encTimeElapsed,
+                                                 // StackTraceWriter:
+                                                 String encTraceMessage,
+                                                 String encSmartTrimmedStackTrace, String encStackTrace )
+        throws NumberFormatException
+    {
+        if ( encoding == null )
+        {
+            // corrupted or incomplete stream
+            return null;
+        }
+
+        String source = decode( encSource, encoding );
+        String sourceText = decode( encSourceText, encoding );
+        String name = decode( encName, encoding );
+        String nameText = decode( encNameText, encoding );
+        String group = decode( encGroup, encoding );
+        StackTraceWriter stackTraceWriter =
+            decodeTrace( encoding, encTraceMessage, encSmartTrimmedStackTrace, encStackTrace );
+        Integer elapsed = decodeToInteger( encTimeElapsed );
+        String message = decode( encMessage, encoding );
+        return reportEntry( source, sourceText, name, nameText,
+            group, stackTraceWriter, elapsed, message, Collections.<String, String>emptyMap() );
+    }
+
+    static Integer decodeToInteger( String line )
+    {
+        return line == null || "-".equals( line ) ? null : Integer.decode( line );
+    }
+
+    /**
+     * 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..e13846b
--- /dev/null
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/LegacyForkChannel.java
@@ -0,0 +1,87 @@
+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.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.ForkNodeArguments;
+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
+{
+    LegacyForkChannel( @Nonnull ForkNodeArguments arguments )
+    {
+        super( arguments );
+    }
+
+    @Override
+    public void connectToClient()
+    {
+    }
+
+    @Override
+    public String getForkNodeConnectionString()
+    {
+        return "pipe://" + getArguments().getForkChannelId();
+    }
+
+    @Override
+    public boolean useStdOut()
+    {
+        return true;
+    }
+
+    @Override
+    public CloseableDaemonThread bindCommandReader( @Nonnull CommandReader commands,
+                                                    WritableByteChannel stdIn )
+    {
+        return new StreamFeeder( "std-in-fork-" + getArguments().getForkChannelId(), stdIn, commands,
+            getArguments().getConsoleLogger() );
+    }
+
+    @Override
+    public CloseableDaemonThread bindEventHandler( @Nonnull EventHandler<Event> eventHandler,
+                                                   @Nonnull CountdownCloseable countdownCloseable,
+                                                   ReadableByteChannel stdOut )
+    {
+        return new EventConsumerThread( "fork-" + getArguments().getForkChannelId() + "-event-thread", stdOut,
+            eventHandler, countdownCloseable, getArguments() );
+    }
+
+    @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 61%
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..29d79af 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,21 @@ 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.surefire.extensions.ForkChannel;
+import org.apache.maven.surefire.extensions.ForkNodeArguments;
+import org.apache.maven.surefire.extensions.ForkNodeFactory;
+
+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( @Nonnull ForkNodeArguments arguments )
     {
-        TestSuite suite = new TestSuite();
-        suite.addTest( new JUnit4TestAdapter( CommandlineExecutorTest.class ) );
-        return suite;
+        return new LegacyForkChannel( arguments );
     }
 }
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..11aeb18
--- /dev/null
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/extensions/SurefireForkChannel.java
@@ -0,0 +1,169 @@
+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.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.ForkNodeArguments;
+import org.apache.maven.surefire.extensions.util.CountdownCloseable;
+
+import javax.annotation.Nonnull;
+import java.io.Closeable;
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketOption;
+import java.nio.channels.AsynchronousServerSocketChannel;
+import java.nio.channels.AsynchronousSocketChannel;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.WritableByteChannel;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+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.AsynchronousChannelGroup.withThreadPool;
+import static java.nio.channels.AsynchronousServerSocketChannel.open;
+import static org.apache.maven.surefire.util.internal.Channels.newBufferedChannel;
+import static org.apache.maven.surefire.util.internal.Channels.newChannel;
+import static org.apache.maven.surefire.util.internal.Channels.newInputStream;
+import static org.apache.maven.surefire.util.internal.Channels.newOutputStream;
+import static org.apache.maven.surefire.util.internal.DaemonThreadFactory.newDaemonThreadFactory;
+
+/**
+ * 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 ExecutorService THREAD_POOL = Executors.newCachedThreadPool( newDaemonThreadFactory() );
+
+    private final AsynchronousServerSocketChannel server;
+    private final String localHost;
+    private final int localPort;
+    private volatile AsynchronousSocketChannel worker;
+
+    SurefireForkChannel( @Nonnull ForkNodeArguments arguments ) throws IOException
+    {
+        super( arguments );
+        server = open( withThreadPool( THREAD_POOL ) );
+        setTrueOptions( SO_REUSEADDR, TCP_NODELAY, SO_KEEPALIVE );
+        InetAddress ip = Inet4Address.getLoopbackAddress();
+        server.bind( new InetSocketAddress( ip, 0 ), 1 );
+        InetSocketAddress localAddress = (InetSocketAddress) server.getLocalAddress();
+        localHost = localAddress.getHostString();
+        localPort = localAddress.getPort();
+    }
+
+    @Override
+    public void connectToClient() throws IOException
+    {
+        if ( worker != null )
+        {
+            throw new IllegalStateException( "already accepted TCP client connection" );
+        }
+
+        try
+        {
+            worker = server.accept().get();
+        }
+        catch ( InterruptedException e )
+        {
+            throw new IOException( e.getLocalizedMessage(), e );
+        }
+        catch ( ExecutionException e )
+        {
+            throw new IOException( e.getLocalizedMessage(), e.getCause() );
+        }
+    }
+
+    @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://" + localHost + ":" + localPort;
+    }
+
+    @Override
+    public boolean useStdOut()
+    {
+        return false;
+    }
+
+    @Override
+    public CloseableDaemonThread bindCommandReader( @Nonnull CommandReader commands,
+                                                    WritableByteChannel stdIn )
+    {
+        // dont use newBufferedChannel here - may cause the command is not sent and the JVM hangs
+        // only newChannel flushes the message
+        // newBufferedChannel does not flush
+        WritableByteChannel channel = newChannel( newOutputStream( worker ) );
+        return new StreamFeeder( "commands-fork-" + getArguments().getForkChannelId(), channel, commands,
+            getArguments().getConsoleLogger() );
+    }
+
+    @Override
+    public CloseableDaemonThread bindEventHandler( @Nonnull EventHandler<Event> eventHandler,
+                                                   @Nonnull CountdownCloseable countdownCloseable,
+                                                   ReadableByteChannel stdOut )
+    {
+        ReadableByteChannel channel = newBufferedChannel( newInputStream( worker ) );
+        return new EventConsumerThread( "fork-" + getArguments().getForkChannelId() + "-event-thread", channel,
+            eventHandler, countdownCloseable, getArguments() );
+    }
+
+    @Override
+    public void close() throws IOException
+    {
+        //noinspection unused,EmptyTryBlock,EmptyTryBlock
+        try ( Closeable c1 = worker; Closeable 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 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/SurefireForkNodeFactory.java
index df9cca1..0b47784 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.surefire.extensions.ForkChannel;
+import org.apache.maven.surefire.extensions.ForkNodeArguments;
+import org.apache.maven.surefire.extensions.ForkNodeFactory;
+
+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( @Nonnull ForkNodeArguments arguments )
+        throws IOException
     {
-        TestSuite suite = new TestSuite();
-        suite.addTest( new JUnit4TestAdapter( CommandlineExecutorTest.class ) );
-        return suite;
+        return new SurefireForkChannel( arguments );
     }
 }
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..4c1ac37 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,17 @@ 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.ForkNodeArguments;
+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 +39,26 @@ 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 org.apache.maven.surefire.util.internal.WritableBufferedByteChannel;
 
+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 org.apache.maven.surefire.util.internal.Channels.newBufferedChannel;
+import static org.apache.maven.surefire.util.internal.Channels.newChannel;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 /**
  * @author Kristian Rosenvold
@@ -74,8 +86,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 +94,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 +102,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 +110,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 +118,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 +126,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 +134,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 +142,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 +150,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 +158,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 +166,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 +174,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 +182,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 +224,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 +241,85 @@ 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( newBufferedChannel( printStream ) ), false )
                 .testStarting( expected );
 
-        new ForkingRunListener( new ForkedChannelEncoder( anotherPrintStream ), false )
+        new ForkingRunListener(
+            new LegacyMasterProcessChannelEncoder( newBufferedChannel( 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 );
+        ForkNodeArguments arguments = mock( ForkNodeArguments.class );
+        when( arguments.getConsoleLogger() ).thenReturn( logger );
+        ReadableByteChannel channel = newChannel( new ByteArrayInputStream( stream ) );
+        try ( EventConsumerThread t = new EventConsumerThread( "t", channel, handler, countdown, arguments ) )
+        {
+            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 +369,8 @@ public class ForkingRunListenerTest
 
     private RunListener createForkingRunListener()
     {
-        return new ForkingRunListener( new ForkedChannelEncoder( printStream ), false );
+        WritableBufferedByteChannel channel = (WritableBufferedByteChannel) newChannel( printStream );
+        return new ForkingRunListener( new LegacyMasterProcessChannelEncoder( channel ), false );
     }
 
     private class StandardTestRun
@@ -326,15 +384,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 +405,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 +425,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..7c89492 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.booter.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..3e2023e 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.booter.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..67342e3 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,80 @@ 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.ForkNodeArguments;
+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.surefire.shared.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 +107,88 @@ 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();
-    }
+        ForkNodeArguments arguments = mock( ForkNodeArguments.class );
+        when( arguments.getConsoleLogger() ).thenReturn( logger );
+        when( logger.isDebugEnabled() ).thenReturn( false );
+        when( logger.isInfoEnabled() ).thenReturn( true );
+        ReadableByteChannel channel = newChannel( new ByteArrayInputStream( nativeStream.getBytes() ) );
+        try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, arguments ) )
+        {
+            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 );
+
+        verify( logger ).isDebugEnabled();
+
+        verify( logger ).isInfoEnabled();
 
-    @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 )
-                .info( "Listening for transport dt_socket at address: bla" );
+            .thenReturn( true );
+        ForkNodeArguments arguments = mock( ForkNodeArguments.class );
+        when( arguments.getConsoleLogger() ).thenReturn( logger );
+        when( logger.isDebugEnabled() ).thenReturn( true );
+        when( logger.isInfoEnabled() ).thenReturn( false );
+        ReadableByteChannel channel = newChannel( new ByteArrayInputStream( nativeStream.getBytes() ) );
+        try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, arguments ) )
+        {
+            t.start();
+
+            Event event = eventHandler.pullEvent();
+            assertThat( event.isControlCategory() )
+                .isTrue();
+            assertThat( event.getEventType() )
+                .isEqualTo( BOOTERCODE_BYE );
+
+            verify( logger )
+                .debug( "Listening for transport dt_socket at address: bla" );
+
+            countdown.awaitClosed();
+        }
+
+        assertThat( eventHandler.sizeOfEventCache() )
+            .isEqualTo( 0 );
+
+        verify( logger ).isDebugEnabled();
+
         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 +196,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 +205,6 @@ public class ForkClientTest
 
     @Test
     public void shouldAcquireNextTest()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -296,10 +212,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 +236,6 @@ public class ForkClientTest
 
     @Test
     public void shouldNotifyWithBye()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -330,11 +243,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 +272,6 @@ public class ForkClientTest
 
     @Test
     public void shouldStopOnNextTest()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -369,10 +279,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 +289,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 +312,6 @@ public class ForkClientTest
 
     @Test
     public void shouldReceiveStdOut()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -415,10 +322,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 +354,6 @@ public class ForkClientTest
 
     @Test
     public void shouldReceiveStdOutNewLine()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -460,10 +364,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 +396,6 @@ public class ForkClientTest
 
     @Test
     public void shouldReceiveStdErr()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -505,10 +406,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 +438,6 @@ public class ForkClientTest
 
     @Test
     public void shouldReceiveStdErrNewLine()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -550,10 +448,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 +480,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 +506,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 +524,53 @@ 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() )
+        ForkNodeArguments arguments = mock( ForkNodeArguments.class );
+        when( arguments.getConsoleLogger() ).thenReturn( logger );
+        ReadableByteChannel channel = newChannel( new ByteArrayInputStream( nativeStream.getBytes() ) );
+        try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, arguments ) )
+        {
+            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 +581,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 +613,6 @@ public class ForkClientTest
 
     @Test
     public void shouldLogConsoleDebug()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -752,13 +623,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 +655,6 @@ public class ForkClientTest
 
     @Test
     public void shouldLogConsoleInfo()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -800,11 +665,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 +697,6 @@ public class ForkClientTest
 
     @Test
     public void shouldSendSystemProperty()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -846,11 +707,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 +737,6 @@ public class ForkClientTest
 
     @Test
     public void shouldSendTestsetStartingKilled()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -890,21 +747,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 +760,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 +769,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 +805,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 +832,6 @@ public class ForkClientTest
 
     @Test
     public void shouldSendTestsetStarting()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -1021,21 +842,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 +855,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 +866,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 +901,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 +928,6 @@ public class ForkClientTest
 
     @Test
     public void shouldSendTestsetCompleted()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -1156,21 +938,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 +951,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 +960,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 +996,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 +1021,6 @@ public class ForkClientTest
 
     @Test
     public void shouldSendTestStarting()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -1285,21 +1031,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 +1053,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 +1092,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 +1115,6 @@ public class ForkClientTest
 
     @Test
     public void shouldSendTestSucceeded()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -1415,21 +1125,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 +1147,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 +1192,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 +1219,6 @@ public class ForkClientTest
 
     @Test
     public void shouldSendTestFailed()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -1557,21 +1229,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 +1251,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 +1280,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 +1304,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 +1329,6 @@ public class ForkClientTest
 
     @Test
     public void shouldSendTestSkipped()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -1703,21 +1339,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 +1361,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 +1410,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 +1437,6 @@ public class ForkClientTest
 
     @Test
     public void shouldSendTestError()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -1849,21 +1447,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 +1470,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 +1516,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 +1543,6 @@ public class ForkClientTest
 
     @Test
     public void shouldSendTestAssumptionFailure()
-            throws IOException
     {
         String cwd = System.getProperty( "user.dir" );
         File target = new File( cwd, "target" );
@@ -1995,21 +1553,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 +1576,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 +1621,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 +1646,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/E2ETest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/E2ETest.java
new file mode 100644
index 0000000..80a3474
--- /dev/null
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/E2ETest.java
@@ -0,0 +1,182 @@
+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.MasterProcessChannelEncoder;
+import org.apache.maven.surefire.booter.spi.SurefireMasterProcessChannelProcessorFactory;
+import org.apache.maven.surefire.eventapi.Event;
+import org.apache.maven.surefire.extensions.EventHandler;
+import org.apache.maven.surefire.extensions.ForkNodeArguments;
+import org.apache.maven.surefire.extensions.util.CountdownCloseable;
+import org.apache.maven.surefire.report.ConsoleOutputReceiver;
+import org.junit.Test;
+
+import javax.annotation.Nonnull;
+import java.io.Closeable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Simulates the End To End use case where Maven process and Surefire process communicate using the TCP/IP protocol.
+ */
+@SuppressWarnings( "checkstyle:magicnumber" )
+public class E2ETest
+{
+    private static final String LONG_STRING =
+        "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789";
+
+    @Test
+    public void test() throws Exception
+    {
+        ConsoleLogger logger = mock( ConsoleLogger.class );
+        ForkNodeArguments arguments = mock( ForkNodeArguments.class );
+        when( arguments.getForkChannelId() ).thenReturn( 1 );
+        when( arguments.getConsoleLogger() ).thenReturn( logger );
+        final SurefireForkChannel server = new SurefireForkChannel( arguments );
+
+        final String connection = server.getForkNodeConnectionString();
+
+        final SurefireMasterProcessChannelProcessorFactory factory = new SurefireMasterProcessChannelProcessorFactory();
+        factory.connect( connection );
+        final MasterProcessChannelEncoder encoder = factory.createEncoder();
+
+        System.gc();
+
+        TimeUnit.SECONDS.sleep( 3L );
+
+        final CountDownLatch awaitHandlerFinished = new CountDownLatch( 2 );
+
+        Thread t = new Thread()
+        {
+            @Override
+            public void run()
+            {
+                ConsoleOutputReceiver target = new ConsoleOutputReceiver()
+                {
+                    @Override
+                    public void writeTestOutput( String output, boolean newLine, boolean stdout )
+                    {
+                        encoder.stdOut( output, true );
+                    }
+                };
+
+                //PrintStream out = System.out;
+                //PrintStream err = System.err;
+
+                //ConsoleOutputCapture.startCapture( target );
+
+                try
+                {
+                    long t1 = System.currentTimeMillis();
+                    for ( int i = 0; i < 400_000; i++ )
+                    {
+                        //System.out.println( LONG_STRING );
+                        encoder.stdOut( LONG_STRING, true );
+                    }
+                    long t2 = System.currentTimeMillis();
+                    long spent = t2 - t1;
+                    //System.setOut( out );
+                    //System.setErr( err );
+                    System.out.println( spent + "ms on write" );
+                    awaitHandlerFinished.countDown();
+                }
+                catch ( Exception e )
+                {
+                    e.printStackTrace();
+                }
+            }
+        };
+        t.setDaemon( true );
+        t.start();
+
+        server.connectToClient();
+
+        final AtomicLong readTime = new AtomicLong();
+
+        EventHandler<Event> h = new EventHandler<Event>()
+        {
+            private final AtomicInteger counter = new AtomicInteger();
+            private volatile long t1;
+
+            @Override
+            public void handleEvent( @Nonnull Event event )
+            {
+                try
+                {
+                    if ( counter.getAndIncrement() == 0 )
+                    {
+                        t1 = System.currentTimeMillis();
+                    }
+
+                    long t2 = System.currentTimeMillis();
+                    long spent = t2 - t1;
+
+                    if ( counter.get() % 100_000 == 0 )
+                    {
+                        System.out.println( spent + "ms: " + counter.get() );
+                    }
+
+                    if ( counter.get() == 320_000 )
+                    {
+                        readTime.set( spent );
+                        System.out.println( spent + "ms on read" );
+                        awaitHandlerFinished.countDown();
+                    }
+                }
+                catch ( Exception e )
+                {
+                    e.printStackTrace();
+                }
+            }
+        };
+
+        Closeable c = new Closeable()
+        {
+            @Override
+            public void close()
+            {
+            }
+        };
+
+        server.bindEventHandler( h, new CountdownCloseable( c, 1 ), null )
+            .start();
+
+        assertThat( awaitHandlerFinished.await( 30L, TimeUnit.SECONDS ) )
+            .isTrue();
+
+        factory.close();
+        server.close();
+
+        // 2 seconds while using the encoder/decoder
+        // 160 millis of sending pure data without encoder/decoder
+        assertThat( readTime.get() )
+            .describedAs( "The performance test should assert 2s of read time. "
+                + "The limit 6s guarantees that the read time does not exceed this limit on overloaded CPU." )
+            .isPositive()
+            .isLessThanOrEqualTo( 6_000L );
+    }
+}
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..4078de0
--- /dev/null
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/extensions/ForkedProcessEventNotifierTest.java
@@ -0,0 +1,1284 @@
+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.ForkNodeArguments;
+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.apache.maven.surefire.util.internal.WritableBufferedByteChannel;
+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.mockito.ArgumentCaptor;
+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.File;
+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.lang.String.format;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Arrays.copyOfRange;
+import static org.apache.maven.surefire.report.RunMode.NORMAL_RUN;
+import static org.apache.maven.surefire.shared.codec.binary.Base64.encodeBase64String;
+import static org.apache.maven.surefire.util.internal.Channels.newBufferedChannel;
+import static org.apache.maven.surefire.util.internal.Channels.newChannel;
+import static org.fest.assertions.Assertions.assertThat;
+import static org.fest.assertions.Index.atIndex;
+import static org.junit.Assert.assertTrue;
+import static org.junit.rules.ExpectedException.none;
+import static org.mockito.ArgumentMatchers.anyString;
+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();
+            WritableBufferedByteChannel wChannel = newBufferedChannel( out );
+            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( wChannel );
+            Map<String, String> props = ObjectUtils.systemProps();
+            encoder.sendSystemProperties( props );
+            wChannel.close();
+
+            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 );
+            ForkNodeArguments arguments = mock( ForkNodeArguments.class );
+            when( arguments.getConsoleLogger() ).thenReturn( logger );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, arguments ) )
+            {
+                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( newBufferedChannel( 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 );
+            ForkNodeArguments arguments = mock( ForkNodeArguments.class );
+            when( arguments.getConsoleLogger() ).thenReturn( logger );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, arguments ) )
+            {
+                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( newBufferedChannel( 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 );
+            ForkNodeArguments arguments = mock( ForkNodeArguments.class );
+            when( arguments.getConsoleLogger() ).thenReturn( logger );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, arguments ) )
+            {
+                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( newBufferedChannel( 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 );
+            ForkNodeArguments arguments = mock( ForkNodeArguments.class );
+            when( arguments.getConsoleLogger() ).thenReturn( logger );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, arguments ) )
+            {
+                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( newBufferedChannel( 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 );
+            ForkNodeArguments arguments = mock( ForkNodeArguments.class );
+            when( arguments.getConsoleLogger() ).thenReturn( logger );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, arguments ) )
+            {
+                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( newBufferedChannel( 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 );
+            ForkNodeArguments arguments = mock( ForkNodeArguments.class );
+            when( arguments.getConsoleLogger() ).thenReturn( logger );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, arguments ) )
+            {
+                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( newBufferedChannel( 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 );
+            ForkNodeArguments arguments = mock( ForkNodeArguments.class );
+            when( arguments.getConsoleLogger() ).thenReturn( logger );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, arguments ) )
+            {
+                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( newBufferedChannel( 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 );
+            ForkNodeArguments arguments = mock( ForkNodeArguments.class );
+            when( arguments.getConsoleLogger() ).thenReturn( logger );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, arguments ) )
+            {
+                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( newBufferedChannel( 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 );
+            ForkNodeArguments arguments = mock( ForkNodeArguments.class );
+            when( arguments.getConsoleLogger() ).thenReturn( logger );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, arguments ) )
+            {
+                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( newBufferedChannel( 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 );
+            ForkNodeArguments arguments = mock( ForkNodeArguments.class );
+            when( arguments.getConsoleLogger() ).thenReturn( logger );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, arguments ) )
+            {
+                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( newBufferedChannel( 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 );
+            ForkNodeArguments arguments = mock( ForkNodeArguments.class );
+            when( arguments.getConsoleLogger() ).thenReturn( logger );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, arguments ) )
+            {
+                t.start();
+                notifier.notifyEvent( eventHandler.pullEvent() );
+            }
+
+            assertThat( listener.called.get() )
+                .isTrue();
+        }
+
+        @Test
+        public void testStdOutStream() throws Exception
+        {
+            final Stream out = Stream.newStream();
+            WritableBufferedByteChannel wChannel = newBufferedChannel( out );
+            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( wChannel );
+            encoder.stdOut( "msg", false );
+            wChannel.close();
+
+            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 );
+            ForkNodeArguments arguments = mock( ForkNodeArguments.class );
+            when( arguments.getConsoleLogger() ).thenReturn( logger );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, arguments ) )
+            {
+                t.start();
+                notifier.notifyEvent( eventHandler.pullEvent() );
+            }
+
+            assertThat( listener.called.get() )
+                .isTrue();
+        }
+
+        @Test
+        public void testStdOutStreamPrint() throws Exception
+        {
+            final Stream out = Stream.newStream();
+            WritableBufferedByteChannel wChannel = newBufferedChannel( out );
+            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( wChannel );
+            encoder.stdOut( "", false );
+            wChannel.close();
+
+            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 );
+            ForkNodeArguments arguments = mock( ForkNodeArguments.class );
+            when( arguments.getConsoleLogger() ).thenReturn( logger );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, arguments ) )
+            {
+                t.start();
+                notifier.notifyEvent( eventHandler.pullEvent() );
+            }
+
+            assertThat( listener.called.get() )
+                .isTrue();
+        }
+
+        @Test
+        public void testStdOutStreamPrintWithNull() throws Exception
+        {
+            final Stream out = Stream.newStream();
+            WritableBufferedByteChannel wChannel = newBufferedChannel( out );
+            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( wChannel );
+            encoder.stdOut( null, false );
+            wChannel.close();
+
+            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 );
+            ForkNodeArguments arguments = mock( ForkNodeArguments.class );
+            when( arguments.getConsoleLogger() ).thenReturn( logger );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, arguments ) )
+            {
+                t.start();
+                notifier.notifyEvent( eventHandler.pullEvent() );
+            }
+
+            assertThat( listener.called.get() )
+                .isTrue();
+        }
+
+        @Test
+        public void testStdOutStreamPrintln() throws Exception
+        {
+            final Stream out = Stream.newStream();
+            WritableBufferedByteChannel wChannel = newBufferedChannel( out );
+            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( wChannel );
+            encoder.stdOut( "", true );
+            wChannel.close();
+
+            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 );
+            ForkNodeArguments arguments = mock( ForkNodeArguments.class );
+            when( arguments.getConsoleLogger() ).thenReturn( logger );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, arguments ) )
+            {
+                t.start();
+                notifier.notifyEvent( eventHandler.pullEvent() );
+            }
+
+            assertThat( listener.called.get() )
+                .isTrue();
+        }
+
+        @Test
+        public void testStdOutStreamPrintlnWithNull() throws Exception
+        {
+            final Stream out = Stream.newStream();
+            WritableBufferedByteChannel wChannel = newBufferedChannel( out );
+            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( wChannel );
+            encoder.stdOut( null, true );
+            wChannel.close();
+
+            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 );
+            ForkNodeArguments arguments = mock( ForkNodeArguments.class );
+            when( arguments.getConsoleLogger() ).thenReturn( logger );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, arguments ) )
+            {
+                t.start();
+                notifier.notifyEvent( eventHandler.pullEvent() );
+            }
+
+            assertThat( listener.called.get() )
+                .isTrue();
+        }
+
+        @Test
+        public void testStdErrStream() throws Exception
+        {
+            final Stream out = Stream.newStream();
+            WritableBufferedByteChannel wChannel = newBufferedChannel( out );
+            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( wChannel );
+            encoder.stdErr( "msg", false );
+            wChannel.close();
+
+            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 );
+            ForkNodeArguments arguments = mock( ForkNodeArguments.class );
+            when( arguments.getConsoleLogger() ).thenReturn( logger );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, arguments ) )
+            {
+                t.start();
+                notifier.notifyEvent( eventHandler.pullEvent() );
+            }
+
+            assertThat( listener.called.get() )
+                .isTrue();
+        }
+
+        @Test
+        public void shouldCountSameNumberOfSystemProperties() throws Exception
+        {
+            final Stream out = Stream.newStream();
+            WritableBufferedByteChannel wChannel = newBufferedChannel( out );
+            LegacyMasterProcessChannelEncoder encoder = new LegacyMasterProcessChannelEncoder( wChannel );
+            encoder.sendSystemProperties( ObjectUtils.systemProps() );
+            wChannel.close();
+
+            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 );
+            ForkNodeArguments arguments = mock( ForkNodeArguments.class );
+            when( arguments.getConsoleLogger() ).thenReturn( logger );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, arguments ) )
+            {
+                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 );
+            when( logger.isDebugEnabled() ).thenReturn( true );
+            ForkNodeArguments arguments = mock( ForkNodeArguments.class );
+            when( arguments.dumpStreamText( anyString() ) ).thenReturn( new File( "" ) );
+            when( arguments.getConsoleLogger() ).thenReturn( logger );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, arguments ) )
+            {
+                t.start();
+                countdown.awaitClosed();
+            }
+
+            ArgumentCaptor<String> dumpLine = ArgumentCaptor.forClass( String.class );
+            verify( logger, times( 2 ) ).debug( dumpLine.capture() );
+            assertThat( dumpLine.getAllValues() )
+                .hasSize( 2 )
+                .contains( ":maven-surefire-event:abnormal-run:", atIndex( 0 ) )
+                .contains( "-:", atIndex( 1 ) );
+
+            ArgumentCaptor<String> dumpText = ArgumentCaptor.forClass( String.class );
+            verify( arguments, times( 2 ) ).dumpStreamText( dumpText.capture() );
+            String dump = "Corrupted STDOUT by directly writing to native stream in forked JVM 0.";
+            assertThat( dumpText.getAllValues() )
+                .hasSize( 2 )
+                .contains( format( dump + " Stream '%s'.", ":maven-surefire-event:abnormal-run:" ), atIndex( 0 ) )
+                .contains( format( dump + " Stream '%s'.", "-:" ), atIndex( 1 ) );
+
+            ArgumentCaptor<String> warning = ArgumentCaptor.forClass( String.class );
+            verify( arguments, times( 2 ) ).logWarningAtEnd( warning.capture() );
+            dump += " See FAQ web page and the dump file ";
+            assertThat( warning.getAllValues() )
+                .hasSize( 2 );
+            assertThat( warning.getAllValues().get( 0 ) )
+                .startsWith( dump );
+            assertThat( warning.getAllValues().get( 1 ) )
+                .startsWith( dump );
+        }
+
+        @Test
+        public void shouldHandleExit() throws Exception
+        {
+            final Stream out = Stream.newStream();
+            LegacyMasterProcessChannelEncoder encoder =
+                new LegacyMasterProcessChannelEncoder( newBufferedChannel( 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.sendExitError( 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 );
+            ForkNodeArguments arguments = mock( ForkNodeArguments.class );
+            when( arguments.getConsoleLogger() ).thenReturn( logger );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, arguments ) )
+            {
+                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( newBufferedChannel( 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 );
+            ForkNodeArguments arguments = mock( ForkNodeArguments.class );
+            when( arguments.getConsoleLogger() ).thenReturn( logger );
+            try ( EventConsumerThread t = new EventConsumerThread( "t", channel, eventHandler, countdown, arguments ) )
+            {
+                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..22bf702 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,11 @@ 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.E2ETest;
+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 +54,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 +91,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 +100,16 @@ 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 ) );
+        suite.addTest( new JUnit4TestAdapter( E2ETest.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..7b20825
--- /dev/null
+++ b/maven-surefire-common/src/test/java/org/apache/maven/surefire/extensions/ForkChannelTest.java
@@ -0,0 +1,189 @@
+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.plugin.surefire.log.api.ConsoleLogger;
+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.File;
+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.CountDownLatch;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+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
+    {
+        ForkNodeArguments forkNodeArguments = new ForkNodeArguments()
+        {
+            @Override
+            public int getForkChannelId()
+            {
+                return 1;
+            }
+
+            @Override
+            @Nonnull
+            public File dumpStreamText( @Nonnull String text )
+            {
+                return new File( "" );
+            }
+
+            @Override
+            public void logWarningAtEnd( @Nonnull String text )
+            {
+            }
+
+            @Override
+            @Nonnull
+            public ConsoleLogger getConsoleLogger()
+            {
+                return new MockReporter();
+            }
+        };
+
+        ForkNodeFactory factory = new SurefireForkNodeFactory();
+        try ( ForkChannel channel = factory.createForkChannel( forkNodeArguments ) )
+        {
+            assertThat( channel.getArguments().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();
+
+            final TestLessInputStreamBuilder builder = new TestLessInputStreamBuilder();
+            TestLessInputStream commandReader = builder.build();
+            final CountDownLatch isCloseableCalled = new CountDownLatch( 1 );
+            Closeable closeable = new Closeable()
+            {
+                @Override
+                public void close()
+                {
+                    isCloseableCalled.countDown();
+                }
+            };
+            CountdownCloseable cc = new CountdownCloseable( closeable, 1 );
+            Consumer consumer = new Consumer();
+
+            Client client = new Client( uri.getPort() );
+            client.start();
+
+            channel.connectToClient();
+            channel.bindCommandReader( commandReader, null ).start();
+            channel.bindEventHandler( consumer, cc, null ).start();
+
+            commandReader.noop();
+
+            client.join( TESTCASE_TIMEOUT );
+
+            assertThat( hasError.get() )
+                .isFalse();
+
+            assertThat( isCloseableCalled.await( TESTCASE_TIMEOUT, MILLISECONDS ) )
+                .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;
... 11566 lines suppressed ...