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 2019/03/25 20:28:15 UTC

[maven-surefire] branch SUREFIRE-1222-2 updated (106b7d1 -> 9b8c30c)

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

tibordigana pushed a change to branch SUREFIRE-1222-2
in repository https://gitbox.apache.org/repos/asf/maven-surefire.git.


 discard 106b7d1  [SUREFIRE-1222] ForkClient attempts to consume unrelated lines
     new 9b8c30c  [SUREFIRE-1222] ForkClient attempts to consume unrelated lines

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (106b7d1)
            \
             N -- N -- N   refs/heads/SUREFIRE-1222-2 (9b8c30c)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../surefire/booterclient/output/ForkClient.java   |   50 +-
 .../booterclient/ForkingRunListenerTest.java       |    2 +-
 .../plugin/surefire/booterclient/MockReporter.java |   18 +-
 .../booterclient/output/ForkClientTest.java        | 1770 +++++++++++++++++++-
 .../surefire/report/CategorizedReportEntry.java    |    1 -
 5 files changed, 1810 insertions(+), 31 deletions(-)


[maven-surefire] 01/01: [SUREFIRE-1222] ForkClient attempts to consume unrelated lines

Posted by ti...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 9b8c30c5ccb5f6c0bb35456c50fdca35abe212da
Author: tibordigana <ti...@apache.org>
AuthorDate: Sun Mar 10 01:10:08 2019 +0100

    [SUREFIRE-1222] ForkClient attempts to consume unrelated lines
---
 Jenkinsfile                                        |    2 +-
 .../plugin/surefire/booterclient/ForkStarter.java  |   26 +-
 .../surefire/booterclient/output/ForkClient.java   |  486 ++---
 .../booterclient/output/ForkedChannelDecoder.java  |  347 ++++
 .../output/ForkedChannelDecoderErrorHandler.java   |   20 +-
 .../output/ForkedProcessEventListener.java         |   20 +-
 .../output/ForkedProcessExitErrorListener.java     |   20 +-
 .../output/ForkedProcessPropertyEventListener.java |   22 +-
 .../output/ForkedProcessReportEventListener.java   |   24 +-
 .../ForkedProcessStackTraceEventListener.java      |   20 +-
 .../ForkedProcessStandardOutErrEventListener.java  |   22 +-
 .../output/ForkedProcessStringEventListener.java   |   20 +-
 .../surefire/report/ConsoleOutputFileReporter.java |   16 +-
 .../surefire/report/DirectConsoleOutput.java       |   30 +-
 .../surefire/report/StatelessXmlReporter.java      |    2 +-
 .../plugin/surefire/report/TestSetRunListener.java |   20 +-
 .../Utf8RecodingDeferredFileOutputStream.java      |   33 +-
 .../plugin/surefire/report/WrappedReportEntry.java |    2 +-
 .../surefire/runorder/StatisticsReporter.java      |    4 +-
 .../booterclient/ForkingRunListenerTest.java       |   70 +-
 .../plugin/surefire/booterclient/MockReporter.java |   28 +-
 .../booterclient/output/ForkClientTest.java        | 2020 ++++++++++++++++++++
 .../output/ForkedChannelDecoderTest.java           |  834 ++++++++
 .../surefire/report/StatelessXmlReporterTest.java  |    9 +-
 .../org/apache/maven/surefire/JUnit4SuiteTest.java |    4 +
 .../report/ConsoleOutputFileReporterTest.java      |    9 +-
 pom.xml                                            |    6 +
 surefire-api/pom.xml                               |   14 +
 .../maven/surefire/booter/BaseProviderFactory.java |   21 +-
 .../maven/surefire/booter/CommandReader.java       |   32 +-
 .../surefire/booter/ForkedChannelEncoder.java      |  404 ++++
 .../maven/surefire/booter/ForkedProcessEvent.java  |  132 ++
 .../surefire/booter/ForkingReporterFactory.java    |   17 +-
 .../maven/surefire/booter/ForkingRunListener.java  |  293 +--
 .../surefire/booter/MasterProcessCommand.java      |    6 +-
 .../surefire/providerapi/ProviderParameters.java   |    3 +
 .../surefire/report/CategorizedReportEntry.java    |    1 -
 .../surefire/report/ConsoleOutputCapture.java      |  147 +-
 .../surefire/report/ConsoleOutputReceiver.java     |    7 +-
 .../maven/surefire/report/ConsoleStream.java       |    2 +-
 .../report/DefaultDirectConsoleReporter.java       |    6 -
 .../apache/maven/surefire/report/RunListener.java  |    9 +
 .../org/apache/maven/surefire/report/RunMode.java  |   64 +
 .../maven/surefire/testset/TestListResolver.java   |    2 +-
 .../maven/surefire/util/internal/StringUtils.java  |  246 ---
 .../java/org/apache/maven/JUnit4SuiteTest.java     |    6 +-
 .../surefire/booter/ForkedChannelEncoderTest.java  | 1048 ++++++++++
 .../surefire/booter/ForkingRunListenerTest.java    |    3 +-
 .../surefire/util/internal/StringUtilsTest.java    |  142 --
 surefire-booter/pom.xml                            |    5 +
 .../apache/maven/surefire/booter/ForkedBooter.java |   72 +-
 .../maven/surefire/booter/LazyTestsToRun.java      |   11 +-
 .../maven/surefire/booter/CommandReaderTest.java   |   10 +-
 .../apache/maven/surefire/its/ConsoleOutputIT.java |   28 +-
 .../surefire/its/fixture/OutputValidator.java      |    3 +-
 .../Surefire1082ParallelJUnitParameterizedIT.java  |    4 +-
 .../jiras/Surefire1535TestNGParallelSuitesIT.java  |   18 +-
 .../surefire/log/api/ConsoleLoggerUtils.java       |    5 +
 .../surefire/common/junit4/JUnit4RunListener.java  |    9 +-
 .../apache/maven/surefire/junit4/MockReporter.java |    7 +
 .../junitplatform/JUnitPlatformProviderTest.java   |   13 +-
 .../maven/surefire/junit/JUnit3Provider.java       |   12 +-
 .../maven/surefire/junit/JUnitTestSetTest.java     |    6 +
 .../surefire/junitcore/ConcurrentRunListener.java  |   12 +-
 .../maven/surefire/junitcore/LogicalStream.java    |   40 +-
 .../junitcore/NonConcurrentRunListener.java        |    5 +-
 .../maven/surefire/junitcore/TestMethod.java       |    6 +-
 .../testng/utils/GroupMatcherMethodSelector.java   |    8 +-
 .../apache/maven/surefire/testng/TestSuite.java    |    4 +-
 69 files changed, 5639 insertions(+), 1360 deletions(-)

diff --git a/Jenkinsfile b/Jenkinsfile
index f2e61f5..3f96cad 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -30,7 +30,7 @@ properties(
     ]
 )
 
-final def oses = ['linux':'ubuntu && !H24', 'windows':'Windows']
+final def oses = ['linux':'ubuntu && !H24', 'windows':'Windows && !windows-2016-1']
 final def mavens = env.BRANCH_NAME == 'master' ? ['3.6.x', '3.2.x'] : ['3.6.x']
 // all non-EOL versions and the first EA
 final def jdks = [12, 11, 8, 7]
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 2d3cc42..5ee5538 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
@@ -92,12 +92,14 @@ import static org.apache.maven.shared.utils.cli.CommandLineUtils.executeCommandL
 import static org.apache.maven.shared.utils.cli.ShutdownHookUtils.addShutDownHook;
 import static org.apache.maven.shared.utils.cli.ShutdownHookUtils.removeShutdownHook;
 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.suite.RunResult.SUCCESS;
 import static org.apache.maven.surefire.suite.RunResult.failure;
 import static org.apache.maven.surefire.suite.RunResult.timeout;
 import static org.apache.maven.surefire.util.internal.ConcurrencyUtils.countDownToZero;
 import static org.apache.maven.surefire.util.internal.DaemonThreadFactory.newDaemonThread;
 import static org.apache.maven.surefire.util.internal.DaemonThreadFactory.newDaemonThreadFactory;
+import static org.apache.maven.surefire.util.internal.StringUtils.NL;
 
 /**
  * Starts the fork or runs in-process.
@@ -650,11 +652,29 @@ public class ForkStarter
                 if ( forkClient.isErrorInFork() )
                 {
                     StackTraceWriter errorInFork = forkClient.getErrorInFork();
-                    // noinspection ThrowFromFinallyBlock
+                    String errorInForkMessage =
+                            errorInFork == null ? null : errorInFork.getThrowable().getLocalizedMessage();
+                    boolean showStackTrace = providerConfiguration.getMainCliOptions().contains( SHOW_ERRORS );
+                    String stackTrace = errorInForkMessage;
+                    if ( showStackTrace )
+                    {
+                        if ( errorInFork != null )
+                        {
+                            if ( stackTrace == null )
+                            {
+                                stackTrace = "";
+                            }
+                            else
+                            {
+                                stackTrace += NL;
+                            }
+                            stackTrace += errorInFork.writeTrimmedTraceToString();
+                        }
+                    }
+                    //noinspection ThrowFromFinallyBlock
                     throw new SurefireBooterForkException( "There was an error in the forked process"
                                                         + detail
-                                                        + '\n'
-                                                        + errorInFork.getThrowable().getLocalizedMessage(), cause );
+                                                        + ( stackTrace == null ? "" : stackTrace ), cause );
                 }
                 if ( !forkClient.isSaidGoodBye() )
                 {
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 436566f..7e7ab78 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
@@ -26,6 +26,7 @@ import org.apache.maven.shared.utils.cli.StreamConsumer;
 import org.apache.maven.surefire.report.ConsoleOutputReceiver;
 import org.apache.maven.surefire.report.ReportEntry;
 import org.apache.maven.surefire.report.RunListener;
+import org.apache.maven.surefire.report.RunMode;
 import org.apache.maven.surefire.report.StackTraceWriter;
 import org.apache.maven.surefire.report.TestSetReportEntry;
 
@@ -33,44 +34,21 @@ import java.io.BufferedReader;
 import java.io.File;
 import java.io.IOException;
 import java.io.StringReader;
-import java.nio.ByteBuffer;
-import java.util.Collections;
 import java.util.Map;
 import java.util.Queue;
 import java.util.Set;
-import java.util.StringTokenizer;
 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.Integer.decode;
 import static java.lang.System.currentTimeMillis;
 import static java.util.Collections.unmodifiableMap;
-import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_BYE;
-import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_CONSOLE;
-import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_DEBUG;
-import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_ERROR;
-import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_NEXT_TEST;
-import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_STDERR;
-import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_STDOUT;
-import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_STOP_ON_NEXT_TEST;
-import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_SYSPROPS;
-import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_TESTSET_COMPLETED;
-import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_TESTSET_STARTING;
-import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_TEST_ASSUMPTIONFAILURE;
-import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_TEST_ERROR;
-import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_TEST_FAILED;
-import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_TEST_SKIPPED;
-import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_TEST_STARTING;
-import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_TEST_SUCCEEDED;
-import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_WARNING;
 import static org.apache.maven.surefire.booter.Shutdown.KILL;
 import static org.apache.maven.surefire.report.CategorizedReportEntry.reportEntry;
+import static org.apache.maven.surefire.util.internal.StringUtils.isBlank;
 import static org.apache.maven.surefire.util.internal.StringUtils.isNotBlank;
-import static org.apache.maven.surefire.util.internal.StringUtils.unescapeBytes;
-import static org.apache.maven.surefire.util.internal.StringUtils.unescapeString;
 
 // todo move to the same package with ForkStarter
 
@@ -95,11 +73,13 @@ public class ForkClient
     private final Queue<String> testsInProgress = new ConcurrentLinkedQueue<>();
 
     /**
-     * {@code testSetStartedAt} is set to non-zero after received
-     * {@link org.apache.maven.surefire.booter.ForkingRunListener#BOOTERCODE_TESTSET_STARTING test-set}.
+     * <em>testSetStartedAt</em> is set to non-zero after received
+     * {@link org.apache.maven.surefire.booter.ForkedChannelEncoder#testSetStarting(ReportEntry, boolean)}.
      */
     private final AtomicLong testSetStartedAt = new AtomicLong( START_TIME_ZERO );
 
+    private final ForkedChannelDecoder decoder = new ForkedChannelDecoder();
+
     private final ConsoleLogger log;
 
     /**
@@ -112,6 +92,8 @@ public class ForkClient
     /**
      * Used by single Thread started by {@link ThreadedStreamConsumer} and therefore does not need to be volatile.
      */
+    private final ForkedChannelDecoderErrorHandler errorHandler;
+
     private RunListener testSetReporter;
 
     /**
@@ -129,8 +111,227 @@ public class ForkClient
         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 );
+        }
+    }
+
+    private final class TestSetStartingListener
+            implements ForkedProcessReportEventListener<TestSetReportEntry>
+    {
+        @Override
+        public void handle( RunMode runMode, TestSetReportEntry reportEntry )
+        {
+            getTestSetReporter().testSetStarting( reportEntry );
+            setCurrentStartTime();
+        }
+    }
+
+    private final class TestSetCompletedListener
+            implements ForkedProcessReportEventListener<TestSetReportEntry>
+    {
+        @Override
+        public void handle( RunMode runMode, TestSetReportEntry reportEntry )
+        {
+            testsInProgress.clear();
+            TestSetReportEntry entry = reportEntry( reportEntry.getSourceName(), reportEntry.getName(),
+                    reportEntry.getGroup(), reportEntry.getStackTraceWriter(), reportEntry.getElapsed(),
+                    reportEntry.getMessage(), getTestVmSystemProperties() );
+            getTestSetReporter().testSetCompleted( entry );
+        }
+    }
+
+    private final class TestStartingListener implements ForkedProcessReportEventListener
+    {
+        @Override
+        public void handle( RunMode runMode, ReportEntry reportEntry )
+        {
+            testsInProgress.offer( reportEntry.getSourceName() );
+            getTestSetReporter().testStarting( reportEntry );
+        }
+    }
+
+    private final class TestSucceededListener implements ForkedProcessReportEventListener
+    {
+        @Override
+        public void handle( RunMode runMode, ReportEntry reportEntry )
+        {
+            testsInProgress.remove( reportEntry.getSourceName() );
+            getTestSetReporter().testSucceeded( reportEntry );
+        }
+    }
+
+    private final class TestFailedListener implements ForkedProcessReportEventListener
+    {
+        @Override
+        public void handle( RunMode runMode, ReportEntry reportEntry )
+        {
+            testsInProgress.remove( reportEntry.getSourceName() );
+            getTestSetReporter().testFailed( reportEntry );
+        }
+    }
+
+    private final class TestSkippedListener implements ForkedProcessReportEventListener
+    {
+        @Override
+        public void handle( RunMode runMode, ReportEntry reportEntry )
+        {
+            testsInProgress.remove( reportEntry.getSourceName() );
+            getTestSetReporter().testSkipped( reportEntry );
+        }
+    }
+
+    private final class TestErrorListener implements ForkedProcessReportEventListener
+    {
+        @Override
+        public void handle( RunMode runMode, ReportEntry reportEntry )
+        {
+            testsInProgress.remove( reportEntry.getSourceName() );
+            getTestSetReporter().testError( reportEntry );
+        }
+    }
+
+    private final class TestAssumptionFailureListener implements ForkedProcessReportEventListener
+    {
+        @Override
+        public void handle( RunMode runMode, ReportEntry reportEntry )
+        {
+            testsInProgress.remove( reportEntry.getSourceName() );
+            getTestSetReporter().testAssumptionFailure( reportEntry );
+        }
+    }
+
+    private final class SystemPropertiesListener implements ForkedProcessPropertyEventListener
+    {
+        @Override
+        public void handle( RunMode runMode, String key, String value )
+        {
+            testVmSystemProperties.put( key, value );
+        }
+    }
+
+    private final class StdOutListener implements ForkedProcessStandardOutErrEventListener
+    {
+        @Override
+        public void handle( RunMode runMode, String output, boolean newLine )
+        {
+            writeTestOutput( output, newLine, true );
+        }
+    }
+
+    private final class StdErrListener implements ForkedProcessStandardOutErrEventListener
+    {
+        @Override
+        public void handle( RunMode runMode, String output, boolean newLine )
+        {
+            writeTestOutput( output, newLine, false );
+        }
+    }
+
+    private final class ConsoleListener implements ForkedProcessStringEventListener
+    {
+        @Override
+        public void handle( String msg )
+        {
+            getOrCreateConsoleLogger()
+                    .info( msg );
+        }
+    }
+
+    private final class AcquireNextTestListener implements ForkedProcessEventListener
+    {
+        @Override
+        public void handle()
+        {
+            notifiableTestStream.provideNewTest();
+        }
+    }
+
+    private class ErrorListener implements ForkedProcessStackTraceEventListener
+    {
+        @Override
+        public void handle( String msg, String smartStackTrace, String stackTrace )
+        {
+            if ( errorInFork == null )
+            {
+                errorInFork = deserializeStackTraceWriter( msg, smartStackTrace, stackTrace );
+                if ( msg != null )
+                {
+                    getOrCreateConsoleLogger()
+                            .error( msg );
+                }
+            }
+            dumpToLoFile( msg, null );
+        }
+    }
+
+    private final class ByeListener implements ForkedProcessEventListener
+    {
+        @Override
+        public void handle()
+        {
+            saidGoodBye = true;
+            notifiableTestStream.acknowledgeByeEventReceived();
+        }
+    }
+
+    private final class StopOnNextTestListener implements ForkedProcessEventListener
+    {
+        @Override
+        public void handle()
+        {
+            stopOnNextTest();
+        }
+    }
+
+    private final class DebugListener implements ForkedProcessStringEventListener
+    {
+        @Override
+        public void handle( String msg )
+        {
+            getOrCreateConsoleLogger()
+                    .debug( msg );
+        }
+    }
+
+    private final class WarningListener implements ForkedProcessStringEventListener
+    {
+        @Override
+        public void handle( String msg )
+        {
+            getOrCreateConsoleLogger()
+                    .warning( msg );
+        }
     }
 
+    /**
+     * Overridden by a subclass, see {@link org.apache.maven.plugin.surefire.booterclient.ForkStarter}.
+     */
     protected void stopOnNextTest()
     {
     }
@@ -204,125 +405,24 @@ public class ForkClient
 
     private void processLine( String event )
     {
-        final OperationalData op;
-        try
-        {
-            op = new OperationalData( event );
-        }
-        catch ( RuntimeException e )
-        {
-            logStreamWarning( e, event );
-            return;
-        }
-        final String remaining = op.getData();
-        switch ( op.getOperationId() )
-        {
-            case BOOTERCODE_TESTSET_STARTING:
-                getTestSetReporter().testSetStarting( createReportEntry( remaining ) );
-                setCurrentStartTime();
-                break;
-            case BOOTERCODE_TESTSET_COMPLETED:
-                testsInProgress.clear();
-
-                getTestSetReporter().testSetCompleted( createReportEntry( remaining, testVmSystemProperties ) );
-                break;
-            case BOOTERCODE_TEST_STARTING:
-                ReportEntry reportEntry = createReportEntry( remaining );
-                testsInProgress.offer( reportEntry.getSourceName() );
-
-                getTestSetReporter().testStarting( createReportEntry( remaining ) );
-                break;
-            case BOOTERCODE_TEST_SUCCEEDED:
-                reportEntry = createReportEntry( remaining );
-                testsInProgress.remove( reportEntry.getSourceName() );
-
-                getTestSetReporter().testSucceeded( createReportEntry( remaining ) );
-                break;
-            case BOOTERCODE_TEST_FAILED:
-                reportEntry = createReportEntry( remaining );
-                testsInProgress.remove( reportEntry.getSourceName() );
-
-                getTestSetReporter().testFailed( createReportEntry( remaining ) );
-                break;
-            case BOOTERCODE_TEST_SKIPPED:
-                reportEntry = createReportEntry( remaining );
-                testsInProgress.remove( reportEntry.getSourceName() );
-
-                getTestSetReporter().testSkipped( createReportEntry( remaining ) );
-                break;
-            case BOOTERCODE_TEST_ERROR:
-                reportEntry = createReportEntry( remaining );
-                testsInProgress.remove( reportEntry.getSourceName() );
-
-                getTestSetReporter().testError( createReportEntry( remaining ) );
-                break;
-            case BOOTERCODE_TEST_ASSUMPTIONFAILURE:
-                reportEntry = createReportEntry( remaining );
-                testsInProgress.remove( reportEntry.getSourceName() );
-
-                getTestSetReporter().testAssumptionFailure( createReportEntry( remaining ) );
-                break;
-            case BOOTERCODE_SYSPROPS:
-                int keyEnd = remaining.indexOf( "," );
-                StringBuilder key = new StringBuilder();
-                StringBuilder value = new StringBuilder();
-                unescapeString( key, remaining.substring( 0, keyEnd ) );
-                unescapeString( value, remaining.substring( keyEnd + 1 ) );
-                testVmSystemProperties.put( key.toString(), value.toString() );
-                break;
-            case BOOTERCODE_STDOUT:
-                writeTestOutput( remaining, true );
-                break;
-            case BOOTERCODE_STDERR:
-                writeTestOutput( remaining, false );
-                break;
-            case BOOTERCODE_CONSOLE:
-                getOrCreateConsoleLogger()
-                        .info( createConsoleMessage( remaining ) );
-                break;
-            case BOOTERCODE_NEXT_TEST:
-                notifiableTestStream.provideNewTest();
-                break;
-            case BOOTERCODE_ERROR:
-                errorInFork = deserializeStackTraceWriter( new StringTokenizer( remaining, "," ) );
-                break;
-            case BOOTERCODE_BYE:
-                saidGoodBye = true;
-                notifiableTestStream.acknowledgeByeEventReceived();
-                break;
-            case BOOTERCODE_STOP_ON_NEXT_TEST:
-                stopOnNextTest();
-                break;
-            case BOOTERCODE_DEBUG:
-                getOrCreateConsoleLogger()
-                        .debug( createConsoleMessage( remaining ) );
-                break;
-            case BOOTERCODE_WARNING:
-                getOrCreateConsoleLogger()
-                        .warning( createConsoleMessage( remaining ) );
-                break;
-            default:
-                logStreamWarning( event );
-        }
+        decoder.handleEvent( event, errorHandler );
     }
 
-    private void logStreamWarning( String event )
+    private File dumpToLoFile( String msg, Throwable e )
     {
-        logStreamWarning( null, event );
+        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( Throwable e, String event )
+    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 + ".";
-
-            InPluginProcessDumpSingleton util = InPluginProcessDumpSingleton.getSingleton();
-            File reportsDir = defaultReporterFactory.getReportsDirectory();
-            File dump =
-                    e == null
-                    ? util.dumpStreamText( msg + " Stream '" + event + "'.", reportsDir, forkNumber )
-                    : util.dumpStreamException( e, msg + " Stream '" + event + "'.", reportsDir, forkNumber );
+            File dump = dumpToLoFile( msg + " Stream '" + event + "'.", e );
 
             if ( printedErrorStream.compareAndSet( false, true ) )
             {
@@ -352,91 +452,29 @@ public class ForkClient
         }
     }
 
-    private void writeTestOutput( String remaining, boolean isStdout )
+    private void writeTestOutput( String output, boolean newLine, boolean isStdout )
     {
-        int csNameEnd = remaining.indexOf( ',' );
-        String charsetName = remaining.substring( 0, csNameEnd );
-        String byteEncoded = remaining.substring( csNameEnd + 1 );
-        ByteBuffer unescaped = unescapeBytes( byteEncoded, charsetName );
-
-        if ( unescaped.hasArray() )
-        {
-            byte[] convertedBytes = unescaped.array();
-            getOrCreateConsoleOutputReceiver()
-                .writeTestOutput( convertedBytes, unescaped.position(), unescaped.remaining(), isStdout );
-        }
-        else
-        {
-            byte[] convertedBytes = new byte[unescaped.remaining()];
-            unescaped.get( convertedBytes, 0, unescaped.remaining() );
-            getOrCreateConsoleOutputReceiver()
-                .writeTestOutput( convertedBytes, 0, convertedBytes.length, isStdout );
-        }
+        getOrCreateConsoleOutputReceiver()
+                .writeTestOutput( output, newLine, isStdout );
     }
 
     public final void consumeMultiLineContent( String s )
             throws IOException
     {
-        BufferedReader stringReader = new BufferedReader( new StringReader( s ) );
-        for ( String s1 = stringReader.readLine(); s1 != null; s1 = stringReader.readLine() )
+        if ( isBlank( s ) )
         {
-            consumeLine( s1 );
+            logStreamWarning( s, null );
         }
-    }
-
-    private String createConsoleMessage( String remaining )
-    {
-        return unescape( remaining );
-    }
-
-    private TestSetReportEntry createReportEntry( String untokenized )
-    {
-        return createReportEntry( untokenized, Collections.<String, String>emptyMap() );
-    }
-
-    private TestSetReportEntry createReportEntry( String untokenized, Map<String, String> systemProperties )
-    {
-        StringTokenizer tokens = new StringTokenizer( untokenized, "," );
-        try
-        {
-            String source = nullableCsv( tokens.nextToken() );
-            String name = nullableCsv( tokens.nextToken() );
-            String group = nullableCsv( tokens.nextToken() );
-            String message = nullableCsv( tokens.nextToken() );
-            String elapsedStr = tokens.nextToken();
-            Integer elapsed = "null".equals( elapsedStr ) ? null : decode( elapsedStr );
-            final StackTraceWriter stackTraceWriter =
-                    tokens.hasMoreTokens() ? deserializeStackTraceWriter( tokens ) : null;
-
-            return reportEntry( source, name, group, stackTraceWriter, elapsed, message, systemProperties );
-        }
-        catch ( RuntimeException e )
+        else
         {
-            throw new RuntimeException( untokenized, e );
+            BufferedReader stringReader = new BufferedReader( new StringReader( s ) );
+            for ( String s1 = stringReader.readLine(); s1 != null; s1 = stringReader.readLine() )
+            {
+                consumeLine( s1 );
+            }
         }
     }
 
-    private StackTraceWriter deserializeStackTraceWriter( StringTokenizer tokens )
-    {
-        String stackTraceMessage = nullableCsv( tokens.nextToken() );
-        String smartStackTrace = nullableCsv( tokens.nextToken() );
-        String stackTrace = tokens.hasMoreTokens() ? nullableCsv( tokens.nextToken() ) : null;
-        boolean hasTrace = stackTrace != null;
-        return hasTrace ? new DeserializedStacktraceWriter( stackTraceMessage, smartStackTrace, stackTrace ) : null;
-    }
-
-    private String nullableCsv( String source )
-    {
-        return "null".equals( source ) ? null : unescape( source );
-    }
-
-    private String unescape( String source )
-    {
-        StringBuilder stringBuffer = new StringBuilder( source.length() );
-        unescapeString( stringBuffer, source );
-        return stringBuffer.toString();
-    }
-
     public final Map<String, String> getTestVmSystemProperties()
     {
         return unmodifiableMap( testVmSystemProperties );
@@ -493,32 +531,10 @@ public class ForkClient
         return !testsInProgress.isEmpty();
     }
 
