[Checkins] SVN: z3c.coverage/trunk/src/z3c/coverage/ Embiggen the test suite. Add some sample coverage files for the tests.

Marius Gedminas marius at pov.lt
Thu Jul 19 16:05:04 EDT 2007


Log message for revision 78191:
  Embiggen the test suite.  Add some sample coverage files for the tests.
  
  

Changed:
  U   z3c.coverage/trunk/src/z3c/coverage/coveragediff.py
  U   z3c.coverage/trunk/src/z3c/coverage/coveragediff.txt
  A   z3c.coverage/trunk/src/z3c/coverage/sampleinput/
  A   z3c.coverage/trunk/src/z3c/coverage/sampleinput/decoy.txt
  A   z3c.coverage/trunk/src/z3c/coverage/sampleinput/z3c.coverage.__init__.cover
  A   z3c.coverage/trunk/src/z3c/coverage/sampleinput/z3c.coverage.coveragediff.cover
  A   z3c.coverage/trunk/src/z3c/coverage/sampleinput/z3c.coverage.coveragereport.cover
  A   z3c.coverage/trunk/src/z3c/coverage/sampleinput/z3c.coverage.tests.cover

-=-
Modified: z3c.coverage/trunk/src/z3c/coverage/coveragediff.py
===================================================================
--- z3c.coverage/trunk/src/z3c/coverage/coveragediff.py	2007-07-19 18:32:20 UTC (rev 78190)
+++ z3c.coverage/trunk/src/z3c/coverage/coveragediff.py	2007-07-19 20:05:03 UTC (rev 78191)
@@ -122,7 +122,12 @@
 
 
 def warn(filename, message):
