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