Home | History | Annotate | Download | only in config
      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          */
     81         public ConfigId() {
     82         }
     83 
     84         /**
     85          * Convenience constructor. Equivalent to calling two-arg constructor
     86          * with {@code null} {@code templateMap}.
     87          */
     88         public ConfigId(String name) {
     89             this(name, null);
     90         }
     91 
     92         /**
     93          * Two-arg convenience constructor. {@code templateMap} may be null.
     94          */
     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
    106         public int hashCode() {
    107             return 2 * ((name == null) ? 0 : name.hashCode()) + 3 * templateMap.hashCode();
    108         }
    109 
    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
    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 
    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
    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
    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
    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
    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
    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 
    240         public ConfigLoader(boolean isGlobalConfig) {
    241             mIsGlobalConfig = isGlobalConfig;
    242         }
    243 
    244         /**
    245          * {@inheritDoc}
    246          */
    247         @Override
    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);
    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. */
    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          */
    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          */
    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
    357         public void loadIncludedConfiguration(
    358                 ConfigurationDef def,
    359                 String parentName,
    360                 String name,
    361                 String deviceTagObject,
    362                 Map<String, String> templateMap)
    363                 throws ConfigurationException {
    364 
    365             String config_name = findConfigName(name, parentName);
    366             mConfigGraph.addEdge(parentName, config_name);
    367             // If the inclusion of configurations is a cycle we throw an exception.
    368             if (!mConfigGraph.isDag()) {
    369                 CLog.e("%s", mConfigGraph);
    370                 throw new ConfigurationException(String.format(
    371                         "Circular configuration include: config '%s' is already included",
    372                         config_name));
    373             }
    374             loadConfiguration(config_name, def, deviceTagObject, templateMap);
    375         }
    376 
    377         /**
    378          * Loads a configuration.
    379          *
    380          * @param name the name of a built-in configuration to load or a file path to configuration
    381          *     xml to load
    382          * @param def the loaded {@link ConfigurationDef}
    383          * @param deviceTagObject name of the current deviceTag if we are loading from a config
    384          *     inside an <include>. Null otherwise.
    385          * @param templateMap map from template-include names to their respective concrete
    386          *     configuration files
    387          * @throws ConfigurationException if a configuration with given name/file path cannot be
    388          *     loaded or parsed
    389          */
    390         void loadConfiguration(
    391                 String name,
    392                 ConfigurationDef def,
    393                 String deviceTagObject,
    394                 Map<String, String> templateMap)
    395                 throws ConfigurationException {
    396             //Log.d(LOG_TAG, String.format("Loading configuration '%s'", name));
    397             BufferedInputStream bufStream = getConfigStream(name);
    398             ConfigurationXmlParser parser = new ConfigurationXmlParser(this, deviceTagObject);
    399             parser.parse(def, name, bufStream, templateMap);
    400             trackConfig(name, def);
    401         }
    402 
    403         /**
    404          * Track config for dynamic loading. Right now only local files are supported.
    405          *
    406          * @param name config's name
    407          * @param def config's def.
    408          */
    409         protected void trackConfig(String name, ConfigurationDef def) {
    410             // Track local config source files
    411             if (!isBundledConfig(name)) {
    412                 def.registerSource(new File(name));
    413             }
    414         }
    415 
    416         /**
    417          * Should track the config's life cycle or not.
    418          *
    419          * @param name config's name
    420          * @return <code>true</code> if the config is trackable, otherwise <code>false</code>.
    421          */
    422         protected boolean isTrackableConfig(String name) {
    423             return !isBundledConfig(name);
    424         }
    425 
    426         /**
    427          * {@inheritDoc}
    428          */
    429         @Override
    430         public boolean isGlobalConfig() {
    431             return mIsGlobalConfig;
    432         }
    433 
    434     }
    435 
    436     protected ConfigurationFactory() {
    437         mConfigDefMap = new Hashtable<ConfigId, ConfigurationDef>();
    438     }
    439 
    440     /**
    441      * Get the singleton {@link IConfigurationFactory} instance.
    442      */
    443     public static IConfigurationFactory getInstance() {
    444         if (sInstance == null) {
    445             sInstance = new ConfigurationFactory();
    446         }
    447         return sInstance;
    448     }
    449 
    450     /**
    451      * Retrieve the {@link ConfigurationDef} for the given name
    452      *
    453      * @param name the name of a built-in configuration to load or a file path to configuration xml
    454      *     to load
    455      * @return {@link ConfigurationDef}
    456      * @throws ConfigurationException if an error occurred loading the config
    457      */
    458     protected ConfigurationDef getConfigurationDef(
    459             String name, boolean isGlobal, Map<String, String> templateMap)
    460             throws ConfigurationException {
    461         return new ConfigLoader(isGlobal).getConfigurationDef(name, templateMap);
    462     }
    463 
    464     /**
    465      * {@inheritDoc}
    466      */
    467     @Override
    468     public IConfiguration createConfigurationFromArgs(String[] arrayArgs)
    469             throws ConfigurationException {
    470         return createConfigurationFromArgs(arrayArgs, null);
    471     }
    472 
    473     /**
    474      * {@inheritDoc}
    475      */
    476     @Override
    477     public IConfiguration createConfigurationFromArgs(String[] arrayArgs,
    478             List<String> unconsumedArgs) throws ConfigurationException {
    479         return createConfigurationFromArgs(arrayArgs, unconsumedArgs, null);
    480     }
    481 
    482     /**
    483      * {@inheritDoc}
    484      */
    485     @Override
    486     public IConfiguration createConfigurationFromArgs(String[] arrayArgs,
    487             List<String> unconsumedArgs, IKeyStoreClient keyStoreClient)
    488             throws ConfigurationException {
    489         List<String> listArgs = new ArrayList<String>(arrayArgs.length);
    490         // FIXME: Update parsing to not care about arg order.
    491         String[] reorderedArrayArgs = reorderArgs(arrayArgs);
    492         IConfiguration config =
    493                 internalCreateConfigurationFromArgs(reorderedArrayArgs, listArgs, keyStoreClient);
    494         config.setCommandLine(arrayArgs);
    495         if (listArgs.contains("--" + CommandOptions.DRY_RUN_OPTION)
    496                 || listArgs.contains("--" + CommandOptions.NOISY_DRY_RUN_OPTION)) {
    497             // In case of dry-run, we replace the KeyStore by a dry-run one.
    498             CLog.w("dry-run detected, we are using a dryrun keystore");
    499             keyStoreClient = new DryRunKeyStore();
    500         }
    501         final List<String> tmpUnconsumedArgs = config.setOptionsFromCommandLineArgs(
    502                 listArgs, keyStoreClient);
    503 
    504         if (unconsumedArgs == null && tmpUnconsumedArgs.size() > 0) {
    505             // (unconsumedArgs == null) is taken as a signal that the caller
    506             // expects all args to
    507             // be processed.
    508             throw new ConfigurationException(String.format(
    509                     "Invalid arguments provided. Unprocessed arguments: %s", tmpUnconsumedArgs));
    510         } else if (unconsumedArgs != null) {
    511             // Return the unprocessed args
    512             unconsumedArgs.addAll(tmpUnconsumedArgs);
    513         }
    514 
    515         return config;
    516     }
    517 
    518     /**
    519      * Creates a {@link Configuration} from the name given in arguments.
    520      * <p/>
    521      * Note will not populate configuration with values from options
    522      *
    523      * @param arrayArgs the full list of command line arguments, including the
    524      *            config name
    525      * @param optionArgsRef an empty list, that will be populated with the
    526      *            option arguments left to be interpreted
    527      * @param keyStoreClient {@link IKeyStoreClient} keystore client to use if
    528      *            any.
    529      * @return An {@link IConfiguration} object representing the configuration
    530      *         that was loaded
    531      * @throws ConfigurationException
    532      */
    533     private IConfiguration internalCreateConfigurationFromArgs(String[] arrayArgs,
    534             List<String> optionArgsRef, IKeyStoreClient keyStoreClient)
    535             throws ConfigurationException {
    536         if (arrayArgs.length == 0) {
    537             throw new ConfigurationException("Configuration to run was not specified");
    538         }
    539         final List<String> listArgs = new ArrayList<>(Arrays.asList(arrayArgs));
    540         // first arg is config name
    541         final String configName = listArgs.remove(0);
    542 
    543         // Steal ConfigurationXmlParser arguments from the command line
    544         final ConfigurationXmlParserSettings parserSettings = new ConfigurationXmlParserSettings();
    545         final ArgsOptionParser templateArgParser = new ArgsOptionParser(parserSettings);
    546         if (keyStoreClient != null) {
    547             templateArgParser.setKeyStore(keyStoreClient);
    548         }
    549         optionArgsRef.addAll(templateArgParser.parseBestEffort(listArgs));
    550         // Check that the same template is not attempted to be loaded twice.
    551         for (String key : parserSettings.templateMap.keySet()) {
    552             if (parserSettings.templateMap.get(key).size() > 1) {
    553                 throw new ConfigurationException(
    554                         String.format("More than one template specified for key '%s'", key));
    555             }
    556         }
    557         Map<String, String> uniqueMap = parserSettings.templateMap.getUniqueMap();
    558         ConfigurationDef configDef = getConfigurationDef(configName, false, uniqueMap);
    559         if (!uniqueMap.isEmpty()) {
    560             // remove the bad ConfigDef from the cache.
    561             for (ConfigId cid : mConfigDefMap.keySet()) {
    562                 if (mConfigDefMap.get(cid) == configDef) {
    563                     CLog.d("Cleaning the cache for this configdef");
    564                     mConfigDefMap.remove(cid);
    565                     break;
    566                 }
    567             }
    568             throw new ConfigurationException(
    569                     String.format("Unused template:map parameters: %s", uniqueMap.toString()));
    570         }
    571         return configDef.createConfiguration();
    572     }
    573 
    574     /**
    575      * {@inheritDoc}
    576      */
    577     @Override
    578     public IGlobalConfiguration createGlobalConfigurationFromArgs(String[] arrayArgs,
    579             List<String> remainingArgs) throws ConfigurationException {
    580         List<String> listArgs = new ArrayList<String>(arrayArgs.length);
    581         IGlobalConfiguration config = internalCreateGlobalConfigurationFromArgs(arrayArgs,
    582                 listArgs);
    583         remainingArgs.addAll(config.setOptionsFromCommandLineArgs(listArgs));
    584 
    585         return config;
    586     }
    587 
    588     /**
    589      * Creates a {@link GlobalConfiguration} from the name given in arguments.
    590      * <p/>
    591      * Note will not populate configuration with values from options
    592      *
    593      * @param arrayArgs the full list of command line arguments, including the config name
    594      * @param optionArgsRef an empty list, that will be populated with the
    595      *            remaining option arguments
    596      * @return a {@link IGlobalConfiguration} created from the args
    597      * @throws ConfigurationException
    598      */
    599     private IGlobalConfiguration internalCreateGlobalConfigurationFromArgs(String[] arrayArgs,
    600             List<String> optionArgsRef) throws ConfigurationException {
    601         if (arrayArgs.length == 0) {
    602             throw new ConfigurationException("Configuration to run was not specified");
    603         }
    604         optionArgsRef.addAll(Arrays.asList(arrayArgs));
    605         // first arg is config name
    606         final String configName = optionArgsRef.remove(0);
    607         ConfigurationDef configDef = getConfigurationDef(configName, true, null);
    608         return configDef.createGlobalConfiguration();
    609     }
    610 
    611     /**
    612      * {@inheritDoc}
    613      */
    614     @Override
    615     public void printHelp(PrintStream out) {
    616         try {
    617             loadAllConfigs(true);
    618         } catch (ConfigurationException e) {
    619             // ignore, should never happen
    620         }
    621         // sort the configs by name before displaying
    622         SortedSet<ConfigurationDef> configDefs = new TreeSet<ConfigurationDef>(
    623                 new ConfigDefComparator());
    624         configDefs.addAll(mConfigDefMap.values());
    625         for (ConfigurationDef def : configDefs) {
    626             out.printf("  %s: %s", def.getName(), def.getDescription());
    627             out.println();
    628         }
    629     }
    630 
    631     /**
    632      * {@inheritDoc}
    633      */
    634     @Override
    635     public List<String> getConfigList() {
    636         return getConfigList(null);
    637     }
    638 
    639     /**
    640      * {@inheritDoc}
    641      */
    642     @Override
    643     public List<String> getConfigList(String subPath) {
    644         return getConfigList(subPath, true);
    645     }
    646 
    647     /** {@inheritDoc} */
    648     @Override
    649     public List<String> getConfigList(String subPath, boolean loadFromEnv) {
    650         Set<String> configNames = getConfigSetFromClasspath(subPath);
    651         if (loadFromEnv) {
    652             // list config on variable path too
    653             configNames.addAll(getConfigNamesFromTestCases(subPath));
    654         }
    655         // sort the configs by name before adding to list
    656         SortedSet<String> configDefs = new TreeSet<String>();
    657         configDefs.addAll(configNames);
    658         List<String> configs = new ArrayList<String>();
    659         configs.addAll(configDefs);
    660         return configs;
    661     }
    662 
    663     /**
    664      * Private helper to get the full set of configurations.
    665      */
    666     private Set<String> getConfigSetFromClasspath(String subPath) {
    667         ClassPathScanner cpScanner = new ClassPathScanner();
    668         return cpScanner.getClassPathEntries(new ConfigClasspathFilter(subPath));
    669     }
    670 
    671     /**
    672      * Helper to get the test config files from test cases directories from build output.
    673      *
    674      * @param subPath where to look for configuration. Can be null.
    675      */
    676     @VisibleForTesting
    677     Set<String> getConfigNamesFromTestCases(String subPath) {
    678         return ConfigurationUtil.getConfigNamesFromDirs(subPath, getExternalTestCasesDirs());
    679     }
    680 
    681     /**
    682      * Loads all configurations found in classpath and test cases directories.
    683      *
    684      * @param discardExceptions true if any ConfigurationException should be ignored.
    685      * @throws ConfigurationException
    686      */
    687     public void loadAllConfigs(boolean discardExceptions) throws ConfigurationException {
    688         ByteArrayOutputStream baos = new ByteArrayOutputStream();
    689         PrintStream ps = new PrintStream(baos);
    690         boolean failed = false;
    691         Set<String> configNames = getConfigSetFromClasspath(null);
    692         // TODO: split the configs into two lists, one from the jar packages and one from test
    693         // cases directories.
    694         configNames.addAll(getConfigNamesFromTestCases(null));
    695         for (String configName : configNames) {
    696             final ConfigId configId = new ConfigId(configName);
    697             try {
    698                 ConfigurationDef configDef = attemptLoad(configId, null);
    699                 mConfigDefMap.put(configId, configDef);
    700             } catch (ConfigurationException e) {
    701                 ps.printf("Failed to load %s: %s", configName, e.getMessage());
    702                 ps.println();
    703                 failed = true;
    704             }
    705         }
    706         if (failed) {
    707             if (discardExceptions) {
    708                 CLog.e("Failure loading configs");
    709                 CLog.e(baos.toString());
    710             } else {
    711                 throw new ConfigurationException(baos.toString());
    712             }
    713         }
    714     }
    715 
    716     /**
    717      * Helper to load a configuration.
    718      */
    719     private ConfigurationDef attemptLoad(ConfigId configId, Map<String, String> templateMap)
    720             throws ConfigurationException {
    721         ConfigurationDef configDef = null;
    722         try {
    723             configDef = getConfigurationDef(configId.name, false, templateMap);
    724             return configDef;
    725         } catch (TemplateResolutionError tre) {
    726             // When a template does not have a default, we try again with known good template
    727             // to make sure file formatting at the very least is fine.
    728             Map<String, String> fakeTemplateMap = new HashMap<String, String>();
    729             if (templateMap != null) {
    730                 fakeTemplateMap.putAll(templateMap);
    731             }
    732             fakeTemplateMap.put(tre.getTemplateKey(), DRY_RUN_TEMPLATE_CONFIG);
    733             // We go recursively in case there are several template to dry run.
    734             return attemptLoad(configId, fakeTemplateMap);
    735         }
    736     }
    737 
    738     /**
    739      * {@inheritDoc}
    740      */
    741     @Override
    742     public void printHelpForConfig(String[] args, boolean importantOnly, PrintStream out) {
    743         try {
    744             IConfiguration config = internalCreateConfigurationFromArgs(args,
    745                     new ArrayList<String>(args.length), null);
    746             config.printCommandUsage(importantOnly, out);
    747         } catch (ConfigurationException e) {
    748             // config must not be specified. Print generic help
    749             printHelp(out);
    750         }
    751     }
    752 
    753     /**
    754      * {@inheritDoc}
    755      */
    756     @Override
    757     public void dumpConfig(String configName, PrintStream out) {
    758         try {
    759             InputStream configStream = getConfigStream(configName);
    760             StreamUtil.copyStreams(configStream, out);
    761         } catch (ConfigurationException e) {
    762             Log.e(LOG_TAG, e);
    763         } catch (IOException e) {
    764             Log.e(LOG_TAG, e);
    765         }
    766     }
    767 
    768     /**
    769      * Return the path prefix of config xml files on classpath
    770      *
    771      * <p>Exposed so unit tests can mock.
    772      *
    773      * @return {@link String} path with trailing /
    774      */
    775     protected String getConfigPrefix() {
    776         return CONFIG_PREFIX;
    777     }
    778 
    779     /**
    780      * Loads an InputStream for given config name
    781      *
    782      * @param name the configuration name to load
    783      * @return a {@link BufferedInputStream} for reading config contents
    784      * @throws ConfigurationException if config could not be found
    785      */
    786     protected BufferedInputStream getConfigStream(String name) throws ConfigurationException {
    787         InputStream configStream = getBundledConfigStream(name);
    788         if (configStream == null) {
    789             // now try to load from file
    790             try {
    791                 configStream = new FileInputStream(name);
    792             } catch (FileNotFoundException e) {
    793                 throw new ConfigurationException(String.format("Could not find configuration '%s'",
    794                         name));
    795             }
    796         }
    797         // buffer input for performance - just in case config file is large
    798         return new BufferedInputStream(configStream);
    799     }
    800 
    801     protected InputStream getBundledConfigStream(String name) {
    802         return getClass()
    803                 .getResourceAsStream(
    804                         String.format("/%s%s%s", getConfigPrefix(), name, CONFIG_SUFFIX));
    805     }
    806 
    807     /**
    808      * Utility method that checks that all configs can be loaded, parsed, and
    809      * all option values set.
    810      * Only exposed so that depending project can validate their configs.
    811      * Should not be exposed in the console.
    812      *
    813      * @throws ConfigurationException if one or more configs failed to load
    814      */
    815     public void loadAndPrintAllConfigs() throws ConfigurationException {
    816         loadAllConfigs(false);
    817         boolean failed = false;
    818         ByteArrayOutputStream baos = new ByteArrayOutputStream();
    819         PrintStream ps = new PrintStream(baos);
    820 
    821         for (ConfigurationDef def : mConfigDefMap.values()) {
    822             try {
    823                 def.createConfiguration().printCommandUsage(false,
    824                         new PrintStream(StreamUtil.nullOutputStream()));
    825             } catch (ConfigurationException e) {
    826                 if (e.getCause() != null &&
    827                         e.getCause() instanceof ClassNotFoundException) {
    828                     ClassNotFoundException cnfe = (ClassNotFoundException) e.getCause();
    829                     String className = cnfe.getLocalizedMessage();
    830                     // Some Cts configs are shipped with Trade Federation, we exclude those from
    831                     // the failure since these packages are not available for loading.
    832                     if (className != null && className.startsWith("com.android.cts.")) {
    833                         CLog.w("Could not confirm %s: %s because not part of Trade Federation "
    834                                 + "packages.", def.getName(), e.getMessage());
    835                         continue;
    836                     }
    837                 } else if (Pattern.matches(CONFIG_ERROR_PATTERN, e.getMessage())) {
    838                     // If options are inside configuration object tag we are able to validate them
    839                     if (!e.getMessage().contains("com.android.") &&
    840                             !e.getMessage().contains("com.google.android.")) {
    841                         // We cannot confirm if an option is indeed missing since a template of
    842                         // option only is possible to avoid repetition in configuration with the
    843                         // same base.
    844                         CLog.w("Could not confirm %s: %s", def.getName(), e.getMessage());
    845                         continue;
    846                     }
    847                 }
    848                 ps.printf("Failed to print %s: %s", def.getName(), e.getMessage());
    849                 ps.println();
    850                 failed = true;
    851             }
    852         }
    853         if (failed) {
    854             throw new ConfigurationException(baos.toString());
    855         }
    856     }
    857 
    858     /**
    859      * Exposed for testing. Return a copy of the Map.
    860      */
    861     protected Map<ConfigId, ConfigurationDef> getMapConfig() {
    862         // We return a copy to ensure it is not modified outside
    863         return new HashMap<ConfigId, ConfigurationDef>(mConfigDefMap);
    864     }
    865 
    866     /** In some particular case, we need to clear the map. */
    867     @VisibleForTesting
    868     public void clearMapConfig() {
    869         mConfigDefMap.clear();
    870     }
    871 
    872     /** Reorder the args so that template:map args are all moved to the front. */
    873     @VisibleForTesting
    874     protected String[] reorderArgs(String[] args) {
    875         List<String> nonTemplateArgs = new ArrayList<String>();
    876         List<String> reorderedArgs = new ArrayList<String>();
    877         String[] reorderedArgsArray = new String[args.length];
    878         String arg;
    879 
    880         // First arg is the config.
    881         if (args.length > 0) {
    882             reorderedArgs.add(args[0]);
    883         }
    884 
    885         // Split out the template and non-template args so we can add
    886         // non-template args at the end while maintaining their order.
    887         for (int i = 1; i < args.length; i++) {
    888             arg = args[i];
    889             if (arg.equals("--template:map")) {
    890                 // We need to account for these two types of template:map args.
    891                 // --template:map tm=tm1
    892                 // --template:map tm tm1
    893                 reorderedArgs.add(arg);
    894                 for (int j = i + 1; j < args.length; j++) {
    895                     if (args[j].startsWith("-")) {
    896                         break;
    897                     } else {
    898                         reorderedArgs.add(args[j]);
    899                         i++;
    900                     }
    901                 }
    902             } else {
    903                 nonTemplateArgs.add(arg);
    904             }
    905         }
    906         reorderedArgs.addAll(nonTemplateArgs);
    907         return reorderedArgs.toArray(reorderedArgsArray);
    908     }
    909 }
    910