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 String objectWithoutNamespace = objClassEntry.getKey(); 271 if (objectWithoutNamespace.contains(":")) { 272 objectWithoutNamespace = objectWithoutNamespace.split(":")[1]; 273 } 274 if (allowedObjects != null 275 && !allowedObjects.contains(objectWithoutNamespace)) { 276 CLog.d("Skipping creation of %s", objectWithoutNamespace); 277 mFilteredObjects = true; 278 continue; 279 } 280 Object configObject = null; 281 try { 282 configObject = createObject(objClassEntry.getKey(), configDef.mClassName); 283 } catch (ClassNotFoundConfigurationException e) { 284 // Store all the loading failure 285 cause = e.getCause(); 286 rejectedObjects.putAll(e.getRejectedObjects()); 287 CLog.e(e); 288 // Don't add in case of issue 289 shouldAddToFlatConfig = false; 290 continue; 291 } 292 Matcher matcher = null; 293 if (mMultiDeviceMode) { 294 matcher = MULTI_PATTERN.matcher(entryName); 295 } 296 if (mMultiDeviceMode && matcher.find()) { 297 // If we find the device namespace, fetch the matching device or create it 298 // if it doesn't exist. 299 IDeviceConfiguration multiDev = null; 300 shouldAddToFlatConfig = false; 301 for (IDeviceConfiguration iDevConfig : deviceObjectList) { 302 if (matcher.group(1).equals(iDevConfig.getDeviceName())) { 303 multiDev = iDevConfig; 304 break; 305 } 306 } 307 if (multiDev == null) { 308 multiDev = new DeviceConfigurationHolder(matcher.group(1)); 309 deviceObjectList.add(multiDev); 310 } 311 // We reference the original object to the device and not to the flat list. 312 multiDev.addSpecificConfig(configObject, matcher.group(2)); 313 multiDev.addFrequency(configObject, configDef.mAppearanceNum); 314 } else { 315 if (Configuration.doesBuiltInObjSupportMultiDevice(entryName)) { 316 if (hybridMultiDeviceHandling) { 317 // Special handling for a multi-device with one Dut and the rest are 318 // non-dut devices. 319 // At this point we are ensured to have only one Dut device. Object 320 // at 321 // the root should are associated with the only device under test 322 // (Dut). 323 List<IDeviceConfiguration> realDevice = 324 deviceObjectList.stream() 325 .filter(object -> (object.isFake() == false)) 326 .collect(Collectors.toList()); 327 if (realDevice.size() != 1) { 328 throw new ConfigurationException( 329 String.format( 330 "Something went very bad, we found '%s' Dut " 331 + "device while expecting one only.", 332 realDevice.size())); 333 } 334 realDevice.get(0).addSpecificConfig(configObject, entryName); 335 realDevice 336 .get(0) 337 .addFrequency(configObject, configDef.mAppearanceNum); 338 } else { 339 // Regular handling of object for single device situation. 340 defaultDeviceConfig.addSpecificConfig(configObject, entryName); 341 defaultDeviceConfig.addFrequency( 342 configObject, configDef.mAppearanceNum); 343 } 344 } else { 345 // Only add to flat list if they are not part of multi device config. 346 objectList.add(configObject); 347 } 348 } 349 } 350 if (shouldAddToFlatConfig) { 351 config.setConfigurationObjectList(entryName, objectList); 352 } 353 } 354 355 checkRejectedObjects(rejectedObjects, cause); 356 357 // We always add the device configuration list so we can rely on it everywhere 358 config.setConfigurationObjectList(Configuration.DEVICE_NAME, deviceObjectList); 359 injectOptions(config, mOptionList); 360 361 List<ITargetPreparer> notILab = new ArrayList<>(); 362 for (IDeviceConfiguration deviceConfig : config.getDeviceConfig()) { 363 for (ITargetPreparer labPreparer : deviceConfig.getLabPreparers()) { 364 if (!(labPreparer instanceof ILabPreparer)) { 365 notILab.add(labPreparer); 366 } 367 } 368 } 369 if (!notILab.isEmpty()) { 370 throw new ConfigurationException( 371 String.format( 372 "The following were specified as lab_preparer " 373 + "but aren't ILabPreparer: %s", 374 notILab), 375 InfraErrorIdentifier.OPTION_CONFIGURATION_ERROR); 376 } 377 return config; 378 } 379 } 380 381 /** Evaluate rejected objects map, if any throw an exception. */ checkRejectedObjects(Map<String, String> rejectedObjects, Throwable cause)382 protected void checkRejectedObjects(Map<String, String> rejectedObjects, Throwable cause) 383 throws ClassNotFoundConfigurationException { 384 // Send all the objects that failed the loading. 385 if (!rejectedObjects.isEmpty()) { 386 throw new ClassNotFoundConfigurationException( 387 String.format( 388 "Failed to load some objects in the configuration '%s': %s", 389 getName(), rejectedObjects), 390 cause, 391 InfraErrorIdentifier.CLASS_NOT_FOUND, 392 rejectedObjects); 393 } 394 } 395 injectOptions(IConfiguration config, List<OptionDef> optionList)396 protected void injectOptions(IConfiguration config, List<OptionDef> optionList) 397 throws ConfigurationException { 398 if (mFilteredObjects) { 399 // If we filtered out some objects, some options might not be injectable anymore, so 400 // we switch to safe inject to avoid errors due to the filtering. 401 config.safeInjectOptionValues(optionList); 402 } else { 403 config.injectOptionValues(optionList); 404 } 405 } 406 407 /** 408 * Creates a global configuration from the info stored in this definition, and populates its 409 * fields with the provided option values. 410 * 411 * @return the created {@link IGlobalConfiguration} 412 * @throws ConfigurationException if configuration could not be created 413 */ createGlobalConfiguration()414 IGlobalConfiguration createGlobalConfiguration() throws ConfigurationException { 415 try (CloseableTraceScope ignored = 416 new CloseableTraceScope("createGlobalConfigurationObjects")) { 417 IGlobalConfiguration config = new GlobalConfiguration(getName(), getDescription()); 418 419 for (Map.Entry<String, List<ConfigObjectDef>> objClassEntry : 420 mObjectClassMap.entrySet()) { 421 List<Object> objectList = new ArrayList<Object>(objClassEntry.getValue().size()); 422 for (ConfigObjectDef configDef : objClassEntry.getValue()) { 423 Object configObject = 424 createObject(objClassEntry.getKey(), configDef.mClassName); 425 objectList.add(configObject); 426 } 427 config.setConfigurationObjectList(objClassEntry.getKey(), objectList); 428 } 429 for (OptionDef optionEntry : mOptionList) { 430 config.injectOptionValue(optionEntry.name, optionEntry.key, optionEntry.value); 431 } 432 433 return config; 434 } 435 } 436 437 /** 438 * Gets the name of this configuration definition 439 * 440 * @return name of this configuration. 441 */ getName()442 public String getName() { 443 return mName; 444 } 445 setMultiDeviceMode(boolean multiDeviceMode)446 public void setMultiDeviceMode(boolean multiDeviceMode) { 447 mMultiDeviceMode = multiDeviceMode; 448 } 449 450 /** Returns whether or not the recorded configuration is multi-device or not. */ isMultiDeviceMode()451 public boolean isMultiDeviceMode() { 452 return mMultiDeviceMode; 453 } 454 455 /** Add a device that needs to be tracked and whether or not it's real. */ addExpectedDevice(String deviceName, boolean isFake)456 public String addExpectedDevice(String deviceName, boolean isFake) { 457 Boolean previous = mExpectedDevices.put(deviceName, isFake); 458 if (previous != null && previous != isFake) { 459 return String.format( 460 "Mismatch for device '%s'. It was defined once as isFake=false, once as " 461 + "isFake=true", 462 deviceName); 463 } 464 return null; 465 } 466 467 /** Returns the current Map of tracked devices and if they are real or not. */ getExpectedDevices()468 public Map<String, Boolean> getExpectedDevices() { 469 return mExpectedDevices; 470 } 471 472 /** 473 * Creates a config object associated with this definition. 474 * 475 * @param objectTypeName the name of the object. Used to generate more descriptive error 476 * messages 477 * @param className the class name of the object to load 478 * @return the config object 479 * @throws ConfigurationException if config object could not be created 480 */ createObject(String objectTypeName, String className)481 private Object createObject(String objectTypeName, String className) 482 throws ConfigurationException { 483 try { 484 Class<?> objectClass = getClassForObject(objectTypeName, className); 485 Object configObject = objectClass.getDeclaredConstructor().newInstance(); 486 checkObjectValid(objectTypeName, className, configObject); 487 return configObject; 488 } catch (InstantiationException | InvocationTargetException | NoSuchMethodException e) { 489 throw new ConfigurationException(String.format( 490 "Could not instantiate class %s for config object type %s", className, 491 objectTypeName), e); 492 } catch (IllegalAccessException e) { 493 throw new ConfigurationException(String.format( 494 "Could not access class %s for config object type %s", className, 495 objectTypeName), e); 496 } 497 } 498 499 /** 500 * Loads the class for the given the config object associated with this definition. 501 * 502 * @param objectTypeName the name of the config object type. Used to generate more descriptive 503 * error messages 504 * @param className the class name of the object to load 505 * @return the config object populated with default option values 506 * @throws ClassNotFoundConfigurationException if config object could not be created 507 */ getClassForObject(String objectTypeName, String className)508 private Class<?> getClassForObject(String objectTypeName, String className) 509 throws ClassNotFoundConfigurationException { 510 try { 511 return Class.forName(className); 512 } catch (ClassNotFoundException e) { 513 ClassNotFoundConfigurationException exception = 514 new ClassNotFoundConfigurationException( 515 String.format( 516 "Could not find class %s for config object type %s", 517 className, objectTypeName), 518 e, 519 InfraErrorIdentifier.CLASS_NOT_FOUND, 520 className, 521 objectTypeName); 522 throw exception; 523 } 524 } 525 526 /** 527 * Check that the loaded object does not present some incoherence. Some combination should not 528 * be done. For example: metric_collectors does extend ITestInvocationListener and could be 529 * declared as a result_reporter, but we do not allow it because it's not how it should be used 530 * in the invocation. 531 * 532 * @param objectTypeName The type of the object declared in the xml. 533 * @param className The string classname that was instantiated 534 * @param configObject The instantiated object. 535 * @throws ConfigurationException if we find an incoherence in the object. 536 */ checkObjectValid(String objectTypeName, String className, Object configObject)537 private void checkObjectValid(String objectTypeName, String className, Object configObject) 538 throws ConfigurationException { 539 if (configObject == null) { 540 throw new ConfigurationException( 541 String.format( 542 "Class %s for type %s didn't instantiate properly", 543 className, objectTypeName), 544 InfraErrorIdentifier.OPTION_CONFIGURATION_ERROR); 545 } 546 if (Configuration.RESULT_REPORTER_TYPE_NAME.equals(objectTypeName) 547 && configObject instanceof IMetricCollector) { 548 // we do not allow IMetricCollector as result_reporter. 549 throw new ConfigurationException( 550 String.format( 551 "Object of type %s was declared as %s.", 552 Configuration.DEVICE_METRICS_COLLECTOR_TYPE_NAME, 553 Configuration.RESULT_REPORTER_TYPE_NAME), 554 InfraErrorIdentifier.OPTION_CONFIGURATION_ERROR); 555 } 556 } 557 } 558