blob: 6c98d5fe7e41247c216c45930f29ec43c43c5abc [file] [log] [blame]
#!/usr/bin/env python
# Copyright 2014 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 Android integration tests/suites.
Dynamically loads all the config.py files in the source code directories,
specifically looking for ones that define a function:
def get_integration_test_runners():
return [...]
Each such function is expected to return a list of test runner objects which
extend SuiteRunnerBase or one of its subclasses to implement the logic for
running that test suite.
"""
import argparse
import collections
import multiprocessing
import os
import subprocess
import sys
import build_common
import dashboard_submit
import util.test.suite_results
from build_options import OPTIONS
from cts import expected_driver_times
from util import color
from util import concurrent
from util import debug
from util import file_util
from util import logging_util
from util import platform_util
from util import remote_executor
from util.test import scoreboard_constants
from util.test import suite_results
from util.test import suite_runner
from util.test import suite_runner_config
from util.test import test_driver
from util.test import test_filter
_BOT_TEST_SUITE_MAX_RETRY_COUNT = 5
_DEFINITIONS_ROOT = 'src/integration_tests/definitions'
_EXPECTATIONS_ROOT = 'src/integration_tests/expectations'
_TEST_METHOD_MAX_RETRY_COUNT = 5
_REPORT_COLOR_FOR_SUITE_EXPECTATION = {
scoreboard_constants.SKIPPED: color.MAGENTA,
scoreboard_constants.EXPECT_FAIL: color.RED,
scoreboard_constants.FLAKE: color.CYAN,
scoreboard_constants.EXPECT_PASS: color.GREEN,
}
def get_all_suite_runners(on_bot, use_gpu):
"""Gets all the suites defined in the various config.py files."""
sys.path.insert(0, 'src')
result = suite_runner_config.load_from_suite_definitions(
_DEFINITIONS_ROOT, _EXPECTATIONS_ROOT, on_bot, use_gpu)
# Check name duplication.
counter = collections.Counter(runner.name for runner in result)
dupped_name_list = [name for name, count in counter.iteritems() if count > 1]
assert not dupped_name_list, (
'Following tests are multiply defined:\n' +
'\n'.join(sorted(dupped_name_list)))
return result
def get_dependencies_for_integration_tests():
"""Gets the paths of all python files needed for the integration tests."""
deps = []
root_path = build_common.get_arc_root()
# We assume that the currently loaded modules is the set needed to run
# the integration tests. We just narrow the list down to the set of files
# contained in the project root directory.
for module in sys.modules.itervalues():
# Filter out built-ins and other special cases
if not module or not hasattr(module, '__file__'):
continue
module_path = module.__file__
# Filter out modules external to the project
if not module_path or not module_path.startswith(root_path):
continue
# Convert references to .pyc files to .py files.
if module_path.endswith('.pyc'):
module_path = module_path[:-1]
deps.append(module_path[len(root_path) + 1:])
return deps
def _select_tests_to_run(all_suite_runners, args):
test_list_filter = test_filter.TestListFilter(
include_pattern_list=args.include_patterns,
exclude_pattern_list=args.exclude_patterns)
test_run_filter = test_filter.TestRunFilter(
include_fail=args.include_failing,
include_large=args.include_large,
include_timeout=args.include_timeouts)
test_driver_list = []
for runner in all_suite_runners:
# Form a list of selected tests for this suite, by forming a fully qualified
# name as "<suite-name>:<test-name>", which is what the patterns given on
# as command line arguments expect to match.
tests_to_run = []
updated_suite_test_expectations = {}
is_runnable = runner.is_runnable()
for test_name, test_expectation in (
runner.expectation_map.iteritems()):
# Check if the test is selected.
if not test_list_filter.should_include(
'%s:%s' % (runner.name, test_name)):
continue
# Add this test and its updated expectation to the dictionary of all
# selected tests. We do this even if just below we do not decide we can
# actually run the test, as otherwise we do not know if it was skipped or
# not.
updated_suite_test_expectations[test_name] = test_expectation
# Exclude tests either because the suite is not runnable, or because each
# individual test is not runnable.
if (not is_runnable or not test_run_filter.should_run(test_expectation)):
continue
tests_to_run.append(test_name)
# If no tests from this suite were selected, continue with the next suite.
if not updated_suite_test_expectations:
continue
# Create TestDriver to run the test suite with setting test expectations.
test_driver_list.append(test_driver.TestDriver(
runner, updated_suite_test_expectations, tests_to_run,
_TEST_METHOD_MAX_RETRY_COUNT if not args.keep_running else sys.maxint,
stop_on_unexpected_failures=not args.keep_running))
def sort_keys(driver):
# Take the negative time to sort descending, while otherwise sorting by name
# ascending.
return (-expected_driver_times.get_expected_driver_time(driver),
driver.name)
return sorted(test_driver_list, key=sort_keys)
def _get_test_driver_list(args):
all_suite_runners = get_all_suite_runners(args.buildbot, not args.use_xvfb)
return _select_tests_to_run(all_suite_runners, args)
def _run_driver(driver, args, prepare_only):
"""Runs a single suite."""
try:
# TODO(mazda): Move preparation into its own parallel pass. It's confusing
# running during something called "run_single_suite".
if not args.noprepare:
driver.prepare(args)
# Run the suite locally when not being invoked for remote execution.
if not prepare_only:
driver.run(args)
finally:
driver.finalize(args)
def _shutdown_unfinished_drivers_gracefully(not_done, test_driver_list):
"""Kills unfinished concurrent test drivers as gracefully as possible."""
# Prevent new tasks from running.
not_cancelled = []
for future in not_done:
# Note: Running tasks cannot be cancelled.
if not future.cancel():
not_cancelled.append(future)
# We try to terminate first, as ./launch_chrome will respond by shutting
# down the chrome process cleanly. The later kill() call will not do so,
# and could leave a running process behind. At this point, consider any
# tests that have not finished as incomplete.
for driver in test_driver_list:
driver.terminate()
# Give everyone ten seconds to terminate.
_, not_cancelled = concurrent.wait(not_cancelled, 10)
if not not_cancelled:
return
# There still remain some running tasks. Kill them.
for driver in test_driver_list:
driver.kill()
concurrent.wait(not_cancelled, 5)
def _run_suites(test_driver_list, args, prepare_only=False):
"""Runs the indicated suites."""
_prepare_output_directory(args)
util.test.suite_results.initialize(test_driver_list, args, prepare_only)
if not test_driver_list:
return False
timeout = (
args.total_timeout if args.total_timeout and not prepare_only else None)
try:
with concurrent.ThreadPoolExecutor(args.jobs, daemon=True) as executor:
futures = [executor.submit(_run_driver, driver, args, prepare_only)
for driver in test_driver_list]
done, not_done = concurrent.wait(futures, timeout,
concurrent.FIRST_EXCEPTION)
try:
# Iterate over the results to propagate an exception if any of the tasks
# aborted by an error in the test drivers. Since such an error is due to
# broken script rather than normal failure in tests, we prefer just to
# die similarly as when Python errors occurred in the main thread.
for future in done:
future.result()
# No exception was raised but some timed-out tasks are remaining.
if not_done:
print '@@@STEP_TEXT@Integration test timed out@@@'
debug.write_frames(sys.stdout)
if args.warn_on_failure:
print '@@@STEP_WARNINGS@@@'
else:
print '@@@STEP_FAILURE@@@'
return False
# All tests passed (or failed) in time.
return True
finally:
if not_done:
_shutdown_unfinished_drivers_gracefully(not_done, test_driver_list)
finally:
for driver in test_driver_list:
driver.finalize(args)
def prepare_suites(args):
test_driver_list = _get_test_driver_list(args)
return _run_suites(test_driver_list, args, prepare_only=True)
def list_fully_qualified_test_names(scoreboards, args):
output = []
for scoreboard in scoreboards:
suite_name = scoreboard.name
for test_name, expectation in scoreboard.get_expectations().iteritems():
output.append(('%s:%s' % (suite_name, test_name), expectation))
output.sort()
for qualified_test_name, expectation in output:
color.write_ansi_escape(
sys.stdout, _REPORT_COLOR_FOR_SUITE_EXPECTATION[expectation],
qualified_test_name + '\n')
def print_chrome_version():
assert not platform_util.is_running_on_cygwin(), (
'Chrome on Windows does not support --version option.')
chrome_path = remote_executor.get_chrome_exe_path()
chrome_version = subprocess.check_output([chrome_path, '--version']).rstrip()
print '@@@STEP_TEXT@%s<br/>@@@' % (chrome_version)
def parse_args(args):
description = 'Runs integration tests, verifying they pass.'
parser = argparse.ArgumentParser(description=description)
parser.add_argument('--buildbot', action='store_true', help='Run tests '
'for the buildbot.')
parser.add_argument('--ansi', action='store_true',
help='Color output using ansi escape sequence')
parser.add_argument('--cts-bot', action='store_true',
help='Run with CTS bot specific config.')
parser.add_argument('--enable-osmesa', action='store_true',
help=('This flag wlll be passed to launch_chome '
'to control GL emulation with OSMesa.'))
parser.add_argument('--include-failing', action='store_true',
help='Include tests which are expected to fail.')
parser.add_argument('--include-large', action='store_true',
help=('Include large tests that may take a long time to '
'run.'))
parser.add_argument('--include-timeouts', action='store_true',
help='Include tests which are expected to timeout.')
parser.add_argument('-j', '--jobs', metavar='N', type=int,
default=min(10, multiprocessing.cpu_count()),
help='Run N tests at once.')
parser.add_argument('--keep-running', action='store_true',
help=('Attempt to recover from unclean failures. '
'Sacrifices failing quickly for complete results. '
''))
parser.add_argument('--launch-chrome-opt', action='append', default=[],
dest='launch_chrome_opts', metavar='OPTIONS',
help=('An Option to pass on to launch_chrome. Repeat as '
'needed for any options to pass on.'))
parser.add_argument('--list', action='store_true',
help=('List the fully qualified names of tests. '
'Can be used with -t and --include-* flags.'))
parser.add_argument('--max-deadline', '--max-timeout',
metavar='T', default=0, type=int,
help=('Maximum deadline for browser tests. The test '
'configuration deadlines are used by default.'))
parser.add_argument('--min-deadline', '--min-timeout',
metavar='T', default=0, type=int,
help=('Minimum deadline for browser tests. The test '
'configuration deadlines are used by default.'))
parser.add_argument('--noninja', action='store_false',
default=True, dest='run_ninja',
help='Do not run ninja before running any tests.')
parser.add_argument('--noprepare', action='store_true',
help='Do not run the suite prepare step - useful for '
'running integration tests from an archived test '
'bundle.')
parser.add_argument('-o', '--output-dir', metavar='DIR',
help='Specify the directory to store test ouput files.')
parser.add_argument('--plan-report', action='store_true',
help=('Generate a report of all tests based on their '
'currently configured expectation of success.'))
parser.add_argument('-q', '--quiet', action='store_true',
help='Do not show passing tests and expected failures.')
parser.add_argument('--stop', action='store_true',
help=('Stops running tests immediately when a failure '
'is reported.'))
parser.add_argument('-t', '--include', action='append',
dest='include_patterns', default=[], metavar='PATTERN',
help=('Identifies tests to include, using shell '
'style glob patterns. For example dalvik.*'))
parser.add_argument('--times', metavar='N',
default=1, type=int, dest='repeat_runs',
help='Runs each test N times.')
parser.add_argument('--total-timeout', metavar='T', default=0, type=int,
help=('If specified, this script stops after running '
'this seconds.'))
parser.add_argument('--use-xvfb', action='store_true', help='Use xvfb-run '
'when launching tests. Used by buildbots.')
parser.add_argument('-v', '--verbose', action='store_const', const='verbose',
dest='output', help='Verbose output.')
parser.add_argument('--warn-on-failure', action='store_true',
help=('Indicates that failing tests should become '
'warnings rather than errors.'))
parser.add_argument('-x', '--exclude', action='append',
dest='exclude_patterns', default=[], metavar='PATTERN',
help=('Identifies tests to exclude, using shell '
'style glob patterns. For example dalvik.tests.*'))
remote_executor.add_remote_arguments(parser)
args = parser.parse_args(args)
remote_executor.maybe_detect_remote_host_type(args)
return args
def set_test_global_state(args):
# Set/reset any global state involved in running the tests.
# These settings need to be made for consistency, and allow the test framework
# itself to be tested.
retry_count = 0
if args.buildbot:
retry_count = _BOT_TEST_SUITE_MAX_RETRY_COUNT
if args.keep_running:
retry_count = sys.maxint
test_driver.TestDriver.set_global_retry_timeout_run_count(retry_count)
def _prepare_output_directory(args):
if args.output_dir:
suite_runner.SuiteRunnerBase.set_output_directory(args.output_dir)
if os.path.exists(suite_runner.SuiteRunnerBase.get_output_directory()):
file_util.rmtree(suite_runner.SuiteRunnerBase.get_output_directory())
file_util.makedirs_safely(
suite_runner.SuiteRunnerBase.get_output_directory())
def _run_suites_and_output_results_remote(args, raw_args):
"""Runs test suite on remote host, and returns the status code on exit.
First of all, this runs "prepare" locally, to set up some files for testing.
Then, sends command to run test on remote host.
Returns the status code of the program. Specifically, 0 on success.
"""
if not prepare_suites(args):
return 1
raw_args.append('--noprepare')
run_result = remote_executor.run_remote_integration_tests(
args, raw_args, get_dependencies_for_integration_tests())
return run_result if not args.warn_on_failure else 0
def _run_suites_and_output_results_local(test_driver_list, args):
"""Runs integration tests locally and returns the status code on exit."""
run_result = _run_suites(test_driver_list, args)
test_failed, passed, total = util.test.suite_results.summarize()
if args.cts_bot:
if total > 0:
dashboard_submit.queue_data('cts', 'count', {
'passed': passed,
'total': total,
})
dashboard_submit.queue_data('cts%', 'coverage%', {
'passed': passed * 100. / total,
})
# If failures are only advisory, we always return zero.
if args.warn_on_failure:
return 0
return 0 if run_result and not test_failed else 1
def _process_args(raw_args):
args = parse_args(raw_args)
logging_util.setup(verbose=(args.output == 'verbose'))
OPTIONS.parse_configure_file()
# Limit to one job at a time when running a suite multiple times. Otherwise
# suites start interfering with each others operations and bad things happen.
if args.repeat_runs > 1:
args.jobs = 1
if args.buildbot and OPTIONS.weird():
args.exclude_patterns.append('cts.*')
# Fixup all patterns to at least be a prefix match for all tests.
# This allows options like "-t cts.CtsHardwareTestCases" to work to select all
# the tests in the suite.
args.include_patterns = [(pattern if '*' in pattern else (pattern + '*'))
for pattern in args.include_patterns]
args.exclude_patterns = [(pattern if '*' in pattern else (pattern + '*'))
for pattern in args.exclude_patterns]
set_test_global_state(args)
if (not args.remote and args.buildbot and
not platform_util.is_running_on_cygwin()):
print_chrome_version()
if platform_util.is_running_on_remote_host():
args.run_ninja = False
return args
def main(raw_args):
args = _process_args(raw_args)
if args.run_ninja:
build_common.run_ninja()
test_driver_list = []
for n in xrange(args.repeat_runs):
test_driver_list.extend(_get_test_driver_list(args))
if args.plan_report:
util.test.suite_results.initialize(test_driver_list, args, False)
suite_results.report_expected_results(
driver.scoreboard for driver in sorted(test_driver_list,
key=lambda driver: driver.name))
return 0
elif args.list:
list_fully_qualified_test_names(
(driver.scoreboard for driver in test_driver_list), args)
return 0
elif args.remote:
return _run_suites_and_output_results_remote(args, raw_args)
else:
return _run_suites_and_output_results_local(test_driver_list, args)
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))