2016-11-30 06:21:53 +01:00
|
|
|
"""Code coverage utilities."""
|
|
|
|
|
|
|
|
from __future__ import absolute_import, print_function
|
|
|
|
|
|
|
|
import os
|
|
|
|
import re
|
|
|
|
|
2017-01-19 01:31:34 +01:00
|
|
|
from lib.target import (
|
|
|
|
walk_module_targets,
|
2017-05-15 18:51:49 +02:00
|
|
|
walk_compile_targets,
|
2017-01-19 01:31:34 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
from lib.util import (
|
|
|
|
display,
|
|
|
|
ApplicationError,
|
|
|
|
EnvironmentConfig,
|
|
|
|
run_command,
|
2017-05-15 11:42:11 +02:00
|
|
|
common_environment,
|
2017-01-19 01:31:34 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
from lib.executor import (
|
|
|
|
Delegate,
|
|
|
|
install_command_requirements,
|
|
|
|
)
|
2016-11-30 06:21:53 +01:00
|
|
|
|
|
|
|
COVERAGE_DIR = 'test/results/coverage'
|
|
|
|
COVERAGE_FILE = os.path.join(COVERAGE_DIR, 'coverage')
|
2017-05-15 11:42:11 +02:00
|
|
|
COVERAGE_GROUPS = ('command', 'target', 'environment', 'version')
|
2016-11-30 06:21:53 +01:00
|
|
|
|
|
|
|
|
|
|
|
def command_coverage_combine(args):
|
|
|
|
"""Patch paths in coverage files and merge into a single file.
|
|
|
|
:type args: CoverageConfig
|
2017-05-15 11:42:11 +02:00
|
|
|
:rtype: list[str]
|
2016-11-30 06:21:53 +01:00
|
|
|
"""
|
|
|
|
coverage = initialize_coverage(args)
|
|
|
|
|
|
|
|
modules = dict((t.module, t.path) for t in list(walk_module_targets()))
|
|
|
|
|
2017-05-11 07:25:02 +02:00
|
|
|
coverage_files = [os.path.join(COVERAGE_DIR, f) for f in os.listdir(COVERAGE_DIR) if '=coverage.' in f]
|
2016-11-30 06:21:53 +01:00
|
|
|
|
|
|
|
ansible_path = os.path.abspath('lib/ansible/') + '/'
|
|
|
|
root_path = os.getcwd() + '/'
|
|
|
|
|
2017-02-25 01:58:56 +01:00
|
|
|
counter = 0
|
2017-05-15 11:42:11 +02:00
|
|
|
groups = {}
|
2017-02-25 01:58:56 +01:00
|
|
|
|
2017-05-15 18:51:49 +02:00
|
|
|
if args.all or args.stub:
|
|
|
|
sources = sorted(os.path.abspath(target.path) for target in walk_compile_targets())
|
|
|
|
else:
|
|
|
|
sources = []
|
|
|
|
|
|
|
|
if args.stub:
|
|
|
|
groups['=stub'] = dict((source, set()) for source in sources)
|
|
|
|
|
2016-11-30 06:21:53 +01:00
|
|
|
for coverage_file in coverage_files:
|
2017-02-25 01:58:56 +01:00
|
|
|
counter += 1
|
|
|
|
display.info('[%4d/%4d] %s' % (counter, len(coverage_files), coverage_file), verbosity=2)
|
|
|
|
|
2016-11-30 06:21:53 +01:00
|
|
|
original = coverage.CoverageData()
|
|
|
|
|
2017-05-15 11:42:11 +02:00
|
|
|
group = get_coverage_group(args, coverage_file)
|
|
|
|
|
|
|
|
if group is None:
|
|
|
|
display.warning('Unexpected name for coverage file: %s' % coverage_file)
|
|
|
|
continue
|
|
|
|
|
2016-11-30 06:21:53 +01:00
|
|
|
if os.path.getsize(coverage_file) == 0:
|
|
|
|
display.warning('Empty coverage file: %s' % coverage_file)
|
|
|
|
continue
|
|
|
|
|
|
|
|
try:
|
|
|
|
original.read_file(coverage_file)
|
|
|
|
except Exception as ex: # pylint: disable=locally-disabled, broad-except
|
|
|
|
display.error(str(ex))
|
|
|
|
continue
|
|
|
|
|
|
|
|
for filename in original.measured_files():
|
2017-05-11 07:25:02 +02:00
|
|
|
arcs = set(original.arcs(filename) or [])
|
|
|
|
|
|
|
|
if not arcs:
|
|
|
|
# This is most likely due to using an unsupported version of coverage.
|
|
|
|
display.warning('No arcs found for "%s" in coverage file: %s' % (filename, coverage_file))
|
|
|
|
continue
|
2016-11-30 06:21:53 +01:00
|
|
|
|
|
|
|
if '/ansible_modlib.zip/ansible/' in filename:
|
|
|
|
new_name = re.sub('^.*/ansible_modlib.zip/ansible/', ansible_path, filename)
|
|
|
|
display.info('%s -> %s' % (filename, new_name), verbosity=3)
|
|
|
|
filename = new_name
|
|
|
|
elif '/ansible_module_' in filename:
|
|
|
|
module = re.sub('^.*/ansible_module_(?P<module>.*).py$', '\\g<module>', filename)
|
2017-05-11 07:25:02 +02:00
|
|
|
if module not in modules:
|
|
|
|
display.warning('Skipping coverage of unknown module: %s' % module)
|
|
|
|
continue
|
2016-11-30 06:21:53 +01:00
|
|
|
new_name = os.path.abspath(modules[module])
|
|
|
|
display.info('%s -> %s' % (filename, new_name), verbosity=3)
|
|
|
|
filename = new_name
|
2017-05-11 07:25:02 +02:00
|
|
|
elif re.search('^(/.*?)?/root/ansible/', filename):
|
|
|
|
new_name = re.sub('^(/.*?)?/root/ansible/', root_path, filename)
|
2016-11-30 06:21:53 +01:00
|
|
|
display.info('%s -> %s' % (filename, new_name), verbosity=3)
|
|
|
|
filename = new_name
|
|
|
|
|
2017-05-15 11:42:11 +02:00
|
|
|
if group not in groups:
|
|
|
|
groups[group] = {}
|
|
|
|
|
|
|
|
arc_data = groups[group]
|
|
|
|
|
2016-11-30 06:21:53 +01:00
|
|
|
if filename not in arc_data:
|
2017-02-28 00:46:15 +01:00
|
|
|
arc_data[filename] = set()
|
2016-11-30 06:21:53 +01:00
|
|
|
|
2017-02-28 00:46:15 +01:00
|
|
|
arc_data[filename].update(arcs)
|
2016-11-30 06:21:53 +01:00
|
|
|
|
2017-05-15 11:42:11 +02:00
|
|
|
output_files = []
|
2016-11-30 06:21:53 +01:00
|
|
|
|
2017-05-15 11:42:11 +02:00
|
|
|
for group in sorted(groups):
|
|
|
|
arc_data = groups[group]
|
2016-11-30 06:21:53 +01:00
|
|
|
|
2017-05-15 11:42:11 +02:00
|
|
|
updated = coverage.CoverageData()
|
2016-11-30 06:21:53 +01:00
|
|
|
|
2017-05-15 11:42:11 +02:00
|
|
|
for filename in arc_data:
|
|
|
|
if not os.path.isfile(filename):
|
|
|
|
display.warning('Invalid coverage path: %s' % filename)
|
|
|
|
continue
|
|
|
|
|
|
|
|
updated.add_arcs({filename: list(arc_data[filename])})
|
|
|
|
|
2017-05-15 18:51:49 +02:00
|
|
|
if args.all:
|
|
|
|
updated.add_arcs(dict((source, []) for source in sources))
|
|
|
|
|
2017-05-15 11:42:11 +02:00
|
|
|
if not args.explain:
|
|
|
|
output_file = COVERAGE_FILE + group
|
|
|
|
updated.write_file(output_file)
|
|
|
|
output_files.append(output_file)
|
|
|
|
|
|
|
|
return sorted(output_files)
|
2016-11-30 06:21:53 +01:00
|
|
|
|
|
|
|
|
|
|
|
def command_coverage_report(args):
|
|
|
|
"""
|
|
|
|
:type args: CoverageConfig
|
|
|
|
"""
|
2017-05-15 11:42:11 +02:00
|
|
|
output_files = command_coverage_combine(args)
|
|
|
|
|
|
|
|
for output_file in output_files:
|
2017-05-15 18:51:49 +02:00
|
|
|
if args.group_by or args.stub:
|
2017-05-15 11:42:11 +02:00
|
|
|
display.info('>>> Coverage Group: %s' % ' '.join(os.path.basename(output_file).split('=')[1:]))
|
|
|
|
|
|
|
|
env = common_environment()
|
|
|
|
env.update(dict(COVERAGE_FILE=output_file))
|
|
|
|
run_command(args, env=env, cmd=['coverage', 'report'])
|
2016-11-30 06:21:53 +01:00
|
|
|
|
|
|
|
|
|
|
|
def command_coverage_html(args):
|
|
|
|
"""
|
|
|
|
:type args: CoverageConfig
|
|
|
|
"""
|
2017-05-15 11:42:11 +02:00
|
|
|
output_files = command_coverage_combine(args)
|
|
|
|
|
|
|
|
for output_file in output_files:
|
|
|
|
dir_name = 'test/results/reports/%s' % os.path.basename(output_file)
|
|
|
|
env = common_environment()
|
|
|
|
env.update(dict(COVERAGE_FILE=output_file))
|
|
|
|
run_command(args, env=env, cmd=['coverage', 'html', '-d', dir_name])
|
2016-11-30 06:21:53 +01:00
|
|
|
|
|
|
|
|
|
|
|
def command_coverage_xml(args):
|
|
|
|
"""
|
|
|
|
:type args: CoverageConfig
|
|
|
|
"""
|
2017-05-15 11:42:11 +02:00
|
|
|
output_files = command_coverage_combine(args)
|
|
|
|
|
|
|
|
for output_file in output_files:
|
|
|
|
xml_name = 'test/results/reports/%s.xml' % os.path.basename(output_file)
|
|
|
|
env = common_environment()
|
|
|
|
env.update(dict(COVERAGE_FILE=output_file))
|
|
|
|
run_command(args, env=env, cmd=['coverage', 'xml', '-o', xml_name])
|
2016-11-30 06:21:53 +01:00
|
|
|
|
|
|
|
|
|
|
|
def command_coverage_erase(args):
|
|
|
|
"""
|
|
|
|
:type args: CoverageConfig
|
|
|
|
"""
|
|
|
|
initialize_coverage(args)
|
|
|
|
|
|
|
|
for name in os.listdir(COVERAGE_DIR):
|
2017-05-11 07:25:02 +02:00
|
|
|
if not name.startswith('coverage') and '=coverage.' not in name:
|
2016-11-30 06:21:53 +01:00
|
|
|
continue
|
|
|
|
|
|
|
|
path = os.path.join(COVERAGE_DIR, name)
|
|
|
|
|
|
|
|
if not args.explain:
|
|
|
|
os.remove(path)
|
|
|
|
|
|
|
|
|
|
|
|
def initialize_coverage(args):
|
|
|
|
"""
|
|
|
|
:type args: CoverageConfig
|
|
|
|
:rtype: coverage
|
|
|
|
"""
|
|
|
|
if args.delegate:
|
|
|
|
raise Delegate()
|
|
|
|
|
|
|
|
if args.requirements:
|
|
|
|
install_command_requirements(args)
|
|
|
|
|
|
|
|
try:
|
|
|
|
import coverage
|
|
|
|
except ImportError:
|
|
|
|
coverage = None
|
|
|
|
|
|
|
|
if not coverage:
|
|
|
|
raise ApplicationError('You must install the "coverage" python module to use this command.')
|
|
|
|
|
|
|
|
return coverage
|
|
|
|
|
|
|
|
|
2017-05-15 11:42:11 +02:00
|
|
|
def get_coverage_group(args, coverage_file):
|
|
|
|
"""
|
|
|
|
:type args: CoverageConfig
|
|
|
|
:type coverage_file: str
|
|
|
|
:rtype: str
|
|
|
|
"""
|
|
|
|
parts = os.path.basename(coverage_file).split('=', 4)
|
|
|
|
|
|
|
|
if len(parts) != 5 or not parts[4].startswith('coverage.'):
|
|
|
|
return None
|
|
|
|
|
|
|
|
names = dict(
|
|
|
|
command=parts[0],
|
|
|
|
target=parts[1],
|
|
|
|
environment=parts[2],
|
|
|
|
version=parts[3],
|
|
|
|
)
|
|
|
|
|
|
|
|
group = ''
|
|
|
|
|
|
|
|
for part in COVERAGE_GROUPS:
|
|
|
|
if part in args.group_by:
|
|
|
|
group += '=%s' % names[part]
|
|
|
|
|
|
|
|
return group
|
|
|
|
|
|
|
|
|
2016-11-30 06:21:53 +01:00
|
|
|
class CoverageConfig(EnvironmentConfig):
|
|
|
|
"""Configuration for the coverage command."""
|
|
|
|
def __init__(self, args):
|
|
|
|
"""
|
|
|
|
:type args: any
|
|
|
|
"""
|
|
|
|
super(CoverageConfig, self).__init__(args, 'coverage')
|
2017-05-15 11:42:11 +02:00
|
|
|
|
|
|
|
self.group_by = frozenset(args.group_by) if 'group_by' in args and args.group_by else set() # type: frozenset[str]
|
2017-05-15 18:51:49 +02:00
|
|
|
self.all = args.all if 'all' in args else False # type: bool
|
|
|
|
self.stub = args.stub if 'stub' in args else False # type: bool
|