• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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