| // 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; |
| } |
| } |