Commit 384cc660 authored by Stefan Behnel's avatar Stefan Behnel

integrate coverage report into annotated HTML source page

parent d7e8796b
......@@ -7,6 +7,7 @@ import os.path
import re
import codecs
import textwrap
from datetime import datetime
from xml.sax.saxutils import escape as html_escape
from StringIO import StringIO
......@@ -79,6 +80,9 @@ class AnnotationCCodeWriter(CCodeWriter):
.cython.line { margin: 0em }
.cython.code { font-size: 9; color: #444444; display: none; margin: 0px 0px 0px 20px; }
.cython.line .run { background-color: #B0FFB0; }
.cython.line .mis { background-color: #FFB0B0; }
.cython.code .py_c_api { color: red; }
.cython.code .py_macro_api { color: #FF7000; }
.cython.code .pyx_c_api { color: #FF3000; }
......@@ -94,16 +98,22 @@ class AnnotationCCodeWriter(CCodeWriter):
.cython.code .c_call { color: #0000FF; }
""")
def save_annotation(self, source_filename, target_filename):
def save_annotation(self, source_filename, target_filename, coverage_xml=None):
with Utils.open_source_file(source_filename) as f:
code = f.read()
generated_code = self.code.get(source_filename, {})
c_file = Utils.decode_filename(os.path.basename(target_filename))
html_filename = os.path.splitext(target_filename)[0] + ".html"
with codecs.open(html_filename, "w", encoding="UTF-8") as out_buffer:
out_buffer.write(self._save_annotation(code, generated_code, c_file, source_filename))
out_buffer.write(self._save_annotation(code, generated_code, c_file, source_filename, coverage_xml))
def _save_annotation_header(self, c_file, source_filename, coverage_timestamp=None):
coverage_info = ''
if coverage_timestamp:
coverage_info = u' with coverage data from {timestamp}'.format(
timestamp=datetime.fromtimestamp(int(coverage_timestamp) // 1000))
def _save_annotation_header(self, c_file, source_filename):
outlist = [
textwrap.dedent(u'''\
<!DOCTYPE html>
......@@ -120,13 +130,14 @@ class AnnotationCCodeWriter(CCodeWriter):
</script>
</head>
<body class="cython">
<p><span style="border-bottom: solid 1px grey;">Generated by Cython {watermark}</span></p>
<p><span style="border-bottom: solid 1px grey;">Generated by Cython {watermark}</span>{more_info}</p>
<p>
<span style="background-color: #FFFF00">Yellow lines</span> hint at Python interaction.<br />
Click on a line that starts with a "<code>+</code>" to see the C code that Cython generated for it.
</p>
''').format(css=self._css(), js=self._js, watermark=Version.watermark,
filename=os.path.basename(source_filename) if source_filename else '')
filename=os.path.basename(source_filename) if source_filename else '',
more_info=coverage_info)
]
if c_file:
outlist.append(u'<p>Raw output: <a href="%s">%s</a></p>\n' % (c_file, c_file))
......@@ -135,19 +146,43 @@ class AnnotationCCodeWriter(CCodeWriter):
def _save_annotation_footer(self):
return (u'</body></html>\n',)
def _save_annotation(self, code, generated_code, c_file=None, source_filename=None):
def _save_annotation(self, code, generated_code, c_file=None, source_filename=None, coverage_xml=None):
"""
lines : original cython source code split by lines
generated_code : generated c code keyed by line number in original file
target filename : name of the file in which to store the generated html
c_file : filename in which the c_code has been written
"""
if coverage_xml is not None and source_filename:
coverage_timestamp = coverage_xml.get('timestamp', '').strip()
covered_lines = self._get_line_coverage(coverage_xml, source_filename)
else:
coverage_timestamp = covered_lines = None
outlist = []
outlist.extend(self._save_annotation_header(c_file, source_filename))
outlist.extend(self._save_annotation_body(code, generated_code))
outlist.extend(self._save_annotation_header(c_file, source_filename, coverage_timestamp))
outlist.extend(self._save_annotation_body(code, generated_code, covered_lines))
outlist.extend(self._save_annotation_footer())
return ''.join(outlist)
def _get_line_coverage(self, coverage_xml, source_filename):
coverage_data = None
for entry in coverage_xml.iterfind('.//class'):
if not entry.get('filename'):
continue
if (entry.get('filename') == source_filename or
os.path.abspath(entry.get('filename')) == source_filename):
coverage_data = entry
break
elif source_filename.endswith(entry.get('filename')):
coverage_data = entry # but we might still find a better match...
if coverage_data is None:
return None
return dict(
(int(line.get('number')), int(line.get('hits')))
for line in coverage_data.iterfind('lines/line')
)
def _htmlify_code(self, code):
try:
from pygments import highlight
......@@ -162,7 +197,7 @@ class AnnotationCCodeWriter(CCodeWriter):
HtmlFormatter(nowrap=True))
return html_code
def _save_annotation_body(self, cython_code, generated_code):
def _save_annotation_body(self, cython_code, generated_code, covered_lines=None):
outlist = [u'<div class="cython">']
pos_comment_marker = u'/* \N{HORIZONTAL ELLIPSIS} */\n'
new_calls_map = dict(
......@@ -180,6 +215,8 @@ class AnnotationCCodeWriter(CCodeWriter):
lines = self._htmlify_code(cython_code).splitlines()
lineno_width = len(str(len(lines)))
if not covered_lines:
covered_lines = None
for k, line in enumerate(lines, 1):
try:
......@@ -204,13 +241,20 @@ class AnnotationCCodeWriter(CCodeWriter):
onclick = ''
expandsymbol = '&#xA0;'
covered = ''
if covered_lines is not None and k in covered_lines:
hits = covered_lines[k]
if hits is not None:
covered = 'run' if hits else 'mis'
outlist.append(
u"<pre class='cython line score-{score}'{onclick}>"
u'<pre class="cython line score-{score}"{onclick}>'
# generate line number with expand symbol in front,
# and the right number of digit
u"{expandsymbol}{line:0{lineno_width}d}: {code}</pre>\n".format(
u'{expandsymbol}<span class="{covered}">{line:0{lineno_width}d}</span>: {code}</pre>\n'.format(
score=score,
expandsymbol=expandsymbol,
covered=covered,
lineno_width=lineno_width,
line=k,
code=line.rstrip(),
......
......@@ -34,6 +34,7 @@ Options:
-D, --no-docstrings Strip docstrings from the compiled module.
-a, --annotate Produce a colorized HTML version of the source.
--annotate-coverage <cov.xml> Include coverage information from cov.xml in HTML annotation.
--line-directives Produce #line directives pointing to the .pyx source
--cplus Output a C++ rather than C file.
--embed[=<method_name>] Generate a main() function that embeds the Python interpreter.
......@@ -115,6 +116,8 @@ def parse_command_line(args):
Options.docstrings = False
elif option in ("-a", "--annotate"):
Options.annotate = True
elif option == "--annotate-coverage":
Options.annotate_coverage_xml = pop_arg()
elif option == "--convert-range":
Options.convert_range = True
elif option == "--line-directives":
......
......@@ -685,6 +685,7 @@ default_options = dict(
cplus = 0,
output_file = None,
annotate = None,
annotate_coverage_xml = None,
generate_pxi = 0,
capi_reexport_cincludes = 0,
working_path = "",
......
......@@ -359,11 +359,24 @@ class ModuleNode(Nodes.Node, Nodes.BlockNode):
if options.gdb_debug:
self._serialize_lineno_map(env, rootwriter)
if Options.annotate or options.annotate:
self._generate_annotations(rootwriter, result)
self._generate_annotations(rootwriter, result, options)
def _generate_annotations(self, rootwriter, result):
def _generate_annotations(self, rootwriter, result, options):
self.annotate(rootwriter)
rootwriter.save_annotation(result.main_source_file, result.c_file)
coverage_xml_filename = Options.annotate_coverage_xml or options.annotate_coverage_xml
if coverage_xml_filename and os.path.exists(coverage_xml_filename):
try:
import xml.etree.cElementTree as ET
except ImportError:
import xml.etree.ElementTree as ET
coverage_xml = ET.parse(coverage_xml_filename).getroot()
for el in coverage_xml.getiterator():
el.tail = None # save some memory
else:
coverage_xml = None
rootwriter.save_annotation(result.main_source_file, result.c_file, coverage_xml=coverage_xml)
# if we included files, additionally generate one annotation file for each
if not self.scope.included_files:
......@@ -387,7 +400,7 @@ class ModuleNode(Nodes.Node, Nodes.BlockNode):
import errno
if e.errno != errno.EEXIST:
raise
rootwriter.save_annotation(source_file, target_file)
rootwriter.save_annotation(source_file, target_file, coverage_xml=coverage_xml)
def _serialize_lineno_map(self, env, ccodewriter):
tb = env.context.gdb_debug_outputwriter
......
......@@ -21,8 +21,13 @@ docstrings = True
# (when all memory will be reclaimed anyways).
generate_cleanup_code = False
# Generate an annotated HTML version of the input source files.
annotate = False
# When annotating source files in HTML, include coverage information from
# this file.
annotate_coverage_xml = None
# This will abort the compilation on the first error occured rather than trying
# to keep going and printing further error messages.
fast_fail = False
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment