blob: 2774b386a0ff9a8744cc6f8aae4f939523beb2be [file] [log] [blame]
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tradefed.config;
import com.android.tradefed.device.metric.IMetricCollector;
import com.android.tradefed.log.LogUtil.CLog;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* Holds a record of a configuration, its associated objects and their options.
*/
public class ConfigurationDef {
/**
* a map of object type names to config object class name(s). Use LinkedHashMap to keep objects
* in the same order they were added.
*/
private final Map<String, List<ConfigObjectDef>> mObjectClassMap = new LinkedHashMap<>();
/** a list of option name/value pairs. */
private final List<OptionDef> mOptionList = new ArrayList<>();
/** a cache of the frequency of every classname */
private final Map<String, Integer> mClassFrequency = new HashMap<>();
/** The set of files (and modification times) that were used to load this config */
private final Map<File, Long> mSourceFiles = new HashMap<>();
/**
* Object to hold info for a className and the appearance number it has (e.g. if a config has
* the same object twice, the first one will have the first appearance number).
*/
public static class ConfigObjectDef {
final String mClassName;
final Integer mAppearanceNum;
ConfigObjectDef(String className, Integer appearance) {
mClassName = className;
mAppearanceNum = appearance;
}
}
private boolean mMultiDeviceMode = false;
private Map<String, Boolean> mExpectedDevices = new LinkedHashMap<>();
private static final Pattern MULTI_PATTERN = Pattern.compile("(.*)(:)(.*)");
public static final String DEFAULT_DEVICE_NAME = "DEFAULT_DEVICE";
/** the unique name of the configuration definition */
private final String mName;
/** a short description of the configuration definition */
private String mDescription = "";
public ConfigurationDef(String name) {
mName = name;
}
/**
* Returns a short description of the configuration
*/
public String getDescription() {
return mDescription;
}
/** Sets the configuration definition description */
public void setDescription(String description) {
mDescription = description;
}
/**
* Adds a config object to the definition
*
* @param typeName the config object type name
* @param className the class name of the config object
* @return the number of times this className has appeared in this {@link ConfigurationDef},
* including this time. Because all {@link ConfigurationDef} methods return these classes
* with a constant ordering, this index can serve as a unique identifier for the just-added
* instance of <code>clasName</code>.
*/
public int addConfigObjectDef(String typeName, String className) {
List<ConfigObjectDef> classList = mObjectClassMap.get(typeName);
if (classList == null) {
classList = new ArrayList<ConfigObjectDef>();
mObjectClassMap.put(typeName, classList);
}
// Increment and store count for this className
Integer freq = mClassFrequency.get(className);
freq = freq == null ? 1 : freq + 1;
mClassFrequency.put(className, freq);
classList.add(new ConfigObjectDef(className, freq));
return freq;
}
/**
* Adds option to the definition
*
* @param optionName the name of the option
* @param optionValue the option value
*/
public void addOptionDef(
String optionName,
String optionKey,
String optionValue,
String optionSource,
String type) {
mOptionList.add(new OptionDef(optionName, optionKey, optionValue, optionSource, type));
}
void addOptionDef(String optionName, String optionKey, String optionValue,
String optionSource) {
mOptionList.add(new OptionDef(optionName, optionKey, optionValue, optionSource, null));
}
/**
* Registers a source file that was used while loading this {@link ConfigurationDef}.
*/
void registerSource(File source) {
mSourceFiles.put(source, source.lastModified());
}
/**
* Determine whether any of the source files have changed since this {@link ConfigurationDef}
* was loaded.
*/
boolean isStale() {
for (Map.Entry<File, Long> entry : mSourceFiles.entrySet()) {
if (entry.getKey().lastModified() > entry.getValue()) {
return true;
}
}
return false;
}
/**
* Get the object type name-class map.
*
* <p>Exposed for unit testing
*/
Map<String, List<ConfigObjectDef>> getObjectClassMap() {
return mObjectClassMap;
}
/**
* Get the option name-value map.
* <p/>
* Exposed for unit testing
*/
List<OptionDef> getOptionList() {
return mOptionList;
}
/**
* Creates a configuration from the info stored in this definition, and populates its fields
* with the provided option values.
*
* @return the created {@link IConfiguration}
* @throws ConfigurationException if configuration could not be created
*/
public IConfiguration createConfiguration() throws ConfigurationException {
IConfiguration config = new Configuration(getName(), getDescription());
List<IDeviceConfiguration> deviceObjectList = new ArrayList<IDeviceConfiguration>();
IDeviceConfiguration defaultDeviceConfig =
new DeviceConfigurationHolder(DEFAULT_DEVICE_NAME);
boolean hybridMultiDeviceHandling = false;
if (!mMultiDeviceMode) {
// We still populate a default device config to avoid special logic in the rest of the
// harness.
deviceObjectList.add(defaultDeviceConfig);
} else {
// FIXME: handle this in a more generic way.
// Get the number of real device (non build-only) device
Long numDut =
mExpectedDevices
.values()
.stream()
.filter(value -> (value == false))
.collect(Collectors.counting());
Long numNonDut =
mExpectedDevices
.values()
.stream()
.filter(value -> (value == true))
.collect(Collectors.counting());
if (numDut == 0 && numNonDut == 0) {
throw new ConfigurationException("No device detected. Should not happen.");
}
if (numNonDut > 0 && numDut == 0) {
// if we only have fake devices, use the default device as real device, and add it
// first.
Map<String, Boolean> copy = new LinkedHashMap<>();
copy.put(DEFAULT_DEVICE_NAME, false);
copy.putAll(mExpectedDevices);
mExpectedDevices = copy;
numDut++;
}
if (numNonDut > 0 && numDut == 1) {
// If we have fake device but only a single real device, is the only use case to
// handle very differently: object at the root of the xml needs to be associated
// with the only DuT.
// All the other use cases can be handled the regular way.
CLog.d(
"One device is under tests while config '%s' requires some fake=true "
+ "devices. Using hybrid parsing of config.",
getName());
hybridMultiDeviceHandling = true;
}
for (String name : mExpectedDevices.keySet()) {
deviceObjectList.add(
new DeviceConfigurationHolder(name, mExpectedDevices.get(name)));
}
}
Map<String, String> rejectedObjects = new HashMap<>();
Throwable cause = null;
for (Map.Entry<String, List<ConfigObjectDef>> objClassEntry : mObjectClassMap.entrySet()) {
List<Object> objectList = new ArrayList<Object>(objClassEntry.getValue().size());
String entryName = objClassEntry.getKey();
boolean shouldAddToFlatConfig = true;
for (ConfigObjectDef configDef : objClassEntry.getValue()) {
Object configObject = null;
try {
configObject = createObject(objClassEntry.getKey(), configDef.mClassName);
} catch (ClassNotFoundConfigurationException e) {
// Store all the loading failure
cause = e.getCause();
rejectedObjects.putAll(e.getRejectedObjects());
CLog.e(e);
// Don't add in case of issue
shouldAddToFlatConfig = false;
continue;
}
Matcher matcher = null;
if (mMultiDeviceMode) {
matcher = MULTI_PATTERN.matcher(entryName);
}
if (mMultiDeviceMode && matcher.find()) {
// If we find the device namespace, fetch the matching device or create it if
// it doesn't exists.
IDeviceConfiguration multiDev = null;
shouldAddToFlatConfig = false;
for (IDeviceConfiguration iDevConfig : deviceObjectList) {
if (matcher.group(1).equals(iDevConfig.getDeviceName())) {
multiDev = iDevConfig;
break;
}
}
if (multiDev == null) {
multiDev = new DeviceConfigurationHolder(matcher.group(1));
deviceObjectList.add(multiDev);
}
// We reference the original object to the device and not to the flat list.
multiDev.addSpecificConfig(configObject);
multiDev.addFrequency(configObject, configDef.mAppearanceNum);
} else {
if (Configuration.doesBuiltInObjSupportMultiDevice(entryName)) {
if (hybridMultiDeviceHandling) {
// Special handling for a multi-device with one Dut and the rest are
// non-dut devices.
// At this point we are ensured to have only one Dut device. Object at
// the root should are associated with the only device under test (Dut).
List<IDeviceConfiguration> realDevice =
deviceObjectList
.stream()
.filter(object -> (object.isFake() == false))
.collect(Collectors.toList());
if (realDevice.size() != 1) {
throw new ConfigurationException(
String.format(
"Something went very bad, we found '%s' Dut "
+ "device while expecting one only.",
realDevice.size()));
}
realDevice.get(0).addSpecificConfig(configObject);
realDevice.get(0).addFrequency(configObject, configDef.mAppearanceNum);
} else {
// Regular handling of object for single device situation.
defaultDeviceConfig.addSpecificConfig(configObject);
defaultDeviceConfig.addFrequency(
configObject, configDef.mAppearanceNum);
}
} else {
// Only add to flat list if they are not part of multi device config.
objectList.add(configObject);
}
}
}
if (shouldAddToFlatConfig) {
config.setConfigurationObjectList(entryName, objectList);
}
}
checkRejectedObjects(rejectedObjects, cause);
// We always add the device configuration list so we can rely on it everywhere
config.setConfigurationObjectList(Configuration.DEVICE_NAME, deviceObjectList);
injectOptions(config, mOptionList);
return config;
}
/** Evaluate rejected objects map, if any throw an exception. */
protected void checkRejectedObjects(Map<String, String> rejectedObjects, Throwable cause)
throws ClassNotFoundConfigurationException {
// Send all the objects that failed the loading.
if (!rejectedObjects.isEmpty()) {
throw new ClassNotFoundConfigurationException(
String.format(
"Failed to load some objects in the configuration '%s': %s",
getName(), rejectedObjects),
cause,
rejectedObjects);
}
}
protected void injectOptions(IConfiguration config, List<OptionDef> optionList)
throws ConfigurationException {
config.injectOptionValues(optionList);
}
/**
* Creates a global configuration from the info stored in this definition, and populates its
* fields with the provided option values.
*
* @return the created {@link IGlobalConfiguration}
* @throws ConfigurationException if configuration could not be created
*/
IGlobalConfiguration createGlobalConfiguration() throws ConfigurationException {
IGlobalConfiguration config = new GlobalConfiguration(getName(), getDescription());
for (Map.Entry<String, List<ConfigObjectDef>> objClassEntry : mObjectClassMap.entrySet()) {
List<Object> objectList = new ArrayList<Object>(objClassEntry.getValue().size());
for (ConfigObjectDef configDef : objClassEntry.getValue()) {
Object configObject = createObject(objClassEntry.getKey(), configDef.mClassName);
objectList.add(configObject);
}
config.setConfigurationObjectList(objClassEntry.getKey(), objectList);
}
for (OptionDef optionEntry : mOptionList) {
config.injectOptionValue(optionEntry.name, optionEntry.key, optionEntry.value);
}
return config;
}
/**
* Gets the name of this configuration definition
*
* @return name of this configuration.
*/
public String getName() {
return mName;
}
public void setMultiDeviceMode(boolean multiDeviceMode) {
mMultiDeviceMode = multiDeviceMode;
}
/** Returns whether or not the recorded configuration is multi-device or not. */
public boolean isMultiDeviceMode() {
return mMultiDeviceMode;
}
/** Add a device that needs to be tracked and whether or not it's real. */
public String addExpectedDevice(String deviceName, boolean isFake) {
Boolean previous = mExpectedDevices.put(deviceName, isFake);
if (previous != null && previous != isFake) {
return String.format(
"Mismatch for device '%s'. It was defined once as isFake=false, once as "
+ "isFake=true",
deviceName);
}
return null;
}
/** Returns the current Map of tracked devices and if they are real or not. */
public Map<String, Boolean> getExpectedDevices() {
return mExpectedDevices;
}
/**
* Creates a config object associated with this definition.
*
* @param objectTypeName the name of the object. Used to generate more descriptive error
* messages
* @param className the class name of the object to load
* @return the config object
* @throws ConfigurationException if config object could not be created
*/
private Object createObject(String objectTypeName, String className)
throws ConfigurationException {
try {
Class<?> objectClass = getClassForObject(objectTypeName, className);
Object configObject = objectClass.getDeclaredConstructor().newInstance();
checkObjectValid(objectTypeName, configObject);
return configObject;
} catch (InstantiationException | InvocationTargetException | NoSuchMethodException e) {
throw new ConfigurationException(String.format(
"Could not instantiate class %s for config object type %s", className,
objectTypeName), e);
} catch (IllegalAccessException e) {
throw new ConfigurationException(String.format(
"Could not access class %s for config object type %s", className,
objectTypeName), e);
}
}
/**
* Loads the class for the given the config object associated with this definition.
*
* @param objectTypeName the name of the config object type. Used to generate more descriptive
* error messages
* @param className the class name of the object to load
* @return the config object populated with default option values
* @throws ClassNotFoundConfigurationException if config object could not be created
*/
private Class<?> getClassForObject(String objectTypeName, String className)
throws ClassNotFoundConfigurationException {
try {
return Class.forName(className);
} catch (ClassNotFoundException e) {
ClassNotFoundConfigurationException exception =
new ClassNotFoundConfigurationException(
String.format(
"Could not find class %s for config object type %s",
className, objectTypeName),
e,
className,
objectTypeName);
throw exception;
}
}
/**
* Check that the loaded object does not present some incoherence. Some combination should not
* be done. For example: metric_collectors does extend ITestInvocationListener and could be
* declared as a result_reporter, but we do not allow it because it's not how it should be used
* in the invocation.
*
* @param objectTypeName The type of the object declared in the xml.
* @param configObject The instantiated object.
* @throws ConfigurationException if we find an incoherence in the object.
*/
private void checkObjectValid(String objectTypeName, Object configObject)
throws ConfigurationException {
if (Configuration.RESULT_REPORTER_TYPE_NAME.equals(objectTypeName)
&& configObject instanceof IMetricCollector) {
// we do not allow IMetricCollector as result_reporter.
throw new ConfigurationException(
String.format(
"Object of type %s was declared as %s.",
Configuration.DEVICE_METRICS_COLLECTOR_TYPE_NAME,
Configuration.RESULT_REPORTER_TYPE_NAME));
}
}
}