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