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:36 UTC

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

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();
             }