-    private static final class OperationalData
+    private StackTraceWriter deserializeStackTraceWriter( String stackTraceMessage,
+                                                          String smartStackTrace, String stackTrace )
     {
-        private final byte operationId;
-        private final String data;
-
-        OperationalData( String event )
-        {
-            operationId = (byte) event.charAt( 0 );
-            int comma = event.indexOf( ",", 3 );
-            if ( comma < 0 )
-            {
-                throw new IllegalArgumentException( "Stream stdin corrupted. Expected comma after third character "
-                                                            + "in command '" + event + "'." );
-            }
-            int rest = event.indexOf( ",", comma );
-            data = event.substring( rest + 1 );
-        }
-
-        byte getOperationId()
-        {
-            return operationId;
-        }
-
-        String getData()
-        {
-            return data;
-        }
+        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
new file mode 100644
index 0000000..60ed138
--- /dev/null
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedChannelDecoder.java
@@ -0,0 +1,347 @@
+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.StringTokenizer;
+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.util.internal.StringUtils.isBlank;
+import static org.apache.maven.surefire.util.internal.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;
+        }
+
+        StringTokenizer tokenizer = new StringTokenizer( line.substring( MAGIC_NUMBER.length() ), ":" );
+        String opcode = tokenizer.hasMoreTokens() ? tokenizer.nextToken() : 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 = tokenizer.hasMoreTokens() ? Charset.forName( tokenizer.nextToken() ) : null;
+                if ( listener != null && encoding != null )
+                {
+                    String msg = tokenizer.hasMoreTokens() ? decode( tokenizer.nextToken(), encoding ) : "";
+                    listener.handle( msg );
+                }
+            }
+            else if ( event.isConsoleErrorCategory() )
+            {
+                Charset encoding = tokenizer.hasMoreTokens() ? Charset.forName( tokenizer.nextToken() ) : null;
+                if ( consoleErrorEventListener != null && encoding != null )
+                {
+                    String msg = tokenizer.hasMoreTokens() ? decode( tokenizer.nextToken(), encoding ) : null;
+                    String smartStackTrace =
+                            tokenizer.hasMoreTokens() ? decode( tokenizer.nextToken(), encoding ) : null;
+                    String stackTrace = tokenizer.hasMoreTokens() ? decode( tokenizer.nextToken(), encoding ) : null;
+                    consoleErrorEventListener.handle( msg, smartStackTrace, stackTrace );
+                }
+            }
+            else if ( event.isStandardStreamCategory() )
+            {
+                ForkedProcessStandardOutErrEventListener listener = stdOutErrEventListeners.get( event );
+                RunMode mode = tokenizer.hasMoreTokens() ? MODES.get( tokenizer.nextToken() ) : null;
+                Charset encoding = tokenizer.hasMoreTokens() ? Charset.forName( tokenizer.nextToken() ) : null;
+                if ( listener != null && encoding != null && mode != null )
+                {
+                    boolean newLine = event == BOOTERCODE_STDOUT_NEW_LINE || event == BOOTERCODE_STDERR_NEW_LINE;
+                    String output = tokenizer.hasMoreTokens() ? decode( tokenizer.nextToken(), encoding ) : "";
+                    listener.handle( mode, output, newLine );
+                }
+            }
+            else if ( event.isSysPropCategory() )
+            {
+                RunMode mode = tokenizer.hasMoreTokens() ? MODES.get( tokenizer.nextToken() ) : null;
+                Charset encoding = tokenizer.hasMoreTokens() ? Charset.forName( tokenizer.nextToken() ) : null;
+                String key = tokenizer.hasMoreTokens() ? decode( tokenizer.nextToken(), encoding ) : "";
+                if ( propertyEventListener != null && isNotBlank( key ) )
+                {
+                    String value = tokenizer.hasMoreTokens() ? decode( tokenizer.nextToken(), encoding ) : "";
+                    propertyEventListener.handle( mode, key, value );
+                }
+            }
+            else if ( event.isTestCategory() )
+            {
+                ForkedProcessReportEventListener listener = reportEventListeners.get( event );
+                RunMode mode = tokenizer.hasMoreTokens() ? MODES.get( tokenizer.nextToken() ) : null;
+                Charset encoding = tokenizer.hasMoreTokens() ? Charset.forName( tokenizer.nextToken() ) : null;
+                if ( listener != null && encoding != null && mode != null )
+                {
+                    String sourceName = tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null;
+                    String name = tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null;
+                    String group = tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null;
+                    String message = tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null;
+                    String elapsed = tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null;
+                    String traceMessage = tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null;
+                    String smartTrimmedStackTrace = tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null;
+                    String stackTrace = tokenizer.hasMoreTokens() ? tokenizer.nextToken() : null;
+
+                    listener.handle( mode, toReportEntry( encoding, sourceName, name, group, message, elapsed,
+                            traceMessage, smartTrimmedStackTrace, stackTrace ) );
+                }
+            }
+            else if ( event.isJvmExitError() )
+            {
+                if ( exitErrorEventListener != null )
+                {
+                    Charset encoding = tokenizer.hasMoreTokens() ? Charset.forName( tokenizer.nextToken() ) : null;
+                    String message = tokenizer.hasMoreTokens() ? decode( tokenizer.nextToken(), encoding ) : "";
+                    String smartTrimmedStackTrace =
+                            tokenizer.hasMoreTokens() ? decode( tokenizer.nextToken(), encoding ) : "";
+                    String stackTrace = tokenizer.hasMoreTokens() ? decode( tokenizer.nextToken(), encoding ) : "";
+                    exitErrorEventListener.handle( message, smartTrimmedStackTrace, stackTrace );
+                }
+            }
+        }
+        catch ( IllegalArgumentException e )
+        {
+            errorHandler.handledError( line, e );
+        }
+    }
+
+    static ReportEntry toReportEntry( Charset encoding,
+                   // ReportEntry:
+                   String encSource, String encName, 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 name = decode( encName, 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, name, 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/surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputReceiver.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedChannelDecoderErrorHandler.java
similarity index 60%
copy from surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputReceiver.java
copy to maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedChannelDecoderErrorHandler.java
index 06d6414..8faa803 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputReceiver.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedChannelDecoderErrorHandler.java
@@ -1,4 +1,4 @@
-package org.apache.maven.surefire.report;
+package org.apache.maven.plugin.surefire.booterclient.output;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -20,20 +20,10 @@ package org.apache.maven.surefire.report;
  */
 
 /**
- * A receiver of stdout/sterr output from running tests. This receiver knows how to associate
- * the output with a given testset.
+ * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
+ * @since 3.0.0-M4
  */
-public interface ConsoleOutputReceiver
+public interface ForkedChannelDecoderErrorHandler
 {
-
-    /**
-     * Forwards process output from the running test-case into the reporting system
-     *
-     * @param buf    the buffer to write
-     * @param off    offset
-     * @param len    len
-     * @param stdout Indicates if this is stdout
-     */
-    void writeTestOutput( byte[] buf, int off, int len, boolean stdout );
-
+    void handledError( String line, Throwable e );
 }
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputReceiver.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessEventListener.java
similarity index 60%
copy from surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputReceiver.java
copy to maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessEventListener.java
index 06d6414..6148295 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputReceiver.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessEventListener.java
@@ -1,4 +1,4 @@
-package org.apache.maven.surefire.report;
+package org.apache.maven.plugin.surefire.booterclient.output;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -20,20 +20,10 @@ package org.apache.maven.surefire.report;
  */
 
 /**
- * A receiver of stdout/sterr output from running tests. This receiver knows how to associate
- * the output with a given testset.
+ * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
+ * @since 3.0.0-M4
  */
-public interface ConsoleOutputReceiver
+public interface ForkedProcessEventListener
 {
-
-    /**
-     * Forwards process output from the running test-case into the reporting system
-     *
-     * @param buf    the buffer to write
-     * @param off    offset
-     * @param len    len
-     * @param stdout Indicates if this is stdout
-     */
-    void writeTestOutput( byte[] buf, int off, int len, boolean stdout );
-
+    void handle();
 }
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputReceiver.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessExitErrorListener.java
similarity index 60%
copy from surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputReceiver.java
copy to maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessExitErrorListener.java
index 06d6414..b14c38c 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputReceiver.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessExitErrorListener.java
@@ -1,4 +1,4 @@
-package org.apache.maven.surefire.report;
+package org.apache.maven.plugin.surefire.booterclient.output;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -20,20 +20,10 @@ package org.apache.maven.surefire.report;
  */
 
 /**
- * A receiver of stdout/sterr output from running tests. This receiver knows how to associate
- * the output with a given testset.
+ * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
+ * @since 3.0.0-M4
  */
-public interface ConsoleOutputReceiver
+public interface ForkedProcessExitErrorListener
 {
-
-    /**
-     * Forwards process output from the running test-case into the reporting system
-     *
-     * @param buf    the buffer to write
-     * @param off    offset
-     * @param len    len
-     * @param stdout Indicates if this is stdout
-     */
-    void writeTestOutput( byte[] buf, int off, int len, boolean stdout );
-
+    void handle( String exceptionMessage, String smartTrimmedStackTrace, String stackTrace );
 }
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputReceiver.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessPropertyEventListener.java
similarity index 60%
copy from surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputReceiver.java
copy to maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessPropertyEventListener.java
index 06d6414..9ef3e09 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputReceiver.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessPropertyEventListener.java
@@ -1,4 +1,4 @@
-package org.apache.maven.surefire.report;
+package org.apache.maven.plugin.surefire.booterclient.output;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,21 +19,13 @@ package org.apache.maven.surefire.report;
  * under the License.
  */
 
+import org.apache.maven.surefire.report.RunMode;
+
 /**
- * A receiver of stdout/sterr output from running tests. This receiver knows how to associate
- * the output with a given testset.
+ * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
+ * @since 3.0.0-M4
  */
-public interface ConsoleOutputReceiver
+public interface ForkedProcessPropertyEventListener
 {
-
-    /**
-     * Forwards process output from the running test-case into the reporting system
-     *
-     * @param buf    the buffer to write
-     * @param off    offset
-     * @param len    len
-     * @param stdout Indicates if this is stdout
-     */
-    void writeTestOutput( byte[] buf, int off, int len, boolean stdout );
-
+    void handle( RunMode runMode, String key, String value );
 }
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputReceiver.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessReportEventListener.java
similarity index 60%
copy from surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputReceiver.java
copy to maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessReportEventListener.java
index 06d6414..06054df 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputReceiver.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessReportEventListener.java
@@ -1,4 +1,4 @@
-package org.apache.maven.surefire.report;
+package org.apache.maven.plugin.surefire.booterclient.output;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,21 +19,15 @@ package org.apache.maven.surefire.report;
  * under the License.
  */
 
+import org.apache.maven.surefire.report.ReportEntry;
+import org.apache.maven.surefire.report.RunMode;
+
 /**
- * A receiver of stdout/sterr output from running tests. This receiver knows how to associate
- * the output with a given testset.
+ * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
+ * @since 3.0.0-M4
+ * @param <T> report entry type
  */
-public interface ConsoleOutputReceiver
+public interface ForkedProcessReportEventListener<T extends ReportEntry>
 {
-
-    /**
-     * Forwards process output from the running test-case into the reporting system
-     *
-     * @param buf    the buffer to write
-     * @param off    offset
-     * @param len    len
-     * @param stdout Indicates if this is stdout
-     */
-    void writeTestOutput( byte[] buf, int off, int len, boolean stdout );
-
+    void handle( RunMode runMode, T reportEntry );
 }
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputReceiver.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessStackTraceEventListener.java
similarity index 60%
copy from surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputReceiver.java
copy to maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessStackTraceEventListener.java
index 06d6414..f54cc40 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputReceiver.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessStackTraceEventListener.java
@@ -1,4 +1,4 @@
-package org.apache.maven.surefire.report;
+package org.apache.maven.plugin.surefire.booterclient.output;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -20,20 +20,10 @@ package org.apache.maven.surefire.report;
  */
 
 /**
- * A receiver of stdout/sterr output from running tests. This receiver knows how to associate
- * the output with a given testset.
+ * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
+ * @since 3.0.0-M4
  */
-public interface ConsoleOutputReceiver
+public interface ForkedProcessStackTraceEventListener
 {
-
-    /**
-     * Forwards process output from the running test-case into the reporting system
-     *
-     * @param buf    the buffer to write
-     * @param off    offset
-     * @param len    len
-     * @param stdout Indicates if this is stdout
-     */
-    void writeTestOutput( byte[] buf, int off, int len, boolean stdout );
-
+    void handle( String msg, String smartStackTrace, String stackTrace );
 }
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputReceiver.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessStandardOutErrEventListener.java
similarity index 60%
copy from surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputReceiver.java
copy to maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessStandardOutErrEventListener.java
index 06d6414..b60d6ed 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputReceiver.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessStandardOutErrEventListener.java
@@ -1,4 +1,4 @@
-package org.apache.maven.surefire.report;
+package org.apache.maven.plugin.surefire.booterclient.output;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,21 +19,13 @@ package org.apache.maven.surefire.report;
  * under the License.
  */
 
+import org.apache.maven.surefire.report.RunMode;
+
 /**
- * A receiver of stdout/sterr output from running tests. This receiver knows how to associate
- * the output with a given testset.
+ * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
+ * @since 3.0.0-M4
  */
-public interface ConsoleOutputReceiver
+public interface ForkedProcessStandardOutErrEventListener
 {
-
-    /**
-     * Forwards process output from the running test-case into the reporting system
-     *
-     * @param buf    the buffer to write
-     * @param off    offset
-     * @param len    len
-     * @param stdout Indicates if this is stdout
-     */
-    void writeTestOutput( byte[] buf, int off, int len, boolean stdout );
-
+    void handle( RunMode runMode, String output, boolean newLine );
 }
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputReceiver.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessStringEventListener.java
similarity index 60%
copy from surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputReceiver.java
copy to maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessStringEventListener.java
index 06d6414..9bd863a 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputReceiver.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedProcessStringEventListener.java
@@ -1,4 +1,4 @@
-package org.apache.maven.surefire.report;
+package org.apache.maven.plugin.surefire.booterclient.output;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -20,20 +20,10 @@ package org.apache.maven.surefire.report;
  */
 
 /**
- * A receiver of stdout/sterr output from running tests. This receiver knows how to associate
- * the output with a given testset.
+ * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
+ * @since 3.0.0-M4
  */
-public interface ConsoleOutputReceiver
+public interface ForkedProcessStringEventListener
 {
-
-    /**
-     * Forwards process output from the running test-case into the reporting system
-     *
-     * @param buf    the buffer to write
-     * @param off    offset
-     * @param len    len
-     * @param stdout Indicates if this is stdout
-     */
-    void writeTestOutput( byte[] buf, int off, int len, boolean stdout );
-
+    void handle( String msg );
 }
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/ConsoleOutputFileReporter.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/ConsoleOutputFileReporter.java
index f6645b1..857b68e 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/ConsoleOutputFileReporter.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/ConsoleOutputFileReporter.java
@@ -30,7 +30,9 @@ import java.io.IOException;
 import java.util.concurrent.atomic.AtomicStampedReference;
 import java.util.concurrent.locks.ReentrantLock;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.apache.maven.plugin.surefire.report.FileReporter.getReportFile;
+import static org.apache.maven.surefire.util.internal.StringUtils.NL;
 
 /**
  * Surefire output consumer proxy that writes test output to a {@link java.io.File} for each test suite.
@@ -99,7 +101,7 @@ public class ConsoleOutputFileReporter
     }
 
     @Override
-    public void writeTestOutput( byte[] buf, int off, int len, boolean stdout )
+    public void writeTestOutput( String output, boolean newLine, boolean stdout )
     {
         lock.lock();
         try
@@ -121,12 +123,22 @@ public class ConsoleOutputFileReporter
                     os = new BufferedOutputStream( new FileOutputStream( file ), STREAM_BUFFER_SIZE );
                     fileOutputStream.set( os, OPEN );
                 }
-                os.write( buf, off, len );
+
+                if ( output == null )
+                {
+                    output = "null";
+                }
+                os.write( output.getBytes( UTF_8 ) );
+                if ( newLine )
+                {
+                    os.write( NL.getBytes( UTF_8 ) );
+                }
             }
         }
         catch ( IOException e )
         {
             dumpException( e );
+            // todo use UncheckedIOException in Java 8
             throw new RuntimeException( e );
         }
         finally
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/DirectConsoleOutput.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/DirectConsoleOutput.java
index 056e903..6a94562 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/DirectConsoleOutput.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/DirectConsoleOutput.java
@@ -19,15 +19,10 @@ package org.apache.maven.plugin.surefire.report;
  * under the License.
  */
 
-import java.io.PrintStream;
-import java.nio.CharBuffer;
-import java.nio.charset.CharacterCodingException;
-import java.nio.charset.Charset;
-
 import org.apache.maven.surefire.report.ReportEntry;
 
-import static java.nio.ByteBuffer.wrap;
-import static java.nio.charset.Charset.defaultCharset;
+import java.io.PrintStream;
+
 import static java.util.Objects.requireNonNull;
 
 /**
@@ -41,8 +36,6 @@ import static java.util.Objects.requireNonNull;
 public class DirectConsoleOutput
     implements TestcycleConsoleOutputReceiver
 {
-    private static final Charset STANDARD_CHARSET = defaultCharset();
-
     private final PrintStream sout;
 
     private final PrintStream serr;
@@ -54,21 +47,16 @@ public class DirectConsoleOutput
     }
 
     @Override
-    public void writeTestOutput( byte[] buf, int off, int len, boolean stdout )
+    public void writeTestOutput( String output, boolean newLine, boolean stdout )
     {
         PrintStream stream = stdout ? sout : serr;
-        //noinspection SynchronizationOnLocalVariableOrMethodParameter
-        synchronized ( stream )
+        if ( newLine )
+        {
+            stream.println( output );
+        }
+        else
         {
-            try
-            {
-                CharBuffer decode = STANDARD_CHARSET.newDecoder().decode( wrap( buf, off, len ) );
-                stream.append( decode );
-            }
-            catch ( CharacterCodingException e )
-            {
-                stream.write( buf, off, len );
-            }
+            stream.print( output );
         }
     }
 
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/StatelessXmlReporter.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/StatelessXmlReporter.java
index c54943c..11cec8d 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/StatelessXmlReporter.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/StatelessXmlReporter.java
@@ -33,8 +33,8 @@ import java.io.FilterOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
-import java.util.Deque;
 import java.util.ArrayList;
+import java.util.Deque;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/TestSetRunListener.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/TestSetRunListener.java
index 995d687..decf63c 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/TestSetRunListener.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/TestSetRunListener.java
@@ -30,12 +30,15 @@ import org.apache.maven.plugin.surefire.runorder.StatisticsReporter;
 import org.apache.maven.surefire.report.ConsoleOutputReceiver;
 import org.apache.maven.surefire.report.ReportEntry;
 import org.apache.maven.surefire.report.RunListener;
+import org.apache.maven.surefire.report.RunMode;
 import org.apache.maven.surefire.report.TestSetReportEntry;
 
 import static org.apache.maven.plugin.surefire.report.ReportEntryType.ERROR;
 import static org.apache.maven.plugin.surefire.report.ReportEntryType.FAILURE;
 import static org.apache.maven.plugin.surefire.report.ReportEntryType.SKIPPED;
 import static org.apache.maven.plugin.surefire.report.ReportEntryType.SUCCESS;
+import static org.apache.maven.surefire.report.RunMode.NORMAL_RUN;
+import static java.util.Objects.requireNonNull;
 
 /**
  * Reports data for a single test set.
@@ -66,6 +69,8 @@ public class TestSetRunListener
 
     private Utf8RecodingDeferredFileOutputStream testStdErr = initDeferred( "stderr" );
 
+    private volatile RunMode runMode = NORMAL_RUN;
+
     @SuppressWarnings( "checkstyle:parameternumber" )
     public TestSetRunListener( ConsoleReporter consoleReporter, FileReporter fileReporter,
                                StatelessXmlReporter simpleXMLReporter,
@@ -143,13 +148,13 @@ public class TestSetRunListener
     }
 
     @Override
-    public synchronized void writeTestOutput( byte[] buf, int off, int len, boolean stdout )
+    public void writeTestOutput( String output, boolean newLine, boolean stdout )
     {
         try
         {
-            Utf8RecodingDeferredFileOutputStream os = stdout ? testStdOut : testStdErr;
-            os.write( buf, off, len );
-            consoleOutputReceiver.writeTestOutput( buf, off, len, stdout );
+            Utf8RecodingDeferredFileOutputStream stream = stdout ? testStdOut : testStdErr;
+            stream.write( output, newLine );
+            consoleOutputReceiver.writeTestOutput( output, newLine, stdout );
         }
         catch ( IOException e )
         {
@@ -247,6 +252,13 @@ public class TestSetRunListener
     {
     }
 
+    public RunMode markAs( RunMode currentRunMode )
+    {
+        RunMode runMode = this.runMode;
+        this.runMode = requireNonNull( currentRunMode );
+        return runMode;
+    }
+
     @Override
     public void testAssumptionFailure( ReportEntry report )
     {
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/Utf8RecodingDeferredFileOutputStream.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/Utf8RecodingDeferredFileOutputStream.java
index 8b496cd..c3eda01 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/Utf8RecodingDeferredFileOutputStream.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/Utf8RecodingDeferredFileOutputStream.java
@@ -23,11 +23,9 @@ import org.apache.commons.io.output.DeferredFileOutputStream;
 
 import java.io.IOException;
 import java.io.OutputStream;
-import java.nio.ByteBuffer;
-import java.nio.CharBuffer;
-import java.nio.charset.Charset;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.maven.surefire.util.internal.StringUtils.NL;
 
 /**
  * A deferred file output stream decorator that recodes the bytes written into the stream from the VM default encoding
@@ -44,10 +42,10 @@ class Utf8RecodingDeferredFileOutputStream
     @SuppressWarnings( "checkstyle:magicnumber" )
     Utf8RecodingDeferredFileOutputStream( String channel )
     {
-        this.deferredFileOutputStream = new DeferredFileOutputStream( 1000000, channel, "deferred", null );
+        deferredFileOutputStream = new DeferredFileOutputStream( 1000000, channel, "deferred", null );
     }
 
-    public synchronized void write( byte[] buf, int off, int len )
+    public synchronized void write( String output, boolean newLine )
         throws IOException
     {
         if ( closed )
@@ -55,28 +53,15 @@ class Utf8RecodingDeferredFileOutputStream
             return;
         }
 
-        if ( !Charset.defaultCharset().equals( UTF_8 ) )
+        if ( output == null )
         {
-            CharBuffer decodedFromDefaultCharset = Charset.defaultCharset().decode( ByteBuffer.wrap( buf, off, len ) );
-            ByteBuffer utf8Encoded = UTF_8.encode( decodedFromDefaultCharset );
-
-            if ( utf8Encoded.hasArray() )
-            {
-                byte[] convertedBytes = utf8Encoded.array();
-
-                deferredFileOutputStream.write( convertedBytes, utf8Encoded.position(), utf8Encoded.remaining() );
-            }
-            else
-            {
-                byte[] convertedBytes = new byte[utf8Encoded.remaining()];
-                utf8Encoded.get( convertedBytes, 0, utf8Encoded.remaining() );
-
-                deferredFileOutputStream.write( convertedBytes, 0, convertedBytes.length );
-            }
+            output = "null";
         }
-        else
+
+        deferredFileOutputStream.write( output.getBytes( UTF_8 ) );
+        if ( newLine )
         {
-            deferredFileOutputStream.write( buf, off, len );
+            deferredFileOutputStream.write( NL.getBytes( UTF_8 ) );
         }
     }
 
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/WrappedReportEntry.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/WrappedReportEntry.java
index efec3a6..bb4c1b1 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/WrappedReportEntry.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/WrappedReportEntry.java
@@ -28,8 +28,8 @@ import java.util.Map;
 
 import static java.util.Collections.unmodifiableMap;
 import static org.apache.maven.plugin.surefire.report.ReporterUtils.formatElapsedTime;
-import static org.apache.maven.surefire.util.internal.StringUtils.isBlank;
 import static org.apache.maven.surefire.util.internal.StringUtils.NL;
+import static org.apache.maven.surefire.util.internal.StringUtils.isBlank;
 
 /**
  * @author Kristian Rosenvold
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/runorder/StatisticsReporter.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/runorder/StatisticsReporter.java
index 2d9a175..fb5450b 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/runorder/StatisticsReporter.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/runorder/StatisticsReporter.java
@@ -42,11 +42,11 @@ public class StatisticsReporter
         this( dataFile, fromFile( dataFile ), new RunEntryStatisticsMap() );
     }
 
-    protected StatisticsReporter( File dataFile, RunEntryStatisticsMap existing, RunEntryStatisticsMap newRestuls )
+    protected StatisticsReporter( File dataFile, RunEntryStatisticsMap existing, RunEntryStatisticsMap newResults )
     {
         this.dataFile = dataFile;
         this.existing = existing;
-        this.newResults = newRestuls;
+        this.newResults = newResults;
     }
 
     public synchronized void testSetCompleted()
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 0fd275f..6b693bd 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/ForkingRunListenerTest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/ForkingRunListenerTest.java
@@ -25,6 +25,7 @@ import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.Notifiable
 import org.apache.maven.plugin.surefire.booterclient.output.ForkClient;
 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.report.CategorizedReportEntry;
 import org.apache.maven.surefire.report.ConsoleOutputReceiver;
@@ -40,11 +41,11 @@ import org.hamcrest.MatcherAssert;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.PrintStream;
-import java.nio.charset.Charset;
+import java.util.Collections;
 import java.util.List;
-import java.util.StringTokenizer;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
 
-import static org.hamcrest.Matchers.greaterThan;
 import static org.hamcrest.Matchers.is;
 
 /**
@@ -57,10 +58,6 @@ public class ForkingRunListenerTest
 
     private final PrintStream printStream, anotherPrintStream;
 
-    final int defaultChannel = 17;
-
-    final int anotherChannel = 18;
-
     public ForkingRunListenerTest()
     {
         content = new ByteArrayOutputStream();
@@ -76,20 +73,6 @@ public class ForkingRunListenerTest
         content.reset();
     }
 
-    public void testHeaderCreation()
-    {
-        final byte[] header = ForkingRunListener.createHeader( (byte) 'F', 0xCAFE );
-        String asString = new String( header );
-        assertEquals( "F,cafe," + Charset.defaultCharset().name() + ",", asString );
-    }
-
-    public void testHeaderCreationShort()
-    {
-        final byte[] header = ForkingRunListener.createHeader( (byte) 'F', 0xE );
-        String asString = new String( header );
-        assertEquals( "F,e," + Charset.defaultCharset().name() + ",", asString );
-    }
-
     public void testSetStarting()
         throws ReporterException, IOException
     {
@@ -117,19 +100,6 @@ public class ForkingRunListenerTest
         standardTestRun.assertExpected( MockReporter.TEST_STARTING, expected );
     }
 
-    public void testStringTokenizer()
-    {
-        String test = "5,11,com.abc.TestClass,testMethod,null,22,,,";
-        StringTokenizer tok = new StringTokenizer( test, "," );
-        assertEquals( "5", tok.nextToken() );
-        assertEquals( "11", tok.nextToken() );
-        assertEquals( "com.abc.TestClass", tok.nextToken() );
-        assertEquals( "testMethod", tok.nextToken() );
-        assertEquals( "null", tok.nextToken() );
-        assertEquals( "22", tok.nextToken() );
-        assertFalse( tok.hasMoreTokens() );
-    }
-
     public void testSucceded()
         throws ReporterException, IOException
     {
@@ -199,7 +169,7 @@ public class ForkingRunListenerTest
         final StandardTestRun standardTestRun = new StandardTestRun();
         ConsoleLogger directConsoleReporter = (ConsoleLogger) standardTestRun.run();
         directConsoleReporter.info( "HeyYou" );
-        standardTestRun.assertExpected( MockReporter.CONSOLE_OUTPUT, "HeyYou" );
+        standardTestRun.assertExpected( MockReporter.CONSOLE_INFO, "HeyYou" );
     }
 
     public void testConsoleOutput()
@@ -207,7 +177,7 @@ public class ForkingRunListenerTest
     {
         final StandardTestRun standardTestRun = new StandardTestRun();
         ConsoleOutputReceiver directConsoleReporter = (ConsoleOutputReceiver) standardTestRun.run();
-        directConsoleReporter.writeTestOutput( "HeyYou".getBytes(), 0, 6, true );
+        directConsoleReporter.writeTestOutput( "HeyYou", false, true );
         standardTestRun.assertExpected( MockReporter.STDOUT, "HeyYou" );
     }
 
@@ -218,16 +188,17 @@ public class ForkingRunListenerTest
         standardTestRun.run();
 
         reset();
-        createForkingRunListener( defaultChannel );
+        createForkingRunListener();
 
         TestSetMockReporterFactory providerReporterFactory = new TestSetMockReporterFactory();
         NullConsoleLogger log = new NullConsoleLogger();
         ForkClient forkStreamClient =
-                new ForkClient( providerReporterFactory, new MockNotifiableTestStream(), log, null, 1 );
+                new ForkClient( providerReporterFactory, new MockNotifiableTestStream(), log, new AtomicBoolean(), 1 );
 
-        forkStreamClient.consumeMultiLineContent( content.toString( "UTF-8" ) );
+        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=" );
 
-        MatcherAssert.assertThat( forkStreamClient.getTestVmSystemProperties().size(), is( greaterThan( 1 ) ) );
+        MatcherAssert.assertThat( forkStreamClient.getTestVmSystemProperties().size(), is( 2 ) );
     }
 
     public void testMultipleEntries()
@@ -237,7 +208,7 @@ public class ForkingRunListenerTest
         standardTestRun.run();
 
         reset();
-        RunListener forkingReporter = createForkingRunListener( defaultChannel );
+        RunListener forkingReporter = createForkingRunListener();
 
         TestSetReportEntry reportEntry = createDefaultReportEntry();
         forkingReporter.testSetStarting( reportEntry );
@@ -267,10 +238,10 @@ public class ForkingRunListenerTest
         ReportEntry expected = createDefaultReportEntry();
         final SimpleReportEntry secondExpected = createAnotherDefaultReportEntry();
 
-        new ForkingRunListener( printStream, defaultChannel, false )
+        new ForkingRunListener( new ForkedChannelEncoder( printStream ), false )
                 .testStarting( expected );
 
-        new ForkingRunListener( anotherPrintStream, anotherChannel, false )
+        new ForkingRunListener( new ForkedChannelEncoder( anotherPrintStream ), false )
                 .testSkipped( secondExpected );
 
         TestSetMockReporterFactory providerReporterFactory = new TestSetMockReporterFactory();
@@ -295,9 +266,14 @@ public class ForkingRunListenerTest
 
     // Todo: Test weird characters
 
+    private SimpleReportEntry createDefaultReportEntry( Map<String, String> sysProps )
+    {
+        return new SimpleReportEntry( "com.abc.TestClass", "testMethod", null, 22, sysProps );
+    }
+
     private SimpleReportEntry createDefaultReportEntry()
     {
-        return new SimpleReportEntry( "com.abc.TestClass", "testMethod", 22 );
+        return createDefaultReportEntry( Collections.<String, String>emptyMap() );
     }
 
     private SimpleReportEntry createAnotherDefaultReportEntry()
@@ -333,9 +309,9 @@ public class ForkingRunListenerTest
         }
     }
 
-    private RunListener createForkingRunListener( Integer testSetChannel )
+    private RunListener createForkingRunListener()
     {
-        return new ForkingRunListener( printStream, testSetChannel, false );
+        return new ForkingRunListener( new ForkedChannelEncoder( printStream ), false );
     }
 
     private class StandardTestRun
@@ -346,7 +322,7 @@ public class ForkingRunListenerTest
             throws ReporterException
         {
             reset();
-            return createForkingRunListener( defaultChannel );
+            return createForkingRunListener();
         }
 
         public void clientReceiveContent()
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/MockReporter.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/MockReporter.java
index 2c9612f..1a84a63 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/MockReporter.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/MockReporter.java
@@ -24,6 +24,7 @@ import org.apache.maven.surefire.report.ConsoleOutputReceiver;
 import org.apache.maven.surefire.report.ReportEntry;
 import org.apache.maven.surefire.report.RunListener;
 import org.apache.maven.surefire.report.TestSetReportEntry;
+import org.apache.maven.surefire.report.RunMode;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -55,7 +56,13 @@ public class MockReporter
 
     public static final String TEST_ASSUMPTION_FAIL = "TEST_ASSUMPTION_SKIPPED";
 
-    public static final String CONSOLE_OUTPUT = "CONSOLE_OUTPUT";
+    public static final String CONSOLE_INFO = "CONSOLE_INFO";
+
+    public static final String CONSOLE_WARN = "CONSOLE_WARN";
+
+    public static final String CONSOLE_DEBUG = "CONSOLE_DEBUG";
+
+    public static final String CONSOLE_ERR = "CONSOLE_ERR";
 
     public static final String STDOUT = "STDOUT";
 
@@ -125,6 +132,11 @@ public class MockReporter
     {
     }
 
+    @Override
+    public RunMode markAs(RunMode currentRunMode) {
+        return null;
+    }
+
     public void testSkippedByUser( ReportEntry report )
     {
         testSkipped( report );
@@ -167,7 +179,7 @@ public class MockReporter
     @Override
     public void debug( String message )
     {
-        events.add( CONSOLE_OUTPUT );
+        events.add( CONSOLE_DEBUG );
         data.add( message );
     }
 
@@ -180,7 +192,7 @@ public class MockReporter
     @Override
     public void info( String message )
     {
-        events.add( CONSOLE_OUTPUT );
+        events.add( CONSOLE_INFO );
         data.add( message );
     }
 
@@ -193,7 +205,7 @@ public class MockReporter
     @Override
     public void warning( String message )
     {
-        events.add( CONSOLE_OUTPUT );
+        events.add( CONSOLE_WARN );
         data.add( message );
     }
 
@@ -206,24 +218,26 @@ public class MockReporter
     @Override
     public void error( String message )
     {
-        events.add( CONSOLE_OUTPUT );
+        events.add( CONSOLE_ERR );
         data.add( message );
     }
 
     @Override
     public void error( String message, Throwable t )
     {
+        error( message );
     }
 
     @Override
     public void error( Throwable t )
     {
+        error( t.getLocalizedMessage() );
     }
 
     @Override
-    public void writeTestOutput( byte[] buf, int off, int len, boolean stdout )
+    public void writeTestOutput( String output, boolean newLine, boolean stdout )
     {
         events.add( stdout ? STDOUT : STDERR );
-        data.add( new String( buf, off, len ) );
+        data.add( newLine ? output + "\n" : output );
     }
 }
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
new file mode 100644
index 0000000..e4107da
--- /dev/null
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/output/ForkClientTest.java
@@ -0,0 +1,2020 @@
+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.booterclient.MockReporter;
+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.booter.Shutdown;
+import org.apache.maven.surefire.report.ReportEntry;
+import org.apache.maven.surefire.report.SafeThrowable;
+import org.apache.maven.surefire.report.StackTraceWriter;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Arrays.copyOfRange;
+import static org.apache.commons.codec.binary.Base64.encodeBase64String;
+import static org.apache.maven.plugin.surefire.booterclient.MockReporter.*;
+import static org.fest.assertions.Assertions.assertThat;
+import static org.fest.assertions.MapAssert.entry;
+import static org.mockito.Mockito.*;
+
+/**
+ * Test for {@link ForkClient}.
+ *
+ * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
+ * @since 3.0.0-M4
+ */
+public class ForkClientTest
+{
+    @Test
+    public void shouldNotFailOnEmptyInput1()
+    {
+        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();
+    }
+
+    @Test
+    public void shouldNotFailOnEmptyInput2()
+    {
+        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( "   " );
+        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 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();
+    }
+
+    @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();
+    }
+
+    @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
+    {
+        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( 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" );
+        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 shouldBePossibleToKill()
+    {
+        NotifiableTestStream notifiableTestStream = mock( NotifiableTestStream.class );
+
+        ForkClient client = new ForkClient( null, notifiableTestStream, null, null, 0 );
+        client.kill();
+
+        verify( notifiableTestStream, times( 1 ) )
+                .shutdown( eq( Shutdown.KILL ) );
+    }
+
+    @Test
+    public void shouldAcquireNextTest()
+            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" ) );
+        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" );
+        verify( notifiableTestStream, times( 1 ) )
+                .provideNewTest();
+        verifyNoMoreInteractions( notifiableTestStream );
+        verifyZeroInteractions( factory );
+        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 shouldNotifyWithBye()
+            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" ) );
+        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" );
+        client.kill();
+
+        verify( notifiableTestStream, times( 1 ) )
+                .acknowledgeByeEventReceived();
+        verify( notifiableTestStream, never() )
+                .shutdown( any( Shutdown.class ) );
+        verifyNoMoreInteractions( notifiableTestStream );
+        verifyZeroInteractions( factory );
+        assertThat( client.isSaidGoodBye() )
+                .isTrue();
+        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 shouldStopOnNextTest()
+            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" ) );
+        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 )
+        {
+            @Override
+            protected void stopOnNextTest()
+            {
+                super.stopOnNextTest();
+                verified[0] = true;
+            }
+        };
+        client.consumeMultiLineContent( ":maven:surefire:std:out:stop-on-next-test\n" );
+        verifyZeroInteractions( notifiableTestStream );
+        verifyZeroInteractions( factory );
+        assertThat( verified[0] )
+                .isTrue();
+        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 shouldReceiveStdOut()
+            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:std-out-stream:normal-run:UTF-8:bXNn\n" );
+        verifyZeroInteractions( notifiableTestStream );
+        verify( factory, times( 1 ) )
+                .createReporter();
+        verifyNoMoreInteractions( factory );
+        assertThat( client.getReporter() )
+                .isNotNull();
+        assertThat( receiver.getEvents() )
+                .hasSize( 1 )
+                .contains( STDOUT );
+        assertThat( receiver.getData() )
+                .hasSize( 1 )
+                .contains( "msg" );
+        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 shouldReceiveStdOutNewLine()
+            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:std-out-stream-new-line:normal-run:UTF-8:bXNn\n" );
+        verifyZeroInteractions( notifiableTestStream );
+        verify( factory, times( 1 ) )
+                .createReporter();
+        verifyNoMoreInteractions( factory );
+        assertThat( client.getReporter() )
+                .isNotNull();
+        assertThat( receiver.getEvents() )
+                .hasSize( 1 )
+                .contains( STDOUT );
+        assertThat( receiver.getData() )
+                .hasSize( 1 )
+                .contains( "msg\n" );
+        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 shouldReceiveStdErr()
+            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:std-err-stream:normal-run:UTF-8:bXNn\n" );
+        verifyZeroInteractions( notifiableTestStream );
+        verify( factory, times( 1 ) )
+                .createReporter();
+        verifyNoMoreInteractions( factory );
+        assertThat( client.getReporter() )
+                .isNotNull();
+        assertThat( receiver.getEvents() )
+                .hasSize( 1 )
+                .contains( STDERR );
+        assertThat( receiver.getData() )
+                .hasSize( 1 )
+                .contains( "msg" );
+        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 shouldReceiveStdErrNewLine()
+            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:std-err-stream-new-line:normal-run:UTF-8:bXNn\n" );
+        verifyZeroInteractions( notifiableTestStream );
+        verify( factory, times( 1 ) )
+                .createReporter();
+        verifyNoMoreInteractions( factory );
+        assertThat( client.getReporter() )
+                .isNotNull();
+        assertThat( receiver.getEvents() )
+                .hasSize( 1 )
+                .contains( STDERR );
+        assertThat( receiver.getData() )
+                .hasSize( 1 )
+                .contains( "msg\n" );
+        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 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 ) )
+                + ":-:-:-" );
+        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() )
+                .isFalse();
+        assertThat( client.getErrorInFork() )
+                .isNull();
+        assertThat( client.hadTimeout() )
+                .isFalse();
+        assertThat( client.hasTestsInProgress() )
+                .isFalse();
+        assertThat( client.testsInProgress() )
+                .isEmpty();
+        assertThat( client.getTestVmSystemProperties() )
+                .isEmpty();
+    }
+
+    @Test
+    public void shouldLogConsoleErrorWithStackTrace()
+            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 ) )
+                + ":" + 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() )
+                .isTrue();
+        assertThat( client.getErrorInFork() )
+                .isNotNull();
+        assertThat( client.getErrorInFork().getThrowable().getLocalizedMessage() )
+                .isEqualTo( "Listening for transport dt_socket at address:" );
+        assertThat( client.getErrorInFork().smartTrimmedStackTrace() )
+                .isEqualTo( "s1" );
+        assertThat( client.getErrorInFork().writeTrimmedTraceToString() )
+                .isEqualTo( "s2" );
+        assertThat( client.hadTimeout() )
+                .isFalse();
+        assertThat( client.hasTestsInProgress() )
+                .isFalse();
+        assertThat( client.testsInProgress() )
+                .isEmpty();
+        assertThat( client.getTestVmSystemProperties() )
+                .isEmpty();
+    }
+
+    @Test
+    public void shouldLogConsoleWarning()
+            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 );
+        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 ) ) );
+        verifyZeroInteractions( notifiableTestStream );
+        verify( factory, times( 1 ) )
+                .createReporter();
+        verifyNoMoreInteractions( factory );
+        assertThat( client.getReporter() )
+                .isNotNull();
+        assertThat( receiver.getEvents() )
+                .hasSize( 1 )
+                .contains( CONSOLE_WARN );
+        assertThat( receiver.getData() )
+                .hasSize( 1 )
+                .contains( "s1" );
+        assertThat( client.isSaidGoodBye() )
+                .isFalse();
+        assertThat( client.isErrorInFork() )
+                .isFalse();
+        assertThat( client.getErrorInFork() )
+                .isNull();
+        assertThat( client.hadTimeout() )
+                .isFalse();
+        assertThat( client.hasTestsInProgress() )
+                .isFalse();
+        assertThat( client.testsInProgress() )
+                .isEmpty();
+        assertThat( client.getTestVmSystemProperties() )
+                .isEmpty();
+    }
+
+    @Test
+    public void shouldLogConsoleDebug()
+            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 );
+        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 ) ) );
+        verifyZeroInteractions( notifiableTestStream );
+        verify( factory, times( 1 ) )
+                .createReporter();
+        verifyNoMoreInteractions( factory );
+        assertThat( client.getReporter() )
+                .isNotNull();
+        assertThat( receiver.getEvents() )
+                .hasSize( 1 )
+                .contains( CONSOLE_DEBUG );
+        assertThat( receiver.getData() )
+                .hasSize( 1 )
+                .contains( "s1" );
+        assertThat( client.isSaidGoodBye() )
+                .isFalse();
+        assertThat( client.isErrorInFork() )
+                .isFalse();
+        assertThat( client.getErrorInFork() )
+                .isNull();
+        assertThat( client.hadTimeout() )
+                .isFalse();
+        assertThat( client.hasTestsInProgress() )
+                .isFalse();
+        assertThat( client.testsInProgress() )
+                .isEmpty();
+        assertThat( client.getTestVmSystemProperties() )
+                .isEmpty();
+    }
+
+    @Test
+    public void shouldLogConsoleInfo()
+            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-info-log:UTF-8:"
+                + encodeBase64String( "s1".getBytes( UTF_8 ) ) );
+        verifyZeroInteractions( notifiableTestStream );
+        verify( factory, times( 1 ) )
+                .createReporter();
+        verifyNoMoreInteractions( factory );
+        assertThat( client.getReporter() )
+                .isNotNull();
+        assertThat( receiver.getEvents() )
+                .hasSize( 1 )
+                .contains( CONSOLE_INFO );
+        assertThat( receiver.getData() )
+                .hasSize( 1 )
+                .contains( "s1" );
+        assertThat( client.isSaidGoodBye() )
+                .isFalse();
+        assertThat( client.isErrorInFork() )
+                .isFalse();
+        assertThat( client.getErrorInFork() )
+                .isNull();
+        assertThat( client.hadTimeout() )
+                .isFalse();
+        assertThat( client.hasTestsInProgress() )
+                .isFalse();
+        assertThat( client.testsInProgress() )
+                .isEmpty();
+        assertThat( client.getTestVmSystemProperties() )
+                .isEmpty();
+    }
+
+    @Test
+    public void shouldSendSystemProperty()
+            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:sys-prop:normal-run:UTF-8:azE=:djE="
+                + encodeBase64String( "s1".getBytes( UTF_8 ) ) );
+        verifyZeroInteractions( notifiableTestStream );
+        verifyZeroInteractions( factory );
+        assertThat( client.getReporter() )
+                .isNotNull();
+        assertThat( receiver.getEvents() )
+                .isEmpty();
+        assertThat( receiver.getData() )
+                .isEmpty();
+        assertThat( client.isSaidGoodBye() )
+                .isFalse();
+        assertThat( client.isErrorInFork() )
+                .isFalse();
+        assertThat( client.getErrorInFork() )
+                .isNull();
+        assertThat( client.hadTimeout() )
+                .isFalse();
+        assertThat( client.hasTestsInProgress() )
+                .isFalse();
+        assertThat( client.testsInProgress() )
+                .isEmpty();
+        assertThat( client.getTestVmSystemProperties() )
+                .hasSize( 1 );
+        assertThat( client.getTestVmSystemProperties() )
+                .includes( entry( "k1", "v1" ) );
+    }
+
+    @Test
+    public void shouldSendTestsetStartingKilled()
+            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 );
+
+
+        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 );
+        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( "some test" );
+        when( reportEntry.getName() ).thenReturn( "my test" );
+        when( reportEntry.getNameWithGroup() ).thenReturn( "name with group" );
+        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
+                + ":"
+                + 102
+                + ":"
+
+                + encodedExceptionMsg
+                + ":"
+                + encodedSmartStackTrace
+                + ":"
+                + encodedTrimmedStackTrace );
+
+        client.tryToTimeout( System.currentTimeMillis() + 1000L, 1 );
+
+        verify( notifiableTestStream )
+                .shutdown( Shutdown.KILL );
+        verifyNoMoreInteractions( notifiableTestStream );
+        verify( factory ).createReporter();
+        verifyNoMoreInteractions( factory );
+        assertThat( client.getReporter() )
+                .isNotNull();
+        assertThat( receiver.getEvents() )
+                .hasSize( 1 );
+        assertThat( receiver.getEvents() )
+                .contains( SET_STARTING );
+        assertThat( receiver.getData() )
+                .hasSize( 1 );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getSourceName() )
+                .isEqualTo( "pkg.MyTest" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getName() )
+                .isEqualTo( "my test" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getElapsed() )
+                .isEqualTo( 102 );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getMessage() )
+                .isEqualTo( "some test" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getGroup() )
+                .isEqualTo( "this group" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getStackTraceWriter() )
+                .isNotNull();
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) )
+                .getStackTraceWriter().getThrowable().getLocalizedMessage() )
+                .isEqualTo( "msg" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getStackTraceWriter().smartTrimmedStackTrace() )
+                .isEqualTo( "MyTest:86 >> Error" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getStackTraceWriter().writeTraceToString() )
+                .isEqualTo( "trace line 1" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getStackTraceWriter().writeTrimmedTraceToString() )
+                .isEqualTo( "trace line 1" );
+        assertThat( client.isSaidGoodBye() )
+                .isFalse();
+        assertThat( client.isErrorInFork() )
+                .isFalse();
+        assertThat( client.getErrorInFork() )
+                .isNull();
+        assertThat( client.hadTimeout() )
+                .isTrue();
+        assertThat( client.hasTestsInProgress() )
+                .isFalse();
+        assertThat( client.testsInProgress() )
+                .isEmpty();
+        assertThat( client.getTestVmSystemProperties() )
+                .isEmpty();
+        assertThat( client.getTestVmSystemProperties() )
+                .isEmpty();
+    }
+
+    @Test
+    public void shouldSendTestsetStarting()
+            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 );
+
+
+        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 );
+        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( "some test" );
+        when( reportEntry.getName() ).thenReturn( "my test" );
+        when( reportEntry.getNameWithGroup() ).thenReturn( "name with group" );
+        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
+                + ":"
+                + 102
+                + ":"
+
+                + encodedExceptionMsg
+                + ":"
+                + encodedSmartStackTrace
+                + ":"
+                + encodedTrimmedStackTrace );
+
+        client.tryToTimeout( System.currentTimeMillis(), 1 );
+
+        verifyZeroInteractions( notifiableTestStream );
+        verify( factory ).createReporter();
+        verifyNoMoreInteractions( factory );
+        assertThat( client.getReporter() )
+                .isNotNull();
+        assertThat( receiver.getEvents() )
+                .hasSize( 1 );
+        assertThat( receiver.getEvents() )
+                .contains( SET_STARTING );
+        assertThat( receiver.getData() )
+                .hasSize( 1 );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getSourceName() )
+                .isEqualTo( "pkg.MyTest" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getName() )
+                .isEqualTo( "my test" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getElapsed() )
+                .isEqualTo( 102 );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getMessage() )
+                .isEqualTo( "some test" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getGroup() )
+                .isEqualTo( "this group" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getStackTraceWriter() )
+                .isNotNull();
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) )
+                .getStackTraceWriter().getThrowable().getLocalizedMessage() )
+                .isEqualTo( "msg" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getStackTraceWriter().smartTrimmedStackTrace() )
+                .isEqualTo( "MyTest:86 >> Error" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getStackTraceWriter().writeTraceToString() )
+                .isEqualTo( "trace line 1" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getStackTraceWriter().writeTrimmedTraceToString() )
+                .isEqualTo( "trace line 1" );
+        assertThat( client.isSaidGoodBye() )
+                .isFalse();
+        assertThat( client.isErrorInFork() )
+                .isFalse();
+        assertThat( client.getErrorInFork() )
+                .isNull();
+        assertThat( client.hadTimeout() )
+                .isFalse();
+        assertThat( client.hasTestsInProgress() )
+                .isFalse();
+        assertThat( client.testsInProgress() )
+                .isEmpty();
+        assertThat( client.getTestVmSystemProperties() )
+                .isEmpty();
+        assertThat( client.getTestVmSystemProperties() )
+                .isEmpty();
+        assertThat( client.getDefaultReporterFactory() )
+                .isSameAs( factory );
+    }
+
+    @Test
+    public void shouldSendTestsetCompleted()
+            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 );
+
+
+        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 );
+        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( "some test" );
+        when( reportEntry.getName() ).thenReturn( "my test" );
+        when( reportEntry.getNameWithGroup() ).thenReturn( "name with group" );
+        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
+                + ":"
+                + 102
+                + ":"
+
+                + encodedExceptionMsg
+                + ":"
+                + encodedSmartStackTrace
+                + ":"
+                + encodedTrimmedStackTrace );
+
+        verifyZeroInteractions( notifiableTestStream );
+        verify( factory ).createReporter();
+        verifyNoMoreInteractions( factory );
+        assertThat( client.getReporter() )
+                .isNotNull();
+        assertThat( receiver.getEvents() )
+                .hasSize( 1 );
+        assertThat( receiver.getEvents() )
+                .contains( SET_COMPLETED );
+        assertThat( receiver.getData() )
+                .hasSize( 1 );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getSourceName() )
+                .isEqualTo( "pkg.MyTest" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getName() )
+                .isEqualTo( "my test" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getElapsed() )
+                .isEqualTo( 102 );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getMessage() )
+                .isEqualTo( "some test" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getGroup() )
+                .isEqualTo( "this group" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getStackTraceWriter() )
+                .isNotNull();
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) )
+                .getStackTraceWriter().getThrowable().getLocalizedMessage() )
+                .isEqualTo( "msg" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getStackTraceWriter().smartTrimmedStackTrace() )
+                .isEqualTo( "MyTest:86 >> Error" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getStackTraceWriter().writeTraceToString() )
+                .isEqualTo( "trace line 1" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getStackTraceWriter().writeTrimmedTraceToString() )
+                .isEqualTo( "trace line 1" );
+        assertThat( client.isSaidGoodBye() )
+                .isFalse();
+        assertThat( client.isErrorInFork() )
+                .isFalse();
+        assertThat( client.getErrorInFork() )
+                .isNull();
+        assertThat( client.hadTimeout() )
+                .isFalse();
+        assertThat( client.hasTestsInProgress() )
+                .isFalse();
+        assertThat( client.testsInProgress() )
+                .isEmpty();
+        assertThat( client.getTestVmSystemProperties() )
+                .isEmpty();
+        assertThat( client.getTestVmSystemProperties() )
+                .isEmpty();
+        assertThat( client.getDefaultReporterFactory() )
+                .isSameAs( factory );
+    }
+
+    @Test
+    public void shouldSendTestStarting()
+            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 );
+
+
+        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 );
+        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( "some test" );
+        when( reportEntry.getName() ).thenReturn( "my test" );
+        when( reportEntry.getNameWithGroup() ).thenReturn( "name with group" );
+        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
+                + ":"
+                + 102
+                + ":"
+
+                + encodedExceptionMsg
+                + ":"
+                + encodedSmartStackTrace
+                + ":"
+                + encodedTrimmedStackTrace );
+
+        verifyZeroInteractions( notifiableTestStream );
+        verify( factory ).createReporter();
+        verifyNoMoreInteractions( factory );
+        assertThat( client.hasTestsInProgress() )
+                .isTrue();
+        assertThat( client.testsInProgress() )
+                .hasSize( 1 )
+                .contains( "pkg.MyTest" );
+        assertThat( client.getReporter() )
+                .isNotNull();
+        assertThat( receiver.getEvents() )
+                .hasSize( 1 );
+        assertThat( receiver.getEvents() )
+                .contains( TEST_STARTING );
+        assertThat( receiver.getData() )
+                .hasSize( 1 );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getSourceName() )
+                .isEqualTo( "pkg.MyTest" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getName() )
+                .isEqualTo( "my test" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getElapsed() )
+                .isEqualTo( 102 );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getMessage() )
+                .isEqualTo( "some test" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getGroup() )
+                .isEqualTo( "this group" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getStackTraceWriter() )
+                .isNotNull();
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) )
+                .getStackTraceWriter().getThrowable().getLocalizedMessage() )
+                .isEqualTo( "msg" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getStackTraceWriter().smartTrimmedStackTrace() )
+                .isEqualTo( "MyTest:86 >> Error" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getStackTraceWriter().writeTraceToString() )
+                .isEqualTo( "trace line 1" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getStackTraceWriter().writeTrimmedTraceToString() )
+                .isEqualTo( "trace line 1" );
+        assertThat( client.isSaidGoodBye() )
+                .isFalse();
+        assertThat( client.isErrorInFork() )
+                .isFalse();
+        assertThat( client.getErrorInFork() )
+                .isNull();
+        assertThat( client.hadTimeout() )
+                .isFalse();
+        assertThat( client.getTestVmSystemProperties() )
+                .isEmpty();
+        assertThat( client.getTestVmSystemProperties() )
+                .isEmpty();
+        assertThat( client.getDefaultReporterFactory() )
+                .isSameAs( factory );
+    }
+
+    @Test
+    public void shouldSendTestSucceeded()
+            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 );
+
+
+        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 );
+        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( "some test" );
+        when( reportEntry.getName() ).thenReturn( "my test" );
+        when( reportEntry.getNameWithGroup() ).thenReturn( "name with group" );
+        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
+                + ":-:-:-:-:-:-:-" );
+
+        assertThat( client.testsInProgress() )
+                .hasSize( 1 )
+                .contains( "pkg.MyTest" );
+
+        client.consumeMultiLineContent( ":maven:surefire:std:out:test-succeeded:normal-run:UTF-8:"
+                + encodedSourceName
+                + ":"
+                + encodedName
+                + ":"
+                + encodedGroup
+                + ":"
+                + encodedMessage
+                + ":"
+                + 102
+                + ":"
+
+                + encodedExceptionMsg
+                + ":"
+                + encodedSmartStackTrace
+                + ":"
+                + encodedStackTrace );
+
+        verifyZeroInteractions( notifiableTestStream );
+        verify( factory ).createReporter();
+        verifyNoMoreInteractions( factory );
+        assertThat( client.getReporter() )
+                .isNotNull();
+        assertThat( receiver.getEvents() )
+                .hasSize( 2 );
+        assertThat( receiver.getEvents() )
+                .contains( TEST_STARTING, TEST_SUCCEEDED );
+        assertThat( receiver.getData() )
+                .hasSize( 2 );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getSourceName() )
+                .isEqualTo( "pkg.MyTest" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getName() )
+                .isNull();
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getSourceName() )
+                .isEqualTo( "pkg.MyTest" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getName() )
+                .isEqualTo( "my test" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getElapsed() )
+                .isEqualTo( 102 );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getMessage() )
+                .isEqualTo( "some test" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getGroup() )
+                .isEqualTo( "this group" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter() )
+                .isNotNull();
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) )
+                .getStackTraceWriter().getThrowable().getLocalizedMessage() )
+                .isEqualTo( "msg" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter().smartTrimmedStackTrace() )
+                .isEqualTo( "MyTest:86 >> Error" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter().writeTraceToString() )
+                .isEqualTo( "trace line 1\ntrace line 2" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter().writeTrimmedTraceToString() )
+                .isEqualTo( "trace line 1\ntrace line 2" );
+        assertThat( client.isSaidGoodBye() )
+                .isFalse();
+        assertThat( client.isErrorInFork() )
+                .isFalse();
+        assertThat( client.getErrorInFork() )
+                .isNull();
+        assertThat( client.hadTimeout() )
+                .isFalse();
+        assertThat( client.hasTestsInProgress() )
+                .isFalse();
+        assertThat( client.testsInProgress() )
+                .isEmpty();
+        assertThat( client.getTestVmSystemProperties() )
+                .isEmpty();
+        assertThat( client.getTestVmSystemProperties() )
+                .isEmpty();
+        assertThat( client.getDefaultReporterFactory() )
+                .isSameAs( factory );
+    }
+
+    @Test
+    public void shouldSendTestFailed()
+            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 );
+
+
+        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 );
+        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( "some test" );
+        when( reportEntry.getName() ).thenReturn( "my test" );
+        when( reportEntry.getNameWithGroup() ).thenReturn( "name with group" );
+        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
+                + ":-:-:-:-:-:-:-" );
+
+        assertThat( client.testsInProgress() )
+                .hasSize( 1 )
+                .contains( "pkg.MyTest" );
+
+        client.consumeMultiLineContent( ":maven:surefire:std:out:test-failed:normal-run:UTF-8:"
+                + encodedSourceName
+                + ":"
+                + encodedName
+                + ":"
+                + encodedGroup
+                + ":"
+                + encodedMessage
+                + ":"
+                + 102
+                + ":"
+
+                + encodedExceptionMsg
+                + ":"
+                + encodedSmartStackTrace
+                + ":"
+                + encodedTrimmedStackTrace );
+
+        verifyZeroInteractions( notifiableTestStream );
+        verify( factory ).createReporter();
+        verifyNoMoreInteractions( factory );
+        assertThat( client.getReporter() )
+                .isNotNull();
+        assertThat( receiver.getEvents() )
+                .hasSize( 2 );
+        assertThat( receiver.getEvents() )
+                .contains( TEST_STARTING, TEST_FAILED );
+        assertThat( receiver.getData() )
+                .hasSize( 2 );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getSourceName() )
+                .isEqualTo( "pkg.MyTest" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getName() )
+                .isNull();
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getSourceName() )
+                .isEqualTo( "pkg.MyTest" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getName() )
+                .isEqualTo( "my test" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getElapsed() )
+                .isEqualTo( 102 );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getMessage() )
+                .isEqualTo( "some test" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getGroup() )
+                .isEqualTo( "this group" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter() )
+                .isNotNull();
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) )
+                .getStackTraceWriter().getThrowable().getLocalizedMessage() )
+                .isEqualTo( "msg" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter().smartTrimmedStackTrace() )
+                .isEqualTo( "MyTest:86 >> Error" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter().writeTraceToString() )
+                .isEqualTo( "trace line 1" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter().writeTrimmedTraceToString() )
+                .isEqualTo( "trace line 1" );
+        assertThat( client.isSaidGoodBye() )
+                .isFalse();
+        assertThat( client.isErrorInFork() )
+                .isFalse();
+        assertThat( client.getErrorInFork() )
+                .isNull();
+        assertThat( client.hadTimeout() )
+                .isFalse();
+        assertThat( client.hasTestsInProgress() )
+                .isFalse();
+        assertThat( client.testsInProgress() )
+                .isEmpty();
+        assertThat( client.getTestVmSystemProperties() )
+                .isEmpty();
+        assertThat( client.getTestVmSystemProperties() )
+                .isEmpty();
+        assertThat( client.getDefaultReporterFactory() )
+                .isSameAs( factory );
+    }
+
+    @Test
+    public void shouldSendTestSkipped()
+            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 );
+
+
+        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 );
+        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( "some test" );
+        when( reportEntry.getName() ).thenReturn( "my test" );
+        when( reportEntry.getNameWithGroup() ).thenReturn( "name with group" );
+        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
+                + ":-:-:-:-:-:-:-" );
+
+        assertThat( client.testsInProgress() )
+                .hasSize( 1 )
+                .contains( "pkg.MyTest" );
+
+        client.consumeMultiLineContent( ":maven:surefire:std:out:test-skipped:normal-run:UTF-8:"
+                + encodedSourceName
+                + ":"
+                + encodedName
+                + ":"
+                + encodedGroup
+                + ":"
+                + encodedMessage
+                + ":"
+                + 102
+                + ":"
+
+                + encodedExceptionMsg
+                + ":"
+                + encodedSmartStackTrace
+                + ":"
+                + encodedStackTrace );
+
+        verifyZeroInteractions( notifiableTestStream );
+        verify( factory ).createReporter();
+        verifyNoMoreInteractions( factory );
+        assertThat( client.getReporter() )
+                .isNotNull();
+        assertThat( receiver.getEvents() )
+                .hasSize( 2 );
+        assertThat( receiver.getEvents() )
+                .contains( TEST_STARTING, TEST_SKIPPED );
+        assertThat( receiver.getData() )
+                .hasSize( 2 );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getSourceName() )
+                .isEqualTo( "pkg.MyTest" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getName() )
+                .isNull();
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getSourceName() )
+                .isEqualTo( "pkg.MyTest" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getName() )
+                .isEqualTo( "my test" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getElapsed() )
+                .isEqualTo( 102 );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getMessage() )
+                .isEqualTo( "some test" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getGroup() )
+                .isEqualTo( "this group" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter() )
+                .isNotNull();
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) )
+                .getStackTraceWriter().getThrowable().getLocalizedMessage() )
+                .isEqualTo( "msg" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter().smartTrimmedStackTrace() )
+                .isEqualTo( "MyTest:86 >> Error" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter().writeTraceToString() )
+                .isEqualTo( "trace line 1\ntrace line 2" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter().writeTrimmedTraceToString() )
+                .isEqualTo( "trace line 1\ntrace line 2" );
+        assertThat( client.isSaidGoodBye() )
+                .isFalse();
+        assertThat( client.isErrorInFork() )
+                .isFalse();
+        assertThat( client.getErrorInFork() )
+                .isNull();
+        assertThat( client.hadTimeout() )
+                .isFalse();
+        assertThat( client.hasTestsInProgress() )
+                .isFalse();
+        assertThat( client.testsInProgress() )
+                .isEmpty();
+        assertThat( client.getTestVmSystemProperties() )
+                .isEmpty();
+        assertThat( client.getTestVmSystemProperties() )
+                .isEmpty();
+        assertThat( client.getDefaultReporterFactory() )
+                .isSameAs( factory );
+    }
+
+    @Test
+    public void shouldSendTestError()
+            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 );
+
+
+        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 );
+        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( "some test" );
+        when( reportEntry.getName() ).thenReturn( "my test" );
+        when( reportEntry.getNameWithGroup() ).thenReturn( "name with group" );
+        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
+                + ":-:-:-:-:-:-:-" );
+
+        assertThat( client.testsInProgress() )
+                .hasSize( 1 )
+                .contains( "pkg.MyTest" );
+
+        client.consumeMultiLineContent( ":maven:surefire:std:out:test-error:normal-run:UTF-8:"
+                + encodedSourceName
+                + ":"
+                + encodedName
+                + ":"
+                + encodedGroup
+                + ":"
+                + encodedMessage
+                + ":"
+                + 102
+                + ":"
+
+                + encodedExceptionMsg
+                + ":"
+                + encodedSmartStackTrace
+                + ":"
+                + encodedTrimmedStackTrace );
+
+        verifyZeroInteractions( notifiableTestStream );
+        verify( factory ).createReporter();
+        verifyNoMoreInteractions( factory );
+        assertThat( client.getReporter() )
+                .isNotNull();
+        assertThat( receiver.getEvents() )
+                .hasSize( 2 );
+        assertThat( receiver.getEvents() )
+                .contains( TEST_STARTING, TEST_ERROR );
+        assertThat( receiver.getData() )
+                .hasSize( 2 );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getSourceName() )
+                .isEqualTo( "pkg.MyTest" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getName() )
+                .isNull();
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getSourceName() )
+                .isEqualTo( "pkg.MyTest" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getName() )
+                .isEqualTo( "my test" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getElapsed() )
+                .isEqualTo( 102 );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getMessage() )
+                .isEqualTo( "some test" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getGroup() )
+                .isEqualTo( "this group" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter() )
+                .isNotNull();
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) )
+                .getStackTraceWriter().getThrowable().getLocalizedMessage() )
+                .isEqualTo( "msg" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter().smartTrimmedStackTrace() )
+                .isEqualTo( "MyTest:86 >> Error" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter().writeTraceToString() )
+                .isEqualTo( "trace line 1" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter().writeTrimmedTraceToString() )
+                .isEqualTo( "trace line 1" );
+        assertThat( client.isSaidGoodBye() )
+                .isFalse();
+        assertThat( client.isErrorInFork() )
+                .isFalse();
+        assertThat( client.getErrorInFork() )
+                .isNull();
+        assertThat( client.hadTimeout() )
+                .isFalse();
+        assertThat( client.hasTestsInProgress() )
+                .isFalse();
+        assertThat( client.testsInProgress() )
+                .isEmpty();
+        assertThat( client.getTestVmSystemProperties() )
+                .isEmpty();
+        assertThat( client.getTestVmSystemProperties() )
+                .isEmpty();
+        assertThat( client.getDefaultReporterFactory() )
+                .isSameAs( factory );
+    }
+
+    @Test
+    public void shouldSendTestAssumptionFailure()
+            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 );
+
+
+        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 );
+        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( "some test" );
+        when( reportEntry.getName() ).thenReturn( "my test" );
+        when( reportEntry.getNameWithGroup() ).thenReturn( "name with group" );
+        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
+                + ":-:-:-:-:-:-:-" );
+
+        assertThat( client.testsInProgress() )
+                .hasSize( 1 )
+                .contains( "pkg.MyTest" );
+
+        client.consumeMultiLineContent( ":maven:surefire:std:out:test-assumption-failure:normal-run:UTF-8:"
+                + encodedSourceName
+                + ":"
+                + encodedName
+                + ":"
+                + encodedGroup
+                + ":"
+                + encodedMessage
+                + ":"
+                + 102
+                + ":"
+
+                + encodedExceptionMsg
+                + ":"
+                + encodedSmartStackTrace
+                + ":"
+                + encodedStackTrace );
+
+        verifyZeroInteractions( notifiableTestStream );
+        verify( factory ).createReporter();
+        verifyNoMoreInteractions( factory );
+        assertThat( client.getReporter() )
+                .isNotNull();
+        assertThat( receiver.getEvents() )
+                .hasSize( 2 );
+        assertThat( receiver.getEvents() )
+                .contains( TEST_STARTING, TEST_ASSUMPTION_FAIL );
+        assertThat( receiver.getData() )
+                .hasSize( 2 );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getSourceName() )
+                .isEqualTo( "pkg.MyTest" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 0 ) ).getName() )
+                .isNull();
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getSourceName() )
+                .isEqualTo( "pkg.MyTest" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getName() )
+                .isEqualTo( "my test" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getElapsed() )
+                .isEqualTo( 102 );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getMessage() )
+                .isEqualTo( "some test" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getGroup() )
+                .isEqualTo( "this group" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter() )
+                .isNotNull();
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) )
+                .getStackTraceWriter().getThrowable().getLocalizedMessage() )
+                .isEqualTo( "msg" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter().smartTrimmedStackTrace() )
+                .isEqualTo( "MyTest:86 >> Error" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter().writeTraceToString() )
+                .isEqualTo( "trace line 1\ntrace line 2" );
+        assertThat( ( (ReportEntry) receiver.getData().get( 1 ) ).getStackTraceWriter().writeTrimmedTraceToString() )
+                .isEqualTo( "trace line 1\ntrace line 2" );
+        assertThat( client.isSaidGoodBye() )
+                .isFalse();
+        assertThat( client.isErrorInFork() )
+                .isFalse();
+        assertThat( client.getErrorInFork() )
+                .isNull();
+        assertThat( client.hadTimeout() )
+                .isFalse();
+        assertThat( client.hasTestsInProgress() )
+                .isFalse();
+        assertThat( client.testsInProgress() )
+                .isEmpty();
+        assertThat( client.getTestVmSystemProperties() )
+                .isEmpty();
+        assertThat( client.getTestVmSystemProperties() )
+                .isEmpty();
+        assertThat( client.getDefaultReporterFactory() )
+                .isSameAs( factory );
+    }
+
+    private static byte[] toArray( ByteBuffer buffer )
+    {
+        return copyOfRange( buffer.array(), buffer.arrayOffset(), buffer.arrayOffset() + buffer.remaining() );
+    }
+
+}
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
new file mode 100644
index 0000000..894ec2d
--- /dev/null
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedChannelDecoderTest.java
@@ -0,0 +1,834 @@
+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
+        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 );
+            assertThat( reportEntry ).isNull();
+
+            reportEntry = toReportEntry( UTF_8, "", "", "", "", "-", "", "", "" );
+            assertThat( reportEntry ).isNotNull();
+            assertThat( reportEntry.getStackTraceWriter() ).isNull();
+            assertThat( reportEntry.getSourceName() ).isEmpty();
+            assertThat( reportEntry.getName() ).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
+        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.getNameWithGroup() ).thenReturn( "name with group" );
+            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() ) ) );
+
+            ReportEntry decodedReportEntry = toReportEntry( UTF_8, encodedSourceName, encodedName, encodedGroup,
+                                                                  encodedMessage, "-", null, null, null
+            );
+
+            assertThat( decodedReportEntry ).isNotNull();
+            assertThat( decodedReportEntry.getSourceName() ).isEqualTo( reportEntry.getSourceName() );
+            assertThat( decodedReportEntry.getName() ).isEqualTo( reportEntry.getName() );
+            assertThat( decodedReportEntry.getGroup() ).isEqualTo( reportEntry.getGroup() );
+            assertThat( decodedReportEntry.getMessage() ).isEqualTo( reportEntry.getMessage() );
+            assertThat( decodedReportEntry.getStackTraceWriter() ).isNull();
+
+            decodedReportEntry = toReportEntry( UTF_8, encodedSourceName, encodedName, encodedGroup, encodedMessage,
+                    "-", encodedExceptionMsg, encodedSmartStackTrace, null
+            );
+
+            assertThat( decodedReportEntry ).isNotNull();
+            assertThat( decodedReportEntry.getSourceName() ).isEqualTo( reportEntry.getSourceName() );
+            assertThat( decodedReportEntry.getName() ).isEqualTo( reportEntry.getName() );
+            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, encodedName, encodedGroup, encodedMessage,
+                                                      "1003", encodedExceptionMsg, encodedSmartStackTrace, null
+            );
+
+            assertThat( decodedReportEntry ).isNotNull();
+            assertThat( decodedReportEntry.getSourceName() ).isEqualTo( reportEntry.getSourceName() );
+            assertThat( decodedReportEntry.getName() ).isEqualTo( reportEntry.getName() );
+            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, encodedName, encodedGroup, encodedMessage,
+                                                      "1003", encodedExceptionMsg, encodedSmartStackTrace,
+                                                      encodedStackTrace
+            );
+
+            assertThat( decodedReportEntry ).isNotNull();
+            assertThat( decodedReportEntry.getSourceName() ).isEqualTo( reportEntry.getSourceName() );
+            assertThat( decodedReportEntry.getName() ).isEqualTo( reportEntry.getName() );
+            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, encodedName, encodedGroup, encodedMessage,
+                                                      "1003", encodedExceptionMsg, encodedSmartStackTrace,
+                                                      encodedTrimmedStackTrace
+            );
+
+            assertThat( decodedReportEntry ).isNotNull();
+            assertThat( decodedReportEntry.getSourceName() ).isEqualTo( reportEntry.getSourceName() );
+            assertThat( decodedReportEntry.getName() ).isEqualTo( reportEntry.getName() );
+            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 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" )
+        public static String[][] operations = { { "testSetStarting", "setTestSetStartingListener" },
+                                                { "testSetCompleted", "setTestSetCompletedListener" },
+                                                { "testStarting", "setTestStartingListener" },
+                                                { "testSucceeded", "setTestSucceededListener" },
+                                                { "testFailed", "setTestFailedListener" },
+                                                { "testSkipped", "setTestSkippedListener" },
+                                                { "testError", "setTestErrorListener" },
+                                                { "testAssumptionFailure", "setTestAssumptionFailureListener" }
+        };
+
+        @DataPoints( value = "reportedMessage" )
+        public static String[] reportedMessage = { null, "skipped test" };
+
+        @DataPoints( value = "elapsed" )
+        public static Integer[] elapsed = { null, 102 };
+
+        @DataPoints( value = "trim" )
+        public static boolean[] trim = { false, true };
+
+        @DataPoints( value = "msg" )
+        public static boolean[] msg = { false, true };
+
+        @DataPoints( value = "smart" )
+        public static boolean[] smart = { false, true };
+
+        @DataPoints( value = "trace" )
+        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.getNameWithGroup() ).thenReturn( "name with group" );
+            when( reportEntry.getSourceName() ).thenReturn( "pkg.MyTest" );
+            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.getName() ).isEqualTo( this.reportEntry.getName() );
+            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/plugin/surefire/report/StatelessXmlReporterTest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/report/StatelessXmlReporterTest.java
index 315b75f..bebaed3 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/report/StatelessXmlReporterTest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/report/StatelessXmlReporterTest.java
@@ -123,14 +123,11 @@ public class StatelessXmlReporterTest
             stdOutPrefix = "st]]>d-out";
         }
 
