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 22import errno 23import logging 24import signal 25import subprocess 26import tempfile 27import os 28import sys 29 30from collections import namedtuple 31 32# pylint: disable=import-error 33import atest_error 34import atest_utils 35import constants 36 37OLD_OUTPUT_ENV_VAR = 'ATEST_OLD_OUTPUT' 38 39# TestResult contains information of individual tests during a test run. 40TestResult = namedtuple('TestResult', ['runner_name', 'group_name', 41 'test_name', 'status', 'details', 42 'test_count', 'test_time', 43 'runner_total', 'group_total', 44 'additional_info', 'test_run_name']) 45ASSUMPTION_FAILED = 'ASSUMPTION_FAILED' 46FAILED_STATUS = 'FAILED' 47PASSED_STATUS = 'PASSED' 48IGNORED_STATUS = 'IGNORED' 49ERROR_STATUS = 'ERROR' 50 51class TestRunnerBase(object): 52 """Base Test Runner class.""" 53 NAME = '' 54 EXECUTABLE = '' 55 56 def __init__(self, results_dir, **kwargs): 57 """Init stuff for base class.""" 58 self.results_dir = results_dir 59 self.test_log_file = None 60 if not self.NAME: 61 raise atest_error.NoTestRunnerName('Class var NAME is not defined.') 62 if not self.EXECUTABLE: 63 raise atest_error.NoTestRunnerExecutable('Class var EXECUTABLE is ' 64 'not defined.') 65 if kwargs: 66 logging.debug('ignoring the following args: %s', kwargs) 67 68 def run(self, cmd, output_to_stdout=False, env_vars=None): 69 """Shell out and execute command. 70 71 Args: 72 cmd: A string of the command to execute. 73 output_to_stdout: A boolean. If False, the raw output of the run 74 command will not be seen in the terminal. This 75 is the default behavior, since the test_runner's 76 run_tests() method should use atest's 77 result reporter to print the test results. 78 79 Set to True to see the output of the cmd. This 80 would be appropriate for verbose runs. 81 env_vars: Environment variables passed to the subprocess. 82 """ 83 if not output_to_stdout: 84 self.test_log_file = tempfile.NamedTemporaryFile(mode='w', 85 dir=self.results_dir, 86 delete=True) 87 logging.debug('Executing command: %s', cmd) 88 return subprocess.Popen(cmd, preexec_fn=os.setsid, shell=True, 89 stderr=subprocess.STDOUT, stdout=self.test_log_file, 90 env=env_vars) 91 92 # pylint: disable=broad-except 93 def handle_subprocess(self, subproc, func): 94 """Execute the function. Interrupt the subproc when exception occurs. 95 96 Args: 97 subproc: A subprocess to be terminated. 98 func: A function to be run. 99 """ 100 try: 101 signal.signal(signal.SIGINT, self._signal_passer(subproc)) 102 func() 103 except Exception as error: 104 # exc_info=1 tells logging to log the stacktrace 105 logging.debug('Caught exception:', exc_info=1) 106 # Remember our current exception scope, before new try block 107 # Python3 will make this easier, the error itself stores 108 # the scope via error.__traceback__ and it provides a 109 # "raise from error" pattern. 110 # https://docs.python.org/3.5/reference/simple_stmts.html#raise 111 exc_type, exc_msg, traceback_obj = sys.exc_info() 112 # If atest crashes, try to kill subproc group as well. 113 try: 114 logging.debug('Killing subproc: %s', subproc.pid) 115 os.killpg(os.getpgid(subproc.pid), signal.SIGINT) 116 except OSError: 117 # this wipes our previous stack context, which is why 118 # we have to save it above. 119 logging.debug('Subproc already terminated, skipping') 120 finally: 121 if self.test_log_file: 122 with open(self.test_log_file.name, 'r') as f: 123 intro_msg = "Unexpected Issue. Raw Output:" 124 print(atest_utils.colorize(intro_msg, constants.RED)) 125 print(f.read()) 126 # Ignore socket.recv() raising due to ctrl-c 127 if not error.args or error.args[0] != errno.EINTR: 128 raise exc_type, exc_msg, traceback_obj 129 130 def wait_for_subprocess(self, proc): 131 """Check the process status. Interrupt the TF subporcess if user 132 hits Ctrl-C. 133 134 Args: 135 proc: The tradefed subprocess. 136 137 Returns: 138 Return code of the subprocess for running tests. 139 """ 140 try: 141 logging.debug('Runner Name: %s, Process ID: %s', self.NAME, proc.pid) 142 signal.signal(signal.SIGINT, self._signal_passer(proc)) 143 proc.wait() 144 return proc.returncode 145 except: 146 # If atest crashes, kill TF subproc group as well. 147 os.killpg(os.getpgid(proc.pid), signal.SIGINT) 148 raise 149 150 def _signal_passer(self, proc): 151 """Return the signal_handler func bound to proc. 152 153 Args: 154 proc: The tradefed subprocess. 155 156 Returns: 157 signal_handler function. 158 """ 159 def signal_handler(_signal_number, _frame): 160 """Pass SIGINT to proc. 161 162 If user hits ctrl-c during atest run, the TradeFed subprocess 163 won't stop unless we also send it a SIGINT. The TradeFed process 164 is started in a process group, so this SIGINT is sufficient to 165 kill all the child processes TradeFed spawns as well. 166 """ 167 logging.info('Ctrl-C received. Killing subprocess group') 168 os.killpg(os.getpgid(proc.pid), signal.SIGINT) 169 return signal_handler 170 171 def run_tests(self, test_infos, extra_args, reporter): 172 """Run the list of test_infos. 173 174 Should contain code for kicking off the test runs using 175 test_runner_base.run(). Results should be processed and printed 176 via the reporter passed in. 177 178 Args: 179 test_infos: List of TestInfo. 180 extra_args: Dict of extra args to add to test run. 181 reporter: An instance of result_report.ResultReporter. 182 """ 183 raise NotImplementedError 184 185 def host_env_check(self): 186 """Checks that host env has met requirements.""" 187 raise NotImplementedError 188 189 def get_test_runner_build_reqs(self): 190 """Returns a list of build targets required by the test runner.""" 191 raise NotImplementedError 192 193 def generate_run_commands(self, test_infos, extra_args, port=None): 194 """Generate a list of run commands from TestInfos. 195 196 Args: 197 test_infos: A set of TestInfo instances. 198 extra_args: A Dict of extra args to append. 199 port: Optional. An int of the port number to send events to. 200 Subprocess reporter in TF won't try to connect if it's None. 201 202 Returns: 203 A list of run commands to run the tests. 204 """ 205 raise NotImplementedError 206