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