1 /* 2 * Copyright (C) 2010 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 17 package com.android.tradefed.config; 18 19 import com.android.ddmlib.Log; 20 import com.android.tradefed.command.CommandOptions; 21 import com.android.tradefed.log.LogUtil.CLog; 22 import com.android.tradefed.util.ClassPathScanner; 23 import com.android.tradefed.util.ClassPathScanner.IClassPathFilter; 24 import com.android.tradefed.util.DirectedGraph; 25 import com.android.tradefed.util.FileUtil; 26 import com.android.tradefed.util.StreamUtil; 27 import com.android.tradefed.util.SystemUtil; 28 import com.android.tradefed.util.keystore.DryRunKeyStore; 29 import com.android.tradefed.util.keystore.IKeyStoreClient; 30 31 import com.google.common.annotations.VisibleForTesting; 32 33 import java.io.BufferedInputStream; 34 import java.io.ByteArrayOutputStream; 35 import java.io.File; 36 import java.io.FileInputStream; 37 import java.io.FileNotFoundException; 38 import java.io.IOException; 39 import java.io.InputStream; 40 import java.io.PrintStream; 41 import java.util.ArrayList; 42 import java.util.Arrays; 43 import java.util.Comparator; 44 import java.util.HashMap; 45 import java.util.Hashtable; 46 import java.util.List; 47 import java.util.Map; 48 import java.util.Set; 49 import java.util.SortedSet; 50 import java.util.TreeSet; 51 import java.util.regex.Pattern; 52 53 /** 54 * Factory for creating {@link IConfiguration}. 55 */ 56 public class ConfigurationFactory implements IConfigurationFactory { 57 58 private static final String LOG_TAG = "ConfigurationFactory"; 59 private static IConfigurationFactory sInstance = null; 60 private static final String CONFIG_SUFFIX = ".xml"; 61 private static final String CONFIG_PREFIX = "config/"; 62 private static final String DRY_RUN_TEMPLATE_CONFIG = "empty"; 63 private static final String CONFIG_ERROR_PATTERN = "(Could not find option with name )(.*)"; 64 65 private Map<ConfigId, ConfigurationDef> mConfigDefMap; 66 67 /** 68 * A simple struct-like class that stores a configuration's name alongside 69 * the arguments for any {@code <template-include>} tags it may contain. 70 * Because the actual bits stored by the configuration may vary with 71 * template arguments, they must be considered as essential a part of the 72 * configuration's identity as the filename. 73 */ 74 static class ConfigId { 75 public String name = null; 76 public Map<String, String> templateMap = new HashMap<>(); 77 78 /** 79 * No-op constructor 80 */ ConfigId()81 public ConfigId() { 82 } 83 84 /** 85 * Convenience constructor. Equivalent to calling two-arg constructor 86 * with {@code null} {@code templateMap}. 87 */ ConfigId(String name)88 public ConfigId(String name) { 89 this(name, null); 90 } 91 92 /** 93 * Two-arg convenience constructor. {@code templateMap} may be null. 94 */ ConfigId(String name, Map<String, String> templateMap)95 public ConfigId(String name, Map<String, String> templateMap) { 96 this.name = name; 97 if (templateMap != null) { 98 this.templateMap.putAll(templateMap); 99 } 100 } 101 102 /** 103 * {@inheritDoc} 104 */ 105 @Override hashCode()106 public int hashCode() { 107 return 2 * ((name == null) ? 0 : name.hashCode()) + 3 * templateMap.hashCode(); 108 } 109 matches(Object a, Object b)110 private boolean matches(Object a, Object b) { 111 if (a == null && b == null) 112 return true; 113 if (a == null || b == null) 114 return false; 115 return a.equals(b); 116 } 117 118 /** 119 * {@inheritDoc} 120 */ 121 @Override equals(Object other)122 public boolean equals(Object other) { 123 if (other == null) 124 return false; 125 if (!(other instanceof ConfigId)) 126 return false; 127 128 final ConfigId otherConf = (ConfigId) other; 129 return matches(name, otherConf.name) && matches(templateMap, otherConf.templateMap); 130 } 131 } 132 133 /** 134 * A {@link IClassPathFilter} for configuration XML files. 135 */ 136 private class ConfigClasspathFilter implements IClassPathFilter { 137 138 private String mPrefix = null; 139 ConfigClasspathFilter(String prefix)140 public ConfigClasspathFilter(String prefix) { 141 mPrefix = getConfigPrefix(); 142 if (prefix != null) { 143 mPrefix += prefix; 144 } 145 CLog.d("Searching the '%s' config path", mPrefix); 146 } 147 148 /** 149 * {@inheritDoc} 150 */ 151 @Override accept(String pathName)152 public boolean accept(String pathName) { 153 // only accept entries that match the pattern, and that we don't already know about 154 final ConfigId pathId = new ConfigId(pathName); 155 return pathName.startsWith(mPrefix) && pathName.endsWith(CONFIG_SUFFIX) && 156 !mConfigDefMap.containsKey(pathId); 157 } 158 159 /** 160 * {@inheritDoc} 161 */ 162 @Override transform(String pathName)163 public String transform(String pathName) { 164 // strip off CONFIG_PREFIX and CONFIG_SUFFIX 165 int pathStartIndex = getConfigPrefix().length(); 166 int pathEndIndex = pathName.length() - CONFIG_SUFFIX.length(); 167 return pathName.substring(pathStartIndex, pathEndIndex); 168 } 169 } 170 171 /** 172 * A {@link Comparator} for {@link ConfigurationDef} that sorts by 173 * {@link ConfigurationDef#getName()}. 174 */ 175 private static class ConfigDefComparator implements Comparator<ConfigurationDef> { 176 177 /** 178 * {@inheritDoc} 179 */ 180 @Override compare(ConfigurationDef d1, ConfigurationDef d2)181 public int compare(ConfigurationDef d1, ConfigurationDef d2) { 182 return d1.getName().compareTo(d2.getName()); 183 } 184 185 } 186 187 /** 188 * Get a list of {@link File} of the test cases directories 189 * 190 * <p>The wrapper function is for unit test to mock the system calls. 191 * 192 * @return a list of {@link File} of directories of the test cases folder of build output, based 193 * on the value of environment variables. 194 */ 195 @VisibleForTesting getExternalTestCasesDirs()196 List<File> getExternalTestCasesDirs() { 197 return SystemUtil.getExternalTestCasesDirs(); 198 } 199 200 /** 201 * Get the path to the config file for a test case. 202 * 203 * <p>The given name in a test config can be the name of a test case located in an out directory 204 * defined in the following environment variables: 205 * 206 * <p>ANDROID_TARGET_OUT_TESTCASES 207 * 208 * <p>ANDROID_HOST_OUT_TESTCASES 209 * 210 * <p>This method tries to locate the test config name in these directories. If no config is 211 * found, return null. 212 * 213 * @param name Name of a config file. 214 * @return A File object of the config file for the given test case. 215 */ 216 @VisibleForTesting getTestCaseConfigPath(String name)217 File getTestCaseConfigPath(String name) { 218 String[] possibleConfigFileNames = {name + ".xml", name + ".config"}; 219 for (File testCasesDir : getExternalTestCasesDirs()) { 220 for (String configFileName : possibleConfigFileNames) { 221 File config = FileUtil.findFile(testCasesDir, configFileName); 222 if (config != null) { 223 CLog.d("Using config: %s/%s", testCasesDir.getAbsoluteFile(), configFileName); 224 return config; 225 } 226 } 227 } 228 return null; 229 } 230 231 /** 232 * Implementation of {@link IConfigDefLoader} that tracks the included configurations from one 233 * root config, and throws an exception on circular includes. 234 */ 235 protected class ConfigLoader implements IConfigDefLoader { 236 237 private final boolean mIsGlobalConfig; 238 private DirectedGraph<String> mConfigGraph = new DirectedGraph<String>(); 239 ConfigLoader(boolean isGlobalConfig)240 public ConfigLoader(boolean isGlobalConfig) { 241 mIsGlobalConfig = isGlobalConfig; 242 } 243 244 /** 245 * {@inheritDoc} 246 */ 247 @Override getConfigurationDef(String name, Map<String, String> templateMap)248 public ConfigurationDef getConfigurationDef(String name, Map<String, String> templateMap) 249 throws ConfigurationException { 250 251 String configName = findConfigName(name, null); 252 final ConfigId configId = new ConfigId(name, templateMap); 253 ConfigurationDef def = mConfigDefMap.get(configId); 254 255 if (def == null || def.isStale()) { 256 def = new ConfigurationDef(configName); 257 loadConfiguration(configName, def, null, templateMap, null); 258 mConfigDefMap.put(configId, def); 259 } else { 260 if (templateMap != null) { 261 // Clearing the map before returning the cached config to 262 // avoid seeing them as unused. 263 templateMap.clear(); 264 } 265 } 266 return def; 267 } 268 269 /** Returns true if it is a config file found inside the classpath. */ isBundledConfig(String name)270 protected boolean isBundledConfig(String name) { 271 InputStream configStream = 272 getClass() 273 .getResourceAsStream( 274 String.format( 275 "/%s%s%s", getConfigPrefix(), name, CONFIG_SUFFIX)); 276 return configStream != null; 277 } 278 279 /** 280 * Get the absolute path of a local config file. 281 * 282 * @param root parent path of config file 283 * @param name config file 284 * @return absolute path for local config file. 285 * @throws ConfigurationException 286 */ getAbsolutePath(String root, String name)287 private String getAbsolutePath(String root, String name) throws ConfigurationException { 288 File file = new File(name); 289 if (!file.isAbsolute()) { 290 if (root == null) { 291 // if root directory was not specified, get the current 292 // working directory. 293 root = System.getProperty("user.dir"); 294 } 295 file = new File(root, name); 296 } 297 try { 298 return file.getCanonicalPath(); 299 } catch (IOException e) { 300 throw new ConfigurationException(String.format( 301 "Failure when trying to determine local file canonical path %s", e)); 302 } 303 } 304 305 /** 306 * Find config's name based on its name and its parent name. This is used to properly handle 307 * bundle configs and local configs. 308 * 309 * @param name config's name 310 * @param parentName config's parent's name. 311 * @return the config's full name. 312 * @throws ConfigurationException 313 */ findConfigName(String name, String parentName)314 protected String findConfigName(String name, String parentName) 315 throws ConfigurationException { 316 if (isBundledConfig(name)) { 317 return name; 318 } 319 if (parentName == null || isBundledConfig(parentName)) { 320 // Search files for config. 321 String configName = getAbsolutePath(null, name); 322 File localConfig = new File(configName); 323 if (!localConfig.exists()) { 324 localConfig = getTestCaseConfigPath(name); 325 } 326 if (localConfig != null) { 327 return localConfig.getAbsolutePath(); 328 } 329 // Can not find local config. 330 if (parentName == null) { 331 throw new ConfigurationException( 332 String.format("Can not find local config %s.", name)); 333 334 } else { 335 throw new ConfigurationException( 336 String.format( 337 "Bundled config '%s' is including a config '%s' that's neither " 338 + "local nor bundled.", 339 parentName, name)); 340 } 341 } 342 try { 343 // Local configs' include should be relative to their parent's path. 344 String parentRoot = new File(parentName).getParentFile().getCanonicalPath(); 345 return getAbsolutePath(parentRoot, name); 346 } catch (IOException e) { 347 throw new ConfigurationException(e.getMessage(), e.getCause()); 348 } 349 } 350 351 /** 352 * Configs that are bundled inside the tradefed.jar can only include other configs also 353 * bundled inside tradefed.jar. However, local (external) configs can include both local 354 * (external) and bundled configs. 355 */ 356 @Override loadIncludedConfiguration( ConfigurationDef def, String parentName, String name, String deviceTagObject, Map<String, String> templateMap, Set<String> templateSeen)357 public void loadIncludedConfiguration( 358 ConfigurationDef def, 359 String parentName, 360 String name, 361 String deviceTagObject, 362 Map<String, String> templateMap, 363 Set<String> templateSeen) 364 throws ConfigurationException { 365 366 String config_name = findConfigName(name, parentName); 367 mConfigGraph.addEdge(parentName, config_name); 368 // If the inclusion of configurations is a cycle we throw an exception. 369 if (!mConfigGraph.isDag()) { 370 CLog.e("%s", mConfigGraph); 371 throw new ConfigurationException(String.format( 372 "Circular configuration include: config '%s' is already included", 373 config_name)); 374 } 375 loadConfiguration(config_name, def, deviceTagObject, templateMap, templateSeen); 376 } 377 378 /** 379 * Loads a configuration. 380 * 381 * @param name the name of a built-in configuration to load or a file path to configuration 382 * xml to load 383 * @param def the loaded {@link ConfigurationDef} 384 * @param deviceTagObject name of the current deviceTag if we are loading from a config 385 * inside an <include>. Null otherwise. 386 * @param templateMap map from template-include names to their respective concrete 387 * configuration files 388 * @param templateSeen set of template placeholder name already encountered 389 * @throws ConfigurationException if a configuration with given name/file path cannot be 390 * loaded or parsed 391 */ loadConfiguration( String name, ConfigurationDef def, String deviceTagObject, Map<String, String> templateMap, Set<String> templateSeen)392 void loadConfiguration( 393 String name, 394 ConfigurationDef def, 395 String deviceTagObject, 396 Map<String, String> templateMap, 397 Set<String> templateSeen) 398 throws ConfigurationException { 399 BufferedInputStream bufStream = getConfigStream(name); 400 ConfigurationXmlParser parser = new ConfigurationXmlParser(this, deviceTagObject); 401 parser.parse(def, name, bufStream, templateMap, templateSeen); 402 trackConfig(name, def); 403 } 404 405 /** 406 * Track config for dynamic loading. Right now only local files are supported. 407 * 408 * @param name config's name 409 * @param def config's def. 410 */ trackConfig(String name, ConfigurationDef def)411 protected void trackConfig(String name, ConfigurationDef def) { 412 // Track local config source files 413 if (!isBundledConfig(name)) { 414 def.registerSource(new File(name)); 415 } 416 } 417 418 /** 419 * Should track the config's life cycle or not. 420 * 421 * @param name config's name 422 * @return <code>true</code> if the config is trackable, otherwise <code>false</code>. 423 */ isTrackableConfig(String name)424 protected boolean isTrackableConfig(String name) { 425 return !isBundledConfig(name); 426 } 427 428 /** 429 * {@inheritDoc} 430 */ 431 @Override isGlobalConfig()432 public boolean isGlobalConfig() { 433 return mIsGlobalConfig; 434 } 435 436 } 437 ConfigurationFactory()438 protected ConfigurationFactory() { 439 mConfigDefMap = new Hashtable<ConfigId, ConfigurationDef>(); 440 } 441 442 /** 443 * Get the singleton {@link IConfigurationFactory} instance. 444 */ getInstance()445 public static IConfigurationFactory getInstance() { 446 if (sInstance == null) { 447 sInstance = new ConfigurationFactory(); 448 } 449 return sInstance; 450 } 451 452 /** 453 * Retrieve the {@link ConfigurationDef} for the given name 454 * 455 * @param name the name of a built-in configuration to load or a file path to configuration xml 456 * to load 457 * @return {@link ConfigurationDef} 458 * @throws ConfigurationException if an error occurred loading the config 459 */ getConfigurationDef( String name, boolean isGlobal, Map<String, String> templateMap)460 protected ConfigurationDef getConfigurationDef( 461 String name, boolean isGlobal, Map<String, String> templateMap) 462 throws ConfigurationException { 463 return new ConfigLoader(isGlobal).getConfigurationDef(name, templateMap); 464 } 465 466 /** 467 * {@inheritDoc} 468 */ 469 @Override createConfigurationFromArgs(String[] arrayArgs)470 public IConfiguration createConfigurationFromArgs(String[] arrayArgs) 471 throws ConfigurationException { 472 return createConfigurationFromArgs(arrayArgs, null); 473 } 474 475 /** 476 * {@inheritDoc} 477 */ 478 @Override createConfigurationFromArgs(String[] arrayArgs, List<String> unconsumedArgs)479 public IConfiguration createConfigurationFromArgs(String[] arrayArgs, 480 List<String> unconsumedArgs) throws ConfigurationException { 481 return createConfigurationFromArgs(arrayArgs, unconsumedArgs, null); 482 } 483 484 /** 485 * {@inheritDoc} 486 */ 487 @Override createConfigurationFromArgs(String[] arrayArgs, List<String> unconsumedArgs, IKeyStoreClient keyStoreClient)488 public IConfiguration createConfigurationFromArgs(String[] arrayArgs, 489 List<String> unconsumedArgs, IKeyStoreClient keyStoreClient) 490 throws ConfigurationException { 491 List<String> listArgs = new ArrayList<String>(arrayArgs.length); 492 // FIXME: Update parsing to not care about arg order. 493 String[] reorderedArrayArgs = reorderArgs(arrayArgs); 494 IConfiguration config = 495 internalCreateConfigurationFromArgs(reorderedArrayArgs, listArgs, keyStoreClient); 496 config.setCommandLine(arrayArgs); 497 if (listArgs.contains("--" + CommandOptions.DRY_RUN_OPTION) 498 || listArgs.contains("--" + CommandOptions.NOISY_DRY_RUN_OPTION)) { 499 // In case of dry-run, we replace the KeyStore by a dry-run one. 500 CLog.w("dry-run detected, we are using a dryrun keystore"); 501 keyStoreClient = new DryRunKeyStore(); 502 } 503 final List<String> tmpUnconsumedArgs = config.setOptionsFromCommandLineArgs( 504 listArgs, keyStoreClient); 505 506 if (unconsumedArgs == null && tmpUnconsumedArgs.size() > 0) { 507 // (unconsumedArgs == null) is taken as a signal that the caller 508 // expects all args to 509 // be processed. 510 throw new ConfigurationException(String.format( 511 "Invalid arguments provided. Unprocessed arguments: %s", tmpUnconsumedArgs)); 512 } else if (unconsumedArgs != null) { 513 // Return the unprocessed args 514 unconsumedArgs.addAll(tmpUnconsumedArgs); 515 } 516 517 return config; 518 } 519 520 /** 521 * Creates a {@link Configuration} from the name given in arguments. 522 * <p/> 523 * Note will not populate configuration with values from options 524 * 525 * @param arrayArgs the full list of command line arguments, including the 526 * config name 527 * @param optionArgsRef an empty list, that will be populated with the 528 * option arguments left to be interpreted 529 * @param keyStoreClient {@link IKeyStoreClient} keystore client to use if 530 * any. 531 * @return An {@link IConfiguration} object representing the configuration 532 * that was loaded 533 * @throws ConfigurationException 534 */ internalCreateConfigurationFromArgs(String[] arrayArgs, List<String> optionArgsRef, IKeyStoreClient keyStoreClient)535 private IConfiguration internalCreateConfigurationFromArgs(String[] arrayArgs, 536 List<String> optionArgsRef, IKeyStoreClient keyStoreClient) 537 throws ConfigurationException { 538 if (arrayArgs.length == 0) { 539 throw new ConfigurationException("Configuration to run was not specified"); 540 } 541 final List<String> listArgs = new ArrayList<>(Arrays.asList(arrayArgs)); 542 // first arg is config name 543 final String configName = listArgs.remove(0); 544 545 // Steal ConfigurationXmlParser arguments from the command line 546 final ConfigurationXmlParserSettings parserSettings = new ConfigurationXmlParserSettings(); 547 final ArgsOptionParser templateArgParser = new ArgsOptionParser(parserSettings); 548 if (keyStoreClient != null) { 549 templateArgParser.setKeyStore(keyStoreClient); 550 } 551 optionArgsRef.addAll(templateArgParser.parseBestEffort(listArgs)); 552 // Check that the same template is not attempted to be loaded twice. 553 for (String key : parserSettings.templateMap.keySet()) { 554 if (parserSettings.templateMap.get(key).size() > 1) { 555 throw new ConfigurationException( 556 String.format("More than one template specified for key '%s'", key)); 557 } 558 } 559 Map<String, String> uniqueMap = parserSettings.templateMap.getUniqueMap(); 560 ConfigurationDef configDef = getConfigurationDef(configName, false, uniqueMap); 561 if (!uniqueMap.isEmpty()) { 562 // remove the bad ConfigDef from the cache. 563 for (ConfigId cid : mConfigDefMap.keySet()) { 564 if (mConfigDefMap.get(cid) == configDef) { 565 CLog.d("Cleaning the cache for this configdef"); 566 mConfigDefMap.remove(cid); 567 break; 568 } 569 } 570 throw new ConfigurationException( 571 String.format("Unused template:map parameters: %s", uniqueMap.toString())); 572 } 573 return configDef.createConfiguration(); 574 } 575 576 /** 577 * {@inheritDoc} 578 */ 579 @Override createGlobalConfigurationFromArgs(String[] arrayArgs, List<String> remainingArgs)580 public IGlobalConfiguration createGlobalConfigurationFromArgs(String[] arrayArgs, 581 List<String> remainingArgs) throws ConfigurationException { 582 List<String> listArgs = new ArrayList<String>(arrayArgs.length); 583 IGlobalConfiguration config = internalCreateGlobalConfigurationFromArgs(arrayArgs, 584 listArgs); 585 remainingArgs.addAll(config.setOptionsFromCommandLineArgs(listArgs)); 586 587 return config; 588 } 589 590 /** 591 * Creates a {@link GlobalConfiguration} from the name given in arguments. 592 * <p/> 593 * Note will not populate configuration with values from options 594 * 595 * @param arrayArgs the full list of command line arguments, including the config name 596 * @param optionArgsRef an empty list, that will be populated with the 597 * remaining option arguments 598 * @return a {@link IGlobalConfiguration} created from the args 599 * @throws ConfigurationException 600 */ internalCreateGlobalConfigurationFromArgs(String[] arrayArgs, List<String> optionArgsRef)601 private IGlobalConfiguration internalCreateGlobalConfigurationFromArgs(String[] arrayArgs, 602 List<String> optionArgsRef) throws ConfigurationException { 603 if (arrayArgs.length == 0) { 604 throw new ConfigurationException("Configuration to run was not specified"); 605 } 606 optionArgsRef.addAll(Arrays.asList(arrayArgs)); 607 // first arg is config name 608 final String configName = optionArgsRef.remove(0); 609 ConfigurationDef configDef = getConfigurationDef(configName, true, null); 610 IGlobalConfiguration config = configDef.createGlobalConfiguration(); 611 config.setOriginalConfig(configName); 612 return config; 613 } 614 615 /** 616 * {@inheritDoc} 617 */ 618 @Override printHelp(PrintStream out)619 public void printHelp(PrintStream out) { 620 try { 621 loadAllConfigs(true); 622 } catch (ConfigurationException e) { 623 // ignore, should never happen 624 } 625 // sort the configs by name before displaying 626 SortedSet<ConfigurationDef> configDefs = new TreeSet<ConfigurationDef>( 627 new ConfigDefComparator()); 628 configDefs.addAll(mConfigDefMap.values()); 629 for (ConfigurationDef def : configDefs) { 630 out.printf(" %s: %s", def.getName(), def.getDescription()); 631 out.println(); 632 } 633 } 634 635 /** 636 * {@inheritDoc} 637 */ 638 @Override getConfigList()639 public List<String> getConfigList() { 640 return getConfigList(null); 641 } 642 643 /** 644 * {@inheritDoc} 645 */ 646 @Override getConfigList(String subPath)647 public List<String> getConfigList(String subPath) { 648 return getConfigList(subPath, true); 649 } 650 651 /** {@inheritDoc} */ 652 @Override getConfigList(String subPath, boolean loadFromEnv)653 public List<String> getConfigList(String subPath, boolean loadFromEnv) { 654 Set<String> configNames = getConfigSetFromClasspath(subPath); 655 if (loadFromEnv) { 656 // list config on variable path too 657 configNames.addAll(getConfigNamesFromTestCases(subPath)); 658 } 659 // sort the configs by name before adding to list 660 SortedSet<String> configDefs = new TreeSet<String>(); 661 configDefs.addAll(configNames); 662 List<String> configs = new ArrayList<String>(); 663 configs.addAll(configDefs); 664 return configs; 665 } 666 667 /** 668 * Private helper to get the full set of configurations. 669 */ getConfigSetFromClasspath(String subPath)670 private Set<String> getConfigSetFromClasspath(String subPath) { 671 ClassPathScanner cpScanner = new ClassPathScanner(); 672 return cpScanner.getClassPathEntries(new ConfigClasspathFilter(subPath)); 673 } 674 675 /** 676 * Helper to get the test config files from test cases directories from build output. 677 * 678 * @param subPath where to look for configuration. Can be null. 679 */ 680 @VisibleForTesting getConfigNamesFromTestCases(String subPath)681 Set<String> getConfigNamesFromTestCases(String subPath) { 682 return ConfigurationUtil.getConfigNamesFromDirs(subPath, getExternalTestCasesDirs()); 683 } 684 685 /** 686 * Loads all configurations found in classpath and test cases directories. 687 * 688 * @param discardExceptions true if any ConfigurationException should be ignored. 689 * @throws ConfigurationException 690 */ loadAllConfigs(boolean discardExceptions)691 public void loadAllConfigs(boolean discardExceptions) throws ConfigurationException { 692 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 693 PrintStream ps = new PrintStream(baos); 694 boolean failed = false; 695 Set<String> configNames = getConfigSetFromClasspath(null); 696 // TODO: split the configs into two lists, one from the jar packages and one from test 697 // cases directories. 698 configNames.addAll(getConfigNamesFromTestCases(null)); 699 for (String configName : configNames) { 700 final ConfigId configId = new ConfigId(configName); 701 try { 702 ConfigurationDef configDef = attemptLoad(configId, null); 703 mConfigDefMap.put(configId, configDef); 704 } catch (ConfigurationException e) { 705 ps.printf("Failed to load %s: %s", configName, e.getMessage()); 706 ps.println(); 707 failed = true; 708 } 709 } 710 if (failed) { 711 if (discardExceptions) { 712 CLog.e("Failure loading configs"); 713 CLog.e(baos.toString()); 714 } else { 715 throw new ConfigurationException(baos.toString()); 716 } 717 } 718 } 719 720 /** 721 * Helper to load a configuration. 722 */ attemptLoad(ConfigId configId, Map<String, String> templateMap)723 private ConfigurationDef attemptLoad(ConfigId configId, Map<String, String> templateMap) 724 throws ConfigurationException { 725 ConfigurationDef configDef = null; 726 try { 727 configDef = getConfigurationDef(configId.name, false, templateMap); 728 return configDef; 729 } catch (TemplateResolutionError tre) { 730 // When a template does not have a default, we try again with known good template 731 // to make sure file formatting at the very least is fine. 732 Map<String, String> fakeTemplateMap = new HashMap<String, String>(); 733 if (templateMap != null) { 734 fakeTemplateMap.putAll(templateMap); 735 } 736 fakeTemplateMap.put(tre.getTemplateKey(), DRY_RUN_TEMPLATE_CONFIG); 737 // We go recursively in case there are several template to dry run. 738 return attemptLoad(configId, fakeTemplateMap); 739 } 740 } 741 742 /** 743 * {@inheritDoc} 744 */ 745 @Override printHelpForConfig(String[] args, boolean importantOnly, PrintStream out)746 public void printHelpForConfig(String[] args, boolean importantOnly, PrintStream out) { 747 try { 748 IConfiguration config = internalCreateConfigurationFromArgs(args, 749 new ArrayList<String>(args.length), null); 750 config.printCommandUsage(importantOnly, out); 751 } catch (ConfigurationException e) { 752 // config must not be specified. Print generic help 753 printHelp(out); 754 } 755 } 756 757 /** 758 * {@inheritDoc} 759 */ 760 @Override dumpConfig(String configName, PrintStream out)761 public void dumpConfig(String configName, PrintStream out) { 762 try { 763 InputStream configStream = getConfigStream(configName); 764 StreamUtil.copyStreams(configStream, out); 765 } catch (ConfigurationException e) { 766 Log.e(LOG_TAG, e); 767 } catch (IOException e) { 768 Log.e(LOG_TAG, e); 769 } 770 } 771 772 /** 773 * Return the path prefix of config xml files on classpath 774 * 775 * <p>Exposed so unit tests can mock. 776 * 777 * @return {@link String} path with trailing / 778 */ getConfigPrefix()779 protected String getConfigPrefix() { 780 return CONFIG_PREFIX; 781 } 782 783 /** 784 * Loads an InputStream for given config name 785 * 786 * @param name the configuration name to load 787 * @return a {@link BufferedInputStream} for reading config contents 788 * @throws ConfigurationException if config could not be found 789 */ getConfigStream(String name)790 protected BufferedInputStream getConfigStream(String name) throws ConfigurationException { 791 InputStream configStream = getBundledConfigStream(name); 792 if (configStream == null) { 793 // now try to load from file 794 try { 795 configStream = new FileInputStream(name); 796 } catch (FileNotFoundException e) { 797 throw new ConfigurationException(String.format("Could not find configuration '%s'", 798 name)); 799 } 800 } 801 // buffer input for performance - just in case config file is large 802 return new BufferedInputStream(configStream); 803 } 804 getBundledConfigStream(String name)805 protected InputStream getBundledConfigStream(String name) { 806 return getClass() 807 .getResourceAsStream( 808 String.format("/%s%s%s", getConfigPrefix(), name, CONFIG_SUFFIX)); 809 } 810 811 /** 812 * Utility method that checks that all configs can be loaded, parsed, and 813 * all option values set. 814 * Only exposed so that depending project can validate their configs. 815 * Should not be exposed in the console. 816 * 817 * @throws ConfigurationException if one or more configs failed to load 818 */ loadAndPrintAllConfigs()819 public void loadAndPrintAllConfigs() throws ConfigurationException { 820 loadAllConfigs(false); 821 boolean failed = false; 822 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 823 PrintStream ps = new PrintStream(baos); 824 825 for (ConfigurationDef def : mConfigDefMap.values()) { 826 try { 827 def.createConfiguration().printCommandUsage(false, 828 new PrintStream(StreamUtil.nullOutputStream())); 829 } catch (ConfigurationException e) { 830 if (e.getCause() != null && 831 e.getCause() instanceof ClassNotFoundException) { 832 ClassNotFoundException cnfe = (ClassNotFoundException) e.getCause(); 833 String className = cnfe.getLocalizedMessage(); 834 // Some Cts configs are shipped with Trade Federation, we exclude those from 835 // the failure since these packages are not available for loading. 836 if (className != null && className.startsWith("com.android.cts.")) { 837 CLog.w("Could not confirm %s: %s because not part of Trade Federation " 838 + "packages.", def.getName(), e.getMessage()); 839 continue; 840 } 841 } else if (Pattern.matches(CONFIG_ERROR_PATTERN, e.getMessage())) { 842 // If options are inside configuration object tag we are able to validate them 843 if (!e.getMessage().contains("com.android.") && 844 !e.getMessage().contains("com.google.android.")) { 845 // We cannot confirm if an option is indeed missing since a template of 846 // option only is possible to avoid repetition in configuration with the 847 // same base. 848 CLog.w("Could not confirm %s: %s", def.getName(), e.getMessage()); 849 continue; 850 } 851 } 852 ps.printf("Failed to print %s: %s", def.getName(), e.getMessage()); 853 ps.println(); 854 failed = true; 855 } 856 } 857 if (failed) { 858 throw new ConfigurationException(baos.toString()); 859 } 860 } 861 862 /** 863 * Exposed for testing. Return a copy of the Map. 864 */ getMapConfig()865 protected Map<ConfigId, ConfigurationDef> getMapConfig() { 866 // We return a copy to ensure it is not modified outside 867 return new HashMap<ConfigId, ConfigurationDef>(mConfigDefMap); 868 } 869 870 /** In some particular case, we need to clear the map. */ 871 @VisibleForTesting clearMapConfig()872 public void clearMapConfig() { 873 mConfigDefMap.clear(); 874 } 875 876 /** Reorder the args so that template:map args are all moved to the front. */ 877 @VisibleForTesting reorderArgs(String[] args)878 protected String[] reorderArgs(String[] args) { 879 List<String> nonTemplateArgs = new ArrayList<String>(); 880 List<String> reorderedArgs = new ArrayList<String>(); 881 String[] reorderedArgsArray = new String[args.length]; 882 String arg; 883 884 // First arg is the config. 885 if (args.length > 0) { 886 reorderedArgs.add(args[0]); 887 } 888 889 // Split out the template and non-template args so we can add 890 // non-template args at the end while maintaining their order. 891 for (int i = 1; i < args.length; i++) { 892 arg = args[i]; 893 if (arg.equals("--template:map")) { 894 // We need to account for these two types of template:map args. 895 // --template:map tm=tm1 896 // --template:map tm tm1 897 reorderedArgs.add(arg); 898 for (int j = i + 1; j < args.length; j++) { 899 if (args[j].startsWith("-")) { 900 break; 901 } else { 902 reorderedArgs.add(args[j]); 903 i++; 904 } 905 } 906 } else { 907 nonTemplateArgs.add(arg); 908 } 909 } 910 reorderedArgs.addAll(nonTemplateArgs); 911 return reorderedArgs.toArray(reorderedArgsArray); 912 } 913 } 914