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.tradefed.config.ConfigurationDef.OptionDef;
19 import com.android.tradefed.config.ConfigurationException;
20 import com.android.tradefed.config.ConfigurationFactory;
21 import com.android.tradefed.config.ConfigurationUtil;
22 import com.android.tradefed.config.IConfiguration;
23 import com.android.tradefed.config.IConfigurationFactory;
24 import com.android.tradefed.log.LogUtil.CLog;
25 import com.android.tradefed.targetprep.ITargetPreparer;
26 import com.android.tradefed.testtype.IAbi;
27 import com.android.tradefed.testtype.IAbiReceiver;
28 import com.android.tradefed.testtype.IRemoteTest;
29 import com.android.tradefed.testtype.ITestFileFilterReceiver;
30 import com.android.tradefed.testtype.ITestFilterReceiver;
31 import com.android.tradefed.util.AbiUtils;
32 import com.android.tradefed.util.FileUtil;
33 import com.android.tradefed.util.StreamUtil;
34 
35 import com.google.common.base.Strings;
36 
37 import java.io.File;
38 import java.io.FilenameFilter;
39 import java.io.IOException;
40 import java.io.PrintWriter;
41 import java.util.ArrayList;
42 import java.util.Arrays;
43 import java.util.Collection;
44 import java.util.Collections;
45 import java.util.HashMap;
46 import java.util.LinkedHashMap;
47 import java.util.List;
48 import java.util.Map;
49 import java.util.Set;
50 
51 /**
52  * Retrieves Compatibility test module definitions from the repository. TODO: Add the expansion of
53  * suite when loading a module.
54  */
55 public class SuiteModuleLoader {
56 
57     public static final String CONFIG_EXT = ".config";
58     private Map<String, List<OptionDef>> mTestOptions = new HashMap<>();
59     private Map<String, List<OptionDef>> mModuleOptions = new HashMap<>();
60     private boolean mIncludeAll;
61     private Map<String, List<SuiteTestFilter>> mIncludeFilters = new HashMap<>();
62     private Map<String, List<SuiteTestFilter>> mExcludeFilters = new HashMap<>();
63     private IConfigurationFactory mConfigFactory = ConfigurationFactory.getInstance();
64 
65     /**
66      * Ctor for the SuiteModuleLoader.
67      *
68      * @param includeFilters The formatted and parsed include filters.
69      * @param excludeFilters The formatted and parsed exclude filters.
70      * @param testArgs the list of test ({@link IRemoteTest}) arguments.
71      * @param moduleArgs the list of module arguments.
72      */
SuiteModuleLoader( Map<String, List<SuiteTestFilter>> includeFilters, Map<String, List<SuiteTestFilter>> excludeFilters, List<String> testArgs, List<String> moduleArgs)73     public SuiteModuleLoader(
74             Map<String, List<SuiteTestFilter>> includeFilters,
75             Map<String, List<SuiteTestFilter>> excludeFilters,
76             List<String> testArgs,
77             List<String> moduleArgs) {
78         mIncludeAll = includeFilters.isEmpty();
79         mIncludeFilters = includeFilters;
80         mExcludeFilters = excludeFilters;
81 
82         parseArgs(testArgs, mTestOptions);
83         parseArgs(moduleArgs, mModuleOptions);
84     }
85 
86     /** Main loading of configurations, looking into a folder */
loadConfigsFromDirectory( File testsDir, Set<IAbi> abis, String suitePrefix, String suiteTag, List<String> patterns)87     public LinkedHashMap<String, IConfiguration> loadConfigsFromDirectory(
88             File testsDir,
89             Set<IAbi> abis,
90             String suitePrefix,
91             String suiteTag,
92             List<String> patterns) {
93         LinkedHashMap<String, IConfiguration> toRun = new LinkedHashMap<>();
94 
95         List<File> listConfigFiles = new ArrayList<>();
96         List<File> extraTestCasesDirs = Arrays.asList(testsDir);
97         listConfigFiles.addAll(
98                 ConfigurationUtil.getConfigNamesFileFromDirs(
99                         suitePrefix, extraTestCasesDirs, patterns));
100         // Ensure stable initial order of configurations.
101         Collections.sort(listConfigFiles);
102         for (File configFile : listConfigFiles) {
103             toRun.putAll(
104                     loadOneConfig(
105                             configFile.getName(), configFile.getAbsolutePath(), abis, suiteTag));
106         }
107         return toRun;
108     }
109 
110     /**
111      * Main loading of configurations, looking into the resources on the classpath. (TF configs for
112      * example).
113      */
loadConfigsFromJars( Set<IAbi> abis, String suitePrefix, String suiteTag)114     public LinkedHashMap<String, IConfiguration> loadConfigsFromJars(
115             Set<IAbi> abis, String suitePrefix, String suiteTag) {
116         LinkedHashMap<String, IConfiguration> toRun = new LinkedHashMap<>();
117 
118         IConfigurationFactory configFactory = ConfigurationFactory.getInstance();
119         List<String> configs = configFactory.getConfigList(suitePrefix, false);
120         // Sort configs to ensure they are always evaluated and added in the same order.
121         Collections.sort(configs);
122         for (String configName : configs) {
123             toRun.putAll(loadOneConfig(configName, configName, abis, suiteTag));
124         }
125         return toRun;
126     }
127 
128     /**
129      * Pass the filters to the {@link IRemoteTest}. Default behavior is to ignore if the IRemoteTest
130      * does not implements {@link ITestFileFilterReceiver}. This can be overriden to create a more
131      * restrictive behavior.
132      *
133      * @param test The {@link IRemoteTest} that is being considered.
134      * @param abi The Abi we are currently working on.
135      * @param name The name of the module.
136      * @param includeFilters The formatted and parsed include filters.
137      * @param excludeFilters The formatted and parsed exclude filters.
138      */
addFiltersToTest( IRemoteTest test, IAbi abi, String name, Map<String, List<SuiteTestFilter>> includeFilters, Map<String, List<SuiteTestFilter>> excludeFilters)139     public void addFiltersToTest(
140             IRemoteTest test,
141             IAbi abi,
142             String name,
143             Map<String, List<SuiteTestFilter>> includeFilters,
144             Map<String, List<SuiteTestFilter>> excludeFilters) {
145         String moduleId = AbiUtils.createId(abi.getName(), name);
146         if (!(test instanceof ITestFilterReceiver)) {
147             CLog.e("Test in module %s does not implement ITestFilterReceiver.", moduleId);
148             return;
149         }
150         List<SuiteTestFilter> mdIncludes = getFilterList(includeFilters, moduleId);
151         List<SuiteTestFilter> mdExcludes = getFilterList(excludeFilters, moduleId);
152         if (!mdIncludes.isEmpty()) {
153             addTestIncludes((ITestFilterReceiver) test, mdIncludes, name);
154         }
155         if (!mdExcludes.isEmpty()) {
156             addTestExcludes((ITestFilterReceiver) test, mdExcludes, name);
157         }
158     }
159 
160     /**
161      * Load a single config location (file or on TF classpath). It can results in several {@link
162      * IConfiguration}. If a single configuration get expanded in different ways.
163      *
164      * @param configName The actual config name only. (no path)
165      * @param configFullName The fully qualified config name. (with path, if any).
166      * @param abis The set of all abis that needs to run.
167      * @param suiteTag the Tag of the suite aimed to be run.
168      * @return A map of loaded configuration.
169      */
loadOneConfig( String configName, String configFullName, Set<IAbi> abis, String suiteTag)170     private LinkedHashMap<String, IConfiguration> loadOneConfig(
171             String configName, String configFullName, Set<IAbi> abis, String suiteTag) {
172         LinkedHashMap<String, IConfiguration> toRun = new LinkedHashMap<>();
173         final String name = configName.replace(CONFIG_EXT, "");
174         final String[] pathArg = new String[] {configFullName};
175         try {
176             // Invokes parser to process the test module config file
177             // Need to generate a different config for each ABI as we cannot guarantee the
178             // configs are idempotent. This however means we parse the same file multiple times
179             for (IAbi abi : abis) {
180                 String id = AbiUtils.createId(abi.getName(), name);
181                 if (!shouldRunModule(id)) {
182                     // If the module should not run tests based on the state of filters,
183                     // skip this name/abi combination.
184                     continue;
185                 }
186                 IConfiguration config = mConfigFactory.createConfigurationFromArgs(pathArg);
187 
188                 // If a suiteTag is used, we load with it.
189                 if (!Strings.isNullOrEmpty(suiteTag)
190                         && !config.getConfigurationDescription()
191                                 .getSuiteTags()
192                                 .contains(suiteTag)) {
193                     CLog.d(
194                             "Configuration %s does not include the suite-tag '%s'. Ignoring it.",
195                             configFullName, suiteTag);
196                     continue;
197                 }
198 
199                 List<OptionDef> optionsToInject = new ArrayList<>();
200                 if (mModuleOptions.containsKey(name)) {
201                     optionsToInject.addAll(mModuleOptions.get(name));
202                 }
203                 if (mModuleOptions.containsKey(id)) {
204                     optionsToInject.addAll(mModuleOptions.get(id));
205                 }
206                 config.injectOptionValues(optionsToInject);
207 
208                 // Set target preparers
209                 List<ITargetPreparer> preparers = config.getTargetPreparers();
210                 for (ITargetPreparer preparer : preparers) {
211                     if (preparer instanceof IAbiReceiver) {
212                         ((IAbiReceiver) preparer).setAbi(abi);
213                     }
214                 }
215 
216                 // Set IRemoteTests
217                 List<IRemoteTest> tests = config.getTests();
218                 for (IRemoteTest test : tests) {
219                     String className = test.getClass().getName();
220                     if (mTestOptions.containsKey(className)) {
221                         config.injectOptionValues(mTestOptions.get(className));
222                     }
223                     addFiltersToTest(test, abi, name, mIncludeFilters, mExcludeFilters);
224                     if (test instanceof IAbiReceiver) {
225                         ((IAbiReceiver) test).setAbi(abi);
226                     }
227                 }
228 
229                 // add the abi and module name to the description
230                 config.getConfigurationDescription().setAbi(abi);
231                 config.getConfigurationDescription().setModuleName(name);
232                 toRun.put(id, config);
233             }
234         } catch (ConfigurationException e) {
235             throw new RuntimeException(
236                     String.format(
237                             "Error parsing configuration: %s: '%s'",
238                             configFullName, e.getMessage()),
239                     e);
240         }
241         return toRun;
242     }
243 
244     /** @return the {@link Set} of modules whose name contains the given pattern. */
getModuleNamesMatching( File directory, String suitePrefix, String pattern)245     public static Set<File> getModuleNamesMatching(
246             File directory, String suitePrefix, String pattern) {
247         List<File> extraTestCasesDirs = Arrays.asList(directory);
248         List<String> patterns = new ArrayList<>();
249         patterns.add(pattern);
250         Set<File> modules =
251                 ConfigurationUtil.getConfigNamesFileFromDirs(
252                         suitePrefix, extraTestCasesDirs, patterns);
253         return modules;
254     }
255 
256     /**
257      * Utility method that allows to parse and create a structure with the option filters.
258      *
259      * @param stringFilters The original option filters format.
260      * @param filters The filters parsed from the string format.
261      * @param abis The Abis to consider in the filtering.
262      */
addFilters( Set<String> stringFilters, Map<String, List<SuiteTestFilter>> filters, Set<IAbi> abis)263     public static void addFilters(
264             Set<String> stringFilters, Map<String, List<SuiteTestFilter>> filters, Set<IAbi> abis) {
265         for (String filterString : stringFilters) {
266             SuiteTestFilter filter = SuiteTestFilter.createFrom(filterString);
267             String abi = filter.getAbi();
268             if (abi == null) {
269                 for (IAbi a : abis) {
270                     addFilter(a.getName(), filter, filters);
271                 }
272             } else {
273                 addFilter(abi, filter, filters);
274             }
275         }
276     }
277 
addFilter( String abi, SuiteTestFilter filter, Map<String, List<SuiteTestFilter>> filters)278     private static void addFilter(
279             String abi, SuiteTestFilter filter, Map<String, List<SuiteTestFilter>> filters) {
280         getFilterList(filters, AbiUtils.createId(abi, filter.getName())).add(filter);
281     }
282 
getFilterList( Map<String, List<SuiteTestFilter>> filters, String id)283     private static List<SuiteTestFilter> getFilterList(
284             Map<String, List<SuiteTestFilter>> filters, String id) {
285         List<SuiteTestFilter> fs = filters.get(id);
286         if (fs == null) {
287             fs = new ArrayList<>();
288             filters.put(id, fs);
289         }
290         return fs;
291     }
292 
shouldRunModule(String moduleId)293     private boolean shouldRunModule(String moduleId) {
294         List<SuiteTestFilter> mdIncludes = getFilterList(mIncludeFilters, moduleId);
295         List<SuiteTestFilter> mdExcludes = getFilterList(mExcludeFilters, moduleId);
296         // if including all modules or includes exist for this module, and there are not excludes
297         // for the entire module, this module should be run.
298         return (mIncludeAll || !mdIncludes.isEmpty()) && !containsModuleExclude(mdExcludes);
299     }
300 
addTestIncludes( ITestFilterReceiver test, List<SuiteTestFilter> includes, String name)301     private void addTestIncludes(
302             ITestFilterReceiver test, List<SuiteTestFilter> includes, String name) {
303         if (test instanceof ITestFileFilterReceiver) {
304             File includeFile = createFilterFile(name, ".include", includes);
305             ((ITestFileFilterReceiver) test).setIncludeTestFile(includeFile);
306         } else {
307             // add test includes one at a time
308             for (SuiteTestFilter include : includes) {
309                 String filterTestName = include.getTest();
310                 if (filterTestName != null) {
311                     test.addIncludeFilter(filterTestName);
312                 }
313             }
314         }
315     }
316 
addTestExcludes( ITestFilterReceiver test, List<SuiteTestFilter> excludes, String name)317     private void addTestExcludes(
318             ITestFilterReceiver test, List<SuiteTestFilter> excludes, String name) {
319         if (test instanceof ITestFileFilterReceiver) {
320             File excludeFile = createFilterFile(name, ".exclude", excludes);
321             ((ITestFileFilterReceiver) test).setExcludeTestFile(excludeFile);
322         } else {
323             // add test excludes one at a time
324             for (SuiteTestFilter exclude : excludes) {
325                 test.addExcludeFilter(exclude.getTest());
326             }
327         }
328     }
329 
createFilterFile(String prefix, String suffix, List<SuiteTestFilter> filters)330     private File createFilterFile(String prefix, String suffix, List<SuiteTestFilter> filters) {
331         File filterFile = null;
332         PrintWriter out = null;
333         try {
334             filterFile = FileUtil.createTempFile(prefix, suffix);
335             out = new PrintWriter(filterFile);
336             for (SuiteTestFilter filter : filters) {
337                 String filterTest = filter.getTest();
338                 if (filterTest != null) {
339                     out.println(filterTest);
340                 }
341             }
342             out.flush();
343         } catch (IOException e) {
344             throw new RuntimeException("Failed to create filter file");
345         } finally {
346             StreamUtil.close(out);
347         }
348         filterFile.deleteOnExit();
349         return filterFile;
350     }
351 
352     /** Returns true iff one or more test filters in excludes apply to the entire module. */
containsModuleExclude(Collection<SuiteTestFilter> excludes)353     private boolean containsModuleExclude(Collection<SuiteTestFilter> excludes) {
354         for (SuiteTestFilter exclude : excludes) {
355             if (exclude.getTest() == null) {
356                 return true;
357             }
358         }
359         return false;
360     }
361 
362     /** A {@link FilenameFilter} to find all the config files in a directory. */
363     public static class ConfigFilter implements FilenameFilter {
364 
365         /** {@inheritDoc} */
366         @Override
accept(File dir, String name)367         public boolean accept(File dir, String name) {
368             return name.endsWith(CONFIG_EXT);
369         }
370     }
371 
372     /**
373      * Parse a list of args formatted as expected into {@link OptionDef} to be injected to module
374      * configurations.
375      *
376      * <p>Format: <module name / module id / class runner>:<option name>:[<arg-key>:=]<arg-value>
377      */
parseArgs(List<String> args, Map<String, List<OptionDef>> moduleOptions)378     private void parseArgs(List<String> args, Map<String, List<OptionDef>> moduleOptions) {
379         for (String arg : args) {
380             int moduleSep = arg.indexOf(":");
381             if (moduleSep == -1) {
382                 throw new RuntimeException("Expected delimiter ':' for module or class.");
383             }
384             String moduleName = arg.substring(0, moduleSep);
385             String remainder = arg.substring(moduleSep + 1);
386             List<OptionDef> listOption = moduleOptions.get(moduleName);
387             if (listOption == null) {
388                 listOption = new ArrayList<>();
389                 moduleOptions.put(moduleName, listOption);
390             }
391             int optionNameSep = remainder.indexOf(":");
392             if (optionNameSep == -1) {
393                 throw new RuntimeException(
394                         "Expected delimiter ':' between option name and values.");
395             }
396             String optionName = remainder.substring(0, optionNameSep);
397             String optionValueString = remainder.substring(optionNameSep + 1);
398             // TODO: See if QuotationTokenizer can be improved for multi-character delimiter.
399             // or change the delimiter to a single char.
400             String[] tokens = optionValueString.split(":=");
401             OptionDef option = null;
402             if (tokens.length == 1) {
403                 option = new OptionDef(optionName, tokens[0], moduleName);
404             } else if (tokens.length == 2) {
405                 option = new OptionDef(optionName, tokens[0], tokens[1], moduleName);
406             }
407             listOption.add(option);
408         }
409     }
410 }
411