// 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.test;
import fi.jumi.core.api.RunId;
import fi.jumi.core.config.*;
import fi.jumi.core.network.*;
import fi.jumi.launcher.*;
import fi.jumi.launcher.process.*;
import fi.jumi.launcher.ui.*;
import fi.jumi.test.util.CloseAwaitableStringWriter;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.output.*;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import java.io.*;
import java.nio.charset.*;
import java.nio.file.*;
import java.util.*;
import static fi.jumi.core.util.StringMatchers.containsSubStrings;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertNotNull;
public class AppRunner implements TestRule {
// TODO: use a proper sandbox utility
private final Path sandboxDir = TestEnvironment.getSandboxDir().resolve(UUID.randomUUID().toString());
private final SpyProcessStarter processStarter = new SpyProcessStarter(new SystemProcessStarter());
private final CloseAwaitableStringWriter daemonOutput = new CloseAwaitableStringWriter();
private NetworkServer mockNetworkServer = null;
private Charset daemonDefaultCharset = StandardCharsets.UTF_8;
private List<Path> customClasspath = new ArrayList<>();
private JumiLauncher launcher;
private TextUIParser ui;
private String uiRawOutput;
public Path workingDirectory = Paths.get(SuiteConfiguration.DEFAULTS.getWorkingDirectory());
public final SuiteConfigurationBuilder suite = new SuiteConfigurationBuilder();
public final DaemonConfigurationBuilder daemon = new DaemonConfigurationBuilder();
public void setMockNetworkServer(NetworkServer mockNetworkServer) {
this.mockNetworkServer = mockNetworkServer;
}
public void setDaemonDefaultCharset(Charset daemonDefaultCharset) {
this.daemonDefaultCharset = daemonDefaultCharset;
}
public void addToClasspath(Path path) {
customClasspath.add(path);
}
public JumiLauncher getLauncher() {
if (launcher == null) {
launcher = createLauncher();
}
return launcher;
}
private JumiLauncher createLauncher() {
class CustomJumiLauncherBuilder extends JumiLauncherBuilder {
@Override
protected ProcessStarter createProcessStarter() {
return processStarter;
}
@Override
protected NetworkServer createNetworkServer() {
if (mockNetworkServer != null) {
return mockNetworkServer;
}
return super.createNetworkServer();
}
@Override
protected OutputStream createDaemonOutputListener() {
return new TeeOutputStream(
new CloseShieldOutputStream(System.out),
new WriterOutputStream(daemonOutput, daemonDefaultCharset, 1024, true));
}
}
JumiLauncherBuilder builder = new CustomJumiLauncherBuilder();
return builder.build();
}
public Process getDaemonProcess() throws Exception {
return processStarter.lastProcess.get();
}
public String getFinishedDaemonOutput() throws InterruptedException {
daemonOutput.await();
return getCurrentDaemonOutput();
}
public String getCurrentDaemonOutput() throws InterruptedException {
return daemonOutput.toString();
}
public void runTests(Class<?>... testClasses) throws Exception {
startSuiteAsynchronously(suite
.setTestClasses(testClasses)
.freeze());
receiveTestOutput();
}
public void runTestsMatching(String syntaxAndPattern) throws Exception {
startSuiteAsynchronously(suite
.setIncludedTestsPattern(syntaxAndPattern)
.freeze());
receiveTestOutput();
}
public void startSuiteAsynchronously(SuiteConfiguration suite) throws IOException {
getLauncher().start(configure(suite), configure(daemon.freeze()));
}
private SuiteConfiguration configure(SuiteConfiguration suite) throws IOException {
SuiteConfigurationBuilder builder = suite.melt();
builder.setWorkingDirectory(workingDirectory);
builder.addJvmOptions("-Dfile.encoding=" + daemonDefaultCharset.name());
if (TestSystemProperties.useThreadSafetyAgent()) {
builder.addJvmOptions("-javaagent:" + TestEnvironment.getThreadSafetyAgentJar());
}
builder.addToClasspath(TestEnvironment.getSimpleUnitJar());
builder.addToClasspath(TestEnvironment.getSampleClassesDir());
builder.addToClasspath(TestEnvironment.getExtraClasspath());
for (Path path : customClasspath) {
builder.addToClasspath(path);
}
return builder.freeze();
}
private DaemonConfiguration configure(DaemonConfiguration daemon) {
return daemon.melt()
.setJumiHome(sandboxDir.resolve("jumi-home"))
.setLogActorMessages(true)
.freeze();
}
private void receiveTestOutput() throws InterruptedException {
StringBuilder outputBuffer = new StringBuilder();
TextUI ui = new TextUI(launcher.getEventStream(), new PlainTextPrinter(outputBuffer));
ui.updateUntilFinished();
String output = outputBuffer.toString();
printTextUIOutput(output);
this.uiRawOutput = output;
this.ui = new TextUIParser(output);
}
private static void printTextUIOutput(String output) {
synchronized (System.out) {
System.out.println("--- TEXT UI OUTPUT ----");
System.out.println(output);
System.out.println("--- / TEXT UI OUTPUT ----");
}
}
// assertions
public void checkEmptyPassingSuite() {
checkPassingAndFailingTests(0, 0);
checkTotalTestRuns(0);
checkSuitePasses();
}
public void checkSuitePasses() {
assertThat("failing tests", ui.getFailingCount(), is(0));
assertThat("no internal errors", uiRawOutput, not(containsString("Internal Error")));
}
public void checkPassingAndFailingTests(int expectedPassing, int expectedFailing) {
assertThat("passing tests", ui.getPassingCount(), is(expectedPassing));
assertThat("failing tests", ui.getFailingCount(), is(expectedFailing));
}
public void checkTotalTestRuns(int expectedRunCount) {
assertThat("total test runs", ui.getRunCount(), is(expectedRunCount));
}
public void checkContainsRun(String... startAndEndEvents) {
assertNotNull("did not contain a run with the expected events", findRun(startAndEndEvents));
}
public String getRunOutput(Class<?> testClass, String testName) {
RunId runId = findRun(testClass.getSimpleName(), testName, "/", "/");
assertNotNull("run not found", runId);
return getRunOutput(runId);
}
public RunId findRun(String... startAndEndEvents) {
List<String> expectedEvents = Arrays.asList(startAndEndEvents);
for (RunId runId : ui.getRunIds()) {
List<String> actualEvents = ui.getTestStartAndEndEvents(runId);
if (actualEvents.equals(expectedEvents)) {
return runId;
}
}
return null;
}
public String getRunOutput(RunId runId) {
return ui.getRunOutput(runId);
}
public void checkHasStackTrace(String... expectedElements) {
assertThat("stack trace not found in UI output", uiRawOutput, containsSubStrings(expectedElements));
}
// JUnit integration
@Override
public Statement apply(final Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
setup();
try {
base.evaluate();
} finally {
tearDown();
}
}
};
}
private void setup() throws IOException {
Files.createDirectories(sandboxDir);
}
private void tearDown() {
if (launcher != null) {
try {
launcher.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (processStarter.lastProcess.isDone()) {
try {
Process process = processStarter.lastProcess.get();
kill(process);
} catch (Exception e) {
e.printStackTrace();
}
}
try {
FileUtils.forceDelete(sandboxDir.toFile());
} catch (IOException e) {
System.err.println("WARNING: " + e.getMessage());
}
}
private static void kill(Process process) {
process.destroy();
try {
process.waitFor();
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
}
// helpers
public static class SpyProcessStarter implements ProcessStarter {
private final ProcessStarter processStarter;
public final FutureValue<Process> lastProcess = new FutureValue<>();
public SpyProcessStarter(ProcessStarter processStarter) {
this.processStarter = processStarter;
}
@Override
public Process startJavaProcess(JvmArgs jvmArgs) throws IOException {
Process process = processStarter.startJavaProcess(jvmArgs);
lastProcess.set(process);
return process;
}
}
}