From d14f0fe4859d8872eb718df00bb4fef0f8c496a3 Mon Sep 17 00:00:00 2001 From: Michael Muller Date: Tue, 17 Sep 2019 13:23:30 -0400 Subject: [PATCH] Output command test output as well as consuming it (#248) * Output command test output as well as consuming it CommandTestCase currently consumes stdout & stderr for the command being tested. Unfortunately, this results in us not being able to see the command output. Add an output splitter so that output gets written to the original stream in addition to being captured. A simpler approach would be to print the captured data after command completion. However, this won't work for tests that become hung and also won't display results in real-time. Tested: Ran a command test with verboseTestOutput=true, verified that standard output was visible. * Save and restore original stdout/err in cmd tests We have to restore the original stdout/stderr print streams otherwise we end up nesting them across tests which eventually causes the RDE tests to OOM. --- .../registry/tools/CommandTestCase.java | 72 ++++++++++++++++++- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/core/src/test/java/google/registry/tools/CommandTestCase.java b/core/src/test/java/google/registry/tools/CommandTestCase.java index 48def8fbb..9cf23d249 100644 --- a/core/src/test/java/google/registry/tools/CommandTestCase.java +++ b/core/src/test/java/google/registry/tools/CommandTestCase.java @@ -36,8 +36,11 @@ import google.registry.tools.params.ParameterFactory; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.io.OutputStream; import java.io.PrintStream; import java.util.List; +import java.util.concurrent.locks.ReentrantLock; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.rules.TemporaryFolder; @@ -54,8 +57,13 @@ import org.mockito.junit.MockitoRule; @RunWith(JUnit4.class) public abstract class CommandTestCase { + // Lock for stdout/stderr. Note that this is static: since we're dealing with globals, we need + // to lock for the entire JVM. + private static final ReentrantLock streamsLock = new ReentrantLock(); + private final ByteArrayOutputStream stdout = new ByteArrayOutputStream(); private final ByteArrayOutputStream stderr = new ByteArrayOutputStream(); + private PrintStream oldStdout, oldStderr; protected C command; @@ -75,8 +83,22 @@ public abstract class CommandTestCase { // Ensure the UNITTEST environment has been set before constructing a new command instance. RegistryToolEnvironment.UNITTEST.setup(systemPropertyRule); command = newCommandInstance(); - System.setOut(new PrintStream(stdout)); - System.setErr(new PrintStream(stderr)); + + // Capture standard output/error. This is problematic because gradle tests run in parallel in + // the same JVM. So first lock out any other tests in this JVM that are trying to do this + // trick. + streamsLock.lock(); + oldStdout = System.out; + System.setOut(new PrintStream(new OutputSplitter(System.out, stdout))); + oldStderr = System.err; + System.setErr(new PrintStream(new OutputSplitter(System.err, stderr))); + } + + @After + public final void afterCommandTestCase() throws Exception { + System.setOut(oldStdout); + System.setErr(oldStderr); + streamsLock.unlock(); } void runCommandInEnvironment(RegistryToolEnvironment env, String... args) throws Exception { @@ -221,4 +243,50 @@ public abstract class CommandTestCase { throw new RuntimeException(e); } } + + /** + * Splits an output stream, writing it to two other output streams. + * + *

We use this as a replacement for standard out/error so that we can both capture the output + * of the command and display it to the console for debugging. + */ + static class OutputSplitter extends OutputStream { + + OutputStream a, b; + + OutputSplitter(OutputStream a, OutputStream b) { + this.a = a; + this.b = b; + } + + @Override + public void write(byte[] data) throws IOException { + a.write(data); + b.write(data); + } + + @Override + public void write(byte[] data, int off, int len) throws IOException { + a.write(data, off, len); + b.write(data, off, len); + } + + @Override + public void close() throws IOException { + a.close(); + b.close(); + } + + @Override + public void flush() throws IOException { + a.flush(); + b.flush(); + } + + @Override + public void write(int val) throws IOException { + a.write(val); + b.write(val); + } + } }