• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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