blob: 66dbe07b057477c561afb13fae85988db14c7056 [file] [log] [blame] [edit]
#!/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.
"""Traces each test cases of a google-test executable individually.
Gives detailed information about each test case. The logs can be read afterward
with ./trace_inputs.py read -l /path/to/executable.logs
"""
import logging
import multiprocessing
import os
import sys
import time
import isolate # TODO(maruel): Remove references to isolate.
import run_test_cases
import trace_inputs
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
ROOT_DIR = os.path.dirname(os.path.dirname(BASE_DIR))
class Tracer(object):
def __init__(self, tracer, executable, cwd_dir, progress):
# Constants
self.tracer = tracer
self.executable = executable
self.cwd_dir = cwd_dir
self.progress = progress
def map(self, test_case):
"""Traces a single test case and returns its output."""
cmd = [self.executable, '--gtest_filter=%s' % test_case]
cmd = run_test_cases.fix_python_path(cmd)
tracename = test_case.replace('/', '-')
out = []
for retry in range(5):
start = time.time()
returncode, output = self.tracer.trace(
cmd, self.cwd_dir, tracename, True)
duration = time.time() - start
# TODO(maruel): Define a way to detect if an strace log is valid.
valid = True
out.append(
{
'test_case': test_case,
'returncode': returncode,
'duration': duration,
'valid': valid,
'output': output,
})
logging.debug(
'Tracing %s done: %d, %.1fs' % (test_case, returncode, duration))
if retry:
self.progress.update_item(
'%s - %d' % (test_case, retry), True, not valid)
else:
self.progress.update_item(test_case, True, not valid)
if valid:
break
return out
def trace_test_cases(
executable, root_dir, cwd_dir, variables, test_cases, jobs, output_file):
"""Traces test cases one by one."""
assert not os.path.isabs(cwd_dir)
assert os.path.isabs(root_dir) and os.path.isdir(root_dir)
assert os.path.isfile(executable) and os.path.isabs(executable)
if not test_cases:
return 0
# Resolve any symlink.
root_dir = os.path.realpath(root_dir)
full_cwd_dir = os.path.normpath(os.path.join(root_dir, cwd_dir))
assert os.path.isdir(full_cwd_dir)
logname = output_file + '.logs'
progress = run_test_cases.Progress(len(test_cases))
with run_test_cases.ThreadPool(jobs or multiprocessing.cpu_count()) as pool:
api = trace_inputs.get_api()
api.clean_trace(logname)
with api.get_tracer(logname) as tracer:
function = Tracer(tracer, executable, full_cwd_dir, progress).map
for test_case in test_cases:
pool.add_task(function, test_case)
values = pool.join(progress, 0.1)
print ''
print '%.1fs Done post-processing logs. Parsing logs.' % (
time.time() - progress.start)
results = api.parse_log(logname, isolate.default_blacklist)
print '%.1fs Done parsing logs.' % (
time.time() - progress.start)
# Strips to root_dir.
results_processed = {}
for item in results:
if 'results' in item:
item = item.copy()
item['results'] = item['results'].strip_root(root_dir)
results_processed[item['trace']] = item
else:
print >> sys.stderr, 'Got exception while tracing %s: %s' % (
item['trace'], item['exception'])
print '%.1fs Done stripping root.' % (
time.time() - progress.start)
# Flatten.
flattened = {}
for item_list in values:
for item in item_list:
if item['valid']:
test_case = item['test_case']
tracename = test_case.replace('/', '-')
flattened[test_case] = results_processed[tracename].copy()
item_results = flattened[test_case]['results']
tracked, touched = isolate.split_touched(item_results.existent)
flattened[test_case].update({
'processes': len(list(item_results.process.all)),
'results': item_results.flatten(),
'duration': item['duration'],
'returncode': item['returncode'],
'valid': item['valid'],
'variables':
isolate.generate_simplified(
tracked,
[],
touched,
root_dir,
variables,
cwd_dir),
})
del flattened[test_case]['trace']
print '%.1fs Done flattening.' % (
time.time() - progress.start)
# Make it dense if there is more than 20 results.
trace_inputs.write_json(
output_file,
flattened,
False)
# Also write the .isolate file.
# First, get all the files from all results. Use a map to remove dupes.
files = {}
for item in results_processed.itervalues():
files.update((f.full_path, f) for f in item['results'].existent)
# Convert back to a list, discard the keys.
files = files.values()
tracked, touched = isolate.split_touched(files)
value = isolate.generate_isolate(
tracked,
[],
touched,
root_dir,
variables,
cwd_dir)
with open('%s.isolate' % output_file, 'wb') as f:
isolate.pretty_print(value, f)
return 0
def main():
"""CLI frontend to validate arguments."""
default_variables = [('OS', isolate.get_flavor())]
if sys.platform in ('win32', 'cygwin'):
default_variables.append(('EXECUTABLE_SUFFIX', '.exe'))
else:
default_variables.append(('EXECUTABLE_SUFFIX', ''))
parser = run_test_cases.OptionParserTestCases(
usage='%prog <options> [gtest]',
description=sys.modules['__main__'].__doc__)
parser.format_description = lambda *_: parser.description
parser.add_option(
'-c', '--cwd',
default='',
help='Signal to start the process from this relative directory. When '
'specified, outputs the inputs files in a way compatible for '
'gyp processing. Should be set to the relative path containing the '
'gyp file, e.g. \'chrome\' or \'net\'')
parser.add_option(
'-V', '--variable',
nargs=2,
action='append',
default=default_variables,
dest='variables',
metavar='FOO BAR',
help='Variables to process in the .isolate file, default: %default')
parser.add_option(
'--root-dir',
default=ROOT_DIR,
help='Root directory to base everything off. Default: %default')
parser.add_option(
'-o', '--out',
help='output file, defaults to <executable>.test_cases')
options, args = parser.parse_args()
if len(args) != 1:
parser.error(
'Please provide the executable line to run, if you need fancy things '
'like xvfb, start this script from *inside* xvfb, it\'ll be much faster'
'.')
options.root_dir = os.path.abspath(options.root_dir)
if not os.path.isdir(options.root_dir):
parser.error('--root-dir "%s" must exist' % options.root_dir)
if not os.path.isdir(os.path.join(options.root_dir, options.cwd)):
parser.error(
'--cwd "%s" must be an existing directory relative to %s' %
(options.cwd, options.root_dir))
executable = args[0]
if not os.path.isabs(executable):
executable = os.path.abspath(os.path.join(options.root_dir, executable))
if not os.path.isfile(executable):
parser.error('"%s" doesn\'t exist.' % executable)
if not options.out:
options.out = '%s.test_cases' % executable
test_cases = parser.process_gtest_options(executable, options)
# Then run them.
return trace_test_cases(
executable,
options.root_dir,
options.cwd,
dict(options.variables),
test_cases,
options.jobs,
# TODO(maruel): options.timeout,
options.out)
if __name__ == '__main__':
sys.exit(main())