• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3#   Copyright 2019 - The Android Open Source Project
4#
5#   Licensed under the Apache License, Version 2.0 (the "License");
6#   you may not use this file except in compliance with the License.
7#   You may obtain a copy of the License at
8#
9#       http://www.apache.org/licenses/LICENSE-2.0
10#
11#   Unless required by applicable law or agreed to in writing, software
12#   distributed under the License is distributed on an "AS IS" BASIS,
13#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14#   See the License for the specific language governing permissions and
15#   limitations under the License.
16
17import importlib
18import logging
19import os
20import traceback
21import signal
22import subprocess
23
24from blueberry.facade import rootservice_pb2 as facade_rootservice
25from functools import wraps
26from grpc import RpcError
27
28from blueberry.tests.gd.cert.async_subprocess_logger import AsyncSubprocessLogger
29from blueberry.tests.gd.cert.context import append_test_context, get_current_context, pop_test_context, ContextLevel
30from blueberry.tests.gd.cert.gd_device import MOBLY_CONTROLLER_CONFIG_NAME as CONTROLLER_CONFIG_NAME
31from blueberry.tests.gd.cert.os_utils import get_gd_root
32from blueberry.tests.gd.cert.os_utils import read_crash_snippet_and_log_tail
33from blueberry.tests.gd.cert.os_utils import is_subprocess_alive
34from blueberry.tests.gd.cert.os_utils import make_ports_available
35from blueberry.tests.gd.cert.os_utils import TerminalColor
36from blueberry.tests.gd.cert.tracelogger import TraceLogger
37
38from mobly import asserts, signals
39from mobly import base_test
40
41
42class Timeout:
43
44    def __init__(self, seconds=1, error_message='Timeout'):
45        self.seconds = seconds
46        self.error_message = error_message
47
48    def handle_timeout(self, signum, frame):
49        raise TimeoutError(self.error_message)
50
51    def __enter__(self):
52        signal.signal(signal.SIGALRM, self.handle_timeout)
53        signal.alarm(self.seconds)
54
55    def __exit__(self, type, value, traceback):
56        signal.alarm(0)
57
58
59class GdBaseTestClass(base_test.BaseTestClass):
60
61    FUNCTION_CALL_TIMEOUT_SECONDS = 5
62    SUBPROCESS_WAIT_TIMEOUT_SECONDS = 10
63
64    def setup_class(self, dut_module, cert_module):
65        self.dut_module = dut_module
66        self.cert_module = cert_module
67        self.log = TraceLogger(logging.getLogger())
68        self.dut_coverage_info = None
69        self.cert_coverage_info = None
70
71    def teardown_class(self):
72        # assume each test runs the same binary for dut and cert
73        # generate coverage report after running all tests in a class
74        if self.dut_coverage_info:
75            self.dut.generate_coverage_report_for_host(self.dut_coverage_info)
76            self.dut_coverage_info = None
77        if self.cert_coverage_info:
78            self.cert.generate_coverage_report_for_host(self.cert_coverage_info)
79            self.cert_coverage_info = None
80
81    def set_controller_properties_path(self, path):
82        GD_DIR = os.path.join(os.getcwd(), os.pardir)
83        self.controller_properties_file = os.path.join(GD_DIR, path)
84
85    def setup_test(self):
86        append_test_context(test_class_name=self.TAG, test_name=self.current_test_info.name)
87        self.log_path_base = get_current_context().get_full_output_path()
88        self.verbose_mode = bool(self.user_params.get('verbose_mode', False))
89        for config in self.controller_configs[CONTROLLER_CONFIG_NAME]:
90            config['verbose_mode'] = self.verbose_mode
91
92        try:
93            controller_properties_file = self.controller_properties_file
94        except AttributeError:
95            controller_properties_file = ''
96
97        self.setup_rootcanal(controller_properties_file)
98
99        # Parse and construct GD device objects
100        self.register_controller(importlib.import_module('blueberry.tests.gd.cert.gd_device'), builtin=True)
101        self.dut = self.gd_device[1]
102        self.cert = self.gd_device[0]
103        if self.dut.host_only_device:
104            new_dut_coverage_info = self.dut.get_coverage_info()
105            if self.dut_coverage_info:
106                asserts.assert_true(
107                    self.dut_coverage_info == new_dut_coverage_info,
108                    msg="DUT coverage info must be the same for each test run, old: {}, new: {}".format(
109                        self.dut_coverage_info, new_dut_coverage_info))
110            self.dut_coverage_info = new_dut_coverage_info
111        if self.cert.host_only_device:
112            new_cert_coverage_info = self.cert.get_coverage_info()
113            if self.cert_coverage_info:
114                asserts.assert_true(
115                    self.cert_coverage_info == new_cert_coverage_info,
116                    msg="CERT coverage info must be the same for each test run, old: {}, new: {}".format(
117                        self.cert_coverage_info, new_cert_coverage_info))
118            self.cert_coverage_info = new_cert_coverage_info
119
120        try:
121            self.dut.rootservice.StartStack(
122                facade_rootservice.StartStackRequest(
123                    module_under_test=facade_rootservice.BluetoothModule.Value(self.dut_module)))
124        except RpcError as rpc_error:
125            asserts.fail("Failed to start DUT stack, RpcError={!r}".format(rpc_error))
126        try:
127            self.cert.rootservice.StartStack(
128                facade_rootservice.StartStackRequest(
129                    module_under_test=facade_rootservice.BluetoothModule.Value(self.cert_module)))
130        except RpcError as rpc_error:
131            asserts.fail("Failed to start CERT stack, RpcError={!r}".format(rpc_error))
132        self.dut.wait_channel_ready()
133        self.cert.wait_channel_ready()
134
135    def teardown_test(self):
136        stack = ""
137        try:
138            with Timeout(seconds=self.FUNCTION_CALL_TIMEOUT_SECONDS):
139                stack = "CERT"
140                self.cert.rootservice.StopStack(facade_rootservice.StopStackRequest())
141                stack = "DUT"
142                self.dut.rootservice.StopStack(facade_rootservice.StopStackRequest())
143        except RpcError as rpc_error:
144            asserts.fail("Failed to stop {} stack, RpcError={!r}".format(stack, rpc_error))
145        except TimeoutError:
146            logging.error("Failed to stop {} stack in {} s".format(stack, self.FUNCTION_CALL_TIMEOUT_SECONDS))
147        finally:
148            # Destroy GD device objects
149            self._controller_manager.unregister_controllers()
150            self.teardown_rootcanal()
151            pop_test_context()
152
153    def setup_rootcanal(self, controller_properties_file=''):
154        # Start root-canal if needed
155        self.rootcanal_running = False
156        self.rootcanal_logpath = None
157        self.rootcanal_process = None
158        self.rootcanal_logger = None
159        if 'rootcanal' in self.controller_configs:
160            self.rootcanal_running = True
161            # Get root canal binary
162            rootcanal = os.path.join(get_gd_root(), "root-canal")
163            asserts.assert_true(os.path.isfile(rootcanal), "Root canal does not exist at %s" % rootcanal)
164
165            # Get root canal log
166            self.rootcanal_logpath = os.path.join(self.log_path_base, 'rootcanal_logs.txt')
167            # Make sure ports are available
168            rootcanal_config = self.controller_configs['rootcanal']
169            rootcanal_test_port = int(rootcanal_config.get("test_port", "6401"))
170            rootcanal_hci_port = int(rootcanal_config.get("hci_port", "6402"))
171            rootcanal_link_layer_port = int(rootcanal_config.get("link_layer_port", "6403"))
172            asserts.assert_true(
173                make_ports_available((rootcanal_test_port, rootcanal_hci_port, rootcanal_link_layer_port)),
174                "Failed to free ports rootcanal_test_port={}, rootcanal_hci_port={}, rootcanal_link_layer_port={}".
175                format(rootcanal_test_port, rootcanal_hci_port, rootcanal_link_layer_port))
176
177            # Start root canal process
178            rootcanal_cmd = [
179                rootcanal,
180                str(rootcanal_test_port),
181                str(rootcanal_hci_port),
182                str(rootcanal_link_layer_port), '-controller_properties_file=' + controller_properties_file
183            ]
184            self.log.debug("Running %s" % " ".join(rootcanal_cmd))
185            self.rootcanal_process = subprocess.Popen(
186                rootcanal_cmd,
187                cwd=get_gd_root(),
188                env=os.environ.copy(),
189                stdout=subprocess.PIPE,
190                stderr=subprocess.STDOUT,
191                universal_newlines=True)
192
193            asserts.assert_true(self.rootcanal_process, msg="Cannot start root-canal at " + str(rootcanal))
194            asserts.assert_true(
195                is_subprocess_alive(self.rootcanal_process), msg="root-canal stopped immediately after running")
196
197            self.rootcanal_logger = AsyncSubprocessLogger(
198                self.rootcanal_process, [self.rootcanal_logpath],
199                log_to_stdout=self.verbose_mode,
200                tag="rootcanal",
201                color=TerminalColor.MAGENTA)
202
203            # Modify the device config to include the correct root-canal port
204            for gd_device_config in self.controller_configs.get("GdDevice"):
205                gd_device_config["rootcanal_port"] = str(rootcanal_hci_port)
206
207    def teardown_rootcanal(self):
208        if self.rootcanal_running:
209            stop_signal = signal.SIGINT
210            self.rootcanal_process.send_signal(stop_signal)
211            try:
212                return_code = self.rootcanal_process.wait(timeout=self.SUBPROCESS_WAIT_TIMEOUT_SECONDS)
213            except subprocess.TimeoutExpired:
214                logging.error("Failed to interrupt root canal via SIGINT, sending SIGKILL")
215                stop_signal = signal.SIGKILL
216                self.rootcanal_process.kill()
217                try:
218                    return_code = self.rootcanal_process.wait(timeout=self.SUBPROCESS_WAIT_TIMEOUT_SECONDS)
219                except subprocess.TimeoutExpired:
220                    logging.error("Failed to kill root canal")
221                    return_code = -65536
222            if return_code != 0 and return_code != -stop_signal:
223                logging.error("rootcanal stopped with code: %d" % return_code)
224            self.rootcanal_logger.stop()
225
226    @staticmethod
227    def get_module_reference_name(a_module):
228        """Returns the module's module's submodule name as reference name.
229
230        Args:
231            a_module: Any module. Ideally, a controller module.
232        Returns:
233            A string corresponding to the module's name.
234        """
235        return a_module.__name__.split('.')[-1]
236
237    def register_controller(self, controller_module, required=True, builtin=False):
238        """Registers an controller module for a test class. Invokes Mobly's
239        implementation of register_controller.
240        """
241        module_ref_name = self.get_module_reference_name(controller_module)
242        module_config_name = controller_module.MOBLY_CONTROLLER_CONFIG_NAME
243
244        # Get controller objects from Mobly's register_controller
245        controllers = self._controller_manager.register_controller(controller_module, required=required)
246        if not controllers:
247            return None
248
249        # Log controller information
250        # Implementation of "get_info" is optional for a controller module.
251        if hasattr(controller_module, "get_info"):
252            controller_info = controller_module.get_info(controllers)
253            self.log.info("Controller %s: %s", module_config_name, controller_info)
254
255        if builtin:
256            setattr(self, module_ref_name, controllers)
257        return controllers
258
259    def __getattribute__(self, name):
260        attr = super().__getattribute__(name)
261        if not callable(attr) or not GdBaseTestClass.__is_entry_function(name):
262            return attr
263
264        @wraps(attr)
265        def __wrapped(*args, **kwargs):
266            try:
267                return attr(*args, **kwargs)
268            except RpcError as e:
269                exception_info = "".join(traceback.format_exception(e.__class__, e, e.__traceback__))
270                raise signals.TestFailure(
271                    "RpcError during test\n\nRpcError:\n\n%s\n%s" % (exception_info, self.__dump_crashes()))
272
273        return __wrapped
274
275    __ENTRY_METHODS = {"setup_class", "teardown_class", "setup_test", "teardown_test"}
276
277    @staticmethod
278    def __is_entry_function(name):
279        return name.startswith("test_") or name in GdBaseTestClass.__ENTRY_METHODS
280
281    def __dump_crashes(self):
282        """
283        return: formatted stack traces if found, or last few lines of log
284        """
285        dut_crash, dut_log_tail = self.dut.get_crash_snippet_and_log_tail()
286        cert_crash, cert_log_tail = self.cert.get_crash_snippet_and_log_tail()
287        rootcanal_crash = None
288        rootcanal_log_tail = None
289        if self.rootcanal_running and not is_subprocess_alive(self.rootcanal_process):
290            rootcanal_crash, roocanal_log_tail = read_crash_snippet_and_log_tail(self.rootcanal_logpath)
291
292        crash_detail = ""
293        if dut_crash or cert_crash or rootcanal_crash:
294            if rootcanal_crash:
295                crash_detail += "rootcanal crashed:\n\n%s\n\n" % rootcanal_crash
296            if dut_crash:
297                crash_detail += "dut stack crashed:\n\n%s\n\n" % dut_crash
298            if cert_crash:
299                crash_detail += "cert stack crashed:\n\n%s\n\n" % cert_crash
300        else:
301            if rootcanal_log_tail:
302                crash_detail += "rootcanal log tail:\n\n%s\n\n" % rootcanal_log_tail
303            if dut_log_tail:
304                crash_detail += "dut log tail:\n\n%s\n\n" % dut_log_tail
305            if cert_log_tail:
306                crash_detail += "cert log tail:\n\n%s\n\n" % cert_log_tail
307
308        return crash_detail
309