Home | History | Annotate | Download | only in site_utils
      1 # Copyright 2015 The Chromium OS Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 import errno
      6 import os
      7 import re
      8 import shutil
      9 import signal
     10 import stat
     11 import subprocess
     12 import sys
     13 import tempfile
     14 import threading
     15 
     16 import 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.
     20 logging.basicConfig(level=logging.INFO)
     21 
     22 import common
     23 from autotest_lib.client.common_lib.cros import dev_server, retry
     24 from autotest_lib.client.common_lib import logging_manager
     25 from autotest_lib.server.cros.dynamic_suite import suite, constants
     26 from autotest_lib.server.cros import provision
     27 from autotest_lib.server.hosts import factory
     28 from autotest_lib.server import autoserv_utils
     29 from autotest_lib.server import server_logging_config
     30 from autotest_lib.server import utils
     31 
     32 
     33 _autoserv_proc = None
     34 _sigint_handler_lock = threading.Lock()
     35 
     36 _AUTOSERV_SIGINT_TIMEOUT_SECONDS = 5
     37 NO_BOARD = 'ad_hoc_board'
     38 NO_BUILD = 'ad_hoc_build'
     39 _SUITE_REGEX = r'suite:(.*)'
     40 
     41 _TEST_KEY_FILENAME = 'testing_rsa'
     42 TEST_KEY_PATH = ('/mnt/host/source/src/scripts/mod_for_test_scripts/'
     43                   'ssh_keys/%s' % _TEST_KEY_FILENAME)
     44 
     45 _LATEST_RESULTS_DIRECTORY = '/tmp/test_that_latest'
     46 
     47 
     48 class TestThatRunError(Exception):
     49     """Raised if test_that encounters something unexpected while running."""
     50 
     51 
     52 class TestThatProvisioningError(Exception):
     53     """Raised when it fails to provision the DUT to the requested build."""
     54 
     55 
     56 def add_common_args(parser):
     57     """
     58     Add common arguments for both test_that and test_droid to their parser.
     59 
     60     @param parser: argparse.ArgumentParser object to add arguments to.
     61     """
     62     parser.add_argument('tests', nargs='+', metavar='TEST',
     63                         help='Run given test(s). Use suite:SUITE to specify '
     64                              'test suite. Use e:[NAME_PATTERN] to specify a '
     65                              'NAME-matching regular expression. Use '
     66                              'f:[FILE_PATTERN] to specify a filename matching '
     67                              'regular expression. Specified regular '
     68                              'expressions will be implicitly wrapped in '
     69                              '^ and $.')
     70     parser.add_argument('--fast', action='store_true', dest='fast_mode',
     71                         default=False,
     72                         help='Enable fast mode.  This will cause test_droid '
     73                              'to skip time consuming steps like sysinfo and '
     74                              'collecting crash information.')
     75     parser.add_argument('--args', metavar='ARGS',
     76                         help='Whitespace separated argument string to pass '
     77                              'through to test. Only supported for runs '
     78                              'against a local DUT.')
     79     parser.add_argument('--results_dir', metavar='RESULTS_DIR', default=None,
     80                         help='Instead of storing results in a new subdirectory'
     81                              ' of /tmp , store results in RESULTS_DIR. If '
     82                              'RESULTS_DIR already exists, it will be deleted.')
     83     parser.add_argument('--pretend', action='store_true', default=False,
     84                         help='Print autoserv commands that would be run, '
     85                              'rather than running them.')
     86     parser.add_argument('--no-experimental', action='store_true',
     87                         default=False, dest='no_experimental',
     88                         help='When scheduling a suite, skip any tests marked '
     89                              'as experimental. Applies only to tests scheduled'
     90                              ' via suite:[SUITE].')
     91     parser.add_argument('--enforce-deps', action='store_true',
     92                         default=False, dest='enforce_deps',
     93                         help='Skip tests whose DEPENDENCIES can not '
     94                              'be satisfied.')
     95     parser.add_argument('--debug', action='store_true',
     96                         help='Include DEBUG level messages in stdout. Note: '
     97                              'these messages will be included in output log '
     98                              'file regardless. In addition, turn on autoserv '
     99                              'verbosity.')
    100     parser.add_argument('--iterations', action='store', type=int, default=1,
    101                         help='Number of times to run the tests specified.')
    102     parser.add_argument('--ssh_verbosity', action='store', type=int,
    103                         choices=[0, 1, 2, 3], default=0,
    104                         help='Verbosity level for ssh, between 0 and 3 '
    105                              'inclusive.')
    106     parser.add_argument('--ssh_options', action='store', default=None,
    107                         help='A string giving additional options to be '
    108                         'added to ssh commands.')
    109 
    110 
    111 
    112 def fetch_local_suite(autotest_path, suite_predicate, afe, test_arg, remote,
    113                       build=NO_BUILD, board=NO_BOARD,
    114                       results_directory=None, no_experimental=False,
    115                       ignore_deps=True):
    116     """Create a suite from the given suite predicate.
    117 
    118     Satisfaction of dependencies is enforced by Suite.schedule() if
    119     ignore_deps is False. Note that this method assumes only one host,
    120     i.e. |remote|, was added to afe. Suite.schedule() will not
    121     schedule a job if none of the hosts in the afe (in our case,
    122     just one host |remote|) has a label that matches a requested
    123     test dependency.
    124 
    125     @param autotest_path: Absolute path to autotest (in sysroot or
    126                           custom autotest directory set by --autotest_dir).
    127     @param suite_predicate: callable that takes ControlData objects, and
    128                             returns True on those that should be in suite
    129     @param afe: afe object to schedule against (typically a directAFE)
    130     @param test_arg: String. An individual TEST command line argument, e.g.
    131                      'login_CryptohomeMounted' or 'suite:smoke'.
    132     @param remote: String representing the IP of the remote host.
    133     @param build: Build to schedule suite for.
    134     @param board: Board to schedule suite for.
    135     @param results_directory: Absolute path of directory to store results in.
    136                               (results will be stored in subdirectory of this).
    137     @param no_experimental: Skip experimental tests when scheduling a suite.
    138     @param ignore_deps: If True, test dependencies will be ignored.
    139 
    140     @returns: A suite.Suite object.
    141 
    142     """
    143     fs_getter = suite.Suite.create_fs_getter(autotest_path)
    144     devserver = dev_server.ImageServer('')
    145     my_suite = suite.Suite.create_from_predicates([suite_predicate],
    146             {provision.CROS_VERSION_PREFIX: build},
    147             constants.BOARD_PREFIX + board,
    148             devserver, fs_getter, afe=afe,
    149             ignore_deps=ignore_deps,
    150             results_dir=results_directory, forgiving_parser=False)
    151     if len(my_suite.tests) == 0:
    152         (similarity_predicate, similarity_description) = (
    153                 get_predicate_for_possible_test_arg(test_arg))
    154         logging.error('No test found, searching for possible tests with %s',
    155                       similarity_description)
    156         possible_tests = suite.Suite.find_possible_tests(fs_getter,
    157                                                          similarity_predicate)
    158         raise ValueError('Found no tests. Check your suite name, test name, '
    159                          'or test matching wildcard.\nDid you mean any of '
    160                          'following tests?\n  %s' % '\n  '.join(possible_tests))
    161 
    162     if not ignore_deps:
    163         # Log tests whose dependencies can't be satisfied.
    164         labels = [label.name for label in
    165                   afe.get_labels(host__hostname=remote)]
    166         for test in my_suite.tests:
    167             if test.experimental and no_experimental:
    168                 continue
    169             unsatisfiable_deps = set(test.dependencies).difference(labels)
    170             if unsatisfiable_deps:
    171                 logging.warning('%s will be skipped, unsatisfiable '
    172                              'test dependencies: %s', test.name,
    173                              unsatisfiable_deps)
    174     return my_suite
    175 
    176 
    177 def _run_autoserv(command, pretend=False):
    178     """Run autoserv command.
    179 
    180     Run the autoserv command and wait on it. Log the stdout.
    181     Ensure that SIGINT signals are passed along to autoserv.
    182 
    183     @param command: the autoserv command to run.
    184     @returns: exit code of the command.
    185 
    186     """
    187     if not pretend:
    188         logging.debug('Running autoserv command: %s', command)
    189         global _autoserv_proc
    190         _autoserv_proc = subprocess.Popen(command,
    191                                           stdout=subprocess.PIPE,
    192                                           stderr=subprocess.STDOUT)
    193         # This incantation forces unbuffered reading from stdout,
    194         # so that autoserv output can be displayed to the user
    195         # immediately.
    196         for message in iter(_autoserv_proc.stdout.readline, b''):
    197             logging.info('autoserv| %s', message.strip())
    198 
    199         _autoserv_proc.wait()
    200         returncode = _autoserv_proc.returncode
    201         _autoserv_proc = None
    202     else:
    203         logging.info('Pretend mode. Would run autoserv command: %s',
    204                      command)
    205         returncode = 0
    206     return returncode
    207 
    208 
    209 def run_provisioning_job(provision_label, host, autotest_path,
    210                          results_directory, fast_mode,
    211                          ssh_verbosity=0, ssh_options=None,
    212                          pretend=False, autoserv_verbose=False):
    213     """Shell out to autoserv to run provisioning job.
    214 
    215     @param provision_label: Label to provision the machine to.
    216     @param host: Hostname of DUT.
    217     @param autotest_path: Absolute path of autotest directory.
    218     @param results_directory: Absolute path of directory to store results in.
    219                               (results will be stored in subdirectory of this).
    220     @param fast_mode: bool to use fast mode (disables slow autotest features).
    221     @param ssh_verbosity: SSH verbosity level, passed along to autoserv_utils
    222     @param ssh_options: Additional ssh options to be passed to autoserv_utils
    223     @param pretend: If True, will print out autoserv commands rather than
    224                     running them.
    225     @param autoserv_verbose: If true, pass the --verbose flag to autoserv.
    226 
    227     @returns: Absolute path of directory where results were stored.
    228 
    229     """
    230     # TODO(fdeng): When running against a local DUT, autoserv
    231     # is still hitting the AFE in the lab.
    232     # provision_AutoUpdate checks the current build of DUT by
    233     # retrieving build info from AFE. crosbug.com/295178
    234     results_directory = os.path.join(results_directory, 'results-provision')
    235     command = autoserv_utils.autoserv_run_job_command(
    236             os.path.join(autotest_path, 'server'),
    237             machines=host, job=None, verbose=autoserv_verbose,
    238             results_directory=results_directory,
    239             fast_mode=fast_mode, ssh_verbosity=ssh_verbosity,
    240             ssh_options=ssh_options,
    241             extra_args=['--provision', '--job-labels', provision_label],
    242             no_console_prefix=True)
    243     if _run_autoserv(command, pretend) != 0:
    244         raise TestThatProvisioningError('Command returns non-zero code: %s ' %
    245                                         command)
    246     return results_directory
    247 
    248 
    249 def run_job(job, host, autotest_path, results_directory, fast_mode,
    250             id_digits=1, ssh_verbosity=0, ssh_options=None,
    251             args=None, pretend=False,
    252             autoserv_verbose=False, host_attributes={}):
    253     """
    254     Shell out to autoserv to run an individual test job.
    255 
    256     @param job: A Job object containing the control file contents and other
    257                 relevent metadata for this test.
    258     @param host: Hostname of DUT to run test against.
    259     @param autotest_path: Absolute path of autotest directory.
    260     @param results_directory: Absolute path of directory to store results in.
    261                               (results will be stored in subdirectory of this).
    262     @param fast_mode: bool to use fast mode (disables slow autotest features).
    263     @param id_digits: The minimum number of digits that job ids should be
    264                       0-padded to when formatting as a string for results
    265                       directory.
    266     @param ssh_verbosity: SSH verbosity level, passed along to autoserv_utils
    267     @param ssh_options: Additional ssh options to be passed to autoserv_utils
    268     @param args: String that should be passed as args parameter to autoserv,
    269                  and then ultimitely to test itself.
    270     @param pretend: If True, will print out autoserv commands rather than
    271                     running them.
    272     @param autoserv_verbose: If true, pass the --verbose flag to autoserv.
    273     @param host_attributes: Dict of host attributes to pass into autoserv.
    274 
    275     @returns: a tuple, return code of the job and absolute path of directory
    276               where results were stored.
    277     """
    278     with tempfile.NamedTemporaryFile() as temp_file:
    279         temp_file.write(job.control_file)
    280         temp_file.flush()
    281         name_tail = job.name.split('/')[-1]
    282         results_directory = os.path.join(results_directory,
    283                                          'results-%0*d-%s' % (id_digits, job.id,
    284                                                               name_tail))
    285         # Drop experimental keyval in the keval file in the job result folder.
    286         os.makedirs(results_directory)
    287         utils.write_keyval(results_directory,
    288                            {constants.JOB_EXPERIMENTAL_KEY: job.keyvals[
    289                                    constants.JOB_EXPERIMENTAL_KEY]})
    290         extra_args = [temp_file.name]
    291         if args:
    292             extra_args.extend(['--args', args])
    293 
    294         command = autoserv_utils.autoserv_run_job_command(
    295                 os.path.join(autotest_path, 'server'),
    296                 machines=host, job=job, verbose=autoserv_verbose,
    297                 results_directory=results_directory,
    298                 fast_mode=fast_mode, ssh_verbosity=ssh_verbosity,
    299                 ssh_options=ssh_options,
    300                 extra_args=extra_args,
    301                 no_console_prefix=True,
    302                 use_packaging=False,
    303                 host_attributes=host_attributes)
    304 
    305         code = _run_autoserv(command, pretend)
    306         return code, results_directory
    307 
    308 
    309 def setup_local_afe():
    310     """
    311     Setup a local afe database and return a direct_afe object to access it.
    312 
    313     @returns: A autotest_lib.frontend.afe.direct_afe instance.
    314     """
    315     # This import statement is delayed until now rather than running at
    316     # module load time, because it kicks off a local sqlite :memory: backed
    317     # database, and we don't need that unless we are doing a local run.
    318     from autotest_lib.frontend import setup_django_lite_environment
    319     from autotest_lib.frontend.afe import direct_afe
    320     return direct_afe.directAFE()
    321 
    322 
    323 def get_predicate_for_test_arg(test):
    324     """
    325     Gets a suite predicte function for a given command-line argument.
    326 
    327     @param test: String. An individual TEST command line argument, e.g.
    328                          'login_CryptohomeMounted' or 'suite:smoke'
    329     @returns: A (predicate, string) tuple with the necessary suite
    330               predicate, and a description string of the suite that
    331               this predicate will produce.
    332     """
    333     suitematch = re.match(_SUITE_REGEX, test)
    334     name_pattern_match = re.match(r'e:(.*)', test)
    335     file_pattern_match = re.match(r'f:(.*)', test)
    336     if suitematch:
    337         suitename = suitematch.group(1)
    338         return (suite.Suite.name_in_tag_predicate(suitename),
    339                 'suite named %s' % suitename)
    340     if name_pattern_match:
    341         pattern = '^%s$' % name_pattern_match.group(1)
    342         return (suite.Suite.test_name_matches_pattern_predicate(pattern),
    343                 'suite to match name pattern %s' % pattern)
    344     if file_pattern_match:
    345         pattern = '^%s$' % file_pattern_match.group(1)
    346         return (suite.Suite.test_file_matches_pattern_predicate(pattern),
    347                 'suite to match file name pattern %s' % pattern)
    348     return (suite.Suite.test_name_equals_predicate(test),
    349             'job named %s' % test)
    350 
    351 
    352 def get_predicate_for_possible_test_arg(test):
    353     """
    354     Gets a suite predicte function to calculate the similarity of given test
    355     and possible tests.
    356 
    357     @param test: String. An individual TEST command line argument, e.g.
    358                          'login_CryptohomeMounted' or 'suite:smoke'
    359     @returns: A (predicate, string) tuple with the necessary suite
    360               predicate, and a description string of the suite that
    361               this predicate will produce.
    362     """
    363     suitematch = re.match(_SUITE_REGEX, test)
    364     name_pattern_match = re.match(r'e:(.*)', test)
    365     file_pattern_match = re.match(r'f:(.*)', test)
    366     if suitematch:
    367         suitename = suitematch.group(1)
    368         return (suite.Suite.name_in_tag_similarity_predicate(suitename),
    369                 'suite name similar to %s' % suitename)
    370     if name_pattern_match:
    371         pattern = '^%s$' % name_pattern_match.group(1)
    372         return (suite.Suite.test_name_similarity_predicate(pattern),
    373                 'job name similar to %s' % pattern)
    374     if file_pattern_match:
    375         pattern = '^%s$' % file_pattern_match.group(1)
    376         return (suite.Suite.test_file_similarity_predicate(pattern),
    377                 'suite to match file name similar to %s' % pattern)
    378     return (suite.Suite.test_name_similarity_predicate(test),
    379             'job name similar to %s' % test)
    380 
    381 
    382 def add_ssh_identity(temp_directory, ssh_private_key=TEST_KEY_PATH):
    383     """Add an ssh identity to the agent.
    384 
    385     TODO (sbasi) b/26186193: Add support for test_droid and make TEST_KEY_PATH
    386     not Chrome OS specific.
    387 
    388     @param temp_directory: A directory to copy the |private key| into.
    389     @param ssh_private_key: Path to the ssh private key to use for testing.
    390     """
    391     # Add the testing key to the current ssh agent.
    392     if os.environ.has_key('SSH_AGENT_PID'):
    393         # Copy the testing key to the temp directory and make it NOT
    394         # world-readable. Otherwise, ssh-add complains.
    395         shutil.copy(ssh_private_key, temp_directory)
    396         key_copy_path = os.path.join(temp_directory,
    397                                      os.path.basename(ssh_private_key))
    398         os.chmod(key_copy_path, stat.S_IRUSR | stat.S_IWUSR)
    399         p = subprocess.Popen(['ssh-add', key_copy_path],
    400                              stderr=subprocess.STDOUT, stdout=subprocess.PIPE)
    401         p_out, _ = p.communicate()
    402         for line in p_out.splitlines():
    403             logging.info(line)
    404     else:
    405         logging.warning('There appears to be no running ssh-agent. Attempting '
    406                         'to continue without running ssh-add, but ssh commands '
    407                         'may fail.')
    408 
    409 
    410 def _auto_detect_labels(afe, remote):
    411     """Automatically detect host labels and add them to the host in afe.
    412 
    413     Note that the label of board will not be auto-detected.
    414     This method assumes the host |remote| has already been added to afe.
    415 
    416     @param afe: A direct_afe object used to interact with local afe database.
    417     @param remote: The hostname of the remote device.
    418 
    419     """
    420     cros_host = factory.create_host(remote)
    421     labels_to_create = [label for label in cros_host.get_labels()
    422                         if not label.startswith(constants.BOARD_PREFIX)]
    423     labels_to_add_to_afe_host = []
    424     for label in labels_to_create:
    425         new_label = afe.create_label(label)
    426         labels_to_add_to_afe_host.append(new_label.name)
    427     hosts = afe.get_hosts(hostname=remote)
    428     if not hosts:
    429         raise TestThatRunError('Unexpected error: %s has not '
    430                                'been added to afe.' % remote)
    431     afe_host = hosts[0]
    432     afe_host.add_labels(labels_to_add_to_afe_host)
    433 
    434 
    435 def perform_local_run(afe, autotest_path, tests, remote, fast_mode,
    436                       build=NO_BUILD, board=NO_BOARD, args=None,
    437                       pretend=False, no_experimental=False,
    438                       ignore_deps=True,
    439                       results_directory=None, ssh_verbosity=0,
    440                       ssh_options=None,
    441                       autoserv_verbose=False,
    442                       iterations=1,
    443                       host_attributes={}):
    444     """Perform local run of tests.
    445 
    446     This method enforces satisfaction of test dependencies for tests that are
    447     run as a part of a suite.
    448 
    449     @param afe: A direct_afe object used to interact with local afe database.
    450     @param autotest_path: Absolute path of autotest installed in sysroot or
    451                           custom autotest path set by --autotest_dir.
    452     @param tests: List of strings naming tests and suites to run. Suite strings
    453                   should be formed like "suite:smoke".
    454     @param remote: Remote hostname.
    455     @param fast_mode: bool to use fast mode (disables slow autotest features).
    456     @param build: String specifying build for local run.
    457     @param board: String specifyinb board for local run.
    458     @param args: String that should be passed as args parameter to autoserv,
    459                  and then ultimitely to test itself.
    460     @param pretend: If True, will print out autoserv commands rather than
    461                     running them.
    462     @param no_experimental: Skip experimental tests when scheduling a suite.
    463     @param ignore_deps: If True, test dependencies will be ignored.
    464     @param results_directory: Directory to store results in. Defaults to None,
    465                               in which case results will be stored in a new
    466                               subdirectory of /tmp
    467     @param ssh_verbosity: SSH verbosity level, passed through to
    468                           autoserv_utils.
    469     @param ssh_options: Additional ssh options to be passed to autoserv_utils
    470     @param autoserv_verbose: If true, pass the --verbose flag to autoserv.
    471     @param iterations: int number of times to schedule tests.
    472     @param host_attributes: Dict of host attributes to pass into autoserv.
    473 
    474     @returns: A list of return codes each job that has run.
    475     """
    476     # Create host in afe, add board and build labels.
    477     cros_version_label = provision.cros_version_to_label(build)
    478     build_label = afe.create_label(cros_version_label)
    479     board_label = afe.create_label(constants.BOARD_PREFIX + board)
    480     new_host = afe.create_host(remote)
    481     new_host.add_labels([build_label.name, board_label.name])
    482     if not ignore_deps:
    483         logging.info('Auto-detecting labels for %s', remote)
    484         _auto_detect_labels(afe, remote)
    485     # Provision the host to |build|.
    486     if build != NO_BUILD:
    487         logging.info('Provisioning %s...', cros_version_label)
    488         try:
    489             run_provisioning_job(cros_version_label, remote, autotest_path,
    490                                  results_directory, fast_mode,
    491                                  ssh_verbosity, ssh_options,
    492                                  pretend, autoserv_verbose)
    493         except TestThatProvisioningError as e:
    494             logging.error('Provisioning %s to %s failed, tests are aborted, '
    495                           'failure reason: %s',
    496                           remote, cros_version_label, e)
    497             return
    498 
    499     # Create suites that will be scheduled.
    500     suites_and_descriptions = []
    501     for test in tests:
    502         (predicate, description) = get_predicate_for_test_arg(test)
    503         logging.info('Fetching suite for %s...', description)
    504         suite = fetch_local_suite(autotest_path, predicate, afe, test_arg=test,
    505                                   remote=remote,
    506                                   build=build, board=board,
    507                                   results_directory=results_directory,
    508                                   no_experimental=no_experimental,
    509                                   ignore_deps=ignore_deps)
    510         suites_and_descriptions.append((suite, description))
    511 
    512     # Schedule the suites, looping over iterations if necessary.
    513     for iteration in range(iterations):
    514         if iteration > 0:
    515             logging.info('Repeating scheduling for iteration %d:', iteration)
    516 
    517         for suite, description in suites_and_descriptions:
    518             logging.info('Scheduling suite for %s...', description)
    519             ntests = suite.schedule(
    520                     lambda log_entry, log_in_subdir=False: None,
    521                     add_experimental=not no_experimental)
    522             logging.info('... scheduled %s job(s).', ntests)
    523 
    524     if not afe.get_jobs():
    525         logging.info('No jobs scheduled. End of local run.')
    526         return
    527 
    528     last_job_id = afe.get_jobs()[-1].id
    529     job_id_digits = len(str(last_job_id))
    530     codes = []
    531     for job in afe.get_jobs():
    532         code, _ = run_job(job, remote, autotest_path, results_directory,
    533                 fast_mode, job_id_digits, ssh_verbosity, ssh_options, args,
    534                 pretend, autoserv_verbose, host_attributes)
    535         codes.append(code)
    536     return codes
    537 
    538 
    539 def sigint_handler(signum, stack_frame):
    540     #pylint: disable-msg=C0111
    541     """Handle SIGINT or SIGTERM to a local test_that run.
    542 
    543     This handler sends a SIGINT to the running autoserv process,
    544     if one is running, giving it up to 5 seconds to clean up and exit. After
    545     the timeout elapses, autoserv is killed. In either case, after autoserv
    546     exits then this process exits with status 1.
    547     """
    548     # If multiple signals arrive before handler is unset, ignore duplicates
    549     if not _sigint_handler_lock.acquire(False):
    550         return
    551     try:
    552         # Ignore future signals by unsetting handler.
    553         signal.signal(signal.SIGINT, signal.SIG_IGN)
    554         signal.signal(signal.SIGTERM, signal.SIG_IGN)
    555 
    556         logging.warning('Received SIGINT or SIGTERM. Cleaning up and exiting.')
    557         if _autoserv_proc:
    558             logging.warning('Sending SIGINT to autoserv process. Waiting up '
    559                             'to %s seconds for cleanup.',
    560                             _AUTOSERV_SIGINT_TIMEOUT_SECONDS)
    561             _autoserv_proc.send_signal(signal.SIGINT)
    562             timed_out, _ = retry.timeout(_autoserv_proc.wait,
    563                     timeout_sec=_AUTOSERV_SIGINT_TIMEOUT_SECONDS)
    564             if timed_out:
    565                 _autoserv_proc.kill()
    566                 logging.warning('Timed out waiting for autoserv to handle '
    567                                 'SIGINT. Killed autoserv.')
    568     finally:
    569         _sigint_handler_lock.release() # this is not really necessary?
    570         sys.exit(1)
    571 
    572 
    573 def create_results_directory(results_directory=None):
    574     """Create a results directory.
    575 
    576     If no directory is specified this method will create and return a
    577     temp directory to hold results. If a directory name is specified this
    578     method will create a directory at the given path, provided it doesn't
    579     already exist.
    580 
    581     @param results_directory: The path to the results_directory to create.
    582 
    583     @return results_directory: A path to the results_directory, ready for use.
    584     """
    585     if results_directory is None:
    586         # Create a results_directory as subdir of /tmp
    587         results_directory = tempfile.mkdtemp(prefix='test_that_results_')
    588     else:
    589         # Delete results_directory if it already exists.
    590         try:
    591             shutil.rmtree(results_directory)
    592         except OSError as e:
    593             if e.errno != errno.ENOENT:
    594                 raise
    595 
    596         # Create results_directory if it does not exist
    597         try:
    598             os.makedirs(results_directory)
    599         except OSError as e:
    600             if e.errno != errno.EEXIST:
    601                 raise
    602     return results_directory
    603 
    604 
    605 def perform_run_from_autotest_root(autotest_path, argv, tests, remote,
    606                                    build=NO_BUILD, board=NO_BOARD, args=None,
    607                                    pretend=False, no_experimental=False,
    608                                    ignore_deps=True,
    609                                    results_directory=None, ssh_verbosity=0,
    610                                    ssh_options=None,
    611                                    iterations=1, fast_mode=False, debug=False,
    612                                    whitelist_chrome_crashes=False,
    613                                    host_attributes={}):
    614     """
    615     Perform a test_that run, from the |autotest_path|.
    616 
    617     This function is to be called from test_that/test_droid's main() script,
    618     when tests are executed from the |autotest_path|. It handles all stages
    619     of a test run that come after the bootstrap into |autotest_path|.
    620 
    621     @param autotest_path: Full absolute path to the autotest root directory.
    622     @param argv: The arguments list, as passed to main(...)
    623     @param tests: List of strings naming tests and suites to run. Suite strings
    624                   should be formed like "suite:smoke".
    625     @param remote: Remote hostname.
    626     @param build: String specifying build for local run.
    627     @param board: String specifyinb board for local run.
    628     @param args: String that should be passed as args parameter to autoserv,
    629                  and then ultimitely to test itself.
    630     @param pretend: If True, will print out autoserv commands rather than
    631                     running them.
    632     @param no_experimental: Skip experimental tests when scheduling a suite.
    633     @param ignore_deps: If True, test dependencies will be ignored.
    634     @param results_directory: Directory to store results in. Defaults to None,
    635                               in which case results will be stored in a new
    636                               subdirectory of /tmp
    637     @param ssh_verbosity: SSH verbosity level, passed through to
    638                           autoserv_utils.
    639     @param ssh_options: Additional ssh options to be passed to autoserv_utils
    640     @param autoserv_verbose: If true, pass the --verbose flag to autoserv.
    641     @param iterations: int number of times to schedule tests.
    642     @param fast_mode: bool to use fast mode (disables slow autotest features).
    643     @param debug: Logging and autoserv verbosity.
    644     @param whitelist_chrome_crashes: If True, whitelist chrome crashes.
    645     @param host_attributes: Dict of host attributes to pass into autoserv.
    646 
    647     @returns: A return code that test_that should exit with.
    648     """
    649     if results_directory is None or not os.path.exists(results_directory):
    650         raise ValueError('Expected valid results directory, got %s' %
    651                           results_directory)
    652 
    653     logging_manager.configure_logging(
    654             server_logging_config.ServerLoggingConfig(),
    655             results_dir=results_directory,
    656             use_console=True,
    657             verbose=debug,
    658             debug_log_name='test_that')
    659     logging.info('Began logging to %s', results_directory)
    660 
    661     logging.debug('test_that command line was: %s', argv)
    662 
    663     signal.signal(signal.SIGINT, sigint_handler)
    664     signal.signal(signal.SIGTERM, sigint_handler)
    665 
    666     afe = setup_local_afe()
    667     codes = perform_local_run(afe, autotest_path, tests, remote, fast_mode,
    668                       build, board,
    669                       args=args,
    670                       pretend=pretend,
    671                       no_experimental=no_experimental,
    672                       ignore_deps=ignore_deps,
    673                       results_directory=results_directory,
    674                       ssh_verbosity=ssh_verbosity,
    675                       ssh_options=ssh_options,
    676                       autoserv_verbose=debug,
    677                       iterations=iterations,
    678                       host_attributes=host_attributes)
    679     if pretend:
    680         logging.info('Finished pretend run. Exiting.')
    681         return 0
    682 
    683     test_report_command = [os.path.join(os.path.dirname(__file__),
    684                                         'generate_test_report')]
    685     # Experimental test results do not influence the exit code.
    686     test_report_command.append('--ignore_experimental_tests')
    687     if whitelist_chrome_crashes:
    688         test_report_command.append('--whitelist_chrome_crashes')
    689     test_report_command.append(results_directory)
    690     final_result = subprocess.call(test_report_command)
    691     with open(os.path.join(results_directory, 'test_report.log'),
    692               'w') as report_log:
    693         subprocess.call(test_report_command, stdout=report_log)
    694     try:
    695         os.unlink(_LATEST_RESULTS_DIRECTORY)
    696     except OSError:
    697         pass
    698     link_target = os.path.relpath(results_directory,
    699                                   os.path.dirname(_LATEST_RESULTS_DIRECTORY))
    700     if any(codes):
    701         logging.error('Autoserv encountered unexpected errors '
    702                       'when executing jobs.')
    703         final_result = final_result or 1
    704     os.symlink(link_target, _LATEST_RESULTS_DIRECTORY)
    705     logging.info('Finished running tests. Results can be found in %s or %s',
    706                  results_directory, _LATEST_RESULTS_DIRECTORY)
    707     return final_result
    708