#! /usr/bin/env python import sys import figleaf import os import re from optparse import OptionParser import logging logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger('figleaf.htmlizer') def read_exclude_patterns(f): if not f: return [] exclude_patterns = [] fp = open(f) for line in fp: line = line.rstrip() if line and not line.startswith('#'): pattern = re.compile(line) exclude_patterns.append(pattern) return exclude_patterns def report_as_html(coverage, directory, exclude_patterns=[], root=None): ### now, output. keys = coverage.keys() info_dict = {} for k in keys: skip = False for pattern in exclude_patterns: if pattern.search(k): logger.debug('SKIPPING %s -- matches exclusion pattern' % k) skip = True break if skip: continue if k.endswith('figleaf.py'): continue display_filename = k if root: if not k.startswith(root): continue display_filename = k[len(root):] assert not display_filename.startswith("/") assert display_filename.endswith(".py") display_filename = display_filename[:-3] # trim .py display_filename = display_filename.replace("/", ".") if not k.startswith("/"): continue try: pyfile = open(k) #print 'opened', k except IOError: logger.warning('CANNOT OPEN: %s' % k) continue try: lines = figleaf.get_lines(pyfile) except KeyboardInterrupt: raise except Exception, e: pyfile.close() logger.warning('ERROR: %s %s' % (k, str(e))) continue # ok, got all the info. now annotate file ==> html. covered = coverage[k] n_covered = n_lines = 0 pyfile = open(k) output = [] for i, line in enumerate(pyfile): is_covered = False is_line = False i += 1 if i in covered: is_covered = True n_covered += 1 n_lines += 1 elif i in lines: is_line = True n_lines += 1 color = 'black' if is_covered: color = 'green' elif is_line: color = 'red' line = escape_html(line.rstrip()) output.append('%4d. %s' % (color, i, line.rstrip())) try: pcnt = n_covered * 100. / n_lines except ZeroDivisionError: pcnt = 0 info_dict[k] = (n_lines, n_covered, pcnt, display_filename) html_outfile = make_html_filename(display_filename) 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)) html_outfp.write('
\n')
        html_outfp.write("\n".join(output))
        html_outfp.close()

    ### print a summary, too.

    info_dict_items = info_dict.items()

    def sort_by_pcnt(a, b):
        a = a[1][2]
        b = b[1][2]
        return -cmp(a,b)

    def sort_by_uncovered(a, b):
        a_uncovered = a[1][0] - a[1][1]
        b_uncovered = b[1][0] - b[1][1]
        return -cmp(a_uncovered, b_uncovered)

    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_pcnt = 0
    if summary_lines:
        summary_pcnt = float(summary_cover) * 100. / float(summary_lines)


    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')
    stats_fp.write("total files: %d\n" % len(pcnts))
    stats_fp.write("total source lines: %d\n" % summary_lines)
    stats_fp.write("total covered lines: %d\n" % summary_cover)
    stats_fp.write("total uncovered lines: %d\n" %
                   (summary_lines - summary_cover))
    stats_fp.write("total coverage percentage: %.1f\n" % 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))) def emit_table(items, show_totals): index_fp.write('' '' '' '\n') if show_totals: index_fp.write('' '' '' '' '' '' '\n' % (summary_lines, summary_cover, (summary_lines - summary_cover), summary_pcnt,)) for filename, stuff in items: (n_lines, n_covered, percent_covered, display_filename) = stuff html_outfile = make_html_filename(display_filename) index_fp.write('' '' '\n' % (html_outfile, display_filename, n_lines, n_covered, (n_lines - n_covered), percent_covered,)) index_fp.write('
Filename# lines# covered# uncovered% covered
totals:%d%d%d%.1f%%
%s%d%d%d%.1f
\n') # sorted by number of lines that aren't covered index_fp.write('

Sorted by Lines Uncovered

\n') emit_table(info_dict_items, True) # sorted by module name index_fp.write('

Sorted by Module Name (alphabetical)

\n') info_dict_items.sort() emit_table(info_dict_items, False) index_fp.close() logger.info('reported on %d file(s) total\n' % len(info_dict)) return len(info_dict) def prepare_reportdir(dirname='html'): try: os.mkdir(dirname) except OSError: # already exists pass def make_html_filename(orig): return orig + ".html" def escape_html(s): s = s.replace("&", "&") s = s.replace("<", "<") s = s.replace(">", ">") s = s.replace('"', """) return s def main(): ### option_parser = OptionParser() option_parser.add_option('-x', '--exclude-patterns', action="store", dest="exclude_patterns_file", help="file containing regexp patterns to exclude") option_parser.add_option('-d', '--output-directory', action='store', dest="output_dir", default = "html", help="directory for HTML output") option_parser.add_option('-r', '--root', action="store", dest="root", default=None, help="only pay attention to modules under this directory") option_parser.add_option('-q', '--quiet', action='store_true', dest='quiet', help='Suppress all but error messages') (options, args) = option_parser.parse_args() if options.quiet: logging.disable(logging.DEBUG) if options.root: options.root = os.path.abspath(options.root) if options.root[-1] != "/": options.root = options.root + "/" ### load if not args: args = ['.figleaf'] coverage = {} for filename in args: logger.debug("loading coverage info from '%s'\n" % (filename,)) d = figleaf.read_coverage(filename) coverage = figleaf.combine_coverage(coverage, d) if not coverage: logger.warning('EXITING -- no coverage info!\n') sys.exit(-1) ### make directory prepare_reportdir(options.output_dir) report_as_html(coverage, options.output_dir, read_exclude_patterns(options.exclude_patterns_file), options.root)