-    """Warn about test coverage regression."""
+    """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)
 

Modified: z3c.coverage/trunk/src/z3c/coverage/coveragediff.txt
===================================================================
--- z3c.coverage/trunk/src/z3c/coverage/coveragediff.txt	2007-07-19 18:32:20 UTC (rev 78190)
+++ z3c.coverage/trunk/src/z3c/coverage/coveragediff.txt	2007-07-19 20:05:03 UTC (rev 78191)
@@ -9,7 +9,59 @@
 a test suite.
 
 
+Locating coverage files
+-----------------------
 
+The function ``find_coverage_files`` looks for plain-text coverage reports
+in a given directory
+
+    >>> import z3c.coverage, os
+    >>> sampleinput_dir = os.path.join(z3c.coverage.__path__[0], 'sampleinput')
+
+    >>> from z3c.coverage.coveragediff import find_coverage_files
+    >>> for filename in sorted(find_coverage_files(sampleinput_dir)):
+    ...     print filename
+    z3c.coverage.__init__.cover
+    z3c.coverage.coveragediff.cover
+    z3c.coverage.coveragereport.cover
+    z3c.coverage.tests.cover
+
+The function ``filter_coverage_files`` looks for plain-text coverage reports
+ in a given location that match a set of include and exclude patterns
+
+    >>> from z3c.coverage.coveragediff import filter_coverage_files
+    >>> for filename in sorted(filter_coverage_files(sampleinput_dir)):
+    ...     print filename
+    z3c.coverage.__init__.cover
+    z3c.coverage.coveragediff.cover
+    z3c.coverage.coveragereport.cover
+    z3c.coverage.tests.cover
+
+    >>> for filename in sorted(filter_coverage_files(sampleinput_dir,
+    ...                                              include=['diff'])):
+    ...     print filename
+    z3c.coverage.coveragediff.cover
+
+The patterns are regular expressions
+
+    >>> for filename in sorted(filter_coverage_files(sampleinput_dir,
+    ...                                              exclude=['^z'])):
+    ...     print filename
+
+
+Parsing coverage files
+----------------------
+
+The function ``count_coverage`` reads a plain-text coverage reports and
+returns two numbers: the number of tested code lines and the number of
+untested code lines.
+
+    >>> from z3c.coverage.coveragediff import count_coverage
+    >>> filename = os.path.join(sampleinput_dir, 'z3c.coverage.tests.cover')
+    >>> count_coverage(filename)
+    (10, 3)
+
+
 MailSender
 ----------
 

Added: z3c.coverage/trunk/src/z3c/coverage/sampleinput/decoy.txt
===================================================================


Property changes on: z3c.coverage/trunk/src/z3c/coverage/sampleinput/decoy.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: z3c.coverage/trunk/src/z3c/coverage/sampleinput/z3c.coverage.__init__.cover
===================================================================
--- z3c.coverage/trunk/src/z3c/coverage/sampleinput/z3c.coverage.__init__.cover	                        (rev 0)
+++ z3c.coverage/trunk/src/z3c/coverage/sampleinput/z3c.coverage.__init__.cover	2007-07-19 20:05:03 UTC (rev 78191)
@@ -0,0 +1 @@
+    1: # Make a package.

Added: z3c.coverage/trunk/src/z3c/coverage/sampleinput/z3c.coverage.coveragediff.cover
===================================================================
--- z3c.coverage/trunk/src/z3c/coverage/sampleinput/z3c.coverage.coveragediff.cover	                        (rev 0)
+++ z3c.coverage/trunk/src/z3c/coverage/sampleinput/z3c.coverage.coveragediff.cover	2007-07-19 20:05:03 UTC (rev 78191)
@@ -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:
+>>>>>>         include = ['.'] # include everything by default
+    1:     if not exclude:
+>>>>>>         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.
+           """
+>>>>>>     return [fn for fn in os.listdir(dir)
+>>>>>>             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.
+           """
+>>>>>>     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')
+               ''
+       
+           """
+    8:     if string.endswith(suffix):
+    8:         string = string[:-len(suffix)]
+    8:     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.
+           """
+    4:     if not base.endswith('/'):
+    2:         base += '/'
+    4:     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):
+    2:         self.smtp_host = smtp_host
+    2:         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
+    1:         msg = MIMEText(body)
+    1:         if from_addr:
+    1:             msg['From'] = from_addr
+    1:         if to_addr:
+    1:             msg['To'] = to_addr
+    1:         msg['Subject'] = subject
+    1:         smtp = self.connection_class(self.smtp_host, self.smtp_port)
+    1:         smtp.sendmail(from_addr, to_addr, msg.as_string())
+    1:         smtp.quit()
+       
+       
+    2: class ReportPrinter(object):
+    1:     """Reporter to sys.stdout."""
+       
+    1:     def __init__(self, web_url=None):
+    2:         self.web_url = web_url
+       
+    1:     def warn(self, filename, message):
+               """Warn about test coverage regression."""
+    4:         module = strip(os.path.basename(filename), '.cover')
+    4:         print '%s: %s' % (module, message)
+    4:         if self.web_url:
+    2:             url = urljoin(self.web_url, module + '.html')
+    2:             print 'See ' + url
+    2:             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):
+    3:         if not mailer:
+    1:             mailer = MailSender()
+    3:         self.from_addr = from_addr
+    3:         self.to_addr = to_addr
+    3:         self.subject = subject
+    3:         self.web_url = web_url
+    3:         self.mailer = mailer
+    3:         self.warnings = []
+       
+    1:     def warn(self, filename, message):
+               """Warn about test coverage regression."""
+    4:         module = strip(os.path.basename(filename), '.cover')
+    4:         self.warnings.append('%s: %s' % (module, message))
+    4:         if self.web_url:
+    2:             url = urljoin(self.web_url, module + '.html')
+    2:             self.warnings.append('See ' + url + '\n')
+       
+    1:     def send(self):
+               """Send the warnings (if any)."""
+    3:         if self.warnings:
+    2:             body = '\n'.join(self.warnings)
+    2:             self.mailer.send_email(self.from_addr, self.to_addr, self.subject,
+    2:                                    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/sampleinput/z3c.coverage.coveragereport.cover
===================================================================
--- z3c.coverage/trunk/src/z3c/coverage/sampleinput/z3c.coverage.coveragereport.cover	                        (rev 0)
+++ z3c.coverage/trunk/src/z3c/coverage/sampleinput/z3c.coverage.coveragereport.cover	2007-07-19 20:05:03 UTC (rev 78191)
@@ -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 '&nbsp;' * 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">&nbsp;&nbsp;&nbsp;&nbsp;</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 '&gt;&gt;&gt;&gt;&gt;&gt;' 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/sampleinput/z3c.coverage.tests.cover
===================================================================
--- z3c.coverage/trunk/src/z3c/coverage/sampleinput/z3c.coverage.tests.cover	                        (rev 0)
+++ z3c.coverage/trunk/src/z3c/coverage/sampleinput/z3c.coverage.tests.cover	2007-07-19 20:05:03 UTC (rev 78191)
@@ -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