-        byte[] stdOutBytes = (stdOutPrefix + "<null>!\u0020\u0000\u001F").getBytes();
-        stdOut.write( stdOutBytes, 0, stdOutBytes.length );
+        stdOut.write( stdOutPrefix + "<null>!\u0020\u0000\u001F", false );
 
         Utf8RecodingDeferredFileOutputStream stdErr = new Utf8RecodingDeferredFileOutputStream( "fds" );
 
-
-        byte[] stdErrBytes = (stdErrPrefix + "?&-&amp;&#163;\u0020\u0000\u001F").getBytes();
-        stdErr.write( stdErrBytes, 0, stdErrBytes.length );
+        stdErr.write( stdErrPrefix + "?&-&amp;&#163;\u0020\u0000\u001F", false );
         WrappedReportEntry t2 =
                 new WrappedReportEntry( new SimpleReportEntry( getClass().getName(), TEST_TWO, stackTraceWriter, 13 ),
                         ReportEntryType.ERROR, 13, stdOut, stdErr );
@@ -284,7 +281,7 @@ public class StatelessXmlReporterTest
             throws IOException
     {
         Utf8RecodingDeferredFileOutputStream stdOut = new Utf8RecodingDeferredFileOutputStream( "fds2" );
-        stdOut.write( content.getBytes(), 0, content.length() );
+        stdOut.write( content, false );
         return stdOut;
     }
 }
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 9c6037b..c6177cd 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
@@ -38,6 +38,8 @@ import org.apache.maven.plugin.surefire.booterclient.JarManifestForkConfiguratio
 import org.apache.maven.plugin.surefire.booterclient.ModularClasspathForkConfigurationTest;
 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.report.DefaultReporterFactoryTest;
 import org.apache.maven.plugin.surefire.report.StatelessXmlReporterTest;
 import org.apache.maven.plugin.surefire.report.WrappedReportEntryTest;
