blob: be31454826817d1df92d9faca81a9e6246942b02 [file] [log] [blame]
/*
* Copyright (C) 2010 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 com.android.tradefed.testtype;
import com.android.tradefed.build.BuildRetrievalError;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.config.ConfigurationException;
import com.android.tradefed.config.DynamicRemoteFileResolver;
import com.android.tradefed.config.IConfiguration;
import com.android.tradefed.config.IConfigurationReceiver;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.Option.Importance;
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.config.OptionCopier;
import com.android.tradefed.config.OptionSetter;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.device.metric.IMetricCollector;
import com.android.tradefed.device.metric.IMetricCollectorReceiver;
import com.android.tradefed.error.HarnessRuntimeException;
import com.android.tradefed.error.IHarnessException;
import com.android.tradefed.invoker.TestInformation;
import com.android.tradefed.invoker.logger.CurrentInvocation;
import com.android.tradefed.invoker.tracing.CloseableTraceScope;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
import com.android.tradefed.result.FailureDescription;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.ResultForwarder;
import com.android.tradefed.result.TestDescription;
import com.android.tradefed.result.error.ErrorIdentifier;
import com.android.tradefed.result.error.InfraErrorIdentifier;
import com.android.tradefed.testtype.host.PrettyTestEventLogger;
import com.android.tradefed.testtype.junit4.CarryDnaeError;
import com.android.tradefed.testtype.junit4.ExceptionThrowingRunnerWrapper;
import com.android.tradefed.testtype.junit4.JUnit4ResultForwarder;
import com.android.tradefed.testtype.suite.ModuleDefinition;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.JUnit4TestFilter;
import com.android.tradefed.util.StreamUtil;
import com.android.tradefed.util.TestFilterHelper;
import com.google.common.annotations.VisibleForTesting;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import org.junit.Ignore;
import org.junit.internal.runners.ErrorReportingRunner;
import org.junit.runner.Description;
import org.junit.runner.JUnitCore;
import org.junit.runner.Request;
import org.junit.runner.RunWith;
import org.junit.runner.Runner;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.Suite.SuiteClasses;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.time.Duration;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Pattern;
/**
* A test runner for JUnit host based tests. If the test to be run implements {@link IDeviceTest}
* this runner will pass a reference to the device.
*/
@OptionClass(alias = "host")
public class HostTest
implements IDeviceTest,
ITestFilterReceiver,
ITestAnnotationFilterReceiver,
IRemoteTest,
ITestCollector,
IBuildReceiver,
IAbiReceiver,
IShardableTest,
IRuntimeHintProvider,
IConfigurationReceiver {
@Option(name = "class", description = "The JUnit test classes to run, in the format "
+ "<package>.<class>. eg. \"com.android.foo.Bar\". This field can be repeated.",
importance = Importance.IF_UNSET)
private Set<String> mClasses = new LinkedHashSet<>();
@Option(name = "method", description = "The name of the method in the JUnit TestCase to run. "
+ "eg. \"testFooBar\"",
importance = Importance.IF_UNSET)
private String mMethodName;
@Option(
name = "jar",
description = "The jars containing the JUnit test class to run.",
importance = Importance.IF_UNSET
)
private Set<String> mJars = new HashSet<>();
public static final String SET_OPTION_NAME = "set-option";
public static final String SET_OPTION_DESC =
"Options to be passed down to the class under test, key and value should be separated"
+ " by colon \":\"; for example, if class under test supports \"--iteration 1\""
+ " from a command line, it should be passed in as \"--set-option iteration:1\" or"
+ " \"--set-option iteration:key=value\" for passing options to map. Values that"
+ " contain \":\" or \"=\" can be escaped with a backslash. A particular class can"
+ " be targeted by specifying it. \" --set-option <fully qualified class>:<option"
+ " name>:<option value>\"";
@Option(name = SET_OPTION_NAME, description = SET_OPTION_DESC)
private List<String> mKeyValueOptions = new ArrayList<>();
@Option(name = "include-annotation",
description = "The set of annotations a test must have to be run.")
private Set<String> mIncludeAnnotations = new HashSet<>();
@Option(name = "exclude-annotation",
description = "The set of annotations to exclude tests from running. A test must have "
+ "none of the annotations in this list to run.")
private Set<String> mExcludeAnnotations = new HashSet<>();
/**
* It is strongly recommended that clients set include and exclude filters at the suite level
* via the ITestFilter interface rather than relying on include-filter and
* exclude-filter @Options.
*/
@Option(
name = "include-filter",
description = "The set of annotations a test must have to be run.")
private Set<String> mIncludeFilters = new HashSet<>();
/**
* It is strongly recommended that clients set include and exclude filters at the suite level
* via the ITestFilter interface rather than relying on include-filter and
* exclude-filter @Options.
*/
@Option(
name = "exclude-filter",
description =
"The set of annotations to exclude tests from running. A test must have "
+ "none of the annotations in this list to run.")
private Set<String> mExcludeFilters = new HashSet<>();
@Option(name = "collect-tests-only",
description = "Only invoke the instrumentation to collect list of applicable test "
+ "cases. All test run callbacks will be triggered, but test execution will "
+ "not be actually carried out.")
private boolean mCollectTestsOnly = false;
@Option(
name = "runtime-hint",
isTimeVal = true,
description = "The hint about the test's runtime."
)
private long mRuntimeHint = 60000; // 1 minute
enum ShardUnit {
CLASS, METHOD;
}
@Option(name = "shard-unit",
description = "Shard by class or method")
private ShardUnit mShardUnit = ShardUnit.CLASS;
@Option(
name = "enable-pretty-logs",
description =
"whether or not to enable a logging for each test start and end on both host and "
+ "device side."
)
private boolean mEnableHostDeviceLogs = true;
@Option(
name = TestTimeoutEnforcer.TEST_CASE_TIMEOUT_OPTION,
description = TestTimeoutEnforcer.TEST_CASE_TIMEOUT_DESCRIPTION)
private Duration mTestCaseTimeout = Duration.ofSeconds(0L);
private IConfiguration mConfig;
private ITestDevice mDevice;
private IBuildInfo mBuildInfo;
private IAbi mAbi;
private TestInformation mTestInfo;
private TestFilterHelper mFilterHelper;
private boolean mSkipTestClassCheck = false;
private List<Object> mTestMethods;
private List<Class<?>> mLoadedClasses = new ArrayList<>();
private List<URLClassLoader> mOpenClassLoaders = new ArrayList<>();
// Initialized as -1 to indicate that this value needs to be recalculated
// when test count is requested.
private int mNumTestCases = -1;
private List<File> mJUnit4JarFiles = new ArrayList<>();
private static final String EXCLUDE_NO_TEST_FAILURE = "org.junit.runner.manipulation.Filter";
private static final String TEST_FULL_NAME_FORMAT = "%s#%s";
/** Track the downloaded files. */
private List<File> mDownloadedFiles = new ArrayList<>();
public HostTest() {
mFilterHelper =
new TestFilterHelper(
mIncludeFilters, mExcludeFilters, mIncludeAnnotations, mExcludeAnnotations);
}
public void setTestInformation(TestInformation testInfo) {
mTestInfo = testInfo;
}
@Override
public void setConfiguration(IConfiguration configuration) {
mConfig = configuration;
}
/**
* {@inheritDoc}
*/
@Override
public ITestDevice getDevice() {
return mDevice;
}
/**
* {@inheritDoc}
*/
@Override
public void setDevice(ITestDevice device) {
mDevice = device;
}
/** {@inheritDoc} */
@Override
public long getRuntimeHint() {
return mRuntimeHint;
}
/** {@inheritDoc} */
@Override
public void setAbi(IAbi abi) {
mAbi = abi;
}
/** {@inheritDoc} */
@Override
public IAbi getAbi() {
return mAbi;
}
/**
* {@inheritDoc}
*/
@Override
public void setBuild(IBuildInfo buildInfo) {
mBuildInfo = buildInfo;
}
/**
* Get the build info received by HostTest.
*
* @return the {@link IBuildInfo}
*/
protected IBuildInfo getBuild() {
return mBuildInfo;
}
/**
* @return true if shard-unit is method; false otherwise
*/
private boolean shardUnitIsMethod() {
return ShardUnit.METHOD.equals(mShardUnit);
}
/**
* {@inheritDoc}
*/
@Override
public void addIncludeFilter(String filter) {
// If filters change, reset test count so we recompute it next time it's requested.
mNumTestCases = -1;
mFilterHelper.addIncludeFilter(filter);
}
/**
* {@inheritDoc}
*/
@Override
public void addAllIncludeFilters(Set<String> filters) {
mNumTestCases = -1;
mFilterHelper.addAllIncludeFilters(filters);
}
/** {@inheritDoc} */
@Override
public void clearIncludeFilters() {
mNumTestCases = -1;
mFilterHelper.clearIncludeFilters();
}
/**
* {@inheritDoc}
*/
@Override
public void addExcludeFilter(String filter) {
mNumTestCases = -1;
mFilterHelper.addExcludeFilter(filter);
}
/** {@inheritDoc} */
@Override
public Set<String> getIncludeFilters() {
return mFilterHelper.getIncludeFilters();
}
/** {@inheritDoc} */
@Override
public Set<String> getExcludeFilters() {
return mFilterHelper.getExcludeFilters();
}
/**
* {@inheritDoc}
*/
@Override
public void addAllExcludeFilters(Set<String> filters) {
mNumTestCases = -1;
mFilterHelper.addAllExcludeFilters(filters);
}
/** {@inheritDoc} */
@Override
public void clearExcludeFilters() {
mNumTestCases = -1;
mFilterHelper.clearExcludeFilters();
}
/**
* Return the number of test cases across all classes part of the tests
*/
public int countTestCases() {
if (mTestMethods != null) {
return mTestMethods.size();
} else if (mNumTestCases >= 0) {
return mNumTestCases;
}
// Ensure filters are set in the helper
mFilterHelper.addAllIncludeAnnotation(mIncludeAnnotations);
mFilterHelper.addAllExcludeAnnotation(mExcludeAnnotations);
mFilterHelper.addAllIncludeFilters(mIncludeFilters);
mFilterHelper.addAllExcludeFilters(mExcludeFilters);
int count = 0;
for (Class<?> classObj : getClasses()) {
if (IRemoteTest.class.isAssignableFrom(classObj)
|| Test.class.isAssignableFrom(classObj)) {
TestSuite suite = collectTests(collectClasses(classObj));
int suiteCount = suite.countTestCases();
if (suiteCount == 0
&& IRemoteTest.class.isAssignableFrom(classObj)
&& !Test.class.isAssignableFrom(classObj)) {
// If it's a pure IRemoteTest we count the run() as one test.
count++;
} else {
count += suiteCount;
}
} else if (hasJUnit4Annotation(classObj)) {
Request req = Request.aClass(classObj);
req = req.filterWith(new JUnit4TestFilter(mFilterHelper, mJUnit4JarFiles));
Runner checkRunner = req.getRunner();
// If no tests are remaining after filtering, checkRunner is ErrorReportingRunner.
// testCount() for ErrorReportingRunner returns 1, skip this classObj in this case.
if (checkRunner instanceof ErrorReportingRunner) {
if (!EXCLUDE_NO_TEST_FAILURE.equals(
checkRunner.getDescription().getClassName())) {
// If after filtering we have remaining tests that are malformed, we still
// count them toward the total number of tests. (each malformed class will
// count as 1 in the testCount()).
count += checkRunner.testCount();
}
} else {
count += checkRunner.testCount();
}
} else {
count++;
}
}
return mNumTestCases = count;
}
/**
* Clear then set a class name to be run.
*/
protected void setClassName(String className) {
mClasses.clear();
mClasses.add(className);
}
@VisibleForTesting
public Set<String> getClassNames() {
return mClasses;
}
void setMethodName(String methodName) {
mMethodName = methodName;
}
/**
* {@inheritDoc}
*/
@Override
public void addIncludeAnnotation(String annotation) {
mIncludeAnnotations.add(annotation);
mFilterHelper.addIncludeAnnotation(annotation);
}
/**
* {@inheritDoc}
*/
@Override
public void addAllIncludeAnnotation(Set<String> annotations) {
mIncludeAnnotations.addAll(annotations);
mFilterHelper.addAllIncludeAnnotation(annotations);
}
/**
* {@inheritDoc}
*/
@Override
public void addExcludeAnnotation(String notAnnotation) {
mExcludeAnnotations.add(notAnnotation);
mFilterHelper.addExcludeAnnotation(notAnnotation);
}
/**
* {@inheritDoc}
*/
@Override
public void addAllExcludeAnnotation(Set<String> notAnnotations) {
mExcludeAnnotations.addAll(notAnnotations);
mFilterHelper.addAllExcludeAnnotation(notAnnotations);
}
/** {@inheritDoc} */
@Override
public Set<String> getIncludeAnnotations() {
return mIncludeAnnotations;
}
/** {@inheritDoc} */
@Override
public Set<String> getExcludeAnnotations() {
return mExcludeAnnotations;
}
/** {@inheritDoc} */
@Override
public void clearIncludeAnnotations() {
mIncludeAnnotations.clear();
mFilterHelper.clearIncludeAnnotations();
}
/** {@inheritDoc} */
@Override
public void clearExcludeAnnotations() {
mExcludeAnnotations.clear();
mFilterHelper.clearExcludeAnnotations();
}
/**
* Helper to set the information of an object based on some of its type.
*/
private void setTestObjectInformation(Object testObj) {
if (testObj instanceof IBuildReceiver) {
if (mBuildInfo == null) {
throw new IllegalArgumentException("Missing build information");
}
((IBuildReceiver)testObj).setBuild(mBuildInfo);
}
if (testObj instanceof IDeviceTest) {
if (mDevice == null) {
throw new IllegalArgumentException("Missing device");
}
((IDeviceTest)testObj).setDevice(mDevice);
}
// We are more flexible about abi info since not always available.
if (testObj instanceof IAbiReceiver) {
((IAbiReceiver)testObj).setAbi(mAbi);
}
if (testObj instanceof IInvocationContextReceiver) {
((IInvocationContextReceiver) testObj).setInvocationContext(mTestInfo.getContext());
}
if (testObj instanceof ITestInformationReceiver) {
((ITestInformationReceiver) testObj).setTestInformation(mTestInfo);
}
// managed runner should have the same set-option to pass option too.
if (testObj instanceof ISetOptionReceiver) {
try {
OptionSetter setter = new OptionSetter(testObj);
for (String item : mKeyValueOptions) {
setter.setOptionValue(SET_OPTION_NAME, item);
}
} catch (ConfigurationException e) {
throw new RuntimeException(e);
}
}
// TODO(olivernguyen): Clean this up after instrumenting runInstrumentationTests(...) API.
if (testObj instanceof IConfigurationReceiver) {
((IConfigurationReceiver) testObj).setConfiguration(mConfig);
}
}
/** {@inheritDoc} */
@Override
public void run(TestInformation testInfo, ITestInvocationListener listener)
throws DeviceNotAvailableException {
mTestInfo = testInfo;
// Ensure filters are set in the helper
mFilterHelper.addAllIncludeAnnotation(mIncludeAnnotations);
mFilterHelper.addAllExcludeAnnotation(mExcludeAnnotations);
mFilterHelper.addAllIncludeFilters(mIncludeFilters);
mFilterHelper.addAllExcludeFilters(mExcludeFilters);
try {
try {
List<Class<?>> classes = getClasses();
if (!mSkipTestClassCheck) {
if (classes.isEmpty()) {
throw new HarnessRuntimeException(
"No '--class' option was specified.",
InfraErrorIdentifier.OPTION_CONFIGURATION_ERROR);
}
}
if (mMethodName != null && classes.size() > 1) {
throw new HarnessRuntimeException(
String.format(
"'--method' only supports one '--class' name. Multiple were "
+ "given: '%s'",
classes),
InfraErrorIdentifier.OPTION_CONFIGURATION_ERROR);
}
} catch (RuntimeException e) {
listener.testRunStarted(this.getClass().getCanonicalName(), 0);
listener.testRunFailed(createFromException(e));
listener.testRunEnded(0L, new HashMap<String, Metric>());
return;
}
// Add a pretty logger to the events to mark clearly start/end of test cases.
if (mEnableHostDeviceLogs) {
PrettyTestEventLogger logger = new PrettyTestEventLogger(mTestInfo.getDevices());
listener = new ResultForwarder(logger, listener);
}
if (mTestMethods != null) {
runTestCases(listener);
} else {
runTestClasses(listener);
}
} finally {
mLoadedClasses.clear();
for (URLClassLoader cl : mOpenClassLoaders) {
StreamUtil.close(cl);
}
mOpenClassLoaders.clear();
}
}
private void runTestClasses(ITestInvocationListener listener)
throws DeviceNotAvailableException {
for (Class<?> classObj : getClasses()) {
if (IRemoteTest.class.isAssignableFrom(classObj)) {
IRemoteTest test = (IRemoteTest) loadObject(classObj);
applyFilters(classObj, test);
runRemoteTest(listener, test);
} else if (Test.class.isAssignableFrom(classObj)) {
TestSuite junitTest = collectTests(collectClasses(classObj));
// Resolve dynamic files for the junit3 test objects
Enumeration<Test> allTest = junitTest.tests();
while (allTest.hasMoreElements()) {
Test testObj = allTest.nextElement();
mDownloadedFiles.addAll(resolveRemoteFileForObject(testObj));
}
try {
runJUnit3Tests(listener, junitTest, classObj.getName());
} finally {
for (File f : mDownloadedFiles) {
FileUtil.recursiveDelete(f);
}
}
} else if (hasJUnit4Annotation(classObj)) {
// Include the method name filtering
Set<String> includes = mFilterHelper.getIncludeFilters();
if (mMethodName != null) {
includes.add(String.format(TEST_FULL_NAME_FORMAT, classObj.getName(),
mMethodName));
}
// Running in a full JUnit4 manner, no downgrade to JUnit3 {@link Test}
Request req = Request.aClass(classObj);
Runner checkRunner = null;
try {
req = req.filterWith(new JUnit4TestFilter(mFilterHelper, mJUnit4JarFiles));
checkRunner = req.getRunner();
} catch (RuntimeException e) {
listener.testRunStarted(classObj.getName(), 0);
listener.testRunFailed(createFromException(e));
listener.testRunEnded(0L, new HashMap<String, Metric>());
return;
}
runJUnit4Tests(listener, checkRunner, classObj.getName());
} else {
throw new IllegalArgumentException(
String.format("%s is not a supported test", classObj.getName()));
}
}
}
private void runTestCases(ITestInvocationListener listener) throws DeviceNotAvailableException {
Set<String> skippedTests = new LinkedHashSet<>();
for (Object obj : getTestMethods()) {
if (IRemoteTest.class.isInstance(obj)) {
IRemoteTest test = (IRemoteTest) obj;
runRemoteTest(listener, test);
} else if (TestSuite.class.isInstance(obj)) {
TestSuite junitTest = (TestSuite) obj;
if (!runJUnit3Tests(listener, junitTest, junitTest.getName())) {
skippedTests.add(junitTest.getName());
}
} else if (Description.class.isInstance(obj)) {
// Running in a full JUnit4 manner, no downgrade to JUnit3 {@link Test}
Description desc = (Description) obj;
Request req = Request.aClass(desc.getTestClass());
Runner checkRunner = req.filterWith(desc).getRunner();
try {
runJUnit4Tests(listener, checkRunner, desc.getClassName());
} catch (RuntimeException e) {
listener.testRunStarted(desc.getClassName(), 0);
listener.testRunFailed(createFromException(e));
listener.testRunEnded(0L, new HashMap<String, Metric>());
throw e;
}
} else {
throw new IllegalArgumentException(
String.format("%s is not a supported test", obj));
}
}
CLog.v("The following classes were skipped due to no test cases found: %s", skippedTests);
}
private void runRemoteTest(ITestInvocationListener listener, IRemoteTest test)
throws DeviceNotAvailableException {
if (mCollectTestsOnly) {
// Collect only mode is propagated to the test.
if (test instanceof ITestCollector) {
((ITestCollector) test).setCollectTestsOnly(true);
} else {
throw new IllegalArgumentException(
String.format(
"%s does not implement ITestCollector", test.getClass()));
}
}
// Set collectors for completeness but this isn't really supported as part of HostTest.
if (test instanceof IMetricCollectorReceiver) {
((IMetricCollectorReceiver) test)
.setMetricCollectors(new ArrayList<IMetricCollector>());
}
test.run(mTestInfo, listener);
}
/** Returns True if some tests were executed, false otherwise. */
private boolean runJUnit3Tests(
ITestInvocationListener listener, TestSuite junitTest, String className)
throws DeviceNotAvailableException {
if (mCollectTestsOnly) {
// Collect only mode, fake the junit test execution.
int testCount = junitTest.countTestCases();
listener.testRunStarted(className, testCount);
HashMap<String, Metric> empty = new HashMap<>();
for (int i = 0; i < testCount; i++) {
Test t = junitTest.testAt(i);
// Test does not have a getName method.
// using the toString format instead: <testName>(className)
String testName = t.toString().split("\\(")[0];
TestDescription testId = new TestDescription(t.getClass().getName(), testName);
listener.testStarted(testId);
listener.testEnded(testId, empty);
}
HashMap<String, Metric> emptyMap = new HashMap<>();
listener.testRunEnded(0, emptyMap);
if (testCount > 0) {
return true;
} else {
return false;
}
} else {
if (mTestCaseTimeout.toMillis() > 0L) {
listener =
new TestTimeoutEnforcer(
mTestCaseTimeout.toMillis(), TimeUnit.MILLISECONDS, listener);
}
try (CloseableTraceScope ignored = new CloseableTraceScope(className)) {
return JUnitRunUtil.runTest(listener, junitTest, className, mTestInfo);
}
}
}
private void runJUnit4Tests(
ITestInvocationListener listener, Runner checkRunner, String className)
throws DeviceNotAvailableException {
JUnitCore runnerCore = new JUnitCore();
if (mTestCaseTimeout.toMillis() > 0L) {
listener =
new TestTimeoutEnforcer(
mTestCaseTimeout.toMillis(), TimeUnit.MILLISECONDS, listener);
}
JUnit4ResultForwarder list = new JUnit4ResultForwarder(listener);
runnerCore.addListener(list);
if (!(checkRunner instanceof ErrorReportingRunner)) {
// If no tests are remaining after filtering, it returns an Error Runner.
long startTime = System.currentTimeMillis();
listener.testRunStarted(className, checkRunner.testCount());
try (CloseableTraceScope ignore = new CloseableTraceScope(className)) {
if (mCollectTestsOnly) {
fakeDescriptionExecution(checkRunner.getDescription(), list);
} else {
setTestObjectInformation(checkRunner);
ExceptionThrowingRunnerWrapper runnerWrapper =
new ExceptionThrowingRunnerWrapper(checkRunner, mTestInfo);
runnerCore.run(runnerWrapper);
}
} catch (CarryDnaeError e) {
throw e.getDeviceNotAvailableException();
} finally {
for (Description d : findIgnoredClass(checkRunner.getDescription())) {
TestDescription testDescription =
new TestDescription(d.getClassName(), "No Tests");
listener.testStarted(testDescription);
listener.testIgnored(testDescription);
listener.testEnded(testDescription, new HashMap<String, Metric>());
}
listener.testRunEnded(
System.currentTimeMillis() - startTime, new HashMap<String, Metric>());
}
} else {
// Special case where filtering leaves no tests to run, we report no failure
// in this case.
if (EXCLUDE_NO_TEST_FAILURE.equals(
checkRunner.getDescription().getClassName())) {
listener.testRunStarted(className, 0);
listener.testRunEnded(0, new HashMap<String, Metric>());
} else {
// Run the Error runner to get the failures from test classes.
listener.testRunStarted(className, checkRunner.testCount());
RunNotifier failureNotifier = new RunNotifier();
failureNotifier.addListener(list);
checkRunner.run(failureNotifier);
listener.testRunEnded(0, new HashMap<String, Metric>());
}
}
}
/** Search and return all the classes that are @Ignored */
private List<Description> findIgnoredClass(Description description) {
List<Description> ignoredClass = new ArrayList<>();
if (description.isSuite()) {
for (Description childDescription : description.getChildren()) {
ignoredClass.addAll(findIgnoredClass(childDescription));
}
} else {
if (description.getMethodName() == null) {
for (Annotation a : description.getAnnotations()) {
if (a.annotationType() != null && a.annotationType().equals(Ignore.class)) {
ignoredClass.add(description);
break;
}
}
}
}
return ignoredClass;
}
/**
* Helper to fake the execution of JUnit4 Tests, using the {@link Description}
*/
private void fakeDescriptionExecution(Description desc, JUnit4ResultForwarder listener) {
if (desc.getMethodName() == null || !desc.getChildren().isEmpty()) {
for (Description child : desc.getChildren()) {
fakeDescriptionExecution(child, listener);
}
} else {
try {
listener.testStarted(desc);
listener.testFinished(desc);
} catch (Exception e) {
// Should never happen
CLog.e(e);
}
}
}
private Set<Class<?>> collectClasses(Class<?> classObj) {
Set<Class<?>> classes = new HashSet<>();
if (TestSuite.class.isAssignableFrom(classObj)) {
TestSuite testObj = (TestSuite) loadObject(classObj);
classes.addAll(getClassesFromSuite(testObj));
} else {
classes.add(classObj);
}
return classes;
}
private Set<Class<?>> getClassesFromSuite(TestSuite suite) {
Set<Class<?>> classes = new HashSet<>();
Enumeration<Test> tests = suite.tests();
while (tests.hasMoreElements()) {
Test test = tests.nextElement();
if (test instanceof TestSuite) {
classes.addAll(getClassesFromSuite((TestSuite) test));
} else {
classes.addAll(collectClasses(test.getClass()));
}
}
return classes;
}
private TestSuite collectTests(Set<Class<?>> classes) {
TestSuite suite = new TestSuite();
for (Class<?> classObj : classes) {
String packageName = classObj.getPackage().getName();
String className = classObj.getName();
Method[] methods = null;
if (mMethodName == null) {
methods = classObj.getMethods();
} else {
try {
methods = new Method[] {
classObj.getMethod(mMethodName, (Class[]) null)
};
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException(
String.format("Cannot find %s#%s", className, mMethodName), e);
}
}
for (Method method : methods) {
if (!Modifier.isPublic(method.getModifiers())
|| !method.getReturnType().equals(Void.TYPE)
|| method.getParameterTypes().length > 0
|| !method.getName().startsWith("test")
|| !mFilterHelper.shouldRun(packageName, classObj, method)) {
continue;
}
Test testObj = (Test) loadObject(classObj, false);
if (testObj instanceof TestCase) {
((TestCase)testObj).setName(method.getName());
}
suite.addTest(testObj);
}
}
return suite;
}
private List<Object> getTestMethods() throws IllegalArgumentException {
if (mTestMethods != null) {
return mTestMethods;
}
mTestMethods = new ArrayList<>();
mFilterHelper.addAllIncludeAnnotation(mIncludeAnnotations);
mFilterHelper.addAllExcludeAnnotation(mExcludeAnnotations);
List<Class<?>> classes = getClasses();
for (Class<?> classObj : classes) {
if (Test.class.isAssignableFrom(classObj)) {
TestSuite suite = collectTests(collectClasses(classObj));
for (int i = 0; i < suite.testCount(); i++) {
TestSuite singletonSuite = new TestSuite();
singletonSuite.setName(classObj.getName());
Test testObj = suite.testAt(i);
singletonSuite.addTest(testObj);
if (IRemoteTest.class.isInstance(testObj)) {
setTestObjectInformation(testObj);
}
mTestMethods.add(singletonSuite);
}
} else if (IRemoteTest.class.isAssignableFrom(classObj)) {
// a pure IRemoteTest is considered a test method itself
IRemoteTest test = (IRemoteTest) loadObject(classObj);
applyFilters(classObj, test);
mTestMethods.add(test);
} else if (hasJUnit4Annotation(classObj)) {
// Running in a full JUnit4 manner, no downgrade to JUnit3 {@link Test}
Request req = Request.aClass(classObj);
// Include the method name filtering
Set<String> includes = mFilterHelper.getIncludeFilters();
if (mMethodName != null) {
includes.add(String.format(TEST_FULL_NAME_FORMAT, classObj.getName(),
mMethodName));
}
req = req.filterWith(new JUnit4TestFilter(mFilterHelper, mJUnit4JarFiles));
Runner checkRunner = req.getRunner();
Deque<Description> descriptions = new ArrayDeque<>();
descriptions.push(checkRunner.getDescription());
while (!descriptions.isEmpty()) {
Description desc = descriptions.pop();
if (desc.isTest()) {
mTestMethods.add(desc);
}
List<Description> children = desc.getChildren();
Collections.reverse(children);
for (Description child : children) {
descriptions.push(child);
}
}
} else {
throw new IllegalArgumentException(
String.format("%s is not a supported test", classObj.getName()));
}
}
return mTestMethods;
}
protected final List<Class<?>> getClasses() throws IllegalArgumentException {
if (!mLoadedClasses.isEmpty()) {
return mLoadedClasses;
}
// Use a set to avoid repeat between filters and jar search
Set<String> classNames = new HashSet<>();
List<Class<?>> classes = mLoadedClasses;
for (String className : mClasses) {
if (classNames.contains(className)) {
continue;
}
IllegalArgumentException initialError = null;
try {
classes.add(Class.forName(className, true, getClassLoader()));
classNames.add(className);
} catch (ClassNotFoundException e) {
initialError =
new IllegalArgumentException(
String.format("Could not load Test class %s", className), e);
}
if (initialError != null) {
// Fallback search a jar for the module under tests if any.
String moduleName =
mTestInfo
.getContext()
.getAttributes()
.getUniqueMap()
.get(ModuleDefinition.MODULE_NAME);
if (moduleName != null) {
URLClassLoader cl = null;
try {
File f = getJarFile(moduleName + ".jar", mTestInfo);
URL[] urls = {f.toURI().toURL()};
cl = URLClassLoader.newInstance(urls);
mJUnit4JarFiles.add(f);
Class<?> cls = cl.loadClass(className);
classes.add(cls);
classNames.add(className);
initialError = null;
mOpenClassLoaders.add(cl);
} catch (FileNotFoundException
| MalformedURLException
| ClassNotFoundException fallbackSearch) {
StreamUtil.close(cl);
CLog.e(
"Fallback search for a jar containing '%s' didn't work.Consider"
+ " using --jar option directly instead of using --class",
className);
}
}
}
if (initialError != null) {
throw initialError;
}
}
URLClassLoader cl = null;
// Inspect for the jar files
for (String jarName : mJars) {
JarFile jarFile = null;
try {
File file = getJarFile(jarName, mTestInfo);
jarFile = new JarFile(file);
Enumeration<JarEntry> e = jarFile.entries();
URL[] urls = {file.toURI().toURL()};
cl = URLClassLoader.newInstance(urls);
mJUnit4JarFiles.add(file);
mOpenClassLoaders.add(cl);
while (e.hasMoreElements()) {
JarEntry je = e.nextElement();
if (je.isDirectory()
|| !je.getName().endsWith(".class")
|| je.getName().contains("$")) {
continue;
}
String className = getClassName(je.getName());
if (classNames.contains(className)) {
continue;
}
try {
Class<?> cls = cl.loadClass(className);
int modifiers = cls.getModifiers();
if ((IRemoteTest.class.isAssignableFrom(cls)
|| Test.class.isAssignableFrom(cls)
|| hasJUnit4Annotation(cls))
&& !Modifier.isStatic(modifiers)
&& !Modifier.isPrivate(modifiers)
&& !Modifier.isProtected(modifiers)
&& !Modifier.isInterface(modifiers)
&& !Modifier.isAbstract(modifiers)) {
if (!mClasses.isEmpty() && !mClasses.contains(className)) {
continue;
}
classes.add(cls);
classNames.add(className);
}
} catch (UnsupportedClassVersionError ucve) {
throw new IllegalArgumentException(
String.format(
"Could not load class %s from jar %s. Reason:\n%s",
className, jarName, StreamUtil.getStackTrace(ucve)));
} catch (ClassNotFoundException cnfe) {
throw new IllegalArgumentException(
String.format("Cannot find test class %s", className));
} catch (IllegalAccessError | NoClassDefFoundError err) {
// IllegalAccessError can happen when the class or one of its super
// class/interfaces are package-private. We can't load such class from
// here (= outside of the package). Since our intention is not to load
// all classes in the jar, but to find our the main test classes, this
// can be safely skipped.
// NoClassDefFoundErrror is also okay because certain CTS test cases
// might statically link to a jar library (e.g. tools.jar from JDK)
// where certain internal classes in the library are referencing
// classes that are not available in the jar. Again, since our goal here
// is to find test classes, this can be safely skipped.
continue;
}
}
} catch (IOException e) {
CLog.e(e);
throw new IllegalArgumentException(e);
} finally {
StreamUtil.close(jarFile);
}
}
return classes;
}
/** Returns the default classloader. */
@VisibleForTesting
protected ClassLoader getClassLoader() {
return this.getClass().getClassLoader();
}
/** load the class object and set the test info (device, build). */
protected Object loadObject(Class<?> classObj) {
return loadObject(classObj, true);
}
/**
* Load the class object and set the test info if requested.
*
* @param classObj the class object to be loaded.
* @param setInfo True the test infos need to be set.
* @return The loaded object from the class.
*/
private Object loadObject(Class<?> classObj, boolean setInfo) throws IllegalArgumentException {
final String className = classObj.getName();
try {
Object testObj = classObj.getDeclaredConstructor().newInstance();
// set options
setOptionToLoadedObject(testObj, mKeyValueOptions);
// Set the test information if needed.
if (setInfo) {
setTestObjectInformation(testObj);
}
return testObj;
} catch (InstantiationException e) {
throw new IllegalArgumentException(String.format("Could not load Test class %s",
className), e);
} catch (IllegalAccessException e) {
throw new IllegalArgumentException(String.format("Could not load Test class %s",
className), e);
} catch (InvocationTargetException | NoSuchMethodException e) {
throw new IllegalArgumentException(
String.format("Could not load Test class %s", className), e);
}
}
/**
* Helper for Device Runners to use to set options the same way as HostTest, from set-option.
*
* @param testObj the object that will receive the options.
* @param keyValueOptions the list of options formatted as HostTest set-option requires.
*/
public static void setOptionToLoadedObject(Object testObj, List<String> keyValueOptions) {
if (!keyValueOptions.isEmpty()) {
OptionSetter setter;
try {
setter = new OptionSetter(testObj);
} catch (ConfigurationException ce) {
CLog.e(ce);
throw new RuntimeException("error creating option setter", ce);
}
for (String item : keyValueOptions) {
// Support escaping ':' using lookbehind in the regex. The regex engine will
// step backwards to check for the escape char when it matches the delim char.
// If it doesn't find the escape char, then a match is registered.
String delim = ":";
String esc = "\\";
String regex = "(?<!" + Pattern.quote(esc) + ")" + Pattern.quote(delim);
String[] fields = item.split(regex);
String key;
String value;
if (fields.length == 3) {
String target = fields[0];
if (testObj.getClass().getName().equals(target)) {
key = fields[1];
value =
fields[2].replaceAll(
Pattern.quote(esc) + Pattern.quote(delim), delim);
} else {
// TODO: We should track that all targeted option end up assigned
// eventually.
CLog.d(
"Targeted option %s is not applicable to %s",
item, testObj.getClass().getName());
continue;
}
} else if (fields.length == 2) {
key = fields[0];
value = fields[1].replaceAll(Pattern.quote(esc) + Pattern.quote(delim), delim);
} else {
throw new RuntimeException(String.format("invalid option spec \"%s\"", item));
}
try {
injectOption(setter, item, key, value);
} catch (ConfigurationException ce) {
CLog.e(ce);
throw new RuntimeException(
"error passing option '"
+ item
+ "' down to test class as key="
+ key
+ " value="
+ value,
ce);
}
}
}
}
private static void injectOption(OptionSetter setter, String origItem, String key, String value)
throws ConfigurationException {
String esc = "\\";
String delim = "=";
String regex = "(?<!" + Pattern.quote(esc) + ")" + Pattern.quote(delim);
String escDelim = Pattern.quote(esc) + Pattern.quote(delim);
String[] values = value.split(regex);
if (values.length == 1) {
setter.setOptionValue(key, values[0].replaceAll(escDelim, delim));
} else if (values.length == 2) {
setter.setOptionValue(
key,
values[0].replaceAll(escDelim, delim),
values[1].replaceAll(escDelim, delim));
} else {
throw new RuntimeException(
String.format(
"set-option provided '%s' format is invalid. Only one "
+ "'=' is allowed",
origItem));
}
}
/**
* Check if an elements that has annotation pass the filter. Exposed for unit testing.
* @param annotatedElement
* @return false if the test should not run.
*/
protected boolean shouldTestRun(AnnotatedElement annotatedElement) {
return mFilterHelper.shouldTestRun(annotatedElement);
}
/**
* {@inheritDoc}
*/
@Override
public void setCollectTestsOnly(boolean shouldCollectTest) {
mCollectTestsOnly = shouldCollectTest;
}
/**
* Helper to determine if we are dealing with a Test class with Junit4 annotations.
*/
protected boolean hasJUnit4Annotation(Class<?> classObj) {
if (classObj.isAnnotationPresent(SuiteClasses.class)) {
return true;
}
if (classObj.isAnnotationPresent(RunWith.class)) {
return true;
}
for (Method m : classObj.getMethods()) {
if (m.isAnnotationPresent(org.junit.Test.class)) {
return true;
}
}
return false;
}
/**
* Helper method to apply all the filters to an IRemoteTest.
*/
private void applyFilters(Class<?> classObj, IRemoteTest test) {
Set<String> includes = mFilterHelper.getIncludeFilters();
if (mMethodName != null) {
includes.add(String.format(TEST_FULL_NAME_FORMAT, classObj.getName(), mMethodName));
}
Set<String> excludes = mFilterHelper.getExcludeFilters();
if (test instanceof ITestFilterReceiver) {
((ITestFilterReceiver) test).addAllIncludeFilters(includes);
((ITestFilterReceiver) test).addAllExcludeFilters(excludes);
} else if (!includes.isEmpty() || !excludes.isEmpty()) {
throw new IllegalArgumentException(String.format(
"%s does not implement ITestFilterReceiver", classObj.getName()));
}
if (test instanceof ITestAnnotationFilterReceiver) {
((ITestAnnotationFilterReceiver) test).addAllIncludeAnnotation(
mIncludeAnnotations);
((ITestAnnotationFilterReceiver) test).addAllExcludeAnnotation(
mExcludeAnnotations);
}
}
/** We split by individual by either test class or method. */
@Override
public Collection<IRemoteTest> split(Integer shardCount, TestInformation testInfo) {
if (shardCount == null) {
return null;
}
if (shardCount < 1) {
throw new IllegalArgumentException("Must have at least 1 shard");
}
mTestInfo = testInfo;
List<IRemoteTest> listTests = new ArrayList<>();
try {
List<Class<?>> classes = getClasses();
if (classes.isEmpty()) {
throw new IllegalArgumentException("Missing Test class name");
}
if (mMethodName != null && classes.size() > 1) {
throw new IllegalArgumentException("Method name given with multiple test classes");
}
List<? extends Object> testObjects;
if (shardUnitIsMethod()) {
testObjects = getTestMethods();
} else {
testObjects = classes;
// ignore shardCount when shard unit is class;
// simply shard by the number of classes
shardCount = testObjects.size();
}
if (testObjects.size() == 1) {
return null;
}
int i = 0;
int numTotalTestCases = countTestCases();
for (Object testObj : testObjects) {
Class<?> classObj = Class.class.isInstance(testObj) ? (Class<?>) testObj : null;
HostTest test;
if (i >= listTests.size()) {
test = createHostTest(classObj);
test.mRuntimeHint = 0;
// Carry over non-annotation filters to shards.
test.addAllExcludeFilters(mFilterHelper.getExcludeFilters());
test.addAllIncludeFilters(mFilterHelper.getIncludeFilters());
listTests.add(test);
}
test = (HostTest) listTests.get(i);
Collection<? extends Object> subTests;
if (classObj != null) {
test.addClassName(classObj.getName());
test.mJars = this.mJars;
subTests = test.mClasses;
} else {
test.addTestMethod(testObj);
subTests = test.mTestMethods;
}
if (numTotalTestCases == 0) {
// In case there is no tests left
test.mRuntimeHint = 0L;
} else {
test.mRuntimeHint = mRuntimeHint * subTests.size() / numTotalTestCases;
}
i = (i + 1) % shardCount;
}
} finally {
mLoadedClasses.clear();
for (URLClassLoader cl : mOpenClassLoaders) {
StreamUtil.close(cl);
}
mOpenClassLoaders.clear();
}
return listTests;
}
private void addTestMethod(Object testObject) {
if (mTestMethods == null) {
mTestMethods = new ArrayList<>();
mClasses.clear();
}
mTestMethods.add(testObject);
if (IRemoteTest.class.isInstance(testObject)) {
addClassName(testObject.getClass().getName());
} else if (TestSuite.class.isInstance(testObject)) {
addClassName(((TestSuite)testObject).getName());
} else if (Description.class.isInstance(testObject)) {
addClassName(((Description)testObject).getTestClass().getName());
}
}
/**
* Add a class to be ran by HostTest.
*/
private void addClassName(String className) {
mClasses.add(className);
}
/**
* Helper to create a HostTest instance when sharding. Override to return any child from
* HostTest.
*/
protected HostTest createHostTest(Class<?> classObj) {
HostTest test;
try {
test = this.getClass().getDeclaredConstructor().newInstance();
} catch (InstantiationException
| IllegalAccessException
| InvocationTargetException
| NoSuchMethodException e) {
throw new RuntimeException(e);
}
OptionCopier.copyOptionsNoThrow(this, test);
if (classObj != null) {
test.setClassName(classObj.getName());
}
// clean the jar option since we are loading directly from classes after.
test.mJars = new HashSet<>();
// Copy the abi if available
test.setAbi(mAbi);
return test;
}
private String getClassName(String name) {
// -6 because of .class
return name.substring(0, name.length() - 6).replace('/', '.');
}
/**
* Inspect several location where the artifact are usually located for different use cases to
* find our jar.
*/
@VisibleForTesting
protected File getJarFile(String jarName, TestInformation testInfo)
throws FileNotFoundException {
return testInfo.getDependencyFile(jarName, /* target first*/ false);
}
@VisibleForTesting
DynamicRemoteFileResolver createResolver() {
DynamicRemoteFileResolver resolver = new DynamicRemoteFileResolver();
resolver.setDevice(mDevice);
resolver.addExtraArgs(mConfig.getCommandOptions().getDynamicDownloadArgs());
return resolver;
}
private Set<File> resolveRemoteFileForObject(Object obj) {
try (CloseableTraceScope ignore = new CloseableTraceScope("infra:resolveRemoteFiles")) {
OptionSetter setter = new OptionSetter(obj);
return setter.validateRemoteFilePath(createResolver());
} catch (BuildRetrievalError | ConfigurationException e) {
throw new RuntimeException(e);
}
}
private FailureDescription createFromException(Throwable exception) {
FailureDescription failure =
CurrentInvocation.createFailure(StreamUtil.getStackTrace(exception), null)
.setCause(exception);
if (exception instanceof IHarnessException) {
ErrorIdentifier id = ((IHarnessException) exception).getErrorId();
failure.setErrorIdentifier(id);
if (id != null) {
failure.setFailureStatus(id.status());
}
failure.setOrigin(((IHarnessException) exception).getOrigin());
}
return failure;
}
}