• 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.build.BuildSerializedVersion;
20 import com.android.tradefed.device.metric.IMetricCollector;
21 import com.android.tradefed.log.LogUtil.CLog;
22 
23 import java.io.File;
24 import java.io.Serializable;
25 import java.util.ArrayList;
26 import java.util.HashMap;
27 import java.util.LinkedHashMap;
28 import java.util.List;
29 import java.util.Map;
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     /** Holds the details of an option. */
55     public static final class OptionDef implements Serializable {
56         private static final long serialVersionUID = BuildSerializedVersion.VERSION;
57 
58         public final String name;
59         public final String key;
60         public final String value;
61         public final String source;
62         public final String applicableObjectType;
63 
OptionDef(String optionName, String optionValue, String source)64         public OptionDef(String optionName, String optionValue, String source) {
65             this(optionName, null, optionValue, source, null);
66         }
67 
OptionDef(String optionName, String optionKey, String optionValue, String source)68         public OptionDef(String optionName, String optionKey, String optionValue, String source) {
69             this(optionName, optionKey, optionValue, source, null);
70         }
71 
OptionDef( String optionName, String optionKey, String optionValue, String source, String type)72         public OptionDef(
73                 String optionName,
74                 String optionKey,
75                 String optionValue,
76                 String source,
77                 String type) {
78             this.name = optionName;
79             this.key = optionKey;
80             this.value = optionValue;
81             this.source = source;
82             this.applicableObjectType = type;
83         }
84     }
85 
86     /**
87      * Object to hold info for a className and the appearance number it has (e.g. if a config has
88      * the same object twice, the first one will have the first appearance number).
89      */
90     public static class ConfigObjectDef {
91         final String mClassName;
92         final Integer mAppearanceNum;
93 
ConfigObjectDef(String className, Integer appearance)94         ConfigObjectDef(String className, Integer appearance) {
95             mClassName = className;
96             mAppearanceNum = appearance;
97         }
98     }
99 
100     private boolean mMultiDeviceMode = false;
101     private Map<String, Boolean> mExpectedDevices = new LinkedHashMap<>();
102     private static final Pattern MULTI_PATTERN = Pattern.compile("(.*)(:)(.*)");
103     public static final String DEFAULT_DEVICE_NAME = "DEFAULT_DEVICE";
104 
105     /** the unique name of the configuration definition */
106     private final String mName;
107 
108     /** a short description of the configuration definition */
109     private String mDescription = "";
110 
ConfigurationDef(String name)111     public ConfigurationDef(String name) {
112         mName = name;
113     }
114 
115     /**
116      * Returns a short description of the configuration
117      */
getDescription()118     public String getDescription() {
119         return mDescription;
120     }
121 
122     /**
123      * Sets the configuration definition description
124      */
setDescription(String description)125     void setDescription(String description) {
126         mDescription = description;
127     }
128 
129     /**
130      * Adds a config object to the definition
131      *
132      * @param typeName the config object type name
133      * @param className the class name of the config object
134      * @return the number of times this className has appeared in this {@link ConfigurationDef},
135      *         including this time.  Because all {@link ConfigurationDef} methods return these
136      *         classes with a constant ordering, this index can serve as a unique identifier for the
137      *         just-added instance of <code>clasName</code>.
138      */
addConfigObjectDef(String typeName, String className)139     int addConfigObjectDef(String typeName, String className) {
140         List<ConfigObjectDef> classList = mObjectClassMap.get(typeName);
141         if (classList == null) {
142             classList = new ArrayList<ConfigObjectDef>();
143             mObjectClassMap.put(typeName, classList);
144         }
145 
146         // Increment and store count for this className
147         Integer freq = mClassFrequency.get(className);
148         freq = freq == null ? 1 : freq + 1;
149         mClassFrequency.put(className, freq);
150         classList.add(new ConfigObjectDef(className, freq));
151 
152         return freq;
153     }
154 
155     /**
156      * Adds option to the definition
157      *
158      * @param optionName the name of the option
159      * @param optionValue the option value
160      */
addOptionDef( String optionName, String optionKey, String optionValue, String optionSource, String type)161     void addOptionDef(
162             String optionName,
163             String optionKey,
164             String optionValue,
165             String optionSource,
166             String type) {
167         mOptionList.add(new OptionDef(optionName, optionKey, optionValue, optionSource, type));
168     }
169 
addOptionDef(String optionName, String optionKey, String optionValue, String optionSource)170     void addOptionDef(String optionName, String optionKey, String optionValue,
171             String optionSource) {
172         mOptionList.add(new OptionDef(optionName, optionKey, optionValue, optionSource, null));
173     }
174 
175     /**
176      * Registers a source file that was used while loading this {@link ConfigurationDef}.
177      */
registerSource(File source)178     void registerSource(File source) {
179         mSourceFiles.put(source, source.lastModified());
180     }
181 
182     /**
183      * Determine whether any of the source files have changed since this {@link ConfigurationDef}
184      * was loaded.
185      */
isStale()186     boolean isStale() {
187         for (Map.Entry<File, Long> entry : mSourceFiles.entrySet()) {
188             if (entry.getKey().lastModified() > entry.getValue()) {
189                 return true;
190             }
191         }
192         return false;
193     }
194 
195     /**
196      * Get the object type name-class map.
197      *
198      * <p>Exposed for unit testing
199      */
getObjectClassMap()200     Map<String, List<ConfigObjectDef>> getObjectClassMap() {
201         return mObjectClassMap;
202     }
203 
204     /**
205      * Get the option name-value map.
206      * <p/>
207      * Exposed for unit testing
208      */
getOptionList()209     List<OptionDef> getOptionList() {
210         return mOptionList;
211     }
212 
213     /**
214      * Creates a configuration from the info stored in this definition, and populates its fields
215      * with the provided option values.
216      *
217      * @return the created {@link IConfiguration}
218      * @throws ConfigurationException if configuration could not be created
219      */
createConfiguration()220     IConfiguration createConfiguration() throws ConfigurationException {
221         IConfiguration config = new Configuration(getName(), getDescription());
222         List<IDeviceConfiguration> deviceObjectList = new ArrayList<IDeviceConfiguration>();
223         IDeviceConfiguration defaultDeviceConfig =
224                 new DeviceConfigurationHolder(DEFAULT_DEVICE_NAME);
225         boolean hybridMultiDeviceHandling = false;
226 
227         if (!mMultiDeviceMode) {
228             // We still populate a default device config to avoid special logic in the rest of the
229             // harness.
230             deviceObjectList.add(defaultDeviceConfig);
231         } else {
232             // FIXME: handle this in a more generic way.
233             // Get the number of real device (non build-only) device
234             Long numDut =
235                     mExpectedDevices
236                             .values()
237                             .stream()
238                             .filter(value -> (value == false))
239                             .collect(Collectors.counting());
240             Long numNonDut =
241                     mExpectedDevices
242                             .values()
243                             .stream()
244                             .filter(value -> (value == true))
245                             .collect(Collectors.counting());
246             if (numDut == 0 && numNonDut == 0) {
247                 throw new ConfigurationException("No device detected. Should not happen.");
248             }
249             if (numNonDut > 0 && numDut == 0) {
250                 // if we only have fake devices, use the default device as real device, and add it
251                 // first.
252                 Map<String, Boolean> copy = new LinkedHashMap<>();
253                 copy.put(DEFAULT_DEVICE_NAME, false);
254                 copy.putAll(mExpectedDevices);
255                 mExpectedDevices = copy;
256                 numDut++;
257             }
258             if (numNonDut > 0 && numDut == 1) {
259                 // If we have fake device but only a single real device, is the only use case to
260                 // handle very differently: object at the root of the xml needs to be associated
261                 // with the only DuT.
262                 // All the other use cases can be handled the regular way.
263                 CLog.d(
264                         "One device is under tests while config '%s' requires some fake=true "
265                                 + "devices. Using hybrid parsing of config.",
266                         getName());
267                 hybridMultiDeviceHandling = true;
268             }
269             for (String name : mExpectedDevices.keySet()) {
270                 deviceObjectList.add(
271                         new DeviceConfigurationHolder(name, mExpectedDevices.get(name)));
272             }
273         }
274 
275         Map<String, String> rejectedObjects = new HashMap<>();
276         Throwable cause = null;
277 
278         for (Map.Entry<String, List<ConfigObjectDef>> objClassEntry : mObjectClassMap.entrySet()) {
279             List<Object> objectList = new ArrayList<Object>(objClassEntry.getValue().size());
280             String entryName = objClassEntry.getKey();
281             boolean shouldAddToFlatConfig = true;
282 
283             for (ConfigObjectDef configDef : objClassEntry.getValue()) {
284                 Object configObject = null;
285                 try {
286                     configObject = createObject(objClassEntry.getKey(), configDef.mClassName);
287                 } catch (ClassNotFoundConfigurationException e) {
288                     // Store all the loading failure
289                     cause = e.getCause();
290                     rejectedObjects.putAll(e.getRejectedObjects());
291                     CLog.e(e);
292                     // Don't add in case of issue
293                     shouldAddToFlatConfig = false;
294                     continue;
295                 }
296                 Matcher matcher = null;
297                 if (mMultiDeviceMode) {
298                     matcher = MULTI_PATTERN.matcher(entryName);
299                 }
300                 if (mMultiDeviceMode && matcher.find()) {
301                     // If we find the device namespace, fetch the matching device or create it if
302                     // it doesn't exists.
303                     IDeviceConfiguration multiDev = null;
304                     shouldAddToFlatConfig = false;
305                     for (IDeviceConfiguration iDevConfig : deviceObjectList) {
306                         if (matcher.group(1).equals(iDevConfig.getDeviceName())) {
307                             multiDev = iDevConfig;
308                             break;
309                         }
310                     }
311                     if (multiDev == null) {
312                         multiDev = new DeviceConfigurationHolder(matcher.group(1));
313                         deviceObjectList.add(multiDev);
314                     }
315                     // We reference the original object to the device and not to the flat list.
316                     multiDev.addSpecificConfig(configObject);
317                     multiDev.addFrequency(configObject, configDef.mAppearanceNum);
318                 } else {
319                     if (Configuration.doesBuiltInObjSupportMultiDevice(entryName)) {
320                         if (hybridMultiDeviceHandling) {
321                             // Special handling for a multi-device with one Dut and the rest are
322                             // non-dut devices.
323                             // At this point we are ensured to have only one Dut device. Object at
324                             // the root should are associated with the only device under test (Dut).
325                             List<IDeviceConfiguration> realDevice =
326                                     deviceObjectList
327                                             .stream()
328                                             .filter(object -> (object.isFake() == false))
329                                             .collect(Collectors.toList());
330                             if (realDevice.size() != 1) {
331                                 throw new ConfigurationException(
332                                         String.format(
333                                                 "Something went very bad, we found '%s' Dut "
334                                                         + "device while expecting one only.",
335                                                 realDevice.size()));
336                             }
337                             realDevice.get(0).addSpecificConfig(configObject);
338                             realDevice.get(0).addFrequency(configObject, configDef.mAppearanceNum);
339                         } else {
340                             // Regular handling of object for single device situation.
341                             defaultDeviceConfig.addSpecificConfig(configObject);
342                             defaultDeviceConfig.addFrequency(
343                                     configObject, configDef.mAppearanceNum);
344                         }
345                     } else {
346                         // Only add to flat list if they are not part of multi device config.
347                         objectList.add(configObject);
348                     }
349                 }
350             }
351             if (shouldAddToFlatConfig) {
352                 config.setConfigurationObjectList(entryName, objectList);
353             }
354         }
355 
356         checkRejectedObjects(rejectedObjects, cause);
357 
358         // We always add the device configuration list so we can rely on it everywhere
359         config.setConfigurationObjectList(Configuration.DEVICE_NAME, deviceObjectList);
360         injectOptions(config, mOptionList);
361 
362         return config;
363     }
364 
365     /** Evaluate rejected objects map, if any throw an exception. */
checkRejectedObjects(Map<String, String> rejectedObjects, Throwable cause)366     protected void checkRejectedObjects(Map<String, String> rejectedObjects, Throwable cause)
367             throws ClassNotFoundConfigurationException {
368         // Send all the objects that failed the loading.
369         if (!rejectedObjects.isEmpty()) {
370             throw new ClassNotFoundConfigurationException(
371                     String.format(
372                             "Failed to load some objects in the configuration: %s",
373                             rejectedObjects),
374                     cause,
375                     rejectedObjects);
376         }
377     }
378 
injectOptions(IConfiguration config, List<OptionDef> optionList)379     protected void injectOptions(IConfiguration config, List<OptionDef> optionList)
380             throws ConfigurationException {
381         config.injectOptionValues(optionList);
382     }
383 
384     /**
385      * Creates a global configuration from the info stored in this definition, and populates its
386      * fields with the provided option values.
387      *
388      * @return the created {@link IGlobalConfiguration}
389      * @throws ConfigurationException if configuration could not be created
390      */
createGlobalConfiguration()391     IGlobalConfiguration createGlobalConfiguration() throws ConfigurationException {
392         IGlobalConfiguration config = new GlobalConfiguration(getName(), getDescription());
393 
394         for (Map.Entry<String, List<ConfigObjectDef>> objClassEntry : mObjectClassMap.entrySet()) {
395             List<Object> objectList = new ArrayList<Object>(objClassEntry.getValue().size());
396             for (ConfigObjectDef configDef : objClassEntry.getValue()) {
397                 Object configObject = createObject(objClassEntry.getKey(), configDef.mClassName);
398                 objectList.add(configObject);
399             }
400             config.setConfigurationObjectList(objClassEntry.getKey(), objectList);
401         }
402         for (OptionDef optionEntry : mOptionList) {
403             config.injectOptionValue(optionEntry.name, optionEntry.key, optionEntry.value);
404         }
405 
406         return config;
407     }
408 
409     /**
410      * Gets the name of this configuration definition
411      *
412      * @return name of this configuration.
413      */
getName()414     public String getName() {
415         return mName;
416     }
417 
setMultiDeviceMode(boolean multiDeviceMode)418     public void setMultiDeviceMode(boolean multiDeviceMode) {
419         mMultiDeviceMode = multiDeviceMode;
420     }
421 
422     /** Returns whether or not the recorded configuration is multi-device or not. */
isMultiDeviceMode()423     public boolean isMultiDeviceMode() {
424         return mMultiDeviceMode;
425     }
426 
427     /** Add a device that needs to be tracked and whether or not it's real. */
addExpectedDevice(String deviceName, boolean isFake)428     public String addExpectedDevice(String deviceName, boolean isFake) {
429         Boolean previous = mExpectedDevices.put(deviceName, isFake);
430         if (previous != null && previous != isFake) {
431             return String.format(
432                     "Mismatch for device '%s'. It was defined once as isFake=false, once as "
433                             + "isFake=true",
434                     deviceName);
435         }
436         return null;
437     }
438 
439     /** Returns the current Map of tracked devices and if they are real or not. */
getExpectedDevices()440     public Map<String, Boolean> getExpectedDevices() {
441         return mExpectedDevices;
442     }
443 
444     /**
445      * Creates a config object associated with this definition.
446      *
447      * @param objectTypeName the name of the object. Used to generate more descriptive error
448      *            messages
449      * @param className the class name of the object to load
450      * @return the config object
451      * @throws ConfigurationException if config object could not be created
452      */
createObject(String objectTypeName, String className)453     private Object createObject(String objectTypeName, String className)
454             throws ConfigurationException {
455         try {
456             Class<?> objectClass = getClassForObject(objectTypeName, className);
457             Object configObject = objectClass.newInstance();
458             checkObjectValid(objectTypeName, configObject);
459             return configObject;
460         } catch (InstantiationException e) {
461             throw new ConfigurationException(String.format(
462                     "Could not instantiate class %s for config object type %s", className,
463                     objectTypeName), e);
464         } catch (IllegalAccessException e) {
465             throw new ConfigurationException(String.format(
466                     "Could not access class %s for config object type %s", className,
467                     objectTypeName), e);
468         }
469     }
470 
471     /**
472      * Loads the class for the given the config object associated with this definition.
473      *
474      * @param objectTypeName the name of the config object type. Used to generate more descriptive
475      *     error messages
476      * @param className the class name of the object to load
477      * @return the config object populated with default option values
478      * @throws ClassNotFoundConfigurationException if config object could not be created
479      */
getClassForObject(String objectTypeName, String className)480     private Class<?> getClassForObject(String objectTypeName, String className)
481             throws ClassNotFoundConfigurationException {
482         try {
483             return Class.forName(className);
484         } catch (ClassNotFoundException e) {
485             ClassNotFoundConfigurationException exception =
486                     new ClassNotFoundConfigurationException(
487                             String.format(
488                                     "Could not find class %s for config object type %s",
489                                     className, objectTypeName),
490                             e,
491                             className,
492                             objectTypeName);
493             throw exception;
494         }
495     }
496 
497     /**
498      * Check that the loaded object does not present some incoherence. Some combination should not
499      * be done. For example: metric_collectors does extend ITestInvocationListener and could be
500      * declared as a result_reporter, but we do not allow it because it's not how it should be used
501      * in the invocation.
502      *
503      * @param objectTypeName The type of the object declared in the xml.
504      * @param configObject The instantiated object.
505      * @throws ConfigurationException if we find an incoherence in the object.
506      */
checkObjectValid(String objectTypeName, Object configObject)507     private void checkObjectValid(String objectTypeName, Object configObject)
508             throws ConfigurationException {
509         if (Configuration.RESULT_REPORTER_TYPE_NAME.equals(objectTypeName)
510                 && configObject instanceof IMetricCollector) {
511             // we do not allow IMetricCollector as result_reporter.
512             throw new ConfigurationException(
513                     String.format(
514                             "Object of type %s was declared as %s.",
515                             Configuration.DEVICE_METRICS_COLLECTOR_TYPE_NAME,
516                             Configuration.RESULT_REPORTER_TYPE_NAME));
517         }
518     }
519 }
520