1# mode: run
2# tag: coverage,trace
3
4"""
5PYTHON -c "import shutil; shutil.copy('pkg/coverage_test_pyx.pyx', 'pkg/coverage_test_pyx.pxi')"
6PYTHON setup.py build_ext -i
7PYTHON -m coverage run coverage_test.py
8PYTHON collect_coverage.py
9"""
10
11######## setup.py ########
12
13from distutils.core import setup
14from Cython.Build import cythonize
15
16setup(ext_modules = cythonize([
17    'coverage_test_*.py*',
18    'pkg/coverage_test_*.py*'
19]))
20
21
22######## .coveragerc ########
23[run]
24plugins = Cython.Coverage
25
26
27######## pkg/__init__.py ########
28
29######## pkg/coverage_test_py.py ########
30# cython: linetrace=True
31# distutils: define_macros=CYTHON_TRACE=1
32
33def func1(a, b):
34    x = 1               #  5
35    c = func2(a) + b    #  6
36    return x + c        #  7
37
38
39def func2(a):
40    return a * 2        # 11
41
42
43######## pkg/coverage_test_pyx.pyx ########
44# cython: linetrace=True
45# distutils: define_macros=CYTHON_TRACE=1
46
47def func1(int a, int b):
48    cdef int x = 1      #  5
49    c = func2(a) + b    #  6
50    return x + c        #  7
51
52
53def func2(int a):
54    return a * 2        # 11
55
56
57######## coverage_test_include_pyx.pyx ########
58# cython: linetrace=True
59# distutils: define_macros=CYTHON_TRACE=1
60
61cdef int x = 5                                   #  4
62
63cdef int cfunc1(int x):                          #  6
64    return x * 3                                 #  7
65
66include "pkg/coverage_test_pyx.pxi"              #  9
67
68def main_func(int x):                            # 11
69    return cfunc1(x) + func1(x, 4) + func2(x)    # 12
70
71
72######## coverage_test.py ########
73
74import os.path
75try:
76    # io.StringIO in Py2.x cannot handle str ...
77    from StringIO import StringIO
78except ImportError:
79    from io import StringIO
80
81from pkg import coverage_test_py
82from pkg import coverage_test_pyx
83import coverage_test_include_pyx
84
85
86for module in [coverage_test_py, coverage_test_pyx, coverage_test_include_pyx]:
87    assert not any(module.__file__.endswith(ext) for ext in '.py .pyc .pyo .pyw .pyx .pxi'.split()), \
88        module.__file__
89
90
91def run_coverage(module):
92    module_name = module.__name__
93    module_path = module_name.replace('.', os.path.sep) + '.' + module_name.rsplit('_', 1)[-1]
94
95    assert module.func1(1, 2) == (1 * 2) + 2 + 1
96    assert module.func2(2) == 2 * 2
97    if '_include_' in module_name:
98        assert module.main_func(2) == (2 * 3) + ((2 * 2) + 4 + 1) + (2 * 2)
99
100
101if __name__ == '__main__':
102    run_coverage(coverage_test_py)
103    run_coverage(coverage_test_pyx)
104    run_coverage(coverage_test_include_pyx)
105
106
107######## collect_coverage.py ########
108
109import re
110import sys
111import os
112import os.path
113import subprocess
114from glob import iglob
115
116
117def run_coverage_command(*command):
118    env = dict(os.environ, LANG='', LC_ALL='C')
119    process = subprocess.Popen(
120        [sys.executable, '-m', 'coverage'] + list(command),
121        stdout=subprocess.PIPE, env=env)
122    stdout, _ = process.communicate()
123    return stdout
124
125
126def run_report():
127    stdout = run_coverage_command('report', '--show-missing')
128    stdout = stdout.decode('iso8859-1')  # 'safe' decoding
129    lines = stdout.splitlines()
130    print(stdout)
131
132    # FIXME:  'coverage_test_pyx.pxi' may not be found if coverage.py requests it before the .pyx file
133    for module_path in ('coverage_test_py.py', 'coverage_test_pyx.pyx', 'coverage_test_include_pyx.pyx'):
134        assert any(module_path in line for line in lines), "'%s' not found in coverage report:\n\n%s" % (
135            module_path, stdout)
136
137    files = {}
138    line_iter = iter(lines)
139    for line in line_iter:
140        if line.startswith('---'):
141            break
142    extend = [''] * 2
143    for line in line_iter:
144        if not line or line.startswith('---'):
145            continue
146        name, statements, missed, covered, _missing = (line.split(None, 4) + extend)[:5]
147        missing = []
148        for start, end in re.findall('([0-9]+)(?:-([0-9]+))?', _missing):
149            if end:
150                missing.extend(range(int(start), int(end)+1))
151            else:
152                missing.append(int(start))
153        files[os.path.basename(name)] = (statements, missed, covered, missing)
154
155    assert  7 not in files['coverage_test_pyx.pyx'][-1], files['coverage_test_pyx.pyx']
156    assert 12 not in files['coverage_test_pyx.pyx'][-1], files['coverage_test_pyx.pyx']
157
158
159def run_xml_report():
160    stdout = run_coverage_command('xml', '-o', '-')
161    print(stdout)
162
163    import xml.etree.ElementTree as etree
164    data = etree.fromstring(stdout)
165
166    files = {}
167    for module in data.iterfind('.//class'):
168        files[module.get('filename').replace('\\', '/')] = dict(
169            (int(line.get('number')), int(line.get('hits')))
170            for line in module.findall('lines/line')
171        )
172
173    assert files['pkg/coverage_test_pyx.pyx'][5] > 0, files['pkg/coverage_test_pyx.pyx']
174    assert files['pkg/coverage_test_pyx.pyx'][6] > 0, files['pkg/coverage_test_pyx.pyx']
175    assert files['pkg/coverage_test_pyx.pyx'][7] > 0, files['pkg/coverage_test_pyx.pyx']
176
177
178def run_html_report():
179    stdout = run_coverage_command('html', '-d', 'html')
180    _parse_lines = re.compile(
181        r'<p[^>]* id=["\'][^0-9"\']*(?P<id>[0-9]+)[^0-9"\']*["\'][^>]*'
182        r' class=["\'][^"\']*(?P<run>mis|run)[^"\']*["\']').findall
183
184    files = {}
185    for file_path in iglob('html/*.html'):
186        with open(file_path) as f:
187            page = f.read()
188        executed = set()
189        missing = set()
190        for line, has_run in _parse_lines(page):
191            (executed if has_run == 'run' else missing).add(int(line))
192        files[file_path] = (executed, missing)
193
194    executed, missing = [data for path, data in files.items() if 'coverage_test_pyx' in path][0]
195    assert executed
196    assert 5 in executed, executed
197    assert 6 in executed, executed
198    assert 7 in executed, executed
199
200
201if __name__ == '__main__':
202    run_report()
203    run_xml_report()
204    run_html_report()
205