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