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 if not self.NAME: 83 raise atest_error.NoTestRunnerName('Class var NAME is not defined.') 84 if not self.EXECUTABLE: 85 raise atest_error.NoTestRunnerExecutable( 86 'Class var EXECUTABLE is not defined.' 87 ) 88 if kwargs: 89 for key, value in kwargs.items(): 90 if not 'test_infos' in key: 91 logging.debug('Found auxiliary args: %s=%s', key, value) 92 93 def create_invocations( 94 self, 95 extra_args: Dict[str, Any], 96 test_infos: List[test_info.TestInfo], 97 ) -> List[TestRunnerInvocation]: 98 """Creates test runner invocations. 99 100 Args: 101 extra_args: A dict of arguments. 102 test_infos: A list of instances of TestInfo. 103 104 Returns: 105 A list of TestRunnerInvocation instances. 106 """ 107 return [ 108 TestRunnerInvocation( 109 test_runner=self, extra_args=extra_args, test_infos=test_infos 110 ) 111 ] 112 113 def requires_device_update( 114 self, test_infos: List[test_info.TestInfo] 115 ) -> bool: 116 """Checks whether this runner requires device update.""" 117 return False 118 119 def run(self, cmd, output_to_stdout=False, env_vars=None): 120 """Shell out and execute command. 121 122 Args: 123 cmd: A string of the command to execute. 124 output_to_stdout: A boolean. If False, the raw output of the run command 125 will not be seen in the terminal. This is the default behavior, since 126 the test_runner's run_tests() method should use atest's result 127 reporter to print the test results. Set to True to see the output of 128 the cmd. This would be appropriate for verbose runs. 129 env_vars: Environment variables passed to the subprocess. 130 """ 131 if not output_to_stdout: 132 self.test_log_file = tempfile.NamedTemporaryFile( 133 mode='w', dir=self.results_dir, delete=True 134 ) 135 logging.debug('Executing command: %s', cmd) 136 return subprocess.Popen( 137 cmd, 138 start_new_session=True, 139 shell=True, 140 stderr=subprocess.STDOUT, 141 stdout=self.test_log_file, 142 env=env_vars, 143 ) 144 145 # pylint: disable=broad-except 146 def handle_subprocess(self, subproc, func): 147 """Execute the function. Interrupt the subproc when exception occurs. 148 149 Args: 150 subproc: A subprocess to be terminated. 151 func: A function to be run. 152 """ 153 try: 154 signal.signal(signal.SIGINT, self._signal_passer(subproc)) 155 func() 156 except Exception as error: 157 # exc_info=1 tells logging to log the stacktrace 158 logging.debug('Caught exception:', exc_info=1) 159 # If atest crashes, try to kill subproc group as well. 160 try: 161 logging.debug('Killing subproc: %s', subproc.pid) 162 os.killpg(os.getpgid(subproc.pid), signal.SIGINT) 163 except OSError: 164 # this wipes our previous stack context, which is why 165 # we have to save it above. 166 logging.debug('Subproc already terminated, skipping') 167 finally: 168 if self.test_log_file: 169 with open(self.test_log_file.name, 'r') as f: 170 intro_msg = 'Unexpected Issue. Raw Output:' 171 print(atest_utils.mark_red(intro_msg)) 172 print(f.read()) 173 # Ignore socket.recv() raising due to ctrl-c 174 if not error.args or error.args[0] != errno.EINTR: 175 raise error 176 177 def wait_for_subprocess(self, proc): 178 """Check the process status. 179 180 Interrupt the TF subprocess if user hits Ctrl-C. 181 182 Args: 183 proc: The tradefed subprocess. 184 185 Returns: 186 Return code of the subprocess for running tests. 187 """ 188 try: 189 logging.debug('Runner Name: %s, Process ID: %s', self.NAME, proc.pid) 190 signal.signal(signal.SIGINT, self._signal_passer(proc)) 191 proc.wait() 192 return proc.returncode 193 except: 194 # If atest crashes, kill TF subproc group as well. 195 os.killpg(os.getpgid(proc.pid), signal.SIGINT) 196 raise 197 198 def _signal_passer(self, proc): 199 """Return the signal_handler func bound to proc. 200 201 Args: 202 proc: The tradefed subprocess. 203 204 Returns: 205 signal_handler function. 206 """ 207 208 def signal_handler(_signal_number, _frame): 209 """Pass SIGINT to proc. 210 211 If user hits ctrl-c during atest run, the TradeFed subprocess 212 won't stop unless we also send it a SIGINT. The TradeFed process 213 is started in a process group, so this SIGINT is sufficient to 214 kill all the child processes TradeFed spawns as well. 215 """ 216 print('Process ID: %s', proc.pid) 217 try: 218 atest_utils.print_and_log_info( 219 'Ctrl-C received. Killing process group ID: %s', 220 os.getpgid(proc.pid), 221 ) 222 os.killpg(os.getpgid(proc.pid), signal.SIGINT) 223 except ProcessLookupError as e: 224 atest_utils.print_and_log_info(e) 225 226 return signal_handler 227 228 def run_tests(self, test_infos, extra_args, reporter): 229 """Run the list of test_infos. 230 231 Should contain code for kicking off the test runs using 232 test_runner_base.run(). Results should be processed and printed 233 via the reporter passed in. 234 235 Args: 236 test_infos: List of TestInfo. 237 extra_args: Dict of extra args to add to test run. 238 reporter: An instance of result_report.ResultReporter. 239 """ 240 raise NotImplementedError 241 242 def host_env_check(self): 243 """Checks that host env has met requirements.""" 244 raise NotImplementedError 245 246 def get_test_runner_build_reqs(self, test_infos: List[test_info.TestInfo]): 247 """Returns a list of build targets required by the test runner.""" 248 raise NotImplementedError 249 250 def generate_run_commands(self, test_infos, extra_args, port=None): 251 """Generate a list of run commands from TestInfos. 252 253 Args: 254 test_infos: A set of TestInfo instances. 255 extra_args: A Dict of extra args to append. 256 port: Optional. An int of the port number to send events to. Subprocess 257 reporter in TF won't try to connect if it's None. 258 259 Returns: 260 A list of run commands to run the tests. 261 """ 262 raise NotImplementedError 263 264 265def gather_build_targets(test_infos: List[test_info.TestInfo]) -> Set[str]: 266 """Gets all build targets for the given tests. 267 268 Args: 269 test_infos: List of TestInfo. 270 271 Returns: 272 Set of build targets. 273 """ 274 build_targets = set() 275 276 for t_info in test_infos: 277 build_targets |= t_info.build_targets 278 279 return build_targets 280