blob: 49f27b50891b25f42facbefe525ba620cdcb262b [file] [log] [blame]
// Copyright 2016 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 static org.chromium.base.test.BaseChromiumAndroidJUnitRunner.shouldListTests;
import android.content.Context;
import android.support.annotation.CallSuper;
import android.support.test.InstrumentationRegistry;
import android.support.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
import android.support.test.internal.util.AndroidRunnerParams;
import org.junit.rules.MethodRule;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;
import org.chromium.base.CommandLine;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.test.BaseTestResult.PreTestHook;
import org.chromium.base.test.params.MethodParamAnnotationRule;
import org.chromium.base.test.util.DisableIfSkipCheck;
import org.chromium.base.test.util.MinAndroidSdkLevelSkipCheck;
import org.chromium.base.test.util.RestrictionSkipCheck;
import org.chromium.base.test.util.SkipCheck;
import java.io.File;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* A custom runner for JUnit4 tests that checks requirements to conditionally ignore tests.
*
* This ClassRunner imports from AndroidJUnit4ClassRunner which is a hidden but accessible
* class. The reason is that default JUnit4 runner for Android is a final class,
* AndroidJUnit4. We need to extends an inheritable class to change {@link #runChild}
* and {@link #isIgnored} to add SkipChecks and PreTesthook.
*/
public class BaseJUnit4ClassRunner extends AndroidJUnit4ClassRunner {
private static final String TAG = "BaseJUnit4ClassRunnr";
private static final String EXTRA_TRACE_FILE =
"org.chromium.base.test.BaseJUnit4ClassRunner.TraceFile";
/**
* Create a BaseJUnit4ClassRunner to run {@code klass} and initialize values.
*
* To add more SkipCheck or PreTestHook in subclass, create Lists of checks and hooks,
* pass them into the super constructors. If you want make a subclass extendable by other
* class runners, you also have to create a constructor similar to the following one that
* merges default checks or hooks with this checks and hooks passed in by constructor.
*
* <pre>
* <code>
* e.g.
* public ChildRunner extends BaseJUnit4ClassRunner {
* public ChildRunner(final Class<?> klass) {
* throws InitializationError {
* this(klass, Collections.emptyList(), Collections.emptyList(),
* Collections.emptyList());
* }
*
* public ChildRunner(
* final Class<?> klass, List<SkipCheck> checks, List<PreTestHook> hook,
* List<TestRule> rules) { throws InitializationError { super(klass, mergeList( checks,
* getSkipChecks()), mergeList(hooks, getPreTestHooks()));
* }
*
* public List<SkipCheck> getSkipChecks() {...}
*
* public List<PreTestHook> getPreTestHooks() {...}
* </code>
* </pre>
*
* @throws InitializationError if the test class malformed
*/
public BaseJUnit4ClassRunner(final Class<?> klass) throws InitializationError {
super(klass,
new AndroidRunnerParams(InstrumentationRegistry.getInstrumentation(),
InstrumentationRegistry.getArguments(), false, 0L, false));
String traceOutput = InstrumentationRegistry.getArguments().getString(EXTRA_TRACE_FILE);
if (traceOutput != null) {
File traceOutputFile = new File(traceOutput);
File traceOutputDir = traceOutputFile.getParentFile();
if (traceOutputDir != null) {
if (traceOutputDir.exists() || traceOutputDir.mkdirs()) {
TestTraceEvent.enable(traceOutputFile);
}
}
}
}
/**
* Merge two List into a new ArrayList.
*
* Used to merge the default SkipChecks/PreTestHooks with the subclasses's
* SkipChecks/PreTestHooks.
*/
private static <T> List<T> mergeList(List<T> listA, List<T> listB) {
List<T> l = new ArrayList<>(listA);
l.addAll(listB);
return l;
}
@SafeVarargs
protected static <T> List<T> addToList(List<T> list, T... additionalEntries) {
return mergeList(list, Arrays.asList(additionalEntries));
}
@Override
protected void collectInitializationErrors(List<Throwable> errors) {
super.collectInitializationErrors(errors);
// Log any initialization errors to help debugging, as the host-side test runner can get
// confused by the thrown exception.
if (!errors.isEmpty()) {
Log.e(TAG, "Initialization errors in %s: %s", getTestClass().getName(), errors);
}
}
/**
* Override this method to return a list of {@link SkipCheck}s}.
*
* Additional hooks can be added to the list using {@link #addToList}:
* {@code return addToList(super.getSkipChecks(), check1, check2);}
*/
@CallSuper
protected List<SkipCheck> getSkipChecks() {
return Arrays.asList(new RestrictionSkipCheck(InstrumentationRegistry.getTargetContext()),
new MinAndroidSdkLevelSkipCheck(), new DisableIfSkipCheck());
}
/**
* Override this method to return a list of {@link PreTestHook}s.
*
* Additional hooks can be added to the list using {@link #addToList}:
* {@code return addToList(super.getPreTestHooks(), hook1, hook2);}
* TODO(bauerb): Migrate PreTestHook to TestRule.
*/
@CallSuper
protected List<PreTestHook> getPreTestHooks() {
return Collections.emptyList();
}
/**
* Override this method to return a list of method rules that should be applied to all tests
* run with this test runner.
*
* Additional rules can be added to the list using {@link #addToList}:
* {@code return addToList(super.getDefaultMethodRules(), rule1, rule2);}
*/
@CallSuper
protected List<MethodRule> getDefaultMethodRules() {
return Collections.singletonList(new MethodParamAnnotationRule());
}
/**
* Override this method to return a list of rules that should be applied to all tests run with
* this test runner.
*
* Additional rules can be added to the list using {@link #addToList}:
* {@code return addToList(super.getDefaultTestRules(), rule1, rule2);}
*/
@CallSuper
protected List<TestRule> getDefaultTestRules() {
return Collections.emptyList();
}
/**
* Evaluate whether a FrameworkMethod is ignored based on {@code SkipCheck}s.
*/
@Override
protected boolean isIgnored(FrameworkMethod method) {
return super.isIgnored(method) || shouldSkip(method);
}
@Override
protected List<MethodRule> rules(Object target) {
List<MethodRule> declaredRules = super.rules(target);
List<MethodRule> defaultRules = getDefaultMethodRules();
return mergeList(defaultRules, declaredRules);
}
@Override
protected final List<TestRule> getTestRules(Object target) {
List<TestRule> declaredRules = super.getTestRules(target);
List<TestRule> defaultRules = getDefaultTestRules();
return mergeList(declaredRules, defaultRules);
}
/**
* Run test with or without execution based on bundle arguments.
*/
@Override
public void run(RunNotifier notifier) {
ContextUtils.initApplicationContext(
InstrumentationRegistry.getTargetContext().getApplicationContext());
if (shouldListTests(InstrumentationRegistry.getArguments())) {
for (Description child : getDescription().getChildren()) {
notifier.fireTestStarted(child);
notifier.fireTestFinished(child);
}
return;
}
if (!CommandLine.isInitialized()) {
initCommandLineForTest();
}
super.run(notifier);
}
/**
* Override this method to change how test class runner initiate commandline flags
*/
protected void initCommandLineForTest() {
CommandLine.init(null);
}
@Override
protected void runChild(FrameworkMethod method, RunNotifier notifier) {
String testName = method.getName();
TestTraceEvent.begin(testName);
runPreTestHooks(method);
super.runChild(method, notifier);
TestTraceEvent.end(testName);
// A new instance of BaseJUnit4ClassRunner is created on the device
// for each new method, so runChild will only be called once. Thus, we
// can disable tracing, and dump the output, once we get here.
TestTraceEvent.disable();
}
/**
* Loop through all the {@code PreTestHook}s to run them
*/
private void runPreTestHooks(FrameworkMethod frameworkMethod) {
Method testMethod = frameworkMethod.getMethod();
Context targetContext = InstrumentationRegistry.getTargetContext();
for (PreTestHook hook : getPreTestHooks()) {
hook.run(targetContext, testMethod);
}
}
/**
* Loop through all the {@code SkipCheck}s to confirm whether a test should be ignored
*/
private boolean shouldSkip(FrameworkMethod method) {
for (SkipCheck s : getSkipChecks()) {
if (s.shouldSkip(method)) {
return true;
}
}
return false;
}
/*
* Overriding this method to take screenshot of failure before tear down functions are run.
*/
@Override
protected Statement withAfters(FrameworkMethod method, Object test, Statement base) {
return super.withAfters(method, test, new ScreenshotOnFailureStatement(base));
}
}