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 signal 21import subprocess 22import traceback 23 24from functools import wraps 25from grpc import RpcError 26 27from acts import asserts, signals 28from acts.context import get_current_context 29from acts.base_test import BaseTestClass 30 31from cert.async_subprocess_logger import AsyncSubprocessLogger 32from cert.os_utils import get_gd_root 33from cert.os_utils import read_crash_snippet_and_log_tail 34from cert.os_utils import is_subprocess_alive 35from cert.os_utils import make_ports_available 36from cert.os_utils import TerminalColor 37from cert.gd_device import MOBLY_CONTROLLER_CONFIG_NAME as CONTROLLER_CONFIG_NAME 38from facade import rootservice_pb2 as facade_rootservice 39 40 41class GdBaseTestClass(BaseTestClass): 42 43 SUBPROCESS_WAIT_TIMEOUT_SECONDS = 10 44 45 def setup_class(self, dut_module, cert_module): 46 self.dut_module = dut_module 47 self.cert_module = cert_module 48 self.log_path_base = get_current_context().get_full_output_path() 49 self.verbose_mode = bool(self.user_params.get('verbose_mode', False)) 50 for config in self.controller_configs[CONTROLLER_CONFIG_NAME]: 51 config['verbose_mode'] = self.verbose_mode 52 53 # Start root-canal if needed 54 self.rootcanal_running = False 55 if 'rootcanal' in self.controller_configs: 56 self.rootcanal_running = True 57 # Get root canal binary 58 rootcanal = os.path.join(get_gd_root(), "root-canal") 59 asserts.assert_true(os.path.isfile(rootcanal), "Root canal does not exist at %s" % rootcanal) 60 61 # Get root canal log 62 self.rootcanal_logpath = os.path.join(self.log_path_base, 'rootcanal_logs.txt') 63 # Make sure ports are available 64 rootcanal_config = self.controller_configs['rootcanal'] 65 rootcanal_test_port = int(rootcanal_config.get("test_port", "6401")) 66 rootcanal_hci_port = int(rootcanal_config.get("hci_port", "6402")) 67 rootcanal_link_layer_port = int(rootcanal_config.get("link_layer_port", "6403")) 68 asserts.assert_true( 69 make_ports_available((rootcanal_test_port, rootcanal_hci_port, rootcanal_link_layer_port)), 70 "Failed to make root canal ports available") 71 72 # Start root canal process 73 rootcanal_cmd = [ 74 rootcanal, str(rootcanal_test_port), 75 str(rootcanal_hci_port), 76 str(rootcanal_link_layer_port) 77 ] 78 self.log.debug("Running %s" % " ".join(rootcanal_cmd)) 79 self.rootcanal_process = subprocess.Popen( 80 rootcanal_cmd, 81 cwd=get_gd_root(), 82 env=os.environ.copy(), 83 stdout=subprocess.PIPE, 84 stderr=subprocess.STDOUT, 85 universal_newlines=True) 86 asserts.assert_true(self.rootcanal_process, msg="Cannot start root-canal at " + str(rootcanal)) 87 asserts.assert_true( 88 is_subprocess_alive(self.rootcanal_process), msg="root-canal stopped immediately after running") 89 90 self.rootcanal_logger = AsyncSubprocessLogger( 91 self.rootcanal_process, [self.rootcanal_logpath], 92 log_to_stdout=self.verbose_mode, 93 tag="rootcanal", 94 color=TerminalColor.MAGENTA) 95 96 # Modify the device config to include the correct root-canal port 97 for gd_device_config in self.controller_configs.get("GdDevice"): 98 gd_device_config["rootcanal_port"] = str(rootcanal_hci_port) 99 100 # Parse and construct GD device objects 101 self.register_controller(importlib.import_module('cert.gd_device'), builtin=True) 102 self.dut = self.gd_devices[1] 103 self.cert = self.gd_devices[0] 104 105 def teardown_class(self): 106 if self.rootcanal_running: 107 stop_signal = signal.SIGINT 108 self.rootcanal_process.send_signal(stop_signal) 109 try: 110 return_code = self.rootcanal_process.wait(timeout=self.SUBPROCESS_WAIT_TIMEOUT_SECONDS) 111 except subprocess.TimeoutExpired: 112 logging.error("Failed to interrupt root canal via SIGINT, sending SIGKILL") 113 stop_signal = signal.SIGKILL 114 self.rootcanal_process.kill() 115 try: 116 return_code = self.rootcanal_process.wait(timeout=self.SUBPROCESS_WAIT_TIMEOUT_SECONDS) 117 except subprocess.TimeoutExpired: 118 logging.error("Failed to kill root canal") 119 return_code = -65536 120 if return_code != 0 and return_code != -stop_signal: 121 logging.error("rootcanal stopped with code: %d" % return_code) 122 self.rootcanal_logger.stop() 123 124 def setup_test(self): 125 self.dut.rootservice.StartStack( 126 facade_rootservice.StartStackRequest( 127 module_under_test=facade_rootservice.BluetoothModule.Value(self.dut_module),)) 128 self.cert.rootservice.StartStack( 129 facade_rootservice.StartStackRequest( 130 module_under_test=facade_rootservice.BluetoothModule.Value(self.cert_module),)) 131 132 self.dut.wait_channel_ready() 133 self.cert.wait_channel_ready() 134 135 def teardown_test(self): 136 self.cert.rootservice.StopStack(facade_rootservice.StopStackRequest()) 137 self.dut.rootservice.StopStack(facade_rootservice.StopStackRequest()) 138 139 def __getattribute__(self, name): 140 attr = super().__getattribute__(name) 141 if not callable(attr) or not GdBaseTestClass.__is_entry_function(name): 142 return attr 143 144 @wraps(attr) 145 def __wrapped(*args, **kwargs): 146 try: 147 return attr(*args, **kwargs) 148 except RpcError as e: 149 exception_info = "".join(traceback.format_exception(e.__class__, e, e.__traceback__)) 150 raise signals.TestFailure( 151 "RpcError during test\n\nRpcError:\n\n%s\n%s" % (exception_info, self.__dump_crashes())) 152 153 return __wrapped 154 155 __ENTRY_METHODS = {"setup_class", "teardown_class", "setup_test", "teardown_test"} 156 157 @staticmethod 158 def __is_entry_function(name): 159 return name.startswith("test_") or name in GdBaseTestClass.__ENTRY_METHODS 160 161 def __dump_crashes(self): 162 """ 163 :return: formatted stack traces if found, or last few lines of log 164 """ 165 dut_crash, dut_log_tail = self.dut.get_crash_snippet_and_log_tail() 166 cert_crash, cert_log_tail = self.cert.get_crash_snippet_and_log_tail() 167 rootcanal_crash = None 168 rootcanal_log_tail = None 169 if self.rootcanal_running and not is_subprocess_alive(self.rootcanal_process): 170 rootcanal_crash, roocanal_log_tail = read_crash_snippet_and_log_tail(self.rootcanal_logpath) 171 172 crash_detail = "" 173 if dut_crash or cert_crash or rootcanal_crash: 174 if rootcanal_crash: 175 crash_detail += "rootcanal crashed:\n\n%s\n\n" % rootcanal_crash 176 if dut_crash: 177 crash_detail += "dut stack crashed:\n\n%s\n\n" % dut_crash 178 if cert_crash: 179 crash_detail += "cert stack crashed:\n\n%s\n\n" % cert_crash 180 else: 181 if rootcanal_log_tail: 182 crash_detail += "rootcanal log tail:\n\n%s\n\n" % rootcanal_log_tail 183 if dut_log_tail: 184 crash_detail += "dut log tail:\n\n%s\n\n" % dut_log_tail 185 if cert_log_tail: 186 crash_detail += "cert log tail:\n\n%s\n\n" % cert_log_tail 187 188 return crash_detail 189