@@ -91,6 +93,8 @@ public class JUnit4SuiteTest extends TestCase
         suite.addTest( new JUnit4TestAdapter( AbstractSurefireMojoJava7PlusTest.class ) );
         suite.addTest( new JUnit4TestAdapter( ScannerUtilTest.class ) );
         suite.addTest( new JUnit4TestAdapter( MojoMocklessTest.class ) );
+        suite.addTest( new JUnit4TestAdapter( ForkClientTest.class ) );
+        suite.addTest( new JUnit4TestAdapter( ForkedChannelDecoderTest.class ) );
         return suite;
     }
 }
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/surefire/report/ConsoleOutputFileReporterTest.java b/maven-surefire-common/src/test/java/org/apache/maven/surefire/report/ConsoleOutputFileReporterTest.java
index 45b264b..ee086f5 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/surefire/report/ConsoleOutputFileReporterTest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/surefire/report/ConsoleOutputFileReporterTest.java
@@ -48,7 +48,7 @@ public class ConsoleOutputFileReporterTest
         ReportEntry reportEntry = new SimpleReportEntry( getClass().getName(), getClass().getName() );
         ConsoleOutputFileReporter reporter = new ConsoleOutputFileReporter( reportDir, null, null );
         reporter.testSetStarting( reportEntry );
-        reporter.writeTestOutput( "some text".getBytes( US_ASCII ), 0, 5, true );
+        reporter.writeTestOutput( "some ", false, true );
         reporter.testSetCompleted( reportEntry );
         reporter.close();
 
@@ -76,7 +76,7 @@ public class ConsoleOutputFileReporterTest
         ReportEntry reportEntry = new SimpleReportEntry( getClass().getName(), getClass().getName() );
         ConsoleOutputFileReporter reporter = new ConsoleOutputFileReporter( reportDir, suffixText, null );
         reporter.testSetStarting( reportEntry );
-        reporter.writeTestOutput( "some text".getBytes( US_ASCII ), 0, 5, true );
+        reporter.writeTestOutput( "some ", false, true );
         reporter.testSetCompleted( reportEntry );
         reporter.close();
 
@@ -98,7 +98,7 @@ public class ConsoleOutputFileReporterTest
         //noinspection ResultOfMethodCallIgnored
         reportDir.mkdirs();
         ConsoleOutputFileReporter reporter = new ConsoleOutputFileReporter( reportDir, null, null );
-        reporter.writeTestOutput( "some text".getBytes( US_ASCII ), 0, 5, true );
+        reporter.writeTestOutput( "some text", false, true );
         reporter.testSetCompleted( new SimpleReportEntry( getClass().getName(), getClass().getName() ) );
         reporter.close();
 
@@ -129,8 +129,7 @@ public class ConsoleOutputFileReporterTest
                 @Override
                 public Void call()
                 {
-                    byte[] stream = "some text\n".getBytes( US_ASCII );
-                    reporter.writeTestOutput( stream, 0, stream.length, true );
+                    reporter.writeTestOutput( "some text\n", false, true );
                     return null;
                 }
             } );
diff --git a/pom.xml b/pom.xml
index d40d5a5..e30a333 100644
--- a/pom.xml
+++ b/pom.xml
@@ -218,6 +218,12 @@
         </exclusions>
       </dependency>
       <dependency>
+        <!-- version 1.11 used in maven-artifact-transfer:0.11.0 -->
+        <groupId>commons-codec</groupId>
+        <artifactId>commons-codec</artifactId>
+        <version>1.11</version>
+      </dependency>
+      <dependency>
         <groupId>org.apache.maven.shared</groupId>
         <artifactId>maven-artifact-transfer</artifactId>
         <version>0.11.0</version>
diff --git a/surefire-api/pom.xml b/surefire-api/pom.xml
index eee3879..c42041b 100644
--- a/surefire-api/pom.xml
+++ b/surefire-api/pom.xml
@@ -42,10 +42,19 @@
       <artifactId>maven-shared-utils</artifactId>
     </dependency>
     <dependency>
+      <groupId>commons-codec</groupId>
+      <artifactId>commons-codec</artifactId>
+    </dependency>
+    <dependency>
       <groupId>com.google.code.findbugs</groupId>
       <artifactId>jsr305</artifactId>
       <scope>provided</scope>
     </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <build>
@@ -78,6 +87,7 @@
               <artifactSet>
                 <includes>
                   <include>org.apache.maven.shared:maven-shared-utils</include>
+                  <include>commons-codec:commons-codec</include>
                 </includes>
               </artifactSet>
               <relocations>
@@ -85,6 +95,10 @@
                   <pattern>org.apache.maven.shared.utils</pattern>
                   <shadedPattern>org.apache.maven.surefire.shade.api.org.apache.maven.shared.utils</shadedPattern>
                 </relocation>
+                <relocation>
+                  <pattern>org.apache.commons.codec</pattern>
+                  <shadedPattern>org.apache.maven.surefire.shade.api.org.apache.commons.codec</shadedPattern>
+                </relocation>
               </relocations>
             </configuration>
           </execution>
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/booter/BaseProviderFactory.java b/surefire-api/src/main/java/org/apache/maven/surefire/booter/BaseProviderFactory.java
index 2b329ee..ec05580 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/booter/BaseProviderFactory.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/booter/BaseProviderFactory.java
@@ -36,7 +36,6 @@ import org.apache.maven.surefire.util.DirectoryScanner;
 import org.apache.maven.surefire.util.RunOrderCalculator;
 import org.apache.maven.surefire.util.ScanResult;
 
-import java.io.PrintStream;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -51,12 +50,12 @@ public class BaseProviderFactory
     ProviderPropertiesAware, ProviderParameters, TestArtifactInfoAware, RunOrderParametersAware, MainCliOptionsAware,
     FailFastAware, ShutdownAware
 {
-    private static final int ROOT_CHANNEL = 0;
-
     private final ReporterFactory reporterFactory;
 
     private final boolean insideFork;
 
+    private ForkedChannelEncoder forkedChannelEncoder;
+
     private List<CommandLineOption> mainCliOptions = emptyList();
 
     private Map<String, String> providerProperties;
@@ -142,9 +141,8 @@ public class BaseProviderFactory
     @Override
     public ConsoleStream getConsoleLogger()
     {
-        boolean trim = reporterConfiguration.isTrimStackTrace();
-        PrintStream out = reporterConfiguration.getOriginalSystemOut();
-        return insideFork ? new ForkingRunListener( out, ROOT_CHANNEL, trim ) : new DefaultDirectConsoleReporter( out );
+        return insideFork ? new ForkingRunListener( forkedChannelEncoder, reporterConfiguration.isTrimStackTrace() )
+                       : new DefaultDirectConsoleReporter( reporterConfiguration.getOriginalSystemOut() );
     }
 
     @Override
@@ -259,4 +257,15 @@ public class BaseProviderFactory
     {
         this.systemExitTimeout = systemExitTimeout;
     }
+
+    @Override
+    public ForkedChannelEncoder getForkedChannelEncoder()
+    {
+        return forkedChannelEncoder;
+    }
+
+    public void setForkedChannelEncoder( ForkedChannelEncoder forkedChannelEncoder )
+    {
+        this.forkedChannelEncoder = forkedChannelEncoder;
+    }
 }
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/booter/CommandReader.java b/surefire-api/src/main/java/org/apache/maven/surefire/booter/CommandReader.java
index be39ba7..b4e303e 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/booter/CommandReader.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/booter/CommandReader.java
@@ -26,7 +26,6 @@ import org.apache.maven.surefire.testset.TestSetFailedException;
 import java.io.DataInputStream;
 import java.io.EOFException;
 import java.io.IOException;
-import java.io.PrintStream;
 import java.util.Iterator;
 import java.util.NoSuchElementException;
 import java.util.Queue;
@@ -42,7 +41,6 @@ import static java.lang.Thread.State.RUNNABLE;
 import static java.lang.Thread.State.TERMINATED;
 import static java.lang.StrictMath.max;
 import static org.apache.maven.surefire.booter.Command.toShutdown;
-import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_NEXT_TEST;
 import static org.apache.maven.surefire.booter.MasterProcessCommand.BYE_ACK;
 import static org.apache.maven.surefire.booter.MasterProcessCommand.NOOP;
 import static org.apache.maven.surefire.booter.MasterProcessCommand.RUN_CLASS;
@@ -51,7 +49,6 @@ import static org.apache.maven.surefire.booter.MasterProcessCommand.SKIP_SINCE_N
 import static org.apache.maven.surefire.booter.MasterProcessCommand.TEST_SET_FINISHED;
 import static org.apache.maven.surefire.booter.MasterProcessCommand.decode;
 import static org.apache.maven.surefire.util.internal.DaemonThreadFactory.newDaemonThread;
-import static org.apache.maven.surefire.util.internal.StringUtils.encodeStringForForkCommunication;
 import static org.apache.maven.surefire.util.internal.StringUtils.isBlank;
 import static org.apache.maven.surefire.util.internal.StringUtils.isNotBlank;
 
