blob: f7459dd9070c8898f768605d0a670f18c376fd61 [file] [log] [blame]
#!/usr/bin/env python
#
# Copyright (c) 2012 The Chromium 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 all the native unit tests.
1. Copy over test binary to /data/local on device.
2. Resources: chrome/unit_tests requires resources (chrome.pak and en-US.pak)
to be deployed to the device. We use the device's $EXTERNAL_STORAGE as the
base dir (which maps to Context.getExternalFilesDir()).
3. Environment:
3.1. chrome/unit_tests requires (via chrome_paths.cc) a directory named:
$EXTERNAL_STORAGE + /chrome/test/data
4. Run the binary in the device and stream the log to the host.
4.1. Optionally, filter specific tests.
4.2. If we're running a single test suite and we have multiple devices
connected, we'll shard the tests.
5. Clean up the device.
Suppressions:
Individual tests in a test binary can be suppressed by listing it in
the gtest_filter directory in a file of the same name as the test binary,
one test per line. Here is an example:
$ cat gtest_filter/base_unittests_disabled
DataPackTest.Load
ReadOnlyFileUtilTest.ContentsEqual
This file is generated by the tests running on devices. If running on emulator,
additonal filter file which lists the tests only failed in emulator will be
loaded. We don't care about the rare testcases which succeeded on emuatlor, but
failed on device.
"""
import copy
import fnmatch
import logging
import optparse
import os
import signal
import subprocess
import sys
import time
import emulator
from pylib import android_commands
from pylib import buildbot_report
from pylib import cmd_helper
from pylib import debug_info
from pylib import ports
from pylib import run_tests_helper
from pylib import test_options_parser
from pylib.base_test_sharder import BaseTestSharder
from pylib.single_test_runner import SingleTestRunner
_TEST_SUITES = ['base_unittests',
'cc_unittests',
'content_unittests',
'gpu_unittests',
'ipc_tests',
'media_unittests',
'net_unittests',
'sql_unittests',
'sync_unit_tests',
'ui_unittests',
'unit_tests',
'webkit_compositor_bindings_unittests',
]
def FullyQualifiedTestSuites(exe, option_test_suite, build_type):
"""Get a list of absolute paths to test suite targets.
Args:
exe: if True, use the executable-based test runner.
option_test_suite: the test_suite specified as an option.
build_type: 'Release' or 'Debug'.
"""
test_suite_dir = os.path.join(cmd_helper.OutDirectory.get(), build_type)
if option_test_suite:
all_test_suites = [option_test_suite]
else:
all_test_suites = _TEST_SUITES
if exe:
qualified_test_suites = [os.path.join(test_suite_dir, t)
for t in all_test_suites]
else:
# out/(Debug|Release)/$SUITE_apk/$SUITE-debug.apk
qualified_test_suites = [os.path.join(test_suite_dir,
t + '_apk',
t + '-debug.apk')
for t in all_test_suites]
for t, q in zip(all_test_suites, qualified_test_suites):
if not os.path.exists(q):
raise Exception('Test suite %s not found in %s.\n'
'Supported test suites:\n %s\n'
'Ensure it has been built.\n' %
(t, q, _TEST_SUITES))
return qualified_test_suites
class TimeProfile(object):
"""Class for simple profiling of action, with logging of cost."""
def __init__(self, description):
self._description = description
self.Start()
def Start(self):
self._starttime = time.time()
def Stop(self):
"""Stop profiling and dump a log."""
if self._starttime:
stoptime = time.time()
logging.info('%fsec to perform %s',
stoptime - self._starttime, self._description)
self._starttime = None
class Xvfb(object):
"""Class to start and stop Xvfb if relevant. Nop if not Linux."""
def __init__(self):
self._pid = 0
def _IsLinux(self):
"""Return True if on Linux; else False."""
return sys.platform.startswith('linux')
def Start(self):
"""Start Xvfb and set an appropriate DISPLAY environment. Linux only.
Copied from tools/code_coverage/coverage_posix.py
"""
if not self._IsLinux():
return
proc = subprocess.Popen(['Xvfb', ':9', '-screen', '0', '1024x768x24',
'-ac'],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
self._pid = proc.pid
if not self._pid:
raise Exception('Could not start Xvfb')
os.environ['DISPLAY'] = ':9'
# Now confirm, giving a chance for it to start if needed.
for _ in range(10):
proc = subprocess.Popen('xdpyinfo >/dev/null', shell=True)
_, retcode = os.waitpid(proc.pid, 0)
if retcode == 0:
break
time.sleep(0.25)
if retcode != 0:
raise Exception('Could not confirm Xvfb happiness')
def Stop(self):
"""Stop Xvfb if needed. Linux only."""
if self._pid:
try:
os.kill(self._pid, signal.SIGKILL)
except:
pass
del os.environ['DISPLAY']
self._pid = 0
class TestSharder(BaseTestSharder):
"""Responsible for sharding the tests on the connected devices."""
def __init__(self, attached_devices, test_suite, gtest_filter,
test_arguments, timeout, cleanup_test_files, tool,
log_dump_name, fast_and_loose, build_type, in_webkit_checkout):
BaseTestSharder.__init__(self, attached_devices, build_type)
self.test_suite = test_suite
self.test_suite_basename = os.path.basename(test_suite)
self.gtest_filter = gtest_filter or ''
self.test_arguments = test_arguments
self.timeout = timeout
self.cleanup_test_files = cleanup_test_files
self.tool = tool
self.log_dump_name = log_dump_name
self.fast_and_loose = fast_and_loose
self.in_webkit_checkout = in_webkit_checkout
self.all_tests = []
if not self.gtest_filter:
# No filter has been specified, let's add all tests then.
self.all_tests, self.attached_devices = self._GetAllEnabledTests()
self.tests = self.all_tests
def _GetAllEnabledTests(self):
"""Get all enabled tests and available devices.
Obtains a list of enabled tests from the test package on the device,
then filters it again using the diabled list on the host.
Returns:
Tuple of (all enabled tests, available devices).
Raises Exception if all devices failed.
"""
# TODO(frankf): This method is doing too much in a non-systematic way.
# If the intention is to drop flaky devices, why not go through all devices
# instead of breaking on the first succesfull run?
available_devices = list(self.attached_devices)
while available_devices:
try:
return (self._GetTestsFromDevice(available_devices[-1]),
available_devices)
except Exception as e:
logging.warning('Failed obtaining tests from %s %s',
available_devices[-1], e)
available_devices.pop()
raise Exception('No device available to get the list of tests.')
def _GetTestsFromDevice(self, device):
logging.info('Obtaining tests from %s', device)
test_runner = SingleTestRunner(
device,
self.test_suite,
self.gtest_filter,
self.test_arguments,
self.timeout,
self.cleanup_test_files,
self.tool,
0,
not not self.log_dump_name,
self.fast_and_loose,
self.build_type,
self.in_webkit_checkout)
# The executable/apk needs to be copied before we can call GetAllTests.
test_runner.test_package.StripAndCopyExecutable()
all_tests = test_runner.test_package.GetAllTests()
disabled_list = test_runner.GetDisabledTests()
# Only includes tests that do not have any match in the disabled list.
all_tests = filter(lambda t:
not any([fnmatch.fnmatch(t, disabled_pattern)
for disabled_pattern in disabled_list]),
all_tests)
return all_tests
def CreateShardedTestRunner(self, device, index):
"""Creates a suite-specific test runner.
Args:
device: Device serial where this shard will run.
index: Index of this device in the pool.
Returns:
A SingleTestRunner object.
"""
device_num = len(self.attached_devices)
shard_size = (len(self.tests) + device_num - 1) / device_num
shard_test_list = self.tests[index * shard_size : (index + 1) * shard_size]
test_filter = ':'.join(shard_test_list) + self.gtest_filter
return SingleTestRunner(
device,
self.test_suite,
test_filter,
self.test_arguments,
self.timeout,
self.cleanup_test_files, self.tool, index,
not not self.log_dump_name,
self.fast_and_loose,
self.build_type,
self.in_webkit_checkout)
def OnTestsCompleted(self, test_runners, test_results):
"""Notifies that we completed the tests."""
test_results.LogFull('Unit test', os.path.basename(self.test_suite),
self.build_type, self.all_tests)
test_results.PrintAnnotation()
if self.log_dump_name:
# Zip all debug info outputs into a file named by log_dump_name.
debug_info.GTestDebugInfo.ZipAndCleanResults(
os.path.join(
cmd_helper.OutDirectory.get(), self.build_type,
'debug_info_dumps'),
self.log_dump_name)
def _RunATestSuite(options):
"""Run a single test suite.
Helper for Dispatch() to allow stop/restart of the emulator across
test bundles. If using the emulator, we start it on entry and stop
it on exit.
Args:
options: options for running the tests.
Returns:
0 if successful, number of failing tests otherwise.
"""
step_name = os.path.basename(options.test_suite).replace('-debug.apk', '')
buildbot_report.PrintNamedStep(step_name)
attached_devices = []
buildbot_emulators = []
if options.use_emulator:
for n in range(options.emulator_count):
t = TimeProfile('Emulator launch %d' % n)
avd_name = None
if n > 0:
# Creates a temporary AVD for the extra emulators.
avd_name = 'run_tests_avd_%d' % n
buildbot_emulator = emulator.Emulator(avd_name, options.fast_and_loose)
buildbot_emulator.Launch(kill_all_emulators=n == 0)
t.Stop()
buildbot_emulators.append(buildbot_emulator)
attached_devices.append(buildbot_emulator.device)
# Wait for all emulators to boot completed.
map(lambda buildbot_emulator: buildbot_emulator.ConfirmLaunch(True),
buildbot_emulators)
elif options.test_device:
attached_devices = [options.test_device]
else:
attached_devices = android_commands.GetAttachedDevices()
if not attached_devices:
logging.critical('A device must be attached and online.')
buildbot_report.PrintError()
return 1
# Reset the test port allocation. It's important to do it before starting
# to dispatch any tests.
if not ports.ResetTestServerPortAllocation():
raise Exception('Failed to reset test server port.')
if options.gtest_filter:
logging.warning('Sharding is not possible with these configurations.')
attached_devices = [attached_devices[0]]
sharder = TestSharder(
attached_devices,
options.test_suite,
options.gtest_filter,
options.test_arguments,
options.timeout,
options.cleanup_test_files,
options.tool,
options.log_dump,
options.fast_and_loose,
options.build_type,
options.webkit)
test_results = sharder.RunShardedTests()
for buildbot_emulator in buildbot_emulators:
buildbot_emulator.Shutdown()
return len(test_results.failed)
def Dispatch(options):
"""Dispatches the tests, sharding if possible.
If options.use_emulator is True, all tests will be run in new emulator
instance.
Args:
options: options for running the tests.
Returns:
0 if successful, number of failing tests otherwise.
"""
if options.test_suite == 'help':
ListTestSuites()
return 0
if options.use_xvfb:
xvfb = Xvfb()
xvfb.Start()
all_test_suites = FullyQualifiedTestSuites(options.exe, options.test_suite,
options.build_type)
failures = 0
for suite in all_test_suites:
# Give each test suite its own copy of options.
test_options = copy.deepcopy(options)
test_options.test_suite = suite
failures += _RunATestSuite(test_options)
if options.use_xvfb:
xvfb.Stop()
return failures
def ListTestSuites():
"""Display a list of available test suites."""
print 'Available test suites are:'
for test_suite in _TEST_SUITES:
print test_suite
def main(argv):
option_parser = optparse.OptionParser()
test_options_parser.AddTestRunnerOptions(option_parser, default_timeout=0)
option_parser.add_option('-s', '--suite', dest='test_suite',
help='Executable name of the test suite to run '
'(use -s help to list them)')
option_parser.add_option('--out-directory', dest='out_directory',
help='Path to the out/ directory, irrespective of '
'the build type. Only for non-Chromium uses.')
option_parser.add_option('-d', '--device', dest='test_device',
help='Target device the test suite to run ')
option_parser.add_option('-f', '--gtest_filter', dest='gtest_filter',
help='gtest filter')
option_parser.add_option('-a', '--test_arguments', dest='test_arguments',
help='Additional arguments to pass to the test')
option_parser.add_option('-L', dest='log_dump',
help='file name of log dump, which will be put in '
'subfolder debug_info_dumps under the same '
'directory in where the test_suite exists.')
option_parser.add_option('-e', '--emulator', dest='use_emulator',
action='store_true',
help='Run tests in a new instance of emulator')
option_parser.add_option('-n', '--emulator_count',
type='int', default=1,
help='Number of emulators to launch for running the '
'tests.')
option_parser.add_option('-x', '--xvfb', dest='use_xvfb',
action='store_true',
help='Use Xvfb around tests (ignored if not Linux)')
option_parser.add_option('--webkit', action='store_true',
help='Run the tests from a WebKit checkout.')
option_parser.add_option('--fast', '--fast_and_loose', dest='fast_and_loose',
action='store_true',
help='Go faster (but be less stable), '
'for quick testing. Example: when tracking down '
'tests that hang to add to the disabled list, '
'there is no need to redeploy the test binary '
'or data to the device again. '
'Don\'t use on bots by default!')
option_parser.add_option('--repeat', dest='repeat', type='int',
default=2,
help='Repeat count on test timeout')
option_parser.add_option('--exit_code', action='store_true',
help='If set, the exit code will be total number '
'of failures.')
option_parser.add_option('--exe', action='store_true',
help='If set, use the exe test runner instead of '
'the APK.')
options, args = option_parser.parse_args(argv)
if len(args) > 1:
print 'Unknown argument:', args[1:]
option_parser.print_usage()
sys.exit(1)
run_tests_helper.SetLogLevel(options.verbose_count)
if options.out_directory:
cmd_helper.OutDirectory.set(options.out_directory)
if options.use_emulator:
emulator.DeleteAllTempAVDs()
failed_tests_count = Dispatch(options)
# Failures of individual test suites are communicated by printing a
# STEP_FAILURE message.
# Returning a success exit status also prevents the buildbot from incorrectly
# marking the last suite as failed if there were failures in other suites in
# the batch (this happens because the exit status is a sum of all failures
# from all suites, but the buildbot associates the exit status only with the
# most recent step).
if options.exit_code:
return failed_tests_count
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv))