[Checkins] SVN: z3c.coverage/trunk/src/z3c/coverage/ More tests.
Another set of sample data so we can test comparisons of
Marius Gedminas
marius at pov.lt
Thu Jul 19 16:20:27 EDT 2007
Log message for revision 78192:
More tests. Another set of sample data so we can test comparisons of
directories.
Changed:
U z3c.coverage/trunk/src/z3c/coverage/coveragediff.txt
A z3c.coverage/trunk/src/z3c/coverage/moresampleinput/
A z3c.coverage/trunk/src/z3c/coverage/moresampleinput/z3c.coverage.__init__.cover
A z3c.coverage/trunk/src/z3c/coverage/moresampleinput/z3c.coverage.coveragediff.cover
A z3c.coverage/trunk/src/z3c/coverage/moresampleinput/z3c.coverage.coveragereport.cover
A z3c.coverage/trunk/src/z3c/coverage/moresampleinput/z3c.coverage.fakenewmodule.cover
A z3c.coverage/trunk/src/z3c/coverage/moresampleinput/z3c.coverage.tests.cover
-=-
Modified: z3c.coverage/trunk/src/z3c/coverage/coveragediff.txt
===================================================================
--- z3c.coverage/trunk/src/z3c/coverage/coveragediff.txt 2007-07-19 20:05:03 UTC (rev 78191)
+++ z3c.coverage/trunk/src/z3c/coverage/coveragediff.txt 2007-07-19 20:20:27 UTC (rev 78192)
@@ -58,10 +58,70 @@
>>> from z3c.coverage.coveragediff import count_coverage
>>> filename = os.path.join(sampleinput_dir, 'z3c.coverage.tests.cover')
- >>> count_coverage(filename)
- (10, 3)
+ >>> tested, untested = count_coverage(filename)
+ >>> tested
+ 10
+ >>> untested
+ 3
+Comparing coverage files
+------------------------
+
+The function ``compare_file`` reads two coverage reports for the same module
+and reports a warning if the new file has more untested lines of code
+
+ >>> from z3c.coverage.coveragediff import compare_file
+ >>> another_dir = os.path.join(z3c.coverage.__path__[0], 'moresampleinput')
+ >>> old_filename = os.path.join(sampleinput_dir,
+ ... 'z3c.coverage.coveragediff.cover')
+ >>> new_filename = os.path.join(another_dir,
+ ... 'z3c.coverage.coveragediff.cover')
+ >>> compare_file(old_filename, new_filename)
+ z3c.coverage.coveragediff: 36 new lines of untested code
+
+If the number of untested lines is the same or smaller than before, there's
+no output
+
+ >>> compare_file(new_filename, new_filename)
+ >>> compare_file(new_filename, old_filename)
+
+The function ``new_file`` is used to look for untested lines of code in new
+modules.
+
+ >>> from z3c.coverage.coveragediff import new_file
+ >>> new_filename = os.path.join(another_dir,
+ ... 'z3c.coverage.fakenewmodule.cover')
+ >>> new_file(new_filename)
+ z3c.coverage.fakenewmodule: new file with 3 lines of untested code (out of 13)
+
+Once again, if there are no untested lines, ``new_file`` is quiet
+
+ >>> new_filename = os.path.join(another_dir,
+ ... 'z3c.coverage.faketestedmodule.cover')
+ >>> new_file(new_filename)
+
+
+Comparing directories
+---------------------
+
+``compare_dirs`` ties it all together: you pass in two directory names, you get
+a bunch of warnings about regressions
+
+ >>> from z3c.coverage.coveragediff import compare_dirs
+ >>> compare_dirs(sampleinput_dir, another_dir)
+ z3c.coverage.coveragediff: 36 new lines of untested code
+ z3c.coverage.fakenewmodule: new file with 3 lines of untested code (out of 13)
+
+You can pass ``include`` and ``exclude`` arguments as well
+
+ >>> compare_dirs(sampleinput_dir, another_dir, exclude=['[Ff]ake'])
+ z3c.coverage.coveragediff: 36 new lines of untested code
+
+ >>> compare_dirs(sampleinput_dir, another_dir, include=['d.ff'])
+ z3c.coverage.coveragediff: 36 new lines of untested code
+
+
MailSender
----------
Added: z3c.coverage/trunk/src/z3c/coverage/moresampleinput/z3c.coverage.__init__.cover
===================================================================
--- z3c.coverage/trunk/src/z3c/coverage/moresampleinput/z3c.coverage.__init__.cover (rev 0)
+++ z3c.coverage/trunk/src/z3c/coverage/moresampleinput/z3c.coverage.__init__.cover 2007-07-19 20:20:27 UTC (rev 78192)
@@ -0,0 +1 @@
+ 1: # Make a package.
Added: z3c.coverage/trunk/src/z3c/coverage/moresampleinput/z3c.coverage.coveragediff.cover
===================================================================
--- z3c.coverage/trunk/src/z3c/coverage/moresampleinput/z3c.coverage.coveragediff.cover (rev 0)
+++ z3c.coverage/trunk/src/z3c/coverage/moresampleinput/z3c.coverage.coveragediff.cover 2007-07-19 20:20:27 UTC (rev 78192)
@@ -0,0 +1,346 @@
+ #!/usr/bin/env python
+ ##############################################################################
+ #
+ # Copyright (c) 2007 Zope Foundation and Contributors.
+ # All Rights Reserved.
+ #
+ # This software is subject to the provisions of the Zope Public License,
+ # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+ # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+ # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+ # FOR A PARTICULAR PURPOSE.
+ #
+ ##############################################################################
+ """
+ Compare the test coverage between two versions. The promary goal is to find
+ regressions in test coverage (newly added lines of code without tests, or
+ old lines of code that used to have tests but don't any more).
+
+ Usage: coveragediff.py [options] old-dir new-dir
+
+ The directories are expected to contain files named '<package>.<module>.cover'
+ with the format that Python's trace.py produces.
+ 1: """
+
+ 1: import os
+ 1: import re
+ 1: import sys
+ 1: import smtplib
+ 1: import optparse
+ 1: from email.MIMEText import MIMEText
+
+
+ 1: try:
+ 1: any
+>>>>>> except NameError:
+ # python 2.4 compatibility
+>>>>>> def any(list):
+ """Return True if bool(x) is True for any x in the iterable.
+
+ >>> any([1, 'yes', 0, None])
+ True
+ >>> any([0, None, ''])
+ False
+ >>> any([])
+ False
+
+ """
+>>>>>> for item in list:
+>>>>>> if item:
+>>>>>> return True
+>>>>>> return False
+
+
+ 1: def matches(string, list_of_regexes):
+ """Check whether a string matches any of a list of regexes.
+
+ >>> matches('foo', map(re.compile, ['x', 'o']))
+ True
+ >>> matches('foo', map(re.compile, ['x', 'f$']))
+ False
+ >>> matches('foo', [])
+ False
+
+ """
+ 2: return any(regex.search(string) for regex in list_of_regexes)
+
+
+ 1: def filter_files(files, include=(), exclude=()):
+ """Filters a file list by considering only the include patterns, then
+ excluding exclude patterns. Patterns are regular expressions.
+
+ Examples:
+
+ >>> filter_files(['ivija.food', 'ivija.food.tests', 'other.ivija'],
+ ... include=['^ivija'], exclude=['tests'])
+ ['ivija.food']
+
+ >>> filter_files(['ivija.food', 'ivija.food.tests', 'other.ivija'],
+ ... exclude=['tests'])
+ ['ivija.food', 'other.ivija']
+
+ >>> filter_files(['ivija.food', 'ivija.food.tests', 'other.ivija'],
+ ... include=['^ivija'])
+ ['ivija.food', 'ivija.food.tests']
+
+ >>> filter_files(['ivija.food', 'ivija.food.tests', 'other.ivija'])
+ ['ivija.food', 'ivija.food.tests', 'other.ivija']
+
+ """
+ 1: if not include:
+ 1: include = ['.'] # include everything by default
+ 1: if not exclude:
+ 1: exclude = [] # exclude nothing by default
+ 1: include = map(re.compile, include)
+ 1: exclude = map(re.compile, exclude)
+ 1: return [fn for fn in files
+ 1: if matches(fn, include) and not matches(fn, exclude)]
+
+
+ 1: def find_coverage_files(dir):
+ """Find all test coverage files in a given directory.
+
+ The files are expected to end in '.cover'. Weird filenames produced
+ by tracing "fake" code (like '<doctest ...>') are ignored.
+ """
+ 2: return [fn for fn in os.listdir(dir)
+ 14: if fn.endswith('.cover') and not fn.startswith('<')]
+
+
+ 1: def filter_coverage_files(dir, include=(), exclude=()):
+ """Find test coverage files in a given directory matching given patterns.
+
+ The files are expected to end in '.cover'. Weird filenames produced
+ by tracing "fake" code (like '<doctest ...>') are ignored.
+
+ Include/exclude patterns are regular expressions. Include patterns
+ are considered first, then the results are trimmed by the exclude
+ patterns.
+ """
+ 1: return filter_files(find_coverage_files(dir), include, exclude)
+
+
+ 1: def warn(filename, message):
+ """Warn about test coverage regression.
+
+ >>> warn('/tmp/z3c.somepkg.cover', '5 untested lines, ouch!')
+ z3c.somepkg: 5 untested lines, ouch!
+
+ """
+>>>>>> module = strip(os.path.basename(filename), '.cover')
+>>>>>> print '%s: %s' % (module, message)
+
+
+ 1: def compare_dirs(olddir, newdir, include=(), exclude=(), warn=warn):
+ """Compare two directories of coverage files."""
+>>>>>> old_coverage_files = filter_coverage_files(olddir, include, exclude)
+>>>>>> new_coverage_files = filter_coverage_files(newdir, include, exclude)
+
+>>>>>> old_coverage_set = set(old_coverage_files)
+>>>>>> for fn in sorted(new_coverage_files):
+>>>>>> if fn in old_coverage_files:
+>>>>>> compare_file(os.path.join(olddir, fn),
+>>>>>> os.path.join(newdir, fn), warn=warn)
+ else:
+>>>>>> new_file(os.path.join(newdir, fn), warn=warn)
+
+
+ 1: def count_coverage(filename):
+ """Count the number of covered and uncovered lines in a file."""
+>>>>>> covered = uncovered = 0
+>>>>>> for line in file(filename):
+>>>>>> if line.startswith('>>>>>>'):
+>>>>>> uncovered += 1
+>>>>>> elif len(line) >= 7 and not line.startswith(' '*7):
+>>>>>> covered += 1
+>>>>>> return covered, uncovered
+
+
+ 1: def compare_file(oldfile, newfile, warn=warn):
+ """Compare two coverage files."""
+>>>>>> old_covered, old_uncovered = count_coverage(oldfile)
+>>>>>> new_covered, new_uncovered = count_coverage(newfile)
+>>>>>> if new_uncovered > old_uncovered:
+>>>>>> increase = new_uncovered - old_uncovered
+>>>>>> warn(newfile, "%d new lines of untested code" % increase)
+
+
+ 1: def new_file(newfile, warn=warn):
+ """Look for uncovered lines in a new coverage file."""
+>>>>>> covered, uncovered = count_coverage(newfile)
+>>>>>> if uncovered:
+>>>>>> total = covered + uncovered
+>>>>>> msg = "new file with %d lines of untested code (out of %d)" % (
+>>>>>> uncovered, total)
+>>>>>> warn(newfile, msg)
+
+
+ 1: def strip(string, suffix):
+ """Strip a suffix from a string if it exists:
+
+ >>> strip('go bar a foobar', 'bar')
+ 'go bar a foo'
+ >>> strip('go bar a foobar', 'baz')
+ 'go bar a foobar'
+ >>> strip('allofit', 'allofit')
+ ''
+
+ """
+>>>>>> if string.endswith(suffix):
+>>>>>> string = string[:-len(suffix)]
+>>>>>> return string
+
+
+ 1: def urljoin(base, *suburls):
+ """Join base URL and zero or more subURLs.
+
+ This function is best described by examples:
+
+ >>> urljoin('http://example.com')
+ 'http://example.com/'
+
+ >>> urljoin('http://example.com/')
+ 'http://example.com/'
+
+ >>> urljoin('http://example.com', 'a', 'b/c', 'd')
+ 'http://example.com/a/b/c/d'
+
+ >>> urljoin('http://example.com/', 'a', 'b/c', 'd')
+ 'http://example.com/a/b/c/d'
+
+ >>> urljoin('http://example.com/a', 'b/c', 'd')
+ 'http://example.com/a/b/c/d'
+
+ >>> urljoin('http://example.com/a/', 'b/c', 'd')
+ 'http://example.com/a/b/c/d'
+
+ SubURLs should not contain trailing or leading slashes (with one exception:
+ the last subURL may have a trailing slash). SubURLs should not be empty.
+ """
+>>>>>> if not base.endswith('/'):
+>>>>>> base += '/'
+>>>>>> return base + '/'.join(suburls)
+
+
+ 2: class MailSender(object):
+ 1: """Send emails over SMTP"""
+
+ 1: connection_class = smtplib.SMTP
+
+ 1: def __init__(self, smtp_host='localhost', smtp_port=25):
+>>>>>> self.smtp_host = smtp_host
+>>>>>> self.smtp_port = smtp_port
+
+ 1: def send_email(self, from_addr, to_addr, subject, body):
+ """Send an email."""
+ # Note that this won't handle non-ASCII characters correctly.
+ # See http://mg.pov.lt/blog/unicode-emails-in-python.html
+>>>>>> msg = MIMEText(body)
+>>>>>> if from_addr:
+>>>>>> msg['From'] = from_addr
+>>>>>> if to_addr:
+>>>>>> msg['To'] = to_addr
+>>>>>> msg['Subject'] = subject
+>>>>>> smtp = self.connection_class(self.smtp_host, self.smtp_port)
+>>>>>> smtp.sendmail(from_addr, to_addr, msg.as_string())
+>>>>>> smtp.quit()
+
+
+ 2: class ReportPrinter(object):
+ 1: """Reporter to sys.stdout."""
+
+ 1: def __init__(self, web_url=None):
+>>>>>> self.web_url = web_url
+
+ 1: def warn(self, filename, message):
+ """Warn about test coverage regression."""
+>>>>>> module = strip(os.path.basename(filename), '.cover')
+>>>>>> print '%s: %s' % (module, message)
+>>>>>> if self.web_url:
+>>>>>> url = urljoin(self.web_url, module + '.html')
+>>>>>> print 'See ' + url
+>>>>>> print
+
+
+ 2: class ReportEmailer(object):
+ 1: """Warning collector and emailer."""
+
+ 1: def __init__(self, from_addr, to_addr, subject, web_url=None,
+ 1: mailer=None):
+>>>>>> if not mailer:
+>>>>>> mailer = MailSender()
+>>>>>> self.from_addr = from_addr
+>>>>>> self.to_addr = to_addr
+>>>>>> self.subject = subject
+>>>>>> self.web_url = web_url
+>>>>>> self.mailer = mailer
+>>>>>> self.warnings = []
+
+ 1: def warn(self, filename, message):
+ """Warn about test coverage regression."""
+>>>>>> module = strip(os.path.basename(filename), '.cover')
+>>>>>> self.warnings.append('%s: %s' % (module, message))
+>>>>>> if self.web_url:
+>>>>>> url = urljoin(self.web_url, module + '.html')
+>>>>>> self.warnings.append('See ' + url + '\n')
+
+ 1: def send(self):
+ """Send the warnings (if any)."""
+>>>>>> if self.warnings:
+>>>>>> body = '\n'.join(self.warnings)
+>>>>>> self.mailer.send_email(self.from_addr, self.to_addr, self.subject,
+>>>>>> body)
+
+
+ 1: def selftest():
+ """Run all unit tests in this module."""
+>>>>>> import doctest
+>>>>>> nfail, ntests = doctest.testmod()
+>>>>>> if nfail == 0:
+>>>>>> print "All %d tests passed." % ntests
+
+
+ 1: def main():
+ """Parse command line arguments and do stuff."""
+>>>>>> progname = os.path.basename(sys.argv[0])
+>>>>>> parser = optparse.OptionParser("usage: %prog olddir newdir",
+>>>>>> prog=progname)
+>>>>>> parser.add_option('--include', metavar='REGEX',
+>>>>>> help='only consider files matching REGEX',
+>>>>>> action='append')
+>>>>>> parser.add_option('--exclude', metavar='REGEX',
+>>>>>> help='ignore files matching REGEX',
+>>>>>> action='append')
+>>>>>> parser.add_option('--email', metavar='ADDR',
+>>>>>> help='send the report to a given email address'
+ ' (only if regressions were found)',)
+>>>>>> parser.add_option('--from', metavar='ADDR', dest='sender',
+>>>>>> help='set the email sender address')
+>>>>>> parser.add_option('--subject', metavar='SUBJECT',
+>>>>>> default='Unit test coverage regression',
+>>>>>> help='set the email subject')
+>>>>>> parser.add_option('--web-url', metavar='BASEURL', dest='web_url',
+>>>>>> help='include hyperlinks to HTML-ized coverage'
+ ' reports at a given URL')
+>>>>>> parser.add_option('--selftest', help='run integrity tests',
+>>>>>> action='store_true')
+>>>>>> opts, args = parser.parse_args()
+>>>>>> if opts.selftest:
+>>>>>> selftest()
+>>>>>> return
+>>>>>> if len(args) != 2:
+>>>>>> parser.error("wrong number of arguments")
+>>>>>> olddir, newdir = args
+>>>>>> if opts.email:
+>>>>>> reporter = ReportEmailer(opts.sender, opts.email, opts.subject, opts.web_url)
+ else:
+>>>>>> reporter = ReportPrinter(opts.web_url)
+>>>>>> compare_dirs(olddir, newdir, include=opts.include, exclude=opts.exclude,
+>>>>>> warn=reporter.warn)
+>>>>>> if opts.email:
+>>>>>> mailer.send()
+
+
+ 1: if __name__ == '__main__':
+>>>>>> main()
Added: z3c.coverage/trunk/src/z3c/coverage/moresampleinput/z3c.coverage.coveragereport.cover
===================================================================
--- z3c.coverage/trunk/src/z3c/coverage/moresampleinput/z3c.coverage.coveragereport.cover (rev 0)
+++ z3c.coverage/trunk/src/z3c/coverage/moresampleinput/z3c.coverage.coveragereport.cover 2007-07-19 20:20:27 UTC (rev 78192)
@@ -0,0 +1,401 @@
+ #!/usr/bin/env python
+ ##############################################################################
+ #
+ # Copyright (c) 2007 Zope Foundation and Contributors.
+ # All Rights Reserved.
+ #
+ # This software is subject to the provisions of the Zope Public License,
+ # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+ # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+ # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+ # FOR A PARTICULAR PURPOSE.
+ #
+ ##############################################################################
+ """Coverage Report
+
+ Convert unit test coverage reports to HTML.
+
+ Usage: coveragereport.py [report-directory [output-directory]]
+
+ Locates plain-text coverage reports (files named
+ ``dotted.package.name.cover``) in the report directory and produces HTML
+ reports in the output directory. The format of plain-text coverage reports is
+ as follows: the file name is a dotted Python module name with a ``.cover``
+ suffix (e.g. ``zope.app.__init__.cover``). Each line corresponds to the
+ source file line with a 7 character wide prefix. The prefix is one of
+
+ ' ' if a line is not an executable code line
+ ' NNN: ' where NNN is the number of times this line was executed
+ '>>>>>> ' if this line was never executed
+
+ You can produce such files with the Zope test runner by specifying
+ ``--coverage`` on the command line.
+
+ $Id: coveragereport.py 78170 2007-07-19 15:47:33Z mgedmin $
+ 1: """
+ 1: __docformat__ = "reStructuredText"
+
+ 1: import sys
+ 1: import os
+ 1: import datetime
+ 1: import cgi
+
+
+ 2: class CoverageNode(dict):
+ """Tree node.
+
+ Leaf nodes have no children (items() == []) and correspond to Python
+ modules. Branches correspond to Python packages. Child nodes are
+ accessible via the Python mapping protocol, as you would normally use
+ a dict. Item keys are non-qualified module names.
+ 1: """
+
+ 1: def __str__(self):
+>>>>>> covered, total = self.coverage
+>>>>>> uncovered = total - covered
+>>>>>> return '%s%% covered (%s of %s lines uncovered)' % \
+>>>>>> (self.percent, uncovered, total)
+
+ 1: @property
+ def percent(self):
+ """Compute the coverage percentage."""
+>>>>>> covered, total = self.coverage
+>>>>>> if total != 0:
+>>>>>> return int(100 * covered / total)
+ else:
+>>>>>> return 100
+
+ 1: @property
+ def coverage(self):
+ """Return (number_of_lines_covered, number_of_executable_lines).
+
+ Computes the numbers recursively for the first time and caches the
+ result.
+ """
+>>>>>> if not hasattr(self, '_total'): # first-time computation
+>>>>>> self._covered = self._total = 0
+>>>>>> for substats in self.values():
+>>>>>> covered_more, total_more = substats.coverage
+>>>>>> self._covered += covered_more
+>>>>>> self._total += total_more
+>>>>>> return self._covered, self._total
+
+ 1: @property
+ def uncovered(self):
+ """Compute the number of uncovered code lines."""
+>>>>>> covered, total = self.coverage
+>>>>>> return total - covered
+
+
+ 1: def parse_file(filename):
+ """Parse a plain-text coverage report and return (covered, total)."""
+>>>>>> covered = 0
+>>>>>> total = 0
+>>>>>> for line in file(filename):
+>>>>>> if line.startswith(' '*7) or len(line) < 7:
+>>>>>> continue
+>>>>>> total += 1
+>>>>>> if not line.startswith('>>>>>>'):
+>>>>>> covered += 1
+>>>>>> return (covered, total)
+
+
+ 1: def get_file_list(path, filter_fn=None):
+ """Return a list of files in a directory.
+
+ If you can specify a predicate (a callable), only file names matching it
+ will be returned.
+ """
+>>>>>> return filter(filter_fn, os.listdir(path))
+
+
+ 1: def filename_to_list(filename):
+ """Return a list of package/module names from a filename.
+
+ One example is worth a thousand descriptions:
+
+ >>> filename_to_list('schooltool.app.__init__.cover')
+ ['schooltool', 'app', '__init__']
+
+ """
+>>>>>> return filename.split('.')[:-1]
+
+
+ 1: def get_tree_node(tree, index):
+ """Return a tree node for a given path.
+
+ The path is a sequence of child node names.
+
+ Creates intermediate and leaf nodes if necessary.
+ """
+>>>>>> node = tree
+>>>>>> for i in index:
+>>>>>> node = node.setdefault(i, CoverageNode())
+>>>>>> return node
+
+
+ 1: def create_tree(filelist, path):
+ """Create a tree with coverage statistics.
+
+ Takes the directory for coverage reports and a list of filenames relative
+ to that directory. Parses all the files and constructs a module tree with
+ coverage statistics.
+
+ Returns the root node of the tree.
+ """
+>>>>>> tree = CoverageNode()
+>>>>>> for filename in filelist:
+>>>>>> tree_index = filename_to_list(filename)
+>>>>>> node = get_tree_node(tree, tree_index)
+>>>>>> filepath = os.path.join(path, filename)
+>>>>>> node._covered, node._total = parse_file(filepath)
+>>>>>> return tree
+
+
+ 1: def traverse_tree(tree, index, function):
+ """Preorder traversal of a tree.
+
+ ``index`` is the path of the root node (usually []).
+
+ ``function`` gets one argument: the path of a node.
+ """
+>>>>>> function(tree, index)
+>>>>>> for key, node in tree.items():
+>>>>>> traverse_tree(node, index + [key], function)
+
+
+ 1: def traverse_tree_in_order(tree, index, function, order_by):
+ """Preorder traversal of a tree.
+
+ ``index`` is the path of the root node (usually []).
+
+ ``function`` gets one argument: the path of a node.
+
+ ``order_by`` gets one argument a tuple of (key, node).
+ """
+>>>>>> function(tree, index)
+>>>>>> for key, node in sorted(tree.items(), key=order_by):
+>>>>>> traverse_tree(node, index + [key], function)
+
+
+ 1: def index_to_url(index):
+ """Construct a relative hyperlink to a tree node given its path."""
+>>>>>> if index:
+>>>>>> return '%s.html' % '.'.join(index)
+>>>>>> return 'index.html'
+
+
+ 1: def index_to_filename(index):
+ """Construct the plain-text coverage report filename for a node."""
+>>>>>> if index:
+>>>>>> return '%s.cover' % '.'.join(index)
+>>>>>> return ''
+
+
+ 1: def index_to_nice_name(index):
+ """Construct an indented name for the node given its path."""
+>>>>>> if index:
+>>>>>> return ' ' * 4 * (len(index) - 1) + index[-1]
+ else:
+>>>>>> return 'Everything'
+
+
+ 1: def index_to_name(index):
+ """Construct the full name for the node given its path."""
+>>>>>> if index:
+>>>>>> return '.'.join(index)
+>>>>>> return 'everything'
+
+
+ 1: def percent_to_colour(percent):
+>>>>>> if percent == 100:
+>>>>>> return 'green'
+>>>>>> elif percent >= 90:
+>>>>>> return 'yellow'
+>>>>>> elif percent >= 80:
+>>>>>> return 'orange'
+ else:
+>>>>>> return 'red'
+
+
+ 1: def print_table_row(html, node, file_index):
+ """Generate a row for an HTML table."""
+>>>>>> covered, total = node.coverage
+>>>>>> uncovered = total - covered
+>>>>>> percent = node.percent
+>>>>>> nice_name = index_to_nice_name(file_index)
+>>>>>> if not node.keys():
+>>>>>> nice_name += '.py'
+ else:
+>>>>>> nice_name += '/'
+>>>>>> print >> html, '<tr><td><a href="%s">%s</a></td>' % \
+>>>>>> (index_to_url(file_index), nice_name),
+>>>>>> print >> html, '<td style="background: %s"> </td>' % \
+>>>>>> (percent_to_colour(percent)),
+>>>>>> print >> html, '<td>covered %s%% (%s of %s uncovered)</td></tr>' % \
+>>>>>> (percent, uncovered, total)
+
+
+ HEADER = """
+ <html>
+ <head><title>Unit test coverage for %(name)s</title>
+ <style type="text/css">
+ a {text-decoration: none; display: block; padding-right: 1em;}
+ a:hover {background: #EFA;}
+ hr {height: 1px; border: none; border-top: 1px solid gray;}
+ .notcovered {background: #FCC;}
+ .footer {margin: 2em; font-size: small; color: gray;}
+ </style>
+ </head>
+ <body><h1>Unit test coverage for %(name)s</h1>
+ <table>
+ 1: """
+
+
+ FOOTER = """
+ <div class="footer">
+ %s
+ </div>
+ </body>
+ 1: </html>"""
+
+
+ 1: def generate_html(output_filename, tree, my_index, info, path, footer=""):
+ """Generate HTML for a tree node.
+
+ ``output_filename`` is the output file name.
+
+ ``tree`` is the root node of the tree.
+
+ ``my_index`` is the path of the node for which you are generating this HTML
+ file.
+
+ ``info`` is a list of paths of child nodes.
+
+ ``path`` is the directory name for the plain-text report files.
+ """
+>>>>>> html = open(output_filename, 'w')
+>>>>>> print >> html, HEADER % {'name': index_to_name(my_index)}
+>>>>>> info = [(get_tree_node(tree, node_path), node_path) for node_path in info]
+>>>>>> def key((node, node_path)):
+>>>>>> return (len(node_path), -node.uncovered, node_path and node_path[-1])
+>>>>>> info.sort(key=key)
+>>>>>> for node, file_index in info:
+>>>>>> if not file_index:
+>>>>>> continue # skip root node
+>>>>>> print_table_row(html, node, file_index)
+>>>>>> print >> html, '</table><hr/>'
+>>>>>> if not get_tree_node(tree, my_index):
+>>>>>> file_path = os.path.join(path, index_to_filename(my_index))
+>>>>>> text = syntax_highlight(file_path)
+>>>>>> def color_uncov(line):
+>>>>>> if '>>>>>>' in line:
+>>>>>> return ('<div class="notcovered">%s</div>'
+>>>>>> % line.rstrip('\n'))
+>>>>>> return line
+>>>>>> text = ''.join(map(color_uncov, text.splitlines(True)))
+>>>>>> print >> html, '<pre>%s</pre>' % text
+>>>>>> print >> html, FOOTER % footer
+>>>>>> html.close()
+
+
+ 1: def syntax_highlight(filename):
+ """Return HTML with syntax-highlighted Python code from a file."""
+ # XXX can get painful if filenames contain unsafe characters
+>>>>>> pipe = os.popen('enscript -q --footer --header -h --language=html'
+>>>>>> ' --highlight=python --color -o - "%s"' % filename,
+>>>>>> 'r')
+>>>>>> text = pipe.read()
+>>>>>> if pipe.close():
+ # Failed to run enscript; maybe it is not installed? Disable
+ # syntax highlighting then.
+>>>>>> text = cgi.escape(file(filename).read())
+ else:
+>>>>>> text = text[text.find('<PRE>')+len('<PRE>'):]
+>>>>>> text = text[:text.find('</PRE>')]
+>>>>>> return text
+
+
+ 1: def generate_htmls_from_tree(tree, path, report_path, footer=""):
+ """Generate HTML files for all nodes in the tree.
+
+ ``tree`` is the root node of the tree.
+
+ ``path`` is the directory name for the plain-text report files.
+
+ ``report_path`` is the directory name for the output files.
+ """
+>>>>>> def make_html(node, my_index):
+>>>>>> info = []
+>>>>>> def list_parents_and_children(node, index):
+>>>>>> position = len(index)
+>>>>>> my_position = len(my_index)
+>>>>>> if position <= my_position and index == my_index[:position]:
+>>>>>> info.append(index)
+>>>>>> elif (position == my_position + 1 and
+>>>>>> index[:my_position] == my_index):
+>>>>>> info.append(index)
+>>>>>> return
+>>>>>> traverse_tree(tree, [], list_parents_and_children)
+>>>>>> output_filename = os.path.join(report_path, index_to_url(my_index))
+>>>>>> if not my_index:
+>>>>>> return # skip root node
+>>>>>> generate_html(output_filename, tree, my_index, info, path, footer)
+>>>>>> traverse_tree(tree, [], make_html)
+
+
+ 1: def generate_overall_html_from_tree(tree, output_filename, footer=""):
+ """Generate an overall HTML file for all nodes in the tree."""
+>>>>>> html = open(output_filename, 'w')
+>>>>>> print >> html, HEADER % {'name': ', '.join(sorted(tree.keys()))}
+>>>>>> def print_node(node, file_index):
+>>>>>> if file_index: # skip root node
+>>>>>> print_table_row(html, node, file_index)
+>>>>>> def sort_by((key, node)):
+>>>>>> return (-node.uncovered, key)
+>>>>>> traverse_tree_in_order(tree, [], print_node, sort_by)
+>>>>>> print >> html, '</table><hr/>'
+>>>>>> print >> html, FOOTER % footer
+>>>>>> html.close()
+
+
+ 1: def make_coverage_reports(path, report_path):
+ """Convert reports from ``path`` into HTML files in ``report_path``."""
+>>>>>> def filter_fn(filename):
+>>>>>> return (filename.endswith('.cover') and
+>>>>>> 'test' not in filename and
+>>>>>> not filename.startswith('<'))
+>>>>>> filelist = get_file_list(path, filter_fn)
+>>>>>> tree = create_tree(filelist, path)
+>>>>>> rev = get_svn_revision(os.path.join(path, os.path.pardir))
+>>>>>> timestamp = str(datetime.datetime.utcnow())+"Z"
+>>>>>> footer = "Generated for revision %s on %s" % (rev, timestamp)
+>>>>>> generate_htmls_from_tree(tree, path, report_path, footer)
+>>>>>> generate_overall_html_from_tree(tree, os.path.join(report_path,
+>>>>>> 'all.html'), footer)
+
+
+ 1: def get_svn_revision(path):
+ """Return the Subversion revision number for a working directory."""
+>>>>>> rev = os.popen('svnversion "%s"' % path, 'r').readline().strip()
+>>>>>> if not rev:
+>>>>>> rev = "UNKNOWN"
+>>>>>> return rev
+
+
+ 1: def main():
+ """Process command line arguments and produce HTML coverage reports."""
+>>>>>> if len(sys.argv) > 1:
+>>>>>> path = sys.argv[1]
+ else:
+>>>>>> path = 'coverage'
+>>>>>> if len(sys.argv) > 2:
+>>>>>> report_path = sys.argv[2]
+ else:
+>>>>>> report_path = 'coverage/reports'
+>>>>>> make_coverage_reports(path, report_path)
+
+
+ 1: if __name__ == '__main__':
+>>>>>> main()
Added: z3c.coverage/trunk/src/z3c/coverage/moresampleinput/z3c.coverage.fakenewmodule.cover
===================================================================
--- z3c.coverage/trunk/src/z3c/coverage/moresampleinput/z3c.coverage.fakenewmodule.cover (rev 0)
+++ z3c.coverage/trunk/src/z3c/coverage/moresampleinput/z3c.coverage.fakenewmodule.cover 2007-07-19 20:20:27 UTC (rev 78192)
@@ -0,0 +1,23 @@
+ #!/usr/bin/env python
+ """
+ Test suite for z3c.coverage
+ 1: """
+
+ 1: import unittest
+
+ # prefer the zope.testing version, if it is available
+ 1: try:
+ 1: from zope.testing import doctest
+>>>>>> except ImportError:
+>>>>>> import doctest
+
+
+ 1: def test_suite():
+ 1: return unittest.TestSuite([
+ 1: doctest.DocFileSuite('coveragediff.txt'),
+ 1: doctest.DocTestSuite('z3c.coverage.coveragediff'),
+ 1: doctest.DocTestSuite('z3c.coverage.coveragereport'),
+ ])
+
+ 1: if __name__ == '__main__':
+>>>>>> unittest.main(defaultTest='test_suite')
Added: z3c.coverage/trunk/src/z3c/coverage/moresampleinput/z3c.coverage.tests.cover
===================================================================
--- z3c.coverage/trunk/src/z3c/coverage/moresampleinput/z3c.coverage.tests.cover (rev 0)
+++ z3c.coverage/trunk/src/z3c/coverage/moresampleinput/z3c.coverage.tests.cover 2007-07-19 20:20:27 UTC (rev 78192)
@@ -0,0 +1,23 @@
+ #!/usr/bin/env python
+ """
+ Test suite for z3c.coverage
+ 1: """
+
+ 1: import unittest
+
+ # prefer the zope.testing version, if it is available
+ 1: try:
+ 1: from zope.testing import doctest
+>>>>>> except ImportError:
+>>>>>> import doctest
+
+
+ 1: def test_suite():
+ 1: return unittest.TestSuite([
+ 1: doctest.DocFileSuite('coveragediff.txt'),
+ 1: doctest.DocTestSuite('z3c.coverage.coveragediff'),
+ 1: doctest.DocTestSuite('z3c.coverage.coveragereport'),
+ ])
+
+ 1: if __name__ == '__main__':
+>>>>>> unittest.main(defaultTest='test_suite')
More information about the Checkins
mailing list