You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by ti...@apache.org on 2020/02/16 22:53:26 UTC

[maven-surefire] branch maven2surefire-jvm-communication updated: fault tolerant LegacyMasterProcessChannelDecoder

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

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


The following commit(s) were added to refs/heads/maven2surefire-jvm-communication by this push:
     new 85602d5  fault tolerant LegacyMasterProcessChannelDecoder
85602d5 is described below

commit 85602d5a45d30889bf82ec94dfe99ca47879cfd4
Author: tibordigana <ti...@apache.org>
AuthorDate: Sun Feb 16 23:53:15 2020 +0100

    fault tolerant LegacyMasterProcessChannelDecoder
---
 .../surefire/booter/MasterProcessCommand.java      |  11 +-
 .../providerapi/MasterProcessChannelDecoder.java   |   3 +-
 .../maven/surefire/booter/CommandReader.java       |  17 +-
 .../spi/LegacyMasterProcessChannelDecoder.java     | 163 ++++++-----
 .../spi/LegacyMasterProcessChannelEncoder.java     |   5 +-
 ...MasterProcessCommandNoMagicNumberException.java |  38 ---
 .../spi/MasterProcessUnknownCommandException.java  |  39 ---
 .../maven/surefire/booter/CommandReaderTest.java   |  14 +-
 .../maven/surefire/booter/JUnit4SuiteTest.java     |   2 +-
 .../spi/LegacyMasterProcessChannelDecoderTest.java | 298 +++++++++++++++------
 10 files changed, 309 insertions(+), 281 deletions(-)

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 6fa85a1..55e604d 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/booter/MasterProcessCommand.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/booter/MasterProcessCommand.java
@@ -19,6 +19,8 @@ package org.apache.maven.surefire.booter;
  * under the License.
  */
 
+import javax.annotation.Nonnull;
+
 import static java.nio.charset.StandardCharsets.US_ASCII;
 import static java.util.Objects.requireNonNull;
 
@@ -59,11 +61,6 @@ public enum MasterProcessCommand
         this.dataType = requireNonNull( dataType, "dataType cannot be null" );
     }
 
-    public String getOpcode()
-    {
-        return opcodeName;
-    }
-
     public Class<?> getDataType()
     {
         return dataType;
@@ -74,11 +71,11 @@ public enum MasterProcessCommand
         return dataType != Void.class;
     }
 
-    public static MasterProcessCommand byOpcode( String opcode )
+    public static MasterProcessCommand byOpcode( @Nonnull String opcode )
     {
         for ( MasterProcessCommand cmd : values() )
         {
-            if ( cmd.opcodeName.equals( opcode ) )
+            if ( cmd.opcodeName.equals( requireNonNull( opcode ) ) )
             {
                 return cmd;
             }
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/providerapi/MasterProcessChannelDecoder.java b/surefire-api/src/main/java/org/apache/maven/surefire/providerapi/MasterProcessChannelDecoder.java
index 6c64b25..4dc4908 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/providerapi/MasterProcessChannelDecoder.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/providerapi/MasterProcessChannelDecoder.java
@@ -21,6 +21,7 @@ package org.apache.maven.surefire.providerapi;
 
 import org.apache.maven.surefire.booter.Command;
 
+import javax.annotation.Nonnull;
 import java.io.IOException;
 
 /**
@@ -40,7 +41,7 @@ public interface MasterProcessChannelDecoder
      * @return decoded command
      * @throws IOException exception in channel
      */
-    Command decode() throws IOException;
+    @Nonnull Command decode() throws IOException;
 
     @Override
     void close() throws IOException;
diff --git a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/CommandReader.java b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/CommandReader.java
index 4b43fb2..e437f97 100644
--- a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/CommandReader.java
+++ b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/CommandReader.java
@@ -20,8 +20,6 @@ package org.apache.maven.surefire.booter;
  */
 
 import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
-import org.apache.maven.surefire.booter.spi.MasterProcessCommandNoMagicNumberException;
-import org.apache.maven.surefire.booter.spi.MasterProcessUnknownCommandException;
 import org.apache.maven.surefire.providerapi.CommandChainReader;
 import org.apache.maven.surefire.providerapi.CommandListener;
 import org.apache.maven.surefire.providerapi.MasterProcessChannelDecoder;
@@ -393,12 +391,12 @@ public final class CommandReader implements CommandChainReader
                             CommandReader.this.wakeupIterator();
                             callListeners( command );
                                 break;
-                            case BYE_ACK:
-                                callListeners( command );
+                        case BYE_ACK:
+                            callListeners( command );
                             // After SHUTDOWN no more commands can come.
-                                // Hence, do NOT go back to blocking in I/O.
-                                CommandReader.this.state.set( TERMINATED );
-                                break;
+                            // Hence, do NOT go back to blocking in I/O.
+                            CommandReader.this.state.set( TERMINATED );
+                            break;
                         default:
                             callListeners( command );
                             break;
@@ -419,11 +417,6 @@ public final class CommandReader implements CommandChainReader
                     // does not go to finally for non-default config: Shutdown.EXIT or Shutdown.KILL
                 }
             }
