1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.tradefed.testtype.suite; 17 18 import com.android.tradefed.config.ConfigurationDef.OptionDef; 19 import com.android.tradefed.config.ConfigurationException; 20 import com.android.tradefed.config.ConfigurationFactory; 21 import com.android.tradefed.config.ConfigurationUtil; 22 import com.android.tradefed.config.IConfiguration; 23 import com.android.tradefed.config.IConfigurationFactory; 24 import com.android.tradefed.log.LogUtil.CLog; 25 import com.android.tradefed.targetprep.ITargetPreparer; 26 import com.android.tradefed.testtype.IAbi; 27 import com.android.tradefed.testtype.IAbiReceiver; 28 import com.android.tradefed.testtype.IRemoteTest; 29 import com.android.tradefed.testtype.ITestFileFilterReceiver; 30 import com.android.tradefed.testtype.ITestFilterReceiver; 31 import com.android.tradefed.util.AbiUtils; 32 import com.android.tradefed.util.FileUtil; 33 import com.android.tradefed.util.StreamUtil; 34 35 import com.google.common.base.Strings; 36 37 import java.io.File; 38 import java.io.FilenameFilter; 39 import java.io.IOException; 40 import java.io.PrintWriter; 41 import java.util.ArrayList; 42 import java.util.Arrays; 43 import java.util.Collection; 44 import java.util.Collections; 45 import java.util.HashMap; 46 import java.util.LinkedHashMap; 47 import java.util.List; 48 import java.util.Map; 49 import java.util.Set; 50 51 /** 52 * Retrieves Compatibility test module definitions from the repository. TODO: Add the expansion of 53 * suite when loading a module. 54 */ 55 public class SuiteModuleLoader { 56 57 public static final String CONFIG_EXT = ".config"; 58 private Map<String, List<OptionDef>> mTestOptions = new HashMap<>(); 59 private Map<String, List<OptionDef>> mModuleOptions = new HashMap<>(); 60 private boolean mIncludeAll; 61 private Map<String, List<SuiteTestFilter>> mIncludeFilters = new HashMap<>(); 62 private Map<String, List<SuiteTestFilter>> mExcludeFilters = new HashMap<>(); 63 private IConfigurationFactory mConfigFactory = ConfigurationFactory.getInstance(); 64 65 /** 66 * Ctor for the SuiteModuleLoader. 67 * 68 * @param includeFilters The formatted and parsed include filters. 69 * @param excludeFilters The formatted and parsed exclude filters. 70 * @param testArgs the list of test ({@link IRemoteTest}) arguments. 71 * @param moduleArgs the list of module arguments. 72 */ SuiteModuleLoader( Map<String, List<SuiteTestFilter>> includeFilters, Map<String, List<SuiteTestFilter>> excludeFilters, List<String> testArgs, List<String> moduleArgs)73 public SuiteModuleLoader( 74 Map<String, List<SuiteTestFilter>> includeFilters, 75 Map<String, List<SuiteTestFilter>> excludeFilters, 76 List<String> testArgs, 77 List<String> moduleArgs) { 78 mIncludeAll = includeFilters.isEmpty(); 79 mIncludeFilters = includeFilters; 80 mExcludeFilters = excludeFilters; 81 82 parseArgs(testArgs, mTestOptions); 83 parseArgs(moduleArgs, mModuleOptions); 84 } 85 86 /** Main loading of configurations, looking into a folder */ loadConfigsFromDirectory( File testsDir, Set<IAbi> abis, String suitePrefix, String suiteTag, List<String> patterns)87 public LinkedHashMap<String, IConfiguration> loadConfigsFromDirectory( 88 File testsDir, 89 Set<IAbi> abis, 90 String suitePrefix, 91 String suiteTag, 92 List<String> patterns) { 93 LinkedHashMap<String, IConfiguration> toRun = new LinkedHashMap<>(); 94 95 List<File> listConfigFiles = new ArrayList<>(); 96 List<File> extraTestCasesDirs = Arrays.asList(testsDir); 97 listConfigFiles.addAll( 98 ConfigurationUtil.getConfigNamesFileFromDirs( 99 suitePrefix, extraTestCasesDirs, patterns)); 100 // Ensure stable initial order of configurations. 101 Collections.sort(listConfigFiles); 102 for (File configFile : listConfigFiles) { 103 toRun.putAll( 104 loadOneConfig( 105 configFile.getName(), configFile.getAbsolutePath(), abis, suiteTag)); 106 } 107 return toRun; 108 } 109 110 /** 111 * Main loading of configurations, looking into the resources on the classpath. (TF configs for 112 * example). 113 */ loadConfigsFromJars( Set<IAbi> abis, String suitePrefix, String suiteTag)114 public LinkedHashMap<String, IConfiguration> loadConfigsFromJars( 115 Set<IAbi> abis, String suitePrefix, String suiteTag) { 116 LinkedHashMap<String, IConfiguration> toRun = new LinkedHashMap<>(); 117 118 IConfigurationFactory configFactory = ConfigurationFactory.getInstance(); 119 List<String> configs = configFactory.getConfigList(suitePrefix, false); 120 // Sort configs to ensure they are always evaluated and added in the same order. 121 Collections.sort(configs); 122 for (String configName : configs) { 123 toRun.putAll(loadOneConfig(configName, configName, abis, suiteTag)); 124 } 125 return toRun; 126 } 127 128 /** 129 * Pass the filters to the {@link IRemoteTest}. Default behavior is to ignore if the IRemoteTest 130 * does not implements {@link ITestFileFilterReceiver}. This can be overriden to create a more 131 * restrictive behavior. 132 * 133 * @param test The {@link IRemoteTest} that is being considered. 134 * @param abi The Abi we are currently working on. 135 * @param name The name of the module. 136 * @param includeFilters The formatted and parsed include filters. 137 * @param excludeFilters The formatted and parsed exclude filters. 138 */ addFiltersToTest( IRemoteTest test, IAbi abi, String name, Map<String, List<SuiteTestFilter>> includeFilters, Map<String, List<SuiteTestFilter>> excludeFilters)139 public void addFiltersToTest( 140 IRemoteTest test, 141 IAbi abi, 142 String name, 143 Map<String, List<SuiteTestFilter>> includeFilters, 144 Map<String, List<SuiteTestFilter>> excludeFilters) { 145 String moduleId = AbiUtils.createId(abi.getName(), name); 146 if (!(test instanceof ITestFilterReceiver)) { 147 CLog.e("Test in module %s does not implement ITestFilterReceiver.", moduleId); 148 return; 149 } 150 List<SuiteTestFilter> mdIncludes = getFilterList(includeFilters, moduleId); 151 List<SuiteTestFilter> mdExcludes = getFilterList(excludeFilters, moduleId); 152 if (!mdIncludes.isEmpty()) { 153 addTestIncludes((ITestFilterReceiver) test, mdIncludes, name); 154 } 155 if (!mdExcludes.isEmpty()) { 156 addTestExcludes((ITestFilterReceiver) test, mdExcludes, name); 157 } 158 } 159 160 /** 161 * Load a single config location (file or on TF classpath). It can results in several {@link 162 * IConfiguration}. If a single configuration get expanded in different ways. 163 * 164 * @param configName The actual config name only. (no path) 165 * @param configFullName The fully qualified config name. (with path, if any). 166 * @param abis The set of all abis that needs to run. 167 * @param suiteTag the Tag of the suite aimed to be run. 168 * @return A map of loaded configuration. 169 */ loadOneConfig( String configName, String configFullName, Set<IAbi> abis, String suiteTag)170 private LinkedHashMap<String, IConfiguration> loadOneConfig( 171 String configName, String configFullName, Set<IAbi> abis, String suiteTag) { 172 LinkedHashMap<String, IConfiguration> toRun = new LinkedHashMap<>(); 173 final String name = configName.replace(CONFIG_EXT, ""); 174 final String[] pathArg = new String[] {configFullName}; 175 try { 176 // Invokes parser to process the test module config file 177 // Need to generate a different config for each ABI as we cannot guarantee the 178 // configs are idempotent. This however means we parse the same file multiple times 179 for (IAbi abi : abis) { 180 String id = AbiUtils.createId(abi.getName(), name); 181 if (!shouldRunModule(id)) { 182 // If the module should not run tests based on the state of filters, 183 // skip this name/abi combination. 184 continue; 185 } 186 IConfiguration config = mConfigFactory.createConfigurationFromArgs(pathArg); 187 188 // If a suiteTag is used, we load with it. 189 if (!Strings.isNullOrEmpty(suiteTag) 190 && !config.getConfigurationDescription() 191 .getSuiteTags() 192 .contains(suiteTag)) { 193 CLog.d( 194 "Configuration %s does not include the suite-tag '%s'. Ignoring it.", 195 configFullName, suiteTag); 196 continue; 197 } 198 199 List<OptionDef> optionsToInject = new ArrayList<>(); 200 if (mModuleOptions.containsKey(name)) { 201 optionsToInject.addAll(mModuleOptions.get(name)); 202 } 203 if (mModuleOptions.containsKey(id)) { 204 optionsToInject.addAll(mModuleOptions.get(id)); 205 } 206 config.injectOptionValues(optionsToInject); 207 208 // Set target preparers 209 List<ITargetPreparer> preparers = config.getTargetPreparers(); 210 for (ITargetPreparer preparer : preparers) { 211 if (preparer instanceof IAbiReceiver) { 212 ((IAbiReceiver) preparer).setAbi(abi); 213 } 214 } 215 216 // Set IRemoteTests 217 List<IRemoteTest> tests = config.getTests(); 218 for (IRemoteTest test : tests) { 219 String className = test.getClass().getName(); 220 if (mTestOptions.containsKey(className)) { 221 config.injectOptionValues(mTestOptions.get(className)); 222 } 223 addFiltersToTest(test, abi, name, mIncludeFilters, mExcludeFilters); 224 if (test instanceof IAbiReceiver) { 225 ((IAbiReceiver) test).setAbi(abi); 226 } 227 } 228 229 // add the abi and module name to the description 230 config.getConfigurationDescription().setAbi(abi); 231 config.getConfigurationDescription().setModuleName(name); 232 toRun.put(id, config); 233 } 234 } catch (ConfigurationException e) { 235 throw new RuntimeException( 236 String.format( 237 "Error parsing configuration: %s: '%s'", 238 configFullName, e.getMessage()), 239 e); 240 } 241 return toRun; 242 } 243 244 /** @return the {@link Set} of modules whose name contains the given pattern. */ getModuleNamesMatching( File directory, String suitePrefix, String pattern)245 public static Set<File> getModuleNamesMatching( 246 File directory, String suitePrefix, String pattern) { 247 List<File> extraTestCasesDirs = Arrays.asList(directory); 248 List<String> patterns = new ArrayList<>(); 249 patterns.add(pattern); 250 Set<File> modules = 251 ConfigurationUtil.getConfigNamesFileFromDirs( 252 suitePrefix, extraTestCasesDirs, patterns); 253 return modules; 254 } 255 256 /** 257 * Utility method that allows to parse and create a structure with the option filters. 258 * 259 * @param stringFilters The original option filters format. 260 * @param filters The filters parsed from the string format. 261 * @param abis The Abis to consider in the filtering. 262 */ addFilters( Set<String> stringFilters, Map<String, List<SuiteTestFilter>> filters, Set<IAbi> abis)263 public static void addFilters( 264 Set<String> stringFilters, Map<String, List<SuiteTestFilter>> filters, Set<IAbi> abis) { 265 for (String filterString : stringFilters) { 266 SuiteTestFilter filter = SuiteTestFilter.createFrom(filterString); 267 String abi = filter.getAbi(); 268 if (abi == null) { 269 for (IAbi a : abis) { 270 addFilter(a.getName(), filter, filters); 271 } 272 } else { 273 addFilter(abi, filter, filters); 274 } 275 } 276 } 277 addFilter( String abi, SuiteTestFilter filter, Map<String, List<SuiteTestFilter>> filters)278 private static void addFilter( 279 String abi, SuiteTestFilter filter, Map<String, List<SuiteTestFilter>> filters) { 280 getFilterList(filters, AbiUtils.createId(abi, filter.getName())).add(filter); 281 } 282 getFilterList( Map<String, List<SuiteTestFilter>> filters, String id)283 private static List<SuiteTestFilter> getFilterList( 284 Map<String, List<SuiteTestFilter>> filters, String id) { 285 List<SuiteTestFilter> fs = filters.get(id); 286 if (fs == null) { 287 fs = new ArrayList<>(); 288 filters.put(id, fs); 289 } 290 return fs; 291 } 292 shouldRunModule(String moduleId)293 private boolean shouldRunModule(String moduleId) { 294 List<SuiteTestFilter> mdIncludes = getFilterList(mIncludeFilters, moduleId); 295 List<SuiteTestFilter> mdExcludes = getFilterList(mExcludeFilters, moduleId); 296 // if including all modules or includes exist for this module, and there are not excludes 297 // for the entire module, this module should be run. 298 return (mIncludeAll || !mdIncludes.isEmpty()) && !containsModuleExclude(mdExcludes); 299 } 300 addTestIncludes( ITestFilterReceiver test, List<SuiteTestFilter> includes, String name)301 private void addTestIncludes( 302 ITestFilterReceiver test, List<SuiteTestFilter> includes, String name) { 303 if (test instanceof ITestFileFilterReceiver) { 304 File includeFile = createFilterFile(name, ".include", includes); 305 ((ITestFileFilterReceiver) test).setIncludeTestFile(includeFile); 306 } else { 307 // add test includes one at a time 308 for (SuiteTestFilter include : includes) { 309 String filterTestName = include.getTest(); 310 if (filterTestName != null) { 311 test.addIncludeFilter(filterTestName); 312 } 313 } 314 } 315 } 316 addTestExcludes( ITestFilterReceiver test, List<SuiteTestFilter> excludes, String name)317 private void addTestExcludes( 318 ITestFilterReceiver test, List<SuiteTestFilter> excludes, String name) { 319 if (test instanceof ITestFileFilterReceiver) { 320 File excludeFile = createFilterFile(name, ".exclude", excludes); 321 ((ITestFileFilterReceiver) test).setExcludeTestFile(excludeFile); 322 } else { 323 // add test excludes one at a time 324 for (SuiteTestFilter exclude : excludes) { 325 test.addExcludeFilter(exclude.getTest()); 326 } 327 } 328 } 329 createFilterFile(String prefix, String suffix, List<SuiteTestFilter> filters)330 private File createFilterFile(String prefix, String suffix, List<SuiteTestFilter> filters) { 331 File filterFile = null; 332 PrintWriter out = null; 333 try { 334 filterFile = FileUtil.createTempFile(prefix, suffix); 335 out = new PrintWriter(filterFile); 336 for (SuiteTestFilter filter : filters) { 337 String filterTest = filter.getTest(); 338 if (filterTest != null) { 339 out.println(filterTest); 340 } 341 } 342 out.flush(); 343 } catch (IOException e) { 344 throw new RuntimeException("Failed to create filter file"); 345 } finally { 346 StreamUtil.close(out); 347 } 348 filterFile.deleteOnExit(); 349 return filterFile; 350 } 351 352 /** Returns true iff one or more test filters in excludes apply to the entire module. */ containsModuleExclude(Collection<SuiteTestFilter> excludes)353 private boolean containsModuleExclude(Collection<SuiteTestFilter> excludes) { 354 for (SuiteTestFilter exclude : excludes) { 355 if (exclude.getTest() == null) { 356 return true; 357 } 358 } 359 return false; 360 } 361 362 /** A {@link FilenameFilter} to find all the config files in a directory. */ 363 public static class ConfigFilter implements FilenameFilter { 364 365 /** {@inheritDoc} */ 366 @Override accept(File dir, String name)367 public boolean accept(File dir, String name) { 368 return name.endsWith(CONFIG_EXT); 369 } 370 } 371 372 /** 373 * Parse a list of args formatted as expected into {@link OptionDef} to be injected to module 374 * configurations. 375 * 376 * <p>Format: <module name / module id / class runner>:<option name>:[<arg-key>:=]<arg-value> 377 */ parseArgs(List<String> args, Map<String, List<OptionDef>> moduleOptions)378 private void parseArgs(List<String> args, Map<String, List<OptionDef>> moduleOptions) { 379 for (String arg : args) { 380 int moduleSep = arg.indexOf(":"); 381 if (moduleSep == -1) { 382 throw new RuntimeException("Expected delimiter ':' for module or class."); 383 } 384 String moduleName = arg.substring(0, moduleSep); 385 String remainder = arg.substring(moduleSep + 1); 386 List<OptionDef> listOption = moduleOptions.get(moduleName); 387 if (listOption == null) { 388 listOption = new ArrayList<>(); 389 moduleOptions.put(moduleName, listOption); 390 } 391 int optionNameSep = remainder.indexOf(":"); 392 if (optionNameSep == -1) { 393 throw new RuntimeException( 394 "Expected delimiter ':' between option name and values."); 395 } 396 String optionName = remainder.substring(0, optionNameSep); 397 String optionValueString = remainder.substring(optionNameSep + 1); 398 // TODO: See if QuotationTokenizer can be improved for multi-character delimiter. 399 // or change the delimiter to a single char. 400 String[] tokens = optionValueString.split(":="); 401 OptionDef option = null; 402 if (tokens.length == 1) { 403 option = new OptionDef(optionName, tokens[0], moduleName); 404 } else if (tokens.length == 2) { 405 option = new OptionDef(optionName, tokens[0], tokens[1], moduleName); 406 } 407 listOption.add(option); 408 } 409 } 410 } 411