#!/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, ('
[%2d]' % (quoteattr('as ' + ', '.join(as_functions)), len(as_functions))) else: yield 0, ' ' yield c, t if as_functions is not None: yield 0, '
' 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("") paths = sorted(self.files.keys()) for path in paths: fd.write('

%s

' % (self.clean_path(path), escape(path[len(self.prefix):]))) fd.write("") fd.close() def collect_stats(files, fd, pattern): # TODO: Handle compressed callgrind files line_regexs = [ re.compile(r"(?P[0-9]+)(\s[0-9]+)+"), re.compile(r"((jump)|(jcnd))=([0-9]+)\s(?P[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)