[Checkins] SVN: z3c.coverage/trunk/ Support .coverage files produced by coverage.py

Marius Gedminas cvs-admin at zope.org
Thu Sep 6 13:23:53 UTC 2012


Log message for revision 127752:
  Support .coverage files produced by coverage.py

Changed:
  U   z3c.coverage/trunk/CHANGES.txt
  U   z3c.coverage/trunk/buildout.cfg
  U   z3c.coverage/trunk/setup.py
  U   z3c.coverage/trunk/src/z3c/coverage/README.txt
  U   z3c.coverage/trunk/src/z3c/coverage/coveragereport.py
  A   z3c.coverage/trunk/src/z3c/coverage/sampleinput.pickle
  U   z3c.coverage/trunk/src/z3c/coverage/tests.py

-=-
Modified: z3c.coverage/trunk/CHANGES.txt
===================================================================
--- z3c.coverage/trunk/CHANGES.txt	2012-09-06 13:23:43 UTC (rev 127751)
+++ z3c.coverage/trunk/CHANGES.txt	2012-09-06 13:23:49 UTC (rev 127752)
@@ -2,15 +2,19 @@
 CHANGES
 =======
 
-1.2.1 (unreleased)
+1.3.0 (unreleased)
 ------------------
 
-- Bugfix: sorting by numbered of uncovered lines was broken.
+- ``coveragereport`` now accepts ``--help``, ``--verbose`` and ``--quiet``
+  options, with verbose being on by default.
 
-- The ``coveragereport`` script now accepts ``--help``, ``--verbose`` and
-  ``--quiet`` options, with verbose being on by default.
+- ``coveragereport`` now can handle .coverage files produced by
+  http://pypi.python.org/pypi/coverage
 
+- Bugfix: sorting by numbered of uncovered lines was broken in the
+  ``all.html`` report.
 
+
 1.2.0 (2010-02-11)
 ------------------
 

Modified: z3c.coverage/trunk/buildout.cfg
===================================================================
--- z3c.coverage/trunk/buildout.cfg	2012-09-06 13:23:43 UTC (rev 127751)
+++ z3c.coverage/trunk/buildout.cfg	2012-09-06 13:23:49 UTC (rev 127752)
@@ -30,7 +30,7 @@
 recipe = zc.recipe.egg
 eggs = z3c.coverage
 scripts = coveragereport=coverage-report
-arguments = ['${buildout:directory}/parts/coverage-test', '${buildout:directory}/parts/coverage-test/report'] + sys.argv[1:]
+arguments = sys.argv[1:] or ['${buildout:directory}/parts/coverage-test', '${buildout:directory}/parts/coverage-test/report']
 
 [coverage-diff]
 recipe = zc.recipe.egg
@@ -40,4 +40,3 @@
 [coverage]
 recipe = zc.recipe.egg
 eggs = coverage
-       ${test:eggs}

