# Copyright 2017, The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Base test runner class. Class that other test runners will instantiate for test runners. """ from __future__ import print_function import errno import logging import signal import subprocess import tempfile import os import sys from collections import namedtuple # pylint: disable=import-error import atest_error import atest_utils import constants OLD_OUTPUT_ENV_VAR = 'ATEST_OLD_OUTPUT' # TestResult contains information of individual tests during a test run. TestResult = namedtuple('TestResult', ['runner_name', 'group_name', 'test_name', 'status', 'details', 'test_count', 'test_time', 'runner_total', 'group_total', 'additional_info', 'test_run_name']) ASSUMPTION_FAILED = 'ASSUMPTION_FAILED' FAILED_STATUS = 'FAILED' PASSED_STATUS = 'PASSED' IGNORED_STATUS = 'IGNORED' ERROR_STATUS = 'ERROR' class TestRunnerBase(object): """Base Test Runner class.""" NAME = '' EXECUTABLE = '' def __init__(self, results_dir, **kwargs): """Init stuff for base class.""" self.results_dir = results_dir self.test_log_file = None if not self.NAME: raise atest_error.NoTestRunnerName('Class var NAME is not defined.') if not self.EXECUTABLE: raise atest_error.NoTestRunnerExecutable('Class var EXECUTABLE is ' 'not defined.') if kwargs: logging.debug('ignoring the following args: %s', kwargs) def run(self, cmd, output_to_stdout=False, env_vars=None): """Shell out and execute command. Args: cmd: A string of the command to execute. output_to_stdout: A boolean. If False, the raw output of the run command will not be seen in the terminal. This is the default behavior, since the test_runner's run_tests() method should use atest's result reporter to print the test results. Set to True to see the output of the cmd. This would be appropriate for verbose runs. env_vars: Environment variables passed to the subprocess. """ if not output_to_stdout: self.test_log_file = tempfile.NamedTemporaryFile(mode='w', dir=self.results_dir, delete=True) logging.debug('Executing command: %s', cmd) return subprocess.Popen(cmd, preexec_fn=os.setsid, shell=True, stderr=subprocess.STDOUT, stdout=self.test_log_file, env=env_vars) # pylint: disable=broad-except def handle_subprocess(self, subproc, func): """Execute the function. Interrupt the subproc when exception occurs. Args: subproc: A subprocess to be terminated. func: A function to be run. """ try: signal.signal(signal.SIGINT, self._signal_passer(subproc)) func() except Exception as error: # exc_info=1 tells logging to log the stacktrace logging.debug('Caught exception:', exc_info=1) # Remember our current exception scope, before new try block # Python3 will make this easier, the error itself stores # the scope via error.__traceback__ and it provides a # "raise from error" pattern. # https://docs.python.org/3.5/reference/simple_stmts.html#raise exc_type, exc_msg, traceback_obj = sys.exc_info() # If atest crashes, try to kill subproc group as well. try: logging.debug('Killing subproc: %s', subproc.pid) os.killpg(os.getpgid(subproc.pid), signal.SIGINT) except OSError: # this wipes our previous stack context, which is why # we have to save it above. logging.debug('Subproc already terminated, skipping') finally: if self.test_log_file: with open(self.test_log_file.name, 'r') as f: intro_msg = "Unexpected Issue. Raw Output:" print(atest_utils.colorize(intro_msg, constants.RED)) print(f.read()) # Ignore socket.recv() raising due to ctrl-c if not error.args or error.args[0] != errno.EINTR: raise exc_type, exc_msg, traceback_obj def wait_for_subprocess(self, proc): """Check the process status. Interrupt the TF subporcess if user hits Ctrl-C. Args: proc: The tradefed subprocess. Returns: Return code of the subprocess for running tests. """ try: logging.debug('Runner Name: %s, Process ID: %s', self.NAME, proc.pid) signal.signal(signal.SIGINT, self._signal_passer(proc)) proc.wait() return proc.returncode except: # If atest crashes, kill TF subproc group as well. os.killpg(os.getpgid(proc.pid), signal.SIGINT) raise def _signal_passer(self, proc): """Return the signal_handler func bound to proc. Args: proc: The tradefed subprocess. Returns: signal_handler function. """ def signal_handler(_signal_number, _frame): """Pass SIGINT to proc. If user hits ctrl-c during atest run, the TradeFed subprocess won't stop unless we also send it a SIGINT. The TradeFed process is started in a process group, so this SIGINT is sufficient to kill all the child processes TradeFed spawns as well. """ logging.info('Ctrl-C received. Killing subprocess group') os.killpg(os.getpgid(proc.pid), signal.SIGINT) return signal_handler def run_tests(self, test_infos, extra_args, reporter): """Run the list of test_infos. Should contain code for kicking off the test runs using test_runner_base.run(). Results should be processed and printed via the reporter passed in. Args: test_infos: List of TestInfo. extra_args: Dict of extra args to add to test run. reporter: An instance of result_report.ResultReporter. """ raise NotImplementedError def host_env_check(self): """Checks that host env has met requirements.""" raise NotImplementedError def get_test_runner_build_reqs(self): """Returns a list of build targets required by the test runner.""" raise NotImplementedError def generate_run_commands(self, test_infos, extra_args, port=None): """Generate a list of run commands from TestInfos. Args: test_infos: A set of TestInfo instances. extra_args: A Dict of extra args to append. port: Optional. An int of the port number to send events to. Subprocess reporter in TF won't try to connect if it's None. Returns: A list of run commands to run the tests. """ raise NotImplementedError