CofeehousePy/deps/numpy/tools/c_coverage/c_coverage_report.py

185 lines
5.8 KiB
Python

#!/usr/bin/env python3
"""
A script to create C code-coverage reports based on the output of
valgrind's callgrind tool.
"""
import os
import re
import sys
from xml.sax.saxutils import quoteattr, escape
try:
import pygments
if tuple([int(x) for x in pygments.__version__.split('.')]) < (0, 11):
raise ImportError()
from pygments import highlight
from pygments.lexers import CLexer
from pygments.formatters import HtmlFormatter
has_pygments = True
except ImportError:
print("This script requires pygments 0.11 or greater to generate HTML")
has_pygments = False
class FunctionHtmlFormatter(HtmlFormatter):
"""Custom HTML formatter to insert extra information with the lines."""
def __init__(self, lines, **kwargs):
HtmlFormatter.__init__(self, **kwargs)
self.lines = lines
def wrap(self, source, outfile):
for i, (c, t) in enumerate(HtmlFormatter.wrap(self, source, outfile)):
as_functions = self.lines.get(i-1, None)
if as_functions is not None:
yield 0, ('<div title=%s style="background: #ccffcc">[%2d]' %
(quoteattr('as ' + ', '.join(as_functions)),
len(as_functions)))
else:
yield 0, ' '
yield c, t
if as_functions is not None:
yield 0, '</div>'
class SourceFile:
def __init__(self, path):
self.path = path
self.lines = {}
def mark_line(self, lineno, as_func=None):
line = self.lines.setdefault(lineno, set())
if as_func is not None:
as_func = as_func.split("'", 1)[0]
line.add(as_func)
def write_text(self, fd):
source = open(self.path, "r")
for i, line in enumerate(source):
if i + 1 in self.lines:
fd.write("> ")
else:
fd.write("! ")
fd.write(line)
source.close()
def write_html(self, fd):
source = open(self.path, 'r')
code = source.read()
lexer = CLexer()
formatter = FunctionHtmlFormatter(
self.lines,
full=True,
linenos='inline')
fd.write(highlight(code, lexer, formatter))
source.close()
class SourceFiles:
def __init__(self):
self.files = {}
self.prefix = None
def get_file(self, path):
if path not in self.files:
self.files[path] = SourceFile(path)
if self.prefix is None:
self.prefix = path
else:
self.prefix = os.path.commonprefix([self.prefix, path])
return self.files[path]
def clean_path(self, path):
path = path[len(self.prefix):]
return re.sub(r"[^A-Za-z0-9\.]", '_', path)
def write_text(self, root):
for path, source in self.files.items():
fd = open(os.path.join(root, self.clean_path(path)), "w")
source.write_text(fd)
fd.close()
def write_html(self, root):
for path, source in self.files.items():
fd = open(os.path.join(root, self.clean_path(path) + ".html"), "w")
source.write_html(fd)
fd.close()
fd = open(os.path.join(root, 'index.html'), 'w')
fd.write("<html>")
paths = sorted(self.files.keys())
for path in paths:
fd.write('<p><a href="%s.html">%s</a></p>' %
(self.clean_path(path), escape(path[len(self.prefix):])))
fd.write("</html>")
fd.close()
def collect_stats(files, fd, pattern):
# TODO: Handle compressed callgrind files
line_regexs = [
re.compile(r"(?P<lineno>[0-9]+)(\s[0-9]+)+"),
re.compile(r"((jump)|(jcnd))=([0-9]+)\s(?P<lineno>[0-9]+)")
]
current_file = None
current_function = None
for i, line in enumerate(fd):
if re.match("f[lie]=.+", line):
path = line.split('=', 2)[1].strip()
if os.path.exists(path) and re.search(pattern, path):
current_file = files.get_file(path)
else:
current_file = None
elif re.match("fn=.+", line):
current_function = line.split('=', 2)[1].strip()
elif current_file is not None:
for regex in line_regexs:
match = regex.match(line)
if match:
lineno = int(match.group('lineno'))
current_file.mark_line(lineno, current_function)
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
'callgrind_file', nargs='+',
help='One or more callgrind files')
parser.add_argument(
'-d', '--directory', default='coverage',
help='Destination directory for output (default: %(default)s)')
parser.add_argument(
'-p', '--pattern', default='numpy',
help='Regex pattern to match against source file paths '
'(default: %(default)s)')
parser.add_argument(
'-f', '--format', action='append', default=[],
choices=['text', 'html'],
help="Output format(s) to generate. "
"If option not provided, both will be generated.")
args = parser.parse_args()
files = SourceFiles()
for log_file in args.callgrind_file:
log_fd = open(log_file, 'r')
collect_stats(files, log_fd, args.pattern)
log_fd.close()
if not os.path.exists(args.directory):
os.makedirs(args.directory)
if args.format == []:
formats = ['text', 'html']
else:
formats = args.format
if 'text' in formats:
files.write_text(args.directory)
if 'html' in formats:
if not has_pygments:
print("Pygments 0.11 or later is required to generate HTML")
sys.exit(1)
files.write_html(args.directory)