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                         String nameWithParam =
345                                 String.format("%s[%s]", name, param.getParameterIdentifier());
346                         if (shouldRunParameterized(baseId, fullId, nameWithParam, mForcedParameter)) {
347                             IConfiguration paramConfig =
348                                     mConfigFactory.createConfigurationFromArgs(pathArg);
349                             // Mark the parameter in the metadata
350                             paramConfig
351                                     .getConfigurationDescription()
352                                     .addMetadata(
353                                             ConfigurationDescriptor.ACTIVE_PARAMETER_KEY,
354                                             param.getParameterIdentifier());
355                             param.addParameterSpecificConfig(paramConfig);
356                             setUpConfig(name, nameWithParam, baseId, fullId, paramConfig, abi);
357                             param.applySetup(paramConfig);
358                             toRun.put(fullId, paramConfig);
359                         }
360                     }
361                 }
362 
363                 if (mAllowMainlineParameterizedModules) {
364                     // If no options defined in a test config, skip generating.
365                     // TODO(easoncylee) This is still under discussion.
366                     if (mainlineParams.isEmpty()) {
367                         continue;
368                     }
369                     // If we find any parameterized combination for mainline modules.
370                     for (String param : mainlineParams) {
371                         String fullId = String.format("%s[%s]", baseId, param);
372                         String nameWithParam = String.format("%s[%s]", name, param);
373                         if (!shouldRunParameterized(baseId, fullId, nameWithParam, null)) {
374                             continue;
375                         }
376                         // Create mainline handler for each defined mainline parameter.
377                         MainlineModuleHandler handler =
378                                 new MainlineModuleHandler(
379                                         param,
380                                         abi,
381                                         mContext
382                                 );
383                         skipCreatingBaseConfig = true;
384                         IConfiguration paramConfig =
385                                 mConfigFactory.createConfigurationFromArgs(pathArg);
386                         paramConfig
387                                 .getConfigurationDescription()
388                                 .addMetadata(
389                                         ITestSuite.ACTIVE_MAINLINE_PARAMETER_KEY,
390                                         param);
391                         setUpConfig(name, nameWithParam, baseId, fullId, paramConfig, abi);
392                         handler.applySetup(paramConfig);
393                         toRun.put(fullId, paramConfig);
394                     }
395                 }
396 
397                 primaryAbi = false;
398                 // If a parameterized form of the module was forced, we don't create the standard
399                 // version of it.
400                 if (skipCreatingBaseConfig) {
401                     continue;
402                 }
403                 if (shouldRunModule(baseId)) {
404                     // Always add the base regular configuration to the execution.
405                     // Do not pass the nameWithParam in because it would cause the module args be
406                     // injected into config twice if we pass nameWithParam using name.
407                     setUpConfig(name, null, baseId, baseId, config, abi);
408                     toRun.put(baseId, config);
409                 }
410             }
411         } catch (ConfigurationException e) {
412             throw new RuntimeException(
413                     String.format(
414                             "Error parsing configuration: %s: '%s'",
415                             configFullName, e.getMessage()),
416                     e);
417         }
418 
419         return toRun;
420     }
421 
422     /** @return the {@link Set} of modules whose name contains the given pattern. */
getModuleNamesMatching( File directory, String suitePrefix, String pattern)423     public static Set<File> getModuleNamesMatching(
424             File directory, String suitePrefix, String pattern) {
425         List<File> extraTestCasesDirs = Arrays.asList(directory);
426         List<String> patterns = new ArrayList<>();
427         patterns.add(pattern);
428         Set<File> modules =
429                 ConfigurationUtil.getConfigNamesFileFromDirs(
430                         suitePrefix, extraTestCasesDirs, patterns);
431         return modules;
432     }
433 
434     /**
435      * Utility method that allows to parse and create a structure with the option filters.
436      *
437      * @param stringFilters The original option filters format.
438      * @param filters The filters parsed from the string format.
439      * @param abis The Abis to consider in the filtering.
440      */
addFilters( Set<String> stringFilters, Map<String, List<SuiteTestFilter>> filters, Set<IAbi> abis)441     public static void addFilters(
442             Set<String> stringFilters, Map<String, List<SuiteTestFilter>> filters, Set<IAbi> abis) {
443         for (String filterString : stringFilters) {
444             SuiteTestFilter filter = SuiteTestFilter.createFrom(filterString);
445             String abi = filter.getAbi();
446             if (abi == null) {
447                 for (IAbi a : abis) {
448                     addFilter(a.getName(), filter, filters);
449                 }
450             } else {
451                 addFilter(abi, filter, filters);
452             }
453         }
454     }
455 
addFilter( String abi, SuiteTestFilter filter, Map<String, List<SuiteTestFilter>> filters)456     private static void addFilter(
457             String abi, SuiteTestFilter filter, Map<String, List<SuiteTestFilter>> filters) {
458         getFilterList(filters, AbiUtils.createId(abi, filter.getName())).add(filter);
459     }
460 
getFilterList( Map<String, List<SuiteTestFilter>> filters, String id)461     private static List<SuiteTestFilter> getFilterList(
462             Map<String, List<SuiteTestFilter>> filters, String id) {
463         List<SuiteTestFilter> fs = filters.get(id);
464         if (fs == null) {
465             fs = new ArrayList<>();
466             filters.put(id, fs);
467         }
468         return fs;
469     }
470 
shouldRunModule(String moduleId)471     private boolean shouldRunModule(String moduleId) {
472         List<SuiteTestFilter> mdIncludes = getFilterList(mIncludeFilters, moduleId);
473         List<SuiteTestFilter> mdExcludes = getFilterList(mExcludeFilters, moduleId);
474         // if including all modules or includes exist for this module, and there are not excludes
475         // for the entire module, this module should be run.
476         return (mIncludeAll || !mdIncludes.isEmpty()) && !containsModuleExclude(mdExcludes);
477     }
478 
479     /**
480      * Except if the parameterized module is explicitly excluded, including the base module result
481      * in including its parameterization variant.
482      */
shouldRunParameterized( String baseModuleId, String parameterModuleId, String nameWithParam, IModuleParameter forcedModuleParameter)483     private boolean shouldRunParameterized(
484             String baseModuleId,
485             String parameterModuleId,
486             String nameWithParam,
487             IModuleParameter forcedModuleParameter) {
488         // Explicitly excluded
489         List<SuiteTestFilter> excluded = getFilterList(mExcludeFilters, parameterModuleId);
490         List<SuiteTestFilter> excludedParam = getFilterList(mExcludeFilters, nameWithParam);
491         if (containsModuleExclude(excluded) || containsModuleExclude(excludedParam)) {
492             return false;
493         }
494 
495         // Implicitly included due to forced parameter
496         if (forcedModuleParameter != null) {
497             List<SuiteTestFilter> baseInclude = getFilterList(mIncludeFilters, baseModuleId);
498             if (!baseInclude.isEmpty()) {
499                 return true;
500             }
501         }
502         // Explicitly included
503         List<SuiteTestFilter> included = getFilterList(mIncludeFilters, parameterModuleId);
504         List<SuiteTestFilter> includedParam = getFilterList(mIncludeFilters, nameWithParam);
505         if (mIncludeAll || !included.isEmpty() || !includedParam.isEmpty()) {
506             return true;
507         }
508         return false;
509     }
510 
addTestIncludes( ITestFilterReceiver test, List<SuiteTestFilter> includes, String moduleId)511     private void addTestIncludes(
512             ITestFilterReceiver test, List<SuiteTestFilter> includes, String moduleId) {
513         if (test instanceof ITestFileFilterReceiver) {
514             String escapedFileName = escapeFilterFileName(moduleId);
515             File includeFile = createFilterFile(escapedFileName, ".include", includes);
516             ((ITestFileFilterReceiver) test).setIncludeTestFile(includeFile);
517         } else {
518             // add test includes one at a time
519             for (SuiteTestFilter include : includes) {
520                 String filterTestName = include.getTest();
521                 if (filterTestName != null) {
522                     test.addIncludeFilter(filterTestName);
523                 }
524             }
525         }
526     }
527 
addTestExcludes( ITestFilterReceiver test, List<SuiteTestFilter> excludes, String moduleId)528     private void addTestExcludes(
529             ITestFilterReceiver test, List<SuiteTestFilter> excludes, String moduleId) {
530         if (test instanceof ITestFileFilterReceiver) {
531             String escapedFileName = escapeFilterFileName(moduleId);
532             File excludeFile = createFilterFile(escapedFileName, ".exclude", excludes);
533             ((ITestFileFilterReceiver) test).setExcludeTestFile(excludeFile);
534         } else {
535             // add test excludes one at a time
536             for (SuiteTestFilter exclude : excludes) {
537                 test.addExcludeFilter(exclude.getTest());
538             }
539         }
540     }
541 
542     /** module id can contain special characters, avoid them for file names. */
escapeFilterFileName(String moduleId)543     private String escapeFilterFileName(String moduleId) {
544         String escaped = UrlEscapers.urlPathSegmentEscaper().escape(moduleId);
545         return escaped;
546     }
547 
createFilterFile(String prefix, String suffix, List<SuiteTestFilter> filters)548     private File createFilterFile(String prefix, String suffix, List<SuiteTestFilter> filters) {
549         File filterFile = null;
550         PrintWriter out = null;
551         try {
552             filterFile = FileUtil.createTempFile(prefix, suffix);
553             out = new PrintWriter(filterFile);
554             for (SuiteTestFilter filter : filters) {
555                 String filterTest = filter.getTest();
556                 if (filterTest != null) {
557                     out.println(filterTest);
558                 }
559             }
560             out.flush();
561         } catch (IOException e) {
562             throw new RuntimeException("Failed to create filter file");
563         } finally {
564             StreamUtil.close(out);
565         }
566         filterFile.deleteOnExit();
567         return filterFile;
568     }
569 
570     /** Returns true iff one or more test filters in excludes apply to the entire module. */
containsModuleExclude(Collection<SuiteTestFilter> excludes)571     private boolean containsModuleExclude(Collection<SuiteTestFilter> excludes) {
572         for (SuiteTestFilter exclude : excludes) {
573             if (exclude.getTest() == null) {
574                 return true;
575             }
576         }
577         return false;
578     }
579 
580     /** A {@link FilenameFilter} to find all the config files in a directory. */
581     public static class ConfigFilter implements FilenameFilter {
582 
583         /** {@inheritDoc} */
584         @Override
accept(File dir, String name)585         public boolean accept(File dir, String name) {
586             return name.endsWith(CONFIG_EXT);
587         }
588     }
589 
590     /**
591      * Parse a list of args formatted as expected into {@link OptionDef} to be injected to module
592      * configurations.
593      *
594      * <p>Format: <module name / module id / class runner>:<option name>:[<arg-key>:=]<arg-value>
595      */
parseArgs(List<String> args, Map<String, List<OptionDef>> moduleOptions)596     private void parseArgs(List<String> args, Map<String, List<OptionDef>> moduleOptions) {
597         for (String arg : args) {
598             int moduleSep = arg.indexOf(":");
599             if (moduleSep == -1) {
600                 throw new RuntimeException("Expected delimiter ':' for module or class.");
601             }
602             String moduleName = arg.substring(0, moduleSep);
603             String remainder = arg.substring(moduleSep + 1);
604             List<OptionDef> listOption = moduleOptions.get(moduleName);
605             if (listOption == null) {
606                 listOption = new ArrayList<>();
607                 moduleOptions.put(moduleName, listOption);
608             }
609             int optionNameSep = remainder.indexOf(":");
610             if (optionNameSep == -1) {
611                 throw new RuntimeException(
612                         "Expected delimiter ':' between option name and values.");
613             }
614             String optionName = remainder.substring(0, optionNameSep);
615             Pattern pattern = Pattern.compile("\\{(.*)\\}(.*)");
616             Matcher match = pattern.matcher(optionName);
617             if (match.find()) {
618                 String alias = match.group(1);
619                 String name = match.group(2);
620                 optionName = alias + ":" + name;
621             }
622             String optionValueString = remainder.substring(optionNameSep + 1);
623             // TODO: See if QuotationTokenizer can be improved for multi-character delimiter.
624             // or change the delimiter to a single char.
625             String[] tokens = optionValueString.split(":=", 2);
626             OptionDef option = null;
627             if (tokens.length == 1) {
628                 option = new OptionDef(optionName, tokens[0], moduleName);
629             } else if (tokens.length == 2) {
630                 option = new OptionDef(optionName, tokens[0], tokens[1], moduleName);
631             }
632             listOption.add(option);
633         }
634     }
635 
636     /** Gets the list of {@link IModuleParameter}s associated with a module. */
getModuleParameters(String moduleName, IConfiguration config)637     private List<IModuleParameter> getModuleParameters(String moduleName, IConfiguration config)
638             throws ConfigurationException {
639         List<IModuleParameter> params = new ArrayList<>();
640         Set<String> processedParameterArgs = new HashSet<>();
641         // Track family of the parameters to make sure we have no duplicate.
642         Map<String, ModuleParameters> duplicateModule = new LinkedHashMap<>();
643 
644         List<String> parameters =
645                 config.getConfigurationDescription().getMetaData(ITestSuite.PARAMETER_KEY);
646         if (parameters == null || parameters.isEmpty()) {
647             return params;
648         }
649         for (String p : parameters) {
650             if (!processedParameterArgs.add(p)) {
651                 // Avoid processing the same parameter twice
652                 continue;
653             }
654             ModuleParameters suiteParam = ModuleParameters.valueOf(p.toUpperCase());
655             String family = suiteParam.getFamily();
656             if (duplicateModule.containsKey(family)) {
657                 // Duplicate family members are not accepted.
658                 throw new ConfigurationException(
659                         String.format(
660                                 "Module %s is declaring parameter: "
661                                         + "%s and %s when only one expected.",
662                                 moduleName, suiteParam, duplicateModule.get(family)));
663             } else {
664                 duplicateModule.put(suiteParam.getFamily(), suiteParam);
665             }
666             // Do not consider the excluded parameterization dimension
667             if (mExcludedModuleParameters.contains(suiteParam)) {
668                 CLog.d("'%s' was excluded via exclude-module-parameters.", moduleName);
669                 continue;
670             }
671             IModuleParameter handler =
672                     ModuleParametersHelper.getParameterHandler(
673                             suiteParam, /* optionalParams */ mAllowOptionalParameterizedModules);
674             if (handler != null) {
675                 params.add(handler);
676             }
677         }
678         return params;
679     }
680 
681     /** Gets the list of parameterized mainline modules associated with a module. */
682     @VisibleForTesting
getMainlineModuleParameters(IConfiguration config)683     List<String> getMainlineModuleParameters(IConfiguration config)
684             throws ConfigurationException {
685         List<String> params = new ArrayList<>();
686 
687         List<String> parameters =
688                 config.getConfigurationDescription().getMetaData(ITestSuite.MAINLINE_PARAMETER_KEY);
689         if (parameters == null || parameters.isEmpty()) {
690             return params;
691         }
692 
693         return new ArrayList<>(dedupMainlineParameters(parameters, config.getName()));
694     }
695 
696     /**
697      * De-duplicate the given mainline parameters.
698      *
699      * @param parameters The list of given mainline parameters.
700      * @param configName The test configuration name.
701      * @return The de-duplicated mainline modules list.
702      */
703     @VisibleForTesting
dedupMainlineParameters(List<String> parameters, String configName)704     Set<String> dedupMainlineParameters(List<String> parameters, String configName)
705             throws ConfigurationException {
706         Set<String> results = new HashSet<>();
707         for (String param : parameters) {
708             if (!isValidMainlineParam(param)) {
709                 throw new ConfigurationException(
710                         String.format(
711                                 "Illegal mainline module parameter: \"%s\" configured in the " +
712                                 "test config: %s. Parameter must end with .apk/.apex/.apks and " +
713                                 "have no any spaces configured.", param, configName)
714                 );
715             }
716             if (!isInAlphabeticalOrder(param)) {
717                 throw new ConfigurationException(
718                         String.format(
719                                 "Illegal mainline module parameter: \"%s\" configured in the " +
720                                 "test config: %s. Parameter must be configured in alphabetical " +
721                                 "order or with no duplicated modules.", param, configName)
722                 );
723             }
724             results.add(param);
725         }
726         return results;
727     }
728 
729     /** Whether a mainline parameter configured in a test config is in alphabetical order or not. */
730     @VisibleForTesting
isInAlphabeticalOrder(String param)731     boolean isInAlphabeticalOrder(String param) {
732         String previousString = "";
733         for (String currentString : param.split(String.format("\\+"))) {
734             // This is to check if the parameter is in alphabetical order or duplicated.
735             if (currentString.compareTo(previousString) <= 0) {
736                 return false;
737             }
738             previousString = currentString;
739         }
740         return true;
741     }
742 
743     /** Whether the mainline parameter configured in the test config is valid or not. */
744     @VisibleForTesting
isValidMainlineParam(String param)745     boolean isValidMainlineParam(String param) {
746         if (param.contains(" ")) {
747             return false;
748         }
749         for (String m : param.split(String.format("\\+"))) {
750             if (!MAINLINE_PARAMETERS_TO_VALIDATE.stream().anyMatch(entry -> m.endsWith(entry))) {
751                 return false;
752             }
753         }
754         return true;
755     }
756 
757     /**
758      * Setup the options for the module configuration.
759      *
760      * @param name The base name of the module
761      * @param nameWithParam The id of the parameterized mainline module (module name + parameters)
762      * @param id The base id name of the module.
763      * @param fullId The full id of the module (usually abi + module name + parameters)
764      * @param config The module configuration.
765      * @param abi The abi of the module.
766      * @throws ConfigurationException
767      */
setUpConfig(String name, String nameWithParam, String id, String fullId, IConfiguration config, IAbi abi)768     private void setUpConfig(String name, String nameWithParam, String id, String fullId,
769             IConfiguration config, IAbi abi)
770         throws ConfigurationException {
771         List<OptionDef> optionsToInject = new ArrayList<>();
772         if (mModuleOptions.containsKey(name)) {
773             optionsToInject.addAll(mModuleOptions.get(name));
774         }
775         if (nameWithParam != null && mModuleOptions.containsKey(nameWithParam)) {
776             optionsToInject.addAll(mModuleOptions.get(nameWithParam));
777         }
778         if (mModuleOptions.containsKey(id)) {
779             optionsToInject.addAll(mModuleOptions.get(id));
780         }
781         if (mModuleOptions.containsKey(fullId)) {
782             optionsToInject.addAll(mModuleOptions.get(fullId));
783         }
784         config.injectOptionValues(optionsToInject);
785 
786         // Set target preparers
787         for (IDeviceConfiguration holder : config.getDeviceConfig()) {
788             for (ITargetPreparer preparer : holder.getTargetPreparers()) {
789                 String className = preparer.getClass().getName();
790                 if (mTestOrPreparerOptions.containsKey(className)) {
791                     config.injectOptionValues(mTestOrPreparerOptions.get(className));
792                 }
793                 if (preparer instanceof IAbiReceiver) {
794                     ((IAbiReceiver) preparer).setAbi(abi);
795                 }
796             }
797         }
798 
799         // Set IRemoteTests
800         List<IRemoteTest> tests = config.getTests();
801         for (IRemoteTest test : tests) {
802             String className = test.getClass().getName();
803             if (mTestOrPreparerOptions.containsKey(className)) {
804                 config.injectOptionValues(mTestOrPreparerOptions.get(className));
805             }
806             addFiltersToTest(test, abi, fullId, mIncludeFilters, mExcludeFilters);
807             if (test instanceof IAbiReceiver) {
808                 ((IAbiReceiver) test).setAbi(abi);
809             }
810         }
811 
812         // add the abi and module name to the description
813         config.getConfigurationDescription().setAbi(abi);
814         config.getConfigurationDescription().setModuleName(name);
815 
816         config.validateOptions();
817     }
818 
819 
820     /** Whether or not the base configuration should be created for all abis or not. */
shouldCreateMultiAbiForBase(List<IModuleParameter> params)821     private boolean shouldCreateMultiAbiForBase(List<IModuleParameter> params) {
822         for (IModuleParameter param : params) {
823             if (param instanceof NotMultiAbiHandler) {
824                 return false;
825             }
826         }
827         return true;
828     }
829 }
830