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