Modified: z3c.coverage/trunk/setup.py
===================================================================
--- z3c.coverage/trunk/setup.py	2012-09-06 13:23:43 UTC (rev 127751)
+++ z3c.coverage/trunk/setup.py	2012-09-06 13:23:49 UTC (rev 127752)
@@ -23,7 +23,7 @@
 
 setup(
     name='z3c.coverage',
-    version='1.2.1dev',
+    version='1.3.0dev',
     author="Zope Community",
     author_email="zope3-dev at zope.org",
     description="A script to visualize coverage reports via HTML",
@@ -67,6 +67,7 @@
         ),
     install_requires=[
         'setuptools',
+        'coverage',
         ],
     entry_points="""
         [console_scripts]

Modified: z3c.coverage/trunk/src/z3c/coverage/README.txt
===================================================================
--- z3c.coverage/trunk/src/z3c/coverage/README.txt	2012-09-06 13:23:43 UTC (rev 127751)
+++ z3c.coverage/trunk/src/z3c/coverage/README.txt	2012-09-06 13:23:49 UTC (rev 127752)
@@ -8,179 +8,60 @@
 
 Luckily we already have the text input ready:
 
-  >>> import os
-  >>> import z3c.coverage
-  >>> inputDir = os.path.join(
-  ...     os.path.dirname(z3c.coverage.__file__), 'sampleinput')
+    >>> import os
+    >>> import z3c.coverage
+    >>> inputDir = os.path.join(
+    ...     os.path.dirname(z3c.coverage.__file__), 'sampleinput')
 
 Let's create a temporary directory for the output
 
-  >>> import tempfile
-  >>> tempDir = tempfile.mkdtemp(prefix='tmp-z3c.coverage-report-')
+    >>> import tempfile, shutil
+    >>> tempDir = tempfile.mkdtemp(prefix='tmp-z3c.coverage-report-')
 
 The output directory will be created if it doesn't exist already
 
-  >>> outputDir = os.path.join(tempDir, 'report')
+    >>> outputDir = os.path.join(tempDir, 'report')
 
 We can now create the coverage report as follows:
 
-  >>> from z3c.coverage import coveragereport
-  >>> coveragereport.main([inputDir, outputDir, '--quiet'])
+    >>> from z3c.coverage import coveragereport
+    >>> coveragereport.main([inputDir, outputDir, '--quiet'])
 
 Looking at the output directory, we now see several files:
 
-  >>> print '\n'.join(sorted(os.listdir(outputDir)))
-  all.html
-  z3c.coverage.__init__.html
-  z3c.coverage.coveragediff.html
-  z3c.coverage.coveragereport.html
-  z3c.coverage.html
-  z3c.html
+    >>> print '\n'.join(sorted(os.listdir(outputDir)))
+    all.html
+    z3c.coverage.__init__.html
+    z3c.coverage.coveragediff.html
+    z3c.coverage.coveragereport.html
+    z3c.coverage.html
+    z3c.html
 
+Let's clean up
 
-API Tests
----------
+    >>> shutil.rmtree(tempDir)
 
-``CoverageNode`` Class
-~~~~~~~~~~~~~~~~~~~~~~
 
-This class represents a node in the source tree. Simple modules are considered
-leaves and do not have children. Let's create a node for the `z3c` namespace
-first:
+coverage.py support
+~~~~~~~~~~~~~~~~~~~
 
-  >>> z3cNode = coveragereport.CoverageNode()
+We also support coverage reports generated by coverage.py 
 
-Before using the API, let's create a few more nodes and a tree from it:
+    >>> inputFile = os.path.join(
+    ...     os.path.dirname(z3c.coverage.__file__), 'sampleinput.pickle')
 
-  >>> coverageNode = coveragereport.CoverageNode()
-  >>> z3cNode['coverage'] = coverageNode
+    >>> from z3c.coverage import coveragereport
+    >>> coveragereport.main([inputFile, outputDir, '-q',
+    ...                      '--strip-prefix=/home/mg/src/z3c.coverage/src'])
+    >>> print '\n'.join(sorted(os.listdir(outputDir)))
+    all.html
+    z3c.coverage.__init__.html
+    z3c.coverage.coveragediff.html
+    z3c.coverage.coveragereport.html
+    z3c.coverage.html
+    z3c.html
 
-  >>> reportNode = coveragereport.CoverageNode()
-  >>> reportNode._covered, reportNode._total = 40, 134
-  >>> coverageNode['coveragereport'] = reportNode
 
-  >>> diffNode = coveragereport.CoverageNode()
-  >>> diffNode._covered, diffNode._total = 128, 128
-  >>> coverageNode['coveragediff'] = diffNode
-
-  >>> initNode = coveragereport.CoverageNode()
-  >>> initNode._covered, initNode._total = 0, 0
-  >>> coverageNode['__init__'] = initNode
-
-Let's now have a look at the coverage of the `z3c` namespace:
-
-  >>> z3cNode.coverage
-  (168, 262)
-
-We can also ask for the percentile:
-
-  >>> z3cNode.percent
-  64
-  >>> initNode.percent
-  100
-
-We can ask for the amount of uncovered lines:
-
-  >>> z3cNode.uncovered
-  94
-
-Finally, we also can get a nice output:
-
-  >>> print z3cNode
-  64% covered (94 of 262 lines uncovered)
-
-
-`index_to_filename()` function
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Takes an indexed Python path and produces the cover filename for it:
-
-  >>> coveragereport.index_to_filename(('z3c', 'coverage', 'coveragereport'))
-  'z3c.coverage.coveragereport.cover'
-
-  >>> coveragereport.index_to_filename(())
-  ''
-
-`index_to_nice_name()` function
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Takes an indexed Python path and produces a nice "human-readable" string:
-
-  >>> coveragereport.index_to_nice_name(('z3c', 'coverage', 'coveragereport'))
-  '        coveragereport'
-
-  >>> coveragereport.index_to_nice_name(())
-  'Everything'
-
-
-`index_to_name()` function
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Takes an indexed Python path and produces a "human-readable" string:
-
-  >>> coveragereport.index_to_name(('z3c', 'coverage', 'coveragereport'))
-  'z3c.coverage.coveragereport'
-
-  >>> coveragereport.index_to_name(())
-  'everything'
-
-
-`percent_to_colour()` function
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Given a coverage percentage, this function returns a color to represent the
-coverage:
-
-  >>> coveragereport.percent_to_colour(100)
-  'green'
-  >>> coveragereport.percent_to_colour(92)
-  'yellow'
-  >>> coveragereport.percent_to_colour(85)
-  'orange'
-  >>> coveragereport.percent_to_colour(50)
-  'red'
-
-
-`get_svn_revision()` function
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Given a path, the function tries to determine the revision number of the
-file. If it fails, "UNKNOWN" is returned:
-
-  >>> path = os.path.dirname(z3c.coverage.__file__)
-  >>> coveragereport.get_svn_revision(path) != 'UNKNOWN'
-  True
-
-  >>> coveragereport.get_svn_revision(path + '/__init__.py')
-  'UNKNOWN'
-
-
-`syntax_highlight()` function
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-This function takes a cover file, converts it to a nicely colored HTML output:
-
-  >>> filename = os.path.join(
-  ...     os.path.dirname(z3c.coverage.__file__), '__init__.py')
-
-  >>> print coveragereport.syntax_highlight(filename)
-  ... # this will fail if you don't have enscript in your $PATH
-  <BLANKLINE>
-  <I><FONT COLOR="#B22222"># Make a package.
-  </FONT></I>
-
-If the highlighing command is not available, no coloration is done:
-
-  >>> command_orig = coveragereport.HIGHLIGHT_COMMAND
-  >>> coveragereport.HIGHLIGHT_COMMAND = ['aflkakhalkjdsjdhf']
-
-  >>> print coveragereport.syntax_highlight(filename)
-  # Make a package.
-  <BLANKLINE>
-
-  >>> coveragereport.HIGHLIGHT_COMMAND = command_orig
-
-
 `coveragereport.py` is a script
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -195,24 +76,7 @@
 
   >>> execfile(script_file, dict(__name__='__main__'))
 
-Defaults are chosen when no input and output dir is specified:
-
-  >>> def make_coverage_reports_stub(path, report_path, **kw):
-  ...     print path
-  ...     print report_path
-
-  >>> make_coverage_reports_orig = coveragereport.make_coverage_reports
-  >>> coveragereport.make_coverage_reports = make_coverage_reports_stub
-
-  >>> sys.argv = ['coveragereport']
-  >>> coveragereport.main()
-  coverage
-  coverage/reports
-
-  >>> coveragereport.make_coverage_reports = make_coverage_reports_orig
-
 Let's clean up
 
-  >>> import shutil
   >>> shutil.rmtree(tempDir)
 

Modified: z3c.coverage/trunk/src/z3c/coverage/coveragereport.py
===================================================================
--- z3c.coverage/trunk/src/z3c/coverage/coveragereport.py	2012-09-06 13:23:43 UTC (rev 127751)
+++ z3c.coverage/trunk/src/z3c/coverage/coveragereport.py	2012-09-06 13:23:49 UTC (rev 127752)
@@ -16,22 +16,27 @@
 
 Convert trace.py coverage reports to HTML.
 
-Usage: coveragereport.py [report-directory [output-directory]]
+Usage: coveragereport.py [input-dir-or-file 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
+Loads coverage data from plain-text coverage reports (files named
+``dotted.package.name.cover``) in the report directory *or* a .coverage
+file produced by http://pypi.python.org/pypi/coverage 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, or, more generally, by using the
-``trace`` module in the standard library.
+``trace`` module in the standard library.  Although you should consider
+using the above-mentioned coverage package instead, as it's much faster
+than trace.py.
 
 $Id$
 """
