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