• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/python2
2# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6from __future__ import absolute_import
7from __future__ import division
8from __future__ import print_function
9
10import argparse
11import os
12import signal
13import subprocess
14import sys
15
16import logging
17# Turn the logging level to INFO before importing other autotest
18# code, to avoid having failed import logging messages confuse the
19# test_that user.
20logging.basicConfig(level=logging.INFO)
21
22import common
23from autotest_lib.client.common_lib import error, logging_manager
24from autotest_lib.server import server_logging_config
25from autotest_lib.server.cros.dynamic_suite import constants
26from autotest_lib.server.hosts import factory
27from autotest_lib.site_utils import test_runner_utils
28
29
30_QUICKMERGE_SCRIPTNAME = '/mnt/host/source/chromite/bin/autotest_quickmerge'
31
32
33def _get_board_from_host(remote):
34    """Get the board of the remote host.
35
36    @param remote: string representing the IP of the remote host.
37
38    @return: A string representing the board of the remote host.
39    """
40    logging.info('Board unspecified, attempting to determine board from host.')
41    host = factory.create_host(remote)
42    try:
43        board = host.get_board().replace(constants.BOARD_PREFIX, '')
44    except error.AutoservRunError:
45        raise test_runner_utils.TestThatRunError(
46                'Cannot determine board, please specify a --board option.')
47    logging.info('Detected host board: %s', board)
48    return board
49
50
51def _get_model_from_host(remote):
52    """Get the model of the remote host.
53
54    @param remote: string representing the IP of the remote host.
55
56    @return: A string representing the board of the remote host.
57   """
58    logging.info('Model unspecified, attempting to determine model from host.')
59    host = factory.create_host(remote)
60    try:
61        model = host.get_platform()
62    except error.AutoservRunError:
63        raise test_runner_utils.TestThatRunError(
64                'Cannot determine model, please specify a --model option.')
65    logging.info('Detected host model: %s', model)
66    return model
67
68
69def validate_arguments(arguments):
70    """
71    Validates parsed arguments.
72
73    @param arguments: arguments object, as parsed by ParseArguments
74    @raises: ValueError if arguments were invalid.
75    """
76    if arguments.remote == ':lab:':
77        if arguments.args:
78            raise ValueError('--args flag not supported when running against '
79                             ':lab:')
80        if arguments.pretend:
81            raise ValueError('--pretend flag not supported when running '
82                             'against :lab:')
83        if arguments.ssh_verbosity:
84            raise ValueError('--ssh_verbosity flag not supported when running '
85                             'against :lab:')
86        if not arguments.board or arguments.build == test_runner_utils.NO_BUILD:
87            raise ValueError('--board and --build are both required when '
88                             'running against :lab:')
89    else:
90        if arguments.web:
91            raise ValueError('--web flag not supported when running locally')
92
93
94def parse_arguments(argv):
95    """
96    Parse command line arguments
97
98    @param argv: argument list to parse
99    @returns:    parsed arguments
100    @raises SystemExit if arguments are malformed, or required arguments
101            are not present.
102    """
103    return _parse_arguments_internal(argv)[0]
104
105
106def _parse_arguments_internal(argv):
107    """
108    Parse command line arguments
109
110    @param argv: argument list to parse
111    @returns:    tuple of parsed arguments and argv suitable for remote runs
112    @raises SystemExit if arguments are malformed, or required arguments
113            are not present.
114    """
115    local_parser, remote_argv = parse_local_arguments(argv)
116
117    parser = argparse.ArgumentParser(description='Run remote tests.',
118                                     parents=[local_parser])
119
120    parser.add_argument('remote', metavar='REMOTE',
121                        help='hostname[:port] for remote device. Specify '
122                             ':lab: to run in test lab. When tests are run in '
123                             'the lab, test_that will use the client autotest '
124                             'package for the build specified with --build, '
125                             'and the lab server code rather than local '
126                             'changes.')
127    test_runner_utils.add_common_args(parser)
128    parser.add_argument('-b', '--board', metavar='BOARD',
129                        action='store',
130                        help='Board for which the test will run. '
131                             'Default: %(default)s')
132    parser.add_argument('-m',
133                        '--model',
134                        metavar='MODEL',
135                        help='Specific model the test will run against. '
136                        'Matches the model:FAKE_MODEL label for the host.')
137    parser.add_argument('-i', '--build', metavar='BUILD',
138                        default=test_runner_utils.NO_BUILD,
139                        help='Build to test. Device will be reimaged if '
140                             'necessary. Omit flag to skip reimage and test '
141                             'against already installed DUT image. Examples: '
142                             'link-paladin/R34-5222.0.0-rc2, '
143                             'lumpy-release/R34-5205.0.0')
144    parser.add_argument('-p', '--pool', metavar='POOL', default='suites',
145                        help='Pool to use when running tests in the lab. '
146                             'Default is "suites"')
147    parser.add_argument('--autotest_dir', metavar='AUTOTEST_DIR',
148                        help='Use AUTOTEST_DIR instead of normal board sysroot '
149                             'copy of autotest, and skip the quickmerge step.')
150    parser.add_argument('--no-quickmerge', action='store_true', default=False,
151                        dest='no_quickmerge',
152                        help='Skip the quickmerge step and use the sysroot '
153                             'as it currently is. May result in un-merged '
154                             'source tree changes not being reflected in the '
155                             'run. If using --autotest_dir, this flag is '
156                             'automatically applied.')
157    parser.add_argument('--allow-chrome-crashes',
158                        action='store_true',
159                        default=False,
160                        dest='allow_chrome_crashes',
161                        help='Ignore chrome crashes when producing test '
162                        'report. This flag gets passed along to the '
163                        'report generation tool.')
164    parser.add_argument('--ssh_private_key', action='store',
165                        default=test_runner_utils.TEST_KEY_PATH,
166                        help='Path to the private ssh key.')
167    return parser.parse_args(argv), remote_argv
168
169
170def parse_local_arguments(argv):
171    """
172    Strips out arguments that are not to be passed through to runs.
173
174    Add any arguments that should not be passed to remote test_that runs here.
175
176    @param argv: argument list to parse.
177    @returns: tuple of local argument parser and remaining argv.
178    """
179    parser = argparse.ArgumentParser(add_help=False)
180    parser.add_argument('-w', '--web', dest='web', default=None,
181                        help='Address of a webserver to receive test requests.')
182    parser.add_argument('-x', '--max_runtime_mins', type=int,
183                        dest='max_runtime_mins', default=20,
184                        help='Default time allowed for the tests to complete.')
185    parser.add_argument('--no-retries', '--no-retry',
186                        dest='retry', action='store_false', default=True,
187                        help='For local runs only, ignore any retries '
188                             'specified in the control files.')
189    _, remaining_argv = parser.parse_known_args(argv)
190    return parser, remaining_argv
191
192
193def perform_bootstrap_into_autotest_root(arguments, autotest_path, argv):
194    """
195    Perfoms a bootstrap to run test_that from the |autotest_path|.
196
197    This function is to be called from test_that's main() script, when
198    test_that is executed from the source tree location. It runs
199    autotest_quickmerge to update the sysroot unless arguments.no_quickmerge
200    is set. It then executes and waits on the version of test_that.py
201    in |autotest_path|.
202
203    @param arguments: A parsed arguments object, as returned from
204                      test_that.parse_arguments(...).
205    @param autotest_path: Full absolute path to the autotest root directory.
206    @param argv: The arguments list, as passed to main(...)
207
208    @returns: The return code of the test_that script that was executed in
209              |autotest_path|.
210    """
211    logging_manager.configure_logging(
212            server_logging_config.ServerLoggingConfig(),
213            use_console=True,
214            verbose=arguments.debug)
215    if arguments.no_quickmerge:
216        logging.info('Skipping quickmerge step.')
217    else:
218        logging.info('Running autotest_quickmerge step.')
219        command = [_QUICKMERGE_SCRIPTNAME, '--board='+arguments.board]
220        s = subprocess.Popen(command,
221                             stdout=subprocess.PIPE,
222                             stderr=subprocess.STDOUT)
223        for message in iter(s.stdout.readline, b''):
224            logging.info('quickmerge| %s', message.strip())
225        return_code = s.wait()
226        if return_code:
227            raise test_runner_utils.TestThatRunError(
228                    'autotest_quickmerge failed with error code %s.' %
229                    return_code)
230
231    logging.info('Re-running test_that script in %s copy of autotest.',
232                 autotest_path)
233    script_command = os.path.join(autotest_path, 'site_utils',
234                                  'test_that.py')
235    if not os.path.exists(script_command):
236        raise test_runner_utils.TestThatRunError(
237            'Unable to bootstrap to autotest root, %s not found.' %
238            script_command)
239    proc = None
240    def resend_sig(signum, stack_frame):
241        #pylint: disable-msg=C0111
242        if proc:
243            proc.send_signal(signum)
244    signal.signal(signal.SIGINT, resend_sig)
245    signal.signal(signal.SIGTERM, resend_sig)
246
247    proc = subprocess.Popen([script_command] + argv)
248
249    return proc.wait()
250
251
252def _main_for_local_run(argv, arguments):
253    """
254    Effective entry point for local test_that runs.
255
256    @param argv: Script command line arguments.
257    @param arguments: Parsed command line arguments.
258    """
259    if not os.path.exists('/etc/cros_chroot_version'):
260        print('For local runs, script must be run inside chroot.', file=sys.stderr)
261        return 1
262
263    results_directory = test_runner_utils.create_results_directory(
264            arguments.results_dir, arguments.board)
265    test_runner_utils.add_ssh_identity(results_directory,
266                                       arguments.ssh_private_key)
267    arguments.results_dir = results_directory
268
269    # If the board and/or model is not specified through --board and/or
270    # --model, and is not set in the default_board file, determine the board by
271    # ssh-ing into the host. Also prepend it to argv so we can re-use it when we
272    # run test_that from the sysroot.
273    if arguments.board is None:
274        arguments.board = _get_board_from_host(arguments.remote)
275        argv = ['--board=%s' % (arguments.board,)] + argv
276
277    if arguments.model is None:
278        arguments.model = _get_model_from_host(arguments.remote)
279        argv = ['--model=%s' % (arguments.model, )] + argv
280
281    if arguments.autotest_dir:
282        autotest_path = arguments.autotest_dir
283        arguments.no_quickmerge = True
284    else:
285        sysroot_path = os.path.join('/build', arguments.board, '')
286
287        if not os.path.exists(sysroot_path):
288            print(('%s does not exist. Have you run '
289                   'setup_board?' % sysroot_path), file=sys.stderr)
290            return 1
291
292        path_ending = 'usr/local/build/autotest'
293        autotest_path = os.path.join(sysroot_path, path_ending)
294
295    site_utils_path = os.path.join(autotest_path, 'site_utils')
296
297    if not os.path.exists(autotest_path):
298        print(('%s does not exist. Have you run '
299               'build_packages? Or if you are using '
300               '--autotest_dir, make sure it points to '
301               'a valid autotest directory.' % autotest_path), file=sys.stderr)
302        return 1
303
304    realpath = os.path.realpath(__file__)
305    site_utils_path = os.path.realpath(site_utils_path)
306
307    # If we are not running the sysroot version of script, perform
308    # a quickmerge if necessary and then re-execute
309    # the sysroot version of script with the same arguments.
310    if os.path.dirname(realpath) != site_utils_path:
311        return perform_bootstrap_into_autotest_root(
312                arguments, autotest_path, argv)
313    else:
314        return test_runner_utils.perform_run_from_autotest_root(
315                autotest_path,
316                argv,
317                arguments.tests,
318                arguments.remote,
319                build=arguments.build,
320                board=arguments.board,
321                model=arguments.model,
322                args=arguments.args,
323                ignore_deps=not arguments.enforce_deps,
324                results_directory=results_directory,
325                ssh_verbosity=arguments.ssh_verbosity,
326                ssh_options=arguments.ssh_options,
327                iterations=arguments.iterations,
328                fast_mode=arguments.fast_mode,
329                debug=arguments.debug,
330                allow_chrome_crashes=arguments.allow_chrome_crashes,
331                pretend=arguments.pretend,
332                job_retry=arguments.retry)
333
334
335def _main_for_lab_run(argv, arguments):
336    """
337    Effective entry point for lab test_that runs.
338
339    @param argv: Script command line arguments.
340    @param arguments: Parsed command line arguments.
341    """
342    autotest_path = os.path.realpath(os.path.join(
343            os.path.dirname(os.path.realpath(__file__)),
344            '..',
345    ))
346    command = [os.path.join(autotest_path, 'site_utils',
347                            'run_suite.py'),
348               '--board=%s' % (arguments.board,),
349               '--build=%s' % (arguments.build,),
350               '--model=%s' % (arguments.model,),
351               '--suite_name=%s' % 'test_that_wrapper',
352               '--pool=%s' % (arguments.pool,),
353               '--max_runtime_mins=%s' % str(arguments.max_runtime_mins),
354               '--suite_args=%s'
355               % repr({'tests': _suite_arg_tests(argv)})]
356    if arguments.web:
357        command.extend(['--web=%s' % (arguments.web,)])
358    logging.info('About to start lab suite with command %s.', command)
359    return subprocess.call(command)
360
361
362def _suite_arg_tests(argv):
363    """
364    Construct a list of tests to pass into suite_args.
365
366    This is passed in suite_args to run_suite for running a test in the
367    lab.
368
369    @param argv: Remote Script command line arguments.
370    """
371    arguments = parse_arguments(argv)
372    return arguments.tests
373
374
375def main(argv):
376    """
377    Entry point for test_that script.
378
379    @param argv: arguments list
380    """
381    arguments, remote_argv = _parse_arguments_internal(argv)
382    try:
383        validate_arguments(arguments)
384    except ValueError as err:
385        print(('Invalid arguments. %s' % str(err)), file=sys.stderr)
386        return 1
387
388    if arguments.remote == ':lab:':
389        return _main_for_lab_run(remote_argv, arguments)
390    else:
391        return _main_for_local_run(argv, arguments)
392
393
394if __name__ == '__main__':
395    sys.exit(main(sys.argv[1:]))
396