@@ -43,12 +48,29 @@
 import cgi
 import subprocess
 import optparse
+import tempfile
 
 
 HIGHLIGHT_COMMAND = ['enscript', '-q', '--footer', '--header', '-h',
                      '--language=html', '--highlight=python', '--color',
                      '-o', '-']
 
+
+class Lazy(object):
+    """Descriptor for lazy evaluation"""
+
+    def __init__(self, func):
+        self.func = func
+        self.name = func.__name__
+
+    def __get__(self, obj, cls):
+        if obj is None:
+            return self
+        value = self.func(obj)
+        obj.__dict__[self.name] = value
+        return value
+
+
 class CoverageNode(dict):
     """Tree node.
 
@@ -58,56 +80,124 @@
     a dict.  Item keys are non-qualified module names.
     """
 
-    def __str__(self):
-        covered, total = self.coverage
-        uncovered = total - covered
-        return '%s%% covered (%s of %s lines uncovered)' % \
-               (self.percent, uncovered, total)
+    @Lazy
+    def covered(self):
+        return sum(child.covered for child in self.itervalues())
 
-    @property
+    @Lazy
+    def total(self):
+        return sum(child.total for child in self.itervalues())
+
+    @Lazy
+    def uncovered(self):
+        return self.total - self.covered
+
+    @Lazy
     def percent(self):