@@ -189,7 +186,7 @@ public final class CommandReader
     }
 
     /**
-     * @return test classes which have been retrieved by {@link CommandReader#getIterableClasses(PrintStream)}.
+     * @return test classes which have been retrieved by {@link CommandReader#getIterableClasses(ForkedChannelEncoder)}.
      */
     Iterator<String> iterated()
     {
@@ -200,12 +197,12 @@ public final class CommandReader
      * The iterator can be used only in one Thread.
      * Two simultaneous instances are not allowed for sake of only one {@link #nextCommandNotifier}.
      *
-     * @param originalOutStream original stream in current JVM process
+     * @param eventChannel original stream in current JVM process
      * @return Iterator with test classes lazily loaded as commands from the main process
      */
-    Iterable<String> getIterableClasses( PrintStream originalOutStream )
+    Iterable<String> getIterableClasses( ForkedChannelEncoder eventChannel )
     {
-        return new ClassesIterable( originalOutStream );
+        return new ClassesIterable( eventChannel );
     }
 
     public void stop()
@@ -253,32 +250,32 @@ public final class CommandReader
     private final class ClassesIterable
         implements Iterable<String>
     {
-        private final PrintStream originalOutStream;
+        private final ForkedChannelEncoder eventChannel;
 
-        ClassesIterable( PrintStream originalOutStream )
+        ClassesIterable( ForkedChannelEncoder eventChannel )
         {
-            this.originalOutStream = originalOutStream;
+            this.eventChannel = eventChannel;
         }
 
         @Override
         public Iterator<String> iterator()
         {
-            return new ClassesIterator( originalOutStream );
+            return new ClassesIterator( eventChannel );
         }
     }
 
     private final class ClassesIterator
         implements Iterator<String>
     {
-        private final PrintStream originalOutStream;
+        private final ForkedChannelEncoder eventChannel;
 
         private String clazz;
 
         private int nextQueueIndex;
 
-        private ClassesIterator( PrintStream originalOutStream )
+        private ClassesIterator( ForkedChannelEncoder eventChannel )
         {
-            this.originalOutStream = originalOutStream;
+            this.eventChannel = eventChannel;
         }
 
         @Override
@@ -344,12 +341,7 @@ public final class CommandReader
 
         private void requestNextTest()
         {
-            byte[] encoded = encodeStringForForkCommunication( ( (char) BOOTERCODE_NEXT_TEST ) + ",0,want more!\n" );
-            synchronized ( originalOutStream )
-            {
-                originalOutStream.write( encoded, 0, encoded.length );
-                originalOutStream.flush();
-            }
+            eventChannel.acquireNextTest();
         }
 
         private boolean shouldFinish()
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/booter/ForkedChannelEncoder.java b/surefire-api/src/main/java/org/apache/maven/surefire/booter/ForkedChannelEncoder.java
new file mode 100644
index 0000000..9a46fca
--- /dev/null
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/booter/ForkedChannelEncoder.java
@@ -0,0 +1,404 @@
+package org.apache.maven.surefire.booter;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.maven.plugin.surefire.log.api.ConsoleLoggerUtils;
+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 java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.maven.surefire.booter.ForkedProcessEvent.MAGIC_NUMBER;
+import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_SYSPROPS;
+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_ERROR;
+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.BOOTERCODE_JVM_EXIT_ERROR;
+import static org.apache.maven.surefire.report.RunMode.NORMAL_RUN;
+import static org.apache.maven.surefire.report.RunMode.RERUN_TEST_AFTER_FAILURE;
+import static java.util.Objects.requireNonNull;
+
+/**
+ * magic number : opcode : run mode [: opcode specific data]*
+ * <br>
+ *
+ * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
+ * @since 3.0.0-M4
+ */
+public final class ForkedChannelEncoder
+{
+    private static final Base64 BASE64 = new Base64();
+    private static final Charset STREAM_ENCODING = US_ASCII;
+    private static final Charset STRING_ENCODING = UTF_8;
+
+    private final OutputStream out;
+    private final RunMode runMode;
+    private volatile boolean trouble;
+
+    public ForkedChannelEncoder( OutputStream out )
+    {
+        this( out, NORMAL_RUN );
+    }
+
+    private ForkedChannelEncoder( OutputStream out, RunMode runMode )
+    {
+        this.out = requireNonNull( out );
+        this.runMode = requireNonNull( runMode );
+    }
+
+    public ForkedChannelEncoder asRerunMode() // todo apply this and rework providers
+    {
+        return new ForkedChannelEncoder( out, RERUN_TEST_AFTER_FAILURE );
+    }
+
+    public ForkedChannelEncoder asNormalMode()
+    {
+        return new ForkedChannelEncoder( out, NORMAL_RUN );
+    }
+
+    public boolean checkError()
+    {
+        return trouble;
+    }
+
+    public void sendSystemProperties( Map<String, String> sysProps )
+    {
+        for ( Entry<String, String> entry : sysProps.entrySet() )
+        {
+            String key = entry.getKey();
+            String value = entry.getValue();
+            StringBuilder event = encode( BOOTERCODE_SYSPROPS, runMode, key, value );
+            encodeAndPrintEvent( event );
+        }
+    }
+
+    public void testSetStarting( ReportEntry reportEntry, boolean trimStackTraces )
+    {
+        encode( BOOTERCODE_TESTSET_STARTING, runMode, reportEntry, trimStackTraces );
+    }
+
+    public void testSetCompleted( ReportEntry reportEntry, boolean trimStackTraces )
+    {
+        encode( BOOTERCODE_TESTSET_COMPLETED, runMode, reportEntry, trimStackTraces );
+    }
+
+    public void testStarting( ReportEntry reportEntry, boolean trimStackTraces )
+    {
+        encode( BOOTERCODE_TEST_STARTING, runMode, reportEntry, trimStackTraces );
+    }
+
+    public void testSucceeded( ReportEntry reportEntry, boolean trimStackTraces )
+    {
+        encode( BOOTERCODE_TEST_SUCCEEDED, runMode, reportEntry, trimStackTraces );
+    }
+
+    public void testFailed( ReportEntry reportEntry, boolean trimStackTraces )
+    {
+        encode( BOOTERCODE_TEST_FAILED, runMode, reportEntry, trimStackTraces );
+    }
+
+    public void testSkipped( ReportEntry reportEntry, boolean trimStackTraces )
+    {
+        encode( BOOTERCODE_TEST_SKIPPED, runMode, reportEntry, trimStackTraces );
+    }
+
+    public void testError( ReportEntry reportEntry, boolean trimStackTraces )
+    {
+        encode( BOOTERCODE_TEST_ERROR, runMode, reportEntry, trimStackTraces );
+    }
+
+    public void testAssumptionFailure( ReportEntry reportEntry, boolean trimStackTraces )
+    {
+        encode( BOOTERCODE_TEST_ASSUMPTIONFAILURE, runMode, reportEntry, trimStackTraces );
+    }
+
+    public void stdOut( String msg, boolean newLine )
+    {
+        ForkedProcessEvent event = newLine ? BOOTERCODE_STDOUT_NEW_LINE : BOOTERCODE_STDOUT;
+        setOutErr( event.getOpcode(), msg );
+    }
+
+    public void stdErr( String msg, boolean newLine )
+    {
+        ForkedProcessEvent event = newLine ? BOOTERCODE_STDERR_NEW_LINE : BOOTERCODE_STDERR;
+        setOutErr( event.getOpcode(), msg );
+    }
+
+    private void setOutErr( String eventType, String message )
+    {
+        String base64Message = toBase64( message );
+        StringBuilder event = encodeMessage( eventType, runMode.geRunName(), base64Message );
+        encodeAndPrintEvent( event );
+    }
+
+    public void consoleInfoLog( String msg )
+    {
+        StringBuilder event = print( BOOTERCODE_CONSOLE_INFO.getOpcode(), msg );
+        encodeAndPrintEvent( event );
+    }
+
+    public void consoleErrorLog( String msg )
+    {
+        StringBuilder event = print( BOOTERCODE_CONSOLE_ERROR.getOpcode(), msg );
+        encodeAndPrintEvent( event );
+    }
+
+    public void consoleErrorLog( Throwable t )
+    {
+        consoleErrorLog( t.getLocalizedMessage(), t );
+    }
+
+    public void consoleErrorLog( String msg, Throwable t )
+    {
+        StringBuilder encoded = encodeHeader( BOOTERCODE_CONSOLE_ERROR.getOpcode(), null );
+        encode( encoded, msg, null, ConsoleLoggerUtils.toString( t ) );
+        encodeAndPrintEvent( encoded );
+    }
+
+    public void consoleErrorLog( StackTraceWriter stackTraceWriter, boolean trimStackTraces )
+    {
+        error( stackTraceWriter, trimStackTraces, BOOTERCODE_CONSOLE_ERROR );
+    }
+
+    public void consoleDebugLog( String msg )
+    {
+        StringBuilder event = print( BOOTERCODE_CONSOLE_DEBUG.getOpcode(), msg );
+        encodeAndPrintEvent( event );
+    }
+
+    public void consoleWarningLog( String msg )
+    {
+        StringBuilder event = print( BOOTERCODE_CONSOLE_WARNING.getOpcode(), msg );
+        encodeAndPrintEvent( event );
+    }
+
+    public void bye()
+    {
+        encodeOpcode( BOOTERCODE_BYE );
+    }
+
+    public void stopOnNextTest()
+    {
+        encodeOpcode( BOOTERCODE_STOP_ON_NEXT_TEST );
+    }
+
+    public void acquireNextTest()
+    {
+        encodeOpcode( BOOTERCODE_NEXT_TEST );
+    }
+
+    public void sendExitEvent( StackTraceWriter stackTraceWriter, boolean trimStackTraces )
+    {
+        error( stackTraceWriter, trimStackTraces, BOOTERCODE_JVM_EXIT_ERROR );
+    }
+
+    private void error( StackTraceWriter stackTraceWriter, boolean trimStackTraces, ForkedProcessEvent event )
+    {
+        StringBuilder encoded = encodeHeader( event.getOpcode(), null );
+        encode( encoded, stackTraceWriter, trimStackTraces );
+        encodeAndPrintEvent( encoded );
+    }
+
+    private void encode( ForkedProcessEvent operation, RunMode runMode, ReportEntry reportEntry,
+                         boolean trimStackTraces )
+    {
+        StringBuilder event = encode( operation.getOpcode(), runMode.geRunName(), reportEntry, trimStackTraces );
+        encodeAndPrintEvent( event );
+    }
+
+    private void encodeOpcode( ForkedProcessEvent operation )
+    {
+        StringBuilder event = encodeOpcode( operation.getOpcode(), null );
+        encodeAndPrintEvent( event );
+    }
+
+    private void encodeAndPrintEvent( StringBuilder command )
+    {
+        byte[] array = command.append( '\n' ).toString().getBytes( STREAM_ENCODING );
+        synchronized ( out )
+        {
+            try
+            {
+                out.write( array );
+                out.flush();
+            }
+            catch ( IOException e )
+            {
+                DumpErrorSingleton.getSingleton().dumpException( e );
+                trouble = true;
+            }
+        }
+    }
+
+    static StringBuilder encode( ForkedProcessEvent operation, RunMode runMode, String... args )
+    {
+        StringBuilder encodedTo = encodeHeader( operation.getOpcode(), runMode.geRunName() )
+                                          .append( ':' );
+
+        for ( int i = 0; i < args.length; )
+        {
+            String arg = args[i++];
+            encodedTo.append( toBase64( arg ) );
+            if ( i != args.length )
+            {
+                encodedTo.append( ':' );
+            }
+        }
+        return encodedTo;
+    }
+
+    static void encode( StringBuilder encoded, StackTraceWriter stw, boolean trimStackTraces )
+    {
+        SafeThrowable throwable = stw == null ? null : stw.getThrowable();
+        String message = throwable == null ? null : throwable.getLocalizedMessage();
+        String smartStackTrace = stw == null ? null : stw.smartTrimmedStackTrace();
+        String stackTrace = stw == null ? null : toStackTrace( stw, trimStackTraces );
+        encode( encoded, message, smartStackTrace, stackTrace );
+    }
+
+    private static void encode( StringBuilder encoded, String message, String smartStackTrace, String stackTrace )
+    {
+        encoded.append( ':' )
+                .append( toBase64( message ) )
+                .append( ':' )
+                .append( toBase64( smartStackTrace ) )
+                .append( ':' )
+                .append( toBase64( stackTrace ) );
+    }
+
+    /**
+     * Used operations:<br>
+     * <ul>
+     * <li>{@link ForkedProcessEvent#BOOTERCODE_TESTSET_STARTING},</li>
+     * <li>{@link ForkedProcessEvent#BOOTERCODE_TESTSET_COMPLETED},</li>
+     * <li>{@link ForkedProcessEvent#BOOTERCODE_TEST_STARTING},</li>
+     * <li>{@link ForkedProcessEvent#BOOTERCODE_TEST_SUCCEEDED},</li>
+     * <li>{@link ForkedProcessEvent#BOOTERCODE_TEST_FAILED},</li>
+     * <li>{@link ForkedProcessEvent#BOOTERCODE_TEST_ERROR},</li>
+     * <li>{@link ForkedProcessEvent#BOOTERCODE_TEST_SKIPPED},</li>
+     * <li>{@link ForkedProcessEvent#BOOTERCODE_TEST_ASSUMPTIONFAILURE}.</li>
+     * </ul>
+     */
+    static StringBuilder encode( String operation, String runMode, ReportEntry reportEntry,
+                                         boolean trimStackTraces )
+    {
+        StringBuilder encodedTo = encodeHeader( operation, runMode )
+                .append( ':' )
+                .append( toBase64( reportEntry.getSourceName() ) )
+                .append( ':' )
+                .append( toBase64( reportEntry.getName() ) )
+                .append( ':' )
+                .append( toBase64( reportEntry.getGroup() ) )
+                .append( ':' )
+                .append( toBase64( reportEntry.getMessage() ) )
+                .append( ':' )
+                .append( reportEntry.getElapsed() == null ? "-" : reportEntry.getElapsed().toString() );
+
+        encode( encodedTo, reportEntry.getStackTraceWriter(), trimStackTraces );
+
+        return encodedTo;
+    }
+
+    /**
+     * Used in {@link #consoleInfoLog(String)}, {@link #consoleErrorLog(String)}, {@link #consoleDebugLog(String)},
+     * {@link #consoleWarningLog(String)} and private methods extending the buffer.
+     */
+    StringBuilder print( String operation, String... msgs )
+    {
+        String[] encodedMsgs = new String[msgs.length];
+        for ( int i = 0; i < encodedMsgs.length; i++ )
+        {
+            String msg = msgs[i];
+            encodedMsgs[i] = toBase64( msg );
+        }
+        return encodeMessage( operation, null, encodedMsgs );
+    }
+
+    static StringBuilder encodeMessage( String operation, String runMode, String... encodedMsgs )
+    {
+        StringBuilder builder = encodeHeader( operation, runMode );
+        for ( String encodedMsg : encodedMsgs )
+        {
+            builder.append( ':' )
+                    .append( encodedMsg );
+
+        }
+        return builder;
+    }
+
+    static StringBuilder encodeHeader( String operation, String runMode )
+    {
+        return encodeOpcode( operation, runMode )
+                       .append( ':' )
+                       .append( STRING_ENCODING.name() );
+    }
+
+    /**
+     * Used in {@link #bye()}, {@link #stopOnNextTest()} and {@link #encodeOpcode(ForkedProcessEvent)}
+     * and private methods extending the buffer.
+     *
+     * @param operation opcode
+     * @param runMode   run mode
+     * @return encoded command
+     */
+    static StringBuilder encodeOpcode( String operation, String runMode )
+    {
+        StringBuilder s = new StringBuilder( 128 )
+                .append( MAGIC_NUMBER )
+                .append( operation );
+
+        return runMode == null ? s : s.append( ':' ).append( runMode );
+    }
+
+    private static String toStackTrace( StackTraceWriter stw, boolean trimStackTraces )
+    {
+        return trimStackTraces ? stw.writeTrimmedTraceToString() : stw.writeTraceToString();
+    }
+
+    static String toBase64( String msg )
+    {
+        return msg == null ? "-" : new String( BASE64.encode( msg.getBytes( STRING_ENCODING ) ), STREAM_ENCODING );
+    }
+}
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/booter/ForkedProcessEvent.java b/surefire-api/src/main/java/org/apache/maven/surefire/booter/ForkedProcessEvent.java
new file mode 100644
index 0000000..74b9eb9
--- /dev/null
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/booter/ForkedProcessEvent.java
@@ -0,0 +1,132 @@
+package org.apache.maven.surefire.booter;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static java.util.Collections.unmodifiableMap;
+
+/**
+ * Events sent back to the plugin process.
+ *
+ * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
+ * @since 3.0.0-M4
+ */
+public enum ForkedProcessEvent
+{
+    BOOTERCODE_SYSPROPS( "sys-prop" ),
+
+    BOOTERCODE_TESTSET_STARTING( "testset-starting" ),
+    BOOTERCODE_TESTSET_COMPLETED( "testset-completed" ),
+    BOOTERCODE_TEST_STARTING( "test-starting" ),
+    BOOTERCODE_TEST_SUCCEEDED( "test-succeeded" ),
+    BOOTERCODE_TEST_FAILED( "test-failed" ),
+    BOOTERCODE_TEST_SKIPPED( "test-skipped" ),
+    BOOTERCODE_TEST_ERROR( "test-error" ),
+    BOOTERCODE_TEST_ASSUMPTIONFAILURE( "test-assumption-failure" ),
+
+    BOOTERCODE_STDOUT( "std-out-stream" ),
+    BOOTERCODE_STDOUT_NEW_LINE( "std-out-stream-new-line" ),
+    BOOTERCODE_STDERR( "std-err-stream" ),
+    BOOTERCODE_STDERR_NEW_LINE( "std-err-stream-new-line" ),
+
+    BOOTERCODE_CONSOLE_INFO( "console-info-log" ),
+    BOOTERCODE_CONSOLE_DEBUG( "console-debug-log" ),
+    BOOTERCODE_CONSOLE_WARNING( "console-warning-log" ),
+    BOOTERCODE_CONSOLE_ERROR( "console-error-log" ),
+
+    BOOTERCODE_BYE( "bye" ),
+    BOOTERCODE_STOP_ON_NEXT_TEST( "stop-on-next-test" ),
+    BOOTERCODE_NEXT_TEST( "next-test" ),
+
+    BOOTERCODE_JVM_EXIT_ERROR( "jvm-exit-error" );
+
+    public static final String MAGIC_NUMBER = ":maven:surefire:std:out:";
+
+    public static final Map<String, ForkedProcessEvent> EVENTS = events();
+
+    private static Map<String, ForkedProcessEvent> events()
+    {
+        Map<String, ForkedProcessEvent> events = new ConcurrentHashMap<>();
+        for ( ForkedProcessEvent event : values() )
+        {
+            events.put( event.getOpcode(), event );
+        }
+        return unmodifiableMap( events );
+    }
+
+    private final String opcode;
+
+    ForkedProcessEvent( String opcode )
+    {
+        this.opcode = opcode;
+    }
+
+    public String getOpcode()
+    {
+        return opcode;
+    }
+
+    public boolean isSysPropCategory()
+    {
+        return this == BOOTERCODE_SYSPROPS;
+    }
+
+    public boolean isTestCategory()
+    {
+        return this == BOOTERCODE_TESTSET_STARTING
+                || this == BOOTERCODE_TESTSET_COMPLETED
+                || this == BOOTERCODE_TEST_STARTING
+                || this == BOOTERCODE_TEST_SUCCEEDED
+                || this == BOOTERCODE_TEST_FAILED
+                || this == BOOTERCODE_TEST_SKIPPED
+                || this == BOOTERCODE_TEST_ERROR
+                || this == BOOTERCODE_TEST_ASSUMPTIONFAILURE;
+    }
+
+    public boolean isStandardStreamCategory()
+    {
+        return this == BOOTERCODE_STDOUT || this == BOOTERCODE_STDOUT_NEW_LINE
+                || this == BOOTERCODE_STDERR || this == BOOTERCODE_STDERR_NEW_LINE;
+    }
+
+    public boolean isConsoleCategory()
+    {
+        return this == BOOTERCODE_CONSOLE_INFO
+                || this == BOOTERCODE_CONSOLE_DEBUG
+                || this == BOOTERCODE_CONSOLE_WARNING;
+    }
+
+    public boolean isConsoleErrorCategory()
+    {
+        return this == BOOTERCODE_CONSOLE_ERROR;
+    }
+
+    public boolean isControlCategory()
+    {
+        return this == BOOTERCODE_BYE || this == BOOTERCODE_STOP_ON_NEXT_TEST || this == BOOTERCODE_NEXT_TEST;
+    }
+
+    public boolean isJvmExitError()
+    {
+        return this == BOOTERCODE_JVM_EXIT_ERROR;
+    }
+}
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/booter/ForkingReporterFactory.java b/surefire-api/src/main/java/org/apache/maven/surefire/booter/ForkingReporterFactory.java
index 7459ad9..5bb16ee 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/booter/ForkingReporterFactory.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/booter/ForkingReporterFactory.java
@@ -19,9 +19,6 @@ package org.apache.maven.surefire.booter;
  * under the License.
  */
 
-import java.io.PrintStream;
-import java.util.concurrent.atomic.AtomicInteger;
-
 import org.apache.maven.surefire.report.ReporterFactory;
 import org.apache.maven.surefire.report.RunListener;
 import org.apache.maven.surefire.suite.RunResult;
@@ -35,22 +32,20 @@ import org.apache.maven.surefire.suite.RunResult;
 public class ForkingReporterFactory
     implements ReporterFactory
 {
-    private final boolean isTrimstackTrace;
-
-    private final PrintStream originalSystemOut;
+    private final boolean trimstackTrace;
 
-    private final AtomicInteger testSetChannelId = new AtomicInteger( 1 );
+    private final ForkedChannelEncoder eventChannel;
 
-    public ForkingReporterFactory( boolean trimstackTrace, PrintStream originalSystemOut )
+    public ForkingReporterFactory( boolean trimstackTrace, ForkedChannelEncoder eventChannel )
     {
-        isTrimstackTrace = trimstackTrace;
-        this.originalSystemOut = originalSystemOut;
+        this.trimstackTrace = trimstackTrace;
+        this.eventChannel = eventChannel;
     }
 
     @Override
     public RunListener createReporter()
     {
-        return new ForkingRunListener( originalSystemOut, testSetChannelId.getAndIncrement(), isTrimstackTrace );
+        return new ForkingRunListener( eventChannel, trimstackTrace );
     }
 
     @Override
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/booter/ForkingRunListener.java b/surefire-api/src/main/java/org/apache/maven/surefire/booter/ForkingRunListener.java
index 80d08db..528b607 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/booter/ForkingRunListener.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/booter/ForkingRunListener.java
@@ -20,27 +20,15 @@ package org.apache.maven.surefire.booter;
  */
 
 import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
-import org.apache.maven.plugin.surefire.log.api.ConsoleLoggerUtils;
 import org.apache.maven.surefire.report.ConsoleOutputReceiver;
 import org.apache.maven.surefire.report.ConsoleStream;
 import org.apache.maven.surefire.report.ReportEntry;
 import org.apache.maven.surefire.report.RunListener;
-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.RunMode;
 import org.apache.maven.surefire.report.TestSetReportEntry;
-import org.apache.maven.surefire.util.internal.StringUtils.EncodedArray;
 
-import java.io.PrintStream;
-import java.util.Map.Entry;
-
-import static java.lang.Integer.toHexString;
-import static java.nio.charset.Charset.defaultCharset;
-import static org.apache.maven.surefire.util.internal.ObjectUtils.systemProps;
-import static org.apache.maven.surefire.util.internal.ObjectUtils.useNonNull;
-import static org.apache.maven.surefire.util.internal.StringUtils.encodeStringForForkCommunication;
-import static org.apache.maven.surefire.util.internal.StringUtils.escapeBytesToPrintable;
-import static org.apache.maven.surefire.util.internal.StringUtils.escapeToPrintable;
+import static org.apache.maven.surefire.report.RunMode.NORMAL_RUN;
+import static java.util.Objects.requireNonNull;
 
 /**
  * Encodes the full output of the test run to the stdout stream.
@@ -55,188 +43,98 @@ import static org.apache.maven.surefire.util.internal.StringUtils.escapeToPrinta
  * The synchronization in the underlying PrintStream (target instance)
  * is used to preserve thread safety of the output stream. To perform
  * multiple writes/prints for a single request, they must
- * synchronize on "target" variable in this class.
+ * synchronize on "target.out" variable in this class.
  *
  * @author Kristian Rosenvold
  */
 public class ForkingRunListener
     implements RunListener, ConsoleLogger, ConsoleOutputReceiver, ConsoleStream
 {
-    public static final byte BOOTERCODE_TESTSET_STARTING = (byte) '1';
-
-    public static final byte BOOTERCODE_TESTSET_COMPLETED = (byte) '2';
-
-    public static final byte BOOTERCODE_STDOUT = (byte) '3';
-
-    public static final byte BOOTERCODE_STDERR = (byte) '4';
-
-    public static final byte BOOTERCODE_TEST_STARTING = (byte) '5';
-
-    public static final byte BOOTERCODE_TEST_SUCCEEDED = (byte) '6';
-
-    public static final byte BOOTERCODE_TEST_ERROR = (byte) '7';
-
-    public static final byte BOOTERCODE_TEST_FAILED = (byte) '8';
-
-    public static final byte BOOTERCODE_TEST_SKIPPED = (byte) '9';
-
-    public static final byte BOOTERCODE_TEST_ASSUMPTIONFAILURE = (byte) 'G';
-
-    /**
-     * INFO logger
-     * @see ConsoleLogger#info(String)
-     */
-    public static final byte BOOTERCODE_CONSOLE = (byte) 'H';
-
-    public static final byte BOOTERCODE_SYSPROPS = (byte) 'I';
-
-    public static final byte BOOTERCODE_NEXT_TEST = (byte) 'N';
-
-    public static final byte BOOTERCODE_STOP_ON_NEXT_TEST = (byte) 'S';
-
-    /**
-     * ERROR logger
-     * @see ConsoleLogger#error(String)
-     */
-    public static final byte BOOTERCODE_ERROR = (byte) 'X';
-
-    public static final byte BOOTERCODE_BYE = (byte) 'Z';
-
-    /**
-     * DEBUG logger
-     * @see ConsoleLogger#debug(String)
-     */
-    public static final byte BOOTERCODE_DEBUG = (byte) 'D';
-
-    /**
-     * WARNING logger
-     * @see ConsoleLogger#warning(String)
-     */
-    public static final byte BOOTERCODE_WARNING = (byte) 'W';
-
-
-    private final PrintStream target;
-
-    private final int testSetChannelId;
+    private final ForkedChannelEncoder target;
 
-    private final boolean trimStackTraces;
+    private final boolean trim;
 
-    private final byte[] stdOutHeader;
+    private volatile RunMode runMode = NORMAL_RUN;
 
-    private final byte[] stdErrHeader;
-
-    public ForkingRunListener( PrintStream target, int testSetChannelId, boolean trimStackTraces )
+    public ForkingRunListener( ForkedChannelEncoder target, boolean trim )
     {
         this.target = target;
-        this.testSetChannelId = testSetChannelId;
-        this.trimStackTraces = trimStackTraces;
-        stdOutHeader = createHeader( BOOTERCODE_STDOUT, testSetChannelId );
-        stdErrHeader = createHeader( BOOTERCODE_STDERR, testSetChannelId );
-        sendProps();
+        this.trim = trim;
     }
 
     @Override
     public void testSetStarting( TestSetReportEntry report )
     {
-        encodeAndWriteToTarget( toString( BOOTERCODE_TESTSET_STARTING, report, testSetChannelId ) );
+        target.testSetStarting( report, trim );
     }
 
     @Override
     public void testSetCompleted( TestSetReportEntry report )
     {
-        encodeAndWriteToTarget( toString( BOOTERCODE_TESTSET_COMPLETED, report, testSetChannelId ) );
+        target.sendSystemProperties( report.getSystemProperties() );
+        target.testSetCompleted( report, trim );
     }
 
     @Override
     public void testStarting( ReportEntry report )
     {
-        encodeAndWriteToTarget( toString( BOOTERCODE_TEST_STARTING, report, testSetChannelId ) );
+        target.testStarting( report, trim );
     }
 
     @Override
     public void testSucceeded( ReportEntry report )
     {
-        encodeAndWriteToTarget( toString( BOOTERCODE_TEST_SUCCEEDED, report, testSetChannelId ) );
+        target.testSucceeded( report, trim );
     }
 
     @Override
     public void testAssumptionFailure( ReportEntry report )
     {
-        encodeAndWriteToTarget( toString( BOOTERCODE_TEST_ASSUMPTIONFAILURE, report, testSetChannelId ) );
+        target.testAssumptionFailure( report, trim );
     }
 
     @Override
     public void testError( ReportEntry report )
     {
-        encodeAndWriteToTarget( toString( BOOTERCODE_TEST_ERROR, report, testSetChannelId ) );
+        target.testError( report, trim );
     }
 
     @Override
     public void testFailed( ReportEntry report )
     {
-        encodeAndWriteToTarget( toString( BOOTERCODE_TEST_FAILED, report, testSetChannelId ) );
+        target.testFailed( report, trim );
     }
 
     @Override
     public void testSkipped( ReportEntry report )
     {
-        encodeAndWriteToTarget( toString( BOOTERCODE_TEST_SKIPPED, report, testSetChannelId ) );
+        target.testSkipped( report, trim );
     }
 
     @Override
     public void testExecutionSkippedByUser()
     {
-        encodeAndWriteToTarget( toString( BOOTERCODE_STOP_ON_NEXT_TEST, new SimpleReportEntry(), testSetChannelId ) );
+        target.stopOnNextTest();
     }
 
-    private void sendProps()
+    @Override
+    public RunMode markAs( RunMode currentRunMode )
     {
-        for ( Entry<String, String> entry : systemProps().entrySet() )
-        {
-            String value = entry.getValue();
-            encodeAndWriteToTarget( toPropertyString( entry.getKey(), useNonNull( value, "null" ) ) );
-        }
+        RunMode runMode = this.runMode;
+        this.runMode = requireNonNull( currentRunMode );
+        return runMode;
     }
 
     @Override
-    public void writeTestOutput( byte[] buf, int off, int len, boolean stdout )
+    public void writeTestOutput( String output, boolean newLine, boolean stdout )
     {
-        EncodedArray encodedArray = escapeBytesToPrintable( stdout ? stdOutHeader : stdErrHeader, buf, off, len );
-
-        synchronized ( target ) // See notes about synchronization/thread safety in class javadoc
+        if ( stdout )
         {
-            target.write( encodedArray.getArray(), 0, encodedArray.getSize() );
-            target.flush();
-            if ( target.checkError() )
-            {
-                // We MUST NOT throw any exception from this method; otherwise we are in loop and CPU goes up:
-                // ForkingRunListener -> Exception -> JUnit Notifier and RunListener -> ForkingRunListener -> Exception
-                DumpErrorSingleton.getSingleton()
-                        .dumpStreamText( "Unexpected IOException with stream: " + new String( buf, off, len ) );
-            }
+            target.stdOut( output, newLine );
         }
-    }
-
-    public static byte[] createHeader( byte booterCode, int testSetChannel )
-    {
-        return encodeStringForForkCommunication( String.valueOf( (char) booterCode )
-                + ','
-                + Integer.toString( testSetChannel, 16 )
-                + ',' + defaultCharset().name()
-                + ',' );
-    }
-
-    private void log( byte bootCode, String message )
-    {
-        if ( message != null )
+        else
         {
-            StringBuilder sb = new StringBuilder( 7 + message.length() * 5 );
-            append( sb, bootCode ); comma( sb );
-            append( sb, toHexString( testSetChannelId ) ); comma( sb );
-            escapeToPrintable( sb, message );
-
-            sb.append( '\n' );
-            encodeAndWriteToTarget( sb.toString() );
+            target.stdErr( output, newLine );
         }
     }
 
@@ -249,7 +147,7 @@ public class ForkingRunListener
     @Override
     public void debug( String message )
     {
-        log( BOOTERCODE_DEBUG, message );
+        target.consoleDebugLog( message );
     }
 
     @Override
@@ -261,7 +159,7 @@ public class ForkingRunListener
     @Override
     public void info( String message )
     {
-        log( BOOTERCODE_CONSOLE, message );
+        target.consoleInfoLog( message );
     }
 
     @Override
@@ -273,7 +171,7 @@ public class ForkingRunListener
     @Override
     public void warning( String message )
     {
-        log( BOOTERCODE_WARNING, message );
+        target.consoleWarningLog( message );
     }
 
     @Override
@@ -285,13 +183,13 @@ public class ForkingRunListener
     @Override
     public void error( String message )
     {
-        log( BOOTERCODE_ERROR, message );
+        target.consoleErrorLog( message );
     }
 
     @Override
     public void error( String message, Throwable t )
     {
-        error( ConsoleLoggerUtils.toString( message, t ) );
+        target.consoleErrorLog( message, t );
     }
 
     @Override
@@ -300,130 +198,9 @@ public class ForkingRunListener
         error( null, t );
     }
 
-    private void encodeAndWriteToTarget( String string )
-    {
-        byte[] encodeBytes = encodeStringForForkCommunication( string );
-        synchronized ( target ) // See notes about synchronization/thread safety in class javadoc
-        {
-            target.write( encodeBytes, 0, encodeBytes.length );
-            target.flush();
-            if ( target.checkError() )
-            {
-                // We MUST NOT throw any exception from this method; otherwise we are in loop and CPU goes up:
-                // ForkingRunListener -> Exception -> JUnit Notifier and RunListener -> ForkingRunListener -> Exception
-                DumpErrorSingleton.getSingleton().dumpStreamText( "Unexpected IOException: " + string );
-            }
-        }
-    }
-
-    private String toPropertyString( String key, String value )
-    {
-        StringBuilder stringBuilder = new StringBuilder();
-
-        append( stringBuilder, BOOTERCODE_SYSPROPS ); comma( stringBuilder );
-        append( stringBuilder, toHexString( testSetChannelId ) ); comma( stringBuilder );
-
-        escapeToPrintable( stringBuilder, key );
-        comma( stringBuilder );
-        escapeToPrintable( stringBuilder, value );
-        stringBuilder.append( "\n" );
-        return stringBuilder.toString();
-    }
-
-    private String toString( byte operationCode, ReportEntry reportEntry, int testSetChannelId )
-    {
-        StringBuilder stringBuilder = new StringBuilder();
-        append( stringBuilder, operationCode ); comma( stringBuilder );
-        append( stringBuilder, toHexString( testSetChannelId ) ); comma( stringBuilder );
-
-        nullableEncoding( stringBuilder, reportEntry.getSourceName() );
-        comma( stringBuilder );
-        nullableEncoding( stringBuilder, reportEntry.getName() );
-        comma( stringBuilder );
-        nullableEncoding( stringBuilder, reportEntry.getGroup() );
-        comma( stringBuilder );
-        nullableEncoding( stringBuilder, reportEntry.getMessage() );
-        comma( stringBuilder );
-        nullableEncoding( stringBuilder, reportEntry.getElapsed() );
-        encode( stringBuilder, reportEntry.getStackTraceWriter() );
-        stringBuilder.append( "\n" );
-        return stringBuilder.toString();
-    }
-
-    private static void comma( StringBuilder stringBuilder )
-    {
-        stringBuilder.append( "," );
-    }
-
-    private void append( StringBuilder stringBuilder, String message )
-    {
-        stringBuilder.append( encode( message ) );
-    }
-
-    private void append( StringBuilder stringBuilder, byte b )
-    {
-        stringBuilder.append( (char) b );
-    }
-
-    private void nullableEncoding( StringBuilder stringBuilder, Integer source )
-    {
-        stringBuilder.append( source == null ? "null" : source.toString() );
-    }
-
-    private String encode( String source )
-    {
-        return source;
-    }
-
-
-    private static void nullableEncoding( StringBuilder stringBuilder, String source )
-    {
-        if ( source == null || source.isEmpty() )
-        {
-            stringBuilder.append( "null" );
-        }
-        else
-        {
-            escapeToPrintable( stringBuilder, source );
-        }
-    }
-
-    private void encode( StringBuilder stringBuilder, StackTraceWriter stackTraceWriter )
-    {
-        encode( stringBuilder, stackTraceWriter, trimStackTraces );
-    }
-
-    public static void encode( StringBuilder stringBuilder, StackTraceWriter stackTraceWriter, boolean trimStackTraces )
-    {
-        if ( stackTraceWriter != null )
-        {
-            comma( stringBuilder );
-            //noinspection ThrowableResultOfMethodCallIgnored
-            final SafeThrowable throwable = stackTraceWriter.getThrowable();
-            if ( throwable != null )
-            {
-                String message = throwable.getLocalizedMessage();
-                nullableEncoding( stringBuilder, message );
-            }
-            comma( stringBuilder );
-            nullableEncoding( stringBuilder, stackTraceWriter.smartTrimmedStackTrace() );
-            comma( stringBuilder );
-            nullableEncoding( stringBuilder, trimStackTraces
-                ? stackTraceWriter.writeTrimmedTraceToString()
-                : stackTraceWriter.writeTraceToString() );
-        }
-    }
-
     @Override
     public void println( String message )
     {
-        byte[] buf = ( message == null ? "null" : message ).getBytes();
-        println( buf, 0, buf.length );
-    }
-
-    @Override
-    public void println( byte[] buf, int off, int len )
-    {
-        writeTestOutput( buf, off, len, true );
+        writeTestOutput( message, true, true );
     }
 }
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/booter/MasterProcessCommand.java b/surefire-api/src/main/java/org/apache/maven/surefire/booter/MasterProcessCommand.java
index 1f95179..7c4520f 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/booter/MasterProcessCommand.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/booter/MasterProcessCommand.java
@@ -22,11 +22,9 @@ package org.apache.maven.surefire.booter;
 import java.io.DataInputStream;
 import java.io.IOException;
 
-import static java.lang.String.format;
-import static java.nio.charset.StandardCharsets.ISO_8859_1;
 import static java.nio.charset.StandardCharsets.US_ASCII;
 import static java.util.Objects.requireNonNull;
-import static org.apache.maven.surefire.util.internal.StringUtils.encodeStringForForkCommunication;
+import static java.lang.String.format;
 
 /**
  * Commands which are sent from plugin to the forked jvm.
@@ -146,7 +144,6 @@ public enum MasterProcessCommand
         switch ( this )
         {
             case RUN_CLASS:
-                return new String( data, ISO_8859_1 );
             case SHUTDOWN:
                 return new String( data, US_ASCII );
             default:
@@ -159,7 +156,6 @@ public enum MasterProcessCommand
         switch ( this )
         {
             case RUN_CLASS:
-                return encodeStringForForkCommunication( data );
             case SHUTDOWN:
                 return data.getBytes( US_ASCII );
             default:
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/providerapi/ProviderParameters.java b/surefire-api/src/main/java/org/apache/maven/surefire/providerapi/ProviderParameters.java
index b487f06..0fea537 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/providerapi/ProviderParameters.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/providerapi/ProviderParameters.java
@@ -19,6 +19,7 @@ package org.apache.maven.surefire.providerapi;
  * under the License.
  */
 
+import org.apache.maven.surefire.booter.ForkedChannelEncoder;
 import org.apache.maven.surefire.booter.Shutdown;
 import org.apache.maven.surefire.cli.CommandLineOption;
 import org.apache.maven.surefire.report.ConsoleStream;
@@ -149,4 +150,6 @@ public interface ProviderParameters
     Shutdown getShutdown();
 
     Integer getSystemExitTimeout();
+
+    ForkedChannelEncoder getForkedChannelEncoder();
 }
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/report/CategorizedReportEntry.java b/surefire-api/src/main/java/org/apache/maven/surefire/report/CategorizedReportEntry.java
index cad41a2..226999e 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/report/CategorizedReportEntry.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/report/CategorizedReportEntry.java
@@ -27,7 +27,6 @@ import java.util.Map;
  */
 public class CategorizedReportEntry
     extends SimpleReportEntry
-    implements ReportEntry
 {
     private static final String GROUP_PREFIX = " (of ";
 
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputCapture.java b/surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputCapture.java
index df44f63..00051fc 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputCapture.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputCapture.java
@@ -25,7 +25,6 @@ import java.io.PrintStream;
 
 import static java.lang.System.setErr;
 import static java.lang.System.setOut;
-import static org.apache.maven.surefire.util.internal.StringUtils.NL;
 
 /**
  * Deals with system.out/err.
@@ -55,16 +54,14 @@ public final class ConsoleOutputCapture
         @Override
         public void write( byte[] buf, int off, int len )
         {
-            // Note: At this point the supplied "buf" instance is reused, which means
-            // data must be copied out of the buffer
-            target.writeTestOutput( buf, off, len, isStdout );
+            target.writeTestOutput( new String( buf, off, len ), false, isStdout );
         }
 
         @Override
         public void write( byte[] b )
             throws IOException
         {
-            target.writeTestOutput( b, 0, b.length, isStdout );
+            write( b, 0, b.length );
         }
 
         @Override
@@ -81,14 +78,139 @@ public final class ConsoleOutputCapture
         }
 
         @Override
+        public void println( boolean x )
+        {
+            println( x ? "true" : "false" );
+        }
+
+        @Override
+        public void println( char x )
+        {
+            println( String.valueOf( x ) );
+        }
+
+        @Override
+        public void println( int x )
+        {
+            println( String.valueOf( x ) );
+        }
+
+        @Override
+        public void println( long x )
+        {
+            println( String.valueOf( x ) );
+        }
+
+        @Override
+        public void println( float x )
+        {
+            println( String.valueOf( x ) );
+        }
+
+        @Override
+        public void println( double x )
+        {
+            println( String.valueOf( x ) );
+        }
+
+        @Override
+        public void println( char[] x )
+        {
+            println( String.valueOf( x ) );
+        }
+
+        @Override
+        public void println( Object x )
+        {
+            println( String.valueOf( x ) );
+        }
+
+        @Override
         public void println( String s )
         {
-            if ( s == null )
-            {
-                s = "null"; // Shamelessly taken from super.print
-            }
-            final byte[] bytes = ( s + NL ).getBytes();
-            target.writeTestOutput( bytes, 0, bytes.length, isStdout );
+            target.writeTestOutput( s == null ? "null" : s, true, isStdout );
+        }
+
+        @Override
+        public void println()
+        {
+            target.writeTestOutput( "", true, isStdout );
+        }
+
+        @Override
+        public void print( boolean x )
+        {
+            print( x ? "true" : "false" );
+        }
+
+        @Override
+        public void print( char x )
+        {
+            print( String.valueOf( x ) );
+        }
+
+        @Override
+        public void print( int x )
+        {
+            print( String.valueOf( x ) );
+        }
+
+        @Override
+        public void print( long x )
+        {
+            print( String.valueOf( x ) );
+        }
+
+        @Override
+        public void print( float x )
+        {
+            print( String.valueOf( x ) );
+        }
+
+        @Override
+        public void print( double x )
+        {
+            print( String.valueOf( x ) );
+        }
+
+        @Override
+        public void print( char[] x )
+        {
+            print( String.valueOf( x ) );
+        }
+
+        @Override
+        public void print( Object x )
+        {
+            print( String.valueOf( x ) );
+        }
+
+        @Override
+        public void print( String s )
+        {
+            target.writeTestOutput( s == null ? "null" : s, false, isStdout );
+        }
+
+        @Override
+        public PrintStream append( CharSequence csq )
+        {
+            print( csq == null ? "null" : csq.toString() );
+            return this;
+        }
+
+        @Override
+        public PrintStream append( CharSequence csq, int start, int end )
+        {
+            CharSequence s = csq == null ? "null" : csq;
+            print( s.subSequence( start, end ).toString() );
+            return this;
+        }
+
+        @Override
+        public PrintStream append( char c )
+        {
+            print( c );
+            return this;
         }
 
         @Override
@@ -106,9 +228,8 @@ public final class ConsoleOutputCapture
             extends OutputStream
     {
         @Override
-        public void write( int b ) throws IOException
+        public void write( int b )
         {
-
         }
     }
 }
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputReceiver.java b/surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputReceiver.java
index 06d6414..8bc39d0 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputReceiver.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputReceiver.java
@@ -29,11 +29,10 @@ public interface ConsoleOutputReceiver
     /**
      * Forwards process output from the running test-case into the reporting system
      *
-     * @param buf    the buffer to write
-     * @param off    offset
-     * @param len    len
+     * @param output stdout/sterr output from running tests
+     * @param newLine print on new line
      * @param stdout Indicates if this is stdout
      */
-    void writeTestOutput( byte[] buf, int off, int len, boolean stdout );
+    void writeTestOutput( String output, boolean newLine, boolean stdout );
 
 }
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleStream.java b/surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleStream.java
index 8a22ef0..0fc4918 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleStream.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleStream.java
@@ -1 +1 @@
-package org.apache.maven.surefire.report;

/*
 * 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.
 */

/**
 * Delegates to {@link System#out}.
 */
public interface ConsoleStream
{
    void println( String message );
    void println( byte[] buf, 
 int off, int len );
}
\ No newline at end of file
+package org.apache.maven.surefire.report;

/*
 * 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.
 */

/**
 * Delegates to {@link System#out}.
 */
public interface ConsoleStream
{
    void println( String message );
}
\ No newline at end of file
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/report/DefaultDirectConsoleReporter.java b/surefire-api/src/main/java/org/apache/maven/surefire/report/DefaultDirectConsoleReporter.java
index 5298ad9..6cb61c3 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/report/DefaultDirectConsoleReporter.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/report/DefaultDirectConsoleReporter.java
@@ -39,10 +39,4 @@ public final class DefaultDirectConsoleReporter
     {
         systemOut.println( message );
     }
-
-    @Override
-    public void println( byte[] buf, int off, int len )
-    {
-        println( new String( buf, off, len ) );
-    }
 }
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/report/RunListener.java b/surefire-api/src/main/java/org/apache/maven/surefire/report/RunListener.java
index 32c0abd..396b277 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/report/RunListener.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/report/RunListener.java
@@ -93,4 +93,13 @@ public interface RunListener
      * (The event is fired after the Nth test failed to signal skipping the rest of test-set.)
      */
     void testExecutionSkippedByUser();
+
+    /**
+     * Marks the listener with run mode, e.g. normal run or re-run.
+     *
+     * @param currentRunMode    set current run
+     * @return previous run mode; never returns null
+     * @throws NullPointerException if <code>currentRunMode</code> is null
+     */
+    RunMode markAs( RunMode currentRunMode );
 }
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/report/RunMode.java b/surefire-api/src/main/java/org/apache/maven/surefire/report/RunMode.java
new file mode 100644
index 0000000..efd78a3
--- /dev/null
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/report/RunMode.java
@@ -0,0 +1,64 @@
+package org.apache.maven.surefire.report;
+
+/*
+ * 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 java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static java.util.Collections.unmodifiableMap;
+
+/**
+ * Determines the purpose the provider started the tests. It can be either normal run or a kind of re-run type.
+ * <br>
+ * This is important in the logic of {@code StatelessXmlReporter}.
+ *
+ * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
+ * @since 3.0.0-M4
+ */
+public enum RunMode
+{
+    NORMAL_RUN( "normal-run" ),
+    RERUN_TEST_AFTER_FAILURE( "rerun-test-after-failure" );
+    //todo add here RERUN_TESTSET, see https://github.com/apache/maven-surefire/pull/221
+
+    public static final Map<String, RunMode> MODES = modes();
+
+    private static Map<String, RunMode> modes()
+    {
+        Map<String, RunMode> modes = new ConcurrentHashMap<>();
+        for ( RunMode mode : values() )
+        {
+            modes.put( mode.geRunName(), mode );
+        }
+        return unmodifiableMap( modes );
+    }
+
+    private final String runName;
+
+    RunMode( String runName )
+    {
+        this.runName = runName;
+    }
+
+    public String geRunName()
+    {
+        return runName;
+    }
+}
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/testset/TestListResolver.java b/surefire-api/src/main/java/org/apache/maven/surefire/testset/TestListResolver.java
index edc0e8a..266d06a 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/testset/TestListResolver.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/testset/TestListResolver.java
@@ -257,7 +257,7 @@ public class TestListResolver
         }
 
         aggregatedTest += aggregatedTest( "!", getExcludedPatterns() );
