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 |