-        """Compute the coverage percentage."""
-        covered, total = self.coverage
-        if total != 0:
-            return int(100 * covered / total)
+        if self.total != 0:
+            return 100 * self.covered // self.total
         else:
             return 100
 
-    @property
-    def coverage(self):
-        """Return (number_of_lines_covered, number_of_executable_lines).
+    @Lazy
+    def html_source(self):
+        return ''
 
-        Computes the numbers recursively for the first time and caches the
-        result.
+    def __str__(self):
+        return '%s%% covered (%s of %s lines uncovered)' % \
+               (self.percent, self.uncovered, self.total)
+
+    def get_at(self, path):
+        """Return a tree node for a given path.
+
+        The path is a sequence of child node names.
         """
-        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
+        node = self
+        for name in path:
+            node = node[name]
+        return node
 
-    @property
-    def uncovered(self):
-        """Compute the number of uncovered code lines."""
-        covered, total = self.coverage
-        return total - covered
+    def set_at(self, path, node):
+        """Create a tree node at a given path.
 
+        The path is a sequence of child node names.
 
-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)
+        Creates intermediate nodes if necessary.
+        """
+        parent = self
+        for name in path[:-1]:
+            parent = parent.setdefault(name, CoverageNode())
+        parent[path[-1]] = node
 
 
+class TraceCoverageNode(CoverageNode):
+    """Coverage node loaded from an annotated source file."""
+
+    def __init__(self, cover_filename):
+        self.cover_filename = cover_filename
+        self.covered, self.total = self._parse(cover_filename)
+
+    def _parse(self, 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)
+
+    @Lazy
+    def html_source(self):
+        text = syntax_highlight(self.cover_filename)
+        text = highlight_uncovered_lines(text)
+        return '<pre>%s</pre>' % text
+
+
+class CoverageCoverageNode(CoverageNode):
+    """Coverage node loaded from a coverage.py data file."""
+
+    def __init__(self, cov, source_filename):
+        self.cov = cov
+        self.source_filename = source_filename
+        (filename_again, statements, excluded, missing,
+         missing_str) = cov.analysis2(source_filename)
+        self.covered = len(statements) - len(excluded) - len(missing)
+        self.total = len(statements) - len(excluded)
+        self._missing = set(missing)
+        self._statements = set(statements)
+        self._excluded = set(excluded)
+
+    @Lazy
+    def annotated_source(self):
+        MISSING   = '>>>>>> '
+        STATEMENT = '    1: '
+        EXCLUDED  = '     # '
+        OTHER     = '       '
+        lines = []
+        f = open(self.source_filename)
+        for n, line in enumerate(f):
+            n += 1 # workaround lack of enumerate(f, start=1) support in 2.4/5
+            if n in self._missing:      prefix = MISSING
+            elif n in self._excluded:   prefix = EXCLUDED
+            elif n in self._statements: prefix = STATEMENT
+            else:                       prefix = OTHER
+            lines.append(prefix + line)
+        return ''.join(lines)
+
+    @Lazy
+    def html_source(self):
+        tmpf = tempfile.NamedTemporaryFile(suffix='.py.cover')
+        tmpf.write(self.annotated_source)
+        tmpf.flush()
+        text = syntax_highlight(tmpf.name)
+        tmpf.close()
+        text = highlight_uncovered_lines(text)
+        return '<pre>%s</pre>' % text
+
+
 def get_file_list(path, filter_fn=None):
     """Return a list of files in a directory.
 
