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