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