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