stefanbirkner / system-rules

A collection of JUnit rules for testing code which uses java.lang.System.
http://stefanbirkner.github.io/system-rules
Other
547 stars 71 forks source link

ExpectedSystemExit and SystemErrRule or SystemOutRule cause Gradle to throw MessageIOException when used in the same test #38

Closed ghost closed 8 years ago

ghost commented 8 years ago

While creating unit tests to handle some command line output and exceptions thrown in an application, I decided to use SystemErrRule and SystemOutRule to assist with this.

As this particular test is for the main entry point of the application, it is parsing the command line and calling System.exit(1) when invalid command line arguments have been provided.

However, including both of these causes Gradle to throw an exception when trying to run the task gradlew test:

:test
Unexpected exception thrown.
org.gradle.messaging.remote.internal.MessageIOException: Could not read message from '/127.0.0.1:50239'.
        at org.gradle.messaging.remote.internal.inet.SocketConnection.receive(SocketConnection.java:79)
        at org.gradle.messaging.remote.internal.hub.MessageHub$ConnectionReceive.run(MessageHub.java:235)
        at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:54)
        at org.gradle.internal.concurrent.StoppableExecutorImpl$1.run(StoppableExecutorImpl.java:40)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
        at java.lang.Thread.run(Unknown Source)
Caused by: com.esotericsoftware.kryo.KryoException: java.io.IOException: An existing connection was forcibly closed by the remote host
        at com.esotericsoftware.kryo.io.Input.fill(Input.java:141)
        at com.esotericsoftware.kryo.io.Input.require(Input.java:159)
        at com.esotericsoftware.kryo.io.Input.readByte(Input.java:255)
        at org.gradle.internal.serialize.kryo.KryoBackedDecoder.readByte(KryoBackedDecoder.java:80)
        at org.gradle.messaging.remote.internal.hub.InterHubMessageSerializer$MessageReader.read(InterHubMessageSerializer.java:69)
        at org.gradle.messaging.remote.internal.hub.InterHubMessageSerializer$MessageReader.read(InterHubMessageSerializer.java:58)
        at org.gradle.messaging.remote.internal.inet.SocketConnection.receive(SocketConnection.java:74)
        ... 6 more
Caused by: java.io.IOException: An existing connection was forcibly closed by the remote host
        at sun.nio.ch.SocketDispatcher.read0(Native Method)
        at sun.nio.ch.SocketDispatcher.read(Unknown Source)
        at sun.nio.ch.IOUtil.readIntoNativeBuffer(Unknown Source)
        at sun.nio.ch.IOUtil.read(Unknown Source)
        at sun.nio.ch.SocketChannelImpl.read(Unknown Source)
        at org.gradle.messaging.remote.internal.inet.SocketConnection$SocketInputStream.read(SocketConnection.java:158)
        at com.esotericsoftware.kryo.io.Input.fill(Input.java:139)
        ... 12 more
Unexpected exception thrown.
org.gradle.messaging.remote.internal.MessageIOException: Could not write message [EndOfStream] to '/127.0.0.1:50239'.
        at org.gradle.messaging.remote.internal.inet.SocketConnection.dispatch(SocketConnection.java:106)
        at org.gradle.messaging.remote.internal.hub.MessageHub$ConnectionDispatch.run(MessageHub.java:284)
        at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:54)
        at org.gradle.internal.concurrent.StoppableExecutorImpl$1.run(StoppableExecutorImpl.java:40)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
        at java.lang.Thread.run(Unknown Source)
Caused by: java.io.IOException: An existing connection was forcibly closed by the remote host
        at sun.nio.ch.SocketDispatcher.write0(Native Method)
        at sun.nio.ch.SocketDispatcher.write(Unknown Source)
        at sun.nio.ch.IOUtil.writeFromNativeBuffer(Unknown Source)
        at sun.nio.ch.IOUtil.write(Unknown Source)
        at sun.nio.ch.SocketChannelImpl.write(Unknown Source)
        at org.gradle.messaging.remote.internal.inet.SocketConnection$SocketOutputStream.flush(SocketConnection.java:221)
        at org.gradle.messaging.remote.internal.inet.SocketConnection.dispatch(SocketConnection.java:104)
        ... 6 more
:test FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':test'.
> Process 'Gradle Test Executor 1' finished with non-zero exit value 1

In IntelliJ, trying to run this test results in the error response "Failed to start: 0 passed, 1 not started" - so it isn't isolated to Gradle.

Here is an example setup that can reproduce this issue:

Program.java:

package org.test.example;

public final class Program
{
   public static void main(String[] args)
   {
      if (args.length != 2)
      {
         System.out.println("Proper usage: -port <number>");

         System.exit(1);
      }
   }
}

ProgramTest.java:

package org.test.example;

import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.ExpectedSystemExit;
import org.junit.contrib.java.lang.system.SystemErrRule;
import org.junit.contrib.java.lang.system.SystemOutRule;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

@RunWith(PowerMockRunner.class)
@PrepareForTest(Program.class)
public class ProgramTest
{
   /** Handles System.exit() calls. */
   @Rule
   private final ExpectedSystemExit exit = ExpectedSystemExit.none();

   /** Handles System.err calls. */
   @Rule
   private final SystemErrRule err = new SystemErrRule().enableLog().muteForSuccessfulTests();

   /** Handles System.out calls. */
   @Rule
   private final SystemOutRule out = new SystemOutRule().enableLog().muteForSuccessfulTests();

   /**
    * Verify server does not run when only a single valid command line
    * parameter is provided.
    *
    * @throws Exception On error.
    */
   @Test
   public void testMainOneParameterPort() throws Exception
   {
      expectExit();

      Program.main(new String[] { "-port" });
   }

   /**
    * Helper function to setup System.exit() expectations
    */
   private void expectExit()
   {
      exit.expectSystemExitWithStatus(1);

      exit.checkAssertionAfterwards(() ->
      {
         assertFalse(out.getLog().isEmpty());
         assertTrue(err.getLog().isEmpty());
      });
   }
}

Environment Configuration:

Windows 10 Pro x64 10.0.10586

Oracle JDK/JRE x64 1.8.0_66

Gradle 2.5

JUnit 4.12
System Rules 1.15.1
Mockito 1.10.19
PowerMock 1.6.4
stefanbirkner commented 8 years ago

Does the problem still exists if you run the test without the PowerMockRunner?

stefanbirkner commented 8 years ago

The problem seems to be the PowerMockRunner. The test is successful if I don't run it with the PowerMockRunner.

stefanbirkner commented 8 years ago

This is a PowerMock issue: jayway/powermock#427. I created a pull request for PowerMock that solves the problem. I found a workaround, too. The issue does not apply if there is only one rule for a test. This can be achieved by using org.junit.rules.RuleChain:

@RunWith(PowerMockRunner.class)
@PrepareForTest(Program.class)
public class ProgramTest
{
    private final ExpectedSystemExit exit = ExpectedSystemExit.none();
    private final SystemErrRule err = new SystemErrRule().enableLog().muteForSuccessfulTests();
    private final SystemOutRule out = new SystemOutRule().enableLog().muteForSuccessfulTests();

    @Rule
    public final TestRule rule = RuleChain.outerRule(exit).around(err).around(out);

    ...
ghost commented 8 years ago

Thank you for the deep look into this, as well as the provided workaround.

I'll have a chance to test that it works as you've suggested this weekend and will watch for implementation on the PowerMock side.

Since this isn't an issue with the System Rules implementation, this seems like it can be closed.