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.build.BuildInfoKey.BuildInfoFileKey;
19 import com.android.tradefed.build.IBuildInfo;
20 import com.android.tradefed.config.IConfiguration;
21 import com.android.tradefed.config.Option;
22 import com.android.tradefed.config.Option.Importance;
23 import com.android.tradefed.config.OptionClass;
24 import com.android.tradefed.device.DeviceNotAvailableException;
25 import com.android.tradefed.log.LogUtil.CLog;
26 import com.android.tradefed.result.FileInputStreamSource;
27 import com.android.tradefed.result.LogDataType;
28 import com.android.tradefed.testtype.IAbi;
29 import com.android.tradefed.testtype.IRemoteTest;
30 import com.android.tradefed.testtype.suite.params.IModuleParameter;
31 import com.android.tradefed.testtype.suite.params.ModuleParameters;
32 import com.android.tradefed.testtype.suite.params.ModuleParametersHelper;
33 import com.android.tradefed.testtype.suite.params.NegativeHandler;
34 import com.android.tradefed.util.ArrayUtil;
35 import com.android.tradefed.util.FileUtil;
36 
37 import com.google.common.annotations.VisibleForTesting;
38 
39 import java.io.File;
40 import java.io.FileNotFoundException;
41 import java.io.IOException;
42 import java.util.ArrayList;
43 import java.util.HashMap;
44 import java.util.HashSet;
45 import java.util.LinkedHashMap;
46 import java.util.List;
47 import java.util.Map;
48 import java.util.Set;
49 
50 /** A Test for running Compatibility Test Suite with new suite system. */
51 @OptionClass(alias = "base-suite")
52 public class BaseTestSuite extends ITestSuite {
53 
54     public static final String INCLUDE_FILTER_OPTION = "include-filter";
55     public static final String EXCLUDE_FILTER_OPTION = "exclude-filter";
56     public static final String MODULE_OPTION = "module";
57     public static final char MODULE_OPTION_SHORT_NAME = 'm';
58     public static final String TEST_ARG_OPTION = "test-arg";
59     public static final String TEST_OPTION = "test";
60     public static final char TEST_OPTION_SHORT_NAME = 't';
61     public static final String CONFIG_PATTERNS_OPTION = "config-patterns";
62     private static final String MODULE_ARG_OPTION = "module-arg";
63     private static final int MAX_FILTER_DISPLAY = 20;
64 
65     @Option(
66             name = INCLUDE_FILTER_OPTION,
67             description =
68                     "the include module filters to apply. Format: '[abi] <module-name> [test]'."
69                             + " See documentation:"
70                             + "https://source.android.com/devices/tech/test_infra/tradefed/testing/through-suite/option-passing",
71             importance = Importance.ALWAYS)
72     private Set<String> mIncludeFilters = new HashSet<>();
73 
74     @Option(
75             name = EXCLUDE_FILTER_OPTION,
76             description =
77                     "the exclude module filters to apply. Format: '[abi] <module-name> [test]'."
78                             + " See documentation:"
79                             + "https://source.android.com/devices/tech/test_infra/tradefed/testing/through-suite/option-passing",
80             importance = Importance.ALWAYS)
81     private Set<String> mExcludeFilters = new HashSet<>();
82 
83     @Option(
84         name = MODULE_OPTION,
85         shortName = MODULE_OPTION_SHORT_NAME,
86         description = "the test module to run. Only works for configuration in the tests dir.",
87         importance = Importance.IF_UNSET
88     )
89     private String mModuleName = null;
90 
91     @Option(
92         name = TEST_OPTION,
93         shortName = TEST_OPTION_SHORT_NAME,
94         description = "the test to run.",
95         importance = Importance.IF_UNSET
96     )
97     private String mTestName = null;
98 
99     @Option(
100         name = MODULE_ARG_OPTION,
101         description =
102                 "the arguments to pass to a module. The expected format is"
103                         + "\"<module-name>:[{alias}]<arg-name>:[<arg-key>:=]<arg-value>\"",
104         importance = Importance.ALWAYS
105     )
106     private List<String> mModuleArgs = new ArrayList<>();
107 
108     @Option(
109             name = TEST_ARG_OPTION,
110             description =
111                     "The arguments to pass to a test or its preparers. The expected format is"
112                             + "\"<test-class>:<arg-name>:[<arg-key>:=]<arg-value>\"",
113             importance = Importance.ALWAYS)
114     private List<String> mTestArgs = new ArrayList<>();
115 
116     @Option(
117         name = "run-suite-tag",
118         description =
119                 "The tag that must be run. If specified, only configurations containing the "
120                         + "matching suite tag will be able to run."
121     )
122     private String mSuiteTag = null;
123 
124     @Option(
125         name = "prioritize-host-config",
126         description =
127                 "If there are duplicate test configs for host/target, prioritize the host config, "
128                         + "otherwise use the target config."
129     )
130     private boolean mPrioritizeHostConfig = false;
131 
132     @Option(
133         name = "suite-config-prefix",
134         description = "Search only configs with given prefix for suite tags."
135     )
136     private String mSuitePrefix = null;
137 
138     @Option(
139         name = "skip-loading-config-jar",
140         description = "Whether or not to skip loading configurations from the JAR on the classpath."
141     )
142     private boolean mSkipJarLoading = false;
143 
144     @Option(
145         name = CONFIG_PATTERNS_OPTION,
146         description =
147                 "The pattern(s) of the configurations that should be loaded from a directory."
148                         + " If none is explicitly specified, .*.xml and .*.config will be used."
149                         + " Can be repeated."
150     )
151     private List<String> mConfigPatterns = new ArrayList<>();
152 
153     @Option(
154         name = "enable-parameterized-modules",
155         description =
156                 "Whether or not to enable parameterized modules. This is a feature flag for work "
157                         + "in development."
158     )
159     private boolean mEnableParameter = false;
160 
161     @Option(
162         name = "enable-mainline-parameterized-modules",
163         description =
164                 "Whether or not to enable mainline parameterized modules. This is a feature flag "
165                         + "for work in development."
166     )
167     private boolean mEnableMainlineParameter = false;
168 
169     @Option(
170         name = "enable-optional-parameterization",
171         description =
172                 "Whether or not to enable optional parameters. Optional parameters are "
173                         + "parameters not usually used by default."
174     )
175     private boolean mEnableOptionalParameter = false;
176 
177     @Option(
178         name = "module-parameter",
179         description =
180                 "Allows to run only one module parameter type instead of all the combinations. "
181                         + "For example: 'instant_app' would only run the instant_app version of "
182                         + "modules"
183     )
184     private ModuleParameters mForceParameter = null;
185 
186     @Option(
187         name = "exclude-module-parameters",
188         description =
189                 "Exclude some modules parameter from being evaluated in the run combinations."
190                         + "For example: 'instant_app' would exclude all the instant_app version of "
191                         + "modules."
192     )
193     private Set<ModuleParameters> mExcludedModuleParameters = new HashSet<>();
194 
195     @Option(
196             name = "fail-on-everything-filtered",
197             description =
198                     "Whether or not to fail the invocation in case test filter returns"
199                             + " an empty result.")
200     private boolean mFailOnEverythingFiltered = false;
201 
202     private SuiteModuleLoader mModuleRepo;
203     private Map<String, List<SuiteTestFilter>> mIncludeFiltersParsed = new HashMap<>();
204     private Map<String, List<SuiteTestFilter>> mExcludeFiltersParsed = new HashMap<>();
205     private List<File> mConfigPaths = new ArrayList<>();
206 
207     /** {@inheritDoc} */
208     @Override
loadTests()209     public LinkedHashMap<String, IConfiguration> loadTests() {
210         try {
211             File testsDir = getTestsDir();
212             setupFilters(testsDir);
213             Set<IAbi> abis = getAbis(getDevice());
214 
215             // Create and populate the filters here
216             SuiteModuleLoader.addFilters(mIncludeFilters, mIncludeFiltersParsed, abis);
217             SuiteModuleLoader.addFilters(mExcludeFilters, mExcludeFiltersParsed, abis);
218 
219             String includeFilter = mIncludeFiltersParsed.toString();
220             if (mIncludeFiltersParsed.size() > MAX_FILTER_DISPLAY) {
221                 if (isSplitting()) {
222                     includeFilter = includeFilter.substring(0, 100) + "...";
223                 } else {
224                     File suiteIncludeFilters = null;
225                     try {
226                         suiteIncludeFilters =
227                                 FileUtil.createTempFile("suite-include-filters", ".txt");
228                         FileUtil.writeToFile(mIncludeFiltersParsed.toString(), suiteIncludeFilters);
229                         logFilterFile(
230                                 suiteIncludeFilters,
231                                 suiteIncludeFilters.getName(),
232                                 LogDataType.TEXT);
233                         includeFilter = String.format("See %s", suiteIncludeFilters.getName());
234                     } catch (IOException e) {
235                         CLog.e(e);
236                     } finally {
237                         FileUtil.deleteFile(suiteIncludeFilters);
238                     }
239                 }
240             }
241 
242             String excludeFilter = mExcludeFiltersParsed.toString();
243             if (mExcludeFiltersParsed.size() > MAX_FILTER_DISPLAY) {
244                 if (isSplitting()) {
245                     excludeFilter = excludeFilter.substring(0, 100) + "...";
246                 } else {
247                     File suiteExcludeFilters = null;
248                     try {
249                         suiteExcludeFilters =
250                                 FileUtil.createTempFile("suite-exclude-filters", ".txt");
251                         FileUtil.writeToFile(mExcludeFiltersParsed.toString(), suiteExcludeFilters);
252                         logFilterFile(
253                                 suiteExcludeFilters,
254                                 suiteExcludeFilters.getName(),
255                                 LogDataType.TEXT);
256                         excludeFilter = String.format("See %s", suiteExcludeFilters.getName());
257                     } catch (IOException e) {
258                         CLog.e(e);
259                     } finally {
260                         FileUtil.deleteFile(suiteExcludeFilters);
261                     }
262                 }
263             }
264 
265             CLog.d(
266                     "Initializing ModuleRepo\nABIs:%s\n"
267                             + "Test Args:%s\nModule Args:%s\nIncludes:%s\nExcludes:%s",
268                     abis, mTestArgs, mModuleArgs, includeFilter, excludeFilter);
269 
270             mModuleRepo =
271                     createModuleLoader(
272                             mIncludeFiltersParsed, mExcludeFiltersParsed, mTestArgs, mModuleArgs);
273             if (mForceParameter != null && !mEnableParameter) {
274                 throw new IllegalArgumentException(
275                         "'module-parameter' option was specified without "
276                                 + "'enable-parameterized-modules'");
277             }
278             if (mEnableOptionalParameter && !mEnableParameter) {
279                 throw new IllegalArgumentException(
280                         "'enable-optional-parameterization' option was specified without "
281                                 + "'enable-parameterized-modules'");
282             }
283 
284             if (mEnableMainlineParameter) {
285                 mModuleRepo.setMainlineParameterizedModules(mEnableMainlineParameter);
286                 mModuleRepo.setInvocationContext(getInvocationContext());
287             }
288 
289             mModuleRepo.setParameterizedModules(mEnableParameter);
290             mModuleRepo.setOptionalParameterizedModules(mEnableOptionalParameter);
291             mModuleRepo.setModuleParameter(mForceParameter);
292             mModuleRepo.setExcludedModuleParameters(mExcludedModuleParameters);
293 
294             List<File> testsDirectories = new ArrayList<>();
295 
296             // Include host or target first in the search if it exists, we have to this in
297             // BaseTestSuite because it's the only one with the BuildInfo knowledge of linked files
298             if (mPrioritizeHostConfig) {
299                 File hostSubDir = getBuildInfo().getFile(BuildInfoFileKey.HOST_LINKED_DIR);
300                 if (hostSubDir != null && hostSubDir.exists()) {
301                     testsDirectories.add(hostSubDir);
302                 }
303             } else {
304                 File targetSubDir = getBuildInfo().getFile(BuildInfoFileKey.TARGET_LINKED_DIR);
305                 if (targetSubDir != null && targetSubDir.exists()) {
306                     testsDirectories.add(targetSubDir);
307                 }
308             }
309 
310             // Finally add the full test cases directory in case there is no special sub-dir.
311             testsDirectories.add(testsDir);
312             // Actual loading of the configurations.
313             LinkedHashMap<String, IConfiguration> loadedTests =
314                     loadingStrategy(abis, testsDirectories, mSuitePrefix, mSuiteTag);
315 
316             if (mFailOnEverythingFiltered
317                     && loadedTests.isEmpty()
318                     && !mIncludeFiltersParsed.isEmpty()) {
319                 throw new IllegalStateException(
320                         String.format(
321                                 "Include filter '%s' was specified"
322                                         + " but resulted in an empty test set.",
323                                 includeFilter));
324             }
325             return loadedTests;
326         } catch (DeviceNotAvailableException | FileNotFoundException e) {
327             throw new RuntimeException(e);
328         }
329     }
330 
331     /**
332      * Default loading strategy will load from the resources and the tests directory. Can be
333      * extended or replaced.
334      *
335      * @param abis The set of abis to run against.
336      * @param testsDirs The tests directory.
337      * @param suitePrefix A prefix to filter the resource directory.
338      * @param suiteTag The suite tag a module should have to be included. Can be null.
339      * @return A list of loaded configuration for the suite.
340      */
loadingStrategy( Set<IAbi> abis, List<File> testsDirs, String suitePrefix, String suiteTag)341     public LinkedHashMap<String, IConfiguration> loadingStrategy(
342             Set<IAbi> abis, List<File> testsDirs, String suitePrefix, String suiteTag) {
343         LinkedHashMap<String, IConfiguration> loadedConfigs = new LinkedHashMap<>();
344         // Load and return directly the specific config files.
345         if (!mConfigPaths.isEmpty()) {
346             CLog.d(
347                     "Loading the specified configs path '%s' and skip loading from the resources.",
348                     mConfigPaths);
349             return getModuleLoader().loadConfigsFromSpecifiedPaths(mConfigPaths, abis, suiteTag);
350         }
351 
352         // Load configs that are part of the resources
353         if (!mSkipJarLoading) {
354             loadedConfigs.putAll(
355                     getModuleLoader().loadConfigsFromJars(abis, suitePrefix, suiteTag));
356         }
357 
358         // Load the configs that are part of the tests dir
359         if (mConfigPatterns.isEmpty()) {
360             // If no special pattern was configured, use the default configuration patterns we know
361             mConfigPatterns.add(".*\\.config$");
362             mConfigPatterns.add(".*\\.xml$");
363         }
364 
365         loadedConfigs.putAll(
366                 getModuleLoader()
367                         .loadConfigsFromDirectory(
368                                 testsDirs, abis, suitePrefix, suiteTag, mConfigPatterns));
369         return loadedConfigs;
370     }
371 
372     /** {@inheritDoc} */
373     @Override
setBuild(IBuildInfo buildInfo)374     public void setBuild(IBuildInfo buildInfo) {
375         super.setBuild(buildInfo);
376     }
377 
378     /** Sets include-filters for the compatibility test */
setIncludeFilter(Set<String> includeFilters)379     public void setIncludeFilter(Set<String> includeFilters) {
380         mIncludeFilters.addAll(includeFilters);
381     }
382 
383     /** Gets a copy of include-filters for the compatibility test */
getIncludeFilter()384     protected Set<String> getIncludeFilter() {
385         return new HashSet<String>(mIncludeFilters);
386     }
387 
388     /** Sets exclude-filters for the compatibility test */
setExcludeFilter(Set<String> excludeFilters)389     public void setExcludeFilter(Set<String> excludeFilters) {
390         mExcludeFilters.addAll(excludeFilters);
391     }
392 
393     /** Gets a copy of exclude-filters for the compatibility test */
getExcludeFilter()394     protected Set<String> getExcludeFilter() {
395         return new HashSet<String>(mExcludeFilters);
396     }
397 
398     /** Returns the current {@link SuiteModuleLoader}. */
getModuleLoader()399     public SuiteModuleLoader getModuleLoader() {
400         return mModuleRepo;
401     }
402 
403     /** Adds module args */
addModuleArgs(Set<String> moduleArgs)404     public void addModuleArgs(Set<String> moduleArgs) {
405         mModuleArgs.addAll(moduleArgs);
406     }
407 
408     /** Clear the stored module args out */
clearModuleArgs()409     void clearModuleArgs() {
410         mModuleArgs.clear();
411     }
412 
413     /** Add config patterns */
addConfigPatterns(List<String> patterns)414     public void addConfigPatterns(List<String> patterns) {
415         mConfigPatterns.addAll(patterns);
416     }
417 
418     /** Set whether or not parameterized modules are enabled or not. */
setEnableParameterizedModules(boolean enableParameter)419     public void setEnableParameterizedModules(boolean enableParameter) {
420         mEnableParameter = enableParameter;
421     }
422 
423     /** Set whether or not optional parameterized modules are enabled or not. */
setEnableOptionalParameterizedModules(boolean enableOptionalParameter)424     public void setEnableOptionalParameterizedModules(boolean enableOptionalParameter) {
425         mEnableOptionalParameter = enableOptionalParameter;
426     }
427 
setModuleParameter(ModuleParameters forceParameter)428     public void setModuleParameter(ModuleParameters forceParameter) {
429         mForceParameter = forceParameter;
430     }
431 
432     /**
433      * Create the {@link SuiteModuleLoader} responsible to load the {@link IConfiguration} and
434      * assign them some of the options.
435      *
436      * @param includeFiltersFormatted The formatted and parsed include filters.
437      * @param excludeFiltersFormatted The formatted and parsed exclude filters.
438      * @param testArgs the list of test ({@link IRemoteTest}) arguments.
439      * @param moduleArgs the list of module arguments.
440      * @return the created {@link SuiteModuleLoader}.
441      */
createModuleLoader( Map<String, List<SuiteTestFilter>> includeFiltersFormatted, Map<String, List<SuiteTestFilter>> excludeFiltersFormatted, List<String> testArgs, List<String> moduleArgs)442     public SuiteModuleLoader createModuleLoader(
443             Map<String, List<SuiteTestFilter>> includeFiltersFormatted,
444             Map<String, List<SuiteTestFilter>> excludeFiltersFormatted,
445             List<String> testArgs,
446             List<String> moduleArgs) {
447         return new SuiteModuleLoader(
448                 includeFiltersFormatted, excludeFiltersFormatted, testArgs, moduleArgs);
449     }
450 
451     /**
452      * Sets the include/exclude filters up based on if a module name was given.
453      *
454      * @throws FileNotFoundException if any file is not found.
455      */
setupFilters(File testsDir)456     protected void setupFilters(File testsDir) throws FileNotFoundException {
457         if (mModuleName == null) {
458             if (mTestName != null) {
459                 throw new IllegalArgumentException(
460                         "Test name given without module name. Add --module <module-name>");
461             }
462             return;
463         }
464         // If this option (-m / --module) is set only the matching unique module should run.
465         Set<File> modules =
466                 SuiteModuleLoader.getModuleNamesMatching(
467                         testsDir, mSuitePrefix, String.format(".*%s.*.config", mModuleName));
468         // If multiple modules match, do exact match.
469         if (modules.size() > 1) {
470             Set<File> newModules = new HashSet<>();
471             String exactModuleName = String.format("%s.config", mModuleName);
472             for (File module : modules) {
473                 if (module.getName().equals(exactModuleName)) {
474                     newModules.add(module);
475                     modules = newModules;
476                     break;
477                 }
478             }
479         }
480         if (modules.size() == 0) {
481             throw new IllegalArgumentException(
482                     String.format("No modules found matching %s", mModuleName));
483         } else if (modules.size() > 1) {
484             throw new IllegalArgumentException(
485                     String.format(
486                             "Multiple modules found matching %s:\n%s\nWhich one did you "
487                                     + "mean?\n",
488                             mModuleName, ArrayUtil.join("\n", modules)));
489         } else {
490             File mod = modules.iterator().next();
491             String moduleName = mod.getName().replace(".config", "");
492             checkFilters(mIncludeFilters, moduleName);
493             checkFilters(mExcludeFilters, moduleName);
494             mIncludeFilters.add(
495                     new SuiteTestFilter(getRequestedAbi(), moduleName, mTestName).toString());
496             // Create the matching filters for the parameterized version of it if needed.
497             if (mEnableParameter) {
498                 for (ModuleParameters param : ModuleParameters.values()) {
499                     IModuleParameter moduleParam =
500                             ModuleParametersHelper.getParameterHandler(
501                                     param, mEnableOptionalParameter);
502                     if (moduleParam == null) {
503                         continue;
504                     }
505                     if (moduleParam instanceof NegativeHandler) {
506                         continue;
507                     }
508                     String paramModuleName =
509                             String.format(
510                                     "%s[%s]", moduleName, moduleParam.getParameterIdentifier());
511                     mIncludeFilters.add(
512                             new SuiteTestFilter(getRequestedAbi(), paramModuleName, mTestName)
513                                     .toString());
514                 }
515             }
516         }
517     }
518 
519     @Override
cleanUpSuiteSetup()520     void cleanUpSuiteSetup() {
521         super.cleanUpSuiteSetup();
522         // Clean the filters because at that point they have been applied to the runners.
523         // This can save several GB of memories during sharding.
524         mIncludeFilters.clear();
525         mExcludeFilters.clear();
526         mIncludeFiltersParsed.clear();
527         mExcludeFiltersParsed.clear();
528     }
529 
530     /**
531      * Add the config path for {@link SuiteModuleLoader} to limit the search loading
532      * configurations.
533      *
534      * @param configPath A {@code File} with the absolute path of the configuration.
535      */
addConfigPaths(File configPath)536     void addConfigPaths(File configPath) {
537         mConfigPaths.add(configPath);
538     }
539 
540     /** Clear the stored config paths out. */
clearConfigPaths()541     void clearConfigPaths() {
542         mConfigPaths.clear();
543     }
544 
545     /* Helper method designed to remove filters in a list not applicable to the given module */
checkFilters(Set<String> filters, String moduleName)546     private static void checkFilters(Set<String> filters, String moduleName) {
547         Set<String> cleanedFilters = new HashSet<String>();
548         for (String filter : filters) {
549             SuiteTestFilter filterObject = SuiteTestFilter.createFrom(filter);
550             String filterName = filterObject.getName();
551             String filterBaseName = filterObject.getBaseName();
552             if (moduleName.equals(filterName) || moduleName.equals(filterBaseName)) {
553                 cleanedFilters.add(filter); // Module name matches, filter passes
554             }
555         }
556         filters.clear();
557         filters.addAll(cleanedFilters);
558     }
559 
560     /* Return a {@link boolean} for the setting of prioritize-host-config.*/
getPrioritizeHostConfig()561     boolean getPrioritizeHostConfig() {
562         return mPrioritizeHostConfig;
563     }
564 
565     /**
566      * Set option prioritize-host-config.
567      *
568      * @param prioritizeHostConfig true to prioritize host config, i.e., run host test if possible.
569      */
570     @VisibleForTesting
setPrioritizeHostConfig(boolean prioritizeHostConfig)571     protected void setPrioritizeHostConfig(boolean prioritizeHostConfig) {
572         mPrioritizeHostConfig = prioritizeHostConfig;
573     }
574 
575     /** Log a file directly to the result reporter. */
logFilterFile(File filterFile, String dataName, LogDataType type)576     private void logFilterFile(File filterFile, String dataName, LogDataType type) {
577         if (getCurrentTestLogger() == null) {
578             return;
579         }
580         try (FileInputStreamSource source = new FileInputStreamSource(filterFile)) {
581             getCurrentTestLogger().testLog(dataName, type, source);
582         }
583     }
584 }
585