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