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