mirror of
https://github.com/tahoe-lafs/tahoe-lafs.git
synced 2024-12-30 17:56:58 +00:00
355 lines
13 KiB
Python
355 lines
13 KiB
Python
#! /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('<title>figleaf code coverage report</title>\n')
|
|
index_fp.write('<h2>Summary</h2> %d files total: %d files > '
|
|
'90%%, %d files > 75%%, %d files > 50%%<p>'
|
|
% (len(pcnts), len(pcnt_90),
|
|
len(pcnt_75), len(pcnt_50)))
|
|
|
|
# sorted by number of lines that aren't covered
|
|
index_fp.write('<h3>Sorted by Lines Uncovered</h3>\n')
|
|
self.emit_table(index_fp, info_dict_items, show_totals=True)
|
|
|
|
if self.old_coverage is not None:
|
|
index_fp.write('<h3>Sorted by Coverage Added/Lost</h3>\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('<h3>Sorted by Module Name (alphabetical)</h3>\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('<font color="%s">%s%4d. %s</font>' %
|
|
(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: <b>%s</b><br>\n' % (k,))
|
|
html_outfp.write('file stats: <b>%d lines, %d executed: %.1f%% covered</b><br>\n' % (n_lines, n_covered, pcnt))
|
|
if have_old_coverage:
|
|
html_outfp.write('coverage versus previous test: <b>%d lines added, %d lines removed</b><br>\n'
|
|
% (n_added, n_removed))
|
|
|
|
html_outfp.write('<pre>\n')
|
|
for line in output:
|
|
html_outfp.write(line + "\n")
|
|
html_outfp.write('</pre>\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('<table border=1><tr><th>Filename</th>'
|
|
'<th># lines</th><th># covered</th>'
|
|
'<th># uncovered</th>'
|
|
'<th># added</th>'
|
|
'<th># removed</th>'
|
|
'<th>% covered</th></tr>\n')
|
|
else:
|
|
index_fp.write('<table border=1><tr><th>Filename</th>'
|
|
'<th># lines</th><th># covered</th>'
|
|
'<th># uncovered</th>'
|
|
'<th>% covered</th></tr>\n')
|
|
if show_totals:
|
|
(summary_lines, summary_cover,
|
|
summary_added, summary_removed,
|
|
summary_pcnt) = self.summary
|
|
if have_old_coverage:
|
|
index_fp.write('<tr><td><b>totals:</b></td>'
|
|
'<td><b>%d</b></td>' # lines
|
|
'<td><b>%d</b></td>' # cover
|
|
'<td><b>%d</b></td>' # uncover
|
|
'<td><b>%d</b></td>' # added
|
|
'<td><b>%d</b></td>' # removed
|
|
'<td><b>%.1f%%</b></td>'
|
|
'</tr>'
|
|
'<tr></tr>\n'
|
|
% (summary_lines, summary_cover,
|
|
(summary_lines - summary_cover),
|
|
summary_added, summary_removed,
|
|
summary_pcnt,))
|
|
else:
|
|
index_fp.write('<tr><td><b>totals:</b></td>'
|
|
'<td><b>%d</b></td>'
|
|
'<td><b>%d</b></td>'
|
|
'<td><b>%d</b></td>'
|
|
'<td><b>%.1f%%</b></td>'
|
|
'</tr>'
|
|
'<tr></tr>\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('</table>\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('<tr><td><a href="./%s">%s</a></td>'
|
|
'<td>%d</td>' # lines
|
|
'<td>%d</td>' # covered
|
|
'<td>%d</td>' # uncovered
|
|
'<td>%d</td>' # added
|
|
'<td>%d</td>' # removed
|
|
'<td>%.1f</td>'
|
|
'</tr>\n'
|
|
% (html_outfile, display_filename, n_lines,
|
|
n_covered, (n_lines - n_covered),
|
|
n_added, n_removed,
|
|
percent_covered,))
|
|
else:
|
|
index_fp.write('<tr><td><a href="./%s">%s</a></td>'
|
|
'<td>%d</td>'
|
|
'<td>%d</td>'
|
|
'<td>%d</td>'
|
|
'<td>%.1f</td>'
|
|
'</tr>\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()
|