/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.test.internal.runner;
import android.app.Instrumentation;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import androidx.test.filters.AbstractFilter;
import androidx.test.filters.CustomFilter;
import androidx.test.filters.RequiresDevice;
import androidx.test.filters.SdkSuppress;
import androidx.test.internal.runner.ClassPathScanner.ChainedClassNameFilter;
import androidx.test.internal.runner.ClassPathScanner.ExcludeClassNamesFilter;
import androidx.test.internal.runner.ClassPathScanner.ExcludePackageNameFilter;
import androidx.test.internal.runner.ClassPathScanner.ExternalClassNameFilter;
import androidx.test.internal.runner.ClassPathScanner.InclusivePackageNamesFilter;
import androidx.test.internal.runner.filters.TestsRegExFilter;
import androidx.test.internal.util.AndroidRunnerParams;
import androidx.test.internal.util.Checks;
import androidx.tracing.Trace;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import org.junit.runner.Description;
import org.junit.runner.Request;
import org.junit.runner.Runner;
import org.junit.runner.manipulation.Filter;
import org.junit.runner.manipulation.NoTestsRemainException;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.Suite;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.RunnerBuilder;
/**
* A builder for {@link Request} that builds up tests to run, filtered on provided set of
* restrictions.
*/
public class TestRequestBuilder {
private static final String TAG = "TestRequestBuilder";
static final String MISSING_ARGUMENTS_MSG =
"Must provide either classes to run, or paths to scan";
static final String AMBIGUOUS_ARGUMENTS_MSG =
"Ambiguous arguments: cannot provide both test package and test class(es) to run";
private final List<String> pathsToScan = new ArrayList<>();
private Set<String> includedPackages = new HashSet<>();
private Set<String> excludedPackages = new HashSet<>();
private Set<String> includedClasses = new HashSet<>();
private Set<String> excludedClasses = new HashSet<>();
private ClassAndMethodFilter classMethodFilter = new ClassAndMethodFilter();
private final TestsRegExFilter testsRegExFilter = new TestsRegExFilter();
private Filter filter =
new AnnotationExclusionFilter(androidx.test.filters.Suppress.class)
.intersect(new SdkSuppressFilter())
.intersect(new RequiresDeviceFilter())
.intersect(classMethodFilter)
.intersect(testsRegExFilter)
.intersect(new CustomFilters());
private List<Class<? extends RunnerBuilder>> customRunnerBuilderClasses = new ArrayList<>();
private boolean skipExecution = false;
private final DeviceBuild deviceBuild;
private long perTestTimeout = 0;
private final Instrumentation instr;
private final Bundle argsBundle;
private ClassLoader classLoader;
/**
* Instructs the test builder if JUnit3 suite() methods should be executed.
*
* <p>Currently set to false if any method filter is set, for consistency with
* InstrumentationTestRunner.
*/
private boolean ignoreSuiteMethods = false;
/**
* Accessor interface for retrieving device build properties.
*
* <p>Used so unit tests can mock calls
*/
interface DeviceBuild {
/** Returns the SDK API level for current device. */
int getSdkVersionInt();
/** Returns the hardware type of the current device. */
String getHardware();
/** Returns the version code name of the current device. */
String getCodeName();
}
private static class DeviceBuildImpl implements DeviceBuild {
@Override
public int getSdkVersionInt() {
return android.os.Build.VERSION.SDK_INT;
}
@Override
public String getHardware() {
return android.os.Build.HARDWARE;
}
@Override
public String getCodeName() {
return android.os.Build.VERSION.CODENAME;
}
}
/** Filter that only runs tests whose method or class has been annotated with given filter. */
private static class AnnotationInclusionFilter extends AbstractFilter {
private final Class<? extends Annotation> annotationClass;
AnnotationInclusionFilter(Class<? extends Annotation> annotation) {
annotationClass = annotation;
}
/**
* Determine if given test description matches filter.
*
* @param description the {@link Description} describing the test
* @return <code>true</code> if matched
*/
@Override
protected boolean evaluateTest(Description description) {
final Class<?> testClass = description.getTestClass();
return description.getAnnotation(annotationClass) != null
|| (testClass != null && testClass.isAnnotationPresent(annotationClass));
}
/** {@inheritDoc} */
@Override
public String describe() {
return String.format("annotation %s", annotationClass.getName());
}
}
/**
* A filter for test sizes.
*
* <p>Will match if test method has given size annotation, or class does, but only if method does
* not have any other size annotations. ie method size annotation overrides class size annotation.
*/
private static class SizeFilter extends AbstractFilter {
private final TestSize testSize;
SizeFilter(TestSize testSize) {
this.testSize = testSize;
}
@Override
public String describe() {
return "";
}
@Override
protected boolean evaluateTest(Description description) {
// If test method is annotated with test size annotation include it
if (testSize.testMethodIsAnnotatedWithTestSize(description)) {
return true;
} else if (testSize.testClassIsAnnotatedWithTestSize(description)) {
// size annotation matched at class level. Make sure method doesn't have any other
// size annotations
for (Annotation a : description.getAnnotations()) {
if (TestSize.isAnyTestSize(a.annotationType())) {
return false;
}
}
return true;
}
return false;
}
}
/** Filter out tests whose method or class has been annotated with given filter. */
private static class AnnotationExclusionFilter extends AbstractFilter {
private final Class<? extends Annotation> annotationClass;
AnnotationExclusionFilter(Class<? extends Annotation> annotation) {
annotationClass = annotation;
}
@Override
protected boolean evaluateTest(Description description) {
final Class<?> testClass = description.getTestClass();
if ((testClass != null && testClass.isAnnotationPresent(annotationClass))
|| (description.getAnnotation(annotationClass) != null)) {
return false;
}
return true;
}
/** {@inheritDoc} */
@Override
public String describe() {
return String.format("not annotation %s", annotationClass.getName());
}
}
private static class ExtendedSuite extends Suite {
static Suite createSuite(List<Runner> runners) {
try {
return new ExtendedSuite(runners);
} catch (InitializationError e) {
throw new RuntimeException(
"Internal Error: "
+ Suite.class.getName()
+ "(Class<?>, List<Runner>) should never throw an "
+ "InitializationError when passed a null Class",
e);
}
}
ExtendedSuite(List<Runner> runners) throws InitializationError {
super(null, runners);
}
}
private static class CustomFilters extends AbstractFilter {
@Override
protected boolean evaluateTest(Description description) {
Collection<Annotation> allAnnotations = description.getAnnotations();
for (Annotation a : allAnnotations) {
CustomFilter filter = a.annotationType().getAnnotation(CustomFilter.class);
if (filter != null) {
// @CustomFilter is present on this annotation, initialize filter class and check if this
// test should run
Class<? extends AbstractFilter> filterClass = filter.filterClass();
try {
if (!filterClass.getConstructor().newInstance().shouldRun(description)) {
return false; // skip the test
}
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException(
"Must have no argument constructor for class " + filterClass.getName(), e);
} catch (ClassCastException e) {
throw new IllegalArgumentException(
filterClass.getName() + " does not extend androidx.test.filters.AbstractFilter", e);
} catch (InstantiationException | InvocationTargetException | IllegalAccessException e) {
throw new IllegalArgumentException("Failed to create: " + filterClass.getName(), e);
}
}
}
return true; // run the test
}
@Override
public String describe() {
return "skip tests annotated with custom filters if necessary";
}
}
private class SdkSuppressFilter extends AbstractFilter {
@Override
protected boolean evaluateTest(Description description) {
final SdkSuppress sdkSuppress = getAnnotationForTest(description);
if (sdkSuppress != null) {
if ((getDeviceSdkInt() >= sdkSuppress.minSdkVersion()
&& getDeviceSdkInt() <= sdkSuppress.maxSdkVersion())
|| getDeviceCodeName().equals(sdkSuppress.codeName())) {
return true; // run the test
}
return false; // don't run the test
}
return true; // no SdkSuppress, run the test
}
private SdkSuppress getAnnotationForTest(Description description) {
final SdkSuppress s = description.getAnnotation(SdkSuppress.class);
if (s != null) {
return s;
}
final Class<?> testClass = description.getTestClass();
if (testClass != null) {
return testClass.getAnnotation(SdkSuppress.class);
}
return null;
}
/** {@inheritDoc} */
@Override
public String describe() {
return String.format("skip tests annotated with SdkSuppress if necessary");
}
}
/** Class that filters out tests annotated with {@link RequiresDevice} when running on emulator */
@VisibleForTesting
class RequiresDeviceFilter extends AnnotationExclusionFilter {
static final String EMULATOR_HARDWARE_GOLDFISH = "goldfish";
static final String EMULATOR_HARDWARE_RANCHU = "ranchu";
// TODO(b/65053549) Remove once we have a more generic solution
static final String EMULATOR_HARDWARE_GCE = "gce_x86";
private final Set<String> emulatorHardwareNames =
new HashSet<>(
Arrays.asList(
EMULATOR_HARDWARE_GOLDFISH, EMULATOR_HARDWARE_RANCHU, EMULATOR_HARDWARE_GCE));
RequiresDeviceFilter() {
super(RequiresDevice.class);
}
@Override
protected boolean evaluateTest(Description description) {
if (!super.evaluateTest(description)) {
// annotation is present - check if device is an emulator
return !emulatorHardwareNames.contains(getDeviceHardware());
}
return true;
}
/** {@inheritDoc} */
@Override
public String describe() {
return String.format("skip tests annotated with RequiresDevice if necessary");
}
}
private static class ShardingFilter extends Filter {
private final int numShards;
private final int shardIndex;
ShardingFilter(int numShards, int shardIndex) {
this.numShards = numShards;
this.shardIndex = shardIndex;
}
@Override
public boolean shouldRun(Description description) {
if (description.isTest()) {
return (Math.abs(description.hashCode()) % numShards) == shardIndex;
}
// The description is a suite, so assume that it can be run so that filtering is
// applied to its children. If after filtering it has no children then it will be
// automatically filtered out.
return true;
}
/** {@inheritDoc} */
@Override
public String describe() {
return String.format("Shard %s of %s shards", shardIndex, numShards);
}
}
/**
* A {@link Request} that doesn't report an error if all tests are filtered out. Done for
* consistency with InstrumentationTestRunner.
*/
private static class LenientFilterRequest extends Request {
private final Request request;
private final Filter filter;
public LenientFilterRequest(Request classRequest, Filter filter) {
request = classRequest;
this.filter = filter;
}
@Override
public Runner getRunner() {
try {
Runner runner = request.getRunner();
filter.apply(runner);
return runner;
} catch (NoTestsRemainException e) {
// don't treat filtering out all tests as an error
return new BlankRunner();
}
}
}
/** A {@link Runner} that doesn't do anything */
private static class BlankRunner extends Runner {
@Override
public Description getDescription() {
return Description.createSuiteDescription("no tests found");
}
@Override
public void run(RunNotifier notifier) {
// do nothing
}
}
/** A {@link Filter} to support the ability to filter out multiple class#method combinations. */
private static class ClassAndMethodFilter extends AbstractFilter {
private Map<String, MethodFilter> methodFilters = new HashMap<>();
@Override
public boolean evaluateTest(Description description) {
if (methodFilters.isEmpty()) {
return true;
}
String className = description.getClassName();
MethodFilter methodFilter = methodFilters.get(className);
if (methodFilter != null) {
return methodFilter.shouldRun(description);
}
// This test class was not explicitly excluded and none of it's test methods were
// explicitly included or excluded. Should be run, return true:
return true;
}
@Override
public String describe() {
return "Class and method filter";
}
public void addMethod(String className, String methodName) {
MethodFilter methodFilter = methodFilters.get(className);
if (methodFilter == null) {
methodFilter = new MethodFilter(className);
methodFilters.put(className, methodFilter);
}
methodFilter.addInclusionMethod(methodName);
}
public void removeMethod(String className, String methodName) {
MethodFilter methodFilter = methodFilters.get(className);
if (methodFilter == null) {
methodFilter = new MethodFilter(className);
methodFilters.put(className, methodFilter);
}
methodFilter.addExclusionMethod(methodName);
}
}
/** A {@link Filter} used to filter out desired test methods from a given class */
private static class MethodFilter extends AbstractFilter {
private final String className;
private Set<String> includedMethods = new HashSet<>();
private Set<String> excludedMethods = new HashSet<>();
/**
* Constructs a method filter for a given class
*
* @param className name of the class the method belongs to
*/
public MethodFilter(String className) {
this.className = className;
}
@Override
public String describe() {
return "Method filter for " + className + " class";
}
@Override
public boolean evaluateTest(Description description) {
String methodName = description.getMethodName();
// The method name could be null, e.g. if the class is marked with @Ignore. In that
// case there is no matching method to run so filter the test out.
if (methodName == null) {
return false;
}
// Parameterized tests append "[#]" at the end of the method names.
// For instance, "getFoo" would become "getFoo[0]".
// Method filters should be applied against both the parameterized name and root name
String rootMethodName = stripParameterizedSuffix(methodName);
if (excludedMethods.contains(methodName) || excludedMethods.contains(rootMethodName)) {
return false;
}
// don't filter out descriptions with method name "initializationError", since
// Junit will generate such descriptions in error cases, See ErrorReportingRunner
return includedMethods.isEmpty()
|| includedMethods.contains(methodName)
|| includedMethods.contains(rootMethodName)
|| methodName.equals("initializationError");
}
// Strips out the parameterized suffix if it exists
private String stripParameterizedSuffix(String name) {
Pattern suffixPattern = Pattern.compile(".+(\[[0-9]+\])$");
if (suffixPattern.matcher(name).matches()) {
name = name.substring(0, name.lastIndexOf('['));
}
return name;
}
public void addInclusionMethod(String methodName) {
includedMethods.add(methodName);
}
public void addExclusionMethod(String methodName) {
excludedMethods.add(methodName);
}
}
/**
* Creates a TestRequestBuilder
*
* @param instr the {@link Instrumentation} to pass to applicable tests
* @param bundle the {@link Bundle} to pass to applicable tests
*/
public TestRequestBuilder(Instrumentation instr, Bundle bundle) {
this(new DeviceBuildImpl(), instr, bundle);
}
/** Alternate TestRequestBuilder constructor that accepts a custom DeviceBuild */
@VisibleForTesting
TestRequestBuilder(DeviceBuild deviceBuildAccessor, Instrumentation instr, Bundle bundle) {
deviceBuild = Checks.checkNotNull(deviceBuildAccessor);
this.instr = Checks.checkNotNull(instr);
argsBundle = Checks.checkNotNull(bundle);
maybeAddLegacySuppressFilter();
}
// add legacy Suppress filer iff it is on classpath
private void maybeAddLegacySuppressFilter() {
try {
Class<? extends Annotation> legacySuppressClass =
(Class<? extends Annotation>)
Class.forName("android.test.suitebuilder.annotation.Suppress");
filter = filter.intersect(new AnnotationExclusionFilter(legacySuppressClass));
} catch (ClassNotFoundException e) {
// ignore
}
}
/**
* Instruct builder to scan the given paths and add all test classes found. Cannot be used in
* conjunction with {@link #addTestClass} or {@link #addTestMethod} is used.
*
* @param paths the list of paths (.dex and .apk files) to scan
*/
public TestRequestBuilder addPathsToScan(Iterable<String> paths) {
for (String path : paths) {
addPathToScan(path);
}
return this;
}
/**
* Instruct builder to scan given path and add all test classes found. Cannot be used in
* conjunction with {@link #addTestClass} or {@link #addTestMethod} is used.
*
* @param path a filepath to scan for test methods (.dex and .apk files)
*/
public TestRequestBuilder addPathToScan(String path) {
pathsToScan.add(path);
return this;
}
/**
* Set the {@link ClassLoader} to be used to load test cases.
*
* @param loader {@link ClassLoader} to load test cases with.
*/
public TestRequestBuilder setClassLoader(ClassLoader loader) {
classLoader = loader;
return this;
}
/**
* Instructs the test builder if JUnit3 suite() methods should be executed.
*
* @param ignoreSuiteMethods true to ignore all suite methods.
*/
public TestRequestBuilder ignoreSuiteMethods(boolean ignoreSuiteMethods) {
this.ignoreSuiteMethods = ignoreSuiteMethods;
return this;
}
/**
* Add a test class to be executed. All test methods in this class will be executed, unless a test
* method was explicitly included or excluded.
*
* @param className
*/
public TestRequestBuilder addTestClass(String className) {
includedClasses.add(className);
return this;
}
/**
* Excludes a test class. All test methods in this class will be excluded.
*
* @param className
*/
public TestRequestBuilder removeTestClass(String className) {
excludedClasses.add(className);
return this;
}
/** Adds a test method to run. */
public TestRequestBuilder addTestMethod(String testClassName, String testMethodName) {
includedClasses.add(testClassName);
classMethodFilter.addMethod(testClassName, testMethodName);
return this;
}
/** Excludes a test method from being run. */
public TestRequestBuilder removeTestMethod(String testClassName, String testMethodName) {
classMethodFilter.removeMethod(testClassName, testMethodName);
return this;
}
/**
* Run only tests within given java package. Cannot be used in conjunction with
* addTestClass/Method.
*
* <p>At least one {@link #addPathToScan} also must be provided.
*
* @param testPackage the fully qualified java package name
*/
public TestRequestBuilder addTestPackage(String testPackage) {
includedPackages.add(testPackage);
return this;
}
/**
* Excludes all tests within given java package. Cannot be used in conjunction with
* addTestClass/Method.
*
* <p>At least one {@link #addPathToScan} also must be provided.
*
* @param testPackage the fully qualified java package name
*/
public TestRequestBuilder removeTestPackage(String testPackage) {
excludedPackages.add(testPackage);
return this;
}
/**
* Sets the test name filter regular expression filter.
*
* <p>Will filter out tests not matching the given regex.
*
* @param testsRegex a regex for matching against <code>java_package.class#method</code>
*/
public TestRequestBuilder setTestsRegExFilter(String testsRegex) {
this.testsRegExFilter.setPattern(testsRegex);
return this;
}
/**
* Run only tests with given size
*
* @param forTestSize
*/
public TestRequestBuilder addTestSizeFilter(TestSize forTestSize) {
if (!TestSize.NONE.equals(forTestSize)) {
addFilter(new SizeFilter(forTestSize));
} else {
Log.e(TAG, String.format("Unrecognized test size '%s'", forTestSize.getSizeQualifierName()));
}
return this;
}
/**
* Only run tests annotated with given annotation class.
*
* @param annotation the full class name of annotation
*/
public TestRequestBuilder addAnnotationInclusionFilter(String annotation) {
Class<? extends Annotation> annotationClass = loadAnnotationClass(annotation);
if (annotationClass != null) {
addFilter(new AnnotationInclusionFilter(annotationClass));
}
return this;
}
/**
* Skip tests annotated with given annotation class.
*
* @param notAnnotation the full class name of annotation
*/
public TestRequestBuilder addAnnotationExclusionFilter(String notAnnotation) {
Class<? extends Annotation> annotationClass = loadAnnotationClass(notAnnotation);
if (annotationClass != null) {
addFilter(new AnnotationExclusionFilter(annotationClass));
}
return this;
}
public TestRequestBuilder addShardingFilter(int numShards, int shardIndex) {
return addFilter(new ShardingFilter(numShards, shardIndex));
}
public TestRequestBuilder addFilter(Filter filter) {
this.filter = this.filter.intersect(filter);
return this;
}
public TestRequestBuilder addCustomRunnerBuilderClass(
Class<? extends RunnerBuilder> runnerBuilderClass) {
customRunnerBuilderClasses.add(runnerBuilderClass);
return this;
}
/**
* Build a request that will generate test started and test ended events, but will skip actual
* test execution.
*/
public TestRequestBuilder setSkipExecution(boolean b) {
skipExecution = b;
return this;
}
/** Sets milliseconds timeout value applied to each test where 0 means no timeout */
public TestRequestBuilder setPerTestTimeout(long millis) {
perTestTimeout = millis;
return this;
}
/** Convenience method to set builder attributes from {@link RunnerArgs} */
public TestRequestBuilder addFromRunnerArgs(RunnerArgs runnerArgs) {
for (RunnerArgs.TestArg test : runnerArgs.tests) {
if (test.methodName == null) {
addTestClass(test.testClassName);
} else {
addTestMethod(test.testClassName, test.methodName);
}
}
for (RunnerArgs.TestArg test : runnerArgs.notTests) {
if (test.methodName == null) {
removeTestClass(test.testClassName);
} else {
removeTestMethod(test.testClassName, test.methodName);
}
}
for (String pkg : runnerArgs.testPackages) {
addTestPackage(pkg);
}
for (String pkg : runnerArgs.notTestPackages) {
removeTestPackage(pkg);
}
if (runnerArgs.testSize != null) {
addTestSizeFilter(TestSize.fromString(runnerArgs.testSize));
}
for (String annotation : runnerArgs.annotations) {
addAnnotationInclusionFilter(annotation);
}
for (String notAnnotation : runnerArgs.notAnnotations) {
addAnnotationExclusionFilter(notAnnotation);
}
for (Filter filter : runnerArgs.filters) {
addFilter(filter);
}
if (runnerArgs.testTimeout > 0) {
setPerTestTimeout(runnerArgs.testTimeout);
}
if (runnerArgs.numShards > 0
&& runnerArgs.shardIndex >= 0
&& runnerArgs.shardIndex < runnerArgs.numShards) {
addShardingFilter(runnerArgs.numShards, runnerArgs.shardIndex);
}
if (runnerArgs.logOnly || runnerArgs.listTestsForOrchestrator) {
setSkipExecution(true);
}
if (runnerArgs.classLoader != null) {
setClassLoader(runnerArgs.classLoader);
}
for (Class<? extends RunnerBuilder> runnerBuilderClass : runnerArgs.runnerBuilderClasses) {
addCustomRunnerBuilderClass(runnerBuilderClass);
}
if (runnerArgs.testsRegEx != null) {
setTestsRegExFilter(runnerArgs.testsRegEx);
}
return this;
}
/**
* Builds the {@link Request} based on provided data.
*
* @throws java.lang.IllegalArgumentException if provided set of data is not valid
*/
public Request build() {
Trace.beginSection("build test request");
try {
includedPackages.removeAll(excludedPackages);
includedClasses.removeAll(excludedClasses);
validate(includedClasses);
boolean scanningPath = includedClasses.isEmpty();
// If scanning then suite methods are not supported.
boolean ignoreSuiteMethods = this.ignoreSuiteMethods || scanningPath;
AndroidRunnerParams runnerParams =
new AndroidRunnerParams(instr, argsBundle, perTestTimeout, ignoreSuiteMethods);
RunnerBuilder runnerBuilder = getRunnerBuilder(runnerParams);
TestLoader loader = TestLoader.Factory.create(classLoader, runnerBuilder, scanningPath);
Collection<String> classNames;
if (scanningPath) {
// no class restrictions have been specified. Load all classes.
Log.d(TAG, "Using class path scanning to discover tests");
classNames = getClassNamesFromClassPath();
} else {
Log.d(
TAG,
String.format("Skipping class path scanning and directly running %s", includedClasses));
classNames = includedClasses;
}
List<Runner> runners = loader.getRunnersFor(classNames);
Suite suite = ExtendedSuite.createSuite(runners);
Request request = Request.runner(suite);
return new LenientFilterRequest(request, filter);
} finally {
Trace.endSection();
}
}
/** Validate that the set of options provided to this builder are valid and not conflicting */
private void validate(Set<String> classNames) {
if (classNames.isEmpty() && pathsToScan.isEmpty()) {
throw new IllegalArgumentException(MISSING_ARGUMENTS_MSG);
}
// TODO(b/73905202): consider failing if both test classes and scan paths are given.
// Right now that is allowed though
}
/**
* Get the {@link RunnerBuilder} to use to create the {@link Runner} instances.
*
* @param runnerParams {@link AndroidRunnerParams} that stores common runner parameters
* @return a {@link RunnerBuilder}.
*/
private RunnerBuilder getRunnerBuilder(AndroidRunnerParams runnerParams) {
RunnerBuilder builder;
if (skipExecution) {
// If all that is needed is the list of tests then replace the Runner which will
// run the test with one that will simply fire events for each of the tests.
builder = new AndroidLogOnlyBuilder(runnerParams, customRunnerBuilderClasses);
} else {
builder = new AndroidRunnerBuilder(runnerParams, customRunnerBuilderClasses);
}
return builder;
}
private Collection<String> getClassNamesFromClassPath() {
if (pathsToScan.isEmpty()) {
throw new IllegalStateException("neither test class to execute or class paths were provided");
}
Log.i(TAG, String.format("Scanning classpath to find tests in paths %s", pathsToScan));
ClassPathScanner scanner = createClassPathScanner(pathsToScan);
ChainedClassNameFilter filter = new ChainedClassNameFilter();
// exclude inner classes
filter.add(new ExternalClassNameFilter());
for (String pkg : ClassPathScanner.getDefaultExcludedPackages()) {
// Add the test packages to the exclude list unless they were explictly included.
if (!includedPackages.contains(pkg)) {
excludedPackages.add(pkg);
}
}
if (!includedPackages.isEmpty()) {
filter.add(new InclusivePackageNamesFilter(includedPackages));
}
for (String pkg : excludedPackages) {
filter.add(new ExcludePackageNameFilter(pkg));
}
filter.add(new ExcludeClassNamesFilter(excludedClasses));
try {
return scanner.getClassPathEntries(filter);
} catch (IOException e) {
Log.e(TAG, "Failed to scan classes", e);
}
return Collections.emptyList();
}
/**
* Factory method for {@link ClassPathScanner}.
*
* <p>Exposed so unit tests can mock.
*/
ClassPathScanner createClassPathScanner(List<String> classPath) {
return new ClassPathScanner(classPath);
}
@SuppressWarnings("unchecked")
private Class<? extends Annotation> loadAnnotationClass(String className) {
try {
Class<?> clazz = Class.forName(className);
return (Class<? extends Annotation>) clazz;
} catch (ClassNotFoundException e) {
Log.e(TAG, String.format("Could not find annotation class: %s", className));
} catch (ClassCastException e) {
Log.e(TAG, String.format("Class %s is not an annotation", className));
}
return null;
}
private int getDeviceSdkInt() {
return deviceBuild.getSdkVersionInt();
}
private String getDeviceHardware() {
return deviceBuild.getHardware();
}
private String getDeviceCodeName() {
return deviceBuild.getCodeName();
}
}