blob: a9c13e50ebdc0164db186dd2b22236e3fb5b37dd [file]
#!/usr/bin/env python
# Copyright 2014 The LUCI Authors. All rights reserved.
# Use of this source code is governed under the Apache License, Version 2.0
# that can be found in the LICENSE file.
"""Runs the coverage tool on unit tests in %s and prints out report.
The tests are run in parallel for maximum efficiency.
"""
import optparse
import os
import socket
import subprocess
import sys
import tempfile
import time
# When run directly, runs the components/ unit tests.
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def _get_tests(root, blacklist):
"""Yields all the tests to run excluding smoke tests."""
for root, dirs, files in os.walk(root):
for d in dirs[:]:
if d.startswith('.') or d in blacklist:
dirs.remove(d)
for f in files:
if f.endswith('_test.py') and not f.endswith('_smoke_test.py'):
yield os.path.join(root, f)
def _run_coverage(root, args):
"""Returns True on success, e.g. if exit code is 0."""
subprocess.check_call(['coverage'] + args, cwd=root)
def _start_coverage(root, args):
"""Returns a Popen instance."""
return subprocess.Popen(
['coverage'] + args, cwd=root,
stderr=subprocess.STDOUT, stdout=subprocess.PIPE)
def main(root, blacklist, omit):
parser = optparse.OptionParser(
description=sys.modules[__name__].__doc__ % root)
parser.add_option(
'-H', '--html',
help='Generates HTML report to this directory instead of printing it out')
options, args = parser.parse_args()
if args:
parser.error('Unknown args: %s' % args)
try:
_run_coverage(root, ['erase'])
except OSError:
print >> sys.stderr, (
'Please install coverage first.\n'
'See http://nedbatchelder.com/code/coverage for more details.')
return 2
# http://nedbatchelder.com/code/coverage/config.html
rcfile_content = (
'[report]',
'exclude_lines =',
r' pragma: no cover',
r' def __repr__\(',
r' raise NotImplementedError\(',
r' if False:',
r' unittest\.TestCase\.maxDiff = None',
r' (self|test)\.fail\(',
'precision = 1',
)
h, rcfile = tempfile.mkstemp(prefix='coverage', suffix='.rc')
os.close(h)
try:
with open(rcfile, 'wb') as f:
f.write('\n'.join(rcfile_content) + '\n')
flags = [
'run',
'--parallel-mode',
'--omit=' + omit,
'--rcfile=' + rcfile,
'--source=' + root,
]
start = time.time()
processes = {
_start_coverage(root, flags + [test]): None
for test in _get_tests(root, blacklist)
}
if not processes:
print('Failed to find any test in %s' % root)
return 1
# Poll for the processes to complete.
while not all(v is not None for v in processes.itervalues()):
for proc, v in processes.iteritems():
if v is None:
# TODO(maruel): The test may hang if the stdout pipe becomes full. Fix
# if it becomes an issue in practice (it hasn't here) and the speed
# improvement is definitely worth it.
processes[proc] = proc.poll()
if processes[proc] is not None:
# Empty the pipe in any case.
out = proc.communicate()[0]
if processes[proc]:
sys.stdout.write(out)
else:
sys.stdout.write('.')
sys.stdout.flush()
time.sleep(0.1)
end = time.time()
results = [not v for v in processes.itervalues()]
print(
'\n%d out of %d tests succeeded in %.2fs.\n' %
(sum(results), len(results), end-start))
_run_coverage(root, ['combine', '--rcfile=' + rcfile])
if options.html:
full_path = os.path.normpath(os.path.join(root, options.html))
args = [
'html',
'--directory', os.path.relpath(full_path, root),
'--rcfile=' + rcfile,
]
_run_coverage(root, args)
rel_path = os.path.relpath(full_path, os.getcwd())
if rel_path.startswith('..'):
print('First run "cd %s"' % root)
rel_path = options.html
print('Start a web browser with "python -m SimpleHTTPServer"')
print(
'Then point your web browser at http://%s:8000/%s' %
(socket.getfqdn(), rel_path.replace(os.path.sep, '/')))
else:
args = ['report', '-m', '--rcfile=' + rcfile]
_run_coverage(root, args)
print('To generate HTML report, use "coverage html -d out"')
# Pass out the number of tests that failed as exit code.
return len(results) - sum(results)
finally:
os.remove(rcfile)
if __name__ == '__main__':
sys.exit(main(
ROOT_DIR,
('third_party', 'tools'),
'PRESUBMIT.py,third_party/*,tools/*'))