blob: 1f3514add19e8e72d1cb0cb8950b0d938d84ab38 [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.ConfigurationDescriptor;
import com.android.tradefed.config.IConfiguration;
import com.android.tradefed.config.Option;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.util.testmapping.TestInfo;
import com.android.tradefed.util.testmapping.TestMapping;
import com.android.tradefed.util.testmapping.TestOption;
import com.google.common.annotations.VisibleForTesting;
import java.io.File;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Implementation of {@link BaseTestSuite} to run tests specified by option include-filter, or
* TEST_MAPPING files from build, as a suite.
*/
public class TestMappingSuiteRunner extends BaseTestSuite {
@Option(
name = "test-mapping-test-group",
description =
"Group of tests to run, e.g., presubmit, postsubmit. The suite runner "
+ "shall load the tests defined in all TEST_MAPPING files in the source "
+ "code, through build artifact test_mappings.zip."
)
private String mTestGroup = null;
@Option(
name = "test-mapping-keyword",
description =
"Keyword to be matched to the `keywords` setting of a test configured in "
+ "a TEST_MAPPING file. The test will only run if it has all the keywords "
+ "specified in the option. If option test-mapping-test-group is not set, "
+ "test-mapping-keyword option is ignored as the tests to run are not "
+ "loaded directly from TEST_MAPPING files but is supplied via the "
+ "--include-filter arg."
)
private Set<String> mKeywords = new HashSet<>();
@Option(
name = "force-test-mapping-module",
description =
"Run the specified tests only. The tests loaded from all TEST_MAPPING files in "
+ "the source code will be filtered again to force run the specified tests."
)
private Set<String> mTestModulesForced = new HashSet<>();
@Option(
name = "test-mapping-path",
description = "Run tests according to the test mapping path."
)
private List<String> mTestMappingPaths = new ArrayList<>();
@Option(
name = "use-test-mapping-path",
description = "Whether or not to run tests based on the given test mapping path."
)
private boolean mUseTestMappingPath = false;
/** Special definition in the test mapping structure. */
private static final String TEST_MAPPING_INCLUDE_FILTER = "include-filter";
private static final String TEST_MAPPING_EXCLUDE_FILTER = "exclude-filter";
/**
* Load the tests configuration that will be run. Each tests is defined by a {@link
* IConfiguration} and a unique name under which it will report results. There are 2 ways to
* load tests for {@link TestMappingSuiteRunner}:
*
* <p>1. --test-mapping-test-group, which specifies the group of tests in TEST_MAPPING files.
* The runner will parse all TEST_MAPPING files in the source code through build artifact
* test_mappings.zip, and load tests grouped under the given test group.
*
* <p>2. --include-filter, which specifies the name of the test to run. The use case is for
* presubmit check to only run a list of tests related to the Cls to be verifies. The list of
* tests are compiled from the related TEST_MAPPING files in modified source code.
*
* @return a map of test name to the {@link IConfiguration} object of each test.
*/
@Override
public LinkedHashMap<String, IConfiguration> loadTests() {
Set<String> includeFilter = getIncludeFilter();
// Name of the tests
Set<String> testNames = new HashSet<>();
Set<TestInfo> testInfosToRun = new HashSet<>();
if (mTestGroup == null && includeFilter.isEmpty()) {
throw new RuntimeException(
"At least one of the options, --test-mapping-test-group or --include-filter, "
+ "should be set.");
}
if (mTestGroup == null && !mKeywords.isEmpty()) {
throw new RuntimeException(
"Must specify --test-mapping-test-group when applying --test-mapping-keyword.");
}
if (mTestGroup == null && !mTestModulesForced.isEmpty()) {
throw new RuntimeException(
"Must specify --test-mapping-test-group when applying "
+ "--force-test-mapping-module.");
}
if (mTestGroup != null && !includeFilter.isEmpty()) {
throw new RuntimeException(
"If options --test-mapping-test-group is set, option --include-filter should "
+ "not be set.");
}
if (!includeFilter.isEmpty() && !mTestMappingPaths.isEmpty()) {
throw new RuntimeException(
"If option --include-filter is set, option --test-mapping-path should "
+ "not be set.");
}
if (mTestGroup != null) {
if (!mTestMappingPaths.isEmpty()) {
TestMapping.setTestMappingPaths(mTestMappingPaths);
}
testInfosToRun =
TestMapping.getTests(
getBuildInfo(), mTestGroup, getPrioritizeHostConfig(), mKeywords);
if (!mTestModulesForced.isEmpty()) {
CLog.i("Filtering tests for the given names: %s", mTestModulesForced);
testInfosToRun =
testInfosToRun
.stream()
.filter(testInfo -> mTestModulesForced.contains(testInfo.getName()))
.collect(Collectors.toSet());
}
if (testInfosToRun.isEmpty()) {
throw new RuntimeException(
String.format("No test found for the given group: %s.", mTestGroup));
}
for (TestInfo testInfo : testInfosToRun) {
testNames.add(testInfo.getName());
}
setIncludeFilter(testNames);
}
// load all the configurations with include-filter injected.
LinkedHashMap<String, IConfiguration> testConfigs = super.loadTests();
// Create and inject individual tests by calling super.loadTests() with each test info.
for (Map.Entry<String, IConfiguration> entry : testConfigs.entrySet()) {
List<IRemoteTest> allTests = new ArrayList<>();
IConfiguration moduleConfig = entry.getValue();
ConfigurationDescriptor configDescriptor =
moduleConfig.getConfigurationDescription();
String moduleName = configDescriptor.getModuleName();
String configPath = moduleConfig.getName();
Set<TestInfo> testInfos = getTestInfos(testInfosToRun, moduleName);
allTests.addAll(
createIndividualTests(
testInfos, configPath));
if (!allTests.isEmpty()) {
// Set back to IConfiguration only if IRemoteTests are created.
moduleConfig.setTests(allTests);
// Set test sources to ConfigurationDescriptor.
List<String> testSources = getTestSources(testInfos);
configDescriptor.addMetadata(TestMapping.TEST_SOURCES, testSources);
}
}
return testConfigs;
}
/**
* Create individual tests with test infos for a module.
*
* @param testInfos A {@code Set<TestInfo>} containing multiple test options.
* @param configPath A {@code String} of configuration path.
* @return The {@link List} that are injected with the test options.
*/
@VisibleForTesting
List<IRemoteTest> createIndividualTests(Set<TestInfo> testInfos, String configPath) {
List<IRemoteTest> tests = new ArrayList<>();
if (configPath == null) {
throw new RuntimeException(String.format("Configuration path is null."));
}
File configFie = new File(configPath);
if (!configFie.exists()) {
throw new RuntimeException(
String.format("Configuration path: %s doesn't exits.", configPath));
}
// De-duplicate test infos so that there won't be duplicate test options.
testInfos = dedupTestInfos(testInfos);
for (TestInfo testInfo : testInfos) {
// Clean up all the test options injected in SuiteModuleLoader.
super.cleanUpSuiteSetup();
super.clearModuleArgs();
clearConfigPaths();
// Set config path to BaseTestSuite to limit the search.
addConfigPaths(configFie);
// Inject the test options from each test info to SuiteModuleLoader.
parseOptions(testInfo);
LinkedHashMap<String, IConfiguration> config = super.loadTests();
for (Map.Entry<String, IConfiguration> entry : config.entrySet()) {
tests.addAll(entry.getValue().getTests());
}
}
return tests;
}
/**
* Get a list of path of TEST_MAPPING for a module.
*
* @param testInfos A {@code Set<TestInfo>} containing multiple test options.
* @return A {@code List<String>} of TEST_MAPPING path.
*/
@VisibleForTesting
List<String> getTestSources(Set<TestInfo> testInfos) {
List<String> testSources = new ArrayList<>();
for (TestInfo testInfo : testInfos) {
testSources.addAll(testInfo.getSources());
}
return testSources;
}
/**
* Parse the test options for the test info.
*
* @param testInfo A {@code Set<TestInfo>} containing multiple test options.
*/
@VisibleForTesting
void parseOptions(TestInfo testInfo) {
Set<String> mappingIncludeFilters = new HashSet<>();
Set<String> mappingExcludeFilters = new HashSet<>();
// module-arg options compiled from test options for each test.
Set<String> moduleArgs = new HashSet<>();
Set<String> testNames = new HashSet<>();
for (TestOption option : testInfo.getOptions()) {
switch (option.getName()) {
// Handle include and exclude filter at the suite level to hide each
// test runner specific implementation and option names related to filtering
case TEST_MAPPING_INCLUDE_FILTER:
mappingIncludeFilters.add(
String.format("%s %s", testInfo.getName(), option.getValue()));
break;
case TEST_MAPPING_EXCLUDE_FILTER:
mappingExcludeFilters.add(
String.format("%s %s", testInfo.getName(), option.getValue()));
break;
default:
String moduleArg =
String.format("%s:%s", testInfo.getName(), option.getName());
if (option.getValue() != null && !option.getValue().isEmpty()) {
moduleArg = String.format("%s:%s", moduleArg, option.getValue());
}
moduleArgs.add(moduleArg);
break;
}
}
if (mappingIncludeFilters.isEmpty()) {
testNames.add(testInfo.getName());
setIncludeFilter(testNames);
} else {
setIncludeFilter(mappingIncludeFilters);
}
if (!mappingExcludeFilters.isEmpty()) {
setExcludeFilter(mappingExcludeFilters);
}
addModuleArgs(moduleArgs);
}
/**
* De-duplicate test infos with the same test options.
*
* @param testInfos A {@code Set<TestInfo>} containing multiple test options.
* @return A {@code Set<TestInfo>} of tests without duplicated test options.
*/
@VisibleForTesting
Set<TestInfo> dedupTestInfos(Set<TestInfo> testInfos) {
Set<String> nameOptions = new HashSet<>();
Set<TestInfo> dedupTestInfos = new HashSet<>();
for (TestInfo testInfo : testInfos) {
String nameOption = testInfo.getName() + testInfo.getOptions().toString();
if (!nameOptions.contains(nameOption)) {
dedupTestInfos.add(testInfo);
nameOptions.add(nameOption);
}
}
return dedupTestInfos;
}
/**
* Get the test infos for the given module name.
*
* @param testInfos A {@code Set<TestInfo>} containing multiple test options.
* @param moduleName A {@code String} name of a test module.
* @return A {@code Set<TestInfo>} of tests for a module.
*/
@VisibleForTesting
Set<TestInfo> getTestInfos(Set<TestInfo> testInfos, String moduleName) {
return testInfos
.stream()
.filter(testInfo -> moduleName.equals(testInfo.getName()))
.collect(Collectors.toSet());
}
}