5
PYTHON setup.py build_ext -i
6
PYTHON -m coverage run --source=src coverage_test.py
7
PYTHON collect_coverage.py
10
######## setup.py ########
12
from distutils.core import setup
13
from Cython.Build import cythonize
15
setup(ext_modules = cythonize([
16
'src/trivial_module.pyx',
20
######## .coveragerc ########
22
plugins = Cython.Coverage
25
######## src/trivial_module.pyx ########
26
# cython: linetrace=True
27
# distutils: define_macros=CYTHON_TRACE=1 CYTHON_USE_SYS_MONITORING=0
29
def func1(int a, int b):
39
######## coverage_test.py ########
46
trivial_module.__file__.endswith(ext)
47
for ext in '.py .pyc .pyo .pyw .pyx .pxi'.split()
51
def run_coverage(module):
52
assert module.func1(1, 2) == (1 * 2) + 2 + 1
53
assert module.func2(2) == 2 * 2
56
if __name__ == '__main__':
57
run_coverage(trivial_module)
60
######## collect_coverage.py ########
70
def run_coverage_command(*command):
71
env = dict(os.environ, LANG='', LC_ALL='C')
72
process = subprocess.Popen(
73
[sys.executable, '-m', 'coverage'] + list(command),
74
stdout=subprocess.PIPE, env=env)
75
stdout, _ = process.communicate()
80
stdout = run_coverage_command('report', '--show-missing')
81
stdout = stdout.decode('iso8859-1') # 'safe' decoding
82
lines = stdout.splitlines()
85
module_path = 'trivial_module.pyx'
86
assert any(module_path in line for line in lines), (
87
"'%s' not found in coverage report:\n\n%s" % (module_path, stdout))
90
line_iter = iter(lines)
91
for line in line_iter:
92
if line.startswith('---'):
95
for line in line_iter:
96
if not line or line.startswith('---'):
98
name, statements, missed, covered, _missing = (line.split(None, 4) + extend)[:5]
100
for start, end in re.findall('([0-9]+)(?:-([0-9]+))?', _missing):
102
missing.extend(range(int(start), int(end)+1))
104
missing.append(int(start))
105
files[os.path.basename(name)] = (statements, missed, covered, missing)
107
assert 5 not in files[module_path][-1], files[module_path]
108
assert 6 not in files[module_path][-1], files[module_path]
109
assert 7 not in files[module_path][-1], files[module_path]
110
assert 11 not in files[module_path][-1], files[module_path]
114
stdout = run_coverage_command('xml', '-o', '-')
117
import xml.etree.ElementTree as etree
118
data = etree.fromstring(stdout)
121
for module in data.iterfind('.//class'):
122
files[module.get('filename').replace('\\', '/')] = dict(
123
(int(line.get('number')), int(line.get('hits')))
124
for line in module.findall('lines/line')
127
module_path = 'src/trivial_module.pyx'
129
assert files[module_path][5] > 0, files[module_path]
130
assert files[module_path][6] > 0, files[module_path]
131
assert files[module_path][7] > 0, files[module_path]
132
assert files[module_path][11] > 0, files[module_path]
135
def run_html_report():
136
from collections import defaultdict
138
stdout = run_coverage_command('html', '-d', 'html')
139
# coverage 6.1+ changed the order of the attributes => need to parse them separately
140
_parse_id = re.compile(r'id=["\'][^0-9"\']*(?P<id>[0-9]+)[^0-9"\']*["\']').search
141
_parse_state = re.compile(r'class=["\'][^"\']*(?P<state>mis|run|exc)[^"\']*["\']').search
144
for file_path in iglob('html/*.html'):
145
with open(file_path) as f:
147
report = defaultdict(set)
148
for line in re.split(r'id=["\']source["\']', page)[-1].splitlines():
149
lineno = _parse_id(line)
150
state = _parse_state(line)
151
if not lineno or not state:
153
report[state.group('state')].add(int(lineno.group('id')))
154
files[file_path] = (report['run'], report['mis'])
156
executed, missing = [data for path, data in files.items() if 'trivial_module' in path][0]
158
assert 5 in executed, executed
159
assert 6 in executed, executed
160
assert 7 in executed, executed
161
assert 11 in executed, executed
164
if __name__ == '__main__':