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): ...@@ -57,10 +57,19 @@ class Plugin(CoveragePlugin):
_c_files_map = None _c_files_map = None
# map from parsed C files to their content # map from parsed C files to their content
_parsed_c_files = None _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): def sys_info(self):
return [('Cython version', __version__)] 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): def file_tracer(self, filename):
""" """
Try to find a C source file for a file path found by the tracer. Try to find a C source file for a file path found by the tracer.
...@@ -108,7 +117,13 @@ class Plugin(CoveragePlugin): ...@@ -108,7 +117,13 @@ class Plugin(CoveragePlugin):
rel_file_path, code = self._read_source_lines(c_file, filename) rel_file_path, code = self._read_source_lines(c_file, filename)
if code is None: if code is None:
return None # no source found 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): def _find_source_files(self, filename):
basename, ext = os.path.splitext(filename) basename, ext = os.path.splitext(filename)
...@@ -218,10 +233,16 @@ class Plugin(CoveragePlugin): ...@@ -218,10 +233,16 @@ class Plugin(CoveragePlugin):
r'(?:struct|union|enum|class)' r'(?:struct|union|enum|class)'
r'(\s+[^:]+|)\s*:' r'(\s+[^:]+|)\s*:'
).match ).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) code_lines = defaultdict(dict)
executable_lines = defaultdict(set) executable_lines = defaultdict(set)
current_filename = None current_filename = None
if self._excluded_lines_map is None:
self._excluded_lines_map = defaultdict(set)
with open(c_file) as lines: with open(c_file) as lines:
lines = iter(lines) lines = iter(lines)
...@@ -242,6 +263,9 @@ class Plugin(CoveragePlugin): ...@@ -242,6 +263,9 @@ class Plugin(CoveragePlugin):
code_line = match.group(1).rstrip() code_line = match.group(1).rstrip()
if not_executable(code_line): if not_executable(code_line):
break break
if line_is_excluded(code_line):
self._excluded_lines_map[filename].add(lineno)
break
code_lines[filename][lineno] = code_line code_lines[filename][lineno] = code_line
break break
elif match_comment_end(comment_line): elif match_comment_end(comment_line):
...@@ -298,11 +322,12 @@ class CythonModuleReporter(FileReporter): ...@@ -298,11 +322,12 @@ class CythonModuleReporter(FileReporter):
""" """
Provide detailed trace information for one source file to coverage.py. 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) super(CythonModuleReporter, self).__init__(source_file)
self.name = rel_file_path self.name = rel_file_path
self.c_file = c_file self.c_file = c_file
self._code = code self._code = code
self._excluded_lines = excluded_lines
def lines(self): def lines(self):
""" """
...@@ -310,6 +335,12 @@ class CythonModuleReporter(FileReporter): ...@@ -310,6 +335,12 @@ class CythonModuleReporter(FileReporter):
""" """
return set(self._code) 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): def _iter_source_tokens(self):
current_line = 1 current_line = 1
for line_no, code_line in sorted(self._code.items()): for line_no, code_line in sorted(self._code.items()):
...@@ -345,4 +376,6 @@ class CythonModuleReporter(FileReporter): ...@@ -345,4 +376,6 @@ class CythonModuleReporter(FileReporter):
def coverage_init(reg, options): 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): ...@@ -40,6 +40,13 @@ def func2(a):
return a * 2 # 11 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 ######## ######## pkg/coverage_test_pyx.pyx ########
# cython: linetrace=True # cython: linetrace=True
# distutils: define_macros=CYTHON_TRACE=1 # distutils: define_macros=CYTHON_TRACE=1
...@@ -54,6 +61,13 @@ def func2(int a): ...@@ -54,6 +61,13 @@ def func2(int a):
return a * 2 # 11 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 ######## ######## coverage_test_include_pyx.pyx ########
# cython: linetrace=True # cython: linetrace=True
# distutils: define_macros=CYTHON_TRACE=1 # distutils: define_macros=CYTHON_TRACE=1
...@@ -152,8 +166,9 @@ def run_report(): ...@@ -152,8 +166,9 @@ def run_report():
missing.append(int(start)) missing.append(int(start))
files[os.path.basename(name)] = (statements, missed, covered, missing) files[os.path.basename(name)] = (statements, missed, covered, missing)
assert 7 not in files['coverage_test_pyx.pyx'][-1], files['coverage_test_pyx.pyx'] report = files['coverage_test_pyx.pyx']
assert 12 not in files['coverage_test_pyx.pyx'][-1], files['coverage_test_pyx.pyx'] assert 7 not in report[-1], report
assert 12 not in report[-1], report
def run_xml_report(): def run_xml_report():
...@@ -170,35 +185,74 @@ def run_xml_report(): ...@@ -170,35 +185,74 @@ def run_xml_report():
for line in module.findall('lines/line') for line in module.findall('lines/line')
) )
assert files['pkg/coverage_test_pyx.pyx'][5] > 0, files['pkg/coverage_test_pyx.pyx'] report = files['pkg/coverage_test_pyx.pyx']
assert files['pkg/coverage_test_pyx.pyx'][6] > 0, files['pkg/coverage_test_pyx.pyx'] assert report[5] > 0, report
assert files['pkg/coverage_test_pyx.pyx'][7] > 0, files['pkg/coverage_test_pyx.pyx'] 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(): def run_html_report():
from collections import defaultdict
stdout = run_coverage_command('html', '-d', 'html') stdout = run_coverage_command('html', '-d', 'html')
_parse_lines = re.compile( _parse_lines = re.compile(
r'<p[^>]* id=["\'][^0-9"\']*(?P<id>[0-9]+)[^0-9"\']*["\'][^>]*' 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 = {} files = {}
for file_path in iglob('html/*.html'): for file_path in iglob('html/*.html'):
with open(file_path) as f: with open(file_path) as f:
page = f.read() page = f.read()
executed = set() report = defaultdict(set)
missing = set() for line, state in _parse_lines(page):
for line, has_run in _parse_lines(page): report[state].add(int(line))
(executed if has_run == 'run' else missing).add(int(line)) files[file_path] = report
files[file_path] = (executed, missing)
executed, missing = [data for path, data in files.items() if 'coverage_test_pyx' in path][0] for filename, report in files.items():
assert executed if "coverage_test_pyx" not in filename:
assert 5 in executed, executed continue
assert 6 in executed, executed executed = report["run"]
assert 7 in executed, executed 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__': if __name__ == '__main__':
run_report() run_report()
run_xml_report() run_xml_report()
run_json_report()
run_html_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