1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
|
import os
import pathlib
import re
from collections import defaultdict
from coverage.files import (
canonical_filename as _canonical_filename,
)
from coverage.plugin import (
CoveragePlugin,
FileReporter,
FileTracer,
)
CYTHON_EXTENSIONS = {".pxd", ".pyx", ".pxi"}
def canonical_filename(filename):
filename = pathlib.Path(filename).resolve()
filename = _canonical_filename(str(filename))
filename = pathlib.Path(filename)
return filename
class CythonCoveragePlugin(CoveragePlugin):
def configure(self, config):
self.exclude = config.get_option("report:exclude_lines")
def file_tracer(self, filename):
filename = canonical_filename(filename)
ext = filename.suffix
if ext in CYTHON_EXTENSIONS:
return CythonFileTracer(str(filename))
return None
def file_reporter(self, filename):
filename = canonical_filename(filename)
ext = filename.suffix
if ext in CYTHON_EXTENSIONS:
return CythonFileReporter(str(filename), self.exclude)
return None
class CythonFileTracer(FileTracer):
def __init__(self, filename):
super().__init__()
self.filename = filename
def source_filename(self):
return self.filename
class CythonFileReporter(FileReporter):
def __init__(self, filename, exclude=None):
super().__init__(filename)
self.exclude = exclude
def lines(self):
_setup_lines(self.exclude)
return self._get_lines(CODE_LINES)
def excluded_lines(self):
_setup_lines(self.exclude)
return self._get_lines(EXCL_LINES)
def translate_lines(self, lines):
_setup_lines(self.exclude)
exec_lines = self._get_lines(EXEC_LINES)
return set(lines).union(exec_lines)
def _get_lines(self, lines_map):
key = os.path.relpath(self.filename, TOPDIR)
lines = lines_map.get(key, {})
return set(lines)
TOPDIR = pathlib.Path(__file__).resolve().parent.parent
SRCDIR = TOPDIR / "src"
CODE_LINES = None
EXEC_LINES = None
EXCL_LINES = None
def _setup_lines(exclude):
global CODE_LINES, EXEC_LINES, EXCL_LINES
if CODE_LINES is None or EXEC_LINES is None or EXCL_LINES is None:
source = SRCDIR / "mpi4py" / "MPI.c"
CODE_LINES, EXEC_LINES, EXCL_LINES = _parse_c_file(source, exclude)
def _parse_c_file(c_file, exclude_list):
match_filetab_begin = "static const char *__pyx_f[] = {"
match_filetab_begin = re.compile(re.escape(match_filetab_begin)).match
match_filetab_entry = re.compile(r' *"(.*)",').match
match_source_path_line = re.compile(r' */[*] +"(.*)":([0-9]+)$').match
match_current_code_line = re.compile(r" *[*] (.*) # <<<<<<+$").match
match_comment_end = re.compile(r" *[*]/$").match
match_trace_line = re.compile(
r" *__Pyx_TraceLine\((\d+),\d+,__PYX_ERR\((\d+),"
).match
not_executable = re.compile(
"|".join([
r"\s*c(?:type)?def\s+"
r"(?:(?:public|external)\s+)?"
r"(?:struct|union|enum|class)"
r"(\s+[^:]+|)\s*:",
])
).match
if exclude_list:
line_is_excluded = re.compile(
"|".join([rf"(?:{regex})" for regex in exclude_list])
).search
else:
def line_is_excluded(_):
return False
filetab = []
modinit = False
code_lines = defaultdict(dict)
exec_lines = defaultdict(dict)
executable_lines = defaultdict(set)
excluded_lines = defaultdict(set)
with pathlib.Path(c_file).open(encoding="utf-8") as lines:
lines = iter(lines)
for line in lines:
if match_filetab_begin(line):
for line in lines:
match = match_filetab_entry(line)
if not match:
break
filename = match.group(1)
filetab.append(filename)
match = match_source_path_line(line)
if not match:
if '__Pyx_TraceCall("__Pyx_PyMODINIT_FUNC ' in line:
modinit = True
if "__Pyx_TraceLine(" in line:
trace_line = match_trace_line(line)
if trace_line:
lineno, fid = map(int, trace_line.groups())
executable_lines[filetab[fid]].add(lineno)
continue
filename, lineno = match.groups()
lineno = int(lineno)
for comment_line in lines:
match = match_current_code_line(comment_line)
if match:
code_line = match.group(1).rstrip()
if not_executable(code_line):
break
if line_is_excluded(code_line):
excluded_lines[filename].add(lineno)
break
code_lines[filename][lineno] = code_line
if modinit:
exec_lines[filename][lineno] = code_line
break
if match_comment_end(comment_line):
# unexpected comment format - false positive?
break
# Remove lines that generated code but are not traceable.
for filename, lines in code_lines.items():
dead_lines = set(lines).difference(executable_lines.get(filename, ()))
for lineno in dead_lines:
del lines[lineno]
for filename, lines in exec_lines.items():
dead_lines = set(lines).difference(executable_lines.get(filename, ()))
for lineno in dead_lines:
del lines[lineno]
return code_lines, exec_lines, excluded_lines
def coverage_init(reg, options): # noqa: ARG001
plugin = CythonCoveragePlugin()
reg.add_configurer(plugin)
reg.add_file_tracer(plugin)
|