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