-        return aggregatedTest.length() == 0 ? "" : aggregatedTest;
+        return aggregatedTest.isEmpty() ? "" : aggregatedTest;
     }
 
     @Override
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/util/internal/StringUtils.java b/surefire-api/src/main/java/org/apache/maven/surefire/util/internal/StringUtils.java
index 86c09e3..6f49690 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/util/internal/StringUtils.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/util/internal/StringUtils.java
@@ -19,14 +19,9 @@ package org.apache.maven.surefire.util.internal;
  * under the License.
  */
 
-import java.nio.ByteBuffer;
-import java.nio.CharBuffer;
-import java.nio.charset.CharacterCodingException;
-import java.nio.charset.Charset;
 import java.util.StringTokenizer;
 
 import static java.lang.System.lineSeparator;
-import static java.nio.charset.StandardCharsets.ISO_8859_1;
 
 /**
  * <p>
@@ -57,11 +52,6 @@ public final class StringUtils
 {
     public static final String NL = lineSeparator();
 
-    private static final byte[] HEX_CHARS = {
-                    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
-
-    private static final Charset DEFAULT_CHARSET = Charset.defaultCharset();
-
     private StringUtils()
     {
         throw new IllegalStateException( "no instantiable constructor" );
@@ -116,215 +106,6 @@ public final class StringUtils
     }
 
     /**
-     * Escape the specified string to a representation that only consists of nicely printable characters, without any
-     * newlines and without a comma.
-     * <p>
-     * The reverse-method is {@link #unescapeString(StringBuilder, CharSequence)}.
-     *
-     * @param target target string buffer. The required space will be up to {@code str.getBytes().length * 5} chars.
-     * @param str String to escape values in, may be {@code null}.
-     */
-    @SuppressWarnings( "checkstyle:magicnumber" )
-    public static void escapeToPrintable( StringBuilder target, CharSequence str )
-    {
-        if ( target == null )
-        {
-            throw new IllegalArgumentException( "The target buffer must not be null" );
-        }
-        if ( str == null )
-        {
-            return;
-        }
-
-        for ( int i = 0; i < str.length(); i++ )
-        {
-            char c = str.charAt( i );
-
-            // handle non-nicely printable chars and the comma
-            if ( c < 32 || c > 126 || c == '\\' || c == ',' )
-            {
-                target.append( '\\' );
-                target.append( (char) HEX_CHARS[( 0xF000 & c ) >> 12] );
-                target.append( (char) HEX_CHARS[( 0x0F00 & c ) >> 8] );
-                target.append( (char) HEX_CHARS[( 0x00F0 & c ) >> 4] );
-                target.append( (char) HEX_CHARS[( 0x000F & c )] );
-            }
-            else
-            {
-                target.append( c );
-            }
-        }
-    }
-
-    /**
-     * Reverses the effect of {@link #escapeToPrintable(StringBuilder, CharSequence)}.
-     *
-     * @param target target string buffer
-     * @param str the String to un-escape, as created by {@link #escapeToPrintable(StringBuilder, CharSequence)}
-     */
-    public static void unescapeString( StringBuilder target, CharSequence str )
-    {
-        if ( target == null )
-        {
-            throw new IllegalArgumentException( "The target buffer must not be null" );
-        }
-        if ( str == null )
-        {
-            return;
-        }
-
-        for ( int i = 0; i < str.length(); i++ )
-        {
-            char ch = str.charAt( i );
-
-            if ( ch == '\\' )
-            {
-                target.append( (char) (
-                                  digit( str.charAt( ++i ) ) << 12
-                                | digit( str.charAt( ++i ) ) << 8
-                                | digit( str.charAt( ++i ) ) << 4
-                                | digit( str.charAt( ++i ) )
-                                ) );
-            }
-            else
-            {
-                target.append( ch );
-            }
-        }
-    }
-
-    private static int digit( char ch )
-    {
-        if ( ch >= 'a' )
-        {
-            return 10 + ch - 'a';
-        }
-        else if ( ch >= 'A' )
-        {
-            return 10 + ch - 'A';
-        }
-        else
-        {
-            return ch - '0';
-        }
-    }
-
-    /**
-     * Escapes the bytes in the array {@code input} to contain only 'printable' bytes.
-     * <br>
-     * Escaping is done by encoding the non-nicely printable bytes to {@code '\' + upperCaseHexBytes(byte)}.
-     * <br>
-     * The reverse-method is {@link #unescapeBytes(String, String)}.
-     * <br>
-     * The returned byte array is started with aligned sequence {@code header} and finished by {@code \n}.
-     *
-     * @param header prefix header
-     * @param input input buffer
-     * @param off offset in the input buffer
-     * @param len number of bytes to copy from the input buffer
-     * @return number of bytes written to {@code out}
-     * @throws NullPointerException if the specified parameter {@code header} or {@code input} is null
-     * @throws IndexOutOfBoundsException if {@code off} or {@code len} is out of range
-     *         ({@code off < 0 || len < 0 || off >= input.length || len > input.length || off + len > input.length})
-     */
-    @SuppressWarnings( "checkstyle:magicnumber" )
-    public static EncodedArray escapeBytesToPrintable( final byte[] header, final byte[] input, final int off,
-                                                       final int len )
-    {
-        if ( input.length == 0 )
-        {
-            return EncodedArray.EMPTY;
-        }
-        if ( off < 0 || len < 0 || off >= input.length || len > input.length || off + len > input.length )
-        {
-            throw new IndexOutOfBoundsException(
-                    "off < 0 || len < 0 || off >= input.length || len > input.length || off + len > input.length" );
-        }
-        // Hex-escaping can be up to 3 times length of a regular byte. Last character is '\n', see (+1).
-        final byte[] encodeBytes = new byte[header.length + 3 * len + 1];
-        System.arraycopy( header, 0, encodeBytes, 0, header.length );
-        int outputPos = header.length;
-        final int end = off + len;
-        for ( int i = off; i < end; i++ )
-        {
-            final byte b = input[i];
-
-            // handle non-nicely printable bytes
-            if ( b < 32 || b > 126 || b == '\\' || b == ',' )
-            {
-                final int upper = ( 0xF0 & b ) >> 4;
-                final int lower = ( 0x0F & b );
-                encodeBytes[outputPos++] = '\\';
-                encodeBytes[outputPos++] = HEX_CHARS[upper];
-                encodeBytes[outputPos++] = HEX_CHARS[lower];
-            }
-            else
-            {
-                encodeBytes[outputPos++] = b;
-            }
-        }
-        encodeBytes[outputPos++] = (byte) '\n';
-
-        return new EncodedArray( encodeBytes, outputPos );
-    }
-
-    /**
-     * Reverses the effect of {@link #escapeBytesToPrintable(byte[], byte[], int, int)}.
-     *
-     * @param str the input String
-     * @param charsetName the charset name
-     * @return the number of bytes written to {@code out}
-     */
-    public static ByteBuffer unescapeBytes( String str, String charsetName  )
-    {
-        int outPos = 0;
-
-        if ( str == null )
-        {
-            return ByteBuffer.wrap( new byte[0] );
-        }
-
-        byte[] out = new byte[str.length()];
-        for ( int i = 0; i < str.length(); i++ )
-        {
-            char ch = str.charAt( i );
-
-            if ( ch == '\\' )
-            {
-                int upper = digit( str.charAt( ++i ) );
-                int lower = digit( str.charAt( ++i ) );
-                out[outPos++] = (byte) ( upper << 4 | lower );
-            }
-            else
-            {
-                out[outPos++] = (byte) ch;
-            }
-        }
-
-        Charset sourceCharset = Charset.forName( charsetName );
-        if ( !DEFAULT_CHARSET.equals( sourceCharset ) )
-        {
-            CharBuffer decodedFromSourceCharset;
-            try
-            {
-                decodedFromSourceCharset = sourceCharset.newDecoder().decode( ByteBuffer.wrap( out, 0, outPos ) );
-                return DEFAULT_CHARSET.encode( decodedFromSourceCharset );
-            }
-            catch ( CharacterCodingException e )
-            {
-                // ignore and fall through to the non-recoded version
-            }
-        }
-
-        return ByteBuffer.wrap( out, 0, outPos );
-    }
-
-    public static byte[] encodeStringForForkCommunication( String string )
-    {
-        return string.getBytes( ISO_8859_1 );
-    }
-
-    /**
      * Determines if {@code buffer} starts with specific literal(s).
      *
      * @param buffer     Examined StringBuffer
@@ -349,31 +130,4 @@ public final class StringUtils
             return true;
         }
     }
-
-    /**
-     * Escaped string to byte array with offset 0 and certain length.
-     */
-    public static final class EncodedArray
-    {
-        private static final EncodedArray EMPTY = new EncodedArray( new byte[]{}, 0 );
-
-        private final byte[] array;
-        private final int size;
-
-        private EncodedArray( byte[] array, int size )
-        {
-            this.array = array;
-            this.size = size;
-        }
-
-        public byte[] getArray()
-        {
-            return array;
-        }
-
-        public int getSize()
-        {
-            return size;
-        }
-    }
 }
diff --git a/surefire-api/src/test/java/org/apache/maven/JUnit4SuiteTest.java b/surefire-api/src/test/java/org/apache/maven/JUnit4SuiteTest.java
index dad6079..63c4490 100644
--- a/surefire-api/src/test/java/org/apache/maven/JUnit4SuiteTest.java
+++ b/surefire-api/src/test/java/org/apache/maven/JUnit4SuiteTest.java
@@ -23,6 +23,7 @@ import junit.framework.JUnit4TestAdapter;
 import junit.framework.Test;
 import org.apache.maven.plugin.surefire.runorder.ThreadedExecutionSchedulerTest;
 import org.apache.maven.surefire.SpecificTestClassFilterTest;
+import org.apache.maven.surefire.booter.ForkedChannelEncoderTest;
 import org.apache.maven.surefire.booter.ForkingRunListenerTest;
 import org.apache.maven.surefire.booter.MasterProcessCommandTest;
 import org.apache.maven.surefire.booter.SurefireReflectorTest;
@@ -39,7 +40,6 @@ import org.apache.maven.surefire.util.ScanResultTest;
 import org.apache.maven.surefire.util.TestsToRunTest;
 import org.apache.maven.surefire.util.internal.ConcurrencyUtilsTest;
 import org.apache.maven.surefire.util.internal.ImmutableMapTest;
-import org.apache.maven.surefire.util.internal.StringUtilsTest;
 import org.junit.runner.RunWith;
 import org.junit.runners.Suite;
 
@@ -59,7 +59,6 @@ import org.junit.runners.Suite;
     ResolvedTestTest.class,
     TestListResolverTest.class,
     ConcurrencyUtilsTest.class,
-    StringUtilsTest.class,
     DefaultDirectoryScannerTest.class,
     RunOrderCalculatorTest.class,
     RunOrderTest.class,
@@ -68,7 +67,8 @@ import org.junit.runners.Suite;
     SpecificTestClassFilterTest.class,
     FundamentalFilterTest.class,
     ImmutableMapTest.class,
-    ReflectionUtilsTest.class
+    ReflectionUtilsTest.class,
+    ForkedChannelEncoderTest.class
 } )
 @RunWith( Suite.class )
 public class JUnit4SuiteTest
