1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.tradefed.testtype.suite;
17 
18 import com.android.annotations.VisibleForTesting;
19 import com.android.tradefed.config.ConfigurationDescriptor;
20 import com.android.tradefed.config.ConfigurationException;
21 import com.android.tradefed.config.ConfigurationFactory;
22 import com.android.tradefed.config.ConfigurationUtil;
23 import com.android.tradefed.config.IConfiguration;
24 import com.android.tradefed.config.IConfigurationFactory;
25 import com.android.tradefed.config.IDeviceConfiguration;
26 import com.android.tradefed.config.OptionDef;
27 import com.android.tradefed.invoker.IInvocationContext;
28 import com.android.tradefed.log.LogUtil.CLog;
29 import com.android.tradefed.targetprep.ITargetPreparer;
30 import com.android.tradefed.testtype.IAbi;
31 import com.android.tradefed.testtype.IAbiReceiver;
32 import com.android.tradefed.testtype.IRemoteTest;
33 import com.android.tradefed.testtype.ITestFileFilterReceiver;
34 import com.android.tradefed.testtype.ITestFilterReceiver;
35 import com.android.tradefed.testtype.suite.params.IModuleParameter;
36 import com.android.tradefed.testtype.suite.params.MainlineModuleHandler;
37 import com.android.tradefed.testtype.suite.params.ModuleParameters;
38 import com.android.tradefed.testtype.suite.params.ModuleParametersHelper;
39 import com.android.tradefed.testtype.suite.params.NegativeHandler;
40 import com.android.tradefed.testtype.suite.params.NotMultiAbiHandler;
41 import com.android.tradefed.util.AbiUtils;
42 import com.android.tradefed.util.FileUtil;
43 import com.android.tradefed.util.StreamUtil;
44 
45 import com.google.common.base.Strings;
46 import com.google.common.net.UrlEscapers;
47 
48 import java.io.File;
49 import java.io.FilenameFilter;
50 import java.io.IOException;
51 import java.io.PrintWriter;
52 import java.util.ArrayList;
53 import java.util.Arrays;
54 import java.util.Collection;
55 import java.util.Collections;
56 import java.util.HashMap;
57 import java.util.HashSet;
58 import java.util.LinkedHashMap;
59 import java.util.List;
60 import java.util.Map;
61 import java.util.Set;
62 import java.util.regex.Matcher;
63 import java.util.regex.Pattern;
64 
65 /**
66  * Retrieves Compatibility test module definitions from the repository. TODO: Add the expansion of
67  * suite when loading a module.
68  */
69 public class SuiteModuleLoader {
70 
71     public static final String CONFIG_EXT = ".config";
72     private Map<String, List<OptionDef>> mTestOrPreparerOptions = new HashMap<>();
73     private Map<String, List<OptionDef>> mModuleOptions = new HashMap<>();
74     private boolean mIncludeAll;
75     private Map<String, List<SuiteTestFilter>> mIncludeFilters = new HashMap<>();
76     private Map<String, List<SuiteTestFilter>> mExcludeFilters = new HashMap<>();
77     private IConfigurationFactory mConfigFactory = ConfigurationFactory.getInstance();
78     private IInvocationContext mContext;
79 
80     private boolean mAllowParameterizedModules = false;
81     private boolean mAllowMainlineParameterizedModules = false;
82     private boolean mAllowOptionalParameterizedModules = false;
83     private ModuleParameters mForcedModuleParameter = null;
84     private Set<ModuleParameters> mExcludedModuleParameters = new HashSet<>();
85     // Check the mainline parameter configured in a test config must end with .apk, .apks, or .apex.
86     private static final Set<String> MAINLINE_PARAMETERS_TO_VALIDATE =
87             new HashSet<>(Arrays.asList(".apk", ".apks", ".apex"));
88 
89     /**
90      * Ctor for the SuiteModuleLoader.
91      *
92      * @param includeFilters The formatted and parsed include filters.
93      * @param excludeFilters The formatted and parsed exclude filters.
94      * @param testArgs the list of test ({@link IRemoteTest}) arguments.
95      * @param moduleArgs the list of module arguments.
96      */
SuiteModuleLoader( Map<String, List<SuiteTestFilter>> includeFilters, Map<String, List<SuiteTestFilter>> excludeFilters, List<String> testArgs, List<String> moduleArgs)97     public SuiteModuleLoader(
98             Map<String, List<SuiteTestFilter>> includeFilters,
99             Map<String, List<SuiteTestFilter>> excludeFilters,
100             List<String> testArgs,
101             List<String> moduleArgs) {
102         mIncludeAll = includeFilters.isEmpty();
103         mIncludeFilters = includeFilters;
104         mExcludeFilters = excludeFilters;
105 
106         parseArgs(testArgs, mTestOrPreparerOptions);
107         parseArgs(moduleArgs, mModuleOptions);
108     }
109 
setInvocationContext(IInvocationContext context)110     public final void setInvocationContext(IInvocationContext context) {
111         mContext = context;
112     }
113 
114     /** Sets whether or not to allow parameterized modules. */
setParameterizedModules(boolean allowed)115     public final void setParameterizedModules(boolean allowed) {
116         mAllowParameterizedModules = allowed;
117     }
118 
119     /** Sets whether or not to allow parameterized mainline modules. */
setMainlineParameterizedModules(boolean allowed)120     public final void setMainlineParameterizedModules(boolean allowed) {
121         mAllowMainlineParameterizedModules = allowed;
122     }
123 
124     /** Sets whether or not to allow optional parameterized modules. */
setOptionalParameterizedModules(boolean allowed)125     public final void setOptionalParameterizedModules(boolean allowed) {
126         mAllowOptionalParameterizedModules = allowed;
127     }
128 
129     /** Sets the only {@link ModuleParameters} type that should be run. */
setModuleParameter(ModuleParameters param)130     public final void setModuleParameter(ModuleParameters param) {
131         mForcedModuleParameter = param;
132     }
133 
134     /** Sets the set of {@link ModuleParameters} that should not be considered at all. */
setExcludedModuleParameters(Set<ModuleParameters> excludedParams)135     public final void setExcludedModuleParameters(Set<ModuleParameters> excludedParams) {
136         mExcludedModuleParameters = excludedParams;
137     }
138 
139     /** Main loading of configurations, looking into the specified files */
loadConfigsFromSpecifiedPaths( List<File> listConfigFiles, Set<IAbi> abis, String suiteTag)140     public LinkedHashMap<String, IConfiguration> loadConfigsFromSpecifiedPaths(
141             List<File> listConfigFiles,
142             Set<IAbi> abis,
143             String suiteTag) {
144         LinkedHashMap<String, IConfiguration> toRun = new LinkedHashMap<>();
145         for (File configFile : listConfigFiles) {
146             toRun.putAll(
147                     loadOneConfig(
148                             configFile.getName(), configFile.getAbsolutePath(), abis, suiteTag));
149         }
150         return toRun;
151     }
152 
153     /** Main loading of configurations, looking into a folder */
loadConfigsFromDirectory( List<File> testsDirs, Set<IAbi> abis, String suitePrefix, String suiteTag, List<String> patterns)154     public LinkedHashMap<String, IConfiguration> loadConfigsFromDirectory(
155             List<File> testsDirs,
156             Set<IAbi> abis,
157             String suitePrefix,
158             String suiteTag,
159             List<String> patterns) {
160         LinkedHashMap<String, IConfiguration> toRun = new LinkedHashMap<>();
161         List<File> listConfigFiles = new ArrayList<>();
162         listConfigFiles.addAll(
163                 ConfigurationUtil.getConfigNamesFileFromDirs(suitePrefix, testsDirs, patterns));
164         // Ensure stable initial order of configurations.
165         Collections.sort(listConfigFiles);
166         toRun.putAll(loadConfigsFromSpecifiedPaths(listConfigFiles, abis, suiteTag));
167         return toRun;
168     }
169 
170     /**
171      * Main loading of configurations, looking into the resources on the classpath. (TF configs for
172      * example).
173      */
loadConfigsFromJars( Set<IAbi> abis, String suitePrefix, String suiteTag)174     public LinkedHashMap<String, IConfiguration> loadConfigsFromJars(
175             Set<IAbi> abis, String suitePrefix, String suiteTag) {
176         LinkedHashMap<String, IConfiguration> toRun = new LinkedHashMap<>();
177 
178         IConfigurationFactory configFactory = ConfigurationFactory.getInstance();
179         List<String> configs = configFactory.getConfigList(suitePrefix, false);
180         // Sort configs to ensure they are always evaluated and added in the same order.
181         Collections.sort(configs);
182         toRun.putAll(loadTfConfigsFromSpecifiedPaths(configs, abis, suiteTag));
183         return toRun;
184     }
185 
186     /** Main loading of configurations, looking into the specified resources on the classpath. */
loadTfConfigsFromSpecifiedPaths( List<String> configs, Set<IAbi> abis, String suiteTag)187     public LinkedHashMap<String, IConfiguration> loadTfConfigsFromSpecifiedPaths(
188         List<String> configs,
189         Set<IAbi> abis,
190         String suiteTag) {
191         LinkedHashMap<String, IConfiguration> toRun = new LinkedHashMap<>();
192         for (String configName : configs) {
193             toRun.putAll(loadOneConfig(configName, configName, abis, suiteTag));
194         }
195         return toRun;
196     }
197 
198     /**
199      * Pass the filters to the {@link IRemoteTest}. Default behavior is to ignore if the IRemoteTest
200      * does not implements {@link ITestFileFilterReceiver}. This can be overriden to create a more
201      * restrictive behavior.
202      *
203      * @param test The {@link IRemoteTest} that is being considered.
204      * @param abi The Abi we are currently working on.
205      * @param moduleId The id of the module (usually abi + module name).
206      * @param includeFilters The formatted and parsed include filters.
207      * @param excludeFilters The formatted and parsed exclude filters.
208      */
addFiltersToTest( IRemoteTest test, IAbi abi, String moduleId, Map<String, List<SuiteTestFilter>> includeFilters, Map<String, List<SuiteTestFilter>> excludeFilters)209     public void addFiltersToTest(
210             IRemoteTest test,
211             IAbi abi,
212             String moduleId,
213             Map<String, List<SuiteTestFilter>> includeFilters,
214             Map<String, List<SuiteTestFilter>> excludeFilters) {
215         if (!(test instanceof ITestFilterReceiver)) {
216             CLog.e("Test in module %s does not implement ITestFilterReceiver.", moduleId);
217             return;
218         }
219         List<SuiteTestFilter> mdIncludes = getFilterList(includeFilters, moduleId);
220         List<SuiteTestFilter> mdExcludes = getFilterList(excludeFilters, moduleId);
221         if (!mdIncludes.isEmpty()) {
222             addTestIncludes((ITestFilterReceiver) test, mdIncludes, moduleId);
223         }
224         if (!mdExcludes.isEmpty()) {
225             addTestExcludes((ITestFilterReceiver) test, mdExcludes, moduleId);
226         }
227     }
228 
229     /**
230      * Load a single config location (file or on TF classpath). It can results in several {@link
231      * IConfiguration}. If a single configuration get expanded in different ways.
232      *
233      * @param configName The actual config name only. (no path)
234      * @param configFullName The fully qualified config name. (with path, if any).
235      * @param abis The set of all abis that needs to run.
236      * @param suiteTag the Tag of the suite aimed to be run.
237      * @return A map of loaded configuration.
238      */
loadOneConfig( String configName, String configFullName, Set<IAbi> abis, String suiteTag)239     private LinkedHashMap<String, IConfiguration> loadOneConfig(
240             String configName, String configFullName, Set<IAbi> abis, String suiteTag) {
241         LinkedHashMap<String, IConfiguration> toRun = new LinkedHashMap<>();
242         final String name = configName.replace(CONFIG_EXT, "");
243         final String[] pathArg = new String[] {configFullName};
244         try {
245             boolean primaryAbi = true;
246             boolean shouldCreateMultiAbi = true;
247             // If a particular parameter was requested to be run, find it.
248             IModuleParameter mForcedParameter = null;
249             if (mForcedModuleParameter != null) {
250                 mForcedParameter =
251                         ModuleParametersHelper.getParameterHandler(
252                                 mForcedModuleParameter, /* optionalParams */
253                                 mAllowOptionalParameterizedModules);
254             }
255 
256             // Invokes parser to process the test module config file
257             // Need to generate a different config for each ABI as we cannot guarantee the
258             // configs are idempotent. This however means we parse the same file multiple times
259             for (IAbi abi : abis) {
260                 // Only enable the primary abi filtering when switching to the parameterized mode
261                 if (mAllowParameterizedModules && !primaryAbi && !shouldCreateMultiAbi) {
262                     continue;
263                 }
264                 String baseId = AbiUtils.createId(abi.getName(), name);
265                 IConfiguration config = null;
266                 try {
267                     config = mConfigFactory.createConfigurationFromArgs(pathArg);
268                 } catch (ConfigurationException e) {
269                     // If the module should not have been running in the first place, give it a
270                     // pass on the configuration failure.
271                     if (!shouldRunModule(baseId)) {
272                         primaryAbi = false;
273                         // If the module should not run tests based on the state of filters,
274                         // skip this name/abi combination.
275                         continue;
276                     }
277                     throw e;
278                 }
279 
280                 // If a suiteTag is used, we load with it.
281                 if (!Strings.isNullOrEmpty(suiteTag)
282                         && !config.getConfigurationDescription()
283                                 .getSuiteTags()
284                                 .contains(suiteTag)) {
285                     // Do not print here, it could leave several hundred lines of logs.
286                     continue;
287                 }
288 
289                 boolean skipCreatingBaseConfig = false;
290                 List<IModuleParameter> params = null;
291                 List<String> mainlineParams = new ArrayList<>();
292                 try {
293                     params = getModuleParameters(name, config);
294                     mainlineParams = getMainlineModuleParameters(config);
295                 } catch (ConfigurationException e) {
296                     // If the module should not have been running in the first place, give it a
297                     // pass on the configuration failure.
298                     if (!shouldRunModule(baseId)) {
299                         primaryAbi = false;
300                         // If the module should not run tests based on the state of filters,
301                         // skip this name/abi combination.
302                         continue;
303                     }
304                     throw e;
305                 }
306 
307                 // Handle parameterized modules if enabled.
308                 if (mAllowParameterizedModules) {
309 
310                     if (params.isEmpty()
311                             && mForcedParameter != null
312                             && !(mForcedParameter instanceof NegativeHandler)) {
313                         // If the AndroidTest.xml doesn't specify any parameter but we forced a
314                         // parameter like 'instant' to execute. In this case we don't create the
315                         // standard module.
316                         continue;
317                     }
318 
319                     shouldCreateMultiAbi = shouldCreateMultiAbiForBase(params);
320 
321                     // If we find any parameterized combination.
322                     for (IModuleParameter param : params) {
323                         if (param instanceof NegativeHandler) {
324                             if (mForcedParameter != null
325                                     && !param.getClass().equals(mForcedParameter.getClass())) {
326                                 skipCreatingBaseConfig = true;
327                             }
328                             continue;
329                         }
330                         if (mForcedParameter != null) {
331                             // When a particular parameter is forced, only create it not the others
332                             if (param.getClass().equals(mForcedParameter.getClass())) {
333                                 skipCreatingBaseConfig = true;
334                             } else {
335                                 continue;
336                             }
337                         }
338                         // Only create primary abi of parameterized modules
339                         if (!primaryAbi) {
340                             continue;
341                         }
342                         String fullId =
343                                 String.format("%s[%s]", baseId, param.getParameterIdentifier());
344                         if (shouldRunParameterized(baseId, fullId, mForcedParameter)) {
345                             IConfiguration paramConfig =
346                                     mConfigFactory.createConfigurationFromArgs(pathArg);
347                             // Mark the parameter in the metadata
348                             paramConfig
349                                     .getConfigurationDescription()
350                                     .addMetadata(
351                                             ConfigurationDescriptor.ACTIVE_PARAMETER_KEY,
352                                             param.getParameterIdentifier());
353                             param.addParameterSpecificConfig(paramConfig);
354                             setUpConfig(name, baseId, fullId, paramConfig, abi);
355                             param.applySetup(paramConfig);
356                             toRun.put(fullId, paramConfig);
357                         }
358                     }
359                 }
360 
361                 if (mAllowMainlineParameterizedModules) {
362                     // If no options defined in a test config, skip generating.
363                     // TODO(easoncylee) This is still under discussion.
364                     if (mainlineParams.isEmpty()) {
365                         continue;
366                     }
367                     // If we find any parameterized combination for mainline modules.
368                     for (String param : mainlineParams) {
369                         String fullId = String.format("%s[%s]", baseId, param);
370                         if (!shouldRunParameterized(baseId, fullId, null)) {
371                             continue;
372                         }
373                         // Create mainline handler for each defined mainline parameter.
374                         MainlineModuleHandler handler =
375                                 new MainlineModuleHandler(
376                                         param,
377                                         abi,
378                                         mContext
379                                 );
380                         skipCreatingBaseConfig = true;
381                         IConfiguration paramConfig =
382                                 mConfigFactory.createConfigurationFromArgs(pathArg);
383                         paramConfig
384                                 .getConfigurationDescription()
385                                 .addMetadata(
386                                         ITestSuite.ACTIVE_MAINLINE_PARAMETER_KEY,
387                                         param);
388                         setUpConfig(name, baseId, fullId, paramConfig, abi);
389                         handler.applySetup(paramConfig);
390                         toRun.put(fullId, paramConfig);
391                     }
392                 }
393 
394                 primaryAbi = false;
395                 // If a parameterized form of the module was forced, we don't create the standard
396                 // version of it.
397                 if (skipCreatingBaseConfig) {
398                     continue;
399                 }
400                 if (shouldRunModule(baseId)) {
401                     // Always add the base regular configuration to the execution.
402                     setUpConfig(name, baseId, baseId, config, abi);
403                     toRun.put(baseId, config);
404                 }
405             }
406         } catch (ConfigurationException e) {
407             throw new RuntimeException(
408                     String.format(
409                             "Error parsing configuration: %s: '%s'",
410                             configFullName, e.getMessage()),
411                     e);
412         }
413 
414         return toRun;
415     }
416 
417     /** @return the {@link Set} of modules whose name contains the given pattern. */
getModuleNamesMatching( File directory, String suitePrefix, String pattern)418     public static Set<File> getModuleNamesMatching(
419             File directory, String suitePrefix, String pattern) {
420         List<File> extraTestCasesDirs = Arrays.asList(directory);
421         List<String> patterns = new ArrayList<>();
422         patterns.add(pattern);
423         Set<File> modules =
424                 ConfigurationUtil.getConfigNamesFileFromDirs(
425                         suitePrefix, extraTestCasesDirs, patterns);
426         return modules;
427     }
428 
429     /**
430      * Utility method that allows to parse and create a structure with the option filters.
431      *
432      * @param stringFilters The original option filters format.
433      * @param filters The filters parsed from the string format.
434      * @param abis The Abis to consider in the filtering.
435      */
addFilters( Set<String> stringFilters, Map<String, List<SuiteTestFilter>> filters, Set<IAbi> abis)436     public static void addFilters(
437             Set<String> stringFilters, Map<String, List<SuiteTestFilter>> filters, Set<IAbi> abis) {
438         for (String filterString : stringFilters) {
439             SuiteTestFilter filter = SuiteTestFilter.createFrom(filterString);
440             String abi = filter.getAbi();
441             if (abi == null) {
442                 for (IAbi a : abis) {
443                     addFilter(a.getName(), filter, filters);
444                 }
445             } else {
446                 addFilter(abi, filter, filters);
447             }
448         }
449     }
450 
addFilter( String abi, SuiteTestFilter filter, Map<String, List<SuiteTestFilter>> filters)451     private static void addFilter(
452             String abi, SuiteTestFilter filter, Map<String, List<SuiteTestFilter>> filters) {
453         getFilterList(filters, AbiUtils.createId(abi, filter.getName())).add(filter);
454     }
455 
getFilterList( Map<String, List<SuiteTestFilter>> filters, String id)456     private static List<SuiteTestFilter> getFilterList(
457             Map<String, List<SuiteTestFilter>> filters, String id) {
458         List<SuiteTestFilter> fs = filters.get(id);
459         if (fs == null) {
460             fs = new ArrayList<>();
461             filters.put(id, fs);
462         }
463         return fs;
464     }
465 
shouldRunModule(String moduleId)466     private boolean shouldRunModule(String moduleId) {
467         List<SuiteTestFilter> mdIncludes = getFilterList(mIncludeFilters, moduleId);
468         List<SuiteTestFilter> mdExcludes = getFilterList(mExcludeFilters, moduleId);
469         // if including all modules or includes exist for this module, and there are not excludes
470         // for the entire module, this module should be run.
471         return (mIncludeAll || !mdIncludes.isEmpty()) && !containsModuleExclude(mdExcludes);
472     }
473 
474     /**
475      * Except if the parameterized module is explicitly excluded, including the base module result
476      * in including its parameterization variant.
477      */
shouldRunParameterized( String baseModuleId, String parameterModuleId, IModuleParameter forcedModuleParameter)478     private boolean shouldRunParameterized(
479             String baseModuleId, String parameterModuleId, IModuleParameter forcedModuleParameter) {
480         // Explicitly excluded
481         List<SuiteTestFilter> excluded = getFilterList(mExcludeFilters, parameterModuleId);
482         if (containsModuleExclude(excluded)) {
483             return false;
484         }
485 
486         // Implicitly included due to forced parameter
487         if (forcedModuleParameter != null) {
488             List<SuiteTestFilter> baseInclude = getFilterList(mIncludeFilters, baseModuleId);
489             if (!baseInclude.isEmpty()) {
490                 return true;
491             }
492         }
493         // Explicitly included
494         List<SuiteTestFilter> included = getFilterList(mIncludeFilters, parameterModuleId);
495         if (mIncludeAll || !included.isEmpty()) {
496             return true;
497         }
498         return false;
499     }
500 
addTestIncludes( ITestFilterReceiver test, List<SuiteTestFilter> includes, String moduleId)501     private void addTestIncludes(
502             ITestFilterReceiver test, List<SuiteTestFilter> includes, String moduleId) {
503         if (test instanceof ITestFileFilterReceiver) {
504             String escapedFileName = escapeFilterFileName(moduleId);
505             File includeFile = createFilterFile(escapedFileName, ".include", includes);
506             ((ITestFileFilterReceiver) test).setIncludeTestFile(includeFile);
507         } else {
508             // add test includes one at a time
509             for (SuiteTestFilter include : includes) {
510                 String filterTestName = include.getTest();
511                 if (filterTestName != null) {
512                     test.addIncludeFilter(filterTestName);
513                 }
514             }
515         }
516     }
517 
addTestExcludes( ITestFilterReceiver test, List<SuiteTestFilter> excludes, String moduleId)518     private void addTestExcludes(
519             ITestFilterReceiver test, List<SuiteTestFilter> excludes, String moduleId) {
520         if (test instanceof ITestFileFilterReceiver) {
521             String escapedFileName = escapeFilterFileName(moduleId);
522             File excludeFile = createFilterFile(escapedFileName, ".exclude", excludes);
523             ((ITestFileFilterReceiver) test).setExcludeTestFile(excludeFile);
524         } else {
525             // add test excludes one at a time
526             for (SuiteTestFilter exclude : excludes) {
527                 test.addExcludeFilter(exclude.getTest());
528             }
529         }
530     }
531 
532     /** module id can contain special characters, avoid them for file names. */
escapeFilterFileName(String moduleId)533     private String escapeFilterFileName(String moduleId) {
534         String escaped = UrlEscapers.urlPathSegmentEscaper().escape(moduleId);
535         return escaped;
536     }
537 
createFilterFile(String prefix, String suffix, List<SuiteTestFilter> filters)538     private File createFilterFile(String prefix, String suffix, List<SuiteTestFilter> filters) {
539         File filterFile = null;
540         PrintWriter out = null;
541         try {
542             filterFile = FileUtil.createTempFile(prefix, suffix);
543             out = new PrintWriter(filterFile);
544             for (SuiteTestFilter filter : filters) {
545                 String filterTest = filter.getTest();
546                 if (filterTest != null) {
547                     out.println(filterTest);
548                 }
549             }
550             out.flush();
551         } catch (IOException e) {
552             throw new RuntimeException("Failed to create filter file");
553         } finally {
554             StreamUtil.close(out);
555         }
556         filterFile.deleteOnExit();
557         return filterFile;
558     }
559 
560     /** Returns true iff one or more test filters in excludes apply to the entire module. */
containsModuleExclude(Collection<SuiteTestFilter> excludes)561     private boolean containsModuleExclude(Collection<SuiteTestFilter> excludes) {
562         for (SuiteTestFilter exclude : excludes) {
563             if (exclude.getTest() == null) {
564                 return true;
565             }
566         }
567         return false;
568     }
569 
570     /** A {@link FilenameFilter} to find all the config files in a directory. */
571     public static class ConfigFilter implements FilenameFilter {
572 
573         /** {@inheritDoc} */
574         @Override
accept(File dir, String name)575         public boolean accept(File dir, String name) {
576             return name.endsWith(CONFIG_EXT);
577         }
578     }
579 
580     /**
581      * Parse a list of args formatted as expected into {@link OptionDef} to be injected to module
582      * configurations.
583      *
584      * <p>Format: <module name / module id / class runner>:<option name>:[<arg-key>:=]<arg-value>
585      */
parseArgs(List<String> args, Map<String, List<OptionDef>> moduleOptions)586     private void parseArgs(List<String> args, Map<String, List<OptionDef>> moduleOptions) {
587         for (String arg : args) {
588             int moduleSep = arg.indexOf(":");
589             if (moduleSep == -1) {
590                 throw new RuntimeException("Expected delimiter ':' for module or class.");
591             }
592             String moduleName = arg.substring(0, moduleSep);
593             String remainder = arg.substring(moduleSep + 1);
594             List<OptionDef> listOption = moduleOptions.get(moduleName);
595             if (listOption == null) {
596                 listOption = new ArrayList<>();
597                 moduleOptions.put(moduleName, listOption);
598             }
599             int optionNameSep = remainder.indexOf(":");
600             if (optionNameSep == -1) {
601                 throw new RuntimeException(
602                         "Expected delimiter ':' between option name and values.");
603             }
604             String optionName = remainder.substring(0, optionNameSep);
605             Pattern pattern = Pattern.compile("\\{(.*)\\}(.*)");
606             Matcher match = pattern.matcher(optionName);
607             if (match.find()) {
608                 String alias = match.group(1);
609                 String name = match.group(2);
610                 optionName = alias + ":" + name;
611             }
612             String optionValueString = remainder.substring(optionNameSep + 1);
613             // TODO: See if QuotationTokenizer can be improved for multi-character delimiter.
614             // or change the delimiter to a single char.
615             String[] tokens = optionValueString.split(":=", 2);
616             OptionDef option = null;
617             if (tokens.length == 1) {
618                 option = new OptionDef(optionName, tokens[0], moduleName);
619             } else if (tokens.length == 2) {
620                 option = new OptionDef(optionName, tokens[0], tokens[1], moduleName);
621             }
622             listOption.add(option);
623         }
624     }
625 
626     /** Gets the list of {@link IModuleParameter}s associated with a module. */
getModuleParameters(String moduleName, IConfiguration config)627     private List<IModuleParameter> getModuleParameters(String moduleName, IConfiguration config)
628             throws ConfigurationException {
629         List<IModuleParameter> params = new ArrayList<>();
630         Set<String> processedParameterArgs = new HashSet<>();
631         // Track family of the parameters to make sure we have no duplicate.
632         Map<String, ModuleParameters> duplicateModule = new LinkedHashMap<>();
633 
634         List<String> parameters =
635                 config.getConfigurationDescription().getMetaData(ITestSuite.PARAMETER_KEY);
636         if (parameters == null || parameters.isEmpty()) {
637             return params;
638         }
639         for (String p : parameters) {
640             if (!processedParameterArgs.add(p)) {
641                 // Avoid processing the same parameter twice
642                 continue;
643             }
644             ModuleParameters suiteParam = ModuleParameters.valueOf(p.toUpperCase());
645             String family = suiteParam.getFamily();
646             if (duplicateModule.containsKey(family)) {
647                 // Duplicate family members are not accepted.
648                 throw new ConfigurationException(
649                         String.format(
650                                 "Module %s is declaring parameter: "
651                                         + "%s and %s when only one expected.",
652                                 moduleName, suiteParam, duplicateModule.get(family)));
653             } else {
654                 duplicateModule.put(suiteParam.getFamily(), suiteParam);
655             }
656             // Do not consider the excluded parameterization dimension
657             if (mExcludedModuleParameters.contains(suiteParam)) {
658                 CLog.d("'%s' was excluded via exclude-module-parameters.", moduleName);
659                 continue;
660             }
661             IModuleParameter handler =
662                     ModuleParametersHelper.getParameterHandler(
663                             suiteParam, /* optionalParams */ mAllowOptionalParameterizedModules);
664             if (handler != null) {
665                 params.add(handler);
666             }
667         }
668         return params;
669     }
670 
671     /** Gets the list of parameterized mainline modules associated with a module. */
672     @VisibleForTesting
getMainlineModuleParameters(IConfiguration config)673     List<String> getMainlineModuleParameters(IConfiguration config)
674             throws ConfigurationException {
675         List<String> params = new ArrayList<>();
676 
677         List<String> parameters =
678                 config.getConfigurationDescription().getMetaData(ITestSuite.MAINLINE_PARAMETER_KEY);
679         if (parameters == null || parameters.isEmpty()) {
680             return params;
681         }
682 
683         return new ArrayList<>(dedupMainlineParameters(parameters, config.getName()));
684     }
685 
686     /**
687      * De-duplicate the given mainline parameters.
688      *
689      * @param parameters The list of given mainline parameters.
690      * @param configName The test configuration name.
691      * @return The de-duplicated mainline modules list.
692      */
693     @VisibleForTesting
dedupMainlineParameters(List<String> parameters, String configName)694     Set<String> dedupMainlineParameters(List<String> parameters, String configName)
695             throws ConfigurationException {
696         Set<String> results = new HashSet<>();
697         for (String param : parameters) {
698             if (!isValidMainlineParam(param)) {
699                 throw new ConfigurationException(
700                         String.format(
701                                 "Illegal mainline module parameter: \"%s\" configured in the " +
702                                 "test config: %s. Parameter must end with .apk/.apex/.apks and " +
703                                 "have no any spaces configured.", param, configName)
704                 );
705             }
706             if (!isInAlphabeticalOrder(param)) {
707                 throw new ConfigurationException(
708                         String.format(
709                                 "Illegal mainline module parameter: \"%s\" configured in the " +
710                                 "test config: %s. Parameter must be configured in alphabetical " +
711                                 "order or with no duplicated modules.", param, configName)
712                 );
713             }
714             results.add(param);
715         }
716         return results;
717     }
718 
719     /** Whether a mainline parameter configured in a test config is in alphabetical order or not. */
720     @VisibleForTesting
isInAlphabeticalOrder(String param)721     boolean isInAlphabeticalOrder(String param) {
722         String previousString = "";
723         for (String currentString : param.split(String.format("\\+"))) {
724             // This is to check if the parameter is in alphabetical order or duplicated.
725             if (currentString.compareTo(previousString) <= 0) {
726                 return false;
727             }
728             previousString = currentString;
729         }
730         return true;
731     }
732 
733     /** Whether the mainline parameter configured in the test config is valid or not. */
734     @VisibleForTesting
isValidMainlineParam(String param)735     boolean isValidMainlineParam(String param) {
736         if (param.contains(" ")) {
737             return false;
738         }
739         for (String m : param.split(String.format("\\+"))) {
740             if (!MAINLINE_PARAMETERS_TO_VALIDATE.stream().anyMatch(entry -> m.endsWith(entry))) {
741                 return false;
742             }
743         }
744         return true;
745     }
746 
747     /**
748      * Setup the options for the module configuration.
749      *
750      * @param name The base name of the module
751      * @param id The base id name of the module.
752      * @param fullId The full id of the module (usually abi + module name + parameters)
753      * @param config The module configuration.
754      * @param abi The abi of the module.
755      * @throws ConfigurationException
756      */
setUpConfig(String name, String id, String fullId, IConfiguration config, IAbi abi)757     private void setUpConfig(String name, String id, String fullId, IConfiguration config, IAbi abi)
758             throws ConfigurationException {
759         List<OptionDef> optionsToInject = new ArrayList<>();
760         if (mModuleOptions.containsKey(name)) {
761             optionsToInject.addAll(mModuleOptions.get(name));
762         }
763         if (mModuleOptions.containsKey(id)) {
764             optionsToInject.addAll(mModuleOptions.get(id));
765         }
766         if (mModuleOptions.containsKey(fullId)) {
767             optionsToInject.addAll(mModuleOptions.get(fullId));
768         }
769         config.injectOptionValues(optionsToInject);
770 
771         // Set target preparers
772         for (IDeviceConfiguration holder : config.getDeviceConfig()) {
773             for (ITargetPreparer preparer : holder.getTargetPreparers()) {
774                 String className = preparer.getClass().getName();
775                 if (mTestOrPreparerOptions.containsKey(className)) {
776                     config.injectOptionValues(mTestOrPreparerOptions.get(className));
777                 }
778                 if (preparer instanceof IAbiReceiver) {
779                     ((IAbiReceiver) preparer).setAbi(abi);
780                 }
781             }
782         }
783 
784         // Set IRemoteTests
785         List<IRemoteTest> tests = config.getTests();
786         for (IRemoteTest test : tests) {
787             String className = test.getClass().getName();
788             if (mTestOrPreparerOptions.containsKey(className)) {
789                 config.injectOptionValues(mTestOrPreparerOptions.get(className));
790             }
791             addFiltersToTest(test, abi, fullId, mIncludeFilters, mExcludeFilters);
792             if (test instanceof IAbiReceiver) {
793                 ((IAbiReceiver) test).setAbi(abi);
794             }
795         }
796 
797         // add the abi and module name to the description
798         config.getConfigurationDescription().setAbi(abi);
799         config.getConfigurationDescription().setModuleName(name);
800 
801         config.validateOptions();
802     }
803 
804     /** Whether or not the base configuration should be created for all abis or not. */
shouldCreateMultiAbiForBase(List<IModuleParameter> params)805     private boolean shouldCreateMultiAbiForBase(List<IModuleParameter> params) {
806         for (IModuleParameter param : params) {
807             if (param instanceof NotMultiAbiHandler) {
808                 return false;
809             }
810         }
811         return true;
812     }
813 }
814