-            catch ( MasterProcessCommandNoMagicNumberException | MasterProcessUnknownCommandException e )
-            {
-                DumpErrorSingleton.getSingleton().dumpStreamException( e );
-                CommandReader.this.logger.error( e );
-            }
             catch ( IOException e )
             {
                 CommandReader.this.state.set( TERMINATED );
diff --git a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/LegacyMasterProcessChannelDecoder.java b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/LegacyMasterProcessChannelDecoder.java
index c22ee11..b4a31f3 100644
--- a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/LegacyMasterProcessChannelDecoder.java
+++ b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/LegacyMasterProcessChannelDecoder.java
@@ -23,14 +23,15 @@ import org.apache.maven.surefire.booter.Command;
 import org.apache.maven.surefire.booter.MasterProcessCommand;
 import org.apache.maven.surefire.providerapi.MasterProcessChannelDecoder;
 
+import javax.annotation.Nonnull;
 import java.io.EOFException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Iterator;
 import java.util.List;
 
+import static org.apache.maven.surefire.booter.MasterProcessCommand.MAGIC_NUMBER;
+
 /**
  * magic number : opcode [: opcode specific data]*
  * <br>
@@ -42,125 +43,117 @@ public class LegacyMasterProcessChannelDecoder implements MasterProcessChannelDe
 {
     private final InputStream is;
 
-    public LegacyMasterProcessChannelDecoder( InputStream is )
+    public LegacyMasterProcessChannelDecoder( @Nonnull InputStream is )
     {
         this.is = is;
     }
 
-    protected boolean hasData( String opcode )
-    {
-        MasterProcessCommand cmd = MasterProcessCommand.byOpcode( opcode );
-        return cmd == null || cmd.hasDataType();
-    }
-
-    @SuppressWarnings( "checkstyle:innerassignment" )
     @Override
+    @Nonnull
+    @SuppressWarnings( "checkstyle:innerassignment" )
     public Command decode() throws IOException
     {
-        List<String> tokens = new ArrayList<>();
-        StringBuilder frame = new StringBuilder();
-        boolean frameStarted = false;
-        boolean frameFinished = false;
-        boolean notEndOfStream;
-        for ( int r; notEndOfStream = ( r = is.read() ) != -1 ; )
-        {
-            char c = (char) r;
-            if ( frameFinished && c == '\n' )
-            {
-                continue;
-            }
+        List<String> tokens = new ArrayList<>( 3 );
+        StringBuilder token = new StringBuilder( MAGIC_NUMBER.length() );
+        boolean endOfStream;
 
-            if ( !frameStarted )
+        start:
+        do
+        {
+            tokens.clear();
+            token.setLength( 0 );
+            boolean frameStarted = false;
+            for ( int r; !( endOfStream = ( r = is.read() ) == -1 ) ; )
             {
-                if ( c == ':' )
+                char c = (char) r;
+                if ( !frameStarted )
                 {
-                    frameStarted = true;
-                    frameFinished = false;
-                    frame.setLength( 0 );
-                    tokens.clear();
-                    continue;
+                    if ( c == ':' )
+                    {
+                        frameStarted = true;
+                        token.setLength( 0 );
+                        tokens.clear();
+                    }
                 }
-            }
-            else if ( !frameFinished )
-            {
-                boolean isColon = c == ':';
-                if ( isColon || c == '\n' || c == '\r' )
+                else
                 {
-                    tokens.add( frame.toString() );
-                    frame.setLength( 0 );
+                    if ( c == ':' )
+                    {
+                        tokens.add( token.toString() );
+                        token.setLength( 0 );
+                        FrameCompletion completion = isFrameComplete( tokens );
+                        if ( completion == FrameCompletion.COMPLETE )
+                        {
+                            break;
+                        }
+                        else if ( completion == FrameCompletion.MALFORMED )
+                        {
+                            continue start;
+                        }
+                    }
+                    else
+                    {
+                        token.append( c );
+                    }
                 }
-                else
+            }
+
+            if ( isFrameComplete( tokens ) == FrameCompletion.COMPLETE )
+            {
+                MasterProcessCommand cmd = MasterProcessCommand.byOpcode( tokens.get( 1 ) );
+                if ( tokens.size() == 2 )
                 {
-                    frame.append( c );
+                    return new Command( cmd );
                 }
-                boolean isFinishedFrame = isTokenComplete( tokens );
-                if ( isFinishedFrame )
+                else if ( tokens.size() == 3 )
                 {
-                    frameFinished = true;
-                    frameStarted = false;
-                    break;
+                    return new Command( cmd, tokens.get( 2 ) );
                 }
             }
 
-            boolean removed = removeUnsynchronizedTokens( tokens );
-            if ( removed && tokens.isEmpty() )
+            if ( endOfStream )
             {
-                frameStarted = false;
-                frameFinished = true;
+                throw new EOFException();
             }
         }
-
-        if ( !notEndOfStream )
-        {
-            throw new EOFException();
-        }
-
-        if ( tokens.size() <= 1 ) // todo
-        {
-            throw new MasterProcessCommandNoMagicNumberException( frame.toString() );
-        }
-        if ( tokens.size() == 2 )
-        {
-            return new Command( MasterProcessCommand.byOpcode( tokens.get( 1 ) ) );
-        }
-        else if ( tokens.size() == 3 )
-        {
-            return new Command( MasterProcessCommand.byOpcode( tokens.get( 1 ) ), tokens.get( 2 ) );
-        }
-        else
-        {
-            throw new MasterProcessUnknownCommandException( frame.toString() );
-        }
+        while ( true );
     }
 
-    private boolean isTokenComplete( List<String> tokens )
+    private FrameCompletion isFrameComplete( List<String> tokens )
     {
-        if ( tokens.size() >= 2 )
+        if ( !tokens.isEmpty() && !MAGIC_NUMBER.equals( tokens.get( 0 ) ) )
         {
-            return hasData( tokens.get( 1 ) ) == ( tokens.size() == 3 );
+            return FrameCompletion.MALFORMED;
         }
-        return false;
-    }
 
-    private boolean removeUnsynchronizedTokens( Collection<String> tokens )
-    {
-        boolean removed = false;
-        for ( Iterator<String> it = tokens.iterator(); it.hasNext(); )
+        if ( tokens.size() >= 2 )
         {
-            String token = it.next();
-            if ( token.equals( MasterProcessCommand.MAGIC_NUMBER ) )
+            String opcode = tokens.get( 1 );
+            MasterProcessCommand cmd = MasterProcessCommand.byOpcode( opcode );
+            if ( cmd == null )
             {
-                break;
+                return FrameCompletion.MALFORMED;
+            }
+            else if ( cmd.hasDataType() == ( tokens.size() == 3 ) )
+            {
+                return FrameCompletion.COMPLETE;
             }
-            removed = true;
-            it.remove();
-            System.err.println( "Forked JVM could not synchronize the '" + token + "' token with preamble sequence." );
         }
-        return removed;
+        return FrameCompletion.NOT_COMPLETE;
     }
 
     @Override
     public void close()
     {
     }
+
+    /**
+     * Determines whether the token is complete of malformed.
+     */
+    private enum FrameCompletion
+    {
+        NOT_COMPLETE,
+        COMPLETE,
+        MALFORMED
+    }
 }
