// Copyright © 2011-2013, Esko Luontola <www.orfjackal.net>
// This software is released under the Apache License 2.0.
// The license text is at http://www.apache.org/licenses/LICENSE-2.0
package fi.jumi.core.stdout;
import com.google.common.base.Throwables;
import fi.jumi.core.util.ConcurrencyUtil;
import org.apache.commons.io.output.WriterOutputStream;
import org.junit.*;
import org.junit.rules.Timeout;
import java.io.*;
import java.nio.charset.Charset;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import static fi.jumi.core.util.ConcurrencyUtil.*;
import static org.fest.assertions.Assertions.assertThat;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
public class OutputCapturerTest {
private static final int TIMEOUT = 1000;
@Rule
public final Timeout timeout = new Timeout(TIMEOUT);
private final StringWriter printedToOut = new StringWriter();
private final StringWriter printedToErr = new StringWriter();
private final OutputStream realOut = new WriterOutputStream(printedToOut);
private final OutputStream realErr = new WriterOutputStream(printedToErr);
private final OutputCapturer capturer = new OutputCapturer(realOut, realErr, Charset.defaultCharset());
// basic capturing
@Test
public void passes_through_stdout_to_the_real_stdout() {
capturer.out().print("foo");
assertThat("stdout", printedToOut.toString(), is("foo"));
assertThat("stderr", printedToErr.toString(), is(""));
}
@Test
public void passes_through_stderr_to_the_real_stderr() {
capturer.err().print("foo");
assertThat("stdout", printedToOut.toString(), is(""));
assertThat("stderr", printedToErr.toString(), is("foo"));
}
@Test
public void captures_stdout() {
OutputListenerSpy listener = new OutputListenerSpy();
capturer.captureTo(listener);
capturer.out().print("foo");
assertThat(listener.out).as("stdout").containsExactly("foo");
assertThat(listener.err).as("stderr").containsExactly();
}
@Test
public void captures_stderr() {
OutputListenerSpy listener = new OutputListenerSpy();
capturer.captureTo(listener);
capturer.err().print("foo");
assertThat(listener.out).as("stdout").containsExactly();
assertThat(listener.err).as("stderr").containsExactly("foo");
}
@Test
public void single_byte_prints_are_also_captured_and_passed_through() {
OutputListenerSpy listener = new OutputListenerSpy();
capturer.captureTo(listener);
capturer.out().write('.');
assertThat(printedToOut.toString(), is("."));
assertThat(listener.out).containsExactly(".");
}
@Test
public void after_starting_a_new_capture_all_new_events_go_to_the_new_output_listener() {
OutputListenerSpy listener1 = new OutputListenerSpy();
OutputListenerSpy listener2 = new OutputListenerSpy();
capturer.captureTo(listener1);
capturer.captureTo(listener2);
capturer.out().print("foo");
assertThat(listener1.out).containsExactly();
assertThat(listener2.out).containsExactly("foo");
}
@Test
public void starting_a_new_capture_does_not_require_installing_a_new_PrintStream_to_SystemOut() {
OutputListenerSpy listener = new OutputListenerSpy();
PrintStream out = capturer.out();
capturer.captureTo(listener);
out.print("foo");
assertThat(listener.out).containsExactly("foo");
}
// concurrency
@Test
public void concurrent_captures_are_isolated_from_each_other() throws Exception {
CountDownLatch barrier = new CountDownLatch(2);
OutputListenerSpy listener1 = new OutputListenerSpy();
OutputListenerSpy listener2 = new OutputListenerSpy();
runConcurrently(() -> {
capturer.captureTo(listener1);
sync(barrier);
capturer.out().print("from thread 1");
}, () -> {
capturer.captureTo(listener2);
sync(barrier);
capturer.out().print("from thread 2");
});
assertThat(listener1.out).containsExactly("from thread 1");
assertThat(listener2.out).containsExactly("from thread 2");
}
@Test
public void captures_what_is_printed_in_spawned_threads() throws Exception {
OutputListenerSpy listener = new OutputListenerSpy();
capturer.captureTo(listener);
runConcurrently(() -> {
capturer.out().print("from spawned thread");
});
assertThat(listener.out).containsExactly("from spawned thread");
}
@Test
public void when_spawned_threads_print_something_after_the_capture_ends_they_are_still_include_in_the_original_capture() throws InterruptedException {
CountDownLatch beforeFinished = new CountDownLatch(2);
CountDownLatch afterFinished = new CountDownLatch(2);
OutputListenerSpy capture1 = new OutputListenerSpy();
OutputListenerSpy capture2 = new OutputListenerSpy();
capturer.captureTo(capture1);
Thread t = startThread(() -> {
capturer.out().print("before capture finished");
sync(beforeFinished);
sync(afterFinished);
capturer.out().print("after capture finished");
});
sync(beforeFinished);
capturer.captureTo(capture2);
sync(afterFinished);
t.join();
assertThat(capture1.out).containsExactly("before capture finished", "after capture finished");
assertThat(capture2.out).containsExactly();
}
/**
* {@link PrintStream} synchronizes all its operations on itself, but since {@link PrintStream#println} does two
* calls to the underlying {@link OutputStream} (or if the printed text is longer than all the internal buffers),
* it's possible for stdout and stderr to get interleaved.
*/
@Test
public void printing_to_stdout_and_stderr_concurrently() throws Exception {
final int ITERATIONS = 30;
CombinedOutput combinedOutput = new CombinedOutput();
capturer.captureTo(combinedOutput);
runConcurrently(() -> {
for (int i = 0; i < ITERATIONS; i++) {
capturer.out().println("O");
}
}, () -> {
for (int i = 0; i < ITERATIONS; i++) {
capturer.err().println("E");
}
});
assertThat(combinedOutput.toString()).matches("(O\\r?\\n|E\\r?\\n)+");
}
/**
* {@link Throwable#printStackTrace} synchronizes on {@code System.err}, but it can still interleave with something
* that is printed to {@code System.out}. We can fix that by synchronizing all printing on {@code System.err}, but
* only in one direction; the output from {@code Throwable.printStackTrace(System.out)} may still interleave with
* printing to {@code System.err}.
*/
@Test
public void printing_a_stack_trace_to_stderr_and_normally_to_stdout_concurrently() throws Exception {
CountDownLatch isPrintingToOut = new CountDownLatch(1);
CountDownLatch hasPrintedStackTrace = new CountDownLatch(1);
Exception exception = new Exception("dummy exception");
CombinedOutput combinedOutput = new CombinedOutput();
capturer.captureTo(combinedOutput);
runConcurrently(() -> {
await(isPrintingToOut);
exception.printStackTrace(capturer.err());
hasPrintedStackTrace.countDown();
}, () -> {
while (hasPrintedStackTrace.getCount() > 0) {
capturer.out().println("*garbage*");
isPrintingToOut.countDown();
}
});
assertThat(combinedOutput.toString(), containsString(Throwables.getStackTraceAsString(exception)));
}
// helpers
private static void sync(CountDownLatch barrier) {
ConcurrencyUtil.sync(barrier, TIMEOUT);
}
private static void await(CountDownLatch barrier) {
ConcurrencyUtil.await(barrier, TIMEOUT);
}
private static class OutputListenerSpy implements OutputListener {
public List<String> out = Collections.synchronizedList(new ArrayList<String>());
public List<String> err = Collections.synchronizedList(new ArrayList<String>());
@Override
public void out(String text) {
out.add(text);
}
@Override
public void err(String text) {
err.add(text);
}
}
private static class CombinedOutput implements OutputListener {
private final StringBuffer sb = new StringBuffer();
@Override
public void out(String text) {
sb.append(text);
}
@Override
public void err(String text) {
sb.append(text);
}
@Override
public String toString() {
return sb.toString();
}
}
}