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