1#!/usr/bin/env python3
2#
3#   Copyright 2021 - 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 asyncio
18import importlib
19import logging
20import os
21import signal
22import subprocess
23
24from blueberry.tests.gd.cert.async_subprocess_logger import AsyncSubprocessLogger
25from blueberry.tests.gd.cert.context import get_current_context
26from blueberry.tests.gd.cert.os_utils import get_gd_root
27from blueberry.tests.gd.cert.os_utils import get_gd_root
28from blueberry.tests.gd.cert.os_utils import read_crash_snippet_and_log_tail
29from blueberry.tests.gd.cert.os_utils import is_subprocess_alive
30from blueberry.tests.gd.cert.os_utils import make_ports_available
31from blueberry.tests.gd.cert.os_utils import TerminalColor
32from blueberry.tests.gd.cert.tracelogger import TraceLogger
33from blueberry.tests.gd.cert.truth import assertThat
34from blueberry.tests.topshim.lib.adapter_client import AdapterClient
35from blueberry.tests.topshim.lib.async_closable import asyncSafeClose
36from blueberry.tests.topshim.lib.gatt_client import GattClient
37from blueberry.tests.topshim.lib.hf_client_client import HfClientClient
38from blueberry.tests.topshim.lib.hfp_client import HfpClient
39from blueberry.tests.topshim.lib.security_client import SecurityClient
40from blueberry.tests.topshim.lib.topshim_device import TopshimDevice
41
42from mobly import asserts
43from mobly import base_test
44
45CONTROLLER_CONFIG_NAME = "GdDevice"
46
47
48def _setup_class_core(verbose_mode, log_path_base, controller_configs):
49    info = {}
50    info['controller_configs'] = controller_configs
51
52    # Start root-canal if needed
53    info['rootcanal_running'] = False
54    info['rootcanal_logpath'] = None
55    info['rootcanal_process'] = None
56    info['rootcanal_logger'] = None
57    if 'rootcanal' not in info['controller_configs']:
58        return
59    info['rootcanal_running'] = True
60    # Get root canal binary
61    rootcanal = os.path.join(get_gd_root(), "root-canal")
62    info['rootcanal'] = rootcanal
63    info['rootcanal_exist'] = os.path.isfile(rootcanal)
64    if not os.path.isfile(rootcanal):
65        return info
66    # Get root canal log
67    rootcanal_logpath = os.path.join(log_path_base, 'rootcanal_logs.txt')
68    info['rootcanal_logpath'] = rootcanal_logpath
69    # Make sure ports are available
70    rootcanal_config = info['controller_configs']['rootcanal']
71    rootcanal_test_port = int(rootcanal_config.get("test_port", "6401"))
72    rootcanal_hci_port = int(rootcanal_config.get("hci_port", "6402"))
73    rootcanal_link_layer_port = int(rootcanal_config.get("link_layer_port", "6403"))
74
75    info['make_rootcanal_ports_available'] = make_ports_available(
76        (rootcanal_test_port, rootcanal_hci_port, rootcanal_link_layer_port))
77    if not make_ports_available((rootcanal_test_port, rootcanal_hci_port, rootcanal_link_layer_port)):
78        return info
79
80    # Start root canal process
81    rootcanal_cmd = [rootcanal, str(rootcanal_test_port), str(rootcanal_hci_port), str(rootcanal_link_layer_port)]
82    info['rootcanal_cmd'] = rootcanal_cmd
83
84    rootcanal_process = subprocess.Popen(rootcanal_cmd,
85                                         cwd=get_gd_root(),
86                                         env=os.environ.copy(),
87                                         stdout=subprocess.PIPE,
88                                         stderr=subprocess.STDOUT,
89                                         universal_newlines=True)
90
91    info['rootcanal_process'] = rootcanal_process
92    if rootcanal_process:
93        info['is_rootcanal_process_started'] = True
94    else:
95        info['is_rootcanal_process_started'] = False
96        return info
97    info['is_subprocess_alive'] = is_subprocess_alive(rootcanal_process)
98    if not is_subprocess_alive(rootcanal_process):
99        info['is_subprocess_alive'] = False
100        return info
101
102    info['rootcanal_logger'] = AsyncSubprocessLogger(rootcanal_process, [rootcanal_logpath],
103                                                     log_to_stdout=verbose_mode,
104                                                     tag="rootcanal",
105                                                     color=TerminalColor.MAGENTA)
106
107    # Modify the device config to include the correct root-canal port
108    for gd_device_config in info['controller_configs'].get("GdDevice"):
109        gd_device_config["rootcanal_port"] = str(rootcanal_hci_port)
110
111    return info
112
113
114def _teardown_class_core(rootcanal_running, rootcanal_process, rootcanal_logger, subprocess_wait_timeout_seconds):
115    if rootcanal_running:
116        stop_signal = signal.SIGINT
117        rootcanal_process.send_signal(stop_signal)
118        try:
119            return_code = rootcanal_process.wait(timeout=subprocess_wait_timeout_seconds)
120        except subprocess.TimeoutExpired:
121            logging.error("Failed to interrupt root canal via SIGINT, sending SIGKILL")
122            stop_signal = signal.SIGKILL
123            rootcanal_process.kill()
124            try:
125                return_code = rootcanal_process.wait(timeout=subprocess_wait_timeout_seconds)
126            except subprocess.TimeoutExpired:
127                logging.error("Failed to kill root canal")
128                return_code = -65536
129        if return_code != 0 and return_code != -stop_signal:
130            logging.error("rootcanal stopped with code: %d" % return_code)
131        rootcanal_logger.stop()
132
133
134def dump_crashes_core(dut, cert, rootcanal_running, rootcanal_process, rootcanal_logpath):
135    dut_crash, dut_log_tail = dut.get_crash_snippet_and_log_tail()
136    cert_crash, cert_log_tail = cert.get_crash_snippet_and_log_tail()
137    rootcanal_crash = None
138    rootcanal_log_tail = None
139    if rootcanal_running and not is_subprocess_alive(rootcanal_process):
140        rootcanal_crash, roocanal_log_tail = read_crash_snippet_and_log_tail(rootcanal_logpath)
141
142    crash_detail = ""
143    if dut_crash or cert_crash or rootcanal_crash:
144        if rootcanal_crash:
145            crash_detail += "rootcanal crashed:\n\n%s\n\n" % rootcanal_crash
146        if dut_crash:
147            crash_detail += "dut stack crashed:\n\n%s\n\n" % dut_crash
148        if cert_crash:
149            crash_detail += "cert stack crashed:\n\n%s\n\n" % cert_crash
150    else:
151        if rootcanal_log_tail:
152            crash_detail += "rootcanal log tail:\n\n%s\n\n" % rootcanal_log_tail
153        if dut_log_tail:
154            crash_detail += "dut log tail:\n\n%s\n\n" % dut_log_tail
155        if cert_log_tail:
156            crash_detail += "cert log tail:\n\n%s\n\n" % cert_log_tail
157
158    return crash_detail
159
160
161class TopshimBaseTest(base_test.BaseTestClass):
162
163    __dut = None
164    __cert = None
165
166    async def __setup_adapter(self):
167        dut_adapter = AdapterClient(port=self.dut_port)
168        cert_adapter = AdapterClient(port=self.cert_port)
169        started = await dut_adapter._verify_adapter_started()
170        assertThat(started).isTrue()
171        started = started and await cert_adapter._verify_adapter_started()
172        assertThat(started).isTrue()
173        self.__dut = TopshimDevice(dut_adapter, GattClient(port=self.dut_port),
174                                   SecurityClient(dut_adapter, port=self.dut_port),
175                                   HfpClient(port=self.dut_port),
176                                   HfClientClient(port=self.dut_port))
177        self.__cert = TopshimDevice(cert_adapter, GattClient(port=self.cert_port),
178                                    SecurityClient(cert_adapter, port=self.cert_port),
179                                    HfpClient(port=self.cert_port),
180                                    HfClientClient(port=self.cert_port))
181        return started
182
183    async def __teardown_adapter(self):
184        await asyncSafeClose(self.__dut)
185        await asyncSafeClose(self.__cert)
186
187    def dut(self):
188        """
189        Get a handle on the DUT device
190        """
191        return self.__dut
192
193    def cert(self):
194        """
195        Get a handle on the CERT device
196        """
197        return self.__cert
198
199    def setup_class(self):
200        """
201        Configure rootcanal and setup test parameters
202        """
203        self.log = TraceLogger(logging.getLogger())
204        self.log_path_base = get_current_context().get_full_output_path()
205        self.verbose_mode = bool(self.user_params.get('verbose_mode', False))
206        for config in self.controller_configs[CONTROLLER_CONFIG_NAME]:
207            config['verbose_mode'] = self.verbose_mode
208
209        self.info = _setup_class_core(verbose_mode=self.verbose_mode,
210                                      log_path_base=self.log_path_base,
211                                      controller_configs=self.controller_configs)
212        self.rootcanal_running = self.info['rootcanal_running']
213        self.rootcanal_logpath = self.info['rootcanal_logpath']
214        self.rootcanal_process = self.info['rootcanal_process']
215        self.rootcanal_logger = self.info['rootcanal_logger']
216
217        asserts.assert_true(self.info['rootcanal_exist'], "Root canal does not exist at %s" % self.info['rootcanal'])
218        asserts.assert_true(self.info['make_rootcanal_ports_available'], "Failed to make root canal ports available")
219
220        self.log.debug("Running %s" % " ".join(self.info['rootcanal_cmd']))
221        asserts.assert_true(self.info['is_rootcanal_process_started'],
222                            msg="Cannot start root-canal at " + str(self.info['rootcanal']))
223        asserts.assert_true(self.info['is_subprocess_alive'], msg="root-canal stopped immediately after running")
224
225        self.controller_configs = self.info['controller_configs']
226
227        controllers = self.register_controller(importlib.import_module('blueberry.tests.topshim.lib.topshim_device'))
228        self.cert_port = controllers[0].grpc_port
229        self.dut_port = controllers[1].grpc_port
230        asyncio.set_event_loop(asyncio.new_event_loop())
231        asyncio.get_event_loop().run_until_complete(self.__setup_adapter())
232
233    def teardown_class(self):
234        _teardown_class_core(rootcanal_running=self.rootcanal_running,
235                             rootcanal_process=self.rootcanal_process,
236                             rootcanal_logger=self.rootcanal_logger,
237                             subprocess_wait_timeout_seconds=1)
238        asyncio.get_event_loop().run_until_complete(self.__teardown_adapter())
239