blob: 6f8e1dad5b3b2942fe43665ed8ebd9fa12870113 [file] [log] [blame]
#!/usr/bin/env python
# Copyright 2013 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 through isolate_test_cases.py all the tests cases in a google-test
executable, grabs the failures and traces them to generate a new .isolate.
This scripts requires a .isolated file. This file is generated from a .isolate
file. You can use 'GYP_DEFINES=test_isolation_mode=check ninja foo_test_run' to
generate it.
"""
import json
import logging
import os
import subprocess
import sys
import tempfile
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if not ROOT_DIR in sys.path:
sys.path.insert(0, ROOT_DIR)
import isolate
import isolate_test_cases
import run_test_cases
from utils import tools
def with_tempfile(function):
"""Creates a temporary file and calls the inner function."""
def hook(*args, **kwargs):
handle, tempfilepath = tempfile.mkstemp(prefix='fix_test_cases')
os.close(handle)
try:
return function(tempfilepath, *args, **kwargs)
finally:
try:
os.remove(tempfilepath)
except OSError, e:
print >> sys.stderr, 'Failed to remove %s: %s' % (tempfilepath, e)
return hook
def load_run_test_cases_results(run_test_cases_file):
"""Loads a .run_test_cases result file.
Returns a tuple of two lists, (success, failures).
"""
if not os.path.isfile(run_test_cases_file):
print >> sys.stderr, 'Failed to find %s' % run_test_cases_file
return None, None
with open(run_test_cases_file) as f:
try:
data = json.load(f)
except ValueError as e:
print >> sys.stderr, ('Unable to load json file, %s: %s' %
(run_test_cases_file, str(e)))
return None, None
failure = [
test for test, runs in data['test_cases'].iteritems()
if not any(run['returncode'] == 0 for run in runs)
]
success = [
test for test, runs in data['test_cases'].iteritems()
if any(run['returncode'] == 0 for run in runs)
]
return success, failure
def add_verbosity(cmd, verbosity):
"""Adds --verbose flags to |cmd| depending on verbosity."""
if verbosity:
cmd.append('--verbose')
if verbosity > 1:
cmd.append('--verbose')
# This function requires 2 temporary files.
@with_tempfile
@with_tempfile
def run_tests(
tempfilepath_cases, tempfilepath_result, isolated, test_cases, verbosity):
"""Runs all the test cases in an isolated environment."""
with open(tempfilepath_cases, 'w') as f:
f.write('\n'.join(test_cases))
cmd = [
sys.executable, os.path.join(ROOT_DIR, 'isolate.py'),
'run',
'--isolated', isolated,
]
# Make sure isolate.py is verbose.
add_verbosity(cmd, verbosity)
cmd += [
'--',
# This assumes run_test_cases.py is used.
'--result', tempfilepath_result,
'--test-case-file', tempfilepath_cases,
# Do not retry; it's faster to trace flaky test than retrying each failing
# tests 3 times.
# Do not use --run-all, iterate multiple times instead.
'--retries', '0',
# Trace at most 25 test cases at a time. While this may seem particularly
# small, it is because the tracer doesn't scale well on Windows and tends to
# generate multi-gigabytes data files, that needs to be read N-times the
# number of test cases. On linux and OSX it's not that much a big deal but
# if there's 25 test cases that are broken, it's likely that there's a core
# file missing anyway.
'--max-failures', '25',
]
# Make sure run_test_cases.py is verbose.
add_verbosity(cmd, verbosity)
logging.debug(cmd)
retcode = subprocess.call(cmd)
success, failures = load_run_test_cases_results(tempfilepath_result)
# Returning non-zero must match having failures.
assert bool(retcode) == (failures is None or bool(failures))
return success, failures
@with_tempfile
def trace_some(tempfilepath, isolated, test_cases, trace_blacklist, verbosity):
"""Traces the test cases."""
with open(tempfilepath, 'w') as f:
f.write('\n'.join(test_cases))
cmd = [
sys.executable, os.path.join(
ROOT_DIR, 'googletest', 'isolate_test_cases.py'),
'--isolated', isolated,
'--test-case-file', tempfilepath,
# Do not use --run-all here, we assume the test cases will pass inside the
# checkout.
]
for i in trace_blacklist:
cmd.extend(('--trace-blacklist', i))
add_verbosity(cmd, verbosity)
logging.debug(cmd)
return subprocess.call(cmd)
def fix_all(isolated, all_test_cases, trace_blacklist, verbosity):
"""Runs all the test cases in a gtest executable and trace the failing tests.
Returns True on success.
Makes sure the test passes afterward.
"""
# These environment variables could have adverse side-effects.
# TODO(maruel): Be more intelligent about it, for now be safe.
env_blacklist = set(run_test_cases.KNOWN_GTEST_ENV_VARS) - set([
'GTEST_SHARD_INDEX', 'GTEST_TOTAL_SHARDS'])
for i in env_blacklist:
if i in os.environ:
print >> sys.stderr, 'Please unset %s' % i
return False
# Run until test cases remain to be tested.
remaining_test_cases = all_test_cases[:]
if not remaining_test_cases:
print >> sys.stderr, 'Didn\'t find any test case to run'
return 1
previous_failures = set()
had_failure = False
while remaining_test_cases:
# pylint is confused about with_tempfile.
# pylint: disable=E1120
print(
'\nTotal: %5d; Remaining: %5d' % (
len(all_test_cases), len(remaining_test_cases)))
success, failures = run_tests(isolated, remaining_test_cases, verbosity)
if success is None:
if had_failure:
print >> sys.stderr, 'Failed to run test cases'
return 1
# Maybe there's even enough things mapped to start the child process.
logging.info('Failed to run, trace one test case.')
had_failure = True
success = []
failures = [remaining_test_cases[0]]
print(
'\nTotal: %5d; Tried to run: %5d; Ran: %5d; Succeeded: %5d; Failed: %5d'
% (
len(all_test_cases),
len(remaining_test_cases),
len(success) + len(failures),
len(success),
len(failures)))
if not failures:
print('I\'m done. Have a nice day!')
return True
previous_failures.difference_update(success)
# If all the failures had already failed at least once.
if previous_failures.issuperset(failures):
print('The last trace didn\'t help, aborting.')
return False
# Test cases that passed to not need to be retried anymore.
remaining_test_cases = [
i for i in remaining_test_cases if i not in success and i not in failures
]
# Make sure the failures at put at the end. This way if some tests fails
# simply because they are broken, and not because of test isolation, the
# other tests will still be traced.
remaining_test_cases.extend(failures)
# Trace the test cases and update the .isolate file.
print('\nTracing the %d failing tests.' % len(failures))
if trace_some(isolated, failures, trace_blacklist, verbosity):
logging.info('The tracing itself failed.')
return False
previous_failures.update(failures)
def main():
tools.disable_buffering()
parser = run_test_cases.OptionParserTestCases(
usage='%prog <options> -s <something.isolated>')
isolate.add_trace_option(parser)
parser.add_option(
'-s', '--isolated',
help='The isolated file')
options, args = parser.parse_args()
if args:
parser.error('Unsupported arg: %s' % args)
isolate.parse_isolated_option(parser, options, os.getcwd(), True)
_, command, test_cases = isolate_test_cases.safely_load_isolated(
parser, options)
if not command:
parser.error('A command must be defined')
if not test_cases:
parser.error('No test case to run')
return not fix_all(
options.isolated, test_cases, options.trace_blacklist, options.verbose)
if __name__ == '__main__':
sys.exit(main())