@@ -129,20 +219,7 @@
     return filename.split('.')[:-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
-
-
-def create_tree(filelist, path):
+def create_tree_from_files(filelist, path):
     """Create a tree with coverage statistics.
 
     Takes the directory for coverage reports and a list of filenames relative
@@ -151,15 +228,34 @@
 
     Returns the root node of the tree.
     """
-    tree = CoverageNode()
+    root = 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
+        root.set_at(tree_index, TraceCoverageNode(filepath))
+    return root
 
 
+def create_tree_from_coverage(cov, strip_prefix=None):
+    """Create a tree with coverage statistics.
+
+    Takes a coverage.coverage() instance.
+
+    Returns the root node of the tree.
+    """
+    root = CoverageNode()
+    for filename in cov.data.measured_files():
+        if strip_prefix and filename.startswith(strip_prefix):
+            short_name = filename[len(strip_prefix):].lstrip(os.path.sep)
+        else:
+            short_name = cov.file_locator.relative_filename(filename)
+        tree_index = filename_to_list(short_name.replace(os.path.sep, '.'))
+        if 'tests' in tree_index or 'ftests' in tree_index:
+            continue
+        root.set_at(tree_index, CoverageCoverageNode(cov, filename))
+    return root
+
+
 def traverse_tree(tree, index, function):
     """Preorder traversal of a tree.
 
@@ -193,13 +289,6 @@
     return 'index.html'
 
 
-def index_to_filename(index):
-    """Construct the plain-text coverage report filename for a node."""
-    if index:
-        return '%s.cover' % '.'.join(index)
-    return ''
-
-
 def index_to_nice_name(index):
     """Construct an indented name for the node given its path."""
     if index:
@@ -228,9 +317,6 @@
 
 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'
@@ -239,9 +325,9 @@
     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)),
+                   (percent_to_colour(node.percent)),
     print >> html, '<td>covered %s%% (%s of %s uncovered)</td></tr>' % \
-                   (percent, uncovered, total)
+                   (node.percent, node.uncovered, node.total)
 
 
 HEADER = """
@@ -284,7 +370,7 @@
     """
     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]
+    info = [(tree.get_at(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)
@@ -293,18 +379,7 @@
             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):
-            # The line must start with the missing line indicator or some HTML
-            # was put in front of it.
-            if line.startswith('&gt;'*6) or '>'+'&gt;'*6 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, tree.get_at(my_index).html_source
     print >> html, FOOTER % footer
     html.close()
 
@@ -314,7 +389,7 @@
     # TODO: use pygments instead
     try:
         pipe = subprocess.Popen(HIGHLIGHT_COMMAND + [filename],
-                            stdout=subprocess.PIPE)
+                                stdout=subprocess.PIPE)
         text, stderr = pipe.communicate()
         if pipe.returncode != 0:
             raise OSError
@@ -328,6 +403,19 @@
     return text
 
 
+def highlight_uncovered_lines(text):
+    """Highlight lines beginning with '>>>>>>'."""
+    def color_uncov(line):
+        # The line must start with the missing line indicator or some HTML
+        # was put in front of it.
+        if line.startswith('&gt;'*6) or '>'+'&gt;'*6 in line:
+            return ('<div class="notcovered">%s</div>'
+                    % line.rstrip('\n'))
+        return line
+    text = ''.join(map(color_uncov, text.splitlines(True)))
+    return text
+
+
 def generate_htmls_from_tree(tree, path, report_path, footer=""):
     """Generate HTML files for all nodes in the tree.
 
@@ -404,22 +492,39 @@
             'ftests' not in parts)
 
 
-def make_coverage_reports(path, report_path, verbose=True):
+def load_coverage(path, opts):
+    """Load coverage information from ``path``.
+
+    ``path`` can point to a directory full of files named *.cover, or it can
+    point to a single pickle file containing coverage information.
+    """
+    if os.path.isdir(path):
+        filelist = get_file_list(path, filter_fn)
+        tree = create_tree_from_files(filelist, path)
+        return tree
+    else:
+        import coverage
+        cov = coverage.coverage(data_file=path, config_file=False)
+        cov.load()
+        tree = create_tree_from_coverage(cov, strip_prefix=opts.strip_prefix)
+        return tree
+
+
+def make_coverage_reports(path, report_path, opts):
     """Convert reports from ``path`` into HTML files in ``report_path``."""
-    create_report_path(report_path)
-    filelist = get_file_list(path, filter_fn)
-    if verbose:
+    if opts.verbose:
         print "Loading coverage reports from %s" % path
-    tree = create_tree(filelist, path)
-    if verbose:
+    tree = load_coverage(path, opts=opts)
+    if opts.verbose:
         print tree
     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)
+    create_report_path(report_path)
     generate_htmls_from_tree(tree, path, report_path, footer)
     generate_overall_html_from_tree(
         tree, os.path.join(report_path, 'all.html'), footer)
-    if verbose:
+    if opts.verbose:
         print "Generated HTML files in %s" % report_path
 
 
