blob: f74bec12aecba495de488eecb070ec0798bf62b8 [file] [log] [blame]
#!/usr/bin/python
# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Runs tests on VMs in parallel."""
import optparse
import os
import subprocess
import sys
import tempfile
sys.path.append(os.path.join(os.path.dirname(__file__), '../lib'))
from cros_build_lib import Die
from cros_build_lib import Info
_DEFAULT_BASE_SSH_PORT = 9222
class ParallelTestRunner(object):
"""Runs tests on VMs in parallel.
This class is a simple wrapper around cros_run_vm_test that provides an easy
way to spawn several test instances in parallel and aggregate the results when
the tests complete. Only uses emerged autotest packaged, as trying to pull
from the caller's source tree creates races that cause tests to fail.
"""
def __init__(self, tests, base_ssh_port=_DEFAULT_BASE_SSH_PORT, board=None,
image_path=None, order_output=False, quiet=False,
results_dir_root=None):
"""Constructs and initializes the test runner class.
Args:
tests: A list of test names (see run_remote_tests.sh).
base_ssh_port: The base SSH port. Spawned VMs listen to localhost SSH
ports incrementally allocated starting from the base one.
board: The target board. If none, cros_run_vm_tests will use the default
board.
image_path: Full path to the VM image. If none, cros_run_vm_tests will use
the latest image.
order_output: If True, output of individual VMs will be piped to
temporary files and emitted at the end.
quiet: Emits no output from the VMs. Forces --order_output to be false,
and requires specifying --results_dir_root
results_dir_root: The results directory root. If provided, the results
directory root for each test will be created under it with the SSH port
appended to the test name.
"""
self._tests = tests
self._base_ssh_port = base_ssh_port
self._board = board
self._image_path = image_path
self._order_output = order_output
self._quiet = quiet
self._results_dir_root = results_dir_root
def _SpawnTests(self):
"""Spawns VMs and starts the test runs on them.
Runs all tests in |self._tests|. Each test is executed on a separate VM.
Returns:
A list of test process info objects containing the following dictionary
entries:
'test': the test name;
'proc': the Popen process instance for this test run.
"""
ssh_port = self._base_ssh_port
spawned_tests = []
for test in self._tests:
args = [ os.path.join(os.path.dirname(__file__), 'cros_run_vm_test'),
'--snapshot', # The image is shared so don't modify it.
'--no_graphics',
'--use_emerged',
'--ssh_port=%d' % ssh_port ]
if self._board: args.append('--board=%s' % self._board)
if self._image_path: args.append('--image_path=%s' % self._image_path)
results_dir = None
if self._results_dir_root:
results_dir = '%s/%s.%d' % (self._results_dir_root, test, ssh_port)
args.append('--results_dir_root=%s' % results_dir)
args.append(test)
Info('Running %r...' % args)
output = None
if self._quiet:
output = open('/dev/null', mode='w')
Info('Log files are in %s' % results_dir)
elif self._order_output:
output = tempfile.NamedTemporaryFile(prefix='parallel_vm_test_')
Info('Piping output to %s.' % output.name)
proc = subprocess.Popen(args, stdout=output, stderr=output)
test_info = { 'test': test,
'proc': proc,
'output': output }
spawned_tests.append(test_info)
ssh_port = ssh_port + 1
return spawned_tests
def _WaitForCompletion(self, spawned_tests):
"""Waits for tests to complete and returns a list of failed tests.
If the test output was piped to a file, dumps the file contents to stdout.
Args:
spawned_tests: A list of test info objects (see _SpawnTests).
Returns:
A list of failed test names.
"""
failed_tests = []
for test_info in spawned_tests:
proc = test_info['proc']
proc.wait()
if proc.returncode: failed_tests.append(test_info['test'])
output = test_info['output']
if output and not self._quiet:
test = test_info['test']
Info('------ START %s:%s ------' % (test, output.name))
output.seek(0)
for line in output:
print line,
Info('------ END %s:%s ------' % (test, output.name))
return failed_tests
def Run(self):
"""Runs the tests in |self._tests| on separate VMs in parallel."""
spawned_tests = self._SpawnTests()
failed_tests = self._WaitForCompletion(spawned_tests)
if failed_tests: Die('Tests failed: %r' % failed_tests)
def main():
usage = 'Usage: %prog [options] tests...'
parser = optparse.OptionParser(usage=usage)
parser.add_option('--base_ssh_port', type='int',
default=_DEFAULT_BASE_SSH_PORT,
help='Base SSH port. Spawned VMs listen to localhost SSH '
'ports incrementally allocated starting from the base one. '
'[default: %default]')
parser.add_option('--board',
help='The target board. If none specified, '
'cros_run_vm_test will use the default board.')
parser.add_option('--image_path',
help='Full path to the VM image. If none specified, '
'cros_run_vm_test will use the latest image.')
parser.add_option('--order_output', action='store_true', default=False,
help='Rather than emitting interleaved progress output '
'from the individual VMs, accumulate the outputs in '
'temporary files and dump them at the end.')
parser.add_option('--quiet', action='store_true', default=False,
help='Emits no output from the VMs. Forces --order_output'
'to be false, and requires specifying --results_dir_root')
parser.add_option('--results_dir_root',
help='Root results directory. If none specified, each test '
'will store its results in a separate /tmp directory.')
(options, args) = parser.parse_args()
if not args:
parser.print_help()
Die('no tests provided')
if options.quiet:
options.order_output = False
if not options.results_dir_root:
Die('--quiet requires --results_dir_root')
runner = ParallelTestRunner(args, options.base_ssh_port, options.board,
options.image_path, options.order_output,
options.quiet, options.results_dir_root)
runner.Run()
if __name__ == '__main__':
main()