1# Copyright 2018 Google Inc. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14""" Module for Mobly controller management.""" 15import collections 16import copy 17import logging 18import yaml 19 20from mobly import expects 21from mobly import records 22from mobly import signals 23 24 25def verify_controller_module(module): 26 """Verifies a module object follows the required interface for 27 controllers. 28 29 The interface is explained in the docstring of 30 `base_test.BaseTestClass.register_controller`. 31 32 Args: 33 module: An object that is a controller module. This is usually 34 imported with import statements or loaded by importlib. 35 36 Raises: 37 ControllerError: if the module does not match the Mobly controller 38 interface, or one of the required members is null. 39 """ 40 required_attributes = ('create', 'destroy', 'MOBLY_CONTROLLER_CONFIG_NAME') 41 for attr in required_attributes: 42 if not hasattr(module, attr): 43 raise signals.ControllerError( 44 'Module %s missing required controller module attribute' 45 ' %s.' % (module.__name__, attr)) 46 if not getattr(module, attr): 47 raise signals.ControllerError( 48 'Controller interface %s in %s cannot be null.' % 49 (attr, module.__name__)) 50 51 52class ControllerManager: 53 """Manages the controller objects for Mobly tests. 54 55 This manages the life cycles and info retrieval of all controller objects 56 used in a test. 57 58 Attributes: 59 controller_configs: dict, controller configs provided by the user via 60 test bed config. 61 """ 62 63 def __init__(self, class_name, controller_configs): 64 # Controller object management. 65 self._controller_objects = collections.OrderedDict( 66 ) # controller_name: objects 67 self._controller_modules = {} # controller_name: module 68 self._class_name = class_name 69 self.controller_configs = controller_configs 70 71 def register_controller(self, module, required=True, min_number=1): 72 """Loads a controller module and returns its loaded devices. 73 74 This is to be used in a mobly test class. 75 76 Args: 77 module: A module that follows the controller module interface. 78 required: A bool. If True, failing to register the specified 79 controller module raises exceptions. If False, the objects 80 failed to instantiate will be skipped. 81 min_number: An integer that is the minimum number of controller 82 objects to be created. Default is one, since you should not 83 register a controller module without expecting at least one 84 object. 85 86 Returns: 87 A list of controller objects instantiated from controller_module, or 88 None if no config existed for this controller and it was not a 89 required controller. 90 91 Raises: 92 ControllerError: 93 * The controller module has already been registered. 94 * The actual number of objects instantiated is less than the 95 * `min_number`. 96 * `required` is True and no corresponding config can be found. 97 * Any other error occurred in the registration process. 98 """ 99 verify_controller_module(module) 100 # Use the module's name as the ref name 101 module_ref_name = module.__name__.split('.')[-1] 102 if module_ref_name in self._controller_objects: 103 raise signals.ControllerError( 104 'Controller module %s has already been registered. It cannot ' 105 'be registered again.' % module_ref_name) 106 # Create controller objects. 107 module_config_name = module.MOBLY_CONTROLLER_CONFIG_NAME 108 if module_config_name not in self.controller_configs: 109 if required: 110 raise signals.ControllerError('No corresponding config found for %s' % 111 module_config_name) 112 logging.warning( 113 'No corresponding config found for optional controller %s', 114 module_config_name) 115 return None 116 try: 117 # Make a deep copy of the config to pass to the controller module, 118 # in case the controller module modifies the config internally. 119 original_config = self.controller_configs[module_config_name] 120 controller_config = copy.deepcopy(original_config) 121 objects = module.create(controller_config) 122 except Exception: 123 logging.exception( 124 'Failed to initialize objects for controller %s, abort!', 125 module_config_name) 126 raise 127 if not isinstance(objects, list): 128 raise signals.ControllerError( 129 'Controller module %s did not return a list of objects, abort.' % 130 module_ref_name) 131 # Check we got enough controller objects to continue. 132 actual_number = len(objects) 133 if actual_number < min_number: 134 module.destroy(objects) 135 raise signals.ControllerError( 136 'Expected to get at least %d controller objects, got %d.' % 137 (min_number, actual_number)) 138 # Save a shallow copy of the list for internal usage, so tests can't 139 # affect internal registry by manipulating the object list. 140 self._controller_objects[module_ref_name] = copy.copy(objects) 141 logging.debug('Found %d objects for controller %s', len(objects), 142 module_config_name) 143 self._controller_modules[module_ref_name] = module 144 return objects 145 146 def unregister_controllers(self): 147 """Destroy controller objects and clear internal registry. 148 149 This will be called after each test class. 150 """ 151 # TODO(xpconanfan): actually record these errors instead of just 152 # logging them. 153 for name, module in self._controller_modules.items(): 154 logging.debug('Destroying %s.', name) 155 with expects.expect_no_raises('Exception occurred destroying %s.' % name): 156 module.destroy(self._controller_objects[name]) 157 self._controller_objects = collections.OrderedDict() 158 self._controller_modules = {} 159 160 def _create_controller_info_record(self, controller_module_name): 161 """Creates controller info record for a particular controller type. 162 163 Info is retrieved from all the controller objects spawned from the 164 specified module, using the controller module's `get_info` function. 165 166 Args: 167 controller_module_name: string, the name of the controller module 168 to retrieve info from. 169 170 Returns: 171 A records.ControllerInfoRecord object. 172 """ 173 module = self._controller_modules[controller_module_name] 174 controller_info = None 175 try: 176 controller_info = module.get_info( 177 copy.copy(self._controller_objects[controller_module_name])) 178 except AttributeError: 179 logging.warning( 180 'No optional debug info found for controller ' 181 '%s. To provide it, implement `get_info`.', controller_module_name) 182 try: 183 yaml.dump(controller_info) 184 except TypeError: 185 logging.warning( 186 'The info of controller %s in class "%s" is not ' 187 'YAML serializable! Coercing it to string.', controller_module_name, 188 self._class_name) 189 controller_info = str(controller_info) 190 return records.ControllerInfoRecord(self._class_name, 191 module.MOBLY_CONTROLLER_CONFIG_NAME, 192 controller_info) 193 194 def get_controller_info_records(self): 195 """Get the info records for all the controller objects in the manager. 196 197 New info records for each controller object are created for every call 198 so the latest info is included. 199 200 Returns: 201 List of records.ControllerInfoRecord objects. Each opject conatins 202 the info of a type of controller 203 """ 204 info_records = [] 205 for controller_module_name in self._controller_objects.keys(): 206 with expects.expect_no_raises( 207 'Failed to collect controller info from %s' % controller_module_name): 208 record = self._create_controller_info_record(controller_module_name) 209 if record: 210 info_records.append(record) 211 return info_records 212