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);
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)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          */
loadConfiguration( String name, ConfigurationDef def, String deviceTagObject, Map<String, String> templateMap)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          */
trackConfig(String name, ConfigurationDef def)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          */
isTrackableConfig(String name)422         protected boolean isTrackableConfig(String name) {
423             return !isBundledConfig(name);
424         }
425 
426         /**
427          * {@inheritDoc}
428          */
429         @Override
isGlobalConfig()430         public boolean isGlobalConfig() {
431             return mIsGlobalConfig;
432         }
433 
434     }
435 
ConfigurationFactory()436     protected ConfigurationFactory() {
437         mConfigDefMap = new Hashtable<ConfigId, ConfigurationDef>();
438     }
439 
440     /**
441      * Get the singleton {@link IConfigurationFactory} instance.
442      */
getInstance()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      */
getConfigurationDef( String name, boolean isGlobal, Map<String, String> templateMap)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
createConfigurationFromArgs(String[] arrayArgs)468     public IConfiguration createConfigurationFromArgs(String[] arrayArgs)
469             throws ConfigurationException {
470         return createConfigurationFromArgs(arrayArgs, null);
471     }
472 
473     /**
474      * {@inheritDoc}
475      */
476     @Override
createConfigurationFromArgs(String[] arrayArgs, List<String> unconsumedArgs)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
createConfigurationFromArgs(String[] arrayArgs, List<String> unconsumedArgs, IKeyStoreClient keyStoreClient)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      */
internalCreateConfigurationFromArgs(String[] arrayArgs, List<String> optionArgsRef, IKeyStoreClient keyStoreClient)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
createGlobalConfigurationFromArgs(String[] arrayArgs, List<String> remainingArgs)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      */
internalCreateGlobalConfigurationFromArgs(String[] arrayArgs, List<String> optionArgsRef)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
printHelp(PrintStream out)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
getConfigList()635     public List<String> getConfigList() {
636         return getConfigList(null);
637     }
638 
639     /**
640      * {@inheritDoc}
641      */
642     @Override
getConfigList(String subPath)643     public List<String> getConfigList(String subPath) {
644         return getConfigList(subPath, true);
645     }
646 
647     /** {@inheritDoc} */
648     @Override
getConfigList(String subPath, boolean loadFromEnv)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      */
getConfigSetFromClasspath(String subPath)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
getConfigNamesFromTestCases(String subPath)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      */
loadAllConfigs(boolean discardExceptions)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      */
attemptLoad(ConfigId configId, Map<String, String> templateMap)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
printHelpForConfig(String[] args, boolean importantOnly, PrintStream out)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
dumpConfig(String configName, PrintStream out)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      */
getConfigPrefix()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      */
getConfigStream(String name)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 
getBundledConfigStream(String name)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      */
loadAndPrintAllConfigs()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      */
getMapConfig()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
clearMapConfig()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
reorderArgs(String[] args)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