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