• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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                                       'perf_info'])
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