#! /usr/bin/env python import sys import pickle import figleaf import os import re from twisted.python import usage class RenderOptions(usage.Options): optParameters = [ ("exclude-patterns", "x", None, "file containing regexp patterns to exclude"), ("output-directory", "d", "html", "Directory for HTML output"), ("root", "r", None, "only pay attention to modules under this directory"), ("old-coverage", "o", None, "figleaf pickle from previous build"), ] def opt_root(self, value): self["root"] = os.path.abspath(value) if not self["root"].endswith("/"): self["root"] += "/" def parseArgs(self, *filenames): self.filenames = [".figleaf"] if filenames: self.filenames = list(filenames) class Renderer: def run(self): self.opts = opts = RenderOptions() opts.parseOptions() ### load coverage = {} for filename in opts.filenames: d = figleaf.read_coverage(filename) coverage = figleaf.combine_coverage(coverage, d) if not coverage: sys.exit(-1) self.old_coverage = None if opts["old-coverage"]: try: f = open(opts["old-coverage"], "rb") self.old_coverage = pickle.load(f) except EnvironmentError: pass self.load_exclude_patterns(opts["exclude-patterns"]) ### make directory self.prepare_reportdir(opts["output-directory"]) self.report_as_html(coverage, opts["output-directory"], opts["root"]) def load_exclude_patterns(self, f): self.exclude_patterns = [] if not f: return for line in open(f, "r").readlines(): line = line.rstrip() if line and not line.startswith('#'): self.exclude_patterns.append(re.compile(line)) def prepare_reportdir(self, dirname='html'): try: os.mkdir(dirname) except OSError: # already exists pass def check_excludes(self, fn): for pattern in self.exclude_patterns: if pattern.search(fn): return True return False def make_display_filename(self, fn): root = self.opts["root"] if not root: return fn display_filename = fn[len(root):] assert not display_filename.startswith("/") assert display_filename.endswith(".py") display_filename = display_filename[:-3] # trim .py display_filename = display_filename.replace("/", ".") return display_filename def report_as_html(self, coverage, directory, root=None): ### now, output. keys = coverage.keys() info_dict = {} for k in keys: if self.check_excludes(k): continue if k.endswith('figleaf.py'): continue if not k.startswith("/"): continue if root and not k.startswith(root): continue display_filename = self.make_display_filename(k) info = self.process_file(k, display_filename, coverage) if info: info_dict[k] = info ### print a summary, too. #print info_dict info_dict_items = info_dict.items() def sort_by_pcnt(a, b): a_cmp = (-a[1][4], a[1][5]) b_cmp = (-b[1][4], b[1][5]) return cmp(a_cmp,b_cmp) def sort_by_uncovered(a, b): a_cmp = ( -(a[1][0] - a[1][1]), a[1][5]) b_cmp = ( -(b[1][0] - b[1][1]), b[1][5]) return cmp(a_cmp, b_cmp) def sort_by_delta(a, b): # files which lost coverage line should appear first, followed by # files which gained coverage a_cmp = (-a[1][3], -a[1][2], a[1][5]) b_cmp = (-b[1][3], -b[1][2], b[1][5]) return cmp(a_cmp, b_cmp) info_dict_items.sort(sort_by_uncovered) summary_lines = sum([ v[0] for (k, v) in info_dict_items]) summary_cover = sum([ v[1] for (k, v) in info_dict_items]) summary_added = sum([ v[2] for (k, v) in info_dict_items]) summary_removed = sum([ v[3] for (k, v) in info_dict_items]) summary_pcnt = 0 if summary_lines: summary_pcnt = float(summary_cover) * 100. / float(summary_lines) self.summary = (summary_lines, summary_cover, summary_added, summary_removed, summary_pcnt) pcnts = [ float(v[1]) * 100. / float(v[0]) for (k, v) in info_dict_items if v[0] ] pcnt_90 = [ x for x in pcnts if x >= 90 ] pcnt_75 = [ x for x in pcnts if x >= 75 ] pcnt_50 = [ x for x in pcnts if x >= 50 ] stats_fp = open('%s/stats.out' % (directory,), 'w') self.write_stats(stats_fp, "total files: %d" % len(pcnts)) self.write_stats(stats_fp, "total source lines: %d" % summary_lines) self.write_stats(stats_fp, "total covered lines: %d" % summary_cover) self.write_stats(stats_fp, "total uncovered lines: %d" % (summary_lines - summary_cover)) if self.old_coverage is not None: self.write_stats(stats_fp, "lines added: %d" % summary_added) self.write_stats(stats_fp, "lines removed: %d" % summary_removed) self.write_stats(stats_fp, "total coverage percentage: %.1f" % summary_pcnt) stats_fp.close() ## index.html index_fp = open('%s/index.html' % (directory,), 'w') # summary info index_fp.write('figleaf code coverage report\n') index_fp.write('

Summary

%d files total: %d files > ' '90%%, %d files > 75%%, %d files > 50%%

' % (len(pcnts), len(pcnt_90), len(pcnt_75), len(pcnt_50))) # sorted by number of lines that aren't covered index_fp.write('

Sorted by Lines Uncovered

\n') self.emit_table(index_fp, info_dict_items, show_totals=True) if self.old_coverage is not None: index_fp.write('

Sorted by Coverage Added/Lost

\n') info_dict_items.sort(sort_by_delta) self.emit_table(index_fp, info_dict_items, show_totals=False) # sorted by module name index_fp.write('

Sorted by Module Name (alphabetical)

\n') info_dict_items.sort() self.emit_table(index_fp, info_dict_items, show_totals=False) index_fp.close() return len(info_dict) def process_file(self, k, display_filename, coverage): try: pyfile = open(k) except IOError: return source_lines = figleaf.get_lines(pyfile) have_old_coverage = False if self.old_coverage and k in self.old_coverage: have_old_coverage = True old_coverage = self.old_coverage[k] # ok, got all the info. now annotate file ==> html. covered = coverage[k] n_covered = n_lines = 0 n_added = n_removed = 0 pyfile = open(k) output = [] for i, line in enumerate(pyfile): i += 1 # coverage info is 1-based if i in covered: color = "green" n_covered += 1 n_lines += 1 elif i in source_lines: color = "red" n_lines += 1 else: color = "black" delta = " " if have_old_coverage: if i in covered and i not in old_coverage: delta = "+" n_added += 1 elif i in old_coverage and i not in covered: delta = "-" n_removed += 1 line = self.escape_html(line.rstrip()) output.append('%s%4d. %s' % (color, delta, i, line.rstrip())) try: pcnt = n_covered * 100. / n_lines except ZeroDivisionError: pcnt = 0 html_outfile = self.make_html_filename(display_filename) directory = self.opts["output-directory"] html_outfp = open(os.path.join(directory, html_outfile), 'w') html_outfp.write('source file: %s
\n' % (k,)) html_outfp.write('file stats: %d lines, %d executed: %.1f%% covered
\n' % (n_lines, n_covered, pcnt)) if have_old_coverage: html_outfp.write('coverage versus previous test: %d lines added, %d lines removed
\n' % (n_added, n_removed)) html_outfp.write('
\n')
        for line in output:
            html_outfp.write(line + "\n")
        html_outfp.write('
\n') html_outfp.close() return (n_lines, n_covered, n_added, n_removed, pcnt, display_filename) def emit_table(self, index_fp, items, show_totals): have_old_coverage = self.old_coverage is not None if have_old_coverage: index_fp.write('' '' '' '' '' '\n') else: index_fp.write('
Filename# lines# covered# uncovered# added# removed% covered
' '' '' '\n') if show_totals: (summary_lines, summary_cover, summary_added, summary_removed, summary_pcnt) = self.summary if have_old_coverage: index_fp.write('' '' # lines '' # cover '' # uncover '' # added '' # removed '' '' '\n' % (summary_lines, summary_cover, (summary_lines - summary_cover), summary_added, summary_removed, summary_pcnt,)) else: index_fp.write('' '' '' '' '' '' '\n' % (summary_lines, summary_cover, (summary_lines - summary_cover), summary_pcnt,)) for filename, stuff in items: self.emit_table_row(index_fp, stuff) index_fp.write('
Filename# lines# covered# uncovered% covered
totals:%d%d%d%d%d%.1f%%
totals:%d%d%d%.1f%%
\n') def emit_table_row(self, index_fp, info): (n_lines, n_covered, n_added, n_removed, percent_covered, display_filename) = info html_outfile = self.make_html_filename(display_filename) if self.old_coverage is not None: index_fp.write('%s' '%d' # lines '%d' # covered '%d' # uncovered '%d' # added '%d' # removed '%.1f' '\n' % (html_outfile, display_filename, n_lines, n_covered, (n_lines - n_covered), n_added, n_removed, percent_covered,)) else: index_fp.write('%s' '%d' '%d' '%d' '%.1f' '\n' % (html_outfile, display_filename, n_lines, n_covered, (n_lines - n_covered), percent_covered,)) def make_html_filename(self, orig): return orig + ".html" def escape_html(self, s): s = s.replace("&", "&") s = s.replace("<", "<") s = s.replace(">", ">") s = s.replace('"', """) return s def write_stats(self, stats_fp, line): stats_fp.write(line + "\n") print line def main(): r = Renderer() r.run()