blob: 42b66842866d7c4336465025163fc033fb1cd503 [file] [log] [blame]
/*
* Copyright (C) 2015 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.cts.core.runner;
import android.app.Activity;
import android.app.Instrumentation;
import android.os.Bundle;
import android.os.Debug;
import android.support.test.internal.runner.listener.InstrumentationResultPrinter;
import android.support.test.internal.runner.listener.InstrumentationRunListener;
import android.support.test.internal.util.AndroidRunnerParams;
import android.util.Log;
import com.android.cts.core.runner.support.ExtendedAndroidRunnerBuilder;
import com.google.common.base.Splitter;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.junit.runner.Computer;
import org.junit.runner.JUnitCore;
import org.junit.runner.Request;
import org.junit.runner.Result;
import org.junit.runner.Runner;
import org.junit.runner.manipulation.Filter;
import org.junit.runner.manipulation.Filterable;
import org.junit.runner.manipulation.NoTestsRemainException;
import org.junit.runner.notification.RunListener;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.RunnerBuilder;
import static com.android.cts.core.runner.AndroidJUnitRunnerConstants.ARGUMENT_COUNT;
import static com.android.cts.core.runner.AndroidJUnitRunnerConstants.ARGUMENT_DEBUG;
import static com.android.cts.core.runner.AndroidJUnitRunnerConstants.ARGUMENT_LOG_ONLY;
import static com.android.cts.core.runner.AndroidJUnitRunnerConstants.ARGUMENT_NOT_TEST_CLASS;
import static com.android.cts.core.runner.AndroidJUnitRunnerConstants.ARGUMENT_NOT_TEST_FILE;
import static com.android.cts.core.runner.AndroidJUnitRunnerConstants.ARGUMENT_NOT_TEST_PACKAGE;
import static com.android.cts.core.runner.AndroidJUnitRunnerConstants.ARGUMENT_TEST_CLASS;
import static com.android.cts.core.runner.AndroidJUnitRunnerConstants.ARGUMENT_TEST_FILE;
import static com.android.cts.core.runner.AndroidJUnitRunnerConstants.ARGUMENT_TEST_PACKAGE;
import static com.android.cts.core.runner.AndroidJUnitRunnerConstants.ARGUMENT_TIMEOUT;
/**
* A drop-in replacement for AndroidJUnitTestRunner, which understands the same arguments, and has
* similar functionality, but can filter by expectations and allows a custom runner-builder to be
* provided.
*/
public class CoreTestRunner extends Instrumentation {
static final String TAG = "LibcoreTestRunner";
private static final java.lang.String ARGUMENT_ROOT_CLASSES = "core-root-classes";
private static final String ARGUMENT_CORE_LISTENER = "core-listener";
private static final Splitter CLASS_LIST_SPLITTER = Splitter.on(',').trimResults();
/** The args for the runner. */
private Bundle args;
/** Only log the number and names of tests, and not run them. */
private boolean logOnly;
/** The amount of time in millis to wait for a single test to complete. */
private long testTimeout;
/**
* The list of tests to run.
*/
private TestList testList;
/**
* The list of {@link RunListener} classes to create.
*/
private List<Class<? extends RunListener>> listenerClasses;
private Filter expectationFilter;
@Override
public void onCreate(final Bundle args) {
super.onCreate(args);
this.args = args;
boolean debug = "true".equalsIgnoreCase(args.getString(ARGUMENT_DEBUG));
if (debug) {
Log.i(TAG, "Waiting for debugger to connect...");
Debug.waitForDebugger();
Log.i(TAG, "Debugger connected.");
}
// Log the message only after getting a value from the args so that the args are
// unparceled.
Log.d(TAG, "In OnCreate: " + args);
// Treat logOnly and count as the same. This is not quite true as count should only send
// the host the number of tests but logOnly should send the name and number. However,
// this is how this has always behaved and it does not appear to have caused any problems.
// Changing it seems unnecessary given that count is CTSv1 only and CTSv1 will be removed
// soon now that CTSv2 is ready.
boolean testCountOnly = args.getBoolean(ARGUMENT_COUNT);
this.logOnly = "true".equalsIgnoreCase(args.getString(ARGUMENT_LOG_ONLY)) || testCountOnly;
this.testTimeout = parseUnsignedLong(args.getString(ARGUMENT_TIMEOUT), ARGUMENT_TIMEOUT);
expectationFilter = new ExpectationBasedFilter(args);
// The test can be run specifying a list of tests to run, or as cts-tradefed does it,
// by passing a fileName with a test to run on each line.
Set<String> testNameSet = new HashSet<>();
String arg;
if ((arg = args.getString(ARGUMENT_TEST_FILE)) != null) {
// The tests are specified in a file.
try {
testNameSet.addAll(readTestsFromFile(arg));
} catch (IOException err) {
finish(Activity.RESULT_CANCELED, new Bundle());
return;
}
} else if ((arg = args.getString(ARGUMENT_TEST_CLASS)) != null) {
// The tests are specified in a String passed in the bundle.
String[] tests = arg.split(",");
testNameSet.addAll(Arrays.asList(tests));
}
// Tests may be excluded from the run by passing a list of tests not to run,
// or by passing a fileName with a test not to run on each line.
Set<String> notTestNameSet = new HashSet<>();
if ((arg = args.getString(ARGUMENT_NOT_TEST_FILE)) != null) {
// The tests are specified in a file.
try {
notTestNameSet.addAll(readTestsFromFile(arg));
} catch (IOException err) {
finish(Activity.RESULT_CANCELED, new Bundle());
return;
}
} else if ((arg = args.getString(ARGUMENT_NOT_TEST_CLASS)) != null) {
// The classes are specified in a String passed in the bundle
String[] tests = arg.split(",");
notTestNameSet.addAll(Arrays.asList(tests));
}
Set<String> packageNameSet = new HashSet<>();
if ((arg = args.getString(ARGUMENT_TEST_PACKAGE)) != null) {
// The packages are specified in a String passed in the bundle
String[] packages = arg.split(",");
packageNameSet.addAll(Arrays.asList(packages));
}
Set<String> notPackageNameSet = new HashSet<>();
if ((arg = args.getString(ARGUMENT_NOT_TEST_PACKAGE)) != null) {
// The packages are specified in a String passed in the bundle
String[] packages = arg.split(",");
notPackageNameSet.addAll(Arrays.asList(packages));
}
List<String> roots = getRootClassNames(args);
if (roots == null) {
// Find all test classes
Collection<Class<?>> classes = TestClassFinder.getClasses(
Collections.singletonList(getContext().getPackageCodePath()),
getClass().getClassLoader());
testList = new TestList(classes);
} else {
testList = TestList.rootList(roots);
}
testList.addIncludeTestPackages(packageNameSet);
testList.addExcludeTestPackages(notPackageNameSet);
testList.addIncludeTests(testNameSet);
testList.addExcludeTests(notTestNameSet);
listenerClasses = new ArrayList<>();
String listenerArg = args.getString(ARGUMENT_CORE_LISTENER);
if (listenerArg != null) {
List<String> listenerClassNames = CLASS_LIST_SPLITTER.splitToList(listenerArg);
for (String listenerClassName : listenerClassNames) {
try {
Class<? extends RunListener> listenerClass = Class.forName(listenerClassName)
.asSubclass(RunListener.class);
listenerClasses.add(listenerClass);
} catch (ClassNotFoundException e) {
Log.e(TAG, "Could not load listener class: " + listenerClassName, e);
}
}
}
start();
}
private List<String> getRootClassNames(Bundle args) {
String rootClasses = args.getString(ARGUMENT_ROOT_CLASSES);
List<String> roots;
if (rootClasses == null) {
roots = null;
} else {
roots = CLASS_LIST_SPLITTER.splitToList(rootClasses);
}
return roots;
}
@Override
public void onStart() {
if (logOnly) {
Log.d(TAG, "Counting/logging tests only");
} else {
Log.d(TAG, "Running tests");
}
AndroidRunnerParams runnerParams = new AndroidRunnerParams(this, args,
false, testTimeout, false /*ignoreSuiteMethods*/);
Runner runner;
try {
RunnerBuilder runnerBuilder = new ExtendedAndroidRunnerBuilder(runnerParams);
Class[] classes = testList.getClassesToRun();
for (Class cls : classes) {
Log.d(TAG, "Found class to run: " + cls.getName());
}
runner = new Computer().getSuite(runnerBuilder, classes);
if (runner instanceof Filterable) {
Log.d(TAG, "Applying filters");
Filterable filterable = (Filterable) runner;
// Filter out all the tests that are expected to fail.
try {
filterable.filter(expectationFilter);
} catch (NoTestsRemainException e) {
// Sometimes filtering will remove all tests but we do not care about that.
}
Log.d(TAG, "Applied filters");
}
// If the tests are only supposed to be logged and not actually run then replace the
// runner with a runner that will fire notifications for all the tests that would have
// been run. This is needed because CTSv2 does a log only run through a CTS module in
// order to generate a list of tests that will be run so that it can monitor them.
// Encapsulating that in a Runner implementation makes it easier to leverage the
// existing code for running tests.
if (logOnly) {
runner = new DescriptionHierarchyNotifier(runner.getDescription());
}
} catch (InitializationError e) {
throw new RuntimeException("Could not create a suite", e);
}
InstrumentationResultPrinter instrumentationResultPrinter =
new InstrumentationResultPrinter();
instrumentationResultPrinter.setInstrumentation(this);
JUnitCore core = new JUnitCore();
core.addListener(instrumentationResultPrinter);
// If not logging the list of tests then add any additional configured listeners. These
// must be added before firing any events.
if (!logOnly) {
// Add additional configured listeners.
for (Class<? extends RunListener> listenerClass : listenerClasses) {
try {
RunListener runListener = listenerClass.newInstance();
if (runListener instanceof InstrumentationRunListener) {
((InstrumentationRunListener) runListener).setInstrumentation(this);
}
core.addListener(runListener);
} catch (InstantiationException | IllegalAccessException e) {
Log.e(TAG,
"Could not create instance of listener: " + listenerClass, e);
}
}
}
Log.d(TAG, "Finished preparations, running/listing tests");
Bundle results = new Bundle();
Result junitResults = new Result();
try {
junitResults = core.run(Request.runner(runner));
} catch (RuntimeException e) {
final String msg = "Fatal exception when running tests";
Log.e(TAG, msg, e);
// report the exception to instrumentation out
results.putString(Instrumentation.REPORT_KEY_STREAMRESULT,
msg + "\n" + Log.getStackTraceString(e));
} finally {
ByteArrayOutputStream summaryStream = new ByteArrayOutputStream();
// create the stream used to output summary data to the user
PrintStream summaryWriter = new PrintStream(summaryStream);
instrumentationResultPrinter.instrumentationRunFinished(summaryWriter,
results, junitResults);
summaryWriter.close();
results.putString(Instrumentation.REPORT_KEY_STREAMRESULT,
String.format("\n%s", summaryStream.toString()));
}
Log.d(TAG, "Finished");
finish(Activity.RESULT_OK, results);
}
/**
* Read tests from a specified file.
*
* @return class names of tests. If there was an error reading the file, null is returned.
*/
private static List<String> readTestsFromFile(String fileName) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(fileName))) {
List<String> tests = new ArrayList<>();
String line;
while ((line = br.readLine()) != null) {
tests.add(line);
}
return tests;
} catch (IOException err) {
Log.e(TAG, "There was an error reading the test class list: " + err.getMessage());
throw err;
}
}
/**
* Parse long from given value - except either Long or String.
*
* @return the value, -1 if not found
* @throws NumberFormatException if value is negative or not a number
*/
private static long parseUnsignedLong(Object value, String name) {
if (value != null) {
long longValue = Long.parseLong(value.toString());
if (longValue < 0) {
throw new NumberFormatException(name + " can not be negative");
}
return longValue;
}
return -1;
}
}