package cucumber.runtime.android;
import android.app.Instrumentation;
import android.os.Bundle;
import cucumber.runtime.Runtime;
import gherkin.formatter.model.Feature;
import gherkin.formatter.model.Match;
import gherkin.formatter.model.Result;
import gherkin.formatter.model.Scenario;
import java.io.PrintWriter;
import java.io.StringWriter;
/**
* Reports the test results to the instrumentation through {@link Instrumentation#sendStatus(int, Bundle)} calls.
* A "test" represents the execution of a scenario or scenario example lifecycle, which includes the execution of
* following cucumber elements:
* <ul>
* <li>all before hooks</li>
* <li>all background steps</li>
* <li>all scenario / scenario example steps</li>
* <li>all after hooks</li>
* </ul>
*
* Test reports:
* <ul>
* <li>"OK", when all step results are either "PASSED" or "SKIPPED"</li>
* <li>"FAILURE", when any step result of the background or scenario was "FAILED"</li>
* <li>"ERROR", when any step of the background or scenario or any before or after
* hook threw an exception other than an {@link AssertionError}</li>
* </ul>
*/
public class AndroidInstrumentationReporter extends NoOpFormattingReporter {
/**
* Tests status keys.
*/
public static class StatusKeys {
public static final String TEST = "test";
public static final String CLASS = "class";
public static final String STACK = "stack";
public static final String NUMTESTS = "numtests";
}
/**
* Test result status codes.
*/
public static class StatusCodes {
public static final int FAILURE = -2;
public static final int START = 1;
public static final int ERROR = -1;
public static final int OK = 0;
}
/**
* The current cucumber runtime.
*/
private final Runtime runtime;
/**
* The instrumentation to report to.
*/
private final Instrumentation instrumentation;
/**
* The total number of tests which will be executed.
*/
private final int numberOfTests;
/**
* The severest step result of the current test execution.
* This might be a step or hook result.
*/
private Result severestResult;
/**
* The feature of the current test execution.
*/
private Feature currentFeature;
/**
* Creates a new instance for the given parameters
*
* @param runtime the {@link cucumber.runtime.Runtime} to use
* @param instrumentation the {@link android.app.Instrumentation} to report statuses to
* @param numberOfTests the total number of tests to be executed, this is expected to include all scenario outline runs
*/
public AndroidInstrumentationReporter(
final Runtime runtime,
final Instrumentation instrumentation,
final int numberOfTests) {
this.runtime = runtime;
this.instrumentation = instrumentation;
this.numberOfTests = numberOfTests;
}
@Override
public void feature(final Feature feature) {
currentFeature = feature;
}
@Override
public void startOfScenarioLifeCycle(final Scenario scenario) {
resetSeverestResult();
final Bundle testStart = createBundle(currentFeature, scenario);
instrumentation.sendStatus(StatusCodes.START, testStart);
}
@Override
public void before(final Match match, final Result result) {
checkAndSetSeverestStepResult(result);
}
@Override
public void result(final Result result) {
checkAndSetSeverestStepResult(result);
}
@Override
public void after(final Match match, final Result result) {
checkAndSetSeverestStepResult(result);
}
@Override
public void endOfScenarioLifeCycle(final Scenario scenario) {
final Bundle testResult = createBundle(currentFeature, scenario);
if (severestResult.getStatus().equals(Result.FAILED)) {
if (severestResult.getError() instanceof AssertionError) {
testResult.putString(StatusKeys.STACK, severestResult.getErrorMessage());
instrumentation.sendStatus(StatusCodes.FAILURE, testResult);
} else {
testResult.putString(StatusKeys.STACK, getStackTrace(severestResult.getError()));
instrumentation.sendStatus(StatusCodes.ERROR, testResult);
}
return;
}
if (severestResult.getStatus().equals(Result.PASSED)) {
instrumentation.sendStatus( StatusCodes.OK, testResult);
return;
}
if (severestResult.getStatus().equals(Result.SKIPPED.getStatus())) {
instrumentation.sendStatus(StatusCodes.OK, testResult);
return;
}
if (severestResult.getStatus().equals(Result.UNDEFINED.getStatus())) {
testResult.putString(StatusKeys.STACK, getStackTrace(new MissingStepDefinitionError(getLastSnippet())));
instrumentation.sendStatus(StatusCodes.ERROR, testResult);
return;
}
throw new IllegalStateException("Unexpected result status: " + severestResult.getStatus());
}
/**
* Creates a template bundle for reporting the start and end of a test.
*
* @param feature the {@link Feature} of the current execution
* @param scenario the {@link Scenario} of the current execution
* @return the new {@link Bundle}
*/
private Bundle createBundle(final Feature feature, final Scenario scenario) {
final Bundle bundle = new Bundle();
bundle.putInt(StatusKeys.NUMTESTS, numberOfTests);
bundle.putString(StatusKeys.CLASS, String.format("%s %s", feature.getKeyword(), feature.getName()));
bundle.putString(StatusKeys.TEST, String.format("%s %s", scenario.getKeyword(), scenario.getName()));
return bundle;
}
/**
* Determines the last snippet for a detected undefined step.
*
* @return string representation of the snippet
*/
private String getLastSnippet() {
return runtime.getSnippets().get(runtime.getSnippets().size() - 1);
}
/**
* Resets the severest test result for the next scenario life cycle.
*/
private void resetSeverestResult() {
severestResult = null;
}
/**
* Checks if the given {@code result} is more severe than the current {@code severestResult} and updates
* the {@code severestResult} if that should be the case.
*
* @param result the {@link Result} to check
*/
private void checkAndSetSeverestStepResult(final Result result) {
final boolean firstResult = severestResult == null;
if (firstResult) {
severestResult = result;
return;
}
final boolean currentIsPassed = severestResult.getStatus().equals(Result.PASSED);
final boolean nextIsNotPassed = !result.getStatus().equals(Result.PASSED);
if (currentIsPassed && nextIsNotPassed) {
severestResult = result;
}
}
/**
* Creates a string representation of the given {@code throwable}'s stacktrace.
*
* @param throwable the {@link Throwable} to get the stacktrace from
* @return the stacktrace as a string
*/
private static String getStackTrace(final Throwable throwable) {
final StringWriter stringWriter = new StringWriter();
final PrintWriter printWriter = new PrintWriter(stringWriter, true);
throwable.printStackTrace(printWriter);
return stringWriter.getBuffer().toString();
}
}