blob: e52eebe063d63a614a7be378aab2d5393cce2a51 [file] [log] [blame]
/*
* Copyright (C) 2018 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.suite;
import com.android.tradefed.config.IConfiguration;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.DeviceUnresponsiveException;
import com.android.tradefed.device.metric.IMetricCollector;
import com.android.tradefed.invoker.IInvocationContext;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.ILogSaver;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.LogSaverResultForwarder;
import com.android.tradefed.result.MergeStrategy;
import com.android.tradefed.result.TestDescription;
import com.android.tradefed.result.TestRunResult;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.testtype.ITestFilterReceiver;
import com.android.tradefed.testtype.suite.ITestSuite.RetryStrategy;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Sets;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* A wrapper class works on the {@link IRemoteTest} to granulate the IRemoteTest in testcase level.
* An IRemoteTest can contain multiple testcases. Previously, these testcases are treated as a
* whole: When IRemoteTest runs, all testcases will run. Some IRemoteTest (The ones that implements
* ITestFilterReceiver) can accept a whitelist of testcases and only run those testcases. This class
* takes advantage of the existing feature and provides a more flexible way to run test suite.
*
* <ul>
* <li> Single testcase can be retried multiple times (within the same IRemoteTest run) to reduce
* the non-test-error failure rates.
* <li> The retried testcases are dynamically collected from previous run failures.
* </ul>
*
* <p>Note:
*
* <ul>
* <li> The prerequisite to run a subset of test cases is that the test type should implement the
* interface {@link ITestFilterReceiver}.
* <li> X is customized max retry number.
* </ul>
*/
public class GranularRetriableTestWrapper implements IRemoteTest {
private IRemoteTest mTest;
private List<IMetricCollector> mRunMetricCollectors;
private TestFailureListener mFailureListener;
private IInvocationContext mModuleInvocationContext;
private IConfiguration mModuleConfiguration;
private ModuleListener mMainGranularRunListener;
private RetryLogSaverResultForwarder mRetryAttemptForwarder;
private List<ITestInvocationListener> mModuleLevelListeners;
private ILogSaver mLogSaver;
private String mModuleId;
private int mMaxRunLimit;
// Tracking of the metrics
/** How much time are we spending doing the retry attempts */
private long mRetryTime = 0L;
/** The number of test cases that passed after a failed attempt */
private long mSuccessRetried = 0L;
/** The number of test cases that remained failed after all retry attempts */
private long mFailedRetried = 0L;
private RetryStrategy mRetryStrategy = RetryStrategy.RETRY_TEST_CASE_FAILURE;
public GranularRetriableTestWrapper(
IRemoteTest test,
ITestInvocationListener mainListener,
TestFailureListener failureListener,
List<ITestInvocationListener> moduleLevelListeners,
int maxRunLimit) {
mTest = test;
mMainGranularRunListener = new ModuleListener(mainListener);
mFailureListener = failureListener;
mModuleLevelListeners = moduleLevelListeners;
mMaxRunLimit = maxRunLimit;
}
/**
* Set the {@link ModuleDefinition} name as a {@link GranularRetriableTestWrapper} attribute.
*
* @param moduleId the name of the moduleDefinition.
*/
public void setModuleId(String moduleId) {
mModuleId = moduleId;
}
/**
* Set the {@link ModuleDefinition} RunStrategy as a {@link GranularRetriableTestWrapper}
* attribute.
*
* @param skipTestCases whether the testcases should be skipped.
*/
public void setMarkTestsSkipped(boolean skipTestCases) {
mMainGranularRunListener.setMarkTestsSkipped(skipTestCases);
}
/**
* Set the {@link ModuleDefinition}'s runMetricCollector as a {@link
* GranularRetriableTestWrapper} attribute.
*
* @param runMetricCollectors A list of MetricCollector for the module.
*/
public void setMetricCollectors(List<IMetricCollector> runMetricCollectors) {
mRunMetricCollectors = runMetricCollectors;
}
/**
* Set the {@link ModuleDefinition}'s ModuleConfig as a {@link GranularRetriableTestWrapper}
* attribute.
*
* @param moduleConfiguration Provide the module metrics.
*/
public void setModuleConfig(IConfiguration moduleConfiguration) {
mModuleConfiguration = moduleConfiguration;
}
/**
* Set the {@link IInvocationContext} as a {@link GranularRetriableTestWrapper} attribute.
*
* @param moduleInvocationContext The wrapper uses the InvocationContext to initialize the
* MetricCollector when necessary.
*/
public void setInvocationContext(IInvocationContext moduleInvocationContext) {
mModuleInvocationContext = moduleInvocationContext;
}
/**
* Set the Module's {@link ILogSaver} as a {@link GranularRetriableTestWrapper} attribute.
*
* @param logSaver The listeners for each test run should save the logs.
*/
public void setLogSaver(ILogSaver logSaver) {
mLogSaver = logSaver;
}
/** Sets the {@link RetryStrategy} to be used when retrying. */
public final void setRetryStrategy(RetryStrategy retryStrategy) {
mRetryStrategy = retryStrategy;
}
/**
* Initialize a new {@link ModuleListener} for each test run.
*
* @return a {@link ITestInvocationListener} listener which contains the new {@link
* ModuleListener}, the main {@link ITestInvocationListener} and main {@link
* TestFailureListener}, and wrapped by RunMetricsCollector and Module MetricCollector (if
* not initialized).
*/
private ITestInvocationListener initializeListeners() {
List<ITestInvocationListener> currentTestListener = new ArrayList<>();
// Add all the module level listeners, including TestFailureListener
if (mModuleLevelListeners != null) {
currentTestListener.addAll(mModuleLevelListeners);
}
currentTestListener.add(mMainGranularRunListener);
mRetryAttemptForwarder = new RetryLogSaverResultForwarder(mLogSaver, currentTestListener);
ITestInvocationListener runListener = mRetryAttemptForwarder;
if (mFailureListener != null) {
mFailureListener.setLogger(mRetryAttemptForwarder);
currentTestListener.add(mFailureListener);
}
if (mRunMetricCollectors != null) {
// Module only init the collectors here to avoid triggering the collectors when
// replaying the cached events at the end. This ensure metrics are capture at
// the proper time in the invocation.
for (IMetricCollector collector : mRunMetricCollectors) {
runListener = collector.init(mModuleInvocationContext, runListener);
}
}
// The module collectors itself are added: this list will be very limited.
for (IMetricCollector collector : mModuleConfiguration.getMetricCollectors()) {
runListener = collector.init(mModuleInvocationContext, runListener);
}
return runListener;
}
/**
* Schedule a series of {@link IRemoteTest#run(ITestInvocationListener)}.
*
* @param listener The ResultForwarder listener which contains a new moduleListener for each
* run.
*/
@Override
public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
ITestInvocationListener allListeners = initializeListeners();
// First do the regular run, not retried.
intraModuleRun(allListeners);
if (mMaxRunLimit <= 1) {
return;
}
// If the very first attempt failed, then don't proceed.
if (RetryStrategy.RERUN_UNTIL_FAILURE.equals(mRetryStrategy)) {
Set<TestDescription> lastRun = getFailedTestCases(0);
// If we encountered a failure
if (!lastRun.isEmpty() || mMainGranularRunListener.hasRunCrashedAtAttempt(0)) {
CLog.w("%s failed after the first run. Stopping.", lastRun);
return;
}
}
// Deal with retried attempted
long startTime = System.currentTimeMillis();
Set<TestDescription> previousFailedTests = null;
Set<String> originalFilters = new HashSet<>();
// TODO(b/77548917): Right now we only support ITestFilterReceiver. We should expect to
// support ITestFile*Filter*Receiver in the future.
if (mTest instanceof ITestFilterReceiver) {
ITestFilterReceiver test = (ITestFilterReceiver) mTest;
originalFilters = new LinkedHashSet<>(test.getIncludeFilters());
} else if (!shouldHandleFailure(mRetryStrategy)) {
// TODO: improve this for test run failures, since they rerun the full run we should
// be able to rerun even non-ITestFilterReceiver
CLog.d("RetryStrategy does not involved moving filters proceeding with retry.");
} else {
CLog.d(
"%s does not implement ITestFilterReceiver, thus cannot work with "
+ "intra-module retry.",
mTest);
return;
}
try {
CLog.d("Starting intra-module retry.");
for (int attemptNumber = 1; attemptNumber < mMaxRunLimit; attemptNumber++) {
// Reset the filters to original.
if (mTest instanceof ITestFilterReceiver) {
((ITestFilterReceiver) mTest).clearIncludeFilters();
((ITestFilterReceiver) mTest).addAllIncludeFilters(originalFilters);
}
// TODO: sort out the collection of metrics for each strategy
if (shouldHandleFailure(mRetryStrategy)) {
boolean shouldContinue = false;
// In case of test run failure and we should retry test runs
if (RetryStrategy.RETRY_TEST_RUN_FAILURE.equals(mRetryStrategy)
|| RetryStrategy.RETRY_ANY_FAILURE.equals(mRetryStrategy)) {
if (mMainGranularRunListener.hasRunCrashedAtAttempt(attemptNumber - 1)) {
CLog.d("Retrying the run failure.");
shouldContinue = true;
}
}
if (RetryStrategy.RETRY_TEST_CASE_FAILURE.equals(mRetryStrategy)
|| RetryStrategy.RETRY_ANY_FAILURE.equals(mRetryStrategy)) {
// In case of test case failure, we retry with filters.
previousFailedTests = getFailedTestCases(attemptNumber - 1);
if (previousFailedTests.size() > 0 && !shouldContinue) {
CLog.d("Retrying the test case failure.");
shouldContinue = true;
addRetriedTestsToIncludeFilters(mTest, previousFailedTests);
}
}
if (!shouldContinue) {
CLog.d("No test run or test case failures. No need to retry.");
break;
}
}
// Run the tests again
intraModuleRun(allListeners);
Set<TestDescription> lastRun = getFailedTestCases(attemptNumber);
if (shouldHandleFailure(mRetryStrategy)) {
// Evaluate success from what we just ran
if (previousFailedTests != null) {
Set<TestDescription> diff = Sets.difference(previousFailedTests, lastRun);
mSuccessRetried += diff.size();
previousFailedTests = lastRun;
}
}
if (RetryStrategy.RERUN_UNTIL_FAILURE.equals(mRetryStrategy)) {
// If we encountered a failure do not proceed
if (!lastRun.isEmpty()
|| mMainGranularRunListener.hasRunCrashedAtAttempt(attemptNumber)) {
CLog.w("%s failed at iteration %s. Stopping.", lastRun, attemptNumber);
break;
}
}
}
} finally {
if (previousFailedTests != null) {
mFailedRetried += previousFailedTests.size();
}
// Track how long we spend in retry
mRetryTime = System.currentTimeMillis() - startTime;
}
}
/**
* If the strategy needs to handle some failures return True. If it needs to retry no matter
* what like {@link RetryStrategy#ITERATIONS} returns False.
*/
private boolean shouldHandleFailure(RetryStrategy retryStrategy) {
return RetryStrategy.RETRY_ANY_FAILURE.equals(retryStrategy)
|| RetryStrategy.RETRY_TEST_RUN_FAILURE.equals(retryStrategy)
|| RetryStrategy.RETRY_TEST_CASE_FAILURE.equals(retryStrategy);
}
/**
* Collect failed test cases from listener.
*
* @param attemptNumber the 0-indexed integer indicating which attempt to gather failed cases.
*/
private Set<TestDescription> getFailedTestCases(int attemptNumber) {
Set<TestDescription> failedTestCases = new HashSet<TestDescription>();
for (String runName : mMainGranularRunListener.getTestRunNames()) {
TestRunResult run =
mMainGranularRunListener.getTestRunAtAttempt(runName, attemptNumber);
if (run != null) {
failedTestCases.addAll(run.getFailedTests());
}
}
return failedTestCases;
}
/**
* Update the arguments of {@link IRemoteTest} to only run failed tests. This arguments/logic is
* implemented differently for each IRemoteTest testtype in the overridden
* ITestFilterReceiver.addIncludeFilter method.
*
* @param test The {@link IRemoteTest} to evaluate as ITestFilterReceiver.
* @param testDescriptions The set of failed testDescriptions to retry.
*/
private void addRetriedTestsToIncludeFilters(
IRemoteTest test, Set<TestDescription> testDescriptions) {
if (test instanceof ITestFilterReceiver) {
for (TestDescription testCase : testDescriptions) {
String filter = testCase.toString();
((ITestFilterReceiver) test).addIncludeFilter(filter);
}
}
}
/** The workflow for each individual {@link IRemoteTest} run. */
private final void intraModuleRun(ITestInvocationListener runListener)
throws DeviceNotAvailableException {
try {
mTest.run(runListener);
} catch (RuntimeException re) {
CLog.e("Module '%s' - test '%s' threw exception:", mModuleId, mTest.getClass());
CLog.e(re);
CLog.e("Proceeding to the next test.");
runListener.testRunFailed(re.getMessage());
} catch (DeviceUnresponsiveException due) {
// being able to catch a DeviceUnresponsiveException here implies that recovery was
// successful, and test execution should proceed to next module.
CLog.w(
"Ignored DeviceUnresponsiveException because recovery was "
+ "successful, proceeding with next module. Stack trace:");
CLog.w(due);
CLog.w("Proceeding to the next test.");
runListener.testRunFailed(due.getMessage());
} finally {
mRetryAttemptForwarder.incrementAttempt();
}
}
/** Get the merged TestRunResults from each {@link IRemoteTest} run. */
public final List<TestRunResult> getFinalTestRunResults() {
// TODO: Once we are ready to report break-down of results and option will override this.
MergeStrategy strategy = MergeStrategy.ONE_TESTCASE_PASS_IS_PASS;
switch (mRetryStrategy) {
case ITERATIONS:
strategy = MergeStrategy.ANY_FAIL_IS_FAIL;
break;
case RERUN_UNTIL_FAILURE:
strategy = MergeStrategy.ANY_FAIL_IS_FAIL;
break;
case RETRY_ANY_FAILURE:
strategy = MergeStrategy.ANY_PASS_IS_PASS;
break;
case RETRY_TEST_CASE_FAILURE:
strategy = MergeStrategy.ONE_TESTCASE_PASS_IS_PASS;
break;
case RETRY_TEST_RUN_FAILURE:
strategy = MergeStrategy.ONE_TESTRUN_PASS_IS_PASS;
break;
}
mMainGranularRunListener.setMergeStrategy(strategy);
return mMainGranularRunListener.getMergedTestRunResults();
}
@VisibleForTesting
Map<String, List<TestRunResult>> getTestRunResultCollected() {
Map<String, List<TestRunResult>> runResultMap = new LinkedHashMap<>();
for (String runName : mMainGranularRunListener.getTestRunNames()) {
runResultMap.put(runName, mMainGranularRunListener.getTestRunAttempts(runName));
}
return runResultMap;
}
/** Check if any testRunResult has ever failed. This check is used for bug report only. */
public boolean hasFailed() {
return mMainGranularRunListener.hasFailed();
}
/**
* Calculate the number of testcases in the {@link IRemoteTest}. This value distincts the same
* testcases that are rescheduled multiple times.
*/
public final int getExpectedTestsCount() {
return mMainGranularRunListener.getExpectedTests();
}
/** Returns the elapsed time in retry attempts. */
public final long getRetryTime() {
return mRetryTime;
}
/** Returns the number of tests we managed to change status from failed to pass. */
public final long getRetrySuccess() {
return mSuccessRetried;
}
/** Returns the number of tests we couldn't change status from failed to pass. */
public final long getRetryFailed() {
return mFailedRetried;
}
/** Returns the listener containing all the results. */
public ModuleListener getResultListener() {
return mMainGranularRunListener;
}
/** Forwarder that also handles passing the current attempt we are at. */
private class RetryLogSaverResultForwarder extends LogSaverResultForwarder {
private int mAttemptNumber = 0;
public RetryLogSaverResultForwarder(
ILogSaver logSaver, List<ITestInvocationListener> listeners) {
super(logSaver, listeners);
}
@Override
public void testRunStarted(String runName, int testCount) {
testRunStarted(runName, testCount, mAttemptNumber);
}
/** Increment the attempt number. */
public void incrementAttempt() {
mAttemptNumber++;
}
}
}