diff --git a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/LegacyMasterProcessChannelEncoder.java b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/LegacyMasterProcessChannelEncoder.java
index d4b32bd..6ad771e 100644
--- a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/LegacyMasterProcessChannelEncoder.java
+++ b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/LegacyMasterProcessChannelEncoder.java
@@ -29,6 +29,7 @@ import org.apache.maven.surefire.report.RunMode;
 import org.apache.maven.surefire.report.SafeThrowable;
 import org.apache.maven.surefire.report.StackTraceWriter;
 
+import javax.annotation.Nonnull;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.nio.charset.Charset;
@@ -80,12 +81,12 @@ public class LegacyMasterProcessChannelEncoder implements MasterProcessChannelEn
     private final RunMode runMode;
     private volatile boolean trouble;
 
-    public LegacyMasterProcessChannelEncoder( OutputStream out )
+    public LegacyMasterProcessChannelEncoder( @Nonnull OutputStream out )
     {
         this( out, NORMAL_RUN );
     }
 
-    protected LegacyMasterProcessChannelEncoder( OutputStream out, RunMode runMode )
+    protected LegacyMasterProcessChannelEncoder( @Nonnull OutputStream out, @Nonnull RunMode runMode )
     {
         this.out = requireNonNull( out );
         this.runMode = requireNonNull( runMode );
diff --git a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/MasterProcessCommandNoMagicNumberException.java b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/MasterProcessCommandNoMagicNumberException.java
deleted file mode 100644
index 261969e..0000000
--- a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/MasterProcessCommandNoMagicNumberException.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package org.apache.maven.surefire.booter.spi;
-
-/*
- * 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.MasterProcessCommand;
-
-import java.io.IOException;
-
-/**
- * No magic number recognized in the command line, see the JavaDoc in {@link MasterProcessCommand}.
- *
- * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
- * @since 3.0.0-M4
- */
-public class MasterProcessCommandNoMagicNumberException extends IOException
-{
-    MasterProcessCommandNoMagicNumberException( String line )
-    {
-        super( "No magic # recognized in the line '" + line + "'" );
-    }
-}
diff --git a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/MasterProcessUnknownCommandException.java b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/MasterProcessUnknownCommandException.java
deleted file mode 100644
index 11cab97..0000000
--- a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/spi/MasterProcessUnknownCommandException.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package org.apache.maven.surefire.booter.spi;
-
-/*
- * 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.MasterProcessCommand;
-
-import java.io.IOException;
-
-/**
- * No {@link MasterProcessCommand command} recognized according to the opcode
- * encapsulated in the command line, see the JavaDoc in {@link MasterProcessCommand}.
- *
- * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
- * @since 3.0.0-M4
- */
-public class MasterProcessUnknownCommandException extends IOException
-{
-    MasterProcessUnknownCommandException( String line )
-    {
-        super( "Unrecognized command found '" + line + "'" );
-    }
-}
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 74d7b16..452c0d2 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
@@ -43,13 +43,12 @@ import java.util.concurrent.FutureTask;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
 
-import static java.nio.charset.StandardCharsets.US_ASCII;
+import static org.fest.assertions.Assertions.assertThat;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-import static org.fest.assertions.Assertions.assertThat;
 
 /**
  * Testing singleton {@code MasterProcessReader} in multiple class loaders.
@@ -247,9 +246,7 @@ public class CommandReaderTest
 
     private void addTestToPipeline( String cls )
     {
-        String cmd = ":maven-surefire-command:"
-            + MasterProcessCommand.RUN_CLASS.getOpcode() + ':' + cls + '\n';
-        for ( byte cmdByte : cmd.getBytes( US_ASCII ) )
+        for ( byte cmdByte : MasterProcessCommand.RUN_CLASS.encode( cls ) )
         {
             blockingStream.add( cmdByte );
         }
@@ -257,8 +254,7 @@ public class CommandReaderTest
 
     private void addEndOfPipeline()
     {
-        String cmd = ":maven-surefire-command:" + MasterProcessCommand.TEST_SET_FINISHED.getOpcode() + '\n';
-        for ( byte cmdByte : cmd.getBytes( US_ASCII ) )
+        for ( byte cmdByte : MasterProcessCommand.TEST_SET_FINISHED.encode() )
         {
             blockingStream.add( cmdByte );
         }
diff --git a/surefire-booter/src/test/java/org/apache/maven/surefire/booter/JUnit4SuiteTest.java b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/JUnit4SuiteTest.java
index a7c6298..280774e 100644
--- a/surefire-booter/src/test/java/org/apache/maven/surefire/booter/JUnit4SuiteTest.java
+++ b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/JUnit4SuiteTest.java
@@ -46,7 +46,7 @@ public class JUnit4SuiteTest extends TestCase
         suite.addTest( new JUnit4TestAdapter( BooterDeserializerTest.class ) );
         suite.addTestSuite( ClasspathTest.class );
         suite.addTestSuite( PropertiesWrapperTest.class );
-        suite.addTestSuite( LegacyMasterProcessChannelDecoderTest.class );
+        suite.addTest( new JUnit4TestAdapter( LegacyMasterProcessChannelDecoderTest.class ) );
         suite.addTest( new JUnit4TestAdapter( LegacyMasterProcessChannelEncoderTest.class ) );
         suite.addTestSuite( SurefireReflectorTest.class );
         return suite;
diff --git a/surefire-booter/src/test/java/org/apache/maven/surefire/booter/spi/LegacyMasterProcessChannelDecoderTest.java b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/spi/LegacyMasterProcessChannelDecoderTest.java
index 13efee2..5cf6b65 100644
--- a/surefire-booter/src/test/java/org/apache/maven/surefire/booter/spi/LegacyMasterProcessChannelDecoderTest.java
+++ b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/spi/LegacyMasterProcessChannelDecoderTest.java
@@ -19,112 +19,192 @@ package org.apache.maven.surefire.booter.spi;
  * under the License.
  */
 
-import junit.framework.TestCase;
 import org.apache.maven.surefire.booter.Command;
-import org.apache.maven.surefire.booter.MasterProcessCommand;
 import org.apache.maven.surefire.booter.Shutdown;
+import org.junit.Test;
 
 import java.io.ByteArrayInputStream;
+import java.io.EOFException;
 import java.io.IOException;
 import java.io.InputStream;
 
 import static org.apache.maven.surefire.booter.MasterProcessCommand.BYE_ACK;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
+import static org.apache.maven.surefire.booter.MasterProcessCommand.NOOP;
+import static org.apache.maven.surefire.booter.MasterProcessCommand.RUN_CLASS;
+import static org.apache.maven.surefire.booter.MasterProcessCommand.SHUTDOWN;
+import static org.apache.maven.surefire.booter.MasterProcessCommand.SKIP_SINCE_NEXT_TEST;
+import static org.apache.maven.surefire.booter.MasterProcessCommand.TEST_SET_FINISHED;
+import static org.apache.maven.surefire.booter.Shutdown.DEFAULT;
+import static org.apache.maven.surefire.booter.Shutdown.EXIT;
+import static org.apache.maven.surefire.booter.Shutdown.KILL;
 import static org.fest.assertions.Assertions.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
 
 /**
  * Tests for {@link LegacyMasterProcessChannelDecoder}.
  */
 public class LegacyMasterProcessChannelDecoderTest
-    extends TestCase
 {
-    public void testDataToByteArrayAndBack() throws IOException
+    @Test
+    public void testDecoderRunClass() throws IOException
     {
-        for ( MasterProcessCommand commandType : MasterProcessCommand.values() )
-        {
-            switch ( commandType )
-            {
-                case RUN_CLASS:
-                    assertEquals( String.class, commandType.getDataType() );
-                    byte[] encoded = commandType.encode( "pkg.Test" );
-                    assertThat( new String( encoded ) )
-                            .isEqualTo( ":maven-surefire-command:run-testclass:pkg.Test:" );
-                    byte[] line = addNL( encoded, '\n' );
-                    InputStream is = new ByteArrayInputStream( line );
-                    LegacyMasterProcessChannelDecoder decoder = new LegacyMasterProcessChannelDecoder( is );
-                    Command command = decoder.decode();
-                    assertThat( command.getCommandType(), is( commandType ) );
-                    assertThat( command.getData(), is( "pkg.Test" ) );
-                    break;
-                case TEST_SET_FINISHED:
-                    assertThat( commandType ).isSameAs( Command.TEST_SET_FINISHED.getCommandType() );
-                    assertEquals( Void.class, commandType.getDataType() );
-                    encoded = commandType.encode();
-                    assertThat( new String( encoded ) )
-                            .isEqualTo( ":maven-surefire-command:testset-finished:" );
-                    is = new ByteArrayInputStream( encoded );
-                    decoder = new LegacyMasterProcessChannelDecoder( is );
-                    command = decoder.decode();
-                    assertThat( command.getCommandType(), is( commandType ) );
-                    assertNull( command.getData() );
-                    break;
-                case SKIP_SINCE_NEXT_TEST:
-                    assertThat( commandType ).isSameAs( Command.SKIP_SINCE_NEXT_TEST.getCommandType() );
-                    assertEquals( Void.class, commandType.getDataType() );
-                    encoded = commandType.encode();
-                    assertThat( new String( encoded ) )
-                            .isEqualTo( ":maven-surefire-command:skip-since-next-test:" );
-                    is = new ByteArrayInputStream( encoded );
-                    decoder = new LegacyMasterProcessChannelDecoder( is );
-                    command = decoder.decode();
-                    assertThat( command.getCommandType(), is( commandType ) );
-                    assertNull( command.getData() );
-                    break;
-                case SHUTDOWN:
-                    assertEquals( String.class, commandType.getDataType() );
-                    encoded = commandType.encode( Shutdown.EXIT.name() );
-                    assertThat( new String( encoded ) )
-                            .isEqualTo( ":maven-surefire-command:shutdown:EXIT:" );
-                    is = new ByteArrayInputStream( encoded );
-                    decoder = new LegacyMasterProcessChannelDecoder( is );
-                    command = decoder.decode();
-                    assertThat( command.getCommandType(), is( commandType ) );
-                    assertThat( command.getData(), is( "EXIT" ) );
-                    break;
-                case NOOP:
-                    assertThat( commandType ).isSameAs( Command.NOOP.getCommandType() );
-                    assertEquals( Void.class, commandType.getDataType() );
-                    encoded = commandType.encode();
-                    assertThat( new String( encoded ) )
-                            .isEqualTo( ":maven-surefire-command:noop:" );
-                    is = new ByteArrayInputStream( encoded );
-                    decoder = new LegacyMasterProcessChannelDecoder( is );
-                    command = decoder.decode();
-                    assertThat( command.getCommandType(), is( commandType ) );
-                    assertNull( command.getData() );
-                    break;
-                case BYE_ACK:
-                    assertThat( commandType ).isSameAs( Command.BYE_ACK.getCommandType() );
-                    assertEquals( Void.class, commandType.getDataType() );
-                    encoded = commandType.encode();
-                    assertThat( new String( encoded ) )
-                            .isEqualTo( ":maven-surefire-command:bye-ack:" );
-                    is = new ByteArrayInputStream( encoded );
-                    decoder = new LegacyMasterProcessChannelDecoder( is );
-                    command = decoder.decode();
-                    assertThat( command.getCommandType(), is( commandType ) );
-                    assertNull( command.getData() );
-                    break;
-                default:
-                    fail();
-            }
-        }
+        assertEquals( String.class, RUN_CLASS.getDataType() );
+        byte[] encoded = RUN_CLASS.encode( "pkg.Test" );
+        assertThat( new String( encoded ) )
+            .isEqualTo( ":maven-surefire-command:run-testclass:pkg.Test:" );
+        byte[] line = addNL( encoded, '\n' );
+        InputStream is = new ByteArrayInputStream( line );
+        LegacyMasterProcessChannelDecoder decoder = new LegacyMasterProcessChannelDecoder( is );
+        Command command = decoder.decode();
+        assertThat( command.getCommandType() ).isSameAs( RUN_CLASS );
+        assertThat( command.getData() ).isEqualTo( "pkg.Test" );
+    }
+
+    @Test
+    public void testDecoderTestsetFinished() throws IOException
+    {
+        Command command = Command.TEST_SET_FINISHED;
+        assertThat( command.getCommandType() ).isSameAs( TEST_SET_FINISHED );
+        assertEquals( Void.class, TEST_SET_FINISHED.getDataType() );
+        byte[] encoded = TEST_SET_FINISHED.encode();
+        assertThat( new String( encoded ) )
+            .isEqualTo( ":maven-surefire-command:testset-finished:" );
+        ByteArrayInputStream is = new ByteArrayInputStream( encoded );
+        LegacyMasterProcessChannelDecoder decoder = new LegacyMasterProcessChannelDecoder( is );
+        command = decoder.decode();
+        assertThat( command.getCommandType() ).isSameAs( TEST_SET_FINISHED );
+        assertNull( command.getData() );
+    }
+
+    @Test
+    public void testDecoderSkipSinceNextTest() throws IOException
+    {
+        Command command = Command.SKIP_SINCE_NEXT_TEST;
+        assertThat( command.getCommandType() ).isSameAs( SKIP_SINCE_NEXT_TEST );
+        assertEquals( Void.class, SKIP_SINCE_NEXT_TEST.getDataType() );
+        byte[] encoded = SKIP_SINCE_NEXT_TEST.encode();
+        assertThat( new String( encoded ) )
+            .isEqualTo( ":maven-surefire-command:skip-since-next-test:" );
+        ByteArrayInputStream is = new ByteArrayInputStream( encoded );
+        LegacyMasterProcessChannelDecoder decoder = new LegacyMasterProcessChannelDecoder( is );
+        command = decoder.decode();
+        assertThat( command.getCommandType() ).isSameAs( SKIP_SINCE_NEXT_TEST );
+        assertNull( command.getData() );
+    }
+
+    @Test
+    public void testDecoderShutdownWithExit() throws IOException
+    {
+        Shutdown shutdownType = EXIT;
+        assertEquals( String.class, SHUTDOWN.getDataType() );
+        byte[] encoded = SHUTDOWN.encode( shutdownType.name() );
+        assertThat( new String( encoded ) )
+            .isEqualTo( ":maven-surefire-command:shutdown:" + shutdownType + ":" );
+        ByteArrayInputStream is = new ByteArrayInputStream( encoded );
+        LegacyMasterProcessChannelDecoder decoder = new LegacyMasterProcessChannelDecoder( is );
+        Command command = decoder.decode();
+        assertThat( command.getCommandType() ).isSameAs( SHUTDOWN );
+        assertThat( command.getData() ).isEqualTo( shutdownType.name() );
     }
 
-    public void testShouldDecodeTwoCommands() throws IOException
+    @Test
+    public void testDecoderShutdownWithKill() throws IOException
     {
-        String cmd = ":maven-surefire-command:bye-ack\n:maven-surefire-command:bye-ack:";
+        Shutdown shutdownType = KILL;
+        assertEquals( String.class, SHUTDOWN.getDataType() );
+        byte[] encoded = SHUTDOWN.encode( shutdownType.name() );
+        assertThat( new String( encoded ) )
+            .isEqualTo( ":maven-surefire-command:shutdown:" + shutdownType + ":" );
+        ByteArrayInputStream is = new ByteArrayInputStream( encoded );
+        LegacyMasterProcessChannelDecoder decoder = new LegacyMasterProcessChannelDecoder( is );
+        Command command = decoder.decode();
+        assertThat( command.getCommandType() ).isSameAs( SHUTDOWN );
+        assertThat( command.getData() ).isEqualTo( shutdownType.name() );
+    }
+
+    @Test
+    public void testDecoderShutdownWithDefault() throws IOException
+    {
+        Shutdown shutdownType = DEFAULT;
+        assertEquals( String.class, SHUTDOWN.getDataType() );
+        byte[] encoded = SHUTDOWN.encode( shutdownType.name() );
+        assertThat( new String( encoded ) )
+            .isEqualTo( ":maven-surefire-command:shutdown:" + shutdownType + ":" );
+        ByteArrayInputStream is = new ByteArrayInputStream( encoded );
+        LegacyMasterProcessChannelDecoder decoder = new LegacyMasterProcessChannelDecoder( is );
+        Command command = decoder.decode();
+        assertThat( command.getCommandType() ).isSameAs( SHUTDOWN );
+        assertThat( command.getData() ).isEqualTo( shutdownType.name() );
+    }
+
+    @Test
+    public void testDecoderNoop() throws IOException
+    {
+        assertThat( NOOP ).isSameAs( Command.NOOP.getCommandType() );
+        assertEquals( Void.class, NOOP.getDataType() );
+        byte[] encoded = NOOP.encode();
+        assertThat( new String( encoded ) )
+            .isEqualTo( ":maven-surefire-command:noop:" );
+        ByteArrayInputStream is = new ByteArrayInputStream( encoded );
+        LegacyMasterProcessChannelDecoder decoder = new LegacyMasterProcessChannelDecoder( is );
+        Command command = decoder.decode();
+        assertThat( command.getCommandType() ).isSameAs( NOOP );
+        assertNull( command.getData() );
+    }
+
+    @Test
+    public void shouldIgnoreDamagedStream() throws IOException
+    {
+        assertThat( BYE_ACK ).isSameAs( Command.BYE_ACK.getCommandType() );
+        assertEquals( Void.class, BYE_ACK.getDataType() );
+        byte[] encoded = BYE_ACK.encode();
+        assertThat( new String( encoded ) )
+            .isEqualTo( ":maven-surefire-command:bye-ack:" );
+        byte[] streamContent = ( "<something>" + new String( encoded ) + "<damaged>" ).getBytes();
+        ByteArrayInputStream is = new ByteArrayInputStream( streamContent );
+        LegacyMasterProcessChannelDecoder decoder = new LegacyMasterProcessChannelDecoder( is );
+        Command command = decoder.decode();
+        assertThat( command.getCommandType() ).isSameAs( BYE_ACK );
+        assertNull( command.getData() );
+    }
+
+    @Test
+    public void shouldIgnoreDamagedHeader() throws IOException
+    {
+        assertThat( BYE_ACK ).isSameAs( Command.BYE_ACK.getCommandType() );
+        assertEquals( Void.class, BYE_ACK.getDataType() );
+        byte[] encoded = BYE_ACK.encode();
+        assertThat( new String( encoded ) )
+            .isEqualTo( ":maven-surefire-command:bye-ack:" );
+        byte[] streamContent = ( ":<damaged>:" + new String( encoded ) ).getBytes();
+        ByteArrayInputStream is = new ByteArrayInputStream( streamContent );
+        LegacyMasterProcessChannelDecoder decoder = new LegacyMasterProcessChannelDecoder( is );
+        Command command = decoder.decode();
+        assertThat( command.getCommandType() ).isSameAs( BYE_ACK );
+        assertNull( command.getData() );
+    }
+
+    @Test
+    public void testDecoderByeAck() throws IOException
+    {
+        assertThat( BYE_ACK ).isSameAs( Command.BYE_ACK.getCommandType() );
+        assertEquals( Void.class, BYE_ACK.getDataType() );
+        byte[] encoded = BYE_ACK.encode();
+        assertThat( new String( encoded ) )
+            .isEqualTo( ":maven-surefire-command:bye-ack:" );
+        ByteArrayInputStream is = new ByteArrayInputStream( encoded );
+        LegacyMasterProcessChannelDecoder decoder = new LegacyMasterProcessChannelDecoder( is );
+        Command command = decoder.decode();
+        assertThat( command.getCommandType() ).isSameAs( BYE_ACK );
+        assertNull( command.getData() );
+    }
+
+    @Test
+    public void shouldDecodeTwoCommands() throws IOException
+    {
+        String cmd = ":maven-surefire-command:bye-ack:\r\n:maven-surefire-command:bye-ack:";
         InputStream is = new ByteArrayInputStream( cmd.getBytes() );
         LegacyMasterProcessChannelDecoder decoder = new LegacyMasterProcessChannelDecoder( is );
 
@@ -135,6 +215,50 @@ public class LegacyMasterProcessChannelDecoderTest
         command = decoder.decode();
         assertThat( command.getCommandType() ).isEqualTo( BYE_ACK );
         assertThat( command.getData() ).isNull();
+
+        decoder.close();
+    }
+
+    @Test( expected = EOFException.class )
+    public void testIncompleteCommand() throws IOException
+    {
+
+        ByteArrayInputStream is = new ByteArrayInputStream( ":maven-surefire-command:".getBytes() );
+        LegacyMasterProcessChannelDecoder decoder = new LegacyMasterProcessChannelDecoder( is );
+        decoder.decode();
+        fail();
+    }
+
+    @Test( expected = EOFException.class )
+    public void testIncompleteCommandStart() throws IOException
+    {
+
+        ByteArrayInputStream is = new ByteArrayInputStream( new byte[] {':', '\r'} );
+        LegacyMasterProcessChannelDecoder decoder = new LegacyMasterProcessChannelDecoder( is );
+        decoder.decode();
+        fail();
+    }
+
+    @Test( expected = EOFException.class )
+    public void shouldNotDecodeCorruptedCommand() throws IOException
+    {
+        String cmd = ":maven-surefire-command:bye-ack ::maven-surefire-command:";
+        InputStream is = new ByteArrayInputStream( cmd.getBytes() );
+        LegacyMasterProcessChannelDecoder decoder = new LegacyMasterProcessChannelDecoder( is );
+
+        decoder.decode();
+    }
+
+    @Test
+    public void shouldSkipCorruptedCommand() throws IOException
+    {
+        String cmd = ":maven-surefire-command:bye-ack\r\n::maven-surefire-command:noop:";
+        InputStream is = new ByteArrayInputStream( cmd.getBytes() );
+        LegacyMasterProcessChannelDecoder decoder = new LegacyMasterProcessChannelDecoder( is );
+
+        Command command = decoder.decode();
+        assertThat( command.getCommandType() ).isSameAs( NOOP );
+        assertNull( command.getData() );
     }
 
     private static byte[] addNL( byte[] encoded, char... newLines )