summaryrefslogtreecommitdiff
path: root/tools/c_coverage/c_coverage_report.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/c_coverage/c_coverage_report.py')
-rwxr-xr-xtools/c_coverage/c_coverage_report.py182
1 files changed, 182 insertions, 0 deletions
diff --git a/tools/c_coverage/c_coverage_report.py b/tools/c_coverage/c_coverage_report.py
new file mode 100755
index 000000000..0c9574511
--- /dev/null
+++ b/tools/c_coverage/c_coverage_report.py
@@ -0,0 +1,182 @@
+#!/usr/bin/env python
+"""
+A script to create C code-coverage reports based on the output of
+valgrind's callgrind tool.
+"""
+
+import optparse
+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.readlines()):
+ 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("[^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 = self.files.keys()
+ paths.sort()
+ 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("(?P<lineno>[0-9]+)(\s[0-9]+)+"),
+ re.compile("((jump)|(jcnd))=([0-9]+)\s(?P<lineno>[0-9]+)")
+ ]
+
+ current_file = None
+ current_function = None
+ for i, line in enumerate(fd.readlines()):
+ 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__':
+ parser = optparse.OptionParser(
+ usage="[options] callgrind_file(s)")
+ parser.add_option(
+ '-d', '--directory', dest='directory',
+ default='coverage',
+ help='Destination directory for output [default: coverage]')
+ parser.add_option(
+ '-p', '--pattern', dest='pattern',
+ default='numpy',
+ help='Regex pattern to match against source file paths [default: numpy]')
+ parser.add_option(
+ '-f', '--format', dest='format', default=[],
+ action='append', type='choice', choices=('text', 'html'),
+ help="Output format(s) to generate, may be 'text' or 'html' [default: both]")
+ (options, args) = parser.parse_args()
+
+ files = SourceFiles()
+ for log_file in args:
+ log_fd = open(log_file, 'r')
+ collect_stats(files, log_fd, options.pattern)
+ log_fd.close()
+
+ if not os.path.exists(options.directory):
+ os.makedirs(options.directory)
+
+ if options.format == []:
+ formats = ['text', 'html']
+ else:
+ formats = options.format
+ if 'text' in formats:
+ files.write_text(options.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(options.directory)