blob: 22994e3b3f60e927878653920b4fbac861cf3470 [file] [log] [blame]
# 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