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/07 23:36:35 UTC

[maven-surefire] branch SUREFIRE-1222 updated (a4c4f7e -> 29b4e0f)

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

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


 discard a4c4f7e  [SUREFIRE-1222] ForkClient attempts to consume unrelated lines
 discard 8c6ba65  [SUREFIRE-1222] ForkClient attempts to consume unrelated lines
 discard 84b500a  [SUREFIRE-1222] ForkClient attempts to consume unrelated lines
 discard 0fd070c  [SUREFIRE-1222] ForkClient attempts to consume unrelated lines
 discard 8a2d334  [SUREFIRE-1222] ForkClient attempts to consume unrelated lines
 discard 6dd2073  [SUREFIRE-1222] ForkClient attempts to consume unrelated lines
 discard 0a7f856  [SUREFIRE-1222] ForkClient attempts to consume unrelated lines
    omit 64ae8e8  [SUREFIRE-1222] ForkClient attempts to consume unrelated lines
     new 29b4e0f  [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   (a4c4f7e)
            \
             N -- N -- N   refs/heads/SUREFIRE-1222 (29b4e0f)

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:


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

commit 29b4e0f2cdaf3962204acdf8c42ee71d43a4d1dd
Author: Tibor17 <ti...@apache.org>
AuthorDate: Thu Aug 3 14:47:49 2017 +0200

    [SUREFIRE-1222] ForkClient attempts to consume unrelated lines
---
 .../plugin/surefire/AbstractSurefireMojo.java      |   5 +-
 .../plugin/surefire/booterclient/ForkStarter.java  |   9 +-
 .../surefire/booterclient/output/ErrorInFork.java  |  53 +-
 .../surefire/booterclient/output/ForkClient.java   | 451 +++++++------
 .../booterclient/output/ForkedChannelDecoder.java  | 341 ++++++++++
 .../output/ForkedChannelDecoderErrorHandler.java   |  20 +-
 .../output/ForkedProcessBinaryEventListener.java   |  22 +-
 .../output/ForkedProcessEventListener.java         |  20 +-
 .../output/ForkedProcessExitErrorListener.java     |  20 +-
 .../output/ForkedProcessPropertyEventListener.java |  22 +-
 .../output/ForkedProcessReportEventListener.java   |  23 +-
 .../ForkedProcessStackTraceEventListener.java      |  20 +-
 .../output/ForkedProcessStringEventListener.java   |  20 +-
 .../output/MultipleFailureException.java           |   3 +-
 .../surefire/report/ConsoleOutputFileReporter.java |   5 +-
 .../surefire/report/DirectConsoleOutput.java       |  18 +-
 .../surefire/report/NullConsoleOutputReceiver.java |   3 +-
 .../surefire/report/StatelessXmlReporter.java      |   5 +-
 .../plugin/surefire/report/TestSetRunListener.java |  38 +-
 .../Utf8RecodingDeferredFileOutputStream.java      |   5 +-
 .../plugin/surefire/report/WrappedReportEntry.java |   3 +-
 .../surefire/runorder/StatisticsReporter.java      |   4 +-
 .../maven/plugin/surefire/util/FileScanner.java    |   6 +-
 .../booterclient/ForkingRunListenerTest.java       |  36 +-
 .../plugin/surefire/booterclient/MockReporter.java |  11 +-
 .../booterclient/output/ForkClientTest.java        |  29 +-
 .../output/ForkedChannelDecoderTest.java           | 717 +++++++++++++++++++++
 .../org/apache/maven/surefire/JUnit4SuiteTest.java |   2 +
 .../report/ConsoleOutputFileReporterTest.java      |   4 +-
 surefire-api/pom.xml                               |   5 +
 .../maven/surefire/booter/BaseProviderFactory.java |  20 +-
 .../maven/surefire/booter/CommandReader.java       |  32 +-
 .../surefire/booter/ForkedChannelEncoder.java      | 461 +++++++++++++
 .../maven/surefire/booter/ForkedProcessEvent.java  | 129 ++++
 .../surefire/booter/ForkingReporterFactory.java    |  17 +-
 .../maven/surefire/booter/ForkingRunListener.java  | 312 ++-------
 .../surefire/booter/MasterProcessCommand.java      |  30 +-
 .../surefire/providerapi/ProviderParameters.java   |   3 +
 .../surefire/report/ConsoleOutputCapture.java      |  12 +-
 .../surefire/report/ConsoleOutputReceiver.java     |   6 +-
 .../maven/surefire/report/ConsoleStream.java       |   2 +-
 .../report/DefaultDirectConsoleReporter.java       |   5 -
 .../apache/maven/surefire/report/RunListener.java  |   9 +
 .../org/apache/maven/surefire/report/RunMode.java  |  53 +-
 .../maven/surefire/testset/TestListResolver.java   |  14 +-
 .../maven/surefire/util/internal/StringUtils.java  | 217 -------
 .../java/org/apache/maven/JUnit4SuiteTest.java     |   2 -
 .../surefire/booter/ForkedChannelEncoderTest.java  | 481 ++++++++++++++
 .../surefire/booter/ForkingRunListenerTest.java    |   2 +-
 .../surefire/util/internal/StringUtilsTest.java    |  96 ---
 .../apache/maven/surefire/booter/Classpath.java    |   2 +-
 .../apache/maven/surefire/booter/ForkedBooter.java |  63 +-
 .../maven/surefire/booter/LazyTestsToRun.java      |  11 +-
 .../maven/surefire/booter/TypeEncodedValue.java    |   4 +-
 .../maven/surefire/booter/CommandReaderTest.java   |  10 +-
 .../surefire/its/fixture/SurefireLauncher.java     |   2 +-
 .../surefire/log/api/ConsoleLoggerUtils.java       |   5 +
 .../apache/maven/surefire/junit4/MockReporter.java |   7 +
 .../surefire/common/junit48/RequestedTest.java     |   2 +-
 .../maven/surefire/junit/JUnitTestSetTest.java     |   6 +
 surefire-providers/surefire-junit47/pom.xml        |   2 +-
 .../surefire/junitcore/ConcurrentRunListener.java  |  12 +-
 .../maven/surefire/junitcore/JUnitCoreWrapper.java |   2 +-
 .../maven/surefire/junitcore/LogicalStream.java    |  24 +-
 .../junitcore/NonConcurrentRunListener.java        |   4 +-
 .../maven/surefire/junitcore/TestMethod.java       |   4 +-
 .../testng/utils/GroupMatcherMethodSelector.java   |  14 +-
 67 files changed, 2741 insertions(+), 1256 deletions(-)

diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/AbstractSurefireMojo.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/AbstractSurefireMojo.java
index 16c25ec..23c3642 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/AbstractSurefireMojo.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/AbstractSurefireMojo.java
@@ -232,6 +232,7 @@ public abstract class AbstractSurefireMojo
      * being tested. This directory is declared by the parameter <code>testClassesDirectory</code> which defaults
      * to the POM property <code>${project.build.testOutputDirectory}</code>, typically <em>src/test/java</em>
      * unless overridden.
+     * //todo use regex for fully qualified class names and change the filtering abilities
      */
     @Parameter
     // TODO use regex for fully qualified class names in 3.0 and change the filtering abilities
@@ -1260,7 +1261,7 @@ public abstract class AbstractSurefireMojo
 
     protected boolean isAnyConcurrencySelected()
     {
-        return this.getParallel() != null && this.getParallel().trim().length() > 0;
+        return getParallel() != null && !getParallel().trim().isEmpty();
     }
 
     protected boolean isAnyGroupsSelected()
@@ -1821,7 +1822,7 @@ public abstract class AbstractSurefireMojo
             if ( item != null )
             {
                 item = item.trim();
-                if ( item.length() != 0 )
+                if ( !item.isEmpty() )
                 {
                     result.add( item );
                 }
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 b3a3f91..272f082 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
@@ -27,6 +27,7 @@ import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.Notifiable
 import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.OutputStreamFlushableCommandline;
 import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.TestLessInputStream;
 import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.TestProvidingInputStream;
+import org.apache.maven.plugin.surefire.booterclient.output.ErrorInFork;
 import org.apache.maven.plugin.surefire.booterclient.output.ForkClient;
 import org.apache.maven.plugin.surefire.booterclient.output.InPluginProcessDumpSingleton;
 import org.apache.maven.plugin.surefire.booterclient.output.NativeStdErrStreamConsumer;
@@ -46,7 +47,6 @@ import org.apache.maven.surefire.booter.StartupConfiguration;
 import org.apache.maven.surefire.booter.SurefireBooterForkException;
 import org.apache.maven.surefire.booter.SurefireExecutionException;
 import org.apache.maven.surefire.providerapi.SurefireProvider;
-import org.apache.maven.surefire.report.StackTraceWriter;
 import org.apache.maven.surefire.suite.RunResult;
 import org.apache.maven.surefire.testset.TestRequest;
 import org.apache.maven.surefire.util.DefaultScanResult;
@@ -660,12 +660,13 @@ public class ForkStarter
 
                 if ( forkClient.isErrorInFork() )
                 {
-                    StackTraceWriter errorInFork = forkClient.getErrorInFork();
-                    // noinspection ThrowFromFinallyBlock
+                    ErrorInFork errorInFork = forkClient.getErrorInFork();
+                    //todo boolean showStackTrace = providerConfiguration.getMainCliOptions().contains( SHOW_ERRORS );
+                    //noinspection ThrowFromFinallyBlock
                     throw new SurefireBooterForkException( "There was an error in the forked process"
                                                         + detail
                                                         + '\n'
-                                                        + errorInFork.getThrowable().getLocalizedMessage(), cause );
+                                                        + errorInFork, cause );
                 }
                 if ( !forkClient.isSaidGoodBye() )
                 {
diff --git a/surefire-logger-api/src/main/java/org/apache/maven/plugin/surefire/log/api/ConsoleLoggerUtils.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ErrorInFork.java
similarity index 50%
copy from surefire-logger-api/src/main/java/org/apache/maven/plugin/surefire/log/api/ConsoleLoggerUtils.java
copy to maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ErrorInFork.java
index 7eca90b..cd851f6 100644
--- a/surefire-logger-api/src/main/java/org/apache/maven/plugin/surefire/log/api/ConsoleLoggerUtils.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ErrorInFork.java
@@ -1,4 +1,4 @@
-package org.apache.maven.plugin.surefire.log.api;
+package org.apache.maven.plugin.surefire.booterclient.output;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,37 +19,50 @@ package org.apache.maven.plugin.surefire.log.api;
  * under the License.
  */
 
-import java.io.PrintWriter;
-import java.io.StringWriter;
+import static org.apache.maven.surefire.util.internal.StringUtils.isNotBlank;
 
 /**
  * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
- * @since 2.20
+ * @since 2.20.1
  */
-public final class ConsoleLoggerUtils
+public final class ErrorInFork
 {
-    private ConsoleLoggerUtils()
+    private final String localizedMessage;
+    private final String stackTrace;
+
+    public ErrorInFork( String localizedMessage, String stackTrace )
+    {
+        this.localizedMessage = localizedMessage;
+        this.stackTrace = stackTrace;
+    }
+
+    public String getLocalizedMessage()
+    {
+        return localizedMessage;
+    }
+
+    public String getStackTrace()
     {
-        throw new IllegalStateException( "non instantiable constructor" );
+        return stackTrace;
     }
 
-    public static String toString( String message, Throwable t )
+    @Override
+    public String toString()
     {
-        StringWriter result = new StringWriter( 512 );
-        PrintWriter writer = new PrintWriter( result );
-        try
+        StringBuilder builder = new StringBuilder();
+        if ( isNotBlank( localizedMessage ) )
         {
-            if ( message != null )
-            {
-                writer.println( message );
-            }
-            t.printStackTrace( writer );
-            writer.flush();
-            return result.toString();
+            builder.append( localizedMessage );
         }
-        finally
+
+        if ( isNotBlank( stackTrace ) )
         {
-            writer.close();
+            if ( builder.length() != 0 )
+            {
+                builder.append( '\n' );
+            }
+            builder.append( stackTrace );
         }
+        return super.toString();
     }
 }
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 583460f..124d0c7 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,46 +26,22 @@ 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.StackTraceWriter;
+import org.apache.maven.surefire.report.RunMode;
 
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.IOException;
 import java.io.StringReader;
-import java.nio.ByteBuffer;
 import java.util.Properties;
 import java.util.Queue;
 import java.util.Set;
-import java.util.StringTokenizer;
 import java.util.TreeSet;
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.atomic.AtomicLong;
 
-import static java.lang.Integer.decode;
 import static java.lang.System.currentTimeMillis;
-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.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
 
@@ -90,17 +66,21 @@ public class ForkClient
 
     /**
      * <t>testSetStartedAt</t> is set to non-zero after received
-     * {@link org.apache.maven.surefire.booter.ForkingRunListener#BOOTERCODE_TESTSET_STARTING test-set}.
+     * {@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;
 
+    private final ForkedChannelDecoderErrorHandler errorHandler;
+
     private RunListener testSetReporter;
 
     private volatile boolean saidGoodBye;
 
-    private volatile StackTraceWriter errorInFork;
+    private volatile ErrorInFork errorInFork;
 
     private volatile int forkNumber;
 
@@ -114,6 +94,211 @@ public class ForkClient
         this.testVmSystemProperties = testVmSystemProperties;
         this.notifiableTestStream = notifiableTestStream;
         this.log = log;
+        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
+    {
+        @Override
+        public void handle( RunMode runMode, ReportEntry reportEntry )
+        {
+            getTestSetReporter().testSetStarting( reportEntry );
+            setCurrentStartTime();
+        }
+    }
+
+    private final class TestSetCompletedListener implements ForkedProcessReportEventListener
+    {
+        @Override
+        public void handle( RunMode runMode, ReportEntry reportEntry )
+        {
+            testsInProgress.clear();
+            getTestSetReporter().testSetCompleted( reportEntry );
+        }
+    }
+
+    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 )
+        {
+            synchronized ( testVmSystemProperties )
+            {
+                testVmSystemProperties.put( key, value );
+            }
+        }
+    }
+
+    private final class StdOutListener implements ForkedProcessBinaryEventListener
+    {
+        @Override
+        public void handle( RunMode runMode, String output )
+        {
+            writeTestOutput( output, true );
+        }
+    }
+
+    private final class StdErrListener implements ForkedProcessBinaryEventListener
+    {
+        @Override
+        public void handle( RunMode runMode, String output )
+        {
+            writeTestOutput( output, 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 )
+        {
+            errorInFork = new ErrorInFork( msg, stackTrace );
+        }
+    }
+
+    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 );
+        }
     }
 
     protected void stopOnNextTest()
@@ -184,117 +369,10 @@ 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 ) );
-                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 ) );
-                synchronized ( testVmSystemProperties )
-                {
-                    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 );
-        }
-    }
-
-    private void logStreamWarning( String event )
-    {
-        logStreamWarning( null, event );
+        decoder.handleEvent( event, errorHandler );
     }
 
-    private void logStreamWarning( Throwable e, String event )
+    private void logStreamWarning( String event, Throwable e )
     {
         final String msg = "Corrupted stdin stream in forked JVM " + forkNumber + ".";
         final InPluginProcessDumpSingleton util = InPluginProcessDumpSingleton.getSingleton();
@@ -309,26 +387,10 @@ public class ForkClient
         }
     }
 
-    private void writeTestOutput( String remaining, boolean isStdout )
+    private void writeTestOutput( String output, 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, isStdout );
     }
 
     public final void consumeMultiLineContent( String s )
@@ -341,54 +403,6 @@ public class ForkClient
         }
     }
 
-    private String createConsoleMessage( String remaining )
-    {
-        return unescape( remaining );
-    }
-
-    private ReportEntry createReportEntry( String untokenized )
-    {
-        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 );
-        }
-        catch ( RuntimeException e )
-        {
-            throw new RuntimeException( untokenized, e );
-        }
-    }
-
-    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();
-    }
-
     /**
      * Used when getting reporters on the plugin side of a fork.
      * Used by testing purposes only. May not be volatile variable.
@@ -420,7 +434,7 @@ public class ForkClient
         return saidGoodBye;
     }
 
-    public final StackTraceWriter getErrorInFork()
+    public final ErrorInFork getErrorInFork()
     {
         return errorInFork;
     }
@@ -445,33 +459,4 @@ public class ForkClient
         assert this.forkNumber == 0;
         this.forkNumber = forkNumber;
     }
-
-    private static final class OperationalData
-    {
-        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;
-        }
-    }
 }
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..bef3315
--- /dev/null
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedChannelDecoder.java
@@ -0,0 +1,341 @@
+package org.apache.maven.plugin.surefire.booterclient.output;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.apache.maven.surefire.booter.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.StringTokenizer;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import static javax.xml.bind.DatatypeConverter.parseBase64Binary;
+import static org.apache.maven.surefire.booter.ForkedProcessEvent.MAGIC_NUMBER;
+import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_BYE;
+import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_CONSOLE_INFO;
+import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_CONSOLE_DEBUG;
+import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_NEXT_TEST;
+import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_STDERR;
+import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_STDOUT;
+import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_STOP_ON_NEXT_TEST;
+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_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_CONSOLE_WARNING;
+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.ObjectUtils.requireNonNull;
+import static org.apache.maven.surefire.util.internal.StringUtils.isBlank;
+import static org.apache.maven.surefire.util.internal.StringUtils.isNotBlank;
+
+/**
+ * magic number : opcode : run mode [: opcode specific data]*
+ * <p/>
+ *
+ * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
+ * @since 2.20.1
+ */
+public final class ForkedChannelDecoder
+{
+    private static final byte[] EMPTY = {};
+
+    private volatile ForkedProcessPropertyEventListener propertyEventListener;
+    private volatile ForkedProcessStackTraceEventListener consoleErrorEventListener;
+    private volatile ForkedProcessExitErrorListener exitErrorEventListener;
+
+    private final ConcurrentMap<ForkedProcessEvent, ForkedProcessReportEventListener> reportEventListeners =
+            new ConcurrentHashMap<ForkedProcessEvent, ForkedProcessReportEventListener>();
+
+    private final ConcurrentMap<ForkedProcessEvent, ForkedProcessBinaryEventListener> binaryEventListeners =
+            new ConcurrentHashMap<ForkedProcessEvent, ForkedProcessBinaryEventListener>();
+
+    private final ConcurrentMap<ForkedProcessEvent, ForkedProcessStringEventListener> consoleEventListeners =
+            new ConcurrentHashMap<ForkedProcessEvent, ForkedProcessStringEventListener>();
+
+    private final ConcurrentMap<ForkedProcessEvent, ForkedProcessEventListener> controlEventListeners =
+            new ConcurrentHashMap<ForkedProcessEvent, ForkedProcessEventListener>();
+
+    public void setSystemPropertiesListener( ForkedProcessPropertyEventListener listener )
+    {
+        propertyEventListener = requireNonNull( listener );
+    }
+
+    public void setTestSetStartingListener( ForkedProcessReportEventListener 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( ForkedProcessBinaryEventListener listener )
+    {
+        binaryEventListeners.put( BOOTERCODE_STDOUT, requireNonNull( listener ) );
+    }
+
+    public void setStdErrListener( ForkedProcessBinaryEventListener listener )
+    {
+        binaryEventListeners.put( BOOTERCODE_STDERR, 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() ), ":" );
+        RunMode mode = tokenizer.hasMoreTokens() ? MODES.get( tokenizer.nextToken() ) : null;
+        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() )
+            {
+                ForkedProcessBinaryEventListener listener = binaryEventListeners.get( event );
+                Charset encoding = tokenizer.hasMoreTokens() ? Charset.forName( tokenizer.nextToken() ) : null;
+                if ( listener != null && encoding != null && mode != null )
+                {
+                    byte[] stream = tokenizer.hasMoreTokens() ? decodeToBytes( tokenizer.nextToken() ) : EMPTY;
+                    listener.handle( mode, new String( stream, encoding ) );
+                }
+            }
+            else if ( event.isSysPropCategory() )
+            {
+                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 );
+                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 );
+    }
+
+    static String decode( String line, Charset encoding )
+    {
+        return line == null || "-".equals( line ) ? null : new String( parseBase64Binary( line ), encoding );
+    }
+
+    static Integer decodeToInteger( String line )
+    {
+        return line == null || "-".equals( line ) ? null : Integer.decode( line );
+    }
+
+    static byte[] decodeToBytes( String line )
+    {
+        return line == null || "-".equals( line ) ? null : parseBase64Binary( 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..7a2bc9b 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 2.20.1
  */
-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/ForkedProcessBinaryEventListener.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/ForkedProcessBinaryEventListener.java
index 06d6414..ff65045 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/ForkedProcessBinaryEventListener.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 2.20.1
  */
-public interface ConsoleOutputReceiver
+public interface ForkedProcessBinaryEventListener
 {
-
-    /**
-     * 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 );
 }
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..450a7e1 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 2.20.1
  */
-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..b9d88c2 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 2.20.1
  */
-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..647facc 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 2.20.1
  */
-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..476f90b 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,14 @@ 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 2.20.1
  */
-public interface ConsoleOutputReceiver
+public interface ForkedProcessReportEventListener
 {
-
-    /**
-     * 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, ReportEntry 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..641fd50 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 2.20.1
  */
-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/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..3641ba1 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 2.20.1
  */
-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/booterclient/output/MultipleFailureException.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/MultipleFailureException.java
index 4149ad9..f465393 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/MultipleFailureException.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/MultipleFailureException.java
@@ -58,7 +58,8 @@ final class MultipleFailureException
     public String getMessage()
     {
         StringBuilder messages = new StringBuilder();
-        for ( Throwable exception = exceptions.peek(); exception != null; exception = exceptions.peek() )
+        for ( //noinspection ThrowableResultOfMethodCallIgnored
+                Throwable exception = exceptions.peek(); exception != null; exception = exceptions.peek() )
         {
             if ( messages.length() != 0 )
             {
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 f9e59fe..3bb0b06 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
@@ -25,6 +25,7 @@ import java.io.IOException;
 
 import org.apache.maven.surefire.report.ReportEntry;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.apache.maven.plugin.surefire.report.FileReporter.getReportFile;
 
 /**
@@ -79,7 +80,7 @@ public class ConsoleOutputFileReporter
         }
     }
 
-    public void writeTestOutput( byte[] buf, int off, int len, boolean stdout )
+    public void writeTestOutput( String output, boolean stdout )
     {
         try
         {
@@ -93,7 +94,7 @@ public class ConsoleOutputFileReporter
                 File file = getReportFile( reportsDirectory, reportEntryName, reportNameSuffix, "-output.txt" );
                 fileOutputStream = new FileOutputStream( file );
             }
-            fileOutputStream.write( buf, off, len );
+            fileOutputStream.write( output.getBytes( UTF_8 ) );
         }
         catch ( IOException e )
         {
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 25312aa..ab11c5c 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
@@ -20,14 +20,9 @@ package org.apache.maven.plugin.surefire.report;
  */
 
 import java.io.PrintStream;
-import java.nio.ByteBuffer;
-import java.nio.CharBuffer;
-import java.nio.charset.CharacterCodingException;
 
 import org.apache.maven.surefire.report.ReportEntry;
 
-import static java.nio.charset.Charset.defaultCharset;
-
 /**
  * Outputs test system out/system err directly to the console
  * <p/>
@@ -50,18 +45,11 @@ public class DirectConsoleOutput
     }
 
 
-    public void writeTestOutput( byte[] buf, int off, int len, boolean stdout )
+    @Override
+    public void writeTestOutput( String output, boolean stdout )
     {
         final PrintStream stream = stdout ? sout : serr;
-        try
-        {
-            CharBuffer decode = defaultCharset().newDecoder().decode( ByteBuffer.wrap( buf, off, len ) );
-            stream.append( decode );
-        }
-        catch ( CharacterCodingException e )
-        {
-            stream.write( buf, off, len );
-        }
+        stream.append( output );
     }
 
     public void testSetStarting( ReportEntry reportEntry )
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/NullConsoleOutputReceiver.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/NullConsoleOutputReceiver.java
index e18f3aa..e0cd14e 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/NullConsoleOutputReceiver.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/report/NullConsoleOutputReceiver.java
@@ -49,7 +49,8 @@ class NullConsoleOutputReceiver
     {
     }
 
-    public void writeTestOutput( byte[] buf, int off, int len, boolean stdout )
+    @Override
+    public void writeTestOutput( String output, boolean stdout )
     {
     }
 }
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 397c4c1..85788f5 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
@@ -45,6 +45,7 @@ import java.util.StringTokenizer;
 import static org.apache.maven.plugin.surefire.report.DefaultReporterFactory.TestResultType;
 import static org.apache.maven.plugin.surefire.report.FileReporterUtils.stripIllegalFilenameChars;
 import static org.apache.maven.surefire.util.internal.StringUtils.isBlank;
+import static org.apache.maven.surefire.util.internal.StringUtils.isNotBlank;
 
 // CHECKSTYLE_OFF: LineLength
 /**
@@ -335,7 +336,7 @@ public class StatelessXmlReporter
         }
         if ( report.getSourceName() != null )
         {
-            if ( reportNameSuffix != null && reportNameSuffix.length() > 0 )
+            if ( isNotBlank( reportNameSuffix ) )
             {
                 ppw.addAttribute( "classname", report.getSourceName() + "(" + reportNameSuffix + ")" );
             }
@@ -381,7 +382,7 @@ public class StatelessXmlReporter
 
         String stackTrace = report.getStackTrace( trimStackTrace );
 
-        if ( report.getMessage() != null && report.getMessage().length() > 0 )
+        if ( isNotBlank( report.getMessage() ) )
         {
             ppw.addAttribute( "message", extraEscape( report.getMessage(), true ) );
         }
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 f0f996d..d72f9d9 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
@@ -19,21 +19,25 @@ package org.apache.maven.plugin.surefire.report;
  * under the License.
  */
 
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
 import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
 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 java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
 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 org.apache.maven.surefire.util.internal.ObjectUtils.requireNonNull;
 
 /**
  * Reports data for a single test set.
@@ -69,6 +73,8 @@ public class TestSetRunListener
 
     private final StatisticsReporter statisticsReporter;
 
+    private volatile RunMode runMode = NORMAL_RUN;
+
     @SuppressWarnings( "checkstyle:parameternumber" )
     public TestSetRunListener( ConsoleReporter consoleReporter, FileReporter fileReporter,
                                StatelessXmlReporter simpleXMLReporter,
@@ -116,24 +122,19 @@ public class TestSetRunListener
         consoleReporter.getConsoleLogger().error( t );
     }
 
-    public void writeTestOutput( byte[] buf, int off, int len, boolean stdout )
+    public void writeTestOutput( String output, boolean stdout )
     {
         try
         {
-            if ( stdout )
-            {
-                testStdOut.write( buf, off, len );
-            }
-            else
-            {
-                testStdErr.write( buf, off, len );
-            }
+            byte[] content = output.getBytes( UTF_8 );
+            Utf8RecodingDeferredFileOutputStream stream = stdout ? testStdOut : testStdErr;
+            stream.write( content, 0, content.length );
         }
         catch ( IOException e )
         {
             throw new RuntimeException( e );
         }
-        consoleOutputReceiver.writeTestOutput( buf, off, len, stdout );
+        consoleOutputReceiver.writeTestOutput( output, stdout );
     }
 
     public void testSetStarting( ReportEntry report )
@@ -219,6 +220,13 @@ public class TestSetRunListener
     {
     }
 
+    public RunMode markAs( RunMode currentRunMode )
+    {
+        RunMode runMode = this.runMode;
+        this.runMode = requireNonNull( currentRunMode );
+        return runMode;
+    }
+
     public void testAssumptionFailure( ReportEntry report )
     {
         testSkipped( 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 954a2bb..62a7443 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
@@ -44,7 +44,7 @@ class Utf8RecodingDeferredFileOutputStream
     @SuppressWarnings( "checkstyle:magicnumber" )
     public 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 )
@@ -58,13 +58,14 @@ class Utf8RecodingDeferredFileOutputStream
         if ( !Charset.defaultCharset().equals( UTF8 ) )
         {
             CharBuffer decodedFromDefaultCharset = Charset.defaultCharset().decode( ByteBuffer.wrap( buf, off, len ) );
+            //todo check encoding
             ByteBuffer utf8Encoded = UTF8.encode( decodedFromDefaultCharset );
 
             if ( utf8Encoded.hasArray() )
             {
                 byte[] convertedBytes = utf8Encoded.array();
 
-                deferredFileOutputStream.write( convertedBytes, utf8Encoded.position(), utf8Encoded.remaining() );
+                deferredFileOutputStream.write( convertedBytes, utf8Encoded.arrayOffset(), utf8Encoded.remaining() );
             }
             else
             {
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 2394ff0..16b97cf 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
@@ -23,6 +23,7 @@ import org.apache.maven.surefire.report.ReportEntry;
 import org.apache.maven.surefire.report.StackTraceWriter;
 
 import static org.apache.maven.surefire.util.internal.StringUtils.NL;
+import static org.apache.maven.surefire.util.internal.StringUtils.isNotBlank;
 
 /**
  * @author Kristian Rosenvold
@@ -125,7 +126,7 @@ public class WrappedReportEntry
 
     public String getReportName( String suffix )
     {
-        return suffix != null && suffix.length() > 0 ? getReportName() + "(" + suffix + ")" : getReportName();
+        return isNotBlank( suffix ) ? getReportName() + "(" + suffix + ")" : getReportName();
     }
 
     public String getOutput( boolean trimStackTrace )
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 a53db02..256e252 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
@@ -41,11 +41,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/main/java/org/apache/maven/plugin/surefire/util/FileScanner.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/util/FileScanner.java
index 6636f29..8f4f0fd 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/util/FileScanner.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/util/FileScanner.java
@@ -62,7 +62,7 @@ final class FileScanner
             for ( File fileOrDir : filesAndDirs )
             {
                 String name = fileOrDir.getName();
-                if ( name.length() != 0 )
+                if ( !name.isEmpty() )
                 {
                     if ( fileOrDir.isFile() )
                     {
@@ -74,7 +74,7 @@ final class FileScanner
                             if ( filter.shouldRun( toFile( path, simpleClassName ), null ) )
                             {
                                 String fullyQualifiedClassName =
-                                    pAckage.length() == 0 ? simpleClassName : pAckage + '.' + simpleClassName;
+                                    pAckage.isEmpty() ? simpleClassName : pAckage + '.' + simpleClassName;
                                 scannedJavaClassNames.add( fullyQualifiedClassName );
                             }
                         }
@@ -122,7 +122,7 @@ final class FileScanner
     private String toFile( String path, String fileNameWithoutExtension )
     {
         String pathWithoutExtension =
-            path.length() == 0 ? fileNameWithoutExtension : path + '/' + fileNameWithoutExtension;
+            path.isEmpty() ? fileNameWithoutExtension : path + '/' + fileNameWithoutExtension;
         return pathWithoutExtension + ext;
     }
 }
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 19356f5..461b6ce 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;
@@ -38,7 +39,6 @@ import org.apache.maven.surefire.report.StackTraceWriter;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.PrintStream;
-import java.nio.charset.Charset;
 import java.util.List;
 import java.util.Properties;
 import java.util.StringTokenizer;
@@ -57,10 +57,6 @@ public class ForkingRunListenerTest
 
     private final PrintStream printStream, anotherPrintStream;
 
-    final int defaultChannel = 17;
-
-    final int anotherChannel = 18;
-
     public ForkingRunListenerTest()
     {
         content = new ByteArrayOutputStream();
@@ -76,20 +72,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
     {
@@ -207,7 +189,7 @@ public class ForkingRunListenerTest
     {
         final StandardTestRun standardTestRun = new StandardTestRun();
         ConsoleOutputReceiver directConsoleReporter = (ConsoleOutputReceiver) standardTestRun.run();
-        directConsoleReporter.writeTestOutput( "HeyYou".getBytes(), 0, 6, true );
+        directConsoleReporter.writeTestOutput( "HeyYou", true );
         standardTestRun.assertExpected( MockReporter.STDOUT, "HeyYou" );
     }
 
@@ -218,7 +200,7 @@ public class ForkingRunListenerTest
         standardTestRun.run();
 
         reset();
-        createForkingRunListener( defaultChannel );
+        createForkingRunListener();
 
         TestSetMockReporterFactory providerReporterFactory = new TestSetMockReporterFactory();
         final Properties testVmSystemProperties = new Properties();
@@ -239,7 +221,7 @@ public class ForkingRunListenerTest
         standardTestRun.run();
 
         reset();
-        RunListener forkingReporter = createForkingRunListener( defaultChannel );
+        RunListener forkingReporter = createForkingRunListener();
 
         ReportEntry reportEntry = createDefaultReportEntry();
         forkingReporter.testSetStarting( reportEntry );
@@ -269,10 +251,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();
@@ -336,9 +318,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
@@ -349,7 +331,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 b8c9ef0..1b5e704 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
@@ -26,6 +26,7 @@ import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
 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;
 
 /**
  * Internal tests use only.
@@ -115,6 +116,11 @@ public class MockReporter
     {
     }
 
+    @Override
+    public RunMode markAs(RunMode currentRunMode) {
+        return null;
+    }
+
     public void testSkippedByUser( ReportEntry report )
     {
         testSkipped( report );
@@ -179,9 +185,10 @@ public class MockReporter
     {
     }
 
-    public void writeTestOutput( byte[] buf, int off, int len, boolean stdout )
+    @Override
+    public void writeTestOutput( String output, boolean stdout )
     {
         events.add( stdout ? STDOUT : STDERR );
-        data.add( new String( buf, off, len ) );
+        data.add( output );
     }
 }
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputReceiver.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/output/ForkClientTest.java
similarity index 60%
copy from surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputReceiver.java
copy to maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/output/ForkClientTest.java
index 06d6414..3927ebc 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputReceiver.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/output/ForkClientTest.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,20 @@ package org.apache.maven.surefire.report;
  * under the License.
  */
 
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
 /**
- * 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 2.20.1
  */
-public interface ConsoleOutputReceiver
+public class ForkClientTest
 {
+    @Test
+    public void test()
+    {
+        ForkClient client = new ForkClient( null, null, null, null );
+    }
 
-    /**
-     * 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 );
-
-}
+}
\ No newline at end of file
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..1819b27
--- /dev/null
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/output/ForkedChannelDecoderTest.java
@@ -0,0 +1,717 @@
+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.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.Charset.defaultCharset;
+import static javax.xml.bind.DatatypeConverter.printBase64Binary;
+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.Mockito.mock;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
+ * @since 2.20.1
+ */
+@RunWith( Enclosed.class )
+public class ForkedChannelDecoderTest
+{
+    public static class DecoderOperationsTest
+    {
+        @Rule
+        public final ExpectedException rule = none();
+
+        @Test
+        public void shouldBeFailSafe()
+        {
+            Charset encoding = Charset.defaultCharset();
+            assertThat( ForkedChannelDecoder.decode( null, encoding ) ).isNull();
+            assertThat( ForkedChannelDecoder.decode( "-", encoding ) ).isNull();
+            assertThat( ForkedChannelDecoder.decodeToInteger( null ) ).isNull();
+            assertThat( ForkedChannelDecoder.decodeToInteger( "-" ) ).isNull();
+            assertThat( ForkedChannelDecoder.decodeToBytes( null ) ).isNull();
+            assertThat( ForkedChannelDecoder.decodeToBytes( "-" ) ).isNull();
+        }
+
+        @Test
+        public void shouldHaveSystemProperty() throws IOException
+        {
+            Stream out = Stream.newStream();
+            ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( defaultCharset(), out );
+            forkedChannelEncoder.sendSystemProperties();
+
+            ForkedChannelDecoder decoder = new ForkedChannelDecoder();
+            decoder.setSystemPropertiesListener( new PropertyEventAssertionListener() );
+            LineNumberReader reader = out.newReader( defaultCharset() );
+            for ( String line; ( line = reader.readLine() ) != null; )
+            {
+                decoder.handleEvent( line, new AssertionErrorHandler() );
+            }
+            assertThat( reader.getLineNumber() ).isPositive();
+        }
+
+        @Test
+        public void shouldRecognizeEmptyStream4ReportEntry()
+        {
+            ReportEntry reportEntry = toReportEntry( null, null, "", null, null, "", "", "", null );
+            assertThat( reportEntry ).isNull();
+
+            reportEntry = toReportEntry( defaultCharset(), "", "", "", "", "-", "", "", "" );
+            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( defaultCharset(), "", "", "", "", "", "", "", "" );
+            fail();
+        }
+
+        @Test
+        public void testCreatingReportEntry()
+        {
+            final Charset utf8 = Charset.forName( "UTF-8" );
+
+            final String exceptionMessage = "msg";
+            final String encodedExceptionMsg = printBase64Binary( toArray( utf8.encode( exceptionMessage ) ) );
+
+            final String smartStackTrace = "MyTest:86 >> Error";
+            final String encodedSmartStackTrace = printBase64Binary( toArray( utf8.encode( smartStackTrace ) ) );
+
+            final String stackTrace = "Exception: msg\ntrace line 1\ntrace line 2";
+            final String encodedStackTrace = printBase64Binary( toArray( utf8.encode( stackTrace ) ) );
+
+            final String trimmedStackTrace = "trace line 1\ntrace line 2";
+            final String encodedTrimmedStackTrace = printBase64Binary( toArray( utf8.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 = printBase64Binary( toArray( utf8.encode( reportEntry.getSourceName() ) ) );
+            String encodedName = printBase64Binary( toArray( utf8.encode( reportEntry.getName() ) ) );
+            String encodedGroup = printBase64Binary( toArray( utf8.encode( reportEntry.getGroup() ) ) );
+            String encodedMessage = printBase64Binary( toArray( utf8.encode( reportEntry.getMessage() ) ) );
+
+            ReportEntry decodedReportEntry = toReportEntry( utf8, 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( utf8, 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( utf8, 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( utf8, 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( utf8, 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( defaultCharset(), out );
+            forkedChannelEncoder.bye();
+            String read = new String( out.toByteArray(), defaultCharset() );
+            assertThat( read )
+                    .isEqualTo( ":maven:surefire:std:out:normal-run:bye\n" );
+            LineNumberReader lines = out.newReader( defaultCharset() );
+            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( defaultCharset(), out );
+            forkedChannelEncoder.stopOnNextTest();
+            String read = new String( out.toByteArray(), defaultCharset() );
+            assertThat( read )
+                    .isEqualTo( ":maven:surefire:std:out:normal-run:stop-on-next-test\n" );
+            LineNumberReader lines = out.newReader( defaultCharset() );
+            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( defaultCharset(), out );
+            forkedChannelEncoder.acquireNextTest();
+            String read = new String( out.toByteArray(), defaultCharset() );
+            assertThat( read )
+                    .isEqualTo( ":maven:surefire:std:out:normal-run:next-test\n" );
+            LineNumberReader lines = out.newReader( defaultCharset() );
+            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( defaultCharset(), out );
+            forkedChannelEncoder.console( "msg" );
+
+            LineNumberReader lines = out.newReader( defaultCharset() );
+            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( defaultCharset(), out );
+            forkedChannelEncoder.error( "msg" );
+
+            LineNumberReader lines = out.newReader( defaultCharset() );
+            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( defaultCharset(), out );
+            forkedChannelEncoder.error( t );
+
+            LineNumberReader lines = out.newReader( defaultCharset() );
+            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( defaultCharset(), out );
+            StackTraceWriter stackTraceWriter = new DeserializedStacktraceWriter( "1", "2", "3" );
+            forkedChannelEncoder.error( stackTraceWriter, false );
+
+            LineNumberReader lines = out.newReader( defaultCharset() );
+            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( defaultCharset(), out );
+            forkedChannelEncoder.debug( "msg" );
+
+            LineNumberReader lines = out.newReader( defaultCharset() );
+            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( defaultCharset(), out );
+            forkedChannelEncoder.warning( "msg" );
+
+            LineNumberReader lines = out.newReader( defaultCharset() );
+            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
+        {
+            Charset streamEncoding = Charset.forName( "UTF-8" );
+            Stream out = Stream.newStream();
+            ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( streamEncoding, out );
+
+            final Charset encoding = defaultCharset();
+            byte[] msgArray = toArray( encoding.encode( "msg" ) );
+            assertThat( encoding.decode( ByteBuffer.wrap( msgArray ) ).toString() ).isEqualTo( "msg" );
+            forkedChannelEncoder.stdOut( msgArray, 0, msgArray.length );
+
+            LineNumberReader lines = out.newReader( defaultCharset() );
+            ForkedChannelDecoder decoder = new ForkedChannelDecoder();
+            decoder.setStdOutListener( new BinaryEventAssertionListener( NORMAL_RUN, "msg" ) );
+            AssertionErrorHandler errorHandler = mock( AssertionErrorHandler.class );
+            decoder.handleEvent( lines.readLine(), errorHandler );
+            verifyZeroInteractions(errorHandler);
+            assertThat( lines.readLine() )
+                    .isNull();
+        }
+
+        @Test
+        public void testStdErrStream() throws IOException
+        {
+            Charset streamEncoding = Charset.forName( "ISO-8859-1" );
+            Stream out = Stream.newStream();
+            ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( streamEncoding, out );
+
+            final Charset encoding = defaultCharset();
+            byte[] msgArray = toArray( encoding.encode( "msg" ) );
+            assertThat( encoding.decode( ByteBuffer.wrap( msgArray ) ).toString() ).isEqualTo( "msg" );
+            forkedChannelEncoder.stdErr( msgArray, 0, msgArray.length );
+
+            LineNumberReader lines = out.newReader( defaultCharset() );
+            ForkedChannelDecoder decoder = new ForkedChannelDecoder();
+            decoder.setStdErrListener( new BinaryEventAssertionListener( NORMAL_RUN, "msg" ) );
+            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();
+
+            Charset streamEncoding = Charset.forName( "ISO-8859-1" );
+            ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( streamEncoding, out );
+            forkedChannelEncoder.sendSystemProperties();
+
+            LineNumberReader lines = out.newReader( streamEncoding );
+            ForkedChannelDecoder decoder = new ForkedChannelDecoder();
+            decoder.setSystemPropertiesListener( new PropertyEventAssertionListener() );
+            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( defaultCharset(), 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( defaultCharset() ).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 BinaryEventAssertionListener implements ForkedProcessBinaryEventListener
+    {
+        private final RunMode runMode;
+        private final String output;
+
+        BinaryEventAssertionListener( RunMode runMode, String output )
+        {
+            this.runMode = runMode;
+            this.output = output;
+        }
+
+        public void handle( RunMode runMode, String output )
+        {
+            assertThat( runMode )
+                    .isEqualTo( this.runMode );
+
+            assertThat( output )
+                    .isEqualTo( this.output );
+        }
+    }
+
+    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;
+
+        public Stream( ByteArrayOutputStream out )
+        {
+            super( out, true );
+            this.out = out;
+        }
+
+        byte[] toByteArray()
+        {
+            return out.toByteArray();
+        }
+
+        LineNumberReader newReader( Charset streamCharset )
+        {
+            return new LineNumberReader( new StringReader( new String( toByteArray(), streamCharset ) ) );
+        }
+
+        static Stream newStream()
+        {
+            return new Stream( new ByteArrayOutputStream() );
+        }
+    }
+
+    private static byte[] toArray( ByteBuffer buffer )
+    {
+        return Arrays.copyOfRange( buffer.array(), buffer.arrayOffset(), buffer.arrayOffset() + buffer.remaining() );
+    }
+}
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/surefire/JUnit4SuiteTest.java b/maven-surefire-common/src/test/java/org/apache/maven/surefire/JUnit4SuiteTest.java
index 9fb45bf..0c92635 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
@@ -30,6 +30,7 @@ import org.apache.maven.plugin.surefire.booterclient.ForkConfigurationTest;
 import org.apache.maven.plugin.surefire.booterclient.ForkingRunListenerTest;
 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.report.DefaultReporterFactoryTest;
 import org.apache.maven.plugin.surefire.report.StatelessXmlReporterTest;
 import org.apache.maven.plugin.surefire.report.WrappedReportEntryTest;
@@ -75,6 +76,7 @@ import org.junit.runners.Suite;
     SurefireReflectorTest.class,
     ImmutableMapTest.class,
     SurefireHelperTest.class,
+    ForkClientTest.class
 } )
 @RunWith( Suite.class )
 public class JUnit4SuiteTest
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 6943b70..54d4667 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
@@ -43,7 +43,7 @@ public class ConsoleOutputFileReporterTest
         reportEntry = new SimpleReportEntry( this.getClass().getName(), testName );
         reporter = new ConsoleOutputFileReporter( reportDir, null );
         reporter.testSetStarting( reportEntry );
-        reporter.writeTestOutput( "some text".getBytes(), 0, 5, true );
+        reporter.writeTestOutput( "some ", true );
         reporter.testSetCompleted( reportEntry );
 
         File expectedReportFile = new File( reportDir, testName + "-output.txt" );
@@ -62,7 +62,7 @@ public class ConsoleOutputFileReporterTest
         reportEntry = new SimpleReportEntry( this.getClass().getName(), testName );
         reporter = new ConsoleOutputFileReporter( reportDir, suffixText );
         reporter.testSetStarting( reportEntry );
-        reporter.writeTestOutput( "some text".getBytes(), 0, 5, true );
+        reporter.writeTestOutput( "some ", true );
         reporter.testSetCompleted( reportEntry );
 
         File expectedReportFile = new File( reportDir, testName + "-" + suffixText + "-output.txt" );
diff --git a/surefire-api/pom.xml b/surefire-api/pom.xml
index 5f0f2fd..b705e0a 100644
--- a/surefire-api/pom.xml
+++ b/surefire-api/pom.xml
@@ -40,6 +40,11 @@
       <groupId>org.apache.maven.shared</groupId>
       <artifactId>maven-shared-utils</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <build>
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 2a713ef..10cd7a7 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;
@@ -134,9 +133,8 @@ public class BaseProviderFactory
 
     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() );
     }
 
     public void setTestRequest( TestRequest testRequest )
@@ -233,4 +231,14 @@ public class BaseProviderFactory
     {
         this.systemExitTimeout = systemExitTimeout;
     }
+
+    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 2e80f44..3702270 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;
@@ -41,7 +40,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;
@@ -50,7 +48,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;
 import static org.apache.maven.surefire.util.internal.ObjectUtils.requireNonNull;
@@ -190,7 +187,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()
     {
@@ -201,12 +198,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()
@@ -254,31 +251,31 @@ 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;
         }
 
         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;
         }
 
         public boolean hasNext()
@@ -341,12 +338,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..d8ac89a
--- /dev/null
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/booter/ForkedChannelEncoder.java
@@ -0,0 +1,461 @@
+package org.apache.maven.surefire.booter;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.apache.maven.plugin.surefire.log.api.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.Entry;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import static java.lang.System.arraycopy;
+import static java.nio.charset.Charset.defaultCharset;
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static javax.xml.bind.DatatypeConverter.printBase64Binary;
+import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_BYE;
+import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_CONSOLE_INFO;
+import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_CONSOLE_DEBUG;
+import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_CONSOLE_ERROR;
+import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_NEXT_TEST;
+import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_STDERR;
+import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_STDOUT;
+import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_STOP_ON_NEXT_TEST;
+import static org.apache.maven.surefire.booter.ForkedProcessEvent.BOOTERCODE_SYSPROPS;
+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_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_CONSOLE_WARNING;
+import static org.apache.maven.surefire.booter.ForkedProcessEvent.MAGIC_NUMBER;
+import static org.apache.maven.surefire.report.RunMode.NORMAL_RUN;
+import static org.apache.maven.surefire.report.RunMode.RERUN;
+import static org.apache.maven.surefire.util.internal.ObjectUtils.requireNonNull;
+
+/**
+ * magic number : opcode : run mode [: opcode specific data]*
+ * <p/>
+ *
+ * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
+ * @since 2.20.1
+ */
+public final class ForkedChannelEncoder
+{
+    private final Charset streamCharset;
+    private final OutputStream out;
+    private final RunMode runMode;
+    private volatile boolean trouble;
+
+    public ForkedChannelEncoder( OutputStream out )
+    {
+        this( US_ASCII, out, NORMAL_RUN );
+    }
+
+    /**
+     * For testing purposes.
+     *
+     * @param streamCharset    pipe encoding
+     * @param out              pipe
+     */
+    public ForkedChannelEncoder( Charset streamCharset, OutputStream out )
+    {
+        this( streamCharset, out, NORMAL_RUN );
+    }
+
+    private ForkedChannelEncoder( Charset streamCharset, OutputStream out, RunMode runMode )
+    {
+        this.streamCharset = requireNonNull( streamCharset );
+        this.out = requireNonNull( out );
+        this.runMode = requireNonNull( runMode );
+    }
+
+    public ForkedChannelEncoder asRerunMode() // todo apply this and rework providers
+    {
+        return new ForkedChannelEncoder( streamCharset, out, RERUN );
+    }
+
+    public ForkedChannelEncoder asNormalMode()
+    {
+        return new ForkedChannelEncoder( streamCharset, out, NORMAL_RUN );
+    }
+
+    public boolean checkError()
+    {
+        return trouble;
+    }
+
+    public void sendSystemProperties()
+    {
+        SortedMap<String, String> sortedProperties = new TreeMap<String, String>();
+        for ( Entry<?, ?> entry : System.getProperties().entrySet() )
+        {
+            Object key = entry.getKey();
+            Object value = entry.getValue();
+            if ( key instanceof String && ( value == null || value instanceof String ) )
+            {
+                sortedProperties.put( (String) key, (String) value );
+            }
+        }
+
+        for ( Entry<String, String> entry : sortedProperties.entrySet() )
+        {
+            String key = entry.getKey();
+            Object value = entry.getValue();
+            String valueAsString = value == null ? null : value.toString();
+            StringBuilder event = encode( BOOTERCODE_SYSPROPS, runMode, key, valueAsString );
+            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( byte[] buf, int off, int len )
+    {
+        StringBuilder event =
+                stdOutErr( BOOTERCODE_STDOUT.getOpcode(), runMode.geRunName(), buf, off, len, defaultCharset() );
+        encodeAndPrintEvent( event );
+    }
+
+    public void stdOut( String msg )
+    {
+        byte[] buf = ( msg == null ? "null" : msg ).getBytes( UTF_8 );
+        StringBuilder event =
+                stdOutErr( BOOTERCODE_STDOUT.getOpcode(), runMode.geRunName(), buf, 0, buf.length, UTF_8 );
+        encodeAndPrintEvent( event );
+    }
+
+    public void stdErr( byte[] buf, int off, int len )
+    {
+        StringBuilder event =
+                stdOutErr( BOOTERCODE_STDERR.getOpcode(), runMode.geRunName(), buf, off, len, defaultCharset() );
+        encodeAndPrintEvent( event );
+    }
+
+    public void stdErr( String msg )
+    {
+        byte[] buf = ( msg == null ? "null" : msg ).getBytes( UTF_8 );
+        StringBuilder event =
+                stdOutErr( BOOTERCODE_STDERR.getOpcode(), runMode.geRunName(), buf, 0, buf.length, UTF_8 );
+        encodeAndPrintEvent( event );
+    }
+
+    public void console( String msg )
+    {
+        StringBuilder event = print( BOOTERCODE_CONSOLE_INFO.getOpcode(), runMode.geRunName(), UTF_8, msg );
+        encodeAndPrintEvent( event );
+    }
+
+    public void error( String msg )
+    {
+        StringBuilder event = print( BOOTERCODE_CONSOLE_ERROR.getOpcode(), runMode.geRunName(), UTF_8, msg );
+        encodeAndPrintEvent( event );
+    }
+
+    public void error( Throwable t )
+    {
+        error( t.getLocalizedMessage(), t );
+    }
+
+    public void error( String msg, Throwable t )
+    {
+        StringBuilder encoded = encodeHeader( BOOTERCODE_CONSOLE_ERROR.getOpcode(), runMode.geRunName(), UTF_8 );
+        encode( encoded, msg, null, ConsoleLoggerUtils.toString( t ) );
+        encodeAndPrintEvent( encoded );
+    }
+
+    public void error( StackTraceWriter stackTraceWriter, boolean trimStackTraces )
+    {
+        StringBuilder encoded = encodeHeader( BOOTERCODE_CONSOLE_ERROR.getOpcode(), runMode.geRunName(), UTF_8 );
+        encode( encoded, stackTraceWriter, trimStackTraces );
+        encodeAndPrintEvent( encoded );
+    }
+
+    public void debug( String msg )
+    {
+        StringBuilder event = print( BOOTERCODE_CONSOLE_DEBUG.getOpcode(), runMode.geRunName(), UTF_8, msg );
+        encodeAndPrintEvent( event );
+    }
+
+    public void warning( String msg )
+    {
+        StringBuilder event = print( BOOTERCODE_CONSOLE_WARNING.getOpcode(), runMode.geRunName(), UTF_8, 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 );
+    }
+
+    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( runMode.geRunName(), operation.getOpcode() );
+        encodeAndPrintEvent( event );
+    }
+
+    private void encodeAndPrintEvent( StringBuilder command )
+    {
+        byte[] array = command.append( '\n' ).toString().getBytes( streamCharset );
+        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(), UTF_8 )
+                                          .append( ':' );
+
+        for ( int i = 0; i < args.length; )
+        {
+            String arg = args[i++];
+            base64WithUtf8( encodedTo, arg == null ? "-" : 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( ':' );
+        base64WithUtf8( encoded, message );
+        encoded.append( ':' );
+        base64WithUtf8( encoded, smartStackTrace );
+        encoded.append( ':' );
+        base64WithUtf8( encoded, stackTrace );
+    }
+
+    /**
+     * Used operations:<br/>
+     * <p>
+     * <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>
+     * </p>
+     */
+    static StringBuilder encode( String operation, String runMode, ReportEntry reportEntry, boolean trimStackTraces )
+    {
+        StringBuilder encodedTo = encodeHeader( operation, runMode, UTF_8 )
+                                          .append( ':' );
+
+        base64WithUtf8( encodedTo, reportEntry.getSourceName() );
+        encodedTo.append( ':' );
+        base64WithUtf8( encodedTo, reportEntry.getName() );
+        encodedTo.append( ':' );
+        base64WithUtf8( encodedTo, reportEntry.getGroup() );
+        encodedTo.append( ':' );
+        base64WithUtf8( encodedTo, reportEntry.getMessage() );
+        encodedTo.append( ':' )
+                .append( reportEntry.getElapsed() == null ? "-" : reportEntry.getElapsed().toString() );
+        encode( encodedTo, reportEntry.getStackTraceWriter(), trimStackTraces );
+
+        return encodedTo;
+    }
+
+    static StringBuilder stdOutErr( String operation, String runMode, byte[] buf, int off, int len,
+                                    Charset bufEncoding )
+    {
+        final byte[] encodeBytes;
+        if ( off == 0 && buf.length == len )
+        {
+            encodeBytes = buf;
+        }
+        else
+        {
+            encodeBytes = new byte[len];
+            arraycopy( buf, off, encodeBytes, 0, len );
+        }
+        return encodeMessage( operation, runMode, bufEncoding, printBase64Binary( encodeBytes ) );
+    }
+
+    /**
+     * Used in {@link #console(String)}, {@link #error(String)}, {@link #debug(String)} and {@link #warning(String)}
+     * and private methods extending the buffer.
+     */
+    static StringBuilder print( String operation, String runMode, Charset msgEncoding, String... msgs )
+    {
+        String[] encodedMsgs = new String[msgs.length];
+        for ( int i = 0; i < encodedMsgs.length; i++ )
+        {
+            String msg = msgs[i];
+            encodedMsgs[i] = msg == null ? "-" : toBase64( msg, msgEncoding );
+        }
+        return encodeMessage( operation, runMode, msgEncoding, encodedMsgs );
+    }
+
+    static StringBuilder encodeMessage( String operation, String runMode, Charset encoding, String... msgs )
+    {
+        StringBuilder builder = encodeHeader( operation, runMode, encoding );
+        for ( String msg : msgs )
+        {
+            builder.append( ':' )
+                    .append( msg );
+
+        }
+        return builder;
+    }
+
+    static StringBuilder encodeHeader( String operation, String runMode, Charset encoding )
+    {
+        return encodeOpcode( runMode, operation )
+                       .append( ':' )
+                       .append( encoding.name() );
+    }
+
+    /**
+     * Used in {@link #bye()}, {@link #stopOnNextTest()} and {@link #encodeOpcode(ForkedProcessEvent)}
+     * and private methods extending the buffer.
+     *
+     * @param runMode   run mode
+     * @param operation opcode
+     * @return encoded command
+     */
+    static StringBuilder encodeOpcode( String runMode, String operation )
+    {
+        return new StringBuilder( 128 )
+                .append( MAGIC_NUMBER )
+                .append( runMode )
+                .append( ':' )
+                .append( operation );
+    }
+
+    static String base64WithUtf8( String msg )
+    {
+        if ( msg == null )
+        {
+            return "-";
+        }
+        else
+        {
+            byte[] binary = msg.getBytes( UTF_8 );
+            return printBase64Binary( binary );
+        }
+    }
+
+    static void base64WithUtf8( StringBuilder encoded, String msg )
+    {
+        encoded.append( base64WithUtf8( msg ) );
+    }
+
+    private static String toStackTrace( StackTraceWriter stw, boolean trimStackTraces )
+    {
+        return trimStackTraces ? stw.writeTrimmedTraceToString() : stw.writeTraceToString();
+    }
+
+    static String toBase64( String msg, Charset encoding )
+    {
+        return msg == null ? "-" : printBase64Binary( msg.getBytes( 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..a75d5ba
--- /dev/null
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/booter/ForkedProcessEvent.java
@@ -0,0 +1,129 @@
+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 2.20.1
+ */
+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_STDERR( "std-err-stream" ),
+
+    BOOTERCODE_CONSOLE_INFO( "console" ),
+    BOOTERCODE_CONSOLE_DEBUG( "debug" ),
+    BOOTERCODE_CONSOLE_WARNING( "warning" ),
+    BOOTERCODE_CONSOLE_ERROR( "error" ),
+
+    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<String, ForkedProcessEvent>();
+        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_STDERR;
+    }
+
+    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 def345d..b3a7213 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;
@@ -37,21 +34,19 @@ 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;
     }
 
     public RunListener createReporter()
     {
-        return new ForkingRunListener( originalSystemOut, testSetChannelId.getAndIncrement(), isTrimstackTrace );
+        return new ForkingRunListener( eventChannel, trimstackTrace );
     }
 
     public RunResult close()
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 6c0842a..973e5a2 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
@@ -19,24 +19,15 @@ package org.apache.maven.surefire.booter;
  * under the License.
  */
 
-import java.io.PrintStream;
-import java.util.Properties;
-
 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 static java.lang.Integer.toHexString;
-import static java.nio.charset.Charset.defaultCharset;
-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 org.apache.maven.surefire.util.internal.ObjectUtils.requireNonNull;
 
 /**
  * Encodes the full output of the test run to the stdout stream.
@@ -51,216 +42,118 @@ 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';
+    private final ForkedChannelEncoder target;
 
-    public static final byte BOOTERCODE_NEXT_TEST = (byte) 'N';
+    private final boolean trim;
 
-    public static final byte BOOTERCODE_STOP_ON_NEXT_TEST = (byte) 'S';
+    private volatile RunMode runMode = NORMAL_RUN;
 
-    /**
-     * 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 boolean trimStackTraces;
-
-    private final byte[] stdOutHeader;
-
-    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;
+        sendProps(); // todo really needed? - sent after every test class
     }
 
     public void testSetStarting( ReportEntry report )
     {
-        encodeAndWriteToTarget( toString( BOOTERCODE_TESTSET_STARTING, report, testSetChannelId ) );
+        target.testSetStarting( report, trim );
     }
 
     public void testSetCompleted( ReportEntry report )
     {
-        encodeAndWriteToTarget( toString( BOOTERCODE_TESTSET_COMPLETED, report, testSetChannelId ) );
+        target.testSetCompleted( report, trim );
     }
 
     public void testStarting( ReportEntry report )
     {
-        encodeAndWriteToTarget( toString( BOOTERCODE_TEST_STARTING, report, testSetChannelId ) );
+        target.testStarting( report, trim );
     }
 
     public void testSucceeded( ReportEntry report )
     {
-        encodeAndWriteToTarget( toString( BOOTERCODE_TEST_SUCCEEDED, report, testSetChannelId ) );
+        target.testSucceeded( report, trim );
     }
 
     public void testAssumptionFailure( ReportEntry report )
     {
-        encodeAndWriteToTarget( toString( BOOTERCODE_TEST_ASSUMPTIONFAILURE, report, testSetChannelId ) );
+        target.testAssumptionFailure( report, trim );
     }
 
     public void testError( ReportEntry report )
     {
-        encodeAndWriteToTarget( toString( BOOTERCODE_TEST_ERROR, report, testSetChannelId ) );
+        target.testError( report, trim );
     }
 
     public void testFailed( ReportEntry report )
     {
-        encodeAndWriteToTarget( toString( BOOTERCODE_TEST_FAILED, report, testSetChannelId ) );
+        target.testFailed( report, trim );
     }
 
     public void testSkipped( ReportEntry report )
     {
-        encodeAndWriteToTarget( toString( BOOTERCODE_TEST_SKIPPED, report, testSetChannelId ) );
+        target.testSkipped( report, trim );
     }
 
     public void testExecutionSkippedByUser()
     {
-        encodeAndWriteToTarget( toString( BOOTERCODE_STOP_ON_NEXT_TEST, new SimpleReportEntry(), testSetChannelId ) );
-    }
-
-    void sendProps()
-    {
-        Properties systemProperties = System.getProperties();
-
-        if ( systemProperties != null )
-        {
-            for ( final String key : systemProperties.stringPropertyNames() )
-            {
-                String value = systemProperties.getProperty( key );
-                encodeAndWriteToTarget( toPropertyString( key, value == null ? "null" : value ) );
-            }
-        }
+        target.stopOnNextTest();
     }
 
-    public void writeTestOutput( byte[] buf, int off, int len, boolean stdout )
+    public RunMode markAs( RunMode currentRunMode )
     {
-        byte[] header = stdout ? stdOutHeader : stdErrHeader;
-        byte[] content =
-            new byte[buf.length * 3 + 1]; // Hex-escaping can be up to 3 times length of a regular byte.
-        int i = escapeBytesToPrintable( content, 0, buf, off, len );
-        content[i++] = (byte) '\n';
-        byte[] encodeBytes = new byte[header.length + i];
-        System.arraycopy( header, 0, encodeBytes, 0, header.length );
-        System.arraycopy( content, 0, encodeBytes, header.length, i );
-
-        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 with stream: " + new String( buf, off, len ) );
-            }
-        }
+        RunMode runMode = this.runMode;
+        this.runMode = requireNonNull( currentRunMode );
+        return runMode;
     }
 
-    public static byte[] createHeader( byte booterCode, int testSetChannel )
+    void sendProps()
     {
-        return encodeStringForForkCommunication( String.valueOf( (char) booterCode )
-                + ','
-                + Integer.toString( testSetChannel, 16 )
-                + ',' + defaultCharset().name()
-                + ',' );
+        target.sendSystemProperties();
     }
 
-    private void log( byte bootCode, String message )
+    public void writeTestOutput( String output, boolean stdout )
     {
-        if ( message != null )
+        if ( stdout )
         {
-            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.stdOut( output );
+        }
+        else
+        {
+            target.stdErr( output );
         }
     }
 
     public void debug( String message )
     {
-        log( BOOTERCODE_DEBUG, message );
+        target.debug( message );
     }
 
     public void info( String message )
     {
-        log( BOOTERCODE_CONSOLE, message );
+        target.console( message );
     }
 
     public void warning( String message )
     {
-        log( BOOTERCODE_WARNING, message );
+        target.warning( message );
     }
 
     public void error( String message )
     {
-        log( BOOTERCODE_ERROR, message );
+        target.error( message );
     }
 
     public void error( String message, Throwable t )
     {
-        error( ConsoleLoggerUtils.toString( message, t ) );
+        target.error( message, t );
     }
 
     public void error( Throwable t )
@@ -268,137 +161,8 @@ 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 ForkingRunListener append( StringBuilder stringBuilder, String message )
-    {
-        stringBuilder.append( encode( message ) );
-        return this;
-    }
-
-    private ForkingRunListener append( StringBuilder stringBuilder, byte b )
-    {
-        stringBuilder.append( (char) b );
-        return this;
-    }
-
-    private void nullableEncoding( StringBuilder stringBuilder, Integer source )
-    {
-        if ( source == null )
-        {
-            stringBuilder.append( "null" );
-        }
-        else
-        {
-            stringBuilder.append( source.toString() );
-        }
-    }
-
-    private String encode( String source )
-    {
-        return source;
-    }
-
-
-    private static void nullableEncoding( StringBuilder stringBuilder, String source )
-    {
-        if ( source == null || source.length() == 0 )
-        {
-            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() );
-        }
-    }
-
     public void println( String message )
     {
-        byte[] buf = message.getBytes();
-        println( buf, 0, buf.length );
-    }
-
-    public void println( byte[] buf, int off, int len )
-    {
-        writeTestOutput( buf, off, len, true );
+        writeTestOutput( message, 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 f985d26..ceb03fa 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
@@ -21,11 +21,8 @@ package org.apache.maven.surefire.booter;
 
 import java.io.DataInputStream;
 import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.nio.charset.Charset;
 
-import static org.apache.maven.surefire.util.internal.StringUtils.FORK_STREAM_CHARSET_NAME;
-import static org.apache.maven.surefire.util.internal.StringUtils.encodeStringForForkCommunication;
+import static java.nio.charset.StandardCharsets.US_ASCII;
 import static org.apache.maven.surefire.util.internal.ObjectUtils.requireNonNull;
 import static java.lang.String.format;
 
@@ -47,8 +44,6 @@ public enum MasterProcessCommand
     NOOP( 4, Void.class ),
     BYE_ACK( 5, Void.class );
 
-    private static final Charset ASCII = Charset.forName( "US-ASCII" );
-
     private final int id;
 
     private final Class<?> dataType;
@@ -143,21 +138,13 @@ public enum MasterProcessCommand
 
     String toDataTypeAsString( byte... data )
     {
-        try
-        {
-            switch ( this )
-            {
-                case RUN_CLASS:
-                    return new String( data, FORK_STREAM_CHARSET_NAME );
-                case SHUTDOWN:
-                    return new String( data, ASCII );
-                default:
-                    return null;
-            }
-        }
-        catch ( UnsupportedEncodingException e )
+        switch ( this )
         {
-            throw new IllegalStateException( e );
+            case RUN_CLASS:
+            case SHUTDOWN:
+                return new String( data, US_ASCII );
+            default:
+                return null;
         }
     }
 
@@ -166,9 +153,8 @@ public enum MasterProcessCommand
         switch ( this )
         {
             case RUN_CLASS:
-                return encodeStringForForkCommunication( data );
             case SHUTDOWN:
-                return data.getBytes( ASCII );
+                return data.getBytes( US_ASCII );
             default:
                 return new byte[0];
         }
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 9ef7a94..98040a0 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/ConsoleOutputCapture.java b/surefire-api/src/main/java/org/apache/maven/surefire/report/ConsoleOutputCapture.java
index b583593..283b1eb 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.
@@ -56,13 +55,13 @@ public class ConsoleOutputCapture
         {
             // 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 ), isStdout );
         }
 
         public void write( byte[] b )
             throws IOException
         {
-            target.writeTestOutput( b, 0, b.length, isStdout );
+            target.writeTestOutput( new String( b ), isStdout );
         }
 
         public void write( int b )
@@ -81,12 +80,7 @@ public class ConsoleOutputCapture
 
         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, isStdout );
         }
 
         public void close()
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..5019b04 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,9 @@ 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 stdout Indicates if this is stdout
      */
-    void writeTestOutput( byte[] buf, int off, int len, boolean stdout );
+    void writeTestOutput( String output, 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..22486d4 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 f1e0f48..842a745 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
@@ -38,9 +38,4 @@ public final class DefaultDirectConsoleReporter
     {
         systemOut.println( message );
     }
-
-    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 b964430..74e8e9b 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-logger-api/src/main/java/org/apache/maven/plugin/surefire/log/api/ConsoleLoggerUtils.java b/surefire-api/src/main/java/org/apache/maven/surefire/report/RunMode.java
similarity index 53%
copy from surefire-logger-api/src/main/java/org/apache/maven/plugin/surefire/log/api/ConsoleLoggerUtils.java
copy to surefire-api/src/main/java/org/apache/maven/surefire/report/RunMode.java
index 7eca90b..0323d9a 100644
--- a/surefire-logger-api/src/main/java/org/apache/maven/plugin/surefire/log/api/ConsoleLoggerUtils.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/report/RunMode.java
@@ -1,4 +1,4 @@
-package org.apache.maven.plugin.surefire.log.api;
+package org.apache.maven.surefire.report;
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,37 +19,42 @@ package org.apache.maven.plugin.surefire.log.api;
  * under the License.
  */
 
-import java.io.PrintWriter;
-import java.io.StringWriter;
+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 re-run.
+ *
  * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
- * @since 2.20
+ * @since 2.20.1
  */
-public final class ConsoleLoggerUtils
+public enum RunMode
 {
-    private ConsoleLoggerUtils()
-    {
-        throw new IllegalStateException( "non instantiable constructor" );
-    }
+    NORMAL_RUN( "normal-run" ), RERUN( "re-run" );
 
-    public static String toString( String message, Throwable t )
+    public static final Map<String, RunMode> MODES = modes();
+
+    private static Map<String, RunMode> modes()
     {
-        StringWriter result = new StringWriter( 512 );
-        PrintWriter writer = new PrintWriter( result );
-        try
-        {
-            if ( message != null )
-            {
-                writer.println( message );
-            }
-            t.printStackTrace( writer );
-            writer.flush();
-            return result.toString();
-        }
-        finally
+        Map<String, RunMode> modes = new ConcurrentHashMap<String, RunMode>();
+        for ( RunMode mode : values() )
         {
-            writer.close();
+            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 e2ae963..66cef8d 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
@@ -74,7 +74,7 @@ public class TestListResolver
                 for ( String request : split( csvTests, "," ) )
                 {
                     request = request.trim();
-                    if ( request.length() != 0 && !request.equals( "!" ) )
+                    if ( !request.isEmpty() && !request.equals( "!" ) )
                     {
                         resolveTestRequest( request, patterns, includedFilters, excludedFilters );
                     }
@@ -249,7 +249,7 @@ public class TestListResolver
         }
 
         aggregatedTest += aggregatedTest( "!", getExcludedPatterns() );
-        return aggregatedTest.length() == 0 ? "" : aggregatedTest;
+        return aggregatedTest.isEmpty() ? "" : aggregatedTest;
     }
 
     public Set<ResolvedTest> getIncludedPatterns()
@@ -309,7 +309,7 @@ public class TestListResolver
 
     static String removeExclamationMark( String s )
     {
-        return s.length() != 0 && s.charAt( 0 ) == '!' ? s.substring( 1 ) : s;
+        return !s.isEmpty() && s.charAt( 0 ) == '!' ? s.substring( 1 ) : s;
     }
 
     private static void updatedFilters( boolean isExcluded, ResolvedTest test, IncludedExcludedPatterns patterns,
@@ -334,7 +334,7 @@ public class TestListResolver
         for ( ResolvedTest test : tests )
         {
             String readableTest = test.toString();
-            if ( readableTest.length() != 0 )
+            if ( !readableTest.isEmpty() )
             {
                 if ( aggregatedTest.length() != 0 )
                 {
@@ -357,7 +357,7 @@ public class TestListResolver
             if ( exc != null )
             {
                 exc = exc.trim();
-                if ( exc.length() != 0 )
+                if ( !exc.isEmpty() )
                 {
                     if ( exc.contains( "!" ) )
                     {
@@ -457,8 +457,8 @@ public class TestListResolver
         if ( isRegexPrefixedPattern( request ) )
         {
             final String[] unwrapped = unwrapRegex( request );
-            final boolean hasClass = unwrapped[0].length() != 0;
-            final boolean hasMethod = unwrapped[1].length() != 0;
+            final boolean hasClass = !unwrapped[0].isEmpty();
+            final boolean hasMethod = !unwrapped[1].isEmpty();
             if ( hasClass && hasMethod )
             {
                 test = new ResolvedTest( unwrapped[0], unwrapped[1], true );
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 d1838b2..44410de 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,11 +19,6 @@ package org.apache.maven.surefire.util.internal;
  * under the License.
  */
 
-import java.io.UnsupportedEncodingException;
-import java.nio.ByteBuffer;
-import java.nio.CharBuffer;
-import java.nio.charset.CharacterCodingException;
-import java.nio.charset.Charset;
 import java.util.StringTokenizer;
 
 /**
@@ -62,8 +57,6 @@ public final class StringUtils
     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();
-
     // 8-bit charset Latin-1
     public static final String FORK_STREAM_CHARSET_NAME = "ISO-8859-1";
 
@@ -148,216 +141,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 str} to contain only 'printable' bytes.
-     * <p>
-     * Escaping is done by encoding the non-nicely printable bytes to {@code '\' + upperCaseHexBytes(byte)}.
-     * <p>
-     * A save length of {@code out} is {@code len * 3 + outoff}.
-     * <p>
-     * The reverse-method is {@link #unescapeBytes(byte[], String)}.
-     *
-     * @param out output buffer
-     * @param outoff offset in the output buffer
-     * @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}
-     */
-    @SuppressWarnings( "checkstyle:magicnumber" )
-    public static int escapeBytesToPrintable( byte[] out, int outoff, byte[] input, int off, int len )
-    {
-        if ( out == null )
-        {
-            throw new IllegalArgumentException( "The output array must not be null" );
-        }
-        if ( input == null || input.length == 0 )
-        {
-            return 0;
-        }
-        int outputPos = outoff;
-        int end = off + len;
-        for ( int i = off; i < end; i++ )
-        {
-            byte b = input[i];
-
-            // handle non-nicely printable bytes
-            if ( b < 32 || b > 126 || b == '\\' || b == ',' )
-            {
-                int upper = ( 0xF0 & b ) >> 4;
-                int lower = ( 0x0F & b );
-                out[outputPos++] = '\\';
-                out[outputPos++] = HEX_CHARS[upper];
-                out[outputPos++] = HEX_CHARS[lower];
-            }
-            else
-            {
-                out[outputPos++] = b;
-            }
-        }
-
-        return outputPos - outoff;
-    }
-
-    /**
-     * Reverses the effect of {@link #escapeBytesToPrintable(byte[], int, 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 ) );
-                ByteBuffer defaultEncoded = DEFAULT_CHARSET.encode( decodedFromSourceCharset );
-
-                return defaultEncoded;
-            }
-            catch ( CharacterCodingException e )
-            {
-                // ignore and fall through to the non-recoded version
-            }
-        }
-
-        return ByteBuffer.wrap( out, 0, outPos );
-    }
-
-    public static byte[] encodeStringForForkCommunication( String string )
-    {
-        try
-        {
-            return string.getBytes( FORK_STREAM_CHARSET_NAME );
-        }
-        catch ( UnsupportedEncodingException e )
-        {
-           throw new RuntimeException( "The JVM must support Charset " + FORK_STREAM_CHARSET_NAME, e );
-        }
-    }
-
-    /**
      *
      * @param buffer     Examined StringBuffer
      * @param pattern    a pattern which should start in <code>buffer</code>
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 c43a3a6..d64b5a5 100644
--- a/surefire-api/src/test/java/org/apache/maven/JUnit4SuiteTest.java
+++ b/surefire-api/src/test/java/org/apache/maven/JUnit4SuiteTest.java
@@ -39,7 +39,6 @@ import org.apache.maven.surefire.util.TestsToRunTest;
 import org.apache.maven.surefire.util.UrlUtilsTest;
 import org.apache.maven.surefire.util.internal.ByteBufferTest;
 import org.apache.maven.surefire.util.internal.ConcurrencyUtilsTest;
-import org.apache.maven.surefire.util.internal.StringUtilsTest;
 import org.junit.runner.RunWith;
 import org.junit.runners.Suite;
 
@@ -60,7 +59,6 @@ import org.junit.runners.Suite;
     TestListResolverTest.class,
     ByteBufferTest.class,
     ConcurrencyUtilsTest.class,
-    StringUtilsTest.class,
     DefaultDirectoryScannerTest.class,
     RunOrderCalculatorTest.class,
     RunOrderTest.class,
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..a52727d
--- /dev/null
+++ b/surefire-api/src/test/java/org/apache/maven/surefire/booter/ForkedChannelEncoderTest.java
@@ -0,0 +1,481 @@
+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.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.Arrays;
+
+import static java.nio.charset.Charset.defaultCharset;
+import static javax.xml.bind.DatatypeConverter.printBase64Binary;
+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.ForkedChannelEncoder.print;
+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;
+
+/**
+ * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
+ * @since 2.20.1
+ */
+public class ForkedChannelEncoderTest
+{
+
+    @Test
+    public void shouldBeFailSafe()
+    {
+        Charset encoding = Charset.defaultCharset();
+        assertThat( ForkedChannelEncoder.toBase64( null, encoding ) ).isEqualTo( "-" );
+        assertThat( ForkedChannelEncoder.base64WithUtf8( null ) ).isEqualTo( "-" );
+        StringBuilder builder = new StringBuilder();
+        ForkedChannelEncoder.base64WithUtf8( builder, null );
+        assertThat( builder.toString() ).isEqualTo( "-" );
+    }
+
+    @Test
+    public void shouldHaveSystemProperty()
+    {
+        StringBuilder actualEncoded = encode( BOOTERCODE_SYSPROPS, NORMAL_RUN, "arg1", "arg2" );
+        String expected = MAGIC_NUMBER + "normal-run:" + BOOTERCODE_SYSPROPS.getOpcode() + ":UTF-8:YXJnMQ==:YXJnMg==";
+
+        assertThat( actualEncoded.toString() )
+                .isEqualTo( expected );
+    }
+
+    @Test
+    public void safeThrowableShouldBeEncoded()
+    {
+        final Charset utf8 = Charset.forName( "UTF-8" );
+
+        final String exceptionMessage = "msg";
+        final String encodedExceptionMsg = printBase64Binary( toArray( utf8.encode( exceptionMessage ) ) );
+
+        final String smartStackTrace = "MyTest:86 >> Error";
+        final String encodedSmartStackTrace = printBase64Binary( toArray( utf8.encode( smartStackTrace ) ) );
+
+        final String stackTrace = "trace line 1\ntrace line 2";
+        final String encodedStackTrace = printBase64Binary( toArray( utf8.encode( stackTrace ) ) );
+
+        final String trimmedStackTrace = "trace line 1\ntrace line 2";
+        final String encodedTrimmedStackTrace = printBase64Binary( toArray( utf8.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 Charset utf8 = Charset.forName( "UTF-8" );
+
+        final String exceptionMessage = "msg";
+        final String encodedExceptionMsg = printBase64Binary( toArray( utf8.encode( exceptionMessage ) ) );
+
+        final String smartStackTrace = "MyTest:86 >> Error";
+        final String encodedSmartStackTrace = printBase64Binary( toArray( utf8.encode( smartStackTrace ) ) );
+
+        final String stackTrace = "trace line 1\ntrace line 2";
+        final String encodedStackTrace = printBase64Binary( toArray( utf8.encode( stackTrace ) ) );
+
+        final String trimmedStackTrace = "trace line 1\ntrace line 2";
+        final String encodedTrimmedStackTrace = printBase64Binary( toArray( utf8.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 = printBase64Binary( toArray( utf8.encode( reportEntry.getSourceName() ) ) );
+        String encodedName = printBase64Binary( toArray( utf8.encode( reportEntry.getName() ) ) );
+        String encodedGroup = printBase64Binary( toArray( utf8.encode( reportEntry.getGroup() ) ) );
+        String encodedMessage = printBase64Binary( toArray( utf8.encode( reportEntry.getMessage() ) ) );
+
+        StringBuilder encode = encode( "X", "normal-run", reportEntry, false );
+        assertThat( encode.toString() )
+                .isEqualTo( ":maven:surefire:std:out:normal-run:X: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:normal-run:X:UTF-8:"
+                                    + encodedSourceName
+                                    + ":"
+                                    + encodedName
+                                    + ":"
+                                    + encodedGroup
+                                    + ":"
+                                    + encodedMessage
+                                    + ":"
+                                    + 102
+                                    + ":"
+
+                                    + encodedExceptionMsg
+                                    + ":"
+                                    + encodedSmartStackTrace
+                                    + ":"
+                                    + encodedTrimmedStackTrace
+                );
+
+        Stream out = Stream.newStream();
+        ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( defaultCharset(), out );
+
+        forkedChannelEncoder.testSetStarting( reportEntry, true );
+        LineNumberReader printedLines = out.newReader( defaultCharset() );
+        assertThat( printedLines.readLine() )
+                .isEqualTo( ":maven:surefire:std:out:normal-run:testset-starting:UTF-8:"
+                                    + encodedSourceName
+                                    + ":"
+                                    + encodedName
+                                    + ":"
+                                    + encodedGroup
+                                    + ":"
+                                    + encodedMessage
+                                    + ":"
+                                    + 102
+                                    + ":"
+
+                                    + encodedExceptionMsg
+                                    + ":"
+                                    + encodedSmartStackTrace
+                                    + ":"
+                                    + encodedTrimmedStackTrace
+                );
+        assertThat( printedLines.readLine() ).isNull();
+
+        out = Stream.newStream();
+        forkedChannelEncoder = new ForkedChannelEncoder( defaultCharset(), out );
+
+        forkedChannelEncoder.testSetStarting( reportEntry, false );
+        printedLines = out.newReader( defaultCharset() );
+        assertThat( printedLines.readLine() )
+                .isEqualTo( ":maven:surefire:std:out:normal-run:testset-starting:UTF-8:"
+                                    + encodedSourceName
+                                    + ":"
+                                    + encodedName
+                                    + ":"
+                                    + encodedGroup
+                                    + ":"
+                                    + encodedMessage
+                                    + ":"
+                                    + 102
+                                    + ":"
+
+                                    + encodedExceptionMsg
+                                    + ":"
+                                    + encodedSmartStackTrace
+                                    + ":"
+                                    + encodedStackTrace
+                );
+        assertThat( printedLines.readLine() ).isNull();
+    }
+
+    @Test
+    public void testSendOpcode()
+    {
+        StringBuilder encoded = encodeOpcode( "normal-run", "some-opcode" );
+        assertThat( encoded.toString() )
+                .isEqualTo( ":maven:surefire:std:out:normal-run:some-opcode" );
+
+        encoded = encodeHeader( "some-opcode", "normal-run", Charset.forName( "UTF-8" ) );
+        assertThat( encoded.toString() )
+                .isEqualTo( ":maven:surefire:std:out:normal-run:some-opcode:UTF-8" );
+
+        encoded = encodeMessage( "some-opcode", "normal-run", Charset.forName( "UTF-8" ), "msg" );
+        assertThat( encoded.toString() )
+                .isEqualTo( ":maven:surefire:std:out:normal-run:some-opcode:UTF-8:msg" );
+
+        encoded = print( "some-opcode", "normal-run", Charset.forName( "UTF-8" ), "msg" );
+        assertThat( encoded.toString() )
+                .isEqualTo( ":maven:surefire:std:out:normal-run:some-opcode:UTF-8:bXNn" );
+
+        encoded = print( "some-opcode", "normal-run", Charset.forName( "UTF-8" ), new String[] { null } );
+        assertThat( encoded.toString() )
+                .isEqualTo( ":maven:surefire:std:out:normal-run:some-opcode:UTF-8:-" );
+    }
+
+    @Test
+    public void testConsole()
+    {
+        Stream out = Stream.newStream();
+        ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( defaultCharset(), out );
+
+        forkedChannelEncoder.console( "msg" );
+
+        String encoded = new String( out.toByteArray(), defaultCharset() );
+
+        final Charset utf8 = Charset.forName( "UTF-8" );
+
+        String expected = ":maven:surefire:std:out:normal-run:console:UTF-8:"
+                                  + printBase64Binary( toArray( utf8.encode( "msg" ) ) )
+                                  + "\n";
+
+        assertThat( encoded )
+                .isEqualTo( expected );
+    }
+
+    @Test
+    public void testError()
+    {
+        Stream out = Stream.newStream();
+        ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( defaultCharset(), out );
+
+        forkedChannelEncoder.error( "msg" );
+
+        String encoded = new String( out.toByteArray(), defaultCharset() );
+
+        final Charset utf8 = Charset.forName( "UTF-8" );
+
+        String expected = ":maven:surefire:std:out:normal-run:error:UTF-8:"
+                                  + printBase64Binary( toArray( utf8.encode( "msg" ) ) )
+                                  + "\n";
+
+        assertThat( encoded )
+                .isEqualTo( expected );
+    }
+
+    @Test
+    public void testDebug()
+    {
+        Stream out = Stream.newStream();
+        ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( defaultCharset(), out );
+
+        forkedChannelEncoder.debug( "msg" );
+
+        String encoded = new String( out.toByteArray(), defaultCharset() );
+
+        final Charset utf8 = Charset.forName( "UTF-8" );
+
+        String expected = ":maven:surefire:std:out:normal-run:debug:UTF-8:"
+                                  + printBase64Binary( toArray( utf8.encode( "msg" ) ) )
+                                  + "\n";
+
+        assertThat( encoded )
+                .isEqualTo( expected );
+    }
+
+    @Test
+    public void testWarning()
+    {
+        Stream out = Stream.newStream();
+        ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( defaultCharset(), out );
+
+        forkedChannelEncoder.warning( "msg" );
+
+        String encoded = new String( out.toByteArray(), defaultCharset() );
+
+        final Charset utf8 = Charset.forName( "UTF-8" );
+
+        String expected = ":maven:surefire:std:out:normal-run:warning:UTF-8:"
+                                  + printBase64Binary( toArray( utf8.encode( "msg" ) ) )
+                                  + "\n";
+
+        assertThat( encoded )
+                .isEqualTo( expected );
+    }
+
+    @Test
+    public void testStdOutStream() throws IOException
+    {
+        Charset streamEncoding = Charset.forName( "UTF-8" );
+        Stream out = Stream.newStream();
+        ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( streamEncoding, out );
+
+        final Charset encoding = defaultCharset();
+        byte[] msgArray = toArray( encoding.encode( "msg" ) );
+        assertThat( encoding.decode( ByteBuffer.wrap( msgArray ) ).toString() ).isEqualTo( "msg" );
+        forkedChannelEncoder.stdOut( msgArray, 0, msgArray.length );
+
+        String expected = ":maven:surefire:std:out:normal-run:std-out-stream:" + encoding.name() + ":"
+                                  + printBase64Binary( msgArray );
+
+        LineNumberReader printedLines = out.newReader( defaultCharset() );
+        assertThat( printedLines.readLine() )
+                .isEqualTo( expected );
+        assertThat( printedLines.readLine() )
+                .isNull();
+    }
+
+    @Test
+    public void testStdErrStream() throws IOException
+    {
+        Charset streamEncoding = Charset.forName( "ISO-8859-1" );
+        Stream out = Stream.newStream();
+        ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( streamEncoding, out );
+
+        final Charset encoding = defaultCharset();
+        byte[] msgArray = toArray( encoding.encode( "msg" ) );
+        assertThat( encoding.decode( ByteBuffer.wrap( msgArray ) ).toString() ).isEqualTo( "msg" );
+        forkedChannelEncoder.stdErr( msgArray, 0, msgArray.length );
+
+        String expected = ":maven:surefire:std:out:normal-run:std-err-stream:" + encoding.name() + ":"
+                                  + printBase64Binary( msgArray );
+
+        LineNumberReader printedLines = out.newReader( defaultCharset() );
+        assertThat( printedLines.readLine() )
+                .isEqualTo( expected );
+        assertThat( printedLines.readLine() )
+                .isNull();
+    }
+
+    @Test
+    public void shouldCountSameNumberOfSystemProperties() throws IOException
+    {
+        Charset streamEncoding = Charset.forName( "ISO-8859-1" );
+        Stream out = Stream.newStream();
+        ForkedChannelEncoder forkedChannelEncoder = new ForkedChannelEncoder( streamEncoding, out );
+
+        int expectedSize = System.getProperties().size();
+        forkedChannelEncoder.sendSystemProperties();
+
+        LineNumberReader printedLines = out.newReader( streamEncoding );
+
+        int size = 0;
+        for ( String line; ( line = printedLines.readLine() ) != null; size++ )
+        {
+            assertThat( line )
+                    .startsWith( ":maven:surefire:std:out:normal-run:sys-prop:UTF-8:" );
+        }
+
+        assertThat( size )
+                .isEqualTo( expectedSize );
+    }
+
+    private static class Stream extends PrintStream
+    {
+        private final ByteArrayOutputStream out;
+
+        public 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/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..4b6d891 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
@@ -34,7 +34,7 @@ public class ForkingRunListenerTest
     {
         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 c1c8d73..0000000
--- a/surefire-api/src/test/java/org/apache/maven/surefire/util/internal/StringUtilsTest.java
+++ /dev/null
@@ -1,96 +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.charset.Charset;
-
-import junit.framework.TestCase;
-
-/**
- * @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;
-        }
-
-        byte[] escaped = new byte[input.length * 3];
-
-        int escapedBytes = StringUtils.escapeBytesToPrintable( escaped, 0, input, 0, input.length );
-
-        String escapedString = new String( escaped, 0, escapedBytes );
-
-        assertEquals( escapedBytes, escapedString.length() );
-
-        java.nio.ByteBuffer unescaped = StringUtils.unescapeBytes( escapedString, Charset.defaultCharset().name() );
-
-        assertEquals( input.length, unescaped.remaining() - unescaped.position() );
-
-        for ( int i = 0; i < input.length; i++ )
-        {
-            assertEquals( "At position " + i, input[i], unescaped.get() );
-        }
-    }
-}
diff --git a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/Classpath.java b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/Classpath.java
index e7d97c8..cc1f0eb 100644
--- a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/Classpath.java
+++ b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/Classpath.java
@@ -84,7 +84,7 @@ public class Classpath implements Iterable<String>
         for ( String element : elements )
         {
             element = element.trim();
-            if ( element.length() != 0 )
+            if ( !element.isEmpty() )
             {
                 newCp.add( element );
             }
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 fef21d1..caa99b6 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
@@ -23,7 +23,6 @@ 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.ReporterFactory;
-import org.apache.maven.surefire.report.StackTraceWriter;
 import org.apache.maven.surefire.suite.RunResult;
 import org.apache.maven.surefire.testset.TestSetFailedException;
 
@@ -49,13 +48,9 @@ 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.CommandReader.getReader;
-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.
@@ -87,7 +82,7 @@ public final class ForkedBooter
     {
         final CommandReader reader = startupMasterProcessReader();
         final ScheduledFuture<?> pingScheduler = listenToShutdownCommands( reader );
-        final PrintStream originalOut = out;
+        final ForkedChannelEncoder eventChannel = new ForkedChannelEncoder( out );
         try
         {
             final String tmpDir = args[0];
@@ -128,7 +123,7 @@ public final class ForkedBooter
             }
             else if ( readTestsFromInputStream )
             {
-                testSet = new LazyTestsToRun( originalOut );
+                testSet = new LazyTestsToRun( eventChannel );
             }
             else
             {
@@ -137,26 +132,20 @@ public final class ForkedBooter
 
             try
             {
-                runSuitesInProcess( testSet, startupConfiguration, providerConfiguration, originalOut );
+                runSuitesInProcess( testSet, startupConfiguration, providerConfiguration, eventChannel );
             }
             catch ( InvocationTargetException t )
             {
+                Throwable e = t.getTargetException();
                 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" , originalOut );
+                eventChannel.error( 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", originalOut );
+                eventChannel.error( new LegacyPojoStackTraceWriter( "test subsystem", "no method", t ), false );
             }
-            acknowledgedExit( reader, originalOut );
+            acknowledgedExit( reader, eventChannel );
         }
         catch ( Throwable t )
         {
@@ -233,17 +222,6 @@ public final class ForkedBooter
         };
     }
 
-    private static void encodeAndWriteToOutput( String string, PrintStream out )
-    {
-        byte[] encodeBytes = encodeStringForForkCommunication( string );
-        //noinspection SynchronizationOnLocalVariableOrMethodParameter
-        synchronized ( out )
-        {
-            out.write( encodeBytes, 0, encodeBytes.length );
-            out.flush();
-        }
-    }
-
     private static void kill()
     {
         Runtime.getRuntime().halt( 1 );
@@ -255,7 +233,7 @@ public final class ForkedBooter
         System.exit( returnCode );
     }
 
-    private static void acknowledgedExit( CommandReader reader, PrintStream originalOut )
+    private static void acknowledgedExit( CommandReader reader, ForkedChannelEncoder eventChannel )
     {
         final Semaphore barrier = new Semaphore( 0 );
         reader.addByeAckListener( new CommandListener()
@@ -267,7 +245,7 @@ public final class ForkedBooter
                                       }
                                   }
         );
-        encodeAndWriteToOutput( ( (char) BOOTERCODE_BYE ) + ",0,BYE!\n", originalOut );
+        eventChannel.bye();
         launchLastDitchDaemonShutdownThread( 0 );
         long timeoutMillis = max( systemExitTimeoutInSeconds * ONE_SECOND_IN_MILLIS, ONE_SECOND_IN_MILLIS );
         acquireOnePermit( barrier, timeoutMillis );
@@ -288,20 +266,20 @@ public final class ForkedBooter
 
     private static RunResult runSuitesInProcess( Object testSet, StartupConfiguration startupConfiguration,
                                                  ProviderConfiguration providerConfiguration,
-                                                 PrintStream originalSystemOut )
+                                                 ForkedChannelEncoder eventChannel )
         throws SurefireExecutionException, TestSetFailedException, InvocationTargetException
     {
-        final ReporterFactory factory = createForkingReporterFactory( providerConfiguration, originalSystemOut );
+        final ReporterFactory factory = createForkingReporterFactory( providerConfiguration, eventChannel );
 
         return invokeProviderInSameClassLoader( testSet, factory, providerConfiguration, true, startupConfiguration,
-                                                      false );
+                                                      false, eventChannel );
     }
 
     private static ReporterFactory createForkingReporterFactory( ProviderConfiguration providerConfiguration,
-                                                                 PrintStream originalSystemOut )
+                                                                 ForkedChannelEncoder eventChannel )
     {
         final boolean trimStackTrace = providerConfiguration.getReporterConfiguration().isTrimStackTrace();
-        return new ForkingReporterFactory( trimStackTrace, originalSystemOut );
+        return new ForkingReporterFactory( trimStackTrace, eventChannel );
     }
 
     private static synchronized ScheduledThreadPoolExecutor getJvmTerminator()
@@ -346,7 +324,8 @@ public final class ForkedBooter
                                                               ProviderConfiguration providerConfig,
                                                               boolean insideFork,
                                                               StartupConfiguration startupConfig,
-                                                              boolean restoreStreams )
+                                                              boolean restoreStreams,
+                                                              ForkedChannelEncoder forkedChannelEncoder )
         throws TestSetFailedException, InvocationTargetException
     {
         final PrintStream orgSystemOut = out;
@@ -356,8 +335,8 @@ public final class ForkedBooter
 
         try
         {
-            return createProviderInCurrentClassloader( startupConfig, insideFork, providerConfig, factory )
-                           .invoke( testSet );
+            return createProviderInCurrentClassloader( startupConfig, insideFork, providerConfig, factory,
+                    forkedChannelEncoder ).invoke( testSet );
         }
         finally
         {
@@ -371,12 +350,14 @@ public final class ForkedBooter
 
     private static SurefireProvider createProviderInCurrentClassloader( StartupConfiguration startupConfiguration,
                                                                         boolean isInsideFork,
-                                                                       ProviderConfiguration providerConfiguration,
-                                                                       Object reporterManagerFactory )
+                                                                        ProviderConfiguration providerConfiguration,
+                                                                        Object reporterManagerFactory,
+                                                                        ForkedChannelEncoder forkedChannelEncoder )
     {
         BaseProviderFactory bpf = new BaseProviderFactory( (ReporterFactory) reporterManagerFactory, isInsideFork );
         bpf.setTestRequest( providerConfiguration.getTestSuiteDefinition() );
         bpf.setReporterConfiguration( providerConfiguration.getReporterConfiguration() );
+        bpf.setForkedChannelEncoder( forkedChannelEncoder );
         ClassLoader classLoader = currentThread().getContextClassLoader();
         bpf.setClassLoaders( classLoader );
         bpf.setTestArtifactInfo( providerConfiguration.getTestArtifact() );
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 29a59b6..dac922a 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();
 
         public boolean hasNext()
         {
diff --git a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/TypeEncodedValue.java b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/TypeEncodedValue.java
index bbd0f70..606e76b 100644
--- a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/TypeEncodedValue.java
+++ b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/TypeEncodedValue.java
@@ -51,8 +51,8 @@ public class TypeEncodedValue
 
     public Object getDecodedValue( ClassLoader classLoader )
     {
-        // todo: use jdk6 switch case
-        if ( type.trim().length() == 0 )
+        // todo: use jdk7 switch case
+        if ( type.trim().isEmpty() )
         {
             return null;
         }
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 b731dc0..6db8339 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
@@ -94,7 +94,7 @@ public class CommandReaderTest
     @Test
     public void readJustOneClass() throws Exception
     {
-        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();
@@ -113,7 +113,7 @@ public class CommandReaderTest
     @Test
     public void manyClasses() 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() );
         assertThat( it1.next(), is( A.class.getName() ) );
@@ -129,7 +129,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
         {
             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() ) );
             }
         };
@@ -189,7 +189,7 @@ public class CommandReaderTest
         {
             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-integration-tests/src/test/java/org/apache/maven/surefire/its/fixture/SurefireLauncher.java b/surefire-integration-tests/src/test/java/org/apache/maven/surefire/its/fixture/SurefireLauncher.java
index 4cb162d..78a1450 100755
--- a/surefire-integration-tests/src/test/java/org/apache/maven/surefire/its/fixture/SurefireLauncher.java
+++ b/surefire-integration-tests/src/test/java/org/apache/maven/surefire/its/fixture/SurefireLauncher.java
@@ -70,7 +70,7 @@ public final class SurefireLauncher
     public SurefireLauncher setInProcessJavaHome()
     {
         String javaHome = System.getenv( "JAVA_HOME" );
-        if ( javaHome != null && javaHome.length() > 0 )
+        if ( javaHome != null && !javaHome.isEmpty() )
         {
             try
             {
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 7eca90b..b535758 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/junit4/MockReporter.java b/surefire-providers/common-junit4/src/main/java/org/apache/maven/surefire/junit4/MockReporter.java
index 99d7aae..e2c91ac 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
@@ -24,6 +24,7 @@ import java.util.List;
 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.RunMode;
 
 /**
  * Internal tests use only.
@@ -87,6 +88,12 @@ public class MockReporter
     {
     }
 
+    @Override
+    public RunMode markAs( RunMode currentRunMode )
+    {
+        return null;
+    }
+
     public void testSkippedByUser( ReportEntry report )
     {
         testSkipped( report );
diff --git a/surefire-providers/common-junit48/src/main/java/org/apache/maven/surefire/common/junit48/RequestedTest.java b/surefire-providers/common-junit48/src/main/java/org/apache/maven/surefire/common/junit48/RequestedTest.java
index 0eca387..52c2d52 100644
--- a/surefire-providers/common-junit48/src/main/java/org/apache/maven/surefire/common/junit48/RequestedTest.java
+++ b/surefire-providers/common-junit48/src/main/java/org/apache/maven/surefire/common/junit48/RequestedTest.java
@@ -59,7 +59,7 @@ final class RequestedTest
     public String describe()
     {
         String description = test.toString();
-        return description.length() == 0 ? "*" : description;
+        return description.isEmpty() ? "*" : description;
     }
 
     @Override
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 9ca397e..a8071cb 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
@@ -24,6 +24,7 @@ import java.util.List;
 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.testset.TestSetFailedException;
 
 import junit.framework.Test;
@@ -104,6 +105,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/pom.xml b/surefire-providers/surefire-junit47/pom.xml
index 39f68f1..aa91468 100644
--- a/surefire-providers/surefire-junit47/pom.xml
+++ b/surefire-providers/surefire-junit47/pom.xml
@@ -90,7 +90,7 @@
           </execution>
           <execution>
             <id>test</id>
-            <phase>process-sources</phase>
+            <phase>process-test-sources</phase>
             <goals>
               <goal>copy</goal>
             </goals>
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 89c6104..f212415 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.testset.TestSetFailedException;
 
@@ -118,6 +119,11 @@ public abstract class ConcurrentRunListener
         reporterManagerThreadLocal.get().testExecutionSkippedByUser();
     }
 
+    public RunMode markAs( RunMode currentRunMode )
+    {
+        return reporterManagerThreadLocal.get().markAs( currentRunMode );
+    }
+
     public void testAssumptionFailure( ReportEntry failure )
     {
         final TestMethod testMethod = getOrCreateThreadAttachedTestMethod( failure );
@@ -196,18 +202,18 @@ public abstract class ConcurrentRunListener
     }
 
 
-    public void writeTestOutput( byte[] buf, int off, int len, boolean stdout )
+    public void writeTestOutput( String output, boolean stdout )
     {
         TestMethod threadTestMethod = getThreadTestMethod();
         if ( threadTestMethod != null )
         {
             LogicalStream logicalStream = threadTestMethod.getLogicalStream();
-            logicalStream.write( stdout, buf, off, len );
+            logicalStream.write( stdout, output );
         }
         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/JUnitCoreWrapper.java b/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/JUnitCoreWrapper.java
index 2abfa58..656d55b 100644
--- a/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/JUnitCoreWrapper.java
+++ b/surefire-providers/surefire-junit47/src/main/java/org/apache/maven/surefire/junitcore/JUnitCoreWrapper.java
@@ -140,7 +140,7 @@ final class JUnitCoreWrapper
         if ( computer instanceof ParallelComputer )
         {
             String timeoutMessage = ( (ParallelComputer) computer ).describeElapsedTimeout();
-            if ( timeoutMessage.length() != 0 )
+            if ( !timeoutMessage.isEmpty() )
             {
                 throw new TestSetFailedException( timeoutMessage );
             }
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 41e09b8..c79d53a 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
@@ -23,7 +23,6 @@ import java.util.Collection;
 import java.util.concurrent.ConcurrentLinkedQueue;
 
 import org.apache.maven.surefire.report.ConsoleOutputReceiver;
-import org.apache.maven.surefire.util.internal.ByteBuffer;
 
 /**
  * A stream-like object that preserves ordering between stdout/stderr
@@ -34,31 +33,24 @@ public final class LogicalStream
 
     static final class Entry
     {
-        final boolean stdout;
+        private final boolean stdout;
+        private final String text;
 
-        final byte[] b;
-
-        final int off;
-
-        final int len;
-
-        Entry( boolean stdout, byte[] b, int off, int len )
+        Entry( boolean stdout, String text )
         {
             this.stdout = stdout;
-            this.b = ByteBuffer.copy( b, off, len );
-            this.off = 0;
-            this.len = len;
+            this.text = text;
         }
 
         public void writeDetails( ConsoleOutputReceiver outputReceiver )
         {
-            outputReceiver.writeTestOutput( b, off, len, stdout );
+            outputReceiver.writeTestOutput( text, stdout );
         }
 
         @Override
         public String toString()
         {
-            return new String( b, off, len );
+            return text;
         }
 
         public boolean isBlankLine()
@@ -67,9 +59,9 @@ public final class LogicalStream
         }
     }
 
-    public synchronized void write( boolean stdout, byte b[], int off, int len )
+    public synchronized void write( boolean stdout, String text )
     {
-        Entry entry = new Entry( stdout, b, off, len );
+        Entry entry = new Entry( stdout, text );
         if ( !entry.isBlankLine() )
         {
             output.add( entry );
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 539fd73..633385e 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
@@ -47,10 +47,10 @@ public class NonConcurrentRunListener
         super( reporter );
     }
 
-    public synchronized void writeTestOutput( byte[] buf, int off, int len, boolean stdout )
+    public synchronized void writeTestOutput( String output, boolean stdout )
     {
         // We can write immediately: no parallelism and a single class.
-        ( (ConsoleOutputReceiver) reporter ).writeTestOutput( buf, off, len, stdout );
+        ( (ConsoleOutputReceiver) reporter ).writeTestOutput( output, stdout );
     }
 
     protected SimpleReportEntry createReportEntry( Description description )
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 eb388c6..b24833a 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
@@ -172,9 +172,9 @@ class TestMethod
         return ls;
     }
 
-    public void writeTestOutput( byte[] buf, int off, int len, boolean stdout )
+    public void writeTestOutput( String output, boolean stdout )
     {
-        getLogicalStream().write( stdout, buf, off, len );
+        getLogicalStream().write( stdout, output );
     }
 
     public TestSet getTestSet()
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 db3bb41..9ff5fb4 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,21 @@ 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;
+
+import static org.apache.maven.surefire.util.internal.StringUtils.isNotBlank;
+
 /**
  * Method selector delegating to {@link GroupMatcher} to decide if a method is included or not.
  *
@@ -77,7 +79,7 @@ public class GroupMatcherMethodSelector
         {
             AndGroupMatcher matcher = new AndGroupMatcher();
             GroupMatcher in = null;
-            if ( groups != null && groups.trim().length() > 0 )
+            if ( isNotBlank( groups ) )
             {
                 in = new GroupMatcherParser( groups ).parse();
             }
@@ -88,7 +90,7 @@ public class GroupMatcherMethodSelector
             }
 
             GroupMatcher ex = null;
-            if ( excludedGroups != null && excludedGroups.trim().length() > 0 )
+            if ( isNotBlank( excludedGroups ) )
             {
                 ex = new GroupMatcherParser( excludedGroups ).parse();
             }