• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#
2# Copyright (C) 2016 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
17from future import standard_library
18standard_library.install_aliases()
19
20import copy
21import importlib
22import inspect
23import logging
24import os
25import pkgutil
26import signal
27import sys
28
29from vts.runners.host import base_test
30from vts.runners.host import config_parser
31from vts.runners.host import keys
32from vts.runners.host import logger
33from vts.runners.host import records
34from vts.runners.host import signals
35from vts.runners.host import utils
36
37
38def main():
39    """Execute the test class in a test module.
40
41    This is to be used in a test script's main so the script can be executed
42    directly. It will discover all the classes that inherit from BaseTestClass
43    and excute them. all the test results will be aggregated into one.
44
45    A VTS host-driven test case has three args:
46       1st arg: the path of a test case config file.
47       2nd arg: the serial ID of a target device (device config).
48       3rd arg: the path of a test case data dir.
49
50    Returns:
51        The TestResult object that holds the results of the test run.
52    """
53    test_classes = []
54    main_module_members = sys.modules["__main__"]
55    for _, module_member in main_module_members.__dict__.items():
56        if inspect.isclass(module_member):
57            if issubclass(module_member, base_test.BaseTestClass):
58                test_classes.append(module_member)
59    # TODO(angli): Need to handle the case where more than one test class is in
60    # a test script. The challenge is to handle multiple configs and how to do
61    # default config in this case.
62    if len(test_classes) != 1:
63        logging.error("Expected 1 test class per file, found %s.",
64                      len(test_classes))
65        sys.exit(1)
66    test_result = runTestClass(test_classes[0])
67    return test_result
68
69
70def runTestClass(test_class):
71    """Execute one test class.
72
73    This will create a TestRunner, execute one test run with one test class.
74
75    Args:
76        test_class: The test class to instantiate and execute.
77
78    Returns:
79        The TestResult object that holds the results of the test run.
80    """
81    test_cls_name = test_class.__name__
82    if len(sys.argv) < 2:
83        logging.warning("Missing a configuration file. Using the default.")
84        test_configs = [config_parser.GetDefaultConfig(test_cls_name)]
85    else:
86        try:
87            config_path = sys.argv[1]
88            baseline_config = config_parser.GetDefaultConfig(test_cls_name)
89            baseline_config[keys.ConfigKeys.KEY_TESTBED] = [
90                baseline_config[keys.ConfigKeys.KEY_TESTBED]
91            ]
92            test_configs = config_parser.load_test_config_file(
93                config_path, baseline_config=baseline_config)
94        except IndexError:
95            logging.error("No valid config file found.")
96            sys.exit(1)
97
98    test_identifiers = [(test_cls_name, None)]
99
100    for config in test_configs:
101        tr = TestRunner(config, test_identifiers)
102        tr.parseTestConfig(config)
103        try:
104            # Create console signal handler to make sure TestRunner is stopped
105            # in the event of termination.
106            handler = config_parser.gen_term_signal_handler([tr])
107            signal.signal(signal.SIGTERM, handler)
108            signal.signal(signal.SIGINT, handler)
109            tr.runTestClass(test_class, None)
110        finally:
111            tr.stop()
112            return tr.results
113
114
115class TestRunner(object):
116    """The class that instantiates test classes, executes test cases, and
117    report results.
118
119    Attributes:
120        self.test_run_info: A dictionary containing the information needed by
121                            test classes for this test run, including params,
122                            controllers, and other objects. All of these will
123                            be passed to test classes.
124        self.test_configs: A dictionary that is the original test configuration
125                           passed in by user.
126        self.id: A string that is the unique identifier of this test run.
127        self.log_path: A string representing the path of the dir under which
128                       all logs from this test run should be written.
129        self.controller_registry: A dictionary that holds the controller
130                                  objects used in a test run.
131        self.controller_destructors: A dictionary that holds the controller
132                                     distructors. Keys are controllers' names.
133        self.run_list: A list of tuples specifying what tests to run.
134        self.results: The test result object used to record the results of
135                      this test run.
136        self.running: A boolean signifies whether this test run is ongoing or
137                      not.
138    """
139
140    def __init__(self, test_configs, run_list):
141        self.test_run_info = {}
142        self.test_run_info[keys.ConfigKeys.IKEY_DATA_FILE_PATH] = getattr(
143            test_configs, keys.ConfigKeys.IKEY_DATA_FILE_PATH, "./")
144        self.test_configs = test_configs
145        self.testbed_configs = self.test_configs[keys.ConfigKeys.KEY_TESTBED]
146        self.testbed_name = self.testbed_configs[
147            keys.ConfigKeys.KEY_TESTBED_NAME]
148        start_time = logger.getLogFileTimestamp()
149        self.id = "{}@{}".format(self.testbed_name, start_time)
150        # log_path should be set before parsing configs.
151        l_path = os.path.join(self.test_configs[keys.ConfigKeys.KEY_LOG_PATH],
152                              self.testbed_name, start_time)
153        self.log_path = os.path.abspath(l_path)
154        logger.setupTestLogger(self.log_path, self.testbed_name)
155        self.controller_registry = {}
156        self.controller_destructors = {}
157        self.run_list = run_list
158        self.results = records.TestResult()
159        self.running = False
160
161    def __enter__(self):
162        return self
163
164    def __exit__(self, *args):
165        self.stop()
166
167    def importTestModules(self, test_paths):
168        """Imports test classes from test scripts.
169
170        1. Locate all .py files under test paths.
171        2. Import the .py files as modules.
172        3. Find the module members that are test classes.
173        4. Categorize the test classes by name.
174
175        Args:
176            test_paths: A list of directory paths where the test files reside.
177
178        Returns:
179            A dictionary where keys are test class name strings, values are
180            actual test classes that can be instantiated.
181        """
182
183        def is_testfile_name(name, ext):
184            if ext == ".py":
185                if name.endswith("Test") or name.endswith("_test"):
186                    return True
187            return False
188
189        file_list = utils.find_files(test_paths, is_testfile_name)
190        test_classes = {}
191        for path, name, _ in file_list:
192            sys.path.append(path)
193            try:
194                module = importlib.import_module(name)
195            except:
196                for test_cls_name, _ in self.run_list:
197                    alt_name = name.replace('_', '').lower()
198                    alt_cls_name = test_cls_name.lower()
199                    # Only block if a test class on the run list causes an
200                    # import error. We need to check against both naming
201                    # conventions: AaaBbb and aaa_bbb.
202                    if name == test_cls_name or alt_name == alt_cls_name:
203                        msg = ("Encountered error importing test class %s, "
204                               "abort.") % test_cls_name
205                        # This exception is logged here to help with debugging
206                        # under py2, because "raise X from Y" syntax is only
207                        # supported under py3.
208                        logging.exception(msg)
209                        raise USERError(msg)
210                continue
211            for member_name in dir(module):
212                if not member_name.startswith("__"):
213                    if member_name.endswith("Test"):
214                        test_class = getattr(module, member_name)
215                        if inspect.isclass(test_class):
216                            test_classes[member_name] = test_class
217        return test_classes
218
219    def verifyControllerModule(self, module):
220        """Verifies a module object follows the required interface for
221        controllers.
222
223        Args:
224            module: An object that is a controller module. This is usually
225                    imported with import statements or loaded by importlib.
226
227        Raises:
228            ControllerError is raised if the module does not match the vts.runners.host
229            controller interface, or one of the required members is null.
230        """
231        required_attributes = ("create", "destroy",
232                               "VTS_CONTROLLER_CONFIG_NAME")
233        for attr in required_attributes:
234            if not hasattr(module, attr):
235                raise signals.ControllerError(
236                    ("Module %s missing required "
237                     "controller module attribute %s.") % (module.__name__,
238                                                           attr))
239            if not getattr(module, attr):
240                raise signals.ControllerError(
241                    ("Controller interface %s in %s "
242                     "cannot be null.") % (attr, module.__name__))
243
244    def registerController(self, module, start_services=True):
245        """Registers a controller module for a test run.
246
247        This declares a controller dependency of this test class. If the target
248        module exists and matches the controller interface, the controller
249        module will be instantiated with corresponding configs in the test
250        config file. The module should be imported first.
251
252        Params:
253            module: A module that follows the controller module interface.
254            start_services: boolean, controls whether services (e.g VTS agent)
255                            are started on the target.
256
257        Returns:
258            A list of controller objects instantiated from controller_module.
259
260        Raises:
261            ControllerError is raised if no corresponding config can be found,
262            or if the controller module has already been registered.
263        """
264        logging.info("cwd: %s", os.getcwd())
265        logging.info("adb devices: %s", module.list_adb_devices())
266        self.verifyControllerModule(module)
267        module_ref_name = module.__name__.split('.')[-1]
268        if module_ref_name in self.controller_registry:
269            raise signals.ControllerError(
270                ("Controller module %s has already "
271                 "been registered. It can not be "
272                 "registered again.") % module_ref_name)
273        # Create controller objects.
274        create = module.create
275        module_config_name = module.VTS_CONTROLLER_CONFIG_NAME
276        if module_config_name not in self.testbed_configs:
277            raise signals.ControllerError(("No corresponding config found for"
278                                           " %s") % module_config_name)
279        try:
280            # Make a deep copy of the config to pass to the controller module,
281            # in case the controller module modifies the config internally.
282            original_config = self.testbed_configs[module_config_name]
283            controller_config = copy.deepcopy(original_config)
284            logging.info("controller_config: %s", controller_config)
285            if "use_vts_agent" not in self.testbed_configs:
286                objects = create(controller_config, start_services)
287            else:
288                objects = create(controller_config,
289                                 self.testbed_configs["use_vts_agent"])
290        except:
291            logging.exception(("Failed to initialize objects for controller "
292                               "%s, abort!"), module_config_name)
293            raise
294        if not isinstance(objects, list):
295            raise ControllerError(("Controller module %s did not return a list"
296                                   " of objects, abort.") % module_ref_name)
297        self.controller_registry[module_ref_name] = objects
298        logging.debug("Found %d objects for controller %s", len(objects),
299                      module_config_name)
300        destroy_func = module.destroy
301        self.controller_destructors[module_ref_name] = destroy_func
302        return objects
303
304    def unregisterControllers(self):
305        """Destroy controller objects and clear internal registry.
306
307        This will be called at the end of each TestRunner.run call.
308        """
309        for name, destroy in self.controller_destructors.items():
310            try:
311                logging.debug("Destroying %s.", name)
312                dut = self.controller_destructors[name][0]
313                destroy(self.controller_registry[name])
314            except:
315                logging.exception("Exception occurred destroying %s.", name)
316        self.controller_registry = {}
317        self.controller_destructors = {}
318
319    def parseTestConfig(self, test_configs):
320        """Parses the test configuration and unpacks objects and parameters
321        into a dictionary to be passed to test classes.
322
323        Args:
324            test_configs: A json object representing the test configurations.
325        """
326        self.test_run_info[
327            keys.ConfigKeys.IKEY_TESTBED_NAME] = self.testbed_name
328        # Unpack other params.
329        self.test_run_info["registerController"] = self.registerController
330        self.test_run_info[keys.ConfigKeys.IKEY_LOG_PATH] = self.log_path
331        user_param_pairs = []
332        for item in test_configs.items():
333            if item[0] not in keys.ConfigKeys.RESERVED_KEYS:
334                user_param_pairs.append(item)
335        self.test_run_info[keys.ConfigKeys.IKEY_USER_PARAM] = copy.deepcopy(
336            dict(user_param_pairs))
337
338    def runTestClass(self, test_cls, test_cases=None):
339        """Instantiates and executes a test class.
340
341        If test_cases is None, the test cases listed by self.tests will be
342        executed instead. If self.tests is empty as well, no test case in this
343        test class will be executed.
344
345        Args:
346            test_cls: The test class to be instantiated and executed.
347            test_cases: List of test case names to execute within the class.
348
349        Returns:
350            A tuple, with the number of cases passed at index 0, and the total
351            number of test cases at index 1.
352        """
353        self.running = True
354        with test_cls(self.test_run_info) as test_cls_instance:
355            try:
356                cls_result = test_cls_instance.run(test_cases)
357                self.results += cls_result
358            except signals.TestAbortAll as e:
359                self.results += e.results
360                raise e
361
362    def run(self):
363        """Executes test cases.
364
365        This will instantiate controller and test classes, and execute test
366        classes. This can be called multiple times to repeatly execute the
367        requested test cases.
368
369        A call to TestRunner.stop should eventually happen to conclude the life
370        cycle of a TestRunner.
371
372        Args:
373            test_classes: A dictionary where the key is test class name, and
374                          the values are actual test classes.
375        """
376        if not self.running:
377            self.running = True
378        # Initialize controller objects and pack appropriate objects/params
379        # to be passed to test class.
380        self.parseTestConfig(self.test_configs)
381        t_configs = self.test_configs[keys.ConfigKeys.KEY_TEST_PATHS]
382        test_classes = self.importTestModules(t_configs)
383        logging.debug("Executing run list %s.", self.run_list)
384        try:
385            for test_cls_name, test_case_names in self.run_list:
386                if not self.running:
387                    break
388                if test_case_names:
389                    logging.debug("Executing test cases %s in test class %s.",
390                                  test_case_names, test_cls_name)
391                else:
392                    logging.debug("Executing test class %s", test_cls_name)
393                try:
394                    test_cls = test_classes[test_cls_name]
395                except KeyError:
396                    raise USERError(
397                        ("Unable to locate class %s in any of the test "
398                         "paths specified.") % test_cls_name)
399                try:
400                    self.runTestClass(test_cls, test_case_names)
401                except signals.TestAbortAll as e:
402                    logging.warning(
403                        ("Abort all subsequent test classes. Reason: "
404                         "%s"), e)
405                    raise
406        finally:
407            self.unregisterControllers()
408
409    def stop(self):
410        """Releases resources from test run. Should always be called after
411        TestRunner.run finishes.
412
413        This function concludes a test run and writes out a test report.
414        """
415        if self.running:
416            msg = "\nSummary for test run %s: %s\n" % (self.id,
417                                                       self.results.summary())
418            self._writeResultsJsonString()
419            logging.info(msg.strip())
420            logger.killTestLogger(logging.getLogger())
421            self.running = False
422
423    def _writeResultsJsonString(self):
424        """Writes out a json file with the test result info for easy parsing.
425        """
426        path = os.path.join(self.log_path, "test_run_summary.json")
427        with open(path, 'w') as f:
428            f.write(self.results.jsonString())
429