blob: 5b6606cf443c2aa34e92fd22ed4217f4f4b7592e [file] [log] [blame]
/*
* Copyright (C) 2015 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.compatibility.common.tradefed.testtype;
import com.android.compatibility.common.util.AbiUtils;
import com.android.compatibility.common.util.TestFilter;
import com.android.ddmlib.Log.LogLevel;
import com.android.tradefed.build.IBuildInfo;
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.log.LogUtil.CLog;
import com.android.tradefed.testtype.IAbi;
import com.android.tradefed.testtype.IBuildReceiver;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.testtype.IShardableTest;
import com.android.tradefed.util.TimeUtil;
import java.io.File;
import java.io.FilenameFilter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* Retrieves Compatibility test module definitions from the repository.
*/
public class ModuleRepo implements IModuleRepo {
private static final String CONFIG_EXT = ".config";
private static final long SMALL_TEST = TimeUnit.MINUTES.toMillis(2); // Small tests < 2mins
private static final long MEDIUM_TEST = TimeUnit.MINUTES.toMillis(10); // Medium tests < 10mins
static IModuleRepo sInstance;
private int mShards;
private int mModulesPerShard;
private int mSmallModulesPerShard;
private int mMediumModulesPerShard;
private int mLargeModulesPerShard;
private int mModuleCount = 0;
private Set<String> mSerials = new HashSet<>();
private Map<String, Set<String>> mDeviceTokens = new HashMap<>();
private Map<String, Map<String, String>> mTestArgs = new HashMap<>();
private Map<String, Map<String, String>> mModuleArgs = new HashMap<>();
private boolean mIncludeAll;
private Map<String, List<TestFilter>> mIncludeFilters = new HashMap<>();
private Map<String, List<TestFilter>> mExcludeFilters = new HashMap<>();
private IConfigurationFactory mConfigFactory = ConfigurationFactory.getInstance();
private volatile boolean mInitialized = false;
// Holds all the small tests waiting to be run.
private List<IModuleDef> mSmallModules = new ArrayList<>();
// Holds all the medium tests waiting to be run.
private List<IModuleDef> mMediumModules = new ArrayList<>();
// Holds all the large tests waiting to be run.
private List<IModuleDef> mLargeModules = new ArrayList<>();
// Holds all the tests with tokens waiting to be run. Meaning the DUT must have a specific token.
private List<IModuleDef> mTokenModules = new ArrayList<>();
public static IModuleRepo getInstance() {
if (sInstance == null) {
sInstance = new ModuleRepo();
}
return sInstance;
}
public static void tearDown() {
sInstance = null;
}
/**
* {@inheritDoc}
*/
@Override
public int getNumberOfShards() {
return mShards;
}
/**
* {@inheritDoc}
*/
@Override
public int getModulesPerShard() {
return mModulesPerShard;
}
/**
* {@inheritDoc}
*/
@Override
public Map<String, Set<String>> getDeviceTokens() {
return mDeviceTokens;
}
/**
* A {@link FilenameFilter} to find all modules in a directory who match the given pattern.
*/
public static class NameFilter implements FilenameFilter {
private String mPattern;
public NameFilter(String pattern) {
mPattern = pattern;
}
/**
* {@inheritDoc}
*/
@Override
public boolean accept(File dir, String name) {
return name.contains(mPattern) && name.endsWith(CONFIG_EXT);
}
}
/**
* {@inheritDoc}
*/
@Override
public Set<String> getSerials() {
return mSerials;
}
/**
* {@inheritDoc}
*/
@Override
public List<IModuleDef> getSmallModules() {
return mSmallModules;
}
/**
* {@inheritDoc}
*/
@Override
public List<IModuleDef> getMediumModules() {
return mMediumModules;
}
/**
* {@inheritDoc}
*/
@Override
public List<IModuleDef> getLargeModules() {
return mLargeModules;
}
/**
* {@inheritDoc}
*/
@Override
public List<IModuleDef> getTokenModules() {
return mTokenModules;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isInitialized() {
return mInitialized;
}
/**
* {@inheritDoc}
*/
@Override
public void initialize(int shards, File testsDir, Set<IAbi> abis, List<String> deviceTokens,
List<String> testArgs, List<String> moduleArgs, List<String> includeFilters,
List<String> excludeFilters, IBuildInfo buildInfo) {
mInitialized = true;
mShards = shards;
for (String line : deviceTokens) {
String[] parts = line.split(":");
if (parts.length == 2) {
String key = parts[0];
String value = parts[1];
Set<String> list = mDeviceTokens.get(key);
if (list == null) {
list = new HashSet<>();
mDeviceTokens.put(key, list);
}
list.add(value);
} else {
throw new IllegalArgumentException(
String.format("Could not parse device token: %s", line));
}
}
putArgs(testArgs, mTestArgs);
putArgs(moduleArgs, mModuleArgs);
mIncludeAll = includeFilters.isEmpty();
// Include all the inclusions
addFilters(includeFilters, mIncludeFilters, abis);
// Exclude all the exclusions
addFilters(excludeFilters, mExcludeFilters, abis);
File[] configFiles = testsDir.listFiles(new ConfigFilter());
for (File configFile : configFiles) {
final String name = configFile.getName().replace(CONFIG_EXT, "");
final String[] pathArg = new String[] { configFile.getAbsolutePath() };
try {
// Invokes parser to process the test module config file
// Need to generate a different config for each ABI as we cannot guarantee the
// configs are idempotent. This however means we parse the same file multiple times
for (IAbi abi : abis) {
IConfiguration config = mConfigFactory.createConfigurationFromArgs(pathArg);
String id = AbiUtils.createId(abi.getName(), name);
{
Map<String, String> args = new HashMap<>();
if (mModuleArgs.containsKey(name)) {
args.putAll(mModuleArgs.get(name));
}
if (mModuleArgs.containsKey(id)) {
args.putAll(mModuleArgs.get(id));
}
if (args != null && args.size() > 0) {
for (Entry<String, String> entry : args.entrySet()) {
config.injectOptionValue(entry.getKey(), entry.getValue());
}
}
}
List<IRemoteTest> tests = config.getTests();
for (IRemoteTest test : tests) {
String className = test.getClass().getName();
Map<String, String> args = new HashMap<>();
if (mTestArgs.containsKey(className)) {
args.putAll(mTestArgs.get(className));
}
if (args != null && args.size() > 0) {
for (Entry<String, String> entry : args.entrySet()) {
config.injectOptionValue(entry.getKey(), entry.getValue());
}
}
}
List<IRemoteTest> shardedTests = tests;
if (mShards > 1) {
shardedTests = splitShardableTests(tests, buildInfo);
}
for (IRemoteTest test : shardedTests) {
if (test instanceof IBuildReceiver) {
((IBuildReceiver)test).setBuild(buildInfo);
}
addModuleDef(name, abi, test, pathArg);
}
}
} catch (ConfigurationException e) {
throw new RuntimeException(String.format("error parsing config file: %s",
configFile.getName()), e);
}
}
mModulesPerShard = mModuleCount / shards;
if (mModuleCount % shards != 0) {
mModulesPerShard++; // Round up
}
mSmallModulesPerShard = mSmallModules.size() / shards;
mMediumModulesPerShard = mMediumModules.size() / shards;
mLargeModulesPerShard = mLargeModules.size() / shards;
}
private static List<IRemoteTest> splitShardableTests(List<IRemoteTest> tests,
IBuildInfo buildInfo) {
ArrayList<IRemoteTest> shardedList = new ArrayList<>(tests.size());
for (IRemoteTest test : tests) {
if (test instanceof IShardableTest) {
if (test instanceof IBuildReceiver) {
((IBuildReceiver)test).setBuild(buildInfo);
}
shardedList.addAll(((IShardableTest)test).split());
} else {
shardedList.add(test);
}
}
return shardedList;
}
private static void addFilters(List<String> stringFilters,
Map<String, List<TestFilter>> filters, Set<IAbi> abis) {
for (String filterString : stringFilters) {
TestFilter filter = TestFilter.createFrom(filterString);
String abi = filter.getAbi();
if (abi == null) {
for (IAbi a : abis) {
addFilter(a.getName(), filter, filters);
}
} else {
addFilter(abi, filter, filters);
}
}
}
private static void addFilter(String abi, TestFilter filter,
Map<String, List<TestFilter>> filters) {
getFilter(filters, AbiUtils.createId(abi, filter.getName())).add(filter);
}
private static List<TestFilter> getFilter(Map<String, List<TestFilter>> filters, String id) {
List<TestFilter> fs = filters.get(id);
if (fs == null) {
fs = new ArrayList<>();
filters.put(id, fs);
}
return fs;
}
private void addModuleDef(String name, IAbi abi, IRemoteTest test,
String[] configPaths) throws ConfigurationException {
// Invokes parser to process the test module config file
IConfiguration config = mConfigFactory.createConfigurationFromArgs(configPaths);
addModuleDef(new ModuleDef(name, abi, test, config.getTargetPreparers()));
}
private void addModuleDef(IModuleDef moduleDef) {
String id = moduleDef.getId();
boolean includeModule = mIncludeAll;
for (TestFilter include : getFilter(mIncludeFilters, id)) {
String test = include.getTest();
if (test != null) {
// We're including a subset of tests
moduleDef.addIncludeFilter(test);
}
includeModule = true;
}
for (TestFilter exclude : getFilter(mExcludeFilters, id)) {
String test = exclude.getTest();
if (test != null) {
// Excluding a subset of tests, so keep module but give filter
moduleDef.addExcludeFilter(test);
} else {
// Excluding all tests in the module so just remove the whole thing
includeModule = false;
}
}
if (includeModule) {
Set<String> tokens = moduleDef.getTokens();
if (tokens != null && !tokens.isEmpty()) {
mTokenModules.add(moduleDef);
} else if (moduleDef.getRuntimeHint() < SMALL_TEST) {
mSmallModules.add(moduleDef);
} else if (moduleDef.getRuntimeHint() < MEDIUM_TEST) {
mMediumModules.add(moduleDef);
} else {
mLargeModules.add(moduleDef);
}
mModuleCount++;
}
}
/**
* A {@link FilenameFilter} to find all the config files in a directory.
*/
public static class ConfigFilter implements FilenameFilter {
/**
* {@inheritDoc}
*/
@Override
public boolean accept(File dir, String name) {
return name.endsWith(CONFIG_EXT);
}
}
/**
* {@inheritDoc}
*/
@Override
public synchronized List<IModuleDef> getModules(String serial) {
Set<String> tokens = mDeviceTokens.get(serial);
List<IModuleDef> modules = new ArrayList<>(mModulesPerShard);
getModulesWithTokens(tokens, modules);
getModules(modules);
long estimatedTime = 0;
for (IModuleDef def : modules) {
estimatedTime += def.getRuntimeHint();
}
mSerials.add(serial);
if (mSerials.size() == mShards) {
// All shards have been given their workload.
if (!mTokenModules.isEmpty()) {
Set<String> deviceTokens = mDeviceTokens.keySet();
Set<String> moduleTokens = new HashSet<>();
for (IModuleDef module : mTokenModules) {
moduleTokens.addAll(module.getTokens());
}
StringBuilder sb = new StringBuilder("Not all modules could be scheduled.");
for (String token : moduleTokens) {
if (!deviceTokens.contains(token)) {
sb.append("\nNo devices found with token: ");
sb.append(token);
}
}
sb.append("\nDeclare device tokens with \"--device-token <serial>:<token>\"");
throw new IllegalArgumentException(sb.toString());
}
if (!mLargeModules.isEmpty()) {
throw new IllegalArgumentException("Couldn't schedule: " + mLargeModules);
}
if (!mMediumModules.isEmpty()) {
throw new IllegalArgumentException("Couldn't schedule: " + mMediumModules);
}
if (!mSmallModules.isEmpty()) {
throw new IllegalArgumentException("Couldn't schedule: " + mSmallModules);
}
}
Collections.sort(modules, new RuntimeHintComparator());
CLog.logAndDisplay(LogLevel.INFO, String.format(
"%s running %s modules, expected to complete in %s",
serial, modules.size(), TimeUtil.formatElapsedTime(estimatedTime)));
return modules;
}
/**
* Iterates through the remaining tests that require tokens and if the device has all the
* required tokens it will queue that module to run on that device, else the module gets put
* back into the list.
*/
private void getModulesWithTokens(Set<String> tokens, List<IModuleDef> modules) {
if (tokens != null) {
List<IModuleDef> copy = mTokenModules;
mTokenModules = new ArrayList<>();
for (IModuleDef module : copy) {
// If a device has all the tokens required by the module then it can run it.
if (tokens.containsAll(module.getTokens())) {
modules.add(module);
} else {
mTokenModules.add(module);
}
}
}
}
/**
* Adds count modules that do not require tokens, to run on a device.
*/
private void getModules(List<IModuleDef> modules) {
// Take the normal share of modules unless the device already has token modules.
takeModule(mSmallModules, modules, mSmallModulesPerShard - modules.size());
takeModule(mMediumModules, modules, mMediumModulesPerShard);
takeModule(mLargeModules, modules, mLargeModulesPerShard);
// If one bucket runs out, take from any of the others.
boolean success = true;
while (success && modules.size() < mModulesPerShard) {
// Take modules from the buckets until it has enough, or there are no more modules.
success = takeModule(mSmallModules, modules, 1)
|| takeModule(mMediumModules, modules, 1)
|| takeModule(mLargeModules, modules, 1);
}
}
/**
* Takes count modules from the first list and move it to the second.
*/
private static boolean takeModule(
List<IModuleDef> source, List<IModuleDef> destination, int count) {
if (source.isEmpty()) {
return false;
}
if (count > source.size()) {
count = source.size();
}
for (int i = 0; i < count; i++) {
destination.add(source.remove(source.size() - 1));// Take from the end of the arraylist.
}
return true;
}
/**
* @return the {@link List} of modules whose name contains the given pattern.
*/
public static List<String> getModuleNamesMatching(File directory, String pattern) {
String[] names = directory.list(new NameFilter(pattern));
List<String> modules = new ArrayList<String>(names.length);
for (String name : names) {
int index = name.indexOf(CONFIG_EXT);
if (index > 0) {
modules.add(name.substring(0, index));
}
}
return modules;
}
private static void putArgs(List<String> args, Map<String, Map<String, String>> argsMap) {
for (String arg : args) {
String[] parts = arg.split(":");
String target = parts[0];
String key = parts[1];
String value = parts[2];
Map<String, String> map = argsMap.get(target);
if (map == null) {
map = new HashMap<>();
argsMap.put(target, map);
}
map.put(key, value);
}
}
private static class RuntimeHintComparator implements Comparator<IModuleDef> {
@Override
public int compare(IModuleDef def1, IModuleDef def2) {
return (int) Math.signum(def2.getRuntimeHint() - def1.getRuntimeHint());
}
}
}