blob: d7e2ae305d4e5d5abbb10f0fc7fbd2290f27f1da [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.retry;
import static org.junit.Assert.assertNull;
import com.android.annotations.VisibleForTesting;
import com.android.tradefed.config.ConfigurationException;
import com.android.tradefed.config.ConfigurationFactory;
import com.android.tradefed.config.IConfiguration;
import com.android.tradefed.config.IConfigurationFactory;
import com.android.tradefed.config.IConfigurationReceiver;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.Option.Importance;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.IDeviceSelection;
import com.android.tradefed.invoker.TestInformation;
import com.android.tradefed.log.FileLogger;
import com.android.tradefed.log.ILeveledLogOutput;
import com.android.tradefed.result.CollectingTestListener;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.TestDescription;
import com.android.tradefed.result.TestResult;
import com.android.tradefed.result.TestRunResult;
import com.android.tradefed.result.TextResultReporter;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.testtype.suite.BaseTestSuite;
import com.android.tradefed.testtype.suite.SuiteTestFilter;
import com.android.tradefed.util.AbiUtils;
import com.android.tradefed.util.QuotationAwareTokenizer;
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.Map.Entry;
import java.util.Set;
/**
* A special runner that allows to reschedule a previous run tests that failed or where not
* executed.
*/
public final class RetryRescheduler implements IRemoteTest, IConfigurationReceiver {
/** The types of the tests that can be retried. */
public enum RetryType {
FAILED,
NOT_EXECUTED,
}
@Option(
name = "retry-type",
description =
"used to retry tests of a certain status. Possible values include \"failed\" "
+ "and \"not_executed\".")
private RetryType mRetryType = null;
@Option(
name = "new-parameterized-handling",
description =
"Feature flag to test out the newer parameterized method handling for retry.")
private boolean mParameterizedHandling = true;
@Option(
name = BaseTestSuite.MODULE_OPTION,
shortName = BaseTestSuite.MODULE_OPTION_SHORT_NAME,
description = "the test module to run. Only works for configuration in the tests dir."
)
private String mModuleName = null;
/**
* It's possible to add extra exclusion from the rerun. But these tests will not change their
* state.
*/
@Option(
name = BaseTestSuite.EXCLUDE_FILTER_OPTION,
description = "the exclude module filters to apply.",
importance = Importance.ALWAYS
)
private Set<String> mExcludeFilters = new HashSet<>();
public static final String PREVIOUS_LOADER_NAME = "previous_loader";
private IConfiguration mConfiguration;
private IConfigurationFactory mFactory;
private IConfiguration mRescheduledConfiguration;
@Override
public void run(
TestInformation testInfo /* do not use - should be null */,
ITestInvocationListener listener /* do not use - should be null */)
throws DeviceNotAvailableException {
assertNull(testInfo);
assertNull(listener);
// Get the re-loader for previous results
Object loader = mConfiguration.getConfigurationObject(PREVIOUS_LOADER_NAME);
if (loader == null) {
throw new RuntimeException(
String.format(
"An <object> of type %s was expected in the retry.",
PREVIOUS_LOADER_NAME));
}
if (!(loader instanceof ITestSuiteResultLoader)) {
throw new RuntimeException(
String.format(
"%s should be implementing %s",
loader.getClass().getCanonicalName(),
ITestSuiteResultLoader.class.getCanonicalName()));
}
ITestSuiteResultLoader previousLoader = (ITestSuiteResultLoader) loader;
// First init the reloader.
previousLoader.init();
// Then get the command line of the previous run
String commandLine = previousLoader.getCommandLine();
IConfiguration originalConfig;
try {
originalConfig =
getFactory()
.createConfigurationFromArgs(
QuotationAwareTokenizer.tokenizeLine(commandLine));
// Transfer the sharding options from the original command.
originalConfig
.getCommandOptions()
.setShardCount(mConfiguration.getCommandOptions().getShardCount());
originalConfig
.getCommandOptions()
.setShardIndex(mConfiguration.getCommandOptions().getShardIndex());
IDeviceSelection requirements = mConfiguration.getDeviceRequirements();
// It should be safe to use the current requirements against the old config because
// There will be more checks like fingerprint if it was supposed to run.
originalConfig.setDeviceRequirements(requirements);
// Transfer log level from retry to subconfig
ILeveledLogOutput originalLogger = originalConfig.getLogOutput();
ILeveledLogOutput retryLogger = mConfiguration.getLogOutput();
originalLogger.setLogLevel(retryLogger.getLogLevel());
if (originalLogger instanceof FileLogger && retryLogger instanceof FileLogger) {
((FileLogger) originalLogger)
.setLogLevelDisplay(((FileLogger) retryLogger).getLogLevelDisplay());
}
handleExtraResultReporter(originalConfig, mConfiguration);
} catch (ConfigurationException e) {
throw new RuntimeException(e);
}
// Get previous results
CollectingTestListener collectedTests = previousLoader.loadPreviousResults();
previousLoader.cleanUp();
// Appropriately update the configuration
IRemoteTest test = originalConfig.getTests().get(0);
if (!(test instanceof BaseTestSuite)) {
throw new RuntimeException(
"RetryScheduler only works for BaseTestSuite implementations");
}
BaseTestSuite suite = (BaseTestSuite) test;
ResultsPlayer replayer = new ResultsPlayer();
updateRunner(suite, collectedTests, replayer);
collectedTests = null;
updateConfiguration(originalConfig, replayer);
// Do the customization of the configuration for specialized use cases.
customizeConfig(previousLoader, originalConfig);
mRescheduledConfiguration = originalConfig;
}
@Override
public void setConfiguration(IConfiguration configuration) {
mConfiguration = configuration;
}
private IConfigurationFactory getFactory() {
if (mFactory != null) {
return mFactory;
}
return ConfigurationFactory.getInstance();
}
@VisibleForTesting
void setConfigurationFactory(IConfigurationFactory factory) {
mFactory = factory;
}
/** Returns the {@link IConfiguration} that should be retried. */
public final IConfiguration getRetryConfiguration() {
return mRescheduledConfiguration;
}
/**
* Update the configuration to be ready for re-run.
*
* @param suite The {@link BaseTestSuite} that will be re-run.
* @param results The results of the previous run.
* @param replayer The {@link ResultsPlayer} that will replay the non-retried use cases.
*/
private void updateRunner(
BaseTestSuite suite, CollectingTestListener results, ResultsPlayer replayer) {
List<RetryType> types = new ArrayList<>();
if (mRetryType == null) {
types.add(RetryType.FAILED);
types.add(RetryType.NOT_EXECUTED);
} else {
types.add(mRetryType);
}
// Expand the --module option in case no abi is specified.
Set<String> expandedModuleOption = new HashSet<>();
if (mModuleName != null) {
SuiteTestFilter moduleFilter = SuiteTestFilter.createFrom(mModuleName);
expandedModuleOption.add(mModuleName);
if (moduleFilter.getAbi() == null) {
Set<String> abis = AbiUtils.getAbisSupportedByCompatibility();
for (String abi : abis) {
SuiteTestFilter namingFilter =
new SuiteTestFilter(
abi, moduleFilter.getName(), moduleFilter.getTest());
expandedModuleOption.add(namingFilter.toString());
}
}
}
// Expand the exclude-filter in case no abi is specified.
Set<String> extendedExcludeRetryFilters = new HashSet<>();
for (String excludeFilter : mExcludeFilters) {
SuiteTestFilter suiteFilter = SuiteTestFilter.createFrom(excludeFilter);
// Keep the current exclude-filter
extendedExcludeRetryFilters.add(excludeFilter);
if (suiteFilter.getAbi() == null) {
// If no abi is specified, exclude them all.
Set<String> abis = AbiUtils.getAbisSupportedByCompatibility();
for (String abi : abis) {
SuiteTestFilter namingFilter =
new SuiteTestFilter(abi, suiteFilter.getName(), suiteFilter.getTest());
extendedExcludeRetryFilters.add(namingFilter.toString());
}
}
}
// Prepare exclusion filters
for (TestRunResult moduleResult : results.getMergedTestRunResults()) {
// If the module is explicitly excluded from retries, preserve the original results.
if (!extendedExcludeRetryFilters.contains(moduleResult.getName())
&& (expandedModuleOption.isEmpty()
|| expandedModuleOption.contains(moduleResult.getName()))
&& RetryResultHelper.shouldRunModule(moduleResult, types)) {
if (types.contains(RetryType.NOT_EXECUTED)) {
// Clear the run failure since we are attempting to rerun all non-executed
moduleResult.resetRunFailure();
}
Map<TestDescription, TestResult> parameterizedMethods = new LinkedHashMap<>();
for (Entry<TestDescription, TestResult> result :
moduleResult.getTestResults().entrySet()) {
if (!mParameterizedHandling) {
// Put aside all parameterized methods
if (isParameterized(result.getKey())) {
parameterizedMethods.put(result.getKey(), result.getValue());
continue;
}
}
if (!RetryResultHelper.shouldRunTest(result.getValue(), types)) {
addExcludeToConfig(suite, moduleResult, result.getKey().toString());
replayer.addToReplay(
results.getModuleContextForRunResult(moduleResult.getName()),
moduleResult,
result);
}
}
if (!mParameterizedHandling) {
// Handle parameterized methods
for (Entry<String, Map<TestDescription, TestResult>> subMap :
sortMethodToClass(parameterizedMethods).entrySet()) {
boolean shouldNotrerunAnything =
subMap.getValue().entrySet().stream()
.noneMatch(
(v) ->
RetryResultHelper.shouldRunTest(
v.getValue(), types)
== true);
// If None of the base method need to be rerun exclude it
if (shouldNotrerunAnything) {
// Exclude the base method
addExcludeToConfig(suite, moduleResult, subMap.getKey());
// Replay all test cases
for (Entry<TestDescription, TestResult> result :
subMap.getValue().entrySet()) {
replayer.addToReplay(
results.getModuleContextForRunResult(
moduleResult.getName()),
moduleResult,
result);
}
}
}
}
} else {
// Exclude the module completely - it will keep its current status
addExcludeToConfig(suite, moduleResult, null);
replayer.addToReplay(
results.getModuleContextForRunResult(moduleResult.getName()),
moduleResult,
null);
}
}
}
/** Update the configuration to put the replayer before all the actual real tests. */
private void updateConfiguration(IConfiguration config, ResultsPlayer replayer) {
List<IRemoteTest> tests = config.getTests();
List<IRemoteTest> newList = new ArrayList<>();
// Add the replayer first to replay all the tests cases first.
newList.add(replayer);
newList.addAll(tests);
config.setTests(newList);
}
/** Allow the specialized loader to customize the config before re-running it. */
private void customizeConfig(ITestSuiteResultLoader loader, IConfiguration originalConfig) {
loader.customizeConfiguration(originalConfig);
}
/** Add the filter to the suite. */
private void addExcludeToConfig(
BaseTestSuite suite, TestRunResult moduleResult, String testDescription) {
String filter = moduleResult.getName();
if (testDescription != null) {
filter = String.format("%s %s", filter, testDescription);
}
SuiteTestFilter testFilter = SuiteTestFilter.createFrom(filter);
Set<String> excludeFilter = new LinkedHashSet<>();
excludeFilter.add(testFilter.toString());
suite.setExcludeFilter(excludeFilter);
}
/** Returns True if a test case is a parameterized one. */
private boolean isParameterized(TestDescription description) {
return !description.getTestName().equals(description.getTestNameWithoutParams());
}
private Map<String, Map<TestDescription, TestResult>> sortMethodToClass(
Map<TestDescription, TestResult> paramMethods) {
Map<String, Map<TestDescription, TestResult>> returnMap = new LinkedHashMap<>();
for (Entry<TestDescription, TestResult> entry : paramMethods.entrySet()) {
String noParamName =
String.format(
"%s#%s",
entry.getKey().getClassName(),
entry.getKey().getTestNameWithoutParams());
Map<TestDescription, TestResult> forClass = returnMap.get(noParamName);
if (forClass == null) {
forClass = new LinkedHashMap<>();
returnMap.put(noParamName, forClass);
}
forClass.put(entry.getKey(), entry.getValue());
}
return returnMap;
}
/**
* Fetch additional result_reporter from the retry configuration and add them to the original
* command. This is the only allowed modification of the original command: add more result
* end-points.
*/
private void handleExtraResultReporter(
IConfiguration originalConfig, IConfiguration retryConfig) {
// Since we always have 1 default reporter, avoid carrying it for no reason. Only carry
// reporters if some actual ones were specified.
if (retryConfig.getTestInvocationListeners().size() == 1
&& (mConfiguration.getTestInvocationListeners().get(0)
instanceof TextResultReporter)) {
return;
}
List<ITestInvocationListener> listeners = originalConfig.getTestInvocationListeners();
listeners.addAll(retryConfig.getTestInvocationListeners());
originalConfig.setTestInvocationListeners(listeners);
}
}