1 /* 2 * Copyright (C) 2010 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 17 package com.android.tradefed.config; 18 19 import com.android.tradefed.device.metric.IMetricCollector; 20 import com.android.tradefed.log.LogUtil.CLog; 21 22 import java.io.File; 23 import java.lang.reflect.InvocationTargetException; 24 import java.util.ArrayList; 25 import java.util.HashMap; 26 import java.util.LinkedHashMap; 27 import java.util.List; 28 import java.util.Map; 29 import java.util.Set; 30 import java.util.regex.Matcher; 31 import java.util.regex.Pattern; 32 import java.util.stream.Collectors; 33 34 /** 35 * Holds a record of a configuration, its associated objects and their options. 36 */ 37 public class ConfigurationDef { 38 39 /** 40 * a map of object type names to config object class name(s). Use LinkedHashMap to keep objects 41 * in the same order they were added. 42 */ 43 private final Map<String, List<ConfigObjectDef>> mObjectClassMap = new LinkedHashMap<>(); 44 45 /** a list of option name/value pairs. */ 46 private final List<OptionDef> mOptionList = new ArrayList<>(); 47 48 /** a cache of the frequency of every classname */ 49 private final Map<String, Integer> mClassFrequency = new HashMap<>(); 50 51 /** The set of files (and modification times) that were used to load this config */ 52 private final Map<File, Long> mSourceFiles = new HashMap<>(); 53 54 /** 55 * Object to hold info for a className and the appearance number it has (e.g. if a config has 56 * the same object twice, the first one will have the first appearance number). 57 */ 58 public static class ConfigObjectDef { 59 final String mClassName; 60 final Integer mAppearanceNum; 61 ConfigObjectDef(String className, Integer appearance)62 ConfigObjectDef(String className, Integer appearance) { 63 mClassName = className; 64 mAppearanceNum = appearance; 65 } 66 } 67 68 private boolean mMultiDeviceMode = false; 69 private boolean mFilteredObjects = false; 70 private Map<String, Boolean> mExpectedDevices = new LinkedHashMap<>(); 71 private static final Pattern MULTI_PATTERN = Pattern.compile("(.*)(:)(.*)"); 72 public static final String DEFAULT_DEVICE_NAME = "DEFAULT_DEVICE"; 73 74 /** the unique name of the configuration definition */ 75 private final String mName; 76 77 /** a short description of the configuration definition */ 78 private String mDescription = ""; 79 ConfigurationDef(String name)80 public ConfigurationDef(String name) { 81 mName = name; 82 } 83 84 /** 85 * Returns a short description of the configuration 86 */ getDescription()87 public String getDescription() { 88 return mDescription; 89 } 90 91 /** Sets the configuration definition description */ setDescription(String description)92 public void setDescription(String description) { 93 mDescription = description; 94 } 95 96 /** 97 * Adds a config object to the definition 98 * 99 * @param typeName the config object type name 100 * @param className the class name of the config object 101 * @return the number of times this className has appeared in this {@link ConfigurationDef}, 102 * including this time. Because all {@link ConfigurationDef} methods return these classes 103 * with a constant ordering, this index can serve as a unique identifier for the just-added 104 * instance of <code>clasName</code>. 105 */ addConfigObjectDef(String typeName, String className)106 public int addConfigObjectDef(String typeName, String className) { 107 List<ConfigObjectDef> classList = mObjectClassMap.get(typeName); 108 if (classList == null) { 109 classList = new ArrayList<ConfigObjectDef>(); 110 mObjectClassMap.put(typeName, classList); 111 } 112 113 // Increment and store count for this className 114 Integer freq = mClassFrequency.get(className); 115 freq = freq == null ? 1 : freq + 1; 116 mClassFrequency.put(className, freq); 117 classList.add(new ConfigObjectDef(className, freq)); 118 119 return freq; 120 } 121 122 /** 123 * Adds option to the definition 124 * 125 * @param optionName the name of the option 126 * @param optionValue the option value 127 */ addOptionDef( String optionName, String optionKey, String optionValue, String optionSource, String type)128 public void addOptionDef( 129 String optionName, 130 String optionKey, 131 String optionValue, 132 String optionSource, 133 String type) { 134 mOptionList.add(new OptionDef(optionName, optionKey, optionValue, optionSource, type)); 135 } 136 addOptionDef(String optionName, String optionKey, String optionValue, String optionSource)137 void addOptionDef(String optionName, String optionKey, String optionValue, 138 String optionSource) { 139 mOptionList.add(new OptionDef(optionName, optionKey, optionValue, optionSource, null)); 140 } 141 142 /** 143 * Registers a source file that was used while loading this {@link ConfigurationDef}. 144 */ registerSource(File source)145 void registerSource(File source) { 146 mSourceFiles.put(source, source.lastModified()); 147 } 148 149 /** 150 * Determine whether any of the source files have changed since this {@link ConfigurationDef} 151 * was loaded. 152 */ isStale()153 boolean isStale() { 154 for (Map.Entry<File, Long> entry : mSourceFiles.entrySet()) { 155 if (entry.getKey().lastModified() > entry.getValue()) { 156 return true; 157 } 158 } 159 return false; 160 } 161 162 /** 163 * Get the object type name-class map. 164 * 165 * <p>Exposed for unit testing 166 */ getObjectClassMap()167 Map<String, List<ConfigObjectDef>> getObjectClassMap() { 168 return mObjectClassMap; 169 } 170 171 /** 172 * Get the option name-value map. 173 * <p/> 174 * Exposed for unit testing 175 */ getOptionList()176 List<OptionDef> getOptionList() { 177 return mOptionList; 178 } 179 180 /** 181 * Creates a configuration from the info stored in this definition, and populates its fields 182 * with the provided option values. 183 * 184 * @return the created {@link IConfiguration} 185 * @throws ConfigurationException if configuration could not be created 186 */ createConfiguration()187 public IConfiguration createConfiguration() throws ConfigurationException { 188 return createConfiguration(null); 189 } 190 191 /** 192 * Creates a configuration from the info stored in this definition, and populates its fields 193 * with the provided option values. 194 * 195 * @param allowedObjects the set of TF objects that we will create out of the full configuration 196 * @return the created {@link IConfiguration} 197 * @throws ConfigurationException if configuration could not be created 198 */ createConfiguration(Set<String> allowedObjects)199 public IConfiguration createConfiguration(Set<String> allowedObjects) 200 throws ConfigurationException { 201 mFilteredObjects = false; 202 IConfiguration config = new Configuration(getName(), getDescription()); 203 List<IDeviceConfiguration> deviceObjectList = new ArrayList<IDeviceConfiguration>(); 204 IDeviceConfiguration defaultDeviceConfig = 205 new DeviceConfigurationHolder(DEFAULT_DEVICE_NAME); 206 boolean hybridMultiDeviceHandling = false; 207 208 if (!mMultiDeviceMode) { 209 // We still populate a default device config to avoid special logic in the rest of the 210 // harness. 211 deviceObjectList.add(defaultDeviceConfig); 212 } else { 213 // FIXME: handle this in a more generic way. 214 // Get the number of real device (non build-only) device 215 Long numDut = 216 mExpectedDevices 217 .values() 218 .stream() 219 .filter(value -> (value == false)) 220 .collect(Collectors.counting()); 221 Long numNonDut = 222 mExpectedDevices 223 .values() 224 .stream() 225 .filter(value -> (value == true)) 226 .collect(Collectors.counting()); 227 if (numDut == 0 && numNonDut == 0) { 228 throw new ConfigurationException("No device detected. Should not happen."); 229 } 230 if (numNonDut > 0 && numDut == 0) { 231 // if we only have fake devices, use the default device as real device, and add it 232 // first. 233 Map<String, Boolean> copy = new LinkedHashMap<>(); 234 copy.put(DEFAULT_DEVICE_NAME, false); 235 copy.putAll(mExpectedDevices); 236 mExpectedDevices = copy; 237 numDut++; 238 } 239 if (numNonDut > 0 && numDut == 1) { 240 // If we have fake device but only a single real device, is the only use case to 241 // handle very differently: object at the root of the xml needs to be associated 242 // with the only DuT. 243 // All the other use cases can be handled the regular way. 244 CLog.d( 245 "One device is under tests while config '%s' requires some fake=true " 246 + "devices. Using hybrid parsing of config.", 247 getName()); 248 hybridMultiDeviceHandling = true; 249 } 250 for (String name : mExpectedDevices.keySet()) { 251 deviceObjectList.add( 252 new DeviceConfigurationHolder(name, mExpectedDevices.get(name))); 253 } 254 } 255 256 Map<String, String> rejectedObjects = new HashMap<>(); 257 Throwable cause = null; 258 259 for (Map.Entry<String, List<ConfigObjectDef>> objClassEntry : mObjectClassMap.entrySet()) { 260 List<Object> objectList = new ArrayList<Object>(objClassEntry.getValue().size()); 261 String entryName = objClassEntry.getKey(); 262 boolean shouldAddToFlatConfig = true; 263 264 for (ConfigObjectDef configDef : objClassEntry.getValue()) { 265 if (allowedObjects != null && !allowedObjects.contains(objClassEntry.getKey())) { 266 CLog.d("Skipping creation of %s", objClassEntry.getKey()); 267 mFilteredObjects = true; 268 continue; 269 } 270 Object configObject = null; 271 try { 272 configObject = createObject(objClassEntry.getKey(), configDef.mClassName); 273 } catch (ClassNotFoundConfigurationException e) { 274 // Store all the loading failure 275 cause = e.getCause(); 276 rejectedObjects.putAll(e.getRejectedObjects()); 277 CLog.e(e); 278 // Don't add in case of issue 279 shouldAddToFlatConfig = false; 280 continue; 281 } 282 Matcher matcher = null; 283 if (mMultiDeviceMode) { 284 matcher = MULTI_PATTERN.matcher(entryName); 285 } 286 if (mMultiDeviceMode && matcher.find()) { 287 // If we find the device namespace, fetch the matching device or create it if 288 // it doesn't exists. 289 IDeviceConfiguration multiDev = null; 290 shouldAddToFlatConfig = false; 291 for (IDeviceConfiguration iDevConfig : deviceObjectList) { 292 if (matcher.group(1).equals(iDevConfig.getDeviceName())) { 293 multiDev = iDevConfig; 294 break; 295 } 296 } 297 if (multiDev == null) { 298 multiDev = new DeviceConfigurationHolder(matcher.group(1)); 299 deviceObjectList.add(multiDev); 300 } 301 // We reference the original object to the device and not to the flat list. 302 multiDev.addSpecificConfig(configObject); 303 multiDev.addFrequency(configObject, configDef.mAppearanceNum); 304 } else { 305 if (Configuration.doesBuiltInObjSupportMultiDevice(entryName)) { 306 if (hybridMultiDeviceHandling) { 307 // Special handling for a multi-device with one Dut and the rest are 308 // non-dut devices. 309 // At this point we are ensured to have only one Dut device. Object at 310 // the root should are associated with the only device under test (Dut). 311 List<IDeviceConfiguration> realDevice = 312 deviceObjectList 313 .stream() 314 .filter(object -> (object.isFake() == false)) 315 .collect(Collectors.toList()); 316 if (realDevice.size() != 1) { 317 throw new ConfigurationException( 318 String.format( 319 "Something went very bad, we found '%s' Dut " 320 + "device while expecting one only.", 321 realDevice.size())); 322 } 323 realDevice.get(0).addSpecificConfig(configObject); 324 realDevice.get(0).addFrequency(configObject, configDef.mAppearanceNum); 325 } else { 326 // Regular handling of object for single device situation. 327 defaultDeviceConfig.addSpecificConfig(configObject); 328 defaultDeviceConfig.addFrequency( 329 configObject, configDef.mAppearanceNum); 330 } 331 } else { 332 // Only add to flat list if they are not part of multi device config. 333 objectList.add(configObject); 334 } 335 } 336 } 337 if (shouldAddToFlatConfig) { 338 config.setConfigurationObjectList(entryName, objectList); 339 } 340 } 341 342 checkRejectedObjects(rejectedObjects, cause); 343 344 // We always add the device configuration list so we can rely on it everywhere 345 config.setConfigurationObjectList(Configuration.DEVICE_NAME, deviceObjectList); 346 injectOptions(config, mOptionList); 347 348 return config; 349 } 350 351 /** Evaluate rejected objects map, if any throw an exception. */ checkRejectedObjects(Map<String, String> rejectedObjects, Throwable cause)352 protected void checkRejectedObjects(Map<String, String> rejectedObjects, Throwable cause) 353 throws ClassNotFoundConfigurationException { 354 // Send all the objects that failed the loading. 355 if (!rejectedObjects.isEmpty()) { 356 throw new ClassNotFoundConfigurationException( 357 String.format( 358 "Failed to load some objects in the configuration '%s': %s", 359 getName(), rejectedObjects), 360 cause, 361 rejectedObjects); 362 } 363 } 364 injectOptions(IConfiguration config, List<OptionDef> optionList)365 protected void injectOptions(IConfiguration config, List<OptionDef> optionList) 366 throws ConfigurationException { 367 if (mFilteredObjects) { 368 // If we filtered out some objects, some options might not be injectable anymore, so 369 // we switch to safe inject to avoid errors due to the filtering. 370 config.safeInjectOptionValues(optionList); 371 } else { 372 config.injectOptionValues(optionList); 373 } 374 } 375 376 /** 377 * Creates a global configuration from the info stored in this definition, and populates its 378 * fields with the provided option values. 379 * 380 * @return the created {@link IGlobalConfiguration} 381 * @throws ConfigurationException if configuration could not be created 382 */ createGlobalConfiguration()383 IGlobalConfiguration createGlobalConfiguration() throws ConfigurationException { 384 IGlobalConfiguration config = new GlobalConfiguration(getName(), getDescription()); 385 386 for (Map.Entry<String, List<ConfigObjectDef>> objClassEntry : mObjectClassMap.entrySet()) { 387 List<Object> objectList = new ArrayList<Object>(objClassEntry.getValue().size()); 388 for (ConfigObjectDef configDef : objClassEntry.getValue()) { 389 Object configObject = createObject(objClassEntry.getKey(), configDef.mClassName); 390 objectList.add(configObject); 391 } 392 config.setConfigurationObjectList(objClassEntry.getKey(), objectList); 393 } 394 for (OptionDef optionEntry : mOptionList) { 395 config.injectOptionValue(optionEntry.name, optionEntry.key, optionEntry.value); 396 } 397 398 return config; 399 } 400 401 /** 402 * Gets the name of this configuration definition 403 * 404 * @return name of this configuration. 405 */ getName()406 public String getName() { 407 return mName; 408 } 409 setMultiDeviceMode(boolean multiDeviceMode)410 public void setMultiDeviceMode(boolean multiDeviceMode) { 411 mMultiDeviceMode = multiDeviceMode; 412 } 413 414 /** Returns whether or not the recorded configuration is multi-device or not. */ isMultiDeviceMode()415 public boolean isMultiDeviceMode() { 416 return mMultiDeviceMode; 417 } 418 419 /** Add a device that needs to be tracked and whether or not it's real. */ addExpectedDevice(String deviceName, boolean isFake)420 public String addExpectedDevice(String deviceName, boolean isFake) { 421 Boolean previous = mExpectedDevices.put(deviceName, isFake); 422 if (previous != null && previous != isFake) { 423 return String.format( 424 "Mismatch for device '%s'. It was defined once as isFake=false, once as " 425 + "isFake=true", 426 deviceName); 427 } 428 return null; 429 } 430 431 /** Returns the current Map of tracked devices and if they are real or not. */ getExpectedDevices()432 public Map<String, Boolean> getExpectedDevices() { 433 return mExpectedDevices; 434 } 435 436 /** 437 * Creates a config object associated with this definition. 438 * 439 * @param objectTypeName the name of the object. Used to generate more descriptive error 440 * messages 441 * @param className the class name of the object to load 442 * @return the config object 443 * @throws ConfigurationException if config object could not be created 444 */ createObject(String objectTypeName, String className)445 private Object createObject(String objectTypeName, String className) 446 throws ConfigurationException { 447 try { 448 Class<?> objectClass = getClassForObject(objectTypeName, className); 449 Object configObject = objectClass.getDeclaredConstructor().newInstance(); 450 checkObjectValid(objectTypeName, configObject); 451 return configObject; 452 } catch (InstantiationException | InvocationTargetException | NoSuchMethodException e) { 453 throw new ConfigurationException(String.format( 454 "Could not instantiate class %s for config object type %s", className, 455 objectTypeName), e); 456 } catch (IllegalAccessException e) { 457 throw new ConfigurationException(String.format( 458 "Could not access class %s for config object type %s", className, 459 objectTypeName), e); 460 } 461 } 462 463 /** 464 * Loads the class for the given the config object associated with this definition. 465 * 466 * @param objectTypeName the name of the config object type. Used to generate more descriptive 467 * error messages 468 * @param className the class name of the object to load 469 * @return the config object populated with default option values 470 * @throws ClassNotFoundConfigurationException if config object could not be created 471 */ getClassForObject(String objectTypeName, String className)472 private Class<?> getClassForObject(String objectTypeName, String className) 473 throws ClassNotFoundConfigurationException { 474 try { 475 return Class.forName(className); 476 } catch (ClassNotFoundException e) { 477 ClassNotFoundConfigurationException exception = 478 new ClassNotFoundConfigurationException( 479 String.format( 480 "Could not find class %s for config object type %s", 481 className, objectTypeName), 482 e, 483 className, 484 objectTypeName); 485 throw exception; 486 } 487 } 488 489 /** 490 * Check that the loaded object does not present some incoherence. Some combination should not 491 * be done. For example: metric_collectors does extend ITestInvocationListener and could be 492 * declared as a result_reporter, but we do not allow it because it's not how it should be used 493 * in the invocation. 494 * 495 * @param objectTypeName The type of the object declared in the xml. 496 * @param configObject The instantiated object. 497 * @throws ConfigurationException if we find an incoherence in the object. 498 */ checkObjectValid(String objectTypeName, Object configObject)499 private void checkObjectValid(String objectTypeName, Object configObject) 500 throws ConfigurationException { 501 if (Configuration.RESULT_REPORTER_TYPE_NAME.equals(objectTypeName) 502 && configObject instanceof IMetricCollector) { 503 // we do not allow IMetricCollector as result_reporter. 504 throw new ConfigurationException( 505 String.format( 506 "Object of type %s was declared as %s.", 507 Configuration.DEVICE_METRICS_COLLECTOR_TYPE_NAME, 508 Configuration.RESULT_REPORTER_TYPE_NAME)); 509 } 510 } 511 } 512