1# Copyright 2017, The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15""" 16Base test runner class. 17 18Class that other test runners will instantiate for test runners. 19""" 20 21from __future__ import print_function 22 23import errno 24import logging 25import signal 26import subprocess 27import tempfile 28import os 29 30from collections import namedtuple 31from typing import Any, Dict, List, Set 32 33from atest import atest_error 34from atest import atest_utils 35from atest import constants 36from atest.test_finders import test_info 37 38OLD_OUTPUT_ENV_VAR = 'ATEST_OLD_OUTPUT' 39 40# TestResult contains information of individual tests during a test run. 41TestResult = namedtuple('TestResult', ['runner_name', 'group_name', 42 'test_name', 'status', 'details', 43 'test_count', 'test_time', 44 'runner_total', 'group_total', 45 'additional_info', 'test_run_name']) 46ASSUMPTION_FAILED = 'ASSUMPTION_FAILED' 47FAILED_STATUS = 'FAILED' 48PASSED_STATUS = 'PASSED' 49IGNORED_STATUS = 'IGNORED' 50ERROR_STATUS = 'ERROR' 51 52 53class TestRunnerBase: 54 """Base Test Runner class.""" 55 NAME = '' 56 EXECUTABLE = '' 57 58 def __init__(self, results_dir, **kwargs): 59 """Init stuff for base class.""" 60 self.results_dir = results_dir 61 self.test_log_file = None 62 if not self.NAME: 63 raise atest_error.NoTestRunnerName('Class var NAME is not defined.') 64 if not self.EXECUTABLE: 65 raise atest_error.NoTestRunnerExecutable('Class var EXECUTABLE is ' 66 'not defined.') 67 if kwargs: 68 for key, value in kwargs.items(): 69 if not 'test_infos' in key: 70 logging.debug('ignoring the following args: %s=%s', 71 key, value) 72 73 def run(self, cmd, output_to_stdout=False, env_vars=None): 74 """Shell out and execute command. 75 76 Args: 77 cmd: A string of the command to execute. 78 output_to_stdout: A boolean. If False, the raw output of the run 79 command will not be seen in the terminal. This 80 is the default behavior, since the test_runner's 81 run_tests() method should use atest's 82 result reporter to print the test results. 83 84 Set to True to see the output of the cmd. This 85 would be appropriate for verbose runs. 86 env_vars: Environment variables passed to the subprocess. 87 """ 88 if not output_to_stdout: 89 self.test_log_file = tempfile.NamedTemporaryFile( 90 mode='w', dir=self.results_dir, delete=True) 91 logging.debug('Executing command: %s', cmd) 92 return subprocess.Popen(cmd, start_new_session=True, shell=True, 93 stderr=subprocess.STDOUT, 94 stdout=self.test_log_file, env=env_vars) 95 96 # pylint: disable=broad-except 97 def handle_subprocess(self, subproc, func): 98 """Execute the function. Interrupt the subproc when exception occurs. 99 100 Args: 101 subproc: A subprocess to be terminated. 102 func: A function to be run. 103 """ 104 try: 105 signal.signal(signal.SIGINT, self._signal_passer(subproc)) 106 func() 107 except Exception as error: 108 # exc_info=1 tells logging to log the stacktrace 109 logging.debug('Caught exception:', exc_info=1) 110 # If atest crashes, try to kill subproc group as well. 111 try: 112 logging.debug('Killing subproc: %s', subproc.pid) 113 os.killpg(os.getpgid(subproc.pid), signal.SIGINT) 114 except OSError: 115 # this wipes our previous stack context, which is why 116 # we have to save it above. 117 logging.debug('Subproc already terminated, skipping') 118 finally: 119 if self.test_log_file: 120 with open(self.test_log_file.name, 'r') as f: 121 intro_msg = "Unexpected Issue. Raw Output:" 122 print(atest_utils.colorize(intro_msg, constants.RED)) 123 print(f.read()) 124 # Ignore socket.recv() raising due to ctrl-c 125 if not error.args or error.args[0] != errno.EINTR: 126 raise error 127 128 def wait_for_subprocess(self, proc): 129 """Check the process status. Interrupt the TF subporcess if user 130 hits Ctrl-C. 131 132 Args: 133 proc: The tradefed subprocess. 134 135 Returns: 136 Return code of the subprocess for running tests. 137 """ 138 try: 139 logging.debug('Runner Name: %s, Process ID: %s', 140 self.NAME, proc.pid) 141 signal.signal(signal.SIGINT, self._signal_passer(proc)) 142 proc.wait() 143 return proc.returncode 144 except: 145 # If atest crashes, kill TF subproc group as well. 146 os.killpg(os.getpgid(proc.pid), signal.SIGINT) 147 raise 148 149 def _signal_passer(self, proc): 150 """Return the signal_handler func bound to proc. 151 152 Args: 153 proc: The tradefed subprocess. 154 155 Returns: 156 signal_handler function. 157 """ 158 def signal_handler(_signal_number, _frame): 159 """Pass SIGINT to proc. 160 161 If user hits ctrl-c during atest run, the TradeFed subprocess 162 won't stop unless we also send it a SIGINT. The TradeFed process 163 is started in a process group, so this SIGINT is sufficient to 164 kill all the child processes TradeFed spawns as well. 165 """ 166 print('Process ID: %s', proc.pid) 167 logging.info('Ctrl-C received. Killing process group ID: %s', 168 os.getpgid(proc.pid)) 169 os.killpg(os.getpgid(proc.pid), signal.SIGINT) 170 return signal_handler 171 172 def run_tests(self, test_infos, extra_args, reporter): 173 """Run the list of test_infos. 174 175 Should contain code for kicking off the test runs using 176 test_runner_base.run(). Results should be processed and printed 177 via the reporter passed in. 178 179 Args: 180 test_infos: List of TestInfo. 181 extra_args: Dict of extra args to add to test run. 182 reporter: An instance of result_report.ResultReporter. 183 """ 184 raise NotImplementedError 185 186 def host_env_check(self): 187 """Checks that host env has met requirements.""" 188 raise NotImplementedError 189 190 def get_test_runner_build_reqs(self, test_infos: List[test_info.TestInfo]): 191 """Returns a list of build targets required by the test runner.""" 192 raise NotImplementedError 193 194 def generate_run_commands(self, test_infos, extra_args, port=None): 195 """Generate a list of run commands from TestInfos. 196 197 Args: 198 test_infos: A set of TestInfo instances. 199 extra_args: A Dict of extra args to append. 200 port: Optional. An int of the port number to send events to. 201 Subprocess reporter in TF won't try to connect if it's None. 202 203 Returns: 204 A list of run commands to run the tests. 205 """ 206 raise NotImplementedError 207 208 209def gather_build_targets( 210 test_infos: List[test_info.TestInfo]) -> Set[str]: 211 """Gets all build targets for the given tests. 212 213 Args: 214 test_infos: List of TestInfo. 215 216 Returns: 217 Set of build targets. 218 """ 219 build_targets = set() 220 221 for t_info in test_infos: 222 build_targets |= t_info.build_targets 223 224 return build_targets 225