| OLD | NEW |
| (Empty) |
| 1 #!/usr/bin/python | |
| 2 # Copyright (c) 2010 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 | |
| 6 """Runs tests on VMs in parallel.""" | |
| 7 | |
| 8 import optparse | |
| 9 import os | |
| 10 import subprocess | |
| 11 import sys | |
| 12 import tempfile | |
| 13 | |
| 14 sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) | |
| 15 from cros_build_lib import Die | |
| 16 from cros_build_lib import Info | |
| 17 | |
| 18 | |
| 19 _DEFAULT_BASE_SSH_PORT = 9222 | |
| 20 | |
| 21 class ParallelTestRunner(object): | |
| 22 """Runs tests on VMs in parallel. | |
| 23 | |
| 24 This class is a simple wrapper around cros_run_vm_test that provides an easy | |
| 25 way to spawn several test instances in parallel and aggregate the results when | |
| 26 the tests complete. Only uses emerged autotest packaged, as trying to pull | |
| 27 from the caller's source tree creates races that cause tests to fail. | |
| 28 """ | |
| 29 | |
| 30 def __init__(self, tests, base_ssh_port=_DEFAULT_BASE_SSH_PORT, board=None, | |
| 31 image_path=None, order_output=False, quiet=False, | |
| 32 results_dir_root=None): | |
| 33 """Constructs and initializes the test runner class. | |
| 34 | |
| 35 Args: | |
| 36 tests: A list of test names (see run_remote_tests.sh). | |
| 37 base_ssh_port: The base SSH port. Spawned VMs listen to localhost SSH | |
| 38 ports incrementally allocated starting from the base one. | |
| 39 board: The target board. If none, cros_run_vm_tests will use the default | |
| 40 board. | |
| 41 image_path: Full path to the VM image. If none, cros_run_vm_tests will use | |
| 42 the latest image. | |
| 43 order_output: If True, output of individual VMs will be piped to | |
| 44 temporary files and emitted at the end. | |
| 45 quiet: Emits no output from the VMs. Forces --order_output to be false, | |
| 46 and requires specifying --results_dir_root | |
| 47 results_dir_root: The results directory root. If provided, the results | |
| 48 directory root for each test will be created under it with the SSH port | |
| 49 appended to the test name. | |
| 50 """ | |
| 51 self._tests = tests | |
| 52 self._base_ssh_port = base_ssh_port | |
| 53 self._board = board | |
| 54 self._image_path = image_path | |
| 55 self._order_output = order_output | |
| 56 self._quiet = quiet | |
| 57 self._results_dir_root = results_dir_root | |
| 58 | |
| 59 def _SpawnTests(self): | |
| 60 """Spawns VMs and starts the test runs on them. | |
| 61 | |
| 62 Runs all tests in |self._tests|. Each test is executed on a separate VM. | |
| 63 | |
| 64 Returns: | |
| 65 A list of test process info objects containing the following dictionary | |
| 66 entries: | |
| 67 'test': the test name; | |
| 68 'proc': the Popen process instance for this test run. | |
| 69 """ | |
| 70 ssh_port = self._base_ssh_port | |
| 71 spawned_tests = [] | |
| 72 for test in self._tests: | |
| 73 args = [ os.path.join(os.path.dirname(__file__), 'cros_run_vm_test'), | |
| 74 '--snapshot', # The image is shared so don't modify it. | |
| 75 '--no_graphics', | |
| 76 '--use_emerged', | |
| 77 '--ssh_port=%d' % ssh_port ] | |
| 78 if self._board: args.append('--board=%s' % self._board) | |
| 79 if self._image_path: args.append('--image_path=%s' % self._image_path) | |
| 80 results_dir = None | |
| 81 if self._results_dir_root: | |
| 82 results_dir = '%s/%s.%d' % (self._results_dir_root, test, ssh_port) | |
| 83 args.append('--results_dir_root=%s' % results_dir) | |
| 84 args.append(test) | |
| 85 Info('Running %r...' % args) | |
| 86 output = None | |
| 87 if self._quiet: | |
| 88 output = open('/dev/null', mode='w') | |
| 89 Info('Log files are in %s' % results_dir) | |
| 90 elif self._order_output: | |
| 91 output = tempfile.NamedTemporaryFile(prefix='parallel_vm_test_') | |
| 92 Info('Piping output to %s.' % output.name) | |
| 93 proc = subprocess.Popen(args, stdout=output, stderr=output) | |
| 94 test_info = { 'test': test, | |
| 95 'proc': proc, | |
| 96 'output': output } | |
| 97 spawned_tests.append(test_info) | |
| 98 ssh_port = ssh_port + 1 | |
| 99 return spawned_tests | |
| 100 | |
| 101 def _WaitForCompletion(self, spawned_tests): | |
| 102 """Waits for tests to complete and returns a list of failed tests. | |
| 103 | |
| 104 If the test output was piped to a file, dumps the file contents to stdout. | |
| 105 | |
| 106 Args: | |
| 107 spawned_tests: A list of test info objects (see _SpawnTests). | |
| 108 | |
| 109 Returns: | |
| 110 A list of failed test names. | |
| 111 """ | |
| 112 failed_tests = [] | |
| 113 for test_info in spawned_tests: | |
| 114 proc = test_info['proc'] | |
| 115 proc.wait() | |
| 116 if proc.returncode: failed_tests.append(test_info['test']) | |
| 117 output = test_info['output'] | |
| 118 if output and not self._quiet: | |
| 119 test = test_info['test'] | |
| 120 Info('------ START %s:%s ------' % (test, output.name)) | |
| 121 output.seek(0) | |
| 122 for line in output: | |
| 123 print line, | |
| 124 Info('------ END %s:%s ------' % (test, output.name)) | |
| 125 return failed_tests | |
| 126 | |
| 127 def Run(self): | |
| 128 """Runs the tests in |self._tests| on separate VMs in parallel.""" | |
| 129 spawned_tests = self._SpawnTests() | |
| 130 failed_tests = self._WaitForCompletion(spawned_tests) | |
| 131 if failed_tests: Die('Tests failed: %r' % failed_tests) | |
| 132 | |
| 133 | |
| 134 def main(): | |
| 135 usage = 'Usage: %prog [options] tests...' | |
| 136 parser = optparse.OptionParser(usage=usage) | |
| 137 parser.add_option('--base_ssh_port', type='int', | |
| 138 default=_DEFAULT_BASE_SSH_PORT, | |
| 139 help='Base SSH port. Spawned VMs listen to localhost SSH ' | |
| 140 'ports incrementally allocated starting from the base one. ' | |
| 141 '[default: %default]') | |
| 142 parser.add_option('--board', | |
| 143 help='The target board. If none specified, ' | |
| 144 'cros_run_vm_test will use the default board.') | |
| 145 parser.add_option('--image_path', | |
| 146 help='Full path to the VM image. If none specified, ' | |
| 147 'cros_run_vm_test will use the latest image.') | |
| 148 parser.add_option('--order_output', action='store_true', default=False, | |
| 149 help='Rather than emitting interleaved progress output ' | |
| 150 'from the individual VMs, accumulate the outputs in ' | |
| 151 'temporary files and dump them at the end.') | |
| 152 parser.add_option('--quiet', action='store_true', default=False, | |
| 153 help='Emits no output from the VMs. Forces --order_output' | |
| 154 'to be false, and requires specifying --results_dir_root') | |
| 155 parser.add_option('--results_dir_root', | |
| 156 help='Root results directory. If none specified, each test ' | |
| 157 'will store its results in a separate /tmp directory.') | |
| 158 (options, args) = parser.parse_args() | |
| 159 | |
| 160 if not args: | |
| 161 parser.print_help() | |
| 162 Die('no tests provided') | |
| 163 | |
| 164 if options.quiet: | |
| 165 options.order_output = False | |
| 166 if not options.results_dir_root: | |
| 167 Die('--quiet requires --results_dir_root') | |
| 168 runner = ParallelTestRunner(args, options.base_ssh_port, options.board, | |
| 169 options.image_path, options.order_output, | |
| 170 options.quiet, options.results_dir_root) | |
| 171 runner.Run() | |
| 172 | |
| 173 | |
| 174 if __name__ == '__main__': | |
| 175 main() | |
| OLD | NEW |