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