blob: 2b7ac7f4f707574f2f45be3d28d498f53a33d6b5 [file] [log] [blame]
/*
* Copyright (C) 2016 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 static com.google.common.base.Preconditions.checkState;
import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.config.Option;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.log.ITestLogger;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
import com.android.tradefed.result.CollectingTestListener;
import com.android.tradefed.result.FileInputStreamSource;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.result.ResultForwarder;
import com.android.tradefed.result.TestDescription;
import com.android.tradefed.result.TestRunResult;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.ICompressionStrategy;
import com.android.tradefed.util.ListInstrumentationParser;
import com.android.tradefed.util.ListInstrumentationParser.InstrumentationTarget;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* An abstract base class which runs installed instrumentation test(s) and collects execution data
* from each test that was run. Subclasses should implement the {@link #getReportFormat()} method
* to convert the execution data into a human readable report and log it.
*/
public abstract class CodeCoverageTestBase<T extends CodeCoverageReportFormat>
implements IDeviceTest, IRemoteTest, IBuildReceiver {
private ITestDevice mDevice = null;
private IBuildInfo mBuild = null;
@Option(name = "package",
description = "Only run instrumentation targets with the given package name")
private List<String> mPackageFilter = new ArrayList<>();
@Option(name = "runner",
description = "Only run instrumentation targets with the given test runner")
private List<String> mRunnerFilter = new ArrayList<>();
@Option(
name = "instrumentation-arg",
description = "Additional instrumentation arguments to provide to the runner"
)
private Map<String, String> mInstrumentationArgs = new HashMap<String, String>();
@Option(name = "max-tests-per-chunk",
description = "Maximum number of tests to execute in a single call to 'am instrument'. "
+ "Used to limit the number of tests that need to be re-run if one of them crashes.")
private int mMaxTestsPerChunk = Integer.MAX_VALUE;
@Option(name = "compression-strategy",
description = "Class name of an ICompressionStrategy that will be used to compress the "
+ "coverage report into a single archive file.")
private String mCompressionStrategy = "com.android.tradefed.util.ZipCompressionStrategy";
/**
* {@inheritDoc}
*/
@Override
public void setDevice(ITestDevice device) {
mDevice = device;
}
/**
* {@inheritDoc}
*/
@Override
public ITestDevice getDevice() {
return mDevice;
}
/**
* {@inheritDoc}
*/
@Override
public void setBuild(IBuildInfo buildInfo) {
mBuild = buildInfo;
}
/** Returns the {@link IBuildInfo} for this invocation. */
IBuildInfo getBuild() {
return mBuild;
}
/** Returns the package filter as set by the --package option(s). */
List<String> getPackageFilter() {
return mPackageFilter;
}
/** Sets the package-filter option for testing. */
@VisibleForTesting
void setPackageFilter(List<String> packageFilter) {
mPackageFilter = packageFilter;
}
/** Returns the runner filter as set by the --runner option(s). */
List<String> getRunnerFilter() {
return mRunnerFilter;
}
/** Sets the runner-filter option for testing. */
@VisibleForTesting
void setRunnerFilter(List<String> runnerFilter) {
mRunnerFilter = runnerFilter;
}
/** Returns the instrumentation arguments as set by the --instrumentation-arg option(s). */
Map<String, String> getInstrumentationArgs() {
return mInstrumentationArgs;
}
/** Sets the instrumentation-arg options for testing. */
@VisibleForTesting
void setInstrumentationArgs(Map<String, String> instrumentationArgs) {
mInstrumentationArgs = ImmutableMap.copyOf(instrumentationArgs);
}
/** Returns the maximum number of tests to run at once as set by --max-tests-per-chunk. */
int getMaxTestsPerChunk() {
return mMaxTestsPerChunk;
}
/** Sets the max-tests-per-chunk option for testing. */
@VisibleForTesting
void setMaxTestsPerChunk(int maxTestsPerChunk) {
mMaxTestsPerChunk = maxTestsPerChunk;
}
/** Returns the compression strategy that should be used to archive the coverage report. */
ICompressionStrategy getCompressionStrategy() {
try {
Class<?> clazz = Class.forName(mCompressionStrategy);
return clazz.asSubclass(ICompressionStrategy.class).newInstance();
} catch (ClassNotFoundException e) {
throw new RuntimeException("Unknown compression strategy: %s", e);
} catch (ClassCastException e) {
String msg = String.format("%s does not implement ICompressionStrategy",
mCompressionStrategy);
throw new RuntimeException(msg, e);
} catch (IllegalAccessException | InstantiationException e) {
String msg = String.format("Could not instantiate %s. The compression strategy must "
+ "have a public no-args constructor.", mCompressionStrategy);
throw new RuntimeException(msg, e);
}
}
/** Returns the list of output formats to use when generating the coverage report. */
protected abstract List<T> getReportFormat();
/**
* {@inheritDoc}
*/
@Override
public void run(final ITestInvocationListener listener) throws DeviceNotAvailableException {
File reportDir = null;
File reportArchive = null;
// Initialize a listener to collect logged coverage files
try (CoverageCollectingListener coverageListener =
new CoverageCollectingListener(getDevice(), listener)) {
// Make sure there are some installed instrumentation targets
Collection<InstrumentationTarget> instrumentationTargets = getInstrumentationTargets();
if (instrumentationTargets.isEmpty()) {
throw new RuntimeException("No instrumentation targets found");
}
// Run each of the installed instrumentation targets
for (InstrumentationTarget target : instrumentationTargets) {
// Compute the number of shards to use
int numShards = doesRunnerSupportSharding(target) ? getNumberOfShards(target) : 1;
// Split the test into shards and invoke each chunk separately in order to limit the
// number of test methods that need to be re-run if the test crashes.
for (int shardIndex = 0; shardIndex < numShards; shardIndex++) {
// Run the current shard
TestRunResult result = runTest(target, shardIndex, numShards, coverageListener);
// If the shard ran to completion and the coverage file was generated
String coverageFile = result.getRunMetrics().get(
CodeCoverageTest.COVERAGE_REMOTE_FILE_LABEL);
if (!result.isRunFailure() && getDevice().doesFileExist(coverageFile)) {
// Move on to the next shard
continue;
}
// Something went wrong with this shard, so re-run the tests individually
for (TestDescription identifier : collectTests(target, shardIndex, numShards)) {
runTest(target, identifier, coverageListener);
}
}
}
// Generate the coverage report(s) and log it
List<File> measurements = coverageListener.getCoverageFiles();
for (T format : getReportFormat()) {
File report = generateCoverageReport(measurements, format);
try {
doLogReport("coverage", format.getLogDataType(), report, listener);
} finally {
FileUtil.recursiveDelete(report);
}
}
} catch (IOException e) {
// Rethrow
throw new RuntimeException(e);
} finally {
// Cleanup
FileUtil.recursiveDelete(reportDir);
FileUtil.deleteFile(reportArchive);
cleanup();
}
}
/**
* Generates a human-readable coverage report from the given execution data. This method is
* called after all of the tests have finished running.
*
* @param executionData The execution data files collected while running the tests.
* @param format The output format of the generated coverage report.
*/
protected abstract File generateCoverageReport(Collection<File> executionData, T format)
throws IOException;
/**
* Cleans up any resources allocated during a test run. Called at the end of the
* {@link #run(ITestInvocationListener)} after all coverage reports have been logged. This
* method is a stub, but can be overridden by subclasses as necessary.
*/
protected void cleanup() { }
/**
* Logs the given data with the provided logger. The {@code data} can be a regular file, or a
* directory. If the data is a directory, it is compressed into a single archive file before
* being logged.
*
* @param dataName The name to use when logging the data.
* @param dataType The {@link LogDataType} of the data. Ignored if {@code data} is a directory.
* @param data The data to log. Can be a regular file, or a directory.
* @param logger The {@link ITestLogger} with which to log the data.
*/
void doLogReport(String dataName, LogDataType dataType, File data, ITestLogger logger)
throws IOException {
// If the data is a directory, compress it first
InputStreamSource streamSource;
if (data.isDirectory()) {
ICompressionStrategy strategy = getCompressionStrategy();
dataType = strategy.getLogDataType();
streamSource = new FileInputStreamSource(strategy.compress(data), true);
} else {
streamSource = new FileInputStreamSource(data);
}
// Log the data
logger.testLog(dataName, dataType, streamSource);
streamSource.close();
}
/** Returns a new {@link ListInstrumentationParser}. Exposed for unit testing. */
ListInstrumentationParser internalCreateListInstrumentationParser() {
return new ListInstrumentationParser();
}
/** Returns the list of instrumentation targets to run. */
Set<InstrumentationTarget> getInstrumentationTargets()
throws DeviceNotAvailableException {
Set<InstrumentationTarget> ret = new HashSet<>();
// Run pm list instrumentation to get the available instrumentation targets
ListInstrumentationParser parser = internalCreateListInstrumentationParser();
getDevice().executeShellCommand("pm list instrumentation", parser);
// If the package or runner filters are set, only include targets that match
for (InstrumentationTarget target : parser.getInstrumentationTargets()) {
List<String> packageFilter = getPackageFilter();
List<String> runnerFilter = getRunnerFilter();
if ((packageFilter.isEmpty() || packageFilter.contains(target.packageName)) &&
(runnerFilter.isEmpty() || runnerFilter.contains(target.runnerName))) {
ret.add(target);
}
}
return ret;
}
/** Checks whether the given {@link InstrumentationTarget} supports sharding. */
boolean doesRunnerSupportSharding(InstrumentationTarget target)
throws DeviceNotAvailableException {
// Compare the number of tests for a given shard with the total number of tests
return collectTests(target, 0, 2).size() < collectTests(target).size();
}
/** Returns all of the {@link TestDescription}s for the given target. */
Collection<TestDescription> collectTests(InstrumentationTarget target)
throws DeviceNotAvailableException {
return collectTests(target, 0, 1);
}
/** Returns all of the {@link TestDescription}s for the given target and shard. */
Collection<TestDescription> collectTests(
InstrumentationTarget target, int shardIndex, int numShards)
throws DeviceNotAvailableException {
// Create a runner and enable test collection
IRemoteAndroidTestRunner runner = createTestRunner(target, shardIndex, numShards);
runner.setTestCollection(true);
// Run the test and collect the test identifiers
CollectingTestListener listener = new CollectingTestListener();
getDevice().runInstrumentationTests(runner, listener);
return listener.getCurrentRunResults().getCompletedTests();
}
/** Returns a new {@link IRemoteAndroidTestRunner} instance. Exposed for unit testing. */
IRemoteAndroidTestRunner internalCreateTestRunner(String packageName, String runnerName) {
return new RemoteAndroidTestRunner(packageName, runnerName, getDevice().getIDevice());
}
/** Returns a new {@link IRemoteAndroidTestRunner} instance for the given target and shard. */
IRemoteAndroidTestRunner createTestRunner(InstrumentationTarget target,
int shardIndex, int numShards) {
// Get a new IRemoteAndroidTestRunner instance
IRemoteAndroidTestRunner ret = internalCreateTestRunner(
target.packageName, target.runnerName);
// Add instrumentation arguments
for (Map.Entry<String, String> argEntry : getInstrumentationArgs().entrySet()) {
ret.addInstrumentationArg(argEntry.getKey(), argEntry.getValue());
}
// Add shard options if necessary
if (numShards > 1) {
ret.addInstrumentationArg("shardIndex", Integer.toString(shardIndex));
ret.addInstrumentationArg("numShards", Integer.toString(numShards));
}
return ret;
}
/** Computes the number of shards that should be used when invoking the given target. */
int getNumberOfShards(InstrumentationTarget target) throws DeviceNotAvailableException {
double numTests = collectTests(target).size();
return (int)Math.ceil(numTests / getMaxTestsPerChunk());
}
/**
* Runs a single shard from the given {@code target}.
*
* @param target The instrumentation target to run.
* @param shardIndex The index of the shard to run.
* @param numShards The total number of shards for this target.
* @param listener The {@link ITestInvocationListener} to be notified of tests results.
* @return The results for the executed test run.
*/
TestRunResult runTest(InstrumentationTarget target, int shardIndex, int numShards,
ITestInvocationListener listener) throws DeviceNotAvailableException {
return runTest(createTest(target, shardIndex, numShards), listener);
}
/**
* Runs a single test method from the given {@code target}.
*
* @param target The instrumentation target to run.
* @param identifier The individual test method to run.
* @param listener The {@link ITestInvocationListener} to be notified of tests results.
* @return The results for the executed test run.
*/
TestRunResult runTest(
InstrumentationTarget target,
TestDescription identifier,
ITestInvocationListener listener)
throws DeviceNotAvailableException {
return runTest(createTest(target, identifier), listener);
}
/** Runs the given {@link InstrumentationTest} and returns the {@link TestRunResult}. */
TestRunResult runTest(InstrumentationTest test, ITestInvocationListener listener)
throws DeviceNotAvailableException {
// Run the test, and return the run results
CollectingTestListener results = new CollectingTestListener();
test.run(new ResultForwarder(results, listener));
return results.getCurrentRunResults();
}
/** Returns a new {@link InstrumentationTest}. Exposed for unit testing. */
InstrumentationTest internalCreateTest() {
return new InstrumentationTest();
}
/** Returns a new {@link InstrumentationTest} for the given target. */
InstrumentationTest createTest(InstrumentationTarget target) {
// Get a new InstrumentationTest instance
InstrumentationTest ret = internalCreateTest();
ret.setDevice(getDevice());
ret.setPackageName(target.packageName);
ret.setRunnerName(target.runnerName);
// Disable rerun mode, we want to stop the tests as soon as we fail.
ret.setRerunMode(false);
// Add instrumentation arguments
for (Map.Entry<String, String> argEntry : getInstrumentationArgs().entrySet()) {
ret.addInstrumentationArg(argEntry.getKey(), argEntry.getValue());
}
ret.addInstrumentationArg("coverage", "true");
return ret;
}
/** Returns a new {@link InstrumentationTest} for the identified test on the given target. */
InstrumentationTest createTest(InstrumentationTarget target, TestDescription identifier) {
// Get a new InstrumentationTest instance
InstrumentationTest ret = createTest(target);
// Set the specific test method to run
ret.setClassName(identifier.getClassName());
ret.setMethodName(identifier.getTestName());
return ret;
}
/** Returns a new {@link InstrumentationTest} for a particular shard on the given target. */
InstrumentationTest createTest(InstrumentationTarget target, int shardIndex, int numShards) {
// Get a new InstrumentationTest instance
InstrumentationTest ret = createTest(target);
// Add shard options if necessary
if (numShards > 1) {
ret.addInstrumentationArg("shardIndex", Integer.toString(shardIndex));
ret.addInstrumentationArg("numShards", Integer.toString(numShards));
}
return ret;
}
/** A {@link ResultForwarder} which collects coverage files. */
public static class CoverageCollectingListener extends ResultForwarder
implements AutoCloseable {
private ITestDevice mDevice;
private List<File> mCoverageFiles = new ArrayList<>();
private File mCoverageDir;
private String mCurrentRunName;
public CoverageCollectingListener(ITestDevice device, ITestInvocationListener... listeners)
throws IOException {
super(listeners);
mDevice = device;
// Initialize a directory to store the coverage files
mCoverageDir = FileUtil.createTempDir("execution_data");
}
/** Returns the list of collected coverage files. */
public List<File> getCoverageFiles() {
checkState(mCoverageDir != null, "This object is closed");
return mCoverageFiles;
}
/**
* {@inheritDoc}
*/
@Override
public void testLog(String dataName, LogDataType dataType, InputStreamSource dataStream) {
super.testLog(dataName, dataType, dataStream);
checkState(mCoverageDir != null, "This object is closed");
// We only care about coverage files
if (LogDataType.COVERAGE.equals(dataType)) {
// Save coverage data to a temporary location, and don't inform the listeners yet
try {
File coverageFile =
FileUtil.createTempFile(dataName + "_", ".exec", mCoverageDir);
FileUtil.writeToFile(dataStream.createInputStream(), coverageFile);
mCoverageFiles.add(coverageFile);
CLog.d("Got coverage file: %s", coverageFile.getAbsolutePath());
} catch (IOException e) {
CLog.e("Failed to save coverage file");
CLog.e(e);
}
}
}
/** {@inheritDoc} */
@Override
public void testRunStarted(String runName, int testCount) {
super.testRunStarted(runName, testCount);
mCurrentRunName = runName;
}
/** {@inheritDoc} */
@Override
public void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) {
// Look for the coverage file path from the run metrics
Metric coverageFilePathMetric =
runMetrics.get(CodeCoverageTest.COVERAGE_REMOTE_FILE_LABEL);
if (coverageFilePathMetric != null) {
String coverageFilePath =
coverageFilePathMetric.getMeasurements().getSingleString();
if (!Strings.isNullOrEmpty(coverageFilePath)) {
CLog.d("Coverage file at %s", coverageFilePath);
// Try to pull the coverage measurements off of the device
File coverageFile = null;
try {
coverageFile = mDevice.pullFile(coverageFilePath);
if (coverageFile != null) {
try (FileInputStreamSource source =
new FileInputStreamSource(coverageFile)) {
testLog(
mCurrentRunName + "_runtime_coverage",
LogDataType.COVERAGE,
source);
}
} else {
CLog.w(
"Failed to pull coverage file from device: %s",
coverageFilePath);
}
} catch (DeviceNotAvailableException e) {
// Nothing we can do, so just log the error.
CLog.w(e);
} finally {
FileUtil.deleteFile(coverageFile);
}
}
}
super.testRunEnded(elapsedTime, runMetrics);
}
/** {@inheritDoc} */
@Override
public void close() {
FileUtil.recursiveDelete(mCoverageDir);
mCoverageDir = null;
}
}
}