blob: 94e4aa619ad461a48eda86cdc3ded1b5edf833a4 [file] [log] [blame]
/*
* Copyright (C) 2019 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.binary;
import com.google.common.annotations.VisibleForTesting;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.OptionCopier;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.invoker.TestInformation;
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.TestDescription;
import com.android.tradefed.testtype.IAbi;
import com.android.tradefed.testtype.IAbiReceiver;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.testtype.IRuntimeHintProvider;
import com.android.tradefed.testtype.IShardableTest;
import com.android.tradefed.testtype.ITestCollector;
import com.android.tradefed.testtype.ITestFilterReceiver;
import com.android.tradefed.result.error.InfraErrorIdentifier;
import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
import com.android.tradefed.util.StreamUtil;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/** Base class for executable style of tests. For example: binaries, shell scripts. */
public abstract class ExecutableBaseTest
implements IRemoteTest,
IRuntimeHintProvider,
ITestCollector,
IShardableTest,
IAbiReceiver,
ITestFilterReceiver {
public static final String NO_BINARY_ERROR = "Binary %s does not exist.";
@Option(
name = "per-binary-timeout",
isTimeVal = true,
description = "Timeout applied to each binary for their execution.")
private long mTimeoutPerBinaryMs = 5 * 60 * 1000L;
@Option(name = "binary", description = "Path to the binary to be run. Can be repeated.")
private List<String> mBinaryPaths = new ArrayList<>();
@Option(
name = "test-command-line",
description = "The test commands of each test names.",
requiredForRerun = true)
private Map<String, String> mTestCommands = new LinkedHashMap<>();
@Option(
name = "collect-tests-only",
description = "Only dry-run through the tests, do not actually run them.")
private boolean mCollectTestsOnly = false;
@Option(
name = "runtime-hint",
description = "The hint about the test's runtime.",
isTimeVal = true
)
private long mRuntimeHintMs = 60000L; // 1 minute
private IAbi mAbi;
private TestInformation mTestInfo;
private Set<String> mIncludeFilters = new LinkedHashSet<>();
private Set<String> mExcludeFilters = new LinkedHashSet<>();
/**
* Get test commands.
*
* @return the test commands.
*/
@VisibleForTesting
Map<String, String> getTestCommands() {
return mTestCommands;
}
/** @return the timeout applied to each binary for their execution. */
protected long getTimeoutPerBinaryMs() {
return mTimeoutPerBinaryMs;
}
/** {@inheritDoc} */
@Override
public void addIncludeFilter(String filter) {
mIncludeFilters.add(filter);
}
/** {@inheritDoc} */
@Override
public void addExcludeFilter(String filter) {
mExcludeFilters.add(filter);
}
/** {@inheritDoc} */
@Override
public void addAllIncludeFilters(Set<String> filters) {
mIncludeFilters.addAll(filters);
}
/** {@inheritDoc} */
@Override
public void addAllExcludeFilters(Set<String> filters) {
mExcludeFilters.addAll(filters);
}
/** {@inheritDoc} */
@Override
public void clearIncludeFilters() {
mIncludeFilters.clear();
}
/** {@inheritDoc} */
@Override
public void clearExcludeFilters() {
mExcludeFilters.clear();
}
/** {@inheritDoc} */
@Override
public Set<String> getIncludeFilters() {
return mIncludeFilters;
}
/** {@inheritDoc} */
@Override
public Set<String> getExcludeFilters() {
return mExcludeFilters;
}
@Override
public void run(TestInformation testInfo, ITestInvocationListener listener)
throws DeviceNotAvailableException {
mTestInfo = testInfo;
Map<String, String> testCommands = getAllTestCommands();
for (String testName : testCommands.keySet()) {
String cmd = testCommands.get(testName);
String path = findBinary(cmd);
TestDescription description = new TestDescription(testName, testName);
if (shouldSkipCurrentTest(description)) continue;
if (path == null) {
listener.testRunStarted(testName, 0);
FailureDescription failure =
FailureDescription.create(
String.format(NO_BINARY_ERROR, cmd),
FailureStatus.TEST_FAILURE)
.setErrorIdentifier(InfraErrorIdentifier.ARTIFACT_NOT_FOUND);
listener.testRunFailed(failure);
listener.testRunEnded(0L, new HashMap<String, Metric>());
} else {
listener.testRunStarted(testName, 1);
long startTimeMs = System.currentTimeMillis();
listener.testStarted(description);
try {
if (!mCollectTestsOnly) {
// Do not actually run the test if we are dry running it.
runBinary(path, listener, description);
}
} catch (IOException e) {
listener.testFailed(
description, FailureDescription.create(StreamUtil.getStackTrace(e)));
} finally {
listener.testEnded(description, new HashMap<String, Metric>());
listener.testRunEnded(
System.currentTimeMillis() - startTimeMs,
new HashMap<String, Metric>());
}
}
}
}
/**
* Check if current test should be skipped.
*
* @param description The test in progress.
* @return true if the test should be skipped.
*/
private boolean shouldSkipCurrentTest(TestDescription description) {
// Force to skip any test not listed in include filters, or listed in exclude filters.
// exclude filters have highest priority.
String testName = description.getTestName();
if (mExcludeFilters.contains(testName)
|| mExcludeFilters.contains(description.toString())) {
return true;
}
if (!mIncludeFilters.isEmpty()) {
return !mIncludeFilters.contains(testName)
&& !mIncludeFilters.contains(description.toString());
}
return false;
}
/**
* Search for the binary to be able to run it.
*
* @param binary the path of the binary or simply the binary name.
* @return The path to the binary, or null if not found.
*/
public abstract String findBinary(String binary) throws DeviceNotAvailableException;
/**
* Actually run the binary at the given path.
*
* @param binaryPath The path of the binary.
* @param listener The listener where to report the results.
* @param description The test in progress.
*/
public abstract void runBinary(
String binaryPath, ITestInvocationListener listener, TestDescription description)
throws DeviceNotAvailableException, IOException;
/** {@inheritDoc} */
@Override
public final void setCollectTestsOnly(boolean shouldCollectTest) {
mCollectTestsOnly = shouldCollectTest;
}
/** {@inheritDoc} */
@Override
public final long getRuntimeHint() {
return mRuntimeHintMs;
}
/** {@inheritDoc} */
@Override
public final void setAbi(IAbi abi) {
mAbi = abi;
}
/** {@inheritDoc} */
@Override
public IAbi getAbi() {
return mAbi;
}
TestInformation getTestInfo() {
return mTestInfo;
}
/** {@inheritDoc} */
@Override
public final Collection<IRemoteTest> split() {
int testCount = mBinaryPaths.size() + mTestCommands.size();
if (testCount <= 2) {
return null;
}
Collection<IRemoteTest> tests = new ArrayList<>();
for (String path : mBinaryPaths) {
tests.add(getTestShard(path, null, null));
}
Map<String, String> testCommands = new LinkedHashMap<>(mTestCommands);
for (String testName : testCommands.keySet()) {
String cmd = testCommands.get(testName);
tests.add(getTestShard(null, testName, cmd));
}
return tests;
}
/**
* Get a testShard of ExecutableBaseTest.
*
* @param binaryPath the binary path for ExecutableHostTest.
* @param testName the test name for ExecutableTargetTest.
* @param cmd the test command for ExecutableTargetTest.
* @return a shard{@link IRemoteTest} of ExecutableBaseTest{@link ExecutableBaseTest}
*/
private IRemoteTest getTestShard(String binaryPath, String testName, String cmd) {
ExecutableBaseTest shard = null;
try {
shard = this.getClass().getDeclaredConstructor().newInstance();
OptionCopier.copyOptionsNoThrow(this, shard);
shard.mBinaryPaths.clear();
shard.mTestCommands.clear();
if (binaryPath != null) {
// Set one binary per shard
shard.mBinaryPaths.add(binaryPath);
} else if (testName != null && cmd != null) {
// Set one test command per shard
shard.mTestCommands.put(testName, cmd);
}
} catch (InstantiationException
| IllegalAccessException
| InvocationTargetException
| NoSuchMethodException e) {
// This cannot happen because the class was already created once at that point.
throw new RuntimeException(
String.format(
"%s (%s) when attempting to create shard object",
e.getClass().getSimpleName(), e.getMessage()));
}
return shard;
}
/**
* Convert mBinaryPaths to mTestCommands for consistency.
*
* @return a Map{@link LinkedHashMap}<String, String> of testCommands.
*/
private Map<String, String> getAllTestCommands() {
Map<String, String> testCommands = new LinkedHashMap<>(mTestCommands);
for (String binary : mBinaryPaths) {
testCommands.put(new File(binary).getName(), binary);
}
return testCommands;
}
}