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