Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
C
cython
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Labels
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Commits
Open sidebar
nexedi
cython
Commits
29eb0238
Commit
29eb0238
authored
Feb 13, 2015
by
Stefan Behnel
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
implement coverage analysis support as a plugin for the coverage.py tool
parent
32d32d03
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
274 additions
and
13 deletions
+274
-13
CHANGES.rst
CHANGES.rst
+9
-0
Cython/Coverage.py
Cython/Coverage.py
+181
-0
runtests.py
runtests.py
+7
-0
tests/bugs.txt
tests/bugs.txt
+0
-2
tests/run/coverage.srctree
tests/run/coverage.srctree
+77
-11
No files found.
CHANGES.rst
View file @
29eb0238
...
...
@@ -5,6 +5,15 @@ Cython Changelog
Latest changes
==============
Features added
--------------
* Support for coverage.py 4.0+ can be enabled by adding the plugin
"Cython.Coverage" to the ".coveragerc" config file.
Bugs fixed
----------
0.22 (2015-02-11)
=================
...
...
Cython/Coverage.py
0 → 100644
View file @
29eb0238
"""
A Cython plugin for coverage.py
Requires the coverage package at least in version 4.0 (which added the plugin API).
"""
import
re
import
os.path
from
collections
import
defaultdict
try
:
from
coverage.plugin
import
CoveragePlugin
,
FileTracer
,
FileReporter
except
ImportError
:
# version too old?
CoveragePlugin
=
FileTracer
=
FileReporter
=
object
from
.
import
__version__
class
Plugin
(
CoveragePlugin
):
_c_files_map
=
None
def
sys_info
(
self
):
return
[(
'Cython version'
,
__version__
)]
def
file_tracer
(
self
,
filename
):
"""
Try to find a C source file for a file path found by the tracer.
"""
c_file
=
py_file
=
None
filename
=
os
.
path
.
abspath
(
filename
)
if
self
.
_c_files_map
and
filename
in
self
.
_c_files_map
:
c_file
=
self
.
_c_files_map
[
filename
][
0
]
if
c_file
is
None
:
c_file
,
py_file
=
self
.
_find_source_files
(
filename
)
if
not
c_file
:
return
None
try
:
with
open
(
c_file
,
'rb'
)
as
f
:
if
b'/* Generated by Cython '
not
in
f
.
read
(
30
):
return
None
# not a Cython file
except
(
IOError
,
OSError
):
return
None
# parse all source file paths and lines from C file
# to learn about all relevant source files right away (pyx/pxi/pxd)
# FIXME: this might already be too late if the first executed line
# is not from the main .pyx file but a file with a different
# name than the .c file (which prevents us from finding the
# .c file)
self
.
_parse_lines
(
c_file
,
filename
)
return
CythonModuleTracer
(
filename
,
py_file
,
c_file
,
self
.
_c_files_map
)
def
file_reporter
(
self
,
filename
):
if
os
.
path
.
splitext
(
filename
)[
1
].
lower
()
not
in
(
'.pyx'
,
'.pxi'
,
'.pxd'
):
return
None
# let coverage.py handle it (e.g. .py files)
filename
=
os
.
path
.
abspath
(
filename
)
if
not
self
.
_c_files_map
or
filename
not
in
self
.
_c_files_map
:
return
None
# unknown file
c_file
,
rel_file_path
,
code
,
excluded
=
self
.
_c_files_map
[
filename
]
if
code
is
None
:
return
None
# unknown file
return
CythonModuleReporter
(
c_file
,
filename
,
rel_file_path
,
code
,
excluded
)
def
_find_source_files
(
self
,
filename
):
basename
,
ext
=
os
.
path
.
splitext
(
filename
)
if
ext
.
lower
()
not
in
(
'.so'
,
'.dll'
,
'.c'
,
'.cpp'
):
return
None
,
None
if
os
.
path
.
exists
(
basename
+
'.c'
):
c_file
=
basename
+
'.c'
elif
os
.
path
.
exists
(
basename
+
'.cpp'
):
c_file
=
basename
+
'.cpp'
else
:
c_file
=
None
py_source_file
=
None
if
c_file
:
py_source_file
=
os
.
path
.
splitext
(
c_file
)[
0
]
+
'.py'
if
not
os
.
path
.
exists
(
py_source_file
):
py_source_file
=
None
return
c_file
,
py_source_file
def
_parse_lines
(
self
,
c_file
,
sourcefile
):
"""
Parse a Cython generated C/C++ source file and find the executable lines.
Each executable line starts with a comment header that states source file
and line number, as well as the surrounding range of source code lines.
"""
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
code_lines
=
defaultdict
(
dict
)
max_line
=
defaultdict
(
int
)
filenames
=
set
()
with
open
(
c_file
)
as
lines
:
lines
=
iter
(
lines
)
for
line
in
lines
:
match
=
match_source_path_line
(
line
)
if
not
match
:
continue
filename
,
lineno
=
match
.
groups
()
filenames
.
add
(
filename
)
lineno
=
int
(
lineno
)
max_line
[
filename
]
=
max
(
max_line
[
filename
],
lineno
)
for
comment_line
in
lines
:
match
=
match_current_code_line
(
comment_line
)
if
match
:
code_lines
[
filename
][
lineno
]
=
match
.
group
(
1
).
rstrip
()
break
elif
match_comment_end
(
comment_line
):
# unexpected comment format - false positive?
break
excluded_lines
=
dict
(
(
filename
,
set
(
range
(
1
,
max_line
[
filename
]
+
1
))
-
set
(
lines
))
for
filename
,
lines
in
code_lines
.
iteritems
()
)
if
self
.
_c_files_map
is
None
:
self
.
_c_files_map
=
{}
for
filename
in
filenames
:
self
.
_c_files_map
[
os
.
path
.
abspath
(
filename
)]
=
(
c_file
,
filename
,
code_lines
[
filename
],
excluded_lines
[
filename
])
if
sourcefile
not
in
self
.
_c_files_map
:
return
None
,
None
# shouldn't happen ...
return
self
.
_c_files_map
[
sourcefile
][
1
:]
class
CythonModuleTracer
(
FileTracer
):
"""
Find the Python/Cython source file for a Cython module.
"""
def
__init__
(
self
,
module_file
,
py_file
,
c_file
,
c_files_map
):
super
(
CythonModuleTracer
,
self
).
__init__
()
self
.
module_file
=
module_file
self
.
py_file
=
py_file
self
.
c_file
=
c_file
self
.
_c_files_map
=
c_files_map
def
has_dynamic_source_filename
(
self
):
return
True
def
dynamic_source_filename
(
self
,
filename
,
frame
):
source_file
=
frame
.
f_code
.
co_filename
abs_path
=
os
.
path
.
abspath
(
source_file
)
if
self
.
py_file
and
source_file
.
lower
().
endswith
(
'.py'
):
# always let coverage.py handle this case itself
return
self
.
py_file
assert
self
.
_c_files_map
is
not
None
if
abs_path
not
in
self
.
_c_files_map
:
self
.
_c_files_map
[
abs_path
]
=
(
self
.
c_file
,
source_file
,
None
,
None
)
return
abs_path
class
CythonModuleReporter
(
FileReporter
):
"""
Provide detailed trace information for one source file to coverage.py.
"""
def
__init__
(
self
,
c_file
,
source_file
,
rel_file_path
,
code
,
excluded
):
super
(
CythonModuleReporter
,
self
).
__init__
(
source_file
)
self
.
name
=
rel_file_path
self
.
c_file
=
c_file
self
.
_code
=
code
self
.
_excluded
=
excluded
def
statements
(
self
):
return
self
.
_code
.
viewkeys
()
def
excluded_statements
(
self
):
return
self
.
_excluded
runtests.py
View file @
29eb0238
...
...
@@ -1937,6 +1937,13 @@ def runtests(options, cmd_args, coverage=None):
except (ImportError, AttributeError, TypeError):
exclude_selectors.append(RegExSelector('Jedi'))
try:
import coverage
if list(map(int, re.findall('[0-9]+', coverage.__version__ or '0'))) < [4, 0]:
raise ImportError
except (ImportError, AttributeError, TypeError):
exclude_selectors.append(RegExSelector('coverage'))
if options.exclude:
exclude_selectors += [ string_selector(r) for r in options.exclude ]
...
...
tests/bugs.txt
View file @
29eb0238
...
...
@@ -14,8 +14,6 @@ inherited_final_method
tryfinallychaining # also see FIXME in "yield_from_pep380" test
cimport_alias_subclass
coverage # depends on newer coverage.py version
# CPython regression tests that don't current work:
pyregr.test_signal
pyregr.test_capi
...
...
tests/run/coverage.srctree
View file @
29eb0238
...
...
@@ -2,6 +2,7 @@
# tag: coverage,trace
"""
PYTHON -c 'import shutil; shutil.copy("pkg/coverage_test_pyx.pyx", "pkg/coverage_test_pyx.pxi")'
PYTHON setup.py build_ext -i
PYTHON coverage_test.py
"""
...
...
@@ -11,10 +12,20 @@ PYTHON coverage_test.py
from distutils.core import setup
from Cython.Build import cythonize
setup(ext_modules = cythonize('coverage_test_cy.py'))
setup(ext_modules = cythonize([
'coverage_test_*.py*',
'pkg/coverage_test_*.py*'
]))
######## coverage_test_cy.py ########
######## .coveragerc ########
[run]
plugins = Cython.Coverage
######## pkg/__init__.py ########
######## pkg/coverage_test_py.py ########
# cython: linetrace=True
# distutils: define_macros=CYTHON_TRACE=1
...
...
@@ -28,8 +39,38 @@ def func2(a):
return a * 2 # 11
######## pkg/coverage_test_pyx.pyx ########
# cython: linetrace=True
# distutils: define_macros=CYTHON_TRACE=1
def func1(int a, int b):
cdef int x = 1 # 5
c = func2(a) + b # 6
return x + c # 7
def func2(int a):
return a * 2 # 11
######## coverage_test_include_pyx.pyx ########
# cython: linetrace=True
# distutils: define_macros=CYTHON_TRACE=1
cdef int x = 5 # 4
cdef int cfunc1(int x): # 6
return x * 3 # 7
include "pkg/coverage_test_pyx.pxi" # 9
def main_func(int x): # 11
return cfunc1(x) + func1(x, 4) + func2(x) # 12
######## coverage_test.py ########
import os.path
try:
# io.StringIO in Py2.x cannot handle str ...
from StringIO import StringIO
...
...
@@ -38,27 +79,52 @@ except ImportError:
from coverage import coverage
import coverage_test_cy
from pkg import coverage_test_py
from pkg import coverage_test_pyx
import coverage_test_include_pyx
for module in [coverage_test_py, coverage_test_pyx, coverage_test_include_pyx]:
assert not any(module.__file__.endswith(ext) for ext in '.py .pyc .pyo .pyw .pyx .pxi'.split()), \
module.__file__
def run_coverage():
def run_coverage(module):
module_name = module.__name__
module_path = module_name.replace('.', os.path.sep) + '.' + module_name.rsplit('_', 1)[-1]
cov = coverage()
cov.start()
assert coverage_test_cy.func1(1, 2) == 5
assert coverage_test_cy.func2(2) == 4
assert module.func1(1, 2) == (1 * 2) + 2 + 1
assert module.func2(2) == 2 * 2
if '_include_' in module_name:
assert module.main_func(2) == (2 * 3) + ((2 * 2) + 4 + 1) + (2 * 2)
cov.stop()
out = StringIO()
cov.report([coverage_test_cy], file=out)
cov.report(file=out)
#cov.report([module], file=out)
lines = out.getvalue().splitlines()
assert any('coverage_test_cy' in line for line in lines), "coverage_test_cy not found in coverage"
assert any(module_path in line for line in lines), "'%s' not found in coverage report:\n\n%s" % (
module_path, out.getvalue())
mod_file, exec_lines, excl_lines, missing_lines, _ = cov.analysis2(
os.path.splitext(module.__file__)[0] + '.' + module_name.rsplit('_', 1)[-1])
assert module_path in mod_file
if '_include_' in module_name:
executed = set(exec_lines) - set(missing_lines)
assert all(line in executed for line in [7, 12]), '%s / %s' % (exec_lines, missing_lines)
mod_file, exec_lines, excl_lines, missing_lines, _ = cov.analysis2(coverage_test_cy)
assert 'coverage_test_cy' in mod_file
# rest of test if for include file
mod_file, exec_lines, excl_lines, missing_lines, _ = cov.analysis2(
os.path.join(os.path.dirname(module.__file__), "pkg", "coverage_test_pyx.pxi"))
executed = set(exec_lines) - set(missing_lines)
assert all(line in executed for line in [5, 6, 7, 11]), '%s / %s' % (exec_lines, missing_lines)
if __name__ == '__main__':
run_coverage()
run_coverage(coverage_test_py)
run_coverage(coverage_test_pyx)
run_coverage(coverage_test_include_pyx)
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment