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"""Base test runner class. 16 17Class that other test runners will instantiate for test runners. 18""" 19 20from __future__ import print_function 21 22from collections import namedtuple 23import errno 24import logging 25import os 26import signal 27import subprocess 28import tempfile 29from typing import Any, Dict, List, Set 30 31from atest import atest_error 32from atest import atest_utils 33from atest import device_update 34from atest.test_finders import test_info 35from atest.test_runner_invocation import TestRunnerInvocation 36 37OLD_OUTPUT_ENV_VAR = 'ATEST_OLD_OUTPUT' 38 39# TestResult contains information of individual tests during a test run. 40TestResult = namedtuple( 41 'TestResult', 42 [ 43 'runner_name', 44 'group_name', 45 'test_name', 46 'status', 47 'details', 48 'test_count', 49 'test_time', 50 'runner_total', 51 'group_total', 52 'additional_info', 53 'test_run_name', 54 ], 55) 56ASSUMPTION_FAILED = 'ASSUMPTION_FAILED' 57FAILED_STATUS = 'FAILED' 58PASSED_STATUS = 'PASSED' 59IGNORED_STATUS = 'IGNORED' 60ERROR_STATUS = 'ERROR' 61 62# Code for RunnerFinishEvent. 63RESULT_CODE = { 64 PASSED_STATUS: 0, 65 FAILED_STATUS: 1, 66 IGNORED_STATUS: 2, 67 ASSUMPTION_FAILED: 3, 68 ERROR_STATUS: 4, 69} 70 71 72class TestRunnerBase: 73 """Base Test Runner class.""" 74 75 NAME = '' 76 EXECUTABLE = '' 77 78 def __init__(self, results_dir, **kwargs): 79 """Init stuff for base class.""" 80 self.results_dir = results_dir 81 self.test_log_file = None 82 self._subprocess_stdout = None 83 if not self.NAME: 84 raise atest_error.NoTestRunnerName('Class var NAME is not defined.') 85 if not self.EXECUTABLE: 86 raise atest_error.NoTestRunnerExecutable( 87 'Class var EXECUTABLE is not defined.' 88 ) 89 if kwargs: 90 for key, value in kwargs.items(): 91 if not 'test_infos' in key: 92 logging.debug('Found auxiliary args: %s=%s', key, value) 93 94 def create_invocations( 95 self, 96 extra_args: Dict[str, Any], 97 test_infos: List[test_info.TestInfo], 98 ) -> List[TestRunnerInvocation]: 99 """Creates test runner invocations. 100 101 Args: 102 extra_args: A dict of arguments. 103 test_infos: A list of instances of TestInfo. 104 105 Returns: 106 A list of TestRunnerInvocation instances. 107 """ 108 return [ 109 TestRunnerInvocation( 110 test_runner=self, extra_args=extra_args, test_infos=test_infos 111 ) 112 ] 113 114 def requires_device_update( 115 self, test_infos: List[test_info.TestInfo] 116 ) -> bool: 117 """Checks whether this runner requires device update.""" 118 return False 119 120 def run( 121 self, 122 cmd, 123 output_to_stdout=False, 124 env_vars=None, 125 rolling_output_lines=False, 126 ): 127 """Shell out and execute command. 128 129 Args: 130 cmd: A string of the command to execute. 131 output_to_stdout: A boolean. If False, the raw output of the run command 132 will not be seen in the terminal. This is the default behavior, since 133 the test_runner's run_tests() method should use atest's result 134 reporter to print the test results. Set to True to see the output of 135 the cmd. This would be appropriate for verbose runs. 136 env_vars: Environment variables passed to the subprocess. 137 rolling_output_lines: If True, the subprocess output will be streamed 138 with rolling lines when output_to_stdout is False. 139 """ 140 logging.debug('Executing command: %s', cmd) 141 if rolling_output_lines: 142 proc = subprocess.Popen( 143 cmd, 144 start_new_session=True, 145 shell=True, 146 stderr=subprocess.STDOUT, 147 stdout=None if output_to_stdout else subprocess.PIPE, 148 env=env_vars, 149 ) 150 self._subprocess_stdout = proc.stdout 151 return proc 152 else: 153 if not output_to_stdout: 154 self.test_log_file = tempfile.NamedTemporaryFile( 155 mode='w', dir=self.results_dir, delete=True 156 ) 157 return subprocess.Popen( 158 cmd, 159 start_new_session=True, 160 shell=True, 161 stderr=subprocess.STDOUT, 162 stdout=self.test_log_file, 163 env=env_vars, 164 ) 165 166 # pylint: disable=broad-except 167 def handle_subprocess(self, subproc, func): 168 """Execute the function. Interrupt the subproc when exception occurs. 169 170 Args: 171 subproc: A subprocess to be terminated. 172 func: A function to be run. 173 """ 174 try: 175 signal.signal(signal.SIGINT, self._signal_passer(subproc)) 176 func() 177 except Exception as error: 178 # exc_info=1 tells logging to log the stacktrace 179 logging.debug('Caught exception:', exc_info=1) 180 # If atest crashes, try to kill subproc group as well. 181 try: 182 logging.debug('Killing subproc: %s', subproc.pid) 183 os.killpg(os.getpgid(subproc.pid), signal.SIGINT) 184 except OSError: 185 # this wipes our previous stack context, which is why 186 # we have to save it above. 187 logging.debug('Subproc already terminated, skipping') 188 finally: 189 full_output = '' 190 if self._subprocess_stdout: 191 full_output = self._subprocess_stdout.read() 192 elif self.test_log_file: 193 with open(self.test_log_file.name, 'r') as f: 194 full_output = f.read() 195 if full_output: 196 print(atest_utils.mark_red('Unexpected Issue. Raw Output:')) 197 print(full_output) 198 # Ignore socket.recv() raising due to ctrl-c 199 if not error.args or error.args[0] != errno.EINTR: 200 raise error 201 202 def wait_for_subprocess(self, proc): 203 """Check the process status. 204 205 Interrupt the TF subprocess if user hits Ctrl-C. 206 207 Args: 208 proc: The tradefed subprocess. 209 210 Returns: 211 Return code of the subprocess for running tests. 212 """ 213 try: 214 logging.debug('Runner Name: %s, Process ID: %s', self.NAME, proc.pid) 215 signal.signal(signal.SIGINT, self._signal_passer(proc)) 216 proc.wait() 217 return proc.returncode 218 except: 219 # If atest crashes, kill TF subproc group as well. 220 os.killpg(os.getpgid(proc.pid), signal.SIGINT) 221 raise 222 223 def _signal_passer(self, proc): 224 """Return the signal_handler func bound to proc. 225 226 Args: 227 proc: The tradefed subprocess. 228 229 Returns: 230 signal_handler function. 231 """ 232 233 def signal_handler(_signal_number, _frame): 234 """Pass SIGINT to proc. 235 236 If user hits ctrl-c during atest run, the TradeFed subprocess 237 won't stop unless we also send it a SIGINT. The TradeFed process 238 is started in a process group, so this SIGINT is sufficient to 239 kill all the child processes TradeFed spawns as well. 240 """ 241 print('Process ID: %s', proc.pid) 242 try: 243 atest_utils.print_and_log_info( 244 'Ctrl-C received. Killing process group ID: %s', 245 os.getpgid(proc.pid), 246 ) 247 os.killpg(os.getpgid(proc.pid), signal.SIGINT) 248 except ProcessLookupError as e: 249 atest_utils.print_and_log_info(e) 250 251 return signal_handler 252 253 def run_tests(self, test_infos, extra_args, reporter): 254 """Run the list of test_infos. 255 256 Should contain code for kicking off the test runs using 257 test_runner_base.run(). Results should be processed and printed 258 via the reporter passed in. 259 260 Args: 261 test_infos: List of TestInfo. 262 extra_args: Dict of extra args to add to test run. 263 reporter: An instance of result_report.ResultReporter. 264 """ 265 raise NotImplementedError 266 267 def host_env_check(self): 268 """Checks that host env has met requirements.""" 269 raise NotImplementedError 270 271 def get_test_runner_build_reqs(self, test_infos: List[test_info.TestInfo]): 272 """Returns a list of build targets required by the test runner.""" 273 raise NotImplementedError 274 275 def generate_run_commands(self, test_infos, extra_args, port=None): 276 """Generate a list of run commands from TestInfos. 277 278 Args: 279 test_infos: A set of TestInfo instances. 280 extra_args: A Dict of extra args to append. 281 port: Optional. An int of the port number to send events to. Subprocess 282 reporter in TF won't try to connect if it's None. 283 284 Returns: 285 A list of run commands to run the tests. 286 """ 287 raise NotImplementedError 288 289 290def gather_build_targets(test_infos: List[test_info.TestInfo]) -> Set[str]: 291 """Gets all build targets for the given tests. 292 293 Args: 294 test_infos: List of TestInfo. 295 296 Returns: 297 Set of build targets. 298 """ 299 build_targets = set() 300 301 for t_info in test_infos: 302 build_targets |= t_info.build_targets 303 304 return build_targets 305