Commit adb44bd0 authored by Mathias Laurin's avatar Mathias Laurin Committed by GitHub

Do not cover lines that were excluded in the coveragerc config file (GH-3682)

Closes #3680.
parent 65bbb6f2
......@@ -57,10 +57,19 @@ class Plugin(CoveragePlugin):
_c_files_map = None
# map from parsed C files to their content
_parsed_c_files = None
# map from traced files to lines that are excluded from coverage
_excluded_lines_map = None
# list of regex patterns for lines to exclude
_excluded_line_patterns = ()
def sys_info(self):
return [('Cython version', __version__)]
def configure(self, config):
# Entry point for coverage "configurer".
# Read the regular expressions from the coverage config that match lines to be excluded from coverage.
self._excluded_line_patterns = config.get_option("report:exclude_lines")
def file_tracer(self, filename):
"""
Try to find a C source file for a file path found by the tracer.
......@@ -108,7 +117,13 @@ class Plugin(CoveragePlugin):
rel_file_path, code = self._read_source_lines(c_file, filename)
if code is None:
return None # no source found
return CythonModuleReporter(c_file, filename, rel_file_path, code)
return CythonModuleReporter(
c_file,
filename,
rel_file_path,
code,
self._excluded_lines_map.get(rel_file_path, frozenset())
)
def _find_source_files(self, filename):
basename, ext = os.path.splitext(filename)
......@@ -218,10 +233,16 @@ class Plugin(CoveragePlugin):
r'(?:struct|union|enum|class)'
r'(\s+[^:]+|)\s*:'
).match
if self._excluded_line_patterns:
line_is_excluded = re.compile("|".join(["(?:%s)" % regex for regex in self._excluded_line_patterns])).search
else:
line_is_excluded = lambda line: False
code_lines = defaultdict(dict)
executable_lines = defaultdict(set)
current_filename = None
if self._excluded_lines_map is None:
self._excluded_lines_map = defaultdict(set)
with open(c_file) as lines:
lines = iter(lines)
......@@ -242,6 +263,9 @@ class Plugin(CoveragePlugin):
code_line = match.group(1).rstrip()
if not_executable(code_line):
break
if line_is_excluded(code_line):
self._excluded_lines_map[filename].add(lineno)
break
code_lines[filename][lineno] = code_line
break
elif match_comment_end(comment_line):
......@@ -298,11 +322,12 @@ class CythonModuleReporter(FileReporter):
"""
Provide detailed trace information for one source file to coverage.py.
"""
def __init__(self, c_file, source_file, rel_file_path, code):
def __init__(self, c_file, source_file, rel_file_path, code, excluded_lines):
super(CythonModuleReporter, self).__init__(source_file)
self.name = rel_file_path
self.c_file = c_file
self._code = code
self._excluded_lines = excluded_lines
def lines(self):
"""
......@@ -310,6 +335,12 @@ class CythonModuleReporter(FileReporter):
"""
return set(self._code)
def excluded_lines(self):
"""
Return set of line numbers that are excluded from coverage.
"""
return self._excluded_lines
def _iter_source_tokens(self):
current_line = 1
for line_no, code_line in sorted(self._code.items()):
......@@ -345,4 +376,6 @@ class CythonModuleReporter(FileReporter):
def coverage_init(reg, options):
reg.add_file_tracer(Plugin())
plugin = Plugin()
reg.add_configurer(plugin)
reg.add_file_tracer(plugin)
......@@ -40,6 +40,13 @@ def func2(a):
return a * 2 # 11
def func3(a):
x = 1 # 15
a *= 2 # # pragma: no cover
a += x #
return a * 42 # 18 # pragma: no cover
######## pkg/coverage_test_pyx.pyx ########
# cython: linetrace=True
# distutils: define_macros=CYTHON_TRACE=1
......@@ -54,6 +61,13 @@ def func2(int a):
return a * 2 # 11
def func3(int a):
cdef int x = 1 # 15
a *= 2 # # pragma: no cover
a += x #
return a * 42 # 18 # pragma: no cover
######## coverage_test_include_pyx.pyx ########
# cython: linetrace=True
# distutils: define_macros=CYTHON_TRACE=1
......@@ -152,8 +166,9 @@ def run_report():
missing.append(int(start))
files[os.path.basename(name)] = (statements, missed, covered, missing)
assert 7 not in files['coverage_test_pyx.pyx'][-1], files['coverage_test_pyx.pyx']
assert 12 not in files['coverage_test_pyx.pyx'][-1], files['coverage_test_pyx.pyx']
report = files['coverage_test_pyx.pyx']
assert 7 not in report[-1], report
assert 12 not in report[-1], report
def run_xml_report():
......@@ -170,35 +185,74 @@ def run_xml_report():
for line in module.findall('lines/line')
)
assert files['pkg/coverage_test_pyx.pyx'][5] > 0, files['pkg/coverage_test_pyx.pyx']
assert files['pkg/coverage_test_pyx.pyx'][6] > 0, files['pkg/coverage_test_pyx.pyx']
assert files['pkg/coverage_test_pyx.pyx'][7] > 0, files['pkg/coverage_test_pyx.pyx']
report = files['pkg/coverage_test_pyx.pyx']
assert report[5] > 0, report
assert report[6] > 0, report
assert report[7] > 0, report
def run_json_report():
import coverage
if coverage.version_info < (5, 0):
# JSON output comes in coverage 5.0
return
stdout = run_coverage_command('json', '-o', '-')
import json
files = json.loads(stdout.decode("ascii"))['files']
for filename in [
'pkg/coverage_test_py.py',
'pkg/coverage_test_pyx.pyx',
]:
report = files[filename]
summary = report['summary']
assert summary['missing_lines'] == 2, summary
assert summary['excluded_lines'] == 2, summary
assert report['missing_lines'] == [15, 17], report
assert report['excluded_lines'] == [16, 18], report
assert not frozenset(
report['missing_lines'] + report['excluded_lines']
).intersection(report['executed_lines'])
def run_html_report():
from collections import defaultdict
stdout = run_coverage_command('html', '-d', 'html')
_parse_lines = re.compile(
r'<p[^>]* id=["\'][^0-9"\']*(?P<id>[0-9]+)[^0-9"\']*["\'][^>]*'
r' class=["\'][^"\']*(?P<run>mis|run)[^"\']*["\']').findall
r' class=["\'][^"\']*(?P<run>mis|run|exc)[^"\']*["\']').findall
files = {}
for file_path in iglob('html/*.html'):
with open(file_path) as f:
page = f.read()
executed = set()
missing = set()
for line, has_run in _parse_lines(page):
(executed if has_run == 'run' else missing).add(int(line))
files[file_path] = (executed, missing)
report = defaultdict(set)
for line, state in _parse_lines(page):
report[state].add(int(line))
files[file_path] = report
executed, missing = [data for path, data in files.items() if 'coverage_test_pyx' in path][0]
assert executed
assert 5 in executed, executed
assert 6 in executed, executed
assert 7 in executed, executed
for filename, report in files.items():
if "coverage_test_pyx" not in filename:
continue
executed = report["run"]
missing = report["mis"]
excluded = report["exc"]
assert executed
assert 5 in executed, executed
assert 6 in executed, executed
assert 7 in executed, executed
assert 15 in missing, missing
assert 16 in excluded, excluded
assert 17 in missing, missing
assert 18 in excluded, excluded
if __name__ == '__main__':
run_report()
run_xml_report()
run_json_report()
run_html_report()
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment