// 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.launcher.ui;
import fi.jumi.actors.eventizers.Event;
import fi.jumi.actors.queue.MessageQueue;
import fi.jumi.api.drivers.TestId;
import fi.jumi.core.*;
import fi.jumi.core.api.*;
import fi.jumi.core.events.suiteListener.SuiteListenerToEvent;
import org.junit.*;
import org.junit.rules.Timeout;
import static fi.jumi.core.util.Asserts.*;
import static fi.jumi.core.util.StringMatchers.hasOccurrences;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
@SuppressWarnings("CodeBlock2Expr")
public class TextUITest {
private static final String SUMMARY_LINE = "Pass";
@Rule
public final Timeout timeout = new Timeout(1000);
private final MessageQueue<Event<SuiteListener>> stream = new MessageQueue<>();
private final SuiteListener listener = new SuiteListenerToEvent(stream);
private final EventBuilder suite = new EventBuilder(listener);
private final StringBuilder out = new StringBuilder();
private final TextUI ui = new TextUI(stream, new PlainTextPrinter(out));
private String runAndGetOutput() {
ui.update();
return forceUnixLineSeparators(out.toString());
}
private static String forceUnixLineSeparators(String output) {
String lineSeparator = System.getProperty("line.separator");
return output.replaceAll(lineSeparator, "\n");
}
private void assertInOutput(String... expectedLines) {
assertContainsSubStrings(runAndGetOutput(), expectedLines);
}
private void assertNotInOutput(String... expectedLines) {
assertNotContainsSubStrings(runAndGetOutput(), expectedLines);
}
// updating
@Test
public void can_update_non_blockingly() {
ui.update(); // given no events in stream, should exit quickly
assertNotInOutput(SUMMARY_LINE);
}
@Test
public void can_update_blockingly() throws InterruptedException {
Thread t = new Thread(() -> {
SuiteMother.emptySuite(listener);
});
t.start();
ui.updateUntilFinished(); // should exit only after all events have arrived
assertInOutput(SUMMARY_LINE);
}
// summary line
@Test
public void summary_line_for_no_tests() {
SuiteMother.emptySuite(listener);
assertInOutput("Pass: 0, Fail: 0");
}
@Test
public void summary_line_for_one_passing_test() {
SuiteMother.onePassingTest(listener);
assertInOutput("Pass: 1, Fail: 0");
}
@Test
public void summary_line_for_one_failing_test() {
SuiteMother.oneFailingTest(listener);
assertInOutput("Pass: 0, Fail: 1");
}
@Test
public void summary_line_for_multiple_nested_tests() {
SuiteMother.nestedFailingAndPassingTests(listener);
assertInOutput("Pass: 2, Fail: 1");
}
@Test
public void summary_line_is_not_printed_until_all_events_have_arrived() {
suite.begin();
assertNotInOutput(SUMMARY_LINE);
suite.end();
assertInOutput(SUMMARY_LINE);
}
@Test
public void summary_is_silent_if_everything_was_fine() {
SuiteMother.onePassingTest(listener);
assertNotInOutput("There were");
}
@Test
public void summary_tells_whether_there_were_test_failures() {
SuiteMother.oneFailingTest(listener);
assertInOutput("There were test failures");
}
@Test
public void summary_tells_whether_there_were_internal_errors() {
SuiteMother.internalError(listener);
assertInOutput("There were internal errors");
}
@Test
public void each_TestClass_TestId_pair_is_counted_only_once_in_the_summary() {
suite.begin();
RunId run1 = suite.nextRunId();
suite.runStarted(run1, SuiteMother.TEST_FILE);
suite.test(run1, TestId.ROOT, SuiteMother.TEST_CLASS_NAME, () -> {
suite.test(run1, TestId.of(0), "test one");
});
suite.runFinished(run1);
// same root test is executed twice, but should be counted only once in the total
RunId run2 = suite.nextRunId();
suite.runStarted(run2, SuiteMother.TEST_FILE);
suite.test(run2, TestId.ROOT, SuiteMother.TEST_CLASS_NAME, () -> {
suite.test(run2, TestId.of(1), "test two");
});
suite.runFinished(run2);
// a different test class, same TestId, should be counted separately
RunId run3 = suite.nextRunId();
suite.runStarted(run3, TestFile.fromClassName("com.example.AnotherDummyTest"));
suite.test(run3, TestId.ROOT, "AnotherDummyTest");
suite.runFinished(run3);
suite.end();
assertInOutput("Pass: 4, Fail: 0");
}
// test runs
@Test
public void prints_test_run_header() {
suite.begin();
RunId run1 = suite.nextRunId();
suite.runStarted(run1, TestFile.fromClassName("com.example.DummyTest"));
suite.test(run1, TestId.ROOT, "Human-readable name");
suite.runFinished(run1);
suite.end();
// expected content:
// - run ID
// - full name of the test class
// - human-readable name of the test class (it MAY be different from class name)
assertInOutput(
"Run #1 in com.example.DummyTest",
"Human-readable name"
);
}
@Test
public void test_run_header_is_printed_for_each_test_run() {
SuiteMother.twoPassingRuns(listener);
assertInOutput(
"Run #1 in com.example.DummyTest",
"Run #2 in com.example.DummyTest"
);
}
@Test
public void test_run_header_is_printed_only_once_per_test_run() {
suite.begin();
RunId run1 = suite.nextRunId();
// First test of the test run - should print the class name
suite.runStarted(run1, SuiteMother.TEST_FILE);
suite.test(run1, TestId.ROOT, "Human readable name of test class", () -> {
// Second test of the test run - should NOT print the class name a second time,
// because a test run cannot span many classes
suite.test(run1, TestId.of(0), "test one");
});
suite.runFinished(run1);
suite.end();
assertInOutput(SuiteMother.TEST_CLASS); // should show once
assertNotInOutput(SuiteMother.TEST_CLASS, SuiteMother.TEST_CLASS); // should not show twice
}
@Test
public void there_is_a_spacer_between_test_runs() {
ui.setProgressBarVisible(false); // the progress bar interferes with this test
SuiteMother.twoPassingRuns(listener);
assertInOutput("" +
" > - DummyTest\n" +
"\n" + // the expected spacer - an empty line
" > Run #2");
}
@Test
public void interleaved_test_runs_are_reported_without_interleaving() {
SuiteMother.twoInterleavedRuns(listener);
assertInOutput(
"Run #1",
"+ testOne",
"- testOne",
"Run #2",
"+ testTwo",
"- testTwo"
);
}
// test names
@Test
public void prints_that_when_a_test_starts_and_ends() {
suite.begin();
RunId run1 = suite.nextRunId();
suite.runStarted(run1, SuiteMother.TEST_FILE);
suite.test(run1, TestId.ROOT, "Dummy test");
suite.runFinished(run1);
suite.end();
assertInOutput(
"+ Dummy test",
"- Dummy test"
);
}
@Test
public void prints_with_indentation_that_when_a_nested_test_starts_and_ends() {
suite.begin();
RunId run1 = suite.nextRunId();
suite.runStarted(run1, SuiteMother.TEST_FILE);
suite.test(run1, TestId.ROOT, "Dummy test", () -> {
suite.test(run1, TestId.of(0), "test one");
suite.test(run1, TestId.of(1), "test two", () -> {
suite.test(run1, TestId.of(1, 0), "deeply nested test");
});
});
suite.runFinished(run1);
suite.end();
assertInOutput(
"> + Dummy test",
"> + test one",
"> - test one",
"> + test two",
"> + deeply nested test",
"> - deeply nested test",
"> - test two",
"> - Dummy test"
);
}
// stack traces
@Test
public void prints_failure_stack_traces() {
SuiteMother.oneFailingTest(listener);
assertInOutput("java.lang.Throwable: dummy exception");
}
@Test
public void prints_failure_stack_traces_only_after_the_test_is_finished() {
suite.begin();
{
{
RunId run1 = suite.nextRunId();
listener.onTestFileFound(SuiteMother.TEST_FILE);
listener.onTestFound(SuiteMother.TEST_FILE, TestId.ROOT, SuiteMother.TEST_CLASS_NAME);
listener.onRunStarted(run1, SuiteMother.TEST_FILE);
listener.onTestStarted(run1, TestId.ROOT);
listener.onFailure(run1, StackTrace.from(new Throwable("dummy exception")));
assertNotInOutput("java.lang.Throwable: dummy exception");
listener.onTestFinished(run1);
listener.onRunFinished(run1);
}
assertInOutput("java.lang.Throwable: dummy exception");
}
suite.end();
}
@Test
public void prints_failure_stack_traces_only_after_the_surrounding_test_is_finished() { // i.e. the test run is finished
suite.begin();
{
{
RunId run1 = suite.nextRunId();
suite.runStarted(run1, SuiteMother.TEST_FILE);
listener.onTestFound(SuiteMother.TEST_FILE, TestId.ROOT, SuiteMother.TEST_CLASS_NAME);
listener.onTestStarted(run1, TestId.ROOT);
suite.failingTest(run1, TestId.of(0), "testOne",
new Throwable("dummy exception")
);
assertNotInOutput("java.lang.Throwable: dummy exception");
listener.onTestFinished(run1);
listener.onRunFinished(run1);
}
assertInOutput("java.lang.Throwable: dummy exception");
}
suite.end();
}
// standard output
@Test
public void prints_what_tests_printed_to_stdout() {
SuiteMother.printsToStdout(listener);
assertInOutput("printed to stdout\n");
}
@Test
public void prints_what_tests_printed_to_stderr() {
SuiteMother.printsToStderr(listener);
assertInOutput("printed to stderr\n");
}
@Test
public void always_puts_test_start_and_end_events_on_a_new_line() {
SuiteMother.printsToStdoutWithoutNewlineAtEnd(listener);
String printedToStdout = "this doesn't end with newline";
String theNextMetaLine = " > -";
assertInOutput(printedToStdout + "\n" + theNextMetaLine);
}
@Test
public void does_not_crash_if_a_test_continues_printing_after_the_test_run_has_finished() {
SuiteMother.printsAfterTestRunFinished(listener);
assertNotInOutput("printed to stdout");
// The following are hard to do with the current design; maybe implement them only in the GUI
// TODO: show a warning if a test continued printing after the test run finished
// TODO: show the thing that was printed
}
// configuration
@Test
public void can_choose_to_show_passing_tests() {
ui.setPassingTestsVisible(true);
SuiteMother.onePassingTest(listener);
assertInOutput("Run #1");
assertInOutput("Pass: 1");
}
@Test
public void can_choose_to_hide_passing_tests() {
ui.setPassingTestsVisible(false);
SuiteMother.onePassingTest(listener);
assertNotInOutput("Run #1");
assertInOutput("Pass: 1");
}
// suite results
@Test
public void tells_when_the_suite_had_no_failures() {
SuiteMother.onePassingTest(listener);
runAndGetOutput();
assertThat("has failures", ui.hasFailures(), is(false));
}
@Test
public void tells_when_the_suite_had_some_failures() {
SuiteMother.oneFailingTest(listener);
runAndGetOutput();
assertThat("has failures", ui.hasFailures(), is(true));
}
@Test
public void empty_suite_is_considered_to_be_passing() {
SuiteMother.emptySuite(listener);
runAndGetOutput();
assertThat("has failures", ui.hasFailures(), is(false));
}
// internal errors
@Test
public void prints_internal_error_stack_traces() {
SuiteMother.internalError(listener);
runAndGetOutput();
assertInOutput(
"> Internal Error",
"> the internal error message",
"java.lang.Throwable: dummy exception"
);
}
@Test
public void internal_errors_fail_the_whole_suite() {
SuiteMother.internalError(listener);
runAndGetOutput();
assertThat("has failures", ui.hasFailures(), is(true));
}
// progress bar
@Test
public void shows_a_progress_bar() {
SuiteMother.emptySuite(listener);
// We don't want to be too specific about how long the progress bar is,
// so we only check that it starts and ends.
assertInOutput("[---");
assertInOutput("===]");
}
@Test
public void restarts_printing_the_progress_bar_after_printing_a_run() {
SuiteMother.onePassingTest(listener);
String output = runAndGetOutput();
assertThat("should start printing multiple times", output, hasOccurrences(2, "[---"));
assertThat("should print one or more incomplete progress bars", output, hasOccurrences(1, "===\n"));
assertThat("should print exactly one complete progress bar", output, hasOccurrences(1, "===]\n"));
}
}