diff --git a/surefire-api/src/test/java/org/apache/maven/surefire/booter/ForkedChannelEncoderTest.java b/surefire-api/src/test/java/org/apache/maven/surefire/booter/ForkedChannelEncoderTest.java
new file mode 100644
index 0000000..b9708b0
--- /dev/null
+++ b/surefire-api/src/test/java/org/apache/maven/surefire/booter/ForkedChannelEncoderTest.java
@@ -0,0 +1,1048 @@
+package org.apache.maven.surefire.booter;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.apache.maven.surefire.report.ReportEntry;
+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.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.LineNumberReader;
+import java.io.PrintStream;
+import java.io.StringReader;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.util.Map;
+
+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.booter.ForkedChannelEncoder.encode;
+import static org.apache.maven.surefire.booter.ForkedChannelEncoder.encodeHeader;
+import static org.apache.maven.surefire.booter.ForkedChannelEncoder.encodeMessage;
+import static org.apache.maven.surefire.booter.ForkedChannelEncoder.encodeOpcode;
+import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_SYSPROPS;
+import static org.apache.maven.surefire.booter.ForkedProcessEvent.MAGIC_NUMBER;
+import static org.apache.maven.surefire.report.RunMode.NORMAL_RUN;
+import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test for {@link ForkedChannelEncoder}.
+ *
+ * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
+ * @since 3.0.0-M4
+ */
+public class ForkedChannelEncoderTest
+{
+
+    @Test
+    public void shouldBeFailSafe()
+    {
+        assertThat( ForkedChannelEncoder.toBase64( null ) ).isEqualTo( "-" );
+        assertThat( ForkedChannelEncoder.toBase64( "" ) ).isEqualTo( "" );
+    }
+
+    @Test
+    public void shouldHaveSystemProperty()
+    {
+        StringBuilder actualEncoded = encode( BOOTERCODE_SYSPROPS, NORMAL_RUN, "arg1", "arg2" );
+        String expected = MAGIC_NUMBER + BOOTERCODE_SYSPROPS.getOpcode() + ":normal-run" + ":UTF-8:YXJnMQ==:YXJnMg==";
+
+        assertThat( actualEncoded.toString() )
+                .isEqualTo( expected );
+    }
+
+    @Test
+    public void safeThrowableShouldBeEncoded()
+    {
+        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\ntrace line 2";
+        final String encodedTrimmedStackTrace = encodeBase64String( toArray( UTF_8.encode( trimmedStackTrace ) ) );
+
+        SafeThrowable safeThrowable = new SafeThrowable( new Exception( 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 );
+
+        StringBuilder encoded = new StringBuilder();
+        encode( encoded, stackTraceWriter, false );
+        assertThat( encoded.toString() )
+                .isEqualTo( ":" + encodedExceptionMsg + ":" + encodedSmartStackTrace + ":" + encodedStackTrace );
+
+        encoded = new StringBuilder();
+        encode( encoded, stackTraceWriter, true );
+        assertThat( encoded.toString() )
+                .isEqualTo( ":" + encodedExceptionMsg + ":" + encodedSmartStackTrace + ":" + encodedTrimmedStackTrace );
+    }
+
+    @Test
+    public void emptySafeThrowable()
+    {
+        SafeThrowable safeThrowable = new SafeThrowable( new Exception( "" ) );
+
+        StackTraceWriter stackTraceWriter = mock( StackTraceWriter.class );
+        when( stackTraceWriter.getThrowable() ).thenReturn( safeThrowable );
+        when( stackTraceWriter.smartTrimmedStackTrace() ).thenReturn( "" );
+        when( stackTraceWriter.writeTraceToString() ).thenReturn( "" );
+
+        StringBuilder encoded = new StringBuilder();
+        encode( encoded, stackTraceWriter, false );
+
+        assertThat( encoded.toString() )
+                .isEqualTo( ":::" );
+    }
+
+    @Test
+    public void nullSafeThrowable()
+    {
+        SafeThrowable safeThrowable = new SafeThrowable( new Exception() );
+
+        StackTraceWriter stackTraceWriter = mock( StackTraceWriter.class );
+        when( stackTraceWriter.getThrowable() ).thenReturn( safeThrowable );
+
+        StringBuilder encoded = new StringBuilder();
+        encode( encoded, stackTraceWriter, false );
+
+        assertThat( encoded.toString() )
+                .isEqualTo( ":-:-:-" );
+    }
+
+    @Test
+    public void reportEntry() throws IOException
+    {
+        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\ntrace line 2";
+        final String encodedTrimmedStackTrace = encodeBase64String( toArray( UTF_8.encode( trimmedStackTrace ) ) );
+
+        SafeThrowable safeThrowable = new SafeThrowable( new Exception( 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.getNameWithGroup() ).thenReturn( "name with group" );
+        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() ) ) );
+
+        StringBuilder encode = encode( "X", "normal-run", reportEntry, false );
+        assertThat( encode.toString() )
+                .isEqualTo( ":maven:surefire:std:out:X:normal-run:UTF-8:"
+                                    + encodedSourceName
+                                    + ":"
+                                    + encodedName
+                                    + ":"
+                                    + encodedGroup
+                                    + ":"
+                                    + encodedMessage
+                                    + ":"
+                                    + 102
+                                    + ":"
+
+                                    + encodedExceptionMsg
+                                    + ":"
+                                    + encodedSmartStackTrace
+                                    + ":"
+                                    + encodedStackTrace
+                );
+
+        encode = encode( "X", "normal-run", reportEntry, true );
+        assertThat( encode.toString() )
+                .isEqualTo( ":maven:surefire:std:out:X:normal-run:UTF-8:"
+                                    + encodedSourceName
+                                    + ":"
+                                    + encodedName
+                                    + ":"
+                                    + encodedGroup
+                                    + ":"
+                                    + encodedMessage
+                                    + ":"
+                                    + 102
+                                    + ":"
+
+                                    + encodedExceptionMsg
+                                    + ":"
+                                    + encodedSmartStackTrace
+                                    + ":"
+                                    + encodedTrimmedStackTrace
+                );
+
+        Stream out = Stream.newStream();
+        ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
+
+        forkedChannelEncoder.testSetStarting( reportEntry, true );
+        LineNumberReader printedLines = out.newReader( UTF_8 );
+        assertThat( printedLines.readLine() )
+                .isEqualTo( ":maven:surefire:std:out:testset-starting:normal-run:UTF-8:"
+                                    + encodedSourceName
+                                    + ":"
+                                    + encodedName
+                                    + ":"
+                                    + encodedGroup
+                                    + ":"
+                                    + encodedMessage
+                                    + ":"
+                                    + 102
+                                    + ":"
+
+                                    + encodedExceptionMsg
+                                    + ":"
+                                    + encodedSmartStackTrace
+                                    + ":"
+                                    + encodedTrimmedStackTrace
+                );
+        assertThat( printedLines.readLine() ).isNull();
+
+        out = Stream.newStream();
+        forkedChannelEncoder = new ForkedChannelEncoder( out );
+
+        forkedChannelEncoder.testSetStarting( reportEntry, false );
+        printedLines = out.newReader( UTF_8 );
+        assertThat( printedLines.readLine() )
+                .isEqualTo( ":maven:surefire:std:out:testset-starting:normal-run:UTF-8:"
+                                    + encodedSourceName
+                                    + ":"
+                                    + encodedName
+                                    + ":"
+                                    + encodedGroup
+                                    + ":"
+                                    + encodedMessage
+                                    + ":"
+                                    + 102
+                                    + ":"
+
+                                    + encodedExceptionMsg
+                                    + ":"
+                                    + encodedSmartStackTrace
+                                    + ":"
+                                    + encodedStackTrace
+                );
+        assertThat( printedLines.readLine() ).isNull();
+    }
+
+    @Test
+    public void testSetCompleted() throws IOException
+    {
+        String exceptionMessage = "msg";
+        String encodedExceptionMsg = encodeBase64String( toArray( UTF_8.encode( exceptionMessage ) ) );
+
+        String smartStackTrace = "MyTest:86 >> Error";
+        String encodedSmartStackTrace = encodeBase64String( toArray( UTF_8.encode( smartStackTrace ) ) );
+
+        String stackTrace = "trace line 1\ntrace line 2";
+        String encodedStackTrace = encodeBase64String( toArray( UTF_8.encode( stackTrace ) ) );
+
+        String trimmedStackTrace = "trace line 1\ntrace line 2";
+        String encodedTrimmedStackTrace = encodeBase64String( toArray( UTF_8.encode( trimmedStackTrace ) ) );
+
+        SafeThrowable safeThrowable = new SafeThrowable( new Exception( 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.getNameWithGroup() ).thenReturn( "name with group" );
+        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() ) ) );
+
+        Stream out = Stream.newStream();
+        ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
+
+        forkedChannelEncoder.testSetCompleted( reportEntry, false );
+        LineNumberReader printedLines = out.newReader( UTF_8 );
+        assertThat( printedLines.readLine() )
+                .isEqualTo( ":maven:surefire:std:out:testset-completed:normal-run:UTF-8:"
+                        + encodedSourceName
+                        + ":"
+                        + encodedName
+                        + ":"
+                        + encodedGroup
+                        + ":"
+                        + encodedMessage
+                        + ":"
+                        + 102
+                        + ":"
+
+                        + encodedExceptionMsg
+                        + ":"
+                        + encodedSmartStackTrace
+                        + ":"
+                        + encodedStackTrace
+                );
+        assertThat( printedLines.readLine() ).isNull();
+    }
+
+    @Test
+    public void testStarting() throws IOException
+    {
+        String exceptionMessage = "msg";
+        String encodedExceptionMsg = encodeBase64String( toArray( UTF_8.encode( exceptionMessage ) ) );
+
+        String smartStackTrace = "MyTest:86 >> Error";
+        String encodedSmartStackTrace = encodeBase64String( toArray( UTF_8.encode( smartStackTrace ) ) );
+
+        String stackTrace = "trace line 1\ntrace line 2";
+        String encodedStackTrace = encodeBase64String( toArray( UTF_8.encode( stackTrace ) ) );
+
+        String trimmedStackTrace = "trace line 1\ntrace line 2";
+        String encodedTrimmedStackTrace = encodeBase64String( toArray( UTF_8.encode( trimmedStackTrace ) ) );
+
+        SafeThrowable safeThrowable = new SafeThrowable( new Exception( 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.getNameWithGroup() ).thenReturn( "name with group" );
+        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() ) ) );
+
+        Stream out = Stream.newStream();
+        ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
+
+        forkedChannelEncoder.testStarting( reportEntry, true );
+        LineNumberReader printedLines = out.newReader( UTF_8 );
+        assertThat( printedLines.readLine() )
+                .isEqualTo( ":maven:surefire:std:out:test-starting:normal-run:UTF-8:"
+                        + encodedSourceName
+                        + ":"
+                        + encodedName
+                        + ":"
+                        + encodedGroup
+                        + ":"
+                        + encodedMessage
+                        + ":"
+                        + 102
+                        + ":"
+
+                        + encodedExceptionMsg
+                        + ":"
+                        + encodedSmartStackTrace
+                        + ":"
+                        + encodedTrimmedStackTrace
+                );
+        assertThat( printedLines.readLine() ).isNull();
+    }
+
+    @Test
+    public void testSuccess() throws IOException
+    {
+        String exceptionMessage = "msg";
+        String encodedExceptionMsg = encodeBase64String( toArray( UTF_8.encode( exceptionMessage ) ) );
+
+        String smartStackTrace = "MyTest:86 >> Error";
+        String encodedSmartStackTrace = encodeBase64String( toArray( UTF_8.encode( smartStackTrace ) ) );
+
+        String stackTrace = "trace line 1\ntrace line 2";
+        String encodedStackTrace = encodeBase64String( toArray( UTF_8.encode( stackTrace ) ) );
+
+        String trimmedStackTrace = "trace line 1\ntrace line 2";
+        String encodedTrimmedStackTrace = encodeBase64String( toArray( UTF_8.encode( trimmedStackTrace ) ) );
+
+        SafeThrowable safeThrowable = new SafeThrowable( new Exception( 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.getNameWithGroup() ).thenReturn( "name with group" );
+        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() ) ) );
+
+        Stream out = Stream.newStream();
+        ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
+
+        forkedChannelEncoder.testSucceeded( reportEntry, true );
+        LineNumberReader printedLines = out.newReader( UTF_8 );
+        assertThat( printedLines.readLine() )
+                .isEqualTo( ":maven:surefire:std:out:test-succeeded:normal-run:UTF-8:"
+                        + encodedSourceName
+                        + ":"
+                        + encodedName
+                        + ":"
+                        + encodedGroup
+                        + ":"
+                        + encodedMessage
+                        + ":"
+                        + 102
+                        + ":"
+
+                        + encodedExceptionMsg
+                        + ":"
+                        + encodedSmartStackTrace
+                        + ":"
+                        + encodedTrimmedStackTrace
+                );
+        assertThat( printedLines.readLine() ).isNull();
+    }
+
+    @Test
+    public void testFailed() throws IOException
+    {
+        String exceptionMessage = "msg";
+        String encodedExceptionMsg = encodeBase64String( toArray( UTF_8.encode( exceptionMessage ) ) );
+
+        String smartStackTrace = "MyTest:86 >> Error";
+        String encodedSmartStackTrace = encodeBase64String( toArray( UTF_8.encode( smartStackTrace ) ) );
+
+        String stackTrace = "trace line 1\ntrace line 2";
+        String encodedStackTrace = encodeBase64String( toArray( UTF_8.encode( stackTrace ) ) );
+
+        String trimmedStackTrace = "trace line 1\ntrace line 2";
+        String encodedTrimmedStackTrace = encodeBase64String( toArray( UTF_8.encode( trimmedStackTrace ) ) );
+
+        SafeThrowable safeThrowable = new SafeThrowable( new Exception( 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.getNameWithGroup() ).thenReturn( "name with group" );
+        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() ) ) );
+
+        Stream out = Stream.newStream();
+        ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
+
+        forkedChannelEncoder.testFailed( reportEntry, false );
+        LineNumberReader printedLines = out.newReader( UTF_8 );
+        assertThat( printedLines.readLine() )
+                .isEqualTo( ":maven:surefire:std:out:test-failed:normal-run:UTF-8:"
+                        + encodedSourceName
+                        + ":"
+                        + encodedName
+                        + ":"
+                        + encodedGroup
+                        + ":"
+                        + encodedMessage
+                        + ":"
+                        + 102
+                        + ":"
+
+                        + encodedExceptionMsg
+                        + ":"
+                        + encodedSmartStackTrace
+                        + ":"
+                        + encodedStackTrace
+                );
+        assertThat( printedLines.readLine() ).isNull();
+    }
+
+    @Test
+    public void testSkipped() throws IOException
+    {
+        String encodedExceptionMsg = "-";
+
+        String smartStackTrace = "MyTest:86 >> Error";
+        String encodedSmartStackTrace = encodeBase64String( toArray( UTF_8.encode( smartStackTrace ) ) );
+
+        String stackTrace = "trace line 1\ntrace line 2";
+        String encodedStackTrace = encodeBase64String( toArray( UTF_8.encode( stackTrace ) ) );
+
+        String trimmedStackTrace = "trace line 1\ntrace line 2";
+        String encodedTrimmedStackTrace = encodeBase64String( toArray( UTF_8.encode( trimmedStackTrace ) ) );
+
+        SafeThrowable safeThrowable = new SafeThrowable( new Exception() );
+        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.getNameWithGroup() ).thenReturn( "name with group" );
+        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() ) ) );
+
+        Stream out = Stream.newStream();
+        ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
+
+        forkedChannelEncoder.testSkipped( reportEntry, false );
+        LineNumberReader printedLines = out.newReader( UTF_8 );
+        assertThat( printedLines.readLine() )
+                .isEqualTo( ":maven:surefire:std:out:test-skipped:normal-run:UTF-8:"
+                        + encodedSourceName
+                        + ":"
+                        + encodedName
+                        + ":"
+                        + encodedGroup
+                        + ":"
+                        + encodedMessage
+                        + ":"
+                        + 102
+                        + ":"
+
+                        + encodedExceptionMsg
+                        + ":"
+                        + encodedSmartStackTrace
+                        + ":"
+                        + encodedStackTrace
+                );
+        assertThat( printedLines.readLine() ).isNull();
+    }
+
+    @Test
+    public void testError() throws IOException
+    {
+        String encodedExceptionMsg = "-";
+
+        String encodedSmartStackTrace = "-";
+
+        String stackTrace = "trace line 1\ntrace line 2";
+        String encodedStackTrace = encodeBase64String( toArray( UTF_8.encode( stackTrace ) ) );
+
+        String trimmedStackTrace = "trace line 1\ntrace line 2";
+        String encodedTrimmedStackTrace = encodeBase64String( toArray( UTF_8.encode( trimmedStackTrace ) ) );
+
+        SafeThrowable safeThrowable = new SafeThrowable( new Exception() );
+        StackTraceWriter stackTraceWriter = mock( StackTraceWriter.class );
+        when( stackTraceWriter.getThrowable() ).thenReturn( safeThrowable );
+        when( stackTraceWriter.smartTrimmedStackTrace() ).thenReturn( null );
+        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.getNameWithGroup() ).thenReturn( "name with group" );
+        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() ) ) );
+
+        Stream out = Stream.newStream();
+        ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
+
+        forkedChannelEncoder.testError( reportEntry, false );
+        LineNumberReader printedLines = out.newReader( UTF_8 );
+        assertThat( printedLines.readLine() )
+                .isEqualTo( ":maven:surefire:std:out:test-error:normal-run:UTF-8:"
+                        + encodedSourceName
+                        + ":"
+                        + encodedName
+                        + ":"
+                        + encodedGroup
+                        + ":"
+                        + encodedMessage
+                        + ":"
+                        + 102
+                        + ":"
+
+                        + encodedExceptionMsg
+                        + ":"
+                        + encodedSmartStackTrace
+                        + ":"
+                        + encodedStackTrace
+                );
+        assertThat( printedLines.readLine() ).isNull();
+    }
+
+    @Test
+    public void testAssumptionFailure() throws IOException
+    {
+        String exceptionMessage = "msg";
+        String encodedExceptionMsg = encodeBase64String( toArray( UTF_8.encode( exceptionMessage ) ) );
+
+        String smartStackTrace = "MyTest:86 >> Error";
+        String encodedSmartStackTrace = encodeBase64String( toArray( UTF_8.encode( smartStackTrace ) ) );
+
+        String encodedStackTrace = "-";
+
+        SafeThrowable safeThrowable = new SafeThrowable( new Exception( exceptionMessage ) );
+        StackTraceWriter stackTraceWriter = mock( StackTraceWriter.class );
+        when( stackTraceWriter.getThrowable() ).thenReturn( safeThrowable );
+        when( stackTraceWriter.smartTrimmedStackTrace() ).thenReturn( smartStackTrace );
+        when( stackTraceWriter.writeTrimmedTraceToString() ).thenReturn( null );
+        when( stackTraceWriter.writeTraceToString() ).thenReturn( null );
+
+        ReportEntry reportEntry = mock( ReportEntry.class );
+        when( reportEntry.getElapsed() ).thenReturn( null );
+        when( reportEntry.getGroup() ).thenReturn( "this group" );
+        when( reportEntry.getMessage() ).thenReturn( "skipped test" );
+        when( reportEntry.getName() ).thenReturn( "my test" );
+        when( reportEntry.getNameWithGroup() ).thenReturn( "name with group" );
+        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() ) ) );
+
+        Stream out = Stream.newStream();
+        ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
+
+        forkedChannelEncoder.testAssumptionFailure( reportEntry, false );
+        LineNumberReader printedLines = out.newReader( UTF_8 );
+        assertThat( printedLines.readLine() )
+                .isEqualTo( ":maven:surefire:std:out:test-assumption-failure:normal-run:UTF-8:"
+                        + encodedSourceName
+                        + ":"
+                        + encodedName
+                        + ":"
+                        + encodedGroup
+                        + ":"
+                        + encodedMessage
+                        + ":"
+                        + "-"
+                        + ":"
+
+                        + encodedExceptionMsg
+                        + ":"
+                        + encodedSmartStackTrace
+                        + ":"
+                        + encodedStackTrace
+                );
+        assertThat( printedLines.readLine() ).isNull();
+    }
+
+    @Test
+    public void testBye() throws IOException
+    {
+        Stream out = Stream.newStream();
+        ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
+
+        forkedChannelEncoder.bye();
+        LineNumberReader printedLines = out.newReader( UTF_8 );
+        assertThat( printedLines.readLine() )
+                .isEqualTo( ":maven:surefire:std:out:bye" );
+        assertThat( printedLines.readLine() ).isNull();
+    }
+
+    @Test
+    public void testStopOnNextTest() throws IOException
+    {
+        Stream out = Stream.newStream();
+        ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
+
+        forkedChannelEncoder.stopOnNextTest();
+        LineNumberReader printedLines = out.newReader( UTF_8 );
+        assertThat( printedLines.readLine() )
+                .isEqualTo( ":maven:surefire:std:out:stop-on-next-test");
+        assertThat( printedLines.readLine() ).isNull();
+    }
+
+    @Test
+    public void testAcquireNextTest() throws IOException
+    {
+        Stream out = Stream.newStream();
+        ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
+
+        forkedChannelEncoder.acquireNextTest();
+        LineNumberReader printedLines = out.newReader( UTF_8 );
+        assertThat( printedLines.readLine() )
+                .isEqualTo( ":maven:surefire:std:out:next-test" );
+        assertThat( printedLines.readLine() ).isNull();
+    }
+
+    @Test
+    public void testSendOpcode()
+    {
+        StringBuilder encoded = encodeOpcode( "some-opcode", "normal-run" );
+        assertThat( encoded.toString() )
+                .isEqualTo( ":maven:surefire:std:out:some-opcode:normal-run" );
+
+        encoded = encodeHeader( "some-opcode", "normal-run" );
+        assertThat( encoded.toString() )
+                .isEqualTo( ":maven:surefire:std:out:some-opcode:normal-run:UTF-8" );
+
+        encoded = encodeMessage( "some-opcode", "normal-run", "msg" );
+        assertThat( encoded.toString() )
+                .isEqualTo( ":maven:surefire:std:out:some-opcode:normal-run:UTF-8:msg" );
+
+        Stream out = Stream.newStream();
+        ForkedChannelEncoder encoder = new ForkedChannelEncoder( out );
+        encoded = encoder.print( "some-opcode", "msg" );
+        assertThat( encoded.toString() )
+                .isEqualTo( ":maven:surefire:std:out:some-opcode:UTF-8:bXNn" );
+
+        encoded = encoder.print( "some-opcode", new String[] { null } );
+        assertThat( encoded.toString() )
+                .isEqualTo( ":maven:surefire:std:out:some-opcode:UTF-8:-" );
+    }
+
+    @Test
+    public void testConsoleInfo()
+    {
+        Stream out = Stream.newStream();
+        ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
+
+        forkedChannelEncoder.consoleInfoLog( "msg" );
+
+        String encoded = new String( out.toByteArray(), UTF_8 );
+
+        String expected = ":maven:surefire:std:out:console-info-log:UTF-8:"
+                                  + encodeBase64String( toArray( UTF_8.encode( "msg" ) ) )
+                                  + "\n";
+
+        assertThat( encoded )
+                .isEqualTo( expected );
+    }
+
+    @Test
+    public void testConsoleError()
+    {
+        Stream out = Stream.newStream();
+        ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
+
+        forkedChannelEncoder.consoleErrorLog( "msg" );
+
+        String encoded = new String( out.toByteArray(), UTF_8 );
+
+        String expected = ":maven:surefire:std:out:console-error-log:UTF-8:"
+                + encodeBase64String( toArray( UTF_8.encode( "msg" ) ) )
+                + "\n";
+
+        assertThat( encoded )
+                .isEqualTo( expected );
+    }
+
+    @Test
+    public void testConsoleErrorLog1() throws IOException
+    {
+        Stream out = Stream.newStream();
+        ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
+
+        forkedChannelEncoder.consoleErrorLog( new Exception( "msg" ) );
+        LineNumberReader printedLines = out.newReader( UTF_8 );
+        assertThat( printedLines.readLine() )
+                .startsWith( ":maven:surefire:std:out:console-error-log:UTF-8:bXNn:-:" );
+        assertThat( printedLines.readLine() ).isNull();
+    }
+
+    @Test
+    public void testConsoleErrorLog2() throws IOException
+    {
+        Stream out = Stream.newStream();
+        ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
+
+        forkedChannelEncoder.consoleErrorLog( "msg2", new Exception( "msg" ) );
+        LineNumberReader printedLines = out.newReader( UTF_8 );
+        assertThat( printedLines.readLine() )
+                .startsWith( ":maven:surefire:std:out:console-error-log:UTF-8:bXNnMg==:-:" );
+        assertThat( printedLines.readLine() ).isNull();
+    }
+
+    @Test
+    public void testConsoleErrorLog3() 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.consoleErrorLog( stackTraceWriter, true );
+        LineNumberReader printedLines = out.newReader( UTF_8 );
+        assertThat( printedLines.readLine() )
+                .startsWith( ":maven:surefire:std:out:console-error-log:UTF-8:MQ==:Mg==:NA==" );
+        assertThat( printedLines.readLine() ).isNull();
+    }
+
+    @Test
+    public void testConsoleDebug()
+    {
+        Stream out = Stream.newStream();
+        ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
+
+        forkedChannelEncoder.consoleDebugLog( "msg" );
+
+        String encoded = new String( out.toByteArray(), UTF_8 );
+
+        String expected = ":maven:surefire:std:out:console-debug-log:UTF-8:"
+                                  + encodeBase64String( toArray( UTF_8.encode( "msg" ) ) )
+                                  + "\n";
+
+        assertThat( encoded )
+                .isEqualTo( expected );
+    }
+
+    @Test
+    public void testConsoleWarning()
+    {
+        Stream out = Stream.newStream();
+        ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
+
+        forkedChannelEncoder.consoleWarningLog( "msg" );
+
+        String encoded = new String( out.toByteArray(), UTF_8 );
+
+        String expected = ":maven:surefire:std:out:console-warning-log:UTF-8:"
+                                  + encodeBase64String( toArray( UTF_8.encode( "msg" ) ) )
+                                  + "\n";
+
+        assertThat( encoded )
+                .isEqualTo( expected );
+    }
+
+    @Test
+    public void testStdOutStream() throws IOException
+    {
+        Stream out = Stream.newStream();
+        ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
+
+        forkedChannelEncoder.stdOut( "msg", false );
+
+        String expected = ":maven:surefire:std:out:std-out-stream:normal-run:UTF-8:bXNn";
+
+        LineNumberReader printedLines = out.newReader( UTF_8 );
+        assertThat( printedLines.readLine() )
+                .isEqualTo( expected );
+        assertThat( printedLines.readLine() )
+                .isNull();
+    }
+
+    @Test
+    public void testStdOutStreamLn() throws IOException
+    {
+        Stream out = Stream.newStream();
+        ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
+
+        forkedChannelEncoder.stdOut( "msg", true );
+
+        String expected = ":maven:surefire:std:out:std-out-stream-new-line:normal-run:UTF-8:bXNn";
+
+        LineNumberReader printedLines = out.newReader( UTF_8 );
+        assertThat( printedLines.readLine() )
+                .isEqualTo( expected );
+        assertThat( printedLines.readLine() )
+                .isNull();
+    }
+
+    @Test
+    public void testStdErrStream() throws IOException
+    {
+        Stream out = Stream.newStream();
+        ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
+
+        forkedChannelEncoder.stdErr( "msg", false );
+
+        String expected = ":maven:surefire:std:out:std-err-stream:normal-run:UTF-8:bXNn";
+
+        LineNumberReader printedLines = out.newReader( UTF_8 );
+        assertThat( printedLines.readLine() )
+                .isEqualTo( expected );
+        assertThat( printedLines.readLine() )
+                .isNull();
+    }
+
+    @Test
+    public void testStdErrStreamLn() throws IOException
+    {
+        Stream out = Stream.newStream();
+        ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
+
+        forkedChannelEncoder.stdErr( "msg", true );
+
+        String expected = ":maven:surefire:std:out:std-err-stream-new-line:normal-run:UTF-8:bXNn";
+
+        LineNumberReader printedLines = out.newReader( UTF_8 );
+        assertThat( printedLines.readLine() )
+                .isEqualTo( expected );
+        assertThat( printedLines.readLine() )
+                .isNull();
+    }
+
+    @Test
+    public void shouldCountSameNumberOfSystemProperties() throws IOException
+    {
+        Stream out = Stream.newStream();
+        ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( out );
+
+        Map<String, String> sysProps = ObjectUtils.systemProps();
+        int expectedSize = sysProps.size();
+        forkedChannelEncoder.sendSystemProperties( sysProps );
+
+        LineNumberReader printedLines = out.newReader( UTF_8 );
+
+        int size = 0;
+        for ( String line; ( line = printedLines.readLine() ) != null; size++ )
+        {
+            assertThat( line )
+                    .startsWith( ":maven:surefire:std:out:sys-prop:normal-run:UTF-8:" );
+        }
+
+        assertThat( size )
+                .isEqualTo( expectedSize );
+    }
+
+    @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 printedLines = out.newReader( UTF_8 );
+        assertThat( printedLines.readLine() )
+                .startsWith( ":maven:surefire:std:out:jvm-exit-error:UTF-8:MQ==:Mg==:Mw==" );
+    }
+
+    @Test
+    public void shouldHandleExitWithTrimmedTrace() 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, true );
+
+        LineNumberReader printedLines = out.newReader( UTF_8 );
+        assertThat( printedLines.readLine() )
+                .startsWith( ":maven:surefire:std:out:jvm-exit-error:UTF-8:MQ==:Mg==:NA==" );
+    }
+
+    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() );
+    }
+
+}
diff --git a/surefire-api/src/test/java/org/apache/maven/surefire/booter/ForkingRunListenerTest.java b/surefire-api/src/test/java/org/apache/maven/surefire/booter/ForkingRunListenerTest.java
index b71eda2..b56b98b 100644
--- a/surefire-api/src/test/java/org/apache/maven/surefire/booter/ForkingRunListenerTest.java
+++ b/surefire-api/src/test/java/org/apache/maven/surefire/booter/ForkingRunListenerTest.java
@@ -30,11 +30,10 @@ public class ForkingRunListenerTest
     extends TestCase
 {
     public void testInfo()
-        throws Exception
     {
         ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
         PrintStream target = new PrintStream( byteArrayOutputStream );
-        ForkingRunListener forkingRunListener = new ForkingRunListener( target, 1, true );
+        ForkingRunListener forkingRunListener = new ForkingRunListener( new ForkedChannelEncoder( target ), true );
         forkingRunListener.info( new String( new byte[]{ 65 } ) );
         forkingRunListener.info( new String( new byte[]{ } ) );
 
diff --git a/surefire-api/src/test/java/org/apache/maven/surefire/util/internal/StringUtilsTest.java b/surefire-api/src/test/java/org/apache/maven/surefire/util/internal/StringUtilsTest.java
deleted file mode 100644
index e686086..0000000
--- a/surefire-api/src/test/java/org/apache/maven/surefire/util/internal/StringUtilsTest.java
+++ /dev/null
@@ -1,142 +0,0 @@
-package org.apache.maven.surefire.util.internal;
-
-/*
- * 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 java.nio.ByteBuffer;
-import java.nio.charset.Charset;
-
-import junit.framework.TestCase;
-import org.apache.maven.surefire.util.internal.StringUtils.EncodedArray;
-
-import static org.junit.Assert.assertArrayEquals;
-
-/**
- * @author Andreas Gudian
- */
-public class StringUtilsTest
-    extends TestCase
-{
-
-    public void testUnescapeString()
-    {
-        CharSequence inputString = createInputString();
-
-        StringBuilder escaped = new StringBuilder( inputString.length() * 5 );
-        int initialCapacity = escaped.capacity();
-
-        StringUtils.escapeToPrintable( escaped, inputString );
-
-        assertEquals( initialCapacity, escaped.capacity() );
-
-        StringBuilder unescaped = new StringBuilder( inputString.length() );
-        StringUtils.unescapeString( unescaped, escaped );
-
-        assertEquals( inputString.length(), unescaped.length() );
-
-        for ( int i = 0; i < inputString.length(); i++ )
-        {
-            if ( inputString.charAt( i ) != unescaped.charAt( i ) )
-            {
-                fail( "Input and Unescaped String are not equal at position " + i );
-            }
-        }
-    }
-
-    private CharSequence createInputString()
-    {
-        StringBuilder sb = new StringBuilder();
-        for ( int i = 0; i < Character.MAX_CODE_POINT; i++ )
-        {
-            sb.appendCodePoint( i );
-        }
-
-        return sb;
-    }
-
-    public void testUnescapeBytes()
-    {
-        byte[] input = new byte[256];
-
-        for ( int i = 0; i <= 0xFF; i++ )
-        {
-            byte b = (byte) ( 0xFF & i );
-            input[i] = b;
-        }
-
-        EncodedArray encodedArray = StringUtils.escapeBytesToPrintable( new byte[0], input, 0, input.length );
-
-        String escapedString = new String( encodedArray.getArray(), 0, encodedArray.getSize() );
-
-        assertEquals( encodedArray.getSize(), escapedString.length() );
-
-        ByteBuffer unescaped = StringUtils.unescapeBytes( escapedString, Charset.defaultCharset().name() );
-
-        assertEquals( input.length + 1, unescaped.remaining() - unescaped.position() );
-
-        for ( int i = 0; i < input.length; i++ )
-        {
-            assertEquals( "At position " + i, input[i], unescaped.get() );
-        }
-    }
-
-    public void testEscapeWithHeader()
-    {
-        byte[] header = { (byte) 'a' };
-        byte[] input = { (byte) '1' };
-
-        EncodedArray encodedArray = StringUtils.escapeBytesToPrintable( header, input, 0, input.length );
-        assertEquals( 3, encodedArray.getSize() );
-
-        byte[] expectedResult = new byte[] { (byte) 'a', (byte) '1', (byte) '\n' };
-        byte[] actualResult = new byte[encodedArray.getSize()];
-        System.arraycopy( encodedArray.getArray(), 0, actualResult, 0, encodedArray.getSize() );
-
-        assertArrayEquals( expectedResult, actualResult );
-    }
-
-    public void testEmptyByteArray()
-    {
-        byte[] header = { (byte) 'a' };
-        byte[] input = {};
-        EncodedArray encodedArray = StringUtils.escapeBytesToPrintable( header, input, 0, input.length );
-        assertEquals( 0, encodedArray.getSize() );
-        assertEquals( 0, encodedArray.getArray().length );
-    }
-
-    public void testSubstringSmall()
-    {
-        byte[] header = { (byte) 'a' };
-        byte[] input = "PleaseLookAfterThisBear".getBytes();
-        EncodedArray encodedArray = StringUtils.escapeBytesToPrintable( header, input,
-                "Please".length(), "Look".length() );
-        assertEquals( "Look",
-                new String( encodedArray.getArray(), 1, encodedArray.getArray().length-1).trim() );
-    }
-
-    public void testSubstringLarge()
-    {
-        byte[] header = { (byte) 'a' };
-        byte[] input = "TheQuickBrownFoxJumpsOverTheLazyDog".getBytes();
-        EncodedArray encodedArray = StringUtils.escapeBytesToPrintable( header, input,
-                "The".length(), "QuickBrownFoxJumpsOverTheLazy".length() );
-        assertEquals( "QuickBrownFoxJumpsOverTheLazy",
-                new String( encodedArray.getArray(), 1, encodedArray.getArray().length-1).trim() );
-    }
-}
diff --git a/surefire-booter/pom.xml b/surefire-booter/pom.xml
index a03cda1..74cd93c 100644
--- a/surefire-booter/pom.xml
+++ b/surefire-booter/pom.xml
@@ -62,6 +62,11 @@
       <scope>provided</scope>
     </dependency>
     <dependency>
+      <groupId>commons-codec</groupId>
+      <artifactId>commons-codec</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
       <groupId>org.mockito</groupId>
       <artifactId>mockito-core</artifactId>
       <scope>test</scope>
diff --git a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ForkedBooter.java b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ForkedBooter.java
index 580132a..bb3e876 100644
--- a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ForkedBooter.java
+++ b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/ForkedBooter.java
@@ -22,8 +22,6 @@ package org.apache.maven.surefire.booter;
 import org.apache.maven.surefire.providerapi.ProviderParameters;
 import org.apache.maven.surefire.providerapi.SurefireProvider;
 import org.apache.maven.surefire.report.LegacyPojoStackTraceWriter;
-import org.apache.maven.surefire.report.StackTraceWriter;
-import org.apache.maven.surefire.suite.RunResult;
 import org.apache.maven.surefire.testset.TestSetFailedException;
 
 import java.io.File;
@@ -31,7 +29,6 @@ import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.PrintStream;
 import java.lang.management.ManagementFactory;
 import java.lang.reflect.InvocationTargetException;
 import java.security.AccessControlException;
@@ -47,13 +44,9 @@ import static java.lang.Math.max;
 import static java.lang.Thread.currentThread;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
-import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_BYE;
-import static org.apache.maven.surefire.booter.ForkingRunListener.BOOTERCODE_ERROR;
-import static org.apache.maven.surefire.booter.ForkingRunListener.encode;
 import static org.apache.maven.surefire.booter.SystemPropertyManager.setSystemProperties;
 import static org.apache.maven.surefire.util.ReflectionUtils.instantiateOneArg;
 import static org.apache.maven.surefire.util.internal.DaemonThreadFactory.newDaemonThreadFactory;
-import static org.apache.maven.surefire.util.internal.StringUtils.encodeStringForForkCommunication;
 
 /**
  * The part of the booter that is unique to a forked vm.
@@ -74,7 +67,7 @@ public final class ForkedBooter
     private static final String PING_THREAD = "surefire-forkedjvm-ping-";
 
     private final CommandReader commandReader = CommandReader.getReader();
-    private final PrintStream originalOut = System.out;
+    private final ForkedChannelEncoder eventChannel = new ForkedChannelEncoder( System.out );
 
     private volatile long systemExitTimeoutInSeconds = DEFAULT_SYSTEM_EXIT_TIMEOUT_IN_SECONDS;
     private volatile PingScheduler pingScheduler;
@@ -127,22 +120,16 @@ public final class ForkedBooter
         }
         catch ( InvocationTargetException t )
         {
-            DumpErrorSingleton.getSingleton().dumpException( t );
-            StackTraceWriter stackTraceWriter =
-                    new LegacyPojoStackTraceWriter( "test subsystem", "no method", t.getTargetException() );
-            StringBuilder stringBuilder = new StringBuilder();
-            encode( stringBuilder, stackTraceWriter, false );
-            encodeAndWriteToOutput( ( (char) BOOTERCODE_ERROR ) + ",0," + stringBuilder + "\n" );
+            Throwable e = t.getTargetException();
+            DumpErrorSingleton.getSingleton().dumpException( e );
+            eventChannel.consoleErrorLog( new LegacyPojoStackTraceWriter( "test subsystem", "no method", e ), false );
         }
         catch ( Throwable t )
         {
             DumpErrorSingleton.getSingleton().dumpException( t );
-            StackTraceWriter stackTraceWriter = new LegacyPojoStackTraceWriter( "test subsystem", "no method", t );
-            StringBuilder stringBuilder = new StringBuilder();
-            encode( stringBuilder, stackTraceWriter, false );
-            encodeAndWriteToOutput( ( (char) BOOTERCODE_ERROR ) + ",0," + stringBuilder + "\n" );
+            eventChannel.consoleErrorLog( new LegacyPojoStackTraceWriter( "test subsystem", "no method", t ), false );
         }
-        acknowledgedExit();
+        acknowledgedExit( eventChannel );
     }
 
     private Object createTestSet( TypeEncodedValue forkedTestSet, boolean readTestsFromCommandReader, ClassLoader cl )
@@ -153,7 +140,7 @@ public final class ForkedBooter
         }
         else if ( readTestsFromCommandReader )
         {
-            return new LazyTestsToRun( originalOut );
+            return new LazyTestsToRun( eventChannel );
         }
         return null;
     }
@@ -260,7 +247,7 @@ public final class ForkedBooter
                     cancelPingScheduler();
                     DumpErrorSingleton.getSingleton()
                             .dumpText( "Exiting self fork JVM. Received SHUTDOWN command from Maven shutdown hook." );
-                    exit( 1 );
+                    exit1();
                 }
                 // else refers to shutdown=testset, but not used now, keeping reader open
             }
@@ -289,17 +276,6 @@ public final class ForkedBooter
         };
     }
 
-    private void encodeAndWriteToOutput( String string )
-    {
-        byte[] encodeBytes = encodeStringForForkCommunication( string );
-        //noinspection SynchronizationOnLocalVariableOrMethodParameter
-        synchronized ( originalOut )
-        {
-            originalOut.write( encodeBytes, 0, encodeBytes.length );
-            originalOut.flush();
-        }
-    }
-
     private void kill()
     {
         kill( 1 );
@@ -311,13 +287,13 @@ public final class ForkedBooter
         Runtime.getRuntime().halt( returnCode );
     }
 
-    private void exit( int returnCode )
+    private void exit1()
     {
-        launchLastDitchDaemonShutdownThread( returnCode );
-        System.exit( returnCode );
+        launchLastDitchDaemonShutdownThread( 1 );
+        System.exit( 1 );
     }
 
-    private void acknowledgedExit()
+    private void acknowledgedExit( ForkedChannelEncoder eventChannel )
     {
         final Semaphore barrier = new Semaphore( 0 );
         commandReader.addByeAckListener( new CommandListener()
@@ -329,7 +305,7 @@ public final class ForkedBooter
                                               }
                                           }
         );
-        encodeAndWriteToOutput( ( (char) BOOTERCODE_BYE ) + ",0,BYE!\n" );
+        eventChannel.bye();
         launchLastDitchDaemonShutdownThread( 0 );
         long timeoutMillis = max( systemExitTimeoutInSeconds * ONE_SECOND_IN_MILLIS, ONE_SECOND_IN_MILLIS );
         acquireOnePermit( barrier, timeoutMillis );
@@ -338,17 +314,17 @@ public final class ForkedBooter
         System.exit( 0 );
     }
 
-    private RunResult runSuitesInProcess()
-        throws SurefireExecutionException, TestSetFailedException, InvocationTargetException
+    private void runSuitesInProcess()
+        throws TestSetFailedException, InvocationTargetException
     {
         ForkingReporterFactory factory = createForkingReporterFactory();
-        return invokeProviderInSameClassLoader( factory );
+        invokeProviderInSameClassLoader( factory );
     }
 
     private ForkingReporterFactory createForkingReporterFactory()
     {
         final boolean trimStackTrace = providerConfiguration.getReporterConfiguration().isTrimStackTrace();
-        return new ForkingReporterFactory( trimStackTrace, originalOut );
+        return new ForkingReporterFactory( trimStackTrace, eventChannel );
     }
 
     private synchronized ScheduledThreadPoolExecutor getJvmTerminator()
@@ -377,11 +353,10 @@ public final class ForkedBooter
         );
     }
 
-    private RunResult invokeProviderInSameClassLoader( ForkingReporterFactory factory )
+    private void invokeProviderInSameClassLoader( ForkingReporterFactory factory )
         throws TestSetFailedException, InvocationTargetException
     {
-        return createProviderInCurrentClassloader( factory )
-                       .invoke( testSet );
+        createProviderInCurrentClassloader( factory ).invoke( testSet );
     }
 
     private SurefireProvider createProviderInCurrentClassloader( ForkingReporterFactory reporterManagerFactory )
@@ -389,6 +364,7 @@ public final class ForkedBooter
         BaseProviderFactory bpf = new BaseProviderFactory( reporterManagerFactory, true );
         bpf.setTestRequest( providerConfiguration.getTestSuiteDefinition() );
         bpf.setReporterConfiguration( providerConfiguration.getReporterConfiguration() );
+        bpf.setForkedChannelEncoder( eventChannel );
         ClassLoader classLoader = currentThread().getContextClassLoader();
         bpf.setClassLoaders( classLoader );
         bpf.setTestArtifactInfo( providerConfiguration.getTestArtifact() );
@@ -422,7 +398,7 @@ public final class ForkedBooter
             DumpErrorSingleton.getSingleton().dumpException( t );
             t.printStackTrace();
             booter.cancelPingScheduler();
-            booter.exit( 1 );
+            booter.exit1();
         }
     }
 
@@ -431,15 +407,15 @@ public final class ForkedBooter
         return pluginProcessChecker != null && pluginProcessChecker.canUse();
     }
 
-    private static boolean acquireOnePermit( Semaphore barrier, long timeoutMillis )
+    private static void acquireOnePermit( Semaphore barrier, long timeoutMillis )
     {
         try
         {
-            return barrier.tryAcquire( timeoutMillis, MILLISECONDS );
+            barrier.tryAcquire( timeoutMillis, MILLISECONDS );
         }
         catch ( InterruptedException e )
         {
-            return true;
+            // cancel schedulers, stop the command reader and exit 0
         }
     }
 
diff --git a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/LazyTestsToRun.java b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/LazyTestsToRun.java
index 3237d07..9d0b2e0 100644
--- a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/LazyTestsToRun.java
+++ b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/LazyTestsToRun.java
@@ -19,7 +19,6 @@ package org.apache.maven.surefire.booter;
  * under the License.
  */
 
-import java.io.PrintStream;
 import java.util.Collections;
 import java.util.Iterator;
 
@@ -44,24 +43,24 @@ import static org.apache.maven.surefire.util.ReflectionUtils.loadClass;
 final class LazyTestsToRun
     extends TestsToRun
 {
-    private final PrintStream originalOutStream;
+    private final ForkedChannelEncoder eventChannel;
 
     /**
      * C'tor
      *
-     * @param originalOutStream the output stream to use when requesting new new tests
+     * @param eventChannel the output stream to use when requesting new new tests
      */
-    LazyTestsToRun( PrintStream originalOutStream )
+    LazyTestsToRun( ForkedChannelEncoder eventChannel )
     {
         super( Collections.<Class<?>>emptySet() );
 
-        this.originalOutStream = originalOutStream;
+        this.eventChannel = eventChannel;
     }
 
     private final class BlockingIterator
         implements Iterator<Class<?>>
     {
-        private final Iterator<String> it = getReader().getIterableClasses( originalOutStream ).iterator();
+        private final Iterator<String> it = getReader().getIterableClasses( eventChannel ).iterator();
 
         @Override
         public boolean hasNext()
diff --git a/surefire-booter/src/test/java/org/apache/maven/surefire/booter/CommandReaderTest.java b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/CommandReaderTest.java
index 740b46d..00d50b4 100644
--- a/surefire-booter/src/test/java/org/apache/maven/surefire/booter/CommandReaderTest.java
+++ b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/CommandReaderTest.java
@@ -93,7 +93,7 @@ public class CommandReaderTest
     @Test
     public void readJustOneClass()
     {
-        Iterator<String> it = reader.getIterableClasses( nul() ).iterator();
+        Iterator<String> it = reader.getIterableClasses( new ForkedChannelEncoder( nul() ) ).iterator();
         assertTrue( it.hasNext() );
         assertThat( it.next(), is( getClass().getName() ) );
         reader.stop();
@@ -112,7 +112,7 @@ public class CommandReaderTest
     @Test
     public void manyClasses()
     {
-        Iterator<String> it1 = reader.getIterableClasses( nul() ).iterator();
+        Iterator<String> it1 = reader.getIterableClasses( new ForkedChannelEncoder( nul() ) ).iterator();
         assertThat( it1.next(), is( getClass().getName() ) );
         addTestToPipeline( A.class.getName() );
         assertThat( it1.next(), is( A.class.getName() ) );
@@ -128,7 +128,7 @@ public class CommandReaderTest
     @Test
     public void twoIterators() throws Exception
     {
-        Iterator<String> it1 = reader.getIterableClasses( nul() ).iterator();
+        Iterator<String> it1 = reader.getIterableClasses( new ForkedChannelEncoder( nul() ) ).iterator();
 
         assertThat( it1.next(), is( getClass().getName() ) );
         addTestToPipeline( A.class.getName() );
@@ -162,7 +162,7 @@ public class CommandReaderTest
             @Override
             public void run()
             {
-                Iterator<String> it = reader.getIterableClasses( nul() ).iterator();
+                Iterator<String> it = reader.getIterableClasses( new ForkedChannelEncoder( nul() ) ).iterator();
                 assertThat( it.next(), is( CommandReaderTest.class.getName() ) );
             }
         };
@@ -190,7 +190,7 @@ public class CommandReaderTest
             @Override
             public void run()
             {
-                Iterator<String> it = reader.getIterableClasses( nul() ).iterator();
+                Iterator<String> it = reader.getIterableClasses( new ForkedChannelEncoder( nul() ) ).iterator();
                 assertThat( it.next(), is( CommandReaderTest.class.getName() ) );
                 counter.countDown();
                 assertThat( it.next(), is( PropertiesWrapperTest.class.getName() ) );
diff --git a/surefire-its/src/test/java/org/apache/maven/surefire/its/ConsoleOutputIT.java b/surefire-its/src/test/java/org/apache/maven/surefire/its/ConsoleOutputIT.java
index 734d773..6f0802c 100644
--- a/surefire-its/src/test/java/org/apache/maven/surefire/its/ConsoleOutputIT.java
+++ b/surefire-its/src/test/java/org/apache/maven/surefire/its/ConsoleOutputIT.java
@@ -19,13 +19,13 @@ package org.apache.maven.surefire.its;
  * under the License.
  */
 
-import java.nio.charset.Charset;
-
 import org.apache.maven.surefire.its.fixture.OutputValidator;
 import org.apache.maven.surefire.its.fixture.SurefireJUnit4IntegrationTestCase;
 import org.apache.maven.surefire.its.fixture.TestFile;
 import org.junit.Test;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 /**
  * Basic suite test using all known versions of JUnit 4.x
  *
@@ -65,13 +65,13 @@ public class ConsoleOutputIT
     {
         TestFile xmlReportFile = outputValidator.getSurefireReportsXmlFile( "TEST-consoleOutput.Test1.xml" );
         xmlReportFile.assertContainsText( "SoutLine" );
-        xmlReportFile.assertContainsText( normalizeToDefaultCharset( "äöüß" ) );
-        xmlReportFile.assertContainsText( normalizeToDefaultCharset( "failing with ü" ) );
+        xmlReportFile.assertContainsText(  "äöüß" );
+        xmlReportFile.assertContainsText(  "failing with ü" );
 
-        TestFile outputFile = outputValidator.getSurefireReportsFile( "consoleOutput.Test1-output.txt" );
+        TestFile outputFile = outputValidator.getSurefireReportsFile( "consoleOutput.Test1-output.txt", UTF_8 );
         outputFile.assertContainsText( "SoutAgain" );
         outputFile.assertContainsText( "SoutLine" );
-        outputFile.assertContainsText( normalizeToDefaultCharset( "äöüß" ) );
+        outputFile.assertContainsText( "äöüß" );
 
         if ( includeShutdownHook )
         {
@@ -79,22 +79,6 @@ public class ConsoleOutputIT
         }
     }
 
-    /**
-     * @param string the string to normalize
-     * @return the string with all characters not available in the current charset being replaced, e.g. for US-ASCII,
-     *         German umlauts would be replaced to ?
-     */
-    private String normalizeToDefaultCharset( String string )
-    {
-        Charset cs = Charset.defaultCharset();
-        if ( cs.canEncode() )
-        {
-            string = cs.decode( cs.encode( string ) ).toString();
-        }
-
-        return string;
-    }
-
     @Test
     public void largerSoutThanMemory()
     {
diff --git a/surefire-its/src/test/java/org/apache/maven/surefire/its/fixture/OutputValidator.java b/surefire-its/src/test/java/org/apache/maven/surefire/its/fixture/OutputValidator.java
index 5f61fc4..d36c6a6 100644
--- a/surefire-its/src/test/java/org/apache/maven/surefire/its/fixture/OutputValidator.java
+++ b/surefire-its/src/test/java/org/apache/maven/surefire/its/fixture/OutputValidator.java
@@ -30,6 +30,7 @@ import org.apache.maven.it.VerificationException;
 import org.apache.maven.it.Verifier;
 import org.hamcrest.Matcher;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.hamcrest.MatcherAssert.assertThat;
 
 /**
@@ -203,7 +204,7 @@ public class OutputValidator
     public TestFile getSurefireReportsXmlFile( String fileName )
     {
         File targetDir = getSurefireReportsDirectory();
-        return new TestFile( new File( targetDir, fileName ), Charset.forName( "UTF-8" ), this );
+        return new TestFile( new File( targetDir, fileName ), UTF_8, this );
     }
 
     public File getSurefireReportsDirectory()
diff --git a/surefire-its/src/test/java/org/apache/maven/surefire/its/jiras/Surefire1082ParallelJUnitParameterizedIT.java b/surefire-its/src/test/java/org/apache/maven/surefire/its/jiras/Surefire1082ParallelJUnitParameterizedIT.java
index 34dd5ae..5bac506 100644
--- a/surefire-its/src/test/java/org/apache/maven/surefire/its/jiras/Surefire1082ParallelJUnitParameterizedIT.java
+++ b/surefire-its/src/test/java/org/apache/maven/surefire/its/jiras/Surefire1082ParallelJUnitParameterizedIT.java
@@ -26,11 +26,11 @@ import org.apache.maven.surefire.its.fixture.SurefireLauncher;
 import org.apache.maven.surefire.its.fixture.TestFile;
 import org.junit.Test;
 
-import java.nio.charset.Charset;
 import java.util.Collection;
 import java.util.Set;
 import java.util.TreeSet;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.apache.maven.surefire.its.fixture.IsRegex.regex;
 import static org.hamcrest.CoreMatchers.startsWith;
 import static org.hamcrest.core.AnyOf.anyOf;
@@ -50,7 +50,7 @@ public class Surefire1082ParallelJUnitParameterizedIT
     {
         TestFile report = validator.getSurefireReportsFile( "jiras.surefire1082.Jira1082Test-output.txt" );
         report.assertFileExists();
-        return printOnlyTestLines( validator.loadFile( report.getFile(), Charset.forName( "UTF-8" ) ) );
+        return printOnlyTestLines( validator.loadFile( report.getFile(), UTF_8 ) );
     }
 
     private static Set<String> printOnlyTestLines( Collection<String> logs )
diff --git a/surefire-its/src/test/java/org/apache/maven/surefire/its/jiras/Surefire1535TestNGParallelSuitesIT.java b/surefire-its/src/test/java/org/apache/maven/surefire/its/jiras/Surefire1535TestNGParallelSuitesIT.java
index b6e534e..9a8025f 100644
--- a/surefire-its/src/test/java/org/apache/maven/surefire/its/jiras/Surefire1535TestNGParallelSuitesIT.java
+++ b/surefire-its/src/test/java/org/apache/maven/surefire/its/jiras/Surefire1535TestNGParallelSuitesIT.java
@@ -26,8 +26,7 @@ import org.apache.maven.surefire.its.fixture.SurefireLauncher;
 import org.apache.maven.surefire.its.fixture.TestFile;
 import org.junit.Test;
 
-import java.nio.charset.Charset;
-
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.hamcrest.CoreMatchers.anyOf;
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.is;
@@ -36,7 +35,6 @@ import static org.fest.assertions.Assertions.assertThat;
 public class Surefire1535TestNGParallelSuitesIT
         extends SurefireJUnit4IntegrationTestCase
 {
-    private static final Charset UTF8 = Charset.forName( "UTF-8" );
     private static final String TEST_RESULT_1 = platformEncoding( "Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, " );
     private static final String TEST_RESULT_2 = platformEncoding( "Tests run: 2, Failures: 0, Errors: 0, Skipped: 0" );
     private static final String SUITE1 = platformEncoding( "Suite1" );
@@ -53,7 +51,7 @@ public class Surefire1535TestNGParallelSuitesIT
                 .forkCount( 2 )
                 .executeTest();
 
-        TestFile testFile = validator.getSurefireReportsFile( "../surefire-reports-1/TEST-TestSuite.xml", UTF8 );
+        TestFile testFile = validator.getSurefireReportsFile( "../surefire-reports-1/TEST-TestSuite.xml", UTF_8 );
         testFile.assertFileExists();
         testFile.assertContainsText( "<testcase name=\"test\" classname=\"it.ParallelTest" );
         String xml = testFile.readFileToString();
@@ -62,7 +60,7 @@ public class Surefire1535TestNGParallelSuitesIT
         assertThat( parallelTest11 ^ parallelTest12 )
                 .isTrue();
 
-        testFile = validator.getSurefireReportsFile( "../surefire-reports-2/TEST-TestSuite.xml", UTF8 );
+        testFile = validator.getSurefireReportsFile( "../surefire-reports-2/TEST-TestSuite.xml", UTF_8 );
         testFile.assertFileExists();
         testFile.assertContainsText( "<testcase name=\"test\" classname=\"it.ParallelTest" );
         xml = testFile.readFileToString();
@@ -92,7 +90,7 @@ public class Surefire1535TestNGParallelSuitesIT
                 .redirectToFile( true )
                 .executeTest();
 
-        TestFile testFile = validator.getSurefireReportsFile( "../surefire-reports-1/TEST-TestSuite.xml", UTF8 );
+        TestFile testFile = validator.getSurefireReportsFile( "../surefire-reports-1/TEST-TestSuite.xml", UTF_8 );
         testFile.assertFileExists();
         testFile.assertContainsText( "<testcase name=\"test\" classname=\"it.ParallelTest" );
         String xml = testFile.readFileToString();
@@ -100,7 +98,7 @@ public class Surefire1535TestNGParallelSuitesIT
         boolean parallelTest12 = xml.contains( "<testcase name=\"test\" classname=\"it.ParallelTest2\"" );
         assertThat( parallelTest11 ^ parallelTest12 )
                 .isTrue();
-        String log = validator.getSurefireReportsFile( "../surefire-reports-1/TestSuite-output.txt", UTF8 )
+        String log = validator.getSurefireReportsFile( "../surefire-reports-1/TestSuite-output.txt", UTF_8 )
                 .readFileToString();
         assertThat( log.contains( TEST1 ) )
                 .isEqualTo( parallelTest11 );
@@ -111,7 +109,7 @@ public class Surefire1535TestNGParallelSuitesIT
         assertThat( log.contains( SUITE2 ) )
                 .isEqualTo( parallelTest12 );
 
-        testFile = validator.getSurefireReportsFile( "../surefire-reports-2/TEST-TestSuite.xml", UTF8 );
+        testFile = validator.getSurefireReportsFile( "../surefire-reports-2/TEST-TestSuite.xml", UTF_8 );
         testFile.assertFileExists();
         testFile.assertContainsText( "<testcase name=\"test\" classname=\"it.ParallelTest" );
         xml = testFile.readFileToString();
@@ -119,7 +117,7 @@ public class Surefire1535TestNGParallelSuitesIT
         boolean parallelTest22 = xml.contains( "<testcase name=\"test\" classname=\"it.ParallelTest2\"" );
         assertThat( parallelTest21 ^ parallelTest22 )
                 .isTrue();
-        log = validator.getSurefireReportsFile( "../surefire-reports-2/TestSuite-output.txt", UTF8 )
+        log = validator.getSurefireReportsFile( "../surefire-reports-2/TestSuite-output.txt", UTF_8 )
                 .readFileToString();
         assertThat( log.contains( TEST1 ) )
                 .isEqualTo( parallelTest21 );
@@ -197,6 +195,6 @@ public class Surefire1535TestNGParallelSuitesIT
 
     private static String platformEncoding( String text )
     {
-        return new String( text.getBytes( UTF8 ) );
+        return new String( text.getBytes( UTF_8 ) );
     }
 }
diff --git a/surefire-logger-api/src/main/java/org/apache/maven/plugin/surefire/log/api/ConsoleLoggerUtils.java b/surefire-logger-api/src/main/java/org/apache/maven/plugin/surefire/log/api/ConsoleLoggerUtils.java
index 976a48c..df2d44f 100644
--- a/surefire-logger-api/src/main/java/org/apache/maven/plugin/surefire/log/api/ConsoleLoggerUtils.java
+++ b/surefire-logger-api/src/main/java/org/apache/maven/plugin/surefire/log/api/ConsoleLoggerUtils.java
@@ -33,6 +33,11 @@ public final class ConsoleLoggerUtils
         throw new IllegalStateException( "non instantiable constructor" );
     }
 
+    public static String toString( Throwable t )
+    {
+        return toString( null, t );
+    }
+
     public static String toString( String message, Throwable t )
     {
         StringWriter result = new StringWriter( 512 );
diff --git a/surefire-providers/common-junit4/src/main/java/org/apache/maven/surefire/common/junit4/JUnit4RunListener.java b/surefire-providers/common-junit4/src/main/java/org/apache/maven/surefire/common/junit4/JUnit4RunListener.java
index 648f910..234bcf5 100644
--- a/surefire-providers/common-junit4/src/main/java/org/apache/maven/surefire/common/junit4/JUnit4RunListener.java
+++ b/surefire-providers/common-junit4/src/main/java/org/apache/maven/surefire/common/junit4/JUnit4RunListener.java
@@ -47,7 +47,8 @@ public class JUnit4RunListener
 
     /**
      * This flag is set after a failure has occurred so that a {@link RunListener#testSucceeded} event is not fired.
-     * This is necessary because JUnit4 always fires a {@link org.junit.runner.notification.RunListener#testRunFinished}
+     * This is necessary because JUnit4 always fires a
+     * {@link org.junit.runner.notification.RunListener#testRunFinished(Result)}
      * event-- even if there was a failure.
      */
     private final ThreadLocal<Boolean> failureFlag = new InheritableThreadLocal<>();
@@ -128,7 +129,6 @@ public class JUnit4RunListener
         }
     }
 
-    @SuppressWarnings( "UnusedDeclaration" )
     public void testAssumptionFailure( Failure failure )
     {
         try
@@ -191,9 +191,4 @@ public class JUnit4RunListener
             }
         }
     }
-
-    private static boolean isInsaneJunitNullString( String value )
-    {
-        return "null".equals( value );
-    }
 }
diff --git a/surefire-providers/common-junit4/src/main/java/org/apache/maven/surefire/junit4/MockReporter.java b/surefire-providers/common-junit4/src/main/java/org/apache/maven/surefire/junit4/MockReporter.java
index 18f0592..6f93ae5 100644
--- a/surefire-providers/common-junit4/src/main/java/org/apache/maven/surefire/junit4/MockReporter.java
+++ b/surefire-providers/common-junit4/src/main/java/org/apache/maven/surefire/junit4/MockReporter.java
@@ -25,6 +25,7 @@ import java.util.concurrent.atomic.AtomicInteger;
 import org.apache.maven.surefire.report.ReportEntry;
 import org.apache.maven.surefire.report.RunListener;
 import org.apache.maven.surefire.report.TestSetReportEntry;
+import org.apache.maven.surefire.report.RunMode;
 
 /**
  * Internal tests use only.
@@ -93,6 +94,12 @@ public class MockReporter
     {
     }
 
+    @Override
+    public RunMode markAs( RunMode currentRunMode )
+    {
+        return null;
+    }
+
     public void testSkippedByUser( ReportEntry report )
     {
         testSkipped( report );
diff --git a/surefire-providers/surefire-junit-platform/src/test/java/org/apache/maven/surefire/junitplatform/JUnitPlatformProviderTest.java b/surefire-providers/surefire-junit-platform/src/test/java/org/apache/maven/surefire/junitplatform/JUnitPlatformProviderTest.java
index f6b48e9..3e82a15 100644
--- a/surefire-providers/surefire-junit-platform/src/test/java/org/apache/maven/surefire/junitplatform/JUnitPlatformProviderTest.java
+++ b/surefire-providers/surefire-junit-platform/src/test/java/org/apache/maven/surefire/junitplatform/JUnitPlatformProviderTest.java
@@ -29,7 +29,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assumptions.assumeTrue;
-import static org.mockito.AdditionalMatchers.gt;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.inOrder;
@@ -221,16 +220,14 @@ public class JUnitPlatformProviderTest
 
         invokeProvider( provider, VerboseTestClass.class );
 
-        ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass( byte[].class );
-        // @formatter:off
+        ArgumentCaptor<String> captor = ArgumentCaptor.forClass( String.class );
         verify( (ConsoleOutputReceiver) runListener )
-                        .writeTestOutput( captor.capture(), eq( 0 ), gt( 6 ), eq( true ) );
+                        .writeTestOutput( captor.capture(), eq( true ), eq( true ) );
         verify( (ConsoleOutputReceiver) runListener )
-                        .writeTestOutput( captor.capture(), eq( 0 ), gt( 6 ), eq( false ) );
+                        .writeTestOutput( captor.capture(), eq( true ), eq( false ) );
         assertThat( captor.getAllValues() )
-                        .extracting( bytes -> new String( bytes, 0, 6 ) )
-                        .containsExactly( "stdout", "stderr" );
-        // @formatter:on
+                .hasSize( 2 )
+                .containsExactly( "stdout", "stderr" );
     }
 
     @Test
diff --git a/surefire-providers/surefire-junit3/src/main/java/org/apache/maven/surefire/junit/JUnit3Provider.java b/surefire-providers/surefire-junit3/src/main/java/org/apache/maven/surefire/junit/JUnit3Provider.java
index f7bb710..bfdb4eb 100644
--- a/surefire-providers/surefire-junit3/src/main/java/org/apache/maven/surefire/junit/JUnit3Provider.java
+++ b/surefire-providers/surefire-junit3/src/main/java/org/apache/maven/surefire/junit/JUnit3Provider.java
@@ -98,8 +98,8 @@ public class JUnit3Provider
         {
             final RunListener reporter = reporterFactory.createReporter();
             ConsoleOutputCapture.startCapture( (ConsoleOutputReceiver) reporter );
-            final Map<String, String> systemProperties = systemProps();
-            final String smClassName = systemProperties.get( "surefire.security.manager" );
+            Map<String, String> systemProperties = systemProps();
+            String smClassName = System.getProperty( "surefire.security.manager" );
             if ( smClassName != null )
             {
                 SecurityManager securityManager =
@@ -133,13 +133,9 @@ public class JUnit3Provider
                                  Map<String, String> systemProperties )
         throws TestSetFailedException
     {
-        SimpleReportEntry report = new SimpleReportEntry( testSet.getName(), null, systemProperties );
-
-        reporter.testSetStarting( report );
-
+        reporter.testSetStarting( new SimpleReportEntry( testSet.getName(), null ) );
         testSet.execute( reporter, classLoader );
-
-        reporter.testSetCompleted( report );
+        reporter.testSetCompleted( new SimpleReportEntry( testSet.getName(), null, systemProperties ) );
     }
 
     private TestsToRun scanClassPath()
diff --git a/surefire-providers/surefire-junit3/src/test/java/org/apache/maven/surefire/junit/JUnitTestSetTest.java b/surefire-providers/surefire-junit3/src/test/java/org/apache/maven/surefire/junit/JUnitTestSetTest.java
index 1381303..bae582f 100644
--- a/surefire-providers/surefire-junit3/src/test/java/org/apache/maven/surefire/junit/JUnitTestSetTest.java
+++ b/surefire-providers/surefire-junit3/src/test/java/org/apache/maven/surefire/junit/JUnitTestSetTest.java
@@ -25,6 +25,7 @@ import junit.framework.TestSuite;
 import org.apache.maven.surefire.common.junit3.JUnit3Reflector;
 import org.apache.maven.surefire.report.ReportEntry;
 import org.apache.maven.surefire.report.RunListener;
+import org.apache.maven.surefire.report.RunMode;
 import org.apache.maven.surefire.report.TestSetReportEntry;
 
 import java.util.ArrayList;
@@ -115,6 +116,11 @@ public class JUnitTestSetTest
         {
         }
 
+        public RunMode markAs( RunMode currentRunMode )
+        {
+            return RunMode.NORMAL_RUN;
+        }
+
         public void testSkippedByUser( ReportEntry report )
         {
             testSkipped( report );
diff --git a/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/ConcurrentRunListener.java b/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/ConcurrentRunListener.java
index be02d4e..244b83f 100644
--- a/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/ConcurrentRunListener.java
+++ b/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/ConcurrentRunListener.java
@@ -25,6 +25,7 @@ import org.apache.maven.surefire.report.ConsoleStream;
 import org.apache.maven.surefire.report.ReportEntry;
 import org.apache.maven.surefire.report.ReporterFactory;
 import org.apache.maven.surefire.report.RunListener;
+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.testset.TestSetFailedException;
@@ -131,6 +132,11 @@ public abstract class ConcurrentRunListener
         getRunListener().testExecutionSkippedByUser();
     }
 
+    public RunMode markAs( RunMode currentRunMode )
+    {
+        return reporterManagerThreadLocal.get().markAs( currentRunMode );
+    }
+
     @Override
     public void testAssumptionFailure( ReportEntry failure )
     {
@@ -213,18 +219,18 @@ public abstract class ConcurrentRunListener
 
 
     @Override
-    public void writeTestOutput( byte[] buf, int off, int len, boolean stdout )
+    public void writeTestOutput( String output, boolean newLine, boolean stdout )
     {
         TestMethod threadTestMethod = getThreadTestMethod();
         if ( threadTestMethod != null )
         {
             LogicalStream logicalStream = threadTestMethod.getLogicalStream();
-            logicalStream.write( stdout, buf, off, len );
+            logicalStream.write( stdout, output, newLine );
         }
         else
         {
             // Not able to associate output with any thread. Just dump to console
-            consoleStream.println( buf, off, len );
+            consoleStream.println( output );
         }
     }
 }
diff --git a/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/LogicalStream.java b/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/LogicalStream.java
index 068674f..c75c063 100644
--- a/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/LogicalStream.java
+++ b/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/LogicalStream.java
@@ -21,60 +21,40 @@ package org.apache.maven.surefire.junitcore;
 
 import org.apache.maven.surefire.report.ConsoleOutputReceiver;
 
-import java.util.Arrays;
 import java.util.Queue;
 import java.util.concurrent.ConcurrentLinkedQueue;
 
 /**
  * A stream-like object that preserves ordering between stdout/stderr
  */
-public final class LogicalStream
+final class LogicalStream
 {
     private final Queue<Entry> output = new ConcurrentLinkedQueue<>();
 
     private static final class Entry
     {
         private final boolean stdout;
+        private final String text;
+        private final boolean newLine;
 
-        private final byte[] b;
-
-        private final int off;
-
-        private final int len;
-
-        private Entry( boolean stdout, byte[] b, int off, int len )
+        Entry( boolean stdout, String text, boolean newLine )
         {
             this.stdout = stdout;
-            this.b = Arrays.copyOfRange( b, off, off + len );
-            this.off = 0;
-            this.len = len;
-        }
-
-        private void writeDetails( ConsoleOutputReceiver outputReceiver )
-        {
-            outputReceiver.writeTestOutput( b, off, len, stdout );
+            this.text = text;
+            this.newLine = newLine;
         }
     }
 
-    public void write( boolean stdout, byte b[], int off, int len )
+    synchronized void write( boolean stdout, String text, boolean newLine )
     {
-        if ( !isBlankLine( b, len ) )
-        {
-            Entry entry = new Entry( stdout, b, off, len );
-            output.add( entry );
-        }
+        output.add( new Entry( stdout, text, newLine ) );
     }
 
-    public void writeDetails( ConsoleOutputReceiver outputReceiver )
+    void writeDetails( ConsoleOutputReceiver outputReceiver )
     {
         for ( Entry entry = output.poll(); entry != null; entry = output.poll() )
         {
-            entry.writeDetails( outputReceiver );
+            outputReceiver.writeTestOutput( entry.text, entry.newLine, entry.stdout );
         }
     }
-
-    private static boolean isBlankLine( byte[] b, int len )
-    {
-        return b == null || len == 0;
-    }
 }
diff --git a/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/NonConcurrentRunListener.java b/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/NonConcurrentRunListener.java
index 71ec93d..4aaf1c8 100644
--- a/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/NonConcurrentRunListener.java
+++ b/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/NonConcurrentRunListener.java
@@ -55,11 +55,10 @@ public class NonConcurrentRunListener
         super( reporter );
     }
 
-    @Override
-    public void writeTestOutput( byte[] buf, int off, int len, boolean stdout )
+    public synchronized void writeTestOutput( String output, boolean newLine, boolean stdout )
     {
         // We can write immediately: no parallelism and a single class.
-        ( (ConsoleOutputReceiver) reporter ).writeTestOutput( buf, off, len, stdout );
+        ( (ConsoleOutputReceiver) reporter ).writeTestOutput( output, newLine, stdout );
     }
 
     @Override
diff --git a/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/TestMethod.java b/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/TestMethod.java
index ae89fbf..846bf99 100644
--- a/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/TestMethod.java
+++ b/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/TestMethod.java
@@ -185,12 +185,12 @@ class TestMethod
     }
 
     @Override
-    public void writeTestOutput( byte[] buf, int off, int len, boolean stdout )
+    public void writeTestOutput( String output, boolean newLine, boolean stdout )
     {
-        getLogicalStream().write( stdout, buf, off, len );
+        getLogicalStream().write( stdout, output, newLine );
     }
 
-    public TestSet getTestSet()
+    TestSet getTestSet()
     {
         return testSet;
     }
diff --git a/surefire-providers/surefire-testng-utils/src/main/java/org/apache/maven/surefire/testng/utils/GroupMatcherMethodSelector.java b/surefire-providers/surefire-testng-utils/src/main/java/org/apache/maven/surefire/testng/utils/GroupMatcherMethodSelector.java
index e540569..3ba0ec1 100644
--- a/surefire-providers/surefire-testng-utils/src/main/java/org/apache/maven/surefire/testng/utils/GroupMatcherMethodSelector.java
+++ b/surefire-providers/surefire-testng-utils/src/main/java/org/apache/maven/surefire/testng/utils/GroupMatcherMethodSelector.java
@@ -19,19 +19,19 @@ package org.apache.maven.surefire.testng.utils;
  * under the License.
  */
 
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
 import org.apache.maven.surefire.group.match.AndGroupMatcher;
 import org.apache.maven.surefire.group.match.GroupMatcher;
 import org.apache.maven.surefire.group.match.InverseGroupMatcher;
 import org.apache.maven.surefire.group.parse.GroupMatcherParser;
 import org.apache.maven.surefire.group.parse.ParseException;
-
 import org.testng.IMethodSelector;
 import org.testng.IMethodSelectorContext;
 import org.testng.ITestNGMethod;
 
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
 /**
  * Method selector delegating to {@link GroupMatcher} to decide if a method is included or not.
  *
diff --git a/surefire-providers/surefire-testng/src/main/java/org/apache/maven/surefire/testng/TestSuite.java b/surefire-providers/surefire-testng/src/main/java/org/apache/maven/surefire/testng/TestSuite.java
index e9b0424..ffd13ae 100644
--- a/surefire-providers/surefire-testng/src/main/java/org/apache/maven/surefire/testng/TestSuite.java
+++ b/surefire-providers/surefire-testng/src/main/java/org/apache/maven/surefire/testng/TestSuite.java
@@ -35,7 +35,7 @@ abstract class TestSuite
 {
     abstract Map<String, String> getOptions();
 
-    final String getSuiteName()
+    private String getSuiteName()
     {
         String result = getOptions().get( "suitename" );
         return result == null ? "TestSuite" : result;
@@ -43,7 +43,7 @@ abstract class TestSuite
 
     final void startTestSuite( RunListener reporterManager )
     {
-        TestSetReportEntry report = new SimpleReportEntry( getSuiteName(), null, systemProps() );
+        TestSetReportEntry report = new SimpleReportEntry( getSuiteName(), null );
 
         try
         {