1 /*
2  * Copyright (C) 2016 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.IDeviceBuildInfo;
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.config.Option;
25 import com.android.tradefed.device.DeviceNotAvailableException;
26 import com.android.tradefed.log.LogUtil.CLog;
27 import com.android.tradefed.targetprep.ITargetPreparer;
28 import com.android.tradefed.targetprep.multi.IMultiTargetPreparer;
29 import com.android.tradefed.testtype.IAbi;
30 import com.android.tradefed.testtype.IAbiReceiver;
31 import com.android.tradefed.testtype.IRemoteTest;
32 import com.android.tradefed.util.DirectedGraph;
33 import com.android.tradefed.util.StreamUtil;
34 import com.android.tradefed.util.ZipUtil2;
35 
36 import org.apache.commons.compress.archivers.zip.ZipFile;
37 
38 import java.io.File;
39 import java.io.IOException;
40 import java.util.ArrayList;
41 import java.util.Arrays;
42 import java.util.Collections;
43 import java.util.LinkedHashMap;
44 import java.util.List;
45 import java.util.Set;
46 
47 /**
48  * Implementation of {@link ITestSuite} which will load tests from TF jars res/config/suite/
49  * folder.
50  */
51 public class TfSuiteRunner extends ITestSuite {
52 
53     private static final String CONFIG_EXT = ".config";
54 
55     @Option(name = "run-suite-tag", description = "The tag that must be run.",
56             mandatory = true)
57     private String mSuiteTag = null;
58 
59     @Option(
60         name = "suite-config-prefix",
61         description = "Search only configs with given prefix for suite tags."
62     )
63     private String mSuitePrefix = null;
64 
65     @Option(
66         name = "additional-tests-zip",
67         description = "Path to a zip file containing additional tests to be loaded."
68     )
69     private String mAdditionalTestsZip = null;
70 
71     private DirectedGraph<String> mLoadedConfigGraph = null;
72 
73     /** {@inheritDoc} */
74     @Override
loadTests()75     public LinkedHashMap<String, IConfiguration> loadTests() {
76         mLoadedConfigGraph = new DirectedGraph<>();
77         return loadTests(null, mLoadedConfigGraph);
78     }
79 
80     /**
81      * Internal load configuration. Load configuration, expand sub suite runners and track cycle
82      * inclusion to prevent infinite recursion.
83      *
84      * @param parentConfig the name of the config being loaded.
85      * @param graph the directed graph tracking inclusion.
86      * @return a map of module name and the configuration associated.
87      */
loadTests( String parentConfig, DirectedGraph<String> graph)88     private LinkedHashMap<String, IConfiguration> loadTests(
89             String parentConfig, DirectedGraph<String> graph) {
90         LinkedHashMap <String, IConfiguration> configMap =
91                 new LinkedHashMap<String, IConfiguration>();
92         IConfigurationFactory configFactory = ConfigurationFactory.getInstance();
93         // TODO: Do a better job searching for configs.
94         // We do not load config from environment, they should be inside the testsDir of the build
95         // info.
96         List<String> configs = configFactory.getConfigList(mSuitePrefix, false);
97         Set<IAbi> abis = null;
98         if (getBuildInfo() instanceof IDeviceBuildInfo) {
99             IDeviceBuildInfo deviceBuildInfo = (IDeviceBuildInfo) getBuildInfo();
100             File testsDir = deviceBuildInfo.getTestsDir();
101             if (testsDir != null) {
102                 if (mAdditionalTestsZip != null) {
103                     CLog.d(
104                             "Extract general-tests.zip (%s) to tests directory.",
105                             mAdditionalTestsZip);
106                     ZipFile zip = null;
107                     try {
108                         zip = new ZipFile(mAdditionalTestsZip);
109                         ZipUtil2.extractZip(zip, testsDir);
110                     } catch (IOException e) {
111                         RuntimeException runtimeException =
112                                 new RuntimeException(
113                                         String.format(
114                                                 "IO error (%s) when unzipping general-tests.zip",
115                                                 e.toString()),
116                                         e);
117                         throw runtimeException;
118                     } finally {
119                         StreamUtil.close(zip);
120                     }
121                 }
122 
123                 CLog.d(
124                         "Loading extra test configs from the tests directory: %s",
125                         testsDir.getAbsolutePath());
126                 List<File> extraTestCasesDirs = Arrays.asList(testsDir);
127                 configs.addAll(
128                         ConfigurationUtil.getConfigNamesFromDirs(mSuitePrefix, extraTestCasesDirs));
129             }
130         }
131         // Sort configs to ensure they are always evaluated and added in the same order.
132         Collections.sort(configs);
133         for (String configName : configs) {
134             try {
135                 IConfiguration testConfig =
136                         configFactory.createConfigurationFromArgs(new String[]{configName});
137                 if (testConfig.getConfigurationDescription().getSuiteTags().contains(mSuiteTag)) {
138                     // If this config supports running against different ABIs we need to queue up
139                     // multiple instances of this config.
140                     if (canRunMultipleAbis(testConfig)) {
141                         if (abis == null) {
142                             try {
143                                 abis = getAbis(getDevice());
144                             } catch (DeviceNotAvailableException e) {
145                                 throw new RuntimeException(e);
146                             }
147                         }
148                         for (IAbi abi : abis) {
149                             testConfig =
150                                     configFactory.createConfigurationFromArgs(
151                                             new String[] {configName});
152                             String configNameAbi = abi.getName() + " " + configName;
153                             for (IRemoteTest test : testConfig.getTests()) {
154                                 if (test instanceof IAbiReceiver) {
155                                     ((IAbiReceiver) test).setAbi(abi);
156                                 }
157                             }
158                             for (ITargetPreparer preparer : testConfig.getTargetPreparers()) {
159                                 if (preparer instanceof IAbiReceiver) {
160                                     ((IAbiReceiver) preparer).setAbi(abi);
161                                 }
162                             }
163                             for (IMultiTargetPreparer preparer :
164                                     testConfig.getMultiTargetPreparers()) {
165                                 if (preparer instanceof IAbiReceiver) {
166                                     ((IAbiReceiver) preparer).setAbi(abi);
167                                 }
168                             }
169                             LinkedHashMap<String, IConfiguration> expandedConfig =
170                                     expandTestSuites(
171                                             configNameAbi, testConfig, parentConfig, graph);
172                             configMap.putAll(expandedConfig);
173                         }
174                     } else {
175                         LinkedHashMap<String, IConfiguration> expandedConfig =
176                                 expandTestSuites(configName, testConfig, parentConfig, graph);
177                         configMap.putAll(expandedConfig);
178                     }
179                 }
180             } catch (ConfigurationException | NoClassDefFoundError e) {
181                 // Do not print the stack it's too verbose.
182                 CLog.e("Configuration '%s' cannot be loaded, ignoring.", configName);
183             }
184         }
185         return configMap;
186     }
187 
188     /** Helper to determine if a test configuration can be ran against multiple ABIs. */
canRunMultipleAbis(IConfiguration testConfig)189     private boolean canRunMultipleAbis(IConfiguration testConfig) {
190         for (IRemoteTest test : testConfig.getTests()) {
191             if (test instanceof IAbiReceiver) {
192                 return true;
193             }
194         }
195         for (ITargetPreparer preparer : testConfig.getTargetPreparers()) {
196             if (preparer instanceof IAbiReceiver) {
197                 return true;
198             }
199         }
200         for (IMultiTargetPreparer preparer : testConfig.getMultiTargetPreparers()) {
201             if (preparer instanceof IAbiReceiver) {
202                 return true;
203             }
204         }
205         return false;
206     }
207 
208     /**
209      * Helper to expand all TfSuiteRunner included in sub-configuration. Avoid having module inside
210      * module if a suite is ran as part of another suite. Verifies against circular suite
211      * dependencies.
212      */
expandTestSuites( String configName, IConfiguration config, String parentConfig, DirectedGraph<String> graph)213     private LinkedHashMap<String, IConfiguration> expandTestSuites(
214             String configName,
215             IConfiguration config,
216             String parentConfig,
217             DirectedGraph<String> graph) {
218         LinkedHashMap<String, IConfiguration> configMap =
219                 new LinkedHashMap<String, IConfiguration>();
220         List<IRemoteTest> tests = new ArrayList<>(config.getTests());
221         // In case some sub-config are suite too, we expand them to avoid weirdness
222         // of modules inside modules.
223         if (parentConfig != null) {
224             graph.addEdge(parentConfig, configName);
225             if (!graph.isDag()) {
226                 CLog.e("%s", graph);
227                 throw new RuntimeException(
228                         String.format(
229                                 "Circular configuration detected: %s has been included "
230                                         + "several times.",
231                                 configName));
232             }
233         }
234         for (IRemoteTest test : tests) {
235             if (test instanceof TfSuiteRunner) {
236                 TfSuiteRunner runner = (TfSuiteRunner) test;
237                 // Suite runner can only load and run stuff, if it has a suite tag set.
238                 if (runner.getSuiteTag() != null) {
239                     LinkedHashMap<String, IConfiguration> subConfigs =
240                             runner.loadTests(configName, graph);
241                     configMap.putAll(subConfigs);
242                 } else {
243                     CLog.w(
244                             "Config %s does not have a suite-tag it cannot run anything.",
245                             configName);
246                 }
247                 config.getTests().remove(test);
248             }
249         }
250         // If we have any IRemoteTests remaining in the base configuration, it will run.
251         if (!config.getTests().isEmpty()) {
252             configMap.put(sanitizeConfigName(configName), config);
253         }
254         return configMap;
255     }
256 
getSuiteTag()257     private String getSuiteTag() {
258         return mSuiteTag;
259     }
260 
261     /**
262      * Sanitize the config name by sanitizing the module name and reattaching the abi if it is part
263      * of the config name.
264      */
sanitizeConfigName(String originalName)265     private String sanitizeConfigName(String originalName) {
266         String[] segments;
267         if (originalName.contains(" ")) {
268             segments = originalName.split(" ");
269             return segments[0] + " " + sanitizeModuleName(segments[1]);
270         }
271         return sanitizeModuleName(originalName);
272     }
273 
274     /**
275      * Some module names are currently the absolute path name of some config files. We want to
276      * sanitize that look more like a short included config name.
277      */
sanitizeModuleName(String originalName)278     private String sanitizeModuleName(String originalName) {
279         if (originalName.endsWith(CONFIG_EXT)) {
280             originalName = originalName.substring(0, originalName.length() - CONFIG_EXT.length());
281         }
282         if (!originalName.startsWith("/")) {
283             return originalName;
284         }
285         // if it's an absolute path
286         String[] segments = originalName.split("/");
287         if (segments.length < 3) {
288             return originalName;
289         }
290         // return last two segments only
291         return String.join("/", segments[segments.length - 2], segments[segments.length - 1]);
292     }
293 }
294