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