@@ -441,17 +546,20 @@
     """Process command line arguments and produce HTML coverage reports."""
 
     parser = optparse.OptionParser(
-        "usage: %prog [options] [inputdir [outputdir]]",
+        "usage: %prog [options] [inputpath [outputdir]]",
         description=
-            'Converts trace.py coverage reports to HTML.'
-            '  If the input directory is omitted, it defaults to ./coverage.'
-            '  If the output directory is omitted, it defaults to'
-            ' ./coverage/report.')
+            'Converts coverage reports to HTML.  If the input path is'
+            ' omitted, it defaults to coverage or .coverage, whichever'
+            ' exists.  If the output directory is omitted, it defaults to'
+            ' inputpath + /report or ./coverage-reports, depending on whether'
+            ' the input path points to a directory or a file.')
 
     parser.add_option('-q', '--quiet', help='be quiet',
                       action='store_const', const=0, dest='verbose')
     parser.add_option('-v', '--verbose', help='be verbose (default)',
                       action='store_const', const=1, dest='verbose', default=1)
+    parser.add_option('--strip-prefix', metavar='PREFIX',
+                      help='strip base directory from filenames loaded from .coverage')
 
     if args is None:
         args = sys.argv[1:]
@@ -460,17 +568,26 @@
     if len(args) > 0:
         path = args[0]
     else:
-        path = 'coverage'
+        if os.path.exists('coverage'):
+            # backward compat: default input path used to be 'coverage'
+            path = 'coverage'
+        else:
+            path = '.coverage'
 
     if len(args) > 1:
         report_path = args[1]
     else:
-        report_path = 'coverage/reports'
+        if os.path.isdir(path):
+            # backward compat: default input path is 'coverage', default output
+            # path is 'coverage/reports'
+            report_path = os.path.join(path, 'reports')
+        else:
+            report_path = 'coverage-reports'
 
     if len(args) > 2:
         parser.error("too many arguments")
 
-    make_coverage_reports(path, report_path, verbose=opts.verbose)
+    make_coverage_reports(path, report_path, opts=opts)
 
 
 if __name__ == '__main__':

Added: z3c.coverage/trunk/src/z3c/coverage/sampleinput.pickle
===================================================================
--- z3c.coverage/trunk/src/z3c/coverage/sampleinput.pickle	                        (rev 0)
+++ z3c.coverage/trunk/src/z3c/coverage/sampleinput.pickle	2012-09-06 13:23:49 UTC (rev 127752)
@@ -0,0 +1,7 @@
+€}q(U	collectorqUcoverage v3.5.2qUlinesq}q(U:/home/mg/src/z3c.coverage/src/z3c/coverage/coveragediff.pyq]q(KKKKKKK!K"K6KAKDKZK[K\K]K^K_K`KaKdKjKkKnKxK{K‚KƒK†KˆK‰K‹KŒKKŽKK‘K”K–K—K˜K™KšK›KœKŸK¡K¢K£K¤K¥K¨KªK«K¬K­K®K¯K²K½K¾K¿KÂKÜKÝKÞKáKâKäKæKçKèKêKîKïKðKñKòKóKôKõKöKùKúKüKýKÿMMMMMMM	M
+MM
+MMMMMMMMMMMMMMMM!M"M#M$M'M)M*M+M,M-M.M/M0M1M3M4M5M6M7M8M9M;M<M=M>M?M@MBMCMDMEMFMIMJeU3/home/mg/src/z3c.coverage/src/z3c/coverage/tests.pyq]q	(KKKKK	K
+KKKKKKKKAK‚KKžK°KÄKÕKæKùMMM"M#M%M&M'M(M*M+M,M.M/M0M1M2M5eU</home/mg/src/z3c.coverage/src/z3c/coverage/coveragereport.pyq
+]q(K*K+K-K.K/K0K1K2K3K6K7K8K;K<K>K?K at KBKCKDKEKFKGKJKQKSKUKWKYK[K]K_KaKbKdKfKhKjKkKlKnKsKtKuKvKxKK€KK‚K…K†KˆK‰KŠKŒKŽKKK‘K“K”K•K–K˜KšK›KœKŸK K¢K­K½KÈKÎKÑKÚKÝKæKçKèKéKêKëKîKÿMMMMMMMMMMMM M"M#M%M(M*M+M,M/M0M1M2M3M4M5M7M:M<M=M>M@MAMBMCMDMEMFMVM^MaMoMpMqMrMsMtMuMvMwMxMyMzM{M|MM‚MƒM„M…M†MˆM‹MMŽMM’M”M—M˜M™MšM›MœMŸM¨M©MªM«M¬M­M®M¯M°M±M²M³M´MµM¶M·M¸M»M½M¾M¿MÀMÁMÂMÃMÄMÅMÆMÇMÊMËMÌMÏMäMåMæMçMèMëMñMòMóMôMýMÿMMMMMMMM	M
+MMMMMMMMMMMM M!M#M(M)M*M+M-M.M/M1M2M4M6M8M:M;M=M@MBMDMGMJMKeU6/home/mg/src/z3c.coverage/src/z3c/coverage/__init__.pyq]q
+Kauu.
\ No newline at end of file

Modified: z3c.coverage/trunk/src/z3c/coverage/tests.py
===================================================================
--- z3c.coverage/trunk/src/z3c/coverage/tests.py	2012-09-06 13:23:43 UTC (rev 127751)
+++ z3c.coverage/trunk/src/z3c/coverage/tests.py	2012-09-06 13:23:49 UTC (rev 127752)
@@ -3,15 +3,176 @@
 Test suite for z3c.coverage
 """
 
-import unittest
 import doctest
+import os
 import re
+import shutil
+import sys
+import tempfile
+import unittest
 
 from zope.testing import renormalizing
 
-from z3c.coverage.coveragereport import traverse_tree_in_order
+import z3c.coverage
+from z3c.coverage import coveragereport
+from z3c.coverage.coveragereport import (
+    Lazy, CoverageNode, index_to_nice_name, index_to_name, percent_to_colour,
+    traverse_tree_in_order, get_svn_revision, syntax_highlight)
 
 
+def doctest_Lazy():
+    """Test for Lazy
+
+        >>> class MyClass(object):
+        ...     @Lazy
+        ...     def foo(self):
+        ...         print "computing foo"
+        ...         return 42
+
+    This is basic lazy evaluation
+
+        >>> obj = MyClass()
+        >>> obj.foo
+        computing foo
+        42
+
+    The difference from @propetry is that the value is only computed once
+
+        >>> obj.foo
+        42
+
+    Another difference is that you can override it
+
+        >>> obj.foo = 25
+        >>> obj.foo
+        25
+
+    To force recomputation, delete the cached value
+
+        >>> del obj.foo
+        >>> obj.foo
+        computing foo
+        42
+
+    You can access the class attribute as well
+
+        >>> MyClass.foo # doctest: +ELLIPSIS
+        <z3c.coverage.coveragereport.Lazy object at ...>
+
+    """
+
+
+def doctest_CoverageNode():
+    """Test for CoverageNode
+
+        >>> root = CoverageNode()
+        >>> root['z3c'] = CoverageNode()
+        >>> root['z3c']['coverage'] = CoverageNode()
+        >>> root['z3c']['coverage']['coveragereport'] = CoverageNode()
+        >>> root['z3c']['coverage']['coveragereport'].covered = 40
+        >>> root['z3c']['coverage']['coveragereport'].total = 134
+        >>> root['z3c']['coverage']['coveragediff'] = CoverageNode()
+        >>> root['z3c']['coverage']['coveragediff'].covered = 128
+        >>> root['z3c']['coverage']['coveragediff'].total = 128
+        >>> root['z3c']['coverage']['__init__'] = CoverageNode()
+        >>> root['z3c']['coverage']['__init__'].covered = 0
+        >>> root['z3c']['coverage']['__init__'].total = 0
+
+    ``covered`` and ``total`` are lazily-computed properties that sum over
+    their children, recursively
+
+        >>> root.covered
+        168
+        >>> root.total
+        262
+
+    ``uncovered`` is also a lazily-computed property that computes the
+    difference
+
+        >>> root.uncovered
+        94
+
+    We can also ask for the percentile
+
+        >>> root.percent
+        64
+
+    and avoid division by zero
+
+        >>> root['z3c']['coverage']['__init__'].percent
+        100
+
+    There are helpers for traversal
+
+        >>> root.get_at([]) is root
+        True
+        >>> root.get_at(['z3c', 'coverage']) is root['z3c']['coverage']
+        True
+        >>> root.get_at(['z3c', 'nosuch'])
+        Traceback (most recent call last):
+          ...
+        KeyError: 'nosuch'
+
+    and helpers for construction
+
+        >>> root.set_at(['z3c', 'somethingelse'], CoverageNode())
+        >>> root['z3c']['somethingelse']
+        {}
+
+    Finally, we also can get a nice output:
+
+        >>> print root['z3c']
+        64% covered (94 of 262 lines uncovered)
+
+    """
+
+
+def doctest_index_to_nice_name():
+    """Test for index_to_nice_name
+
+    Takes an indexed Python path and produces a nice "human-readable" string:
+
+        >>> index_to_nice_name(['z3c', 'coverage', 'coveragereport'])
+        '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;coveragereport'
+
+        >>> index_to_nice_name([])
+        'Everything'
+
+    """
+
+
+def doctest_index_to_name():
+    """Test for index_to_name
+
+    Takes an indexed Python path and produces a "human-readable" string:
+
+        >>> index_to_name(['z3c', 'coverage', 'coveragereport'])
+        'z3c.coverage.coveragereport'
+
+        >>> index_to_name([])
+        'everything'
+
+    """
+
+
+def doctest_percent_to_colour():
+    """Test for percent_to_colour
+
+    Given a coverage percentage, this function returns a color to represent the
+    coverage:
+
+        >>> percent_to_colour(100)
+        'green'
+        >>> percent_to_colour(92)
+        'yellow'
+        >>> percent_to_colour(85)
+        'orange'
+        >>> percent_to_colour(50)
+        'red'
+
+    """
+
+
 def doctest_traverse_tree_in_order():
     """Test for traverse_tree_in_order
 
@@ -32,6 +193,96 @@
     """
 
 
+def doctest_get_svn_revision():
+    """Test for get_svn_revision
+
+    Given a path, the function tries to determine the revision number of the
+    file. If it fails, "UNKNOWN" is returned:
+
+        >>> path = os.path.dirname(z3c.coverage.__file__)
+        >>> get_svn_revision(path) != 'UNKNOWN'
+        ... # This will fail if you don't have svnversion in your $PATH
+        True
+
+        >>> get_svn_revision(path + '/nosuchpath')
+        'UNKNOWN'
+
+    """
+
+
+def doctest_syntax_highlight_with_enscript():
+    """Test for syntax_highlight
+
+    This function takes a cover file, converts it to a nicely colored HTML output:
+
+        >>> filename = os.path.join(
+        ...     os.path.dirname(z3c.coverage.__file__), '__init__.py')
+
+        >>> print syntax_highlight(filename)
+        ... # this will fail if you don't have enscript in your $PATH
+        <BLANKLINE>
+        <I><FONT COLOR="#B22222"># Make a package.
+        </FONT></I>
+
+    """
+
+
+def doctest_syntax_highlight_without_enscript():
+    """Test for syntax_highlight
+
+    If the highlighing command is not available, no coloration is done:
+
+        >>> command_orig = coveragereport.HIGHLIGHT_COMMAND
+        >>> coveragereport.HIGHLIGHT_COMMAND = ['aflkakhalkjdsjdhf']
+
+        >>> filename = os.path.join(
+        ...     os.path.dirname(z3c.coverage.__file__), '__init__.py')
+
+        >>> print syntax_highlight(filename)
+        # Make a package.
+        <BLANKLINE>
+
+        >>> coveragereport.HIGHLIGHT_COMMAND = command_orig
+
+    """
+
+def doctest_main_default_arguments():
+    """Test for main()
+
+    Defaults are chosen when no input and output dir is specified.
+
+        >>> def make_coverage_reports_stub(path, report_path, **kw):
+        ...     print path
+        ...     print report_path
+
+        >>> make_coverage_reports_orig = coveragereport.make_coverage_reports
+        >>> coveragereport.make_coverage_reports = make_coverage_reports_stub
+
+        >>> orig_argv = sys.argv
+        >>> sys.argv = ['coveragereport']
+
+        >>> tempDir = tempfile.mkdtemp(prefix='tmp-z3c.coverage-report-')
+        >>> orig_cwd = os.getcwd()
+        >>> os.chdir(tempDir)
+        >>> os.mkdir('coverage')
+
+        >>> coveragereport.main()
+        coverage
+        coverage/reports
+
+        >>> os.rmdir('coverage')
+        >>> coveragereport.main()
+        .coverage
+        coverage-reports
+
+        >>> coveragereport.make_coverage_reports = make_coverage_reports_orig
+        >>> sys.argv = orig_argv
+        >>> os.chdir(orig_cwd)
+        >>> shutil.rmtree(tempDir)
+
+    """
+
+
 def test_suite():
     checker = renormalizing.RENormalizing([
                 # optparse in Python 2.4 prints "usage:" and "options:",



More information about the checkins mailing list