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