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