blob: 1f46cd43e62630235f5f7b672eefcb8ca9402a3e [file] [log] [blame]
#!/usr/bin/python
# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import argparse
import logging
import os
import re
import signal
import stat
import subprocess
import sys
import tempfile
import threading
import common
from autotest_lib.client.common_lib.cros import dev_server, retry
from autotest_lib.server.cros.dynamic_suite import suite
from autotest_lib.server.cros.dynamic_suite import constants
from autotest_lib.server import autoserv_utils
try:
from chromite.lib import cros_build_lib
except ImportError:
print 'Unable to import chromite.'
print 'This script must be either:'
print ' - Be run in the chroot.'
print ' - (not yet supported) be run after running '
print ' ../utils/build_externals.py'
_autoserv_proc = None
_sigint_handler_lock = threading.Lock()
_AUTOSERV_SIGINT_TIMEOUT_SECONDS = 5
_NO_BOARD = 'ad_hoc_board'
_NO_BUILD = 'ad_hoc_build'
_QUICKMERGE_SCRIPTNAME = '/mnt/host/source/chromite/bin/autotest_quickmerge'
_TEST_REPORT_SCRIPTNAME = '/usr/bin/generate_test_report'
_LATEST_RESULTS_DIRECTORY = '/tmp/test_that_latest'
def schedule_local_suite(autotest_path, suite_name, afe, build=_NO_BUILD,
board=_NO_BOARD, results_directory=None):
"""
Schedule a suite against a mock afe object, for a local suite run.
@param autotest_path: Absolute path to autotest (in sysroot).
@param suite_name: Name of suite to schedule.
@param afe: afe object to schedule against (typically a directAFE)
@param build: Build to schedule suite for.
@param board: Board to schedule suite for.
@param results_directory: Absolute path of directory to store results in.
(results will be stored in subdirectory of this).
@returns: The number of tests scheduled.
"""
fs_getter = suite.Suite.create_fs_getter(autotest_path)
devserver = dev_server.ImageServer('')
my_suite = suite.Suite.create_from_name(suite_name, build, board,
devserver, fs_getter, afe=afe, ignore_deps=True,
results_dir=results_directory)
if len(my_suite.tests) == 0:
raise ValueError('Suite named %s does not exist, or contains no '
'tests.' % suite_name)
my_suite.schedule(lambda x: None) # Schedule tests, discard record calls.
return len(my_suite.tests)
def schedule_local_test(autotest_path, test_name, afe, build=_NO_BUILD,
board=_NO_BOARD, results_directory=None):
#temporarily disabling pylint
#pylint: disable-msg=C0111
"""
Schedule an individual test against a mock afe object, for a local run.
@param autotest_path: Absolute path to autotest (in sysroot).
@param test_name: Name of test to schedule.
@param afe: afe object to schedule against (typically a directAFE)
@param build: Build to schedule suite for.
@param board: Board to schedule suite for.
@param results_directory: Absolute path of directory to store results in.
(results will be stored in subdirectory of this).
@returns: The number of tests scheduled (may be >1 if there are
multiple tests with the same name).
"""
fs_getter = suite.Suite.create_fs_getter(autotest_path)
devserver = dev_server.ImageServer('')
predicates = [suite.Suite.test_name_equals_predicate(test_name)]
suite_name = 'suite_' + test_name
my_suite = suite.Suite.create_from_predicates(predicates, build, board,
devserver, fs_getter, afe=afe, name=suite_name, ignore_deps=True,
results_dir=results_directory)
if len(my_suite.tests) == 0:
raise ValueError('No tests named %s.' % test_name)
my_suite.schedule(lambda x: None) # Schedule tests, discard record calls.
return len(my_suite.tests)
def run_job(job, host, sysroot_autotest_path, results_directory, fast_mode,
id_digits=1, args=None, pretend=False):
"""
Shell out to autoserv to run an individual test job.
@param job: A Job object containing the control file contents and other
relevent metadata for this test.
@param host: Hostname of DUT to run test against.
@param sysroot_autotest_path: Absolute path of autotest directory.
@param results_directory: Absolute path of directory to store results in.
(results will be stored in subdirectory of this).
@param fast_mode: bool to use fast mode (disables slow autotest features).
@param id_digits: The minimum number of digits that job ids should be
0-padded to when formatting as a string for results
directory.
@param args: String that should be passed as args parameter to autoserv,
and then ultimitely to test itself.
@param pretend: If True, will print out autoserv commands rather than
running them.
@returns: Absolute path of directory where results were stored.
"""
with tempfile.NamedTemporaryFile() as temp_file:
temp_file.write(job.control_file)
temp_file.flush()
results_directory = os.path.join(results_directory,
'results-%0*d' % (id_digits, job.id))
extra_args = [temp_file.name]
if args:
extra_args.extend(['--args', args])
command = autoserv_utils.autoserv_run_job_command(
os.path.join(sysroot_autotest_path, 'server'),
machines=host, job=job, verbose=False,
results_directory=results_directory,
fast_mode=fast_mode,
extra_args=extra_args)
if not pretend:
global _autoserv_proc
_autoserv_proc = subprocess.Popen(command)
_autoserv_proc.wait()
_autoserv_proc = None
return results_directory
else:
logging.info('Pretend mode. Would run autoserv command: %s',
' '.join(command))
def setup_local_afe():
"""
Setup a local afe database and return a direct_afe object to access it.
@returns: A autotest_lib.frontend.afe.direct_afe instance.
"""
# This import statement is delayed until now rather than running at
# module load time, because it kicks off a local sqlite :memory: backed
# database, and we don't need that unless we are doing a local run.
from autotest_lib.frontend import setup_django_lite_environment
from autotest_lib.frontend.afe import direct_afe
return direct_afe.directAFE()
def perform_local_run(afe, autotest_path, tests, remote, fast_mode,
build=_NO_BUILD, board=_NO_BOARD, args=None,
pretend=False):
"""
@param afe: A direct_afe object used to interact with local afe database.
@param autotest_path: Absolute path of sysroot installed autotest.
@param tests: List of strings naming tests and suites to run. Suite strings
should be formed like "suite:smoke".
@param remote: Remote hostname.
@param fast_mode: bool to use fast mode (disables slow autotest features).
@param build: String specifying build for local run.
@param board: String specifyinb board for local run.
@param args: String that should be passed as args parameter to autoserv,
and then ultimitely to test itself.
@param pretend: If True, will print out autoserv commands rather than
running them.
@returns: directory in which results are stored.
"""
afe.create_label(constants.VERSION_PREFIX + build)
afe.create_label(board)
afe.create_host(remote)
results_directory = tempfile.mkdtemp(prefix='test_that_results_')
os.chmod(results_directory, stat.S_IWOTH | stat.S_IROTH | stat.S_IXOTH)
logging.info('Running jobs. Results will be placed in %s',
results_directory)
# Schedule tests / suites in local afe
for test in tests:
suitematch = re.match(r'suite:(.*)', test)
if suitematch:
suitename = suitematch.group(1)
logging.info('Scheduling suite %s...', suitename)
ntests = schedule_local_suite(autotest_path, suitename, afe,
build=build, board=board,
results_directory=results_directory)
else:
logging.info('Scheduling test %s...', test)
ntests = schedule_local_test(autotest_path, test, afe,
build=build, board=board,
results_directory=results_directory)
logging.info('... scheduled %s tests.', ntests)
if not afe.get_jobs():
logging.info('No jobs scheduled. End of local run.')
return results_directory
last_job_id = afe.get_jobs()[-1].id
job_id_digits=len(str(last_job_id))
for job in afe.get_jobs():
run_job(job, remote, autotest_path, results_directory, fast_mode,
job_id_digits, args, pretend)
return results_directory
def validate_arguments(arguments):
"""
Validates parsed arguments.
@param arguments: arguments object, as parsed by ParseArguments
@raises: ValueError if arguments were invalid.
"""
if arguments.build:
raise ValueError('-i/--build flag not yet supported.')
if not arguments.board:
raise ValueError('Board autodetection not yet supported. '
'--board required.')
if arguments.remote == ':lab:':
raise ValueError('Running tests in test lab not yet supported.')
if arguments.args:
raise ValueError('--args flag not supported when running against '
':lab:')
if arguments.pretend:
raise ValueError('--pretend flag not supported when running '
'against :lab:')
def parse_arguments(argv):
"""
Parse command line arguments
@param argv: argument list to parse
@returns: parsed arguments.
"""
parser = argparse.ArgumentParser(description='Run remote tests.')
parser.add_argument('remote', metavar='REMOTE',
help='hostname[:port] for remote device. Specify '
':lab: to run in test lab, or :vm:PORT_NUMBER to '
'run in vm.')
parser.add_argument('tests', nargs='+', metavar='TEST',
help='Run given test(s). Use suite:SUITE to specify '
'test suite.')
parser.add_argument('-b', '--board', metavar='BOARD',
action='store',
help='Board for which the test will run.')
parser.add_argument('-i', '--build', metavar='BUILD',
help='Build to test. Device will be reimaged if '
'necessary. Omit flag to skip reimage and test '
'against already installed DUT image.')
parser.add_argument('--fast', action='store_true', dest='fast_mode',
default=False,
help='Enable fast mode. This will cause test_that to '
'skip time consuming steps like sysinfo and '
'collecting crash information.')
parser.add_argument('--args', metavar='ARGS',
help='Argument string to pass through to test. Only '
'supported for runs against a local DUT.')
parser.add_argument('--pretend', action='store_true', default=False,
help='Print autoserv commands that would be run, '
'rather than running them.')
parser.add_argument('--no-quickmerge', action='store_true', default=False,
dest='no_quickmerge',
help='Skip the quickmerge step and use the sysroot '
'as it currently is. May result in un-merged '
'source tree changes not being reflected in run.')
return parser.parse_args(argv)
def sigint_handler(signum, stack_frame):
#pylint: disable-msg=C0111
"""Handle SIGINT or SIGTERM to a local test_that run.
This handler sends a SIGINT to the running autoserv process,
if one is running, giving it up to 5 seconds to clean up and exit. After
the timeout elapses, autoserv is killed. In either case, after autoserv
exits then this process exits with status 1.
"""
# If multiple signals arrive before handler is unset, ignore duplicates
if not _sigint_handler_lock.acquire(False):
return
try:
# Ignore future signals by unsetting handler.
signal.signal(signal.SIGINT, signal.SIG_IGN)
signal.signal(signal.SIGTERM, signal.SIG_IGN)
logging.warning('Received SIGINT or SIGTERM. Cleaning up and exiting.')
if _autoserv_proc:
logging.warning('Sending SIGINT to autoserv process. Waiting up '
'to %s seconds for cleanup.',
_AUTOSERV_SIGINT_TIMEOUT_SECONDS)
_autoserv_proc.send_signal(signal.SIGINT)
timed_out, _ = retry.timeout(_autoserv_proc.wait,
timeout_sec=_AUTOSERV_SIGINT_TIMEOUT_SECONDS)
if timed_out:
_autoserv_proc.kill()
logging.warning('Timed out waiting for autoserv to handle '
'SIGINT. Killed autoserv.')
finally:
_sigint_handler_lock.release() # this is not really necessary?
sys.exit(1)
def main(argv):
"""
Entry point for test_that script.
@param argv: arguments list
"""
if not cros_build_lib.IsInsideChroot():
logging.error('Script must be invoked inside the chroot.')
return 1
logging.getLogger('').setLevel(logging.INFO)
arguments = parse_arguments(argv)
try:
validate_arguments(arguments)
except ValueError as err:
logging.error('Invalid arguments. %s', err.message)
return 1
# TODO: Determine the following string programatically.
# (same TODO applied to autotest_quickmerge)
sysroot_path = os.path.join('/build', arguments.board, '')
sysroot_autotest_path = os.path.join(sysroot_path, 'usr', 'local',
'autotest', '')
sysroot_site_utils_path = os.path.join(sysroot_autotest_path,
'site_utils')
if not os.path.exists(sysroot_path):
logging.error('%s does not exist. Have you run setup_board?',
sysroot_path)
return 1
if not os.path.exists(sysroot_autotest_path):
logging.error('%s does not exist. Have you run build_packages?',
sysroot_autotest_path)
return 1
# If we are not running the sysroot version of script, perform
# a quickmerge if necessary and then re-execute
# the sysroot version of script with the same arguments.
realpath = os.path.realpath(__file__)
if os.path.dirname(realpath) != sysroot_site_utils_path:
if arguments.no_quickmerge:
logging.info('Skipping quickmerge step as requested.')
else:
subprocess.call([_QUICKMERGE_SCRIPTNAME,
'--board='+arguments.board])
script_command = os.path.join(sysroot_site_utils_path,
os.path.basename(realpath))
proc = None
def resend_sig(signum, stack_frame):
#pylint: disable-msg=C0111
if proc:
proc.send_signal(signum)
signal.signal(signal.SIGINT, resend_sig)
signal.signal(signal.SIGTERM, resend_sig)
proc = subprocess.Popen([script_command] + argv)
return proc.wait()
# Hard coded to True temporarily. This will eventually be parsed to false
# if we are doing a run in the test lab.
local_run = True
signal.signal(signal.SIGINT, sigint_handler)
signal.signal(signal.SIGTERM, sigint_handler)
if local_run:
afe = setup_local_afe()
res_dir= perform_local_run(afe, sysroot_autotest_path, arguments.tests,
arguments.remote, arguments.fast_mode,
args=arguments.args,
pretend=arguments.pretend)
if arguments.pretend:
logging.info('Finished pretend run. Exiting.')
return 0
final_result = subprocess.call([_TEST_REPORT_SCRIPTNAME, res_dir])
logging.info('Finished running tests. Results can be found in %s',
res_dir)
try:
os.unlink(_LATEST_RESULTS_DIRECTORY)
except OSError:
pass
os.symlink(res_dir, _LATEST_RESULTS_DIRECTORY)
return final_result
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))