blob: 7ac4bee1c298492faa2b58ca1783cbda1a5bfff7 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.base.test;
import android.app.Instrumentation;
import android.content.Context;
import android.os.Bundle;
import android.os.SystemClock;
import junit.framework.AssertionFailedError;
import junit.framework.TestCase;
import junit.framework.TestResult;
import org.chromium.base.Log;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.parameter.BaseParameter;
import org.chromium.base.test.util.parameter.Parameter;
import org.chromium.base.test.util.parameter.Parameterizable;
import org.chromium.base.test.util.parameter.ParameterizedTest;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/**
* A test result that can skip tests.
*/
public class BaseTestResult extends TestResult {
private static final String TAG = "base_test";
private static final int SLEEP_INTERVAL_MS = 50;
private static final int WAIT_DURATION_MS = 5000;
private final Instrumentation mInstrumentation;
private final List<SkipCheck> mSkipChecks;
private final List<PreTestHook> mPreTestHooks;
/**
* Creates an instance of BaseTestResult.
*/
public BaseTestResult(Instrumentation instrumentation) {
mSkipChecks = new ArrayList<>();
mPreTestHooks = new ArrayList<>();
mInstrumentation = instrumentation;
}
/**
* An interface for classes that check whether a test case should be skipped.
*/
public interface SkipCheck {
/**
*
* Checks whether the given test case should be skipped.
*
* @param testCase The test case to check.
* @return Whether the test case should be skipped.
*/
boolean shouldSkip(TestCase testCase);
}
/**
* An interface for classes that have some code to run before a test. They run after
* {@link SkipCheck}s. Provides access to the test method (and the annotations defined for it)
* and the instrumentation context.
*/
public interface PreTestHook {
/**
* @param targetContext the instrumentation context that will be used during the test.
* @param testMethod the test method to be run.
*/
public void run(Context targetContext, Method testMethod);
}
/**
* Adds a check for whether a test should run.
*
* @param skipCheck The check to add.
*/
public void addSkipCheck(SkipCheck skipCheck) {
mSkipChecks.add(skipCheck);
}
/**
* Adds hooks that will be executed before each test that runs.
*
* @param preTestHook The hook to add.
*/
public void addPreTestHook(PreTestHook preTestHook) {
mPreTestHooks.add(preTestHook);
}
protected boolean shouldSkip(TestCase test) {
for (SkipCheck s : mSkipChecks) {
if (s.shouldSkip(test)) return true;
}
return false;
}
private void runPreTestHooks(TestCase test) {
try {
Method testMethod = test.getClass().getMethod(test.getName());
Context targetContext = getTargetContext();
for (PreTestHook hook : mPreTestHooks) {
hook.run(targetContext, testMethod);
}
} catch (NoSuchMethodException e) {
Log.e(TAG, "Unable to run pre test hooks.", e);
}
}
@Override
protected void run(TestCase test) {
if (shouldSkip(test)) {
startTest(test);
Bundle skipResult = new Bundle();
skipResult.putString("class", test.getClass().getName());
skipResult.putString("test", test.getName());
skipResult.putBoolean("test_skipped", true);
mInstrumentation.sendStatus(0, skipResult);
endTest(test);
} else {
runPreTestHooks(test);
if (test instanceof Parameterizable) {
try {
runParameterized(test);
} catch (ThreadDeath e) {
Log.e(TAG, "Parameterized test run failed: %s", e);
}
} else {
super.run(test);
}
}
}
@SuppressWarnings("unchecked")
private <T extends TestCase & Parameterizable> void runParameterized(TestCase test)
throws ThreadDeath {
T testCase = (T) test;
// Prepare test.
Parameter.Reader parameterReader = new Parameter.Reader(test);
testCase.setParameterReader(parameterReader);
List<ParameterizedTest> parameterizedTests = parameterReader.getParameterizedTests();
List<ParameterError> errors = new ArrayList<>();
List<ParameterError> failures = new ArrayList<>();
Map<String, BaseParameter> availableParameters = testCase.getAvailableParameters();
// Remove all @ParameterizedTests that contain CommandLineFlags.Parameter -- those
// are handled in test_runner.py as it is needed to re-launch the whole test activity
// to apply command-line args correctly. Note that this way we will also ignore any
// other parameters that may present in these @ParameterizedTests.
for (Iterator<ParameterizedTest> iter = parameterizedTests.iterator(); iter.hasNext();) {
ParameterizedTest paramTest = iter.next();
for (Parameter p: paramTest.parameters()) {
if (CommandLineFlags.Parameter.PARAMETER_TAG.equals(p.tag())) {
iter.remove();
}
}
}
if (parameterizedTests.isEmpty()) {
super.run(test);
} else {
// Start test.
startTest(testCase);
for (ParameterizedTest parameterizedTest : parameterizedTests) {
parameterReader.setCurrentParameterizedTest(parameterizedTest);
try {
setUpParameters(availableParameters, parameterReader);
testCase.runBare();
tearDownParameters(availableParameters, parameterReader);
} catch (AssertionFailedError e) {
failures.add(new ParameterError(e, parameterizedTest));
} catch (ThreadDeath e) {
throw e;
} catch (Throwable e) {
errors.add(new ParameterError(e, parameterizedTest));
}
}
// Generate failures and errors.
if (!failures.isEmpty()) {
addFailure(test, new ParameterizedTestFailure(failures));
}
if (!errors.isEmpty()) {
addError(test, new ParameterizedTestError(errors));
}
// End test.
endTest(testCase);
}
}
private static <T extends TestCase & Parameterizable> void setUpParameters(
Map<String, BaseParameter> availableParameters, Parameter.Reader reader)
throws Exception {
for (Entry<String, BaseParameter> entry : availableParameters.entrySet()) {
if (reader.getParameter(entry.getValue().getTag()) != null) {
entry.getValue().setUp();
}
}
}
private static <T extends TestCase & Parameterizable> void tearDownParameters(
Map<String, BaseParameter> availableParameters, Parameter.Reader reader)
throws Exception {
for (Entry<String, BaseParameter> entry : availableParameters.entrySet()) {
if (reader.getParameter(entry.getValue().getTag()) != null) {
entry.getValue().tearDown();
}
}
}
private static class ParameterError {
private final Throwable mThrowable;
private final ParameterizedTest mParameterizedTest;
public ParameterError(Throwable throwable, ParameterizedTest parameterizedTest) {
mThrowable = throwable;
mParameterizedTest = parameterizedTest;
}
private Throwable getThrowable() {
return mThrowable;
}
private ParameterizedTest getParameterizedTest() {
return mParameterizedTest;
}
}
private static class ParameterizedTestFailure extends AssertionFailedError {
public ParameterizedTestFailure(List<ParameterError> failures) {
super(new ParameterizedTestError(failures).toString());
}
}
private static class ParameterizedTestError extends Exception {
private final List<ParameterError> mErrors;
public ParameterizedTestError(List<ParameterError> errors) {
mErrors = errors;
}
/**
* Error output is as follows.
*
* DEFINITIONS:
* {{ERROR}} is the standard error output from
* {@link ParameterError#getThrowable().toString()}.
* {{PARAMETER_TAG}} is the {@link Parameter#tag()} value associated with the parameter.
* {{ARGUMENT_NAME}} is the {@link Parameter.Argument#name()} associated with the argument.
* {{ARGUMENT_VALUE}} is the value associated with the {@link Parameter.Argument}. This can
* be a String, int, String[], or int[].
*
* With no {@link Parameter}:
* {{ERROR}} (with no parameters)
*
* With Single {@link Parameter} and no {@link Parameter.Argument}:
* {{ERROR}} (with parameters: {{PARAMETER_TAG}} with no arguments)
*
* With Single {@link Parameter} and one {@link Parameter.Argument}:
* {{ERROR}} (with parameters: {{PARAMETER_TAG}} with arguments:
* {{ARGUMENT_NAME}}={{ARGUMENT_VALUE}})
*
* With Single {@link Parameter} and multiple {@link Parameter.Argument}s:
* {{ERROR}} (with parameters: {{PARAMETER_TAG}} with arguments:
* {{ARGUMENT_NAME}}={{ARGUMENT_VALUE}}, {{ARGUMENT_NAME}}={{ARGUMENT_VALUE}}, ...)
*
* DEFINITION:
* {{PARAMETER_ERROR}} is the output of a single {@link Parameter}'s error. Format:
* {{PARAMETER_TAG}} with arguments: {{ARGUMENT_NAME}}={{ARGUMENT_NAME}}, ...
*
* With Multiple {@link Parameter}s:
* {{ERROR}} (with parameters: {{PARAMETER_ERROR}}; {{PARAMETER_ERROR}}; ...)
*
* There will be a trace after this. And this is shown for every possible {@link
* ParameterizedTest} that is failed in the {@link ParameterizedTest.Set} if there is one.
*
* @return the error message and trace of the test failures.
*/
@Override
public String toString() {
if (mErrors.isEmpty()) return "\n";
StringBuilder builder = new StringBuilder();
Iterator<ParameterError> iter = mErrors.iterator();
if (iter.hasNext()) {
builder.append(createErrorBuilder(iter.next()));
}
while (iter.hasNext()) {
builder.append("\n").append(createErrorBuilder(iter.next()));
}
return builder.toString();
}
private static StringBuilder createErrorBuilder(ParameterError error) {
StringBuilder builder = new StringBuilder("\n").append(error.getThrowable().toString());
List<Parameter> parameters =
Arrays.asList(error.getParameterizedTest().parameters());
if (parameters.isEmpty()) {
builder.append(" (with no parameters)");
} else {
Iterator<Parameter> iter = parameters.iterator();
builder.append(" (with parameters: ").append(createParameterBuilder(iter.next()));
while (iter.hasNext()) {
builder.append("; ").append(createParameterBuilder(iter.next()));
}
builder.append(")");
}
return builder.append("\n").append(trace(error));
}
private static StringBuilder createParameterBuilder(Parameter parameter) {
StringBuilder builder = new StringBuilder(parameter.tag());
List<Parameter.Argument> arguments = Arrays.asList(parameter.arguments());
if (arguments.isEmpty()) {
builder.append(" with no arguments");
} else {
Iterator<Parameter.Argument> iter = arguments.iterator();
builder.append(" with arguments: ").append(createArgumentBuilder(iter.next()));
while (iter.hasNext()) {
builder.append(", ").append(createArgumentBuilder(iter.next()));
}
}
return builder;
}
private static StringBuilder createArgumentBuilder(Parameter.Argument argument) {
StringBuilder builder = new StringBuilder(argument.name()).append("=");
if (!Parameter.ArgumentDefault.STRING.equals(argument.stringVar())) {
builder.append(argument.stringVar());
} else if (Parameter.ArgumentDefault.INT != argument.intVar()) {
builder.append(argument.intVar());
} else if (argument.stringArray().length > 0) {
builder.append(Arrays.toString(argument.stringArray()));
} else if (argument.intArray().length > 0) {
builder.append(Arrays.toString(argument.intArray()));
}
return builder;
}
/**
* @return the trace without the error message
*/
private static StringBuilder trace(ParameterError error) {
StringWriter stringWriter = new StringWriter();
PrintWriter writer = new PrintWriter(stringWriter);
error.getThrowable().printStackTrace(writer);
StringBuilder builder = new StringBuilder(stringWriter.getBuffer());
return trim(deleteFirstLine(builder));
}
private static StringBuilder deleteFirstLine(StringBuilder builder) {
return builder.delete(0, builder.indexOf("\n") + 1);
}
private static StringBuilder trim(StringBuilder sb) {
if (sb == null || sb.length() == 0) return sb;
for (int i = sb.length() - 1; i >= 0; i--) {
if (Character.isWhitespace(sb.charAt(i))) {
sb.deleteCharAt(i);
} else {
return sb;
}
}
return sb;
}
}
/**
* Gets the target context.
*
* On older versions of Android, getTargetContext() may initially return null, so we have to
* wait for it to become available.
*
* @return The target {@link Context} if available; null otherwise.
*/
public Context getTargetContext() {
Context targetContext = mInstrumentation.getTargetContext();
try {
long startTime = SystemClock.uptimeMillis();
// TODO(jbudorick): Convert this to CriteriaHelper once that moves to base/.
while (targetContext == null
&& SystemClock.uptimeMillis() - startTime < WAIT_DURATION_MS) {
Thread.sleep(SLEEP_INTERVAL_MS);
targetContext = mInstrumentation.getTargetContext();
}
} catch (InterruptedException e) {
Log.e(TAG, "Interrupted while attempting to initialize the command line.");
}
return targetContext;
}
}