summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2009-04-20 09:13:14 -0400
committerNed Batchelder <ned@nedbatchelder.com>2009-04-20 09:13:14 -0400
commitfb133d4b8881bd8368db54039c1dc3d243c83095 (patch)
treea8474b0fbdbdb56089792b20d2324d0808c3c0c6
parentc05bb65048278484b59bc71f28feb846748db746 (diff)
downloadpython-coveragepy-git-fb133d4b8881bd8368db54039c1dc3d243c83095.tar.gz
HTML reporting, phase 0.
-rw-r--r--coverage/annotate.py22
-rw-r--r--coverage/cmdline.py13
-rw-r--r--coverage/codeunit.py4
-rw-r--r--coverage/html.py132
-rw-r--r--coverage/report.py27
-rw-r--r--test/test_coverage.py2
6 files changed, 177 insertions, 23 deletions
diff --git a/coverage/annotate.py b/coverage/annotate.py
index b861a6f4..ee04910a 100644
--- a/coverage/annotate.py
+++ b/coverage/annotate.py
@@ -13,24 +13,8 @@ class AnnotateReporter(Reporter):
else_re = re.compile(r"\s*else\s*:\s*(#|$)")
def report(self, morfs, directory=None, omit_prefixes=None):
- self.find_code_units(morfs, omit_prefixes)
-
- self.directory = directory
- if self.directory and not os.path.exists(self.directory):
- os.makedirs(self.directory)
-
- for cu in self.code_units:
- try:
- if not cu.relative:
- continue
- statements, excluded, missing, _ = self.coverage.analyze(cu)
- self.annotate_file(cu, statements, excluded, missing)
- except KeyboardInterrupt:
- raise
- except:
- if not self.ignore_errors:
- raise
-
+ self.report_files(self.annotate_file, morfs, directory, omit_prefixes)
+
def annotate_file(self, cu, statements, excluded, missing):
"""Annotate a single file.
@@ -38,7 +22,7 @@ class AnnotateReporter(Reporter):
"""
filename = cu.filename
- source = open(filename, 'r')
+ source = cu.source_file()
if self.directory:
dest_file = os.path.join(self.directory, cu.flat_rootname())
dest_file += ".py,cover"
diff --git a/coverage/cmdline.py b/coverage/cmdline.py
index dfcd3436..e6a6d812 100644
--- a/coverage/cmdline.py
+++ b/coverage/cmdline.py
@@ -3,6 +3,7 @@
import getopt, sys
from coverage.annotate import AnnotateReporter
+from coverage.html import HtmlReporter
from coverage.summary import SummaryReporter
from coverage.execfile import run_python_file
@@ -45,7 +46,7 @@ coverage -a [-d DIR] [-i] [-o DIR,...] [FILE1 FILE2 ...]
Coverage data is saved in the file .coverage by default. Set the
COVERAGE_FILE environment variable to save it somewhere else.
""".strip()
-
+#TODO: add -b to the help.
class CoverageScript:
def __init__(self):
@@ -66,6 +67,7 @@ class CoverageScript:
settings = {}
optmap = {
'-a': 'annotate',
+ '-b': 'html',
'-c': 'combine',
'-d:': 'directory=',
'-e': 'erase',
@@ -95,6 +97,7 @@ class CoverageScript:
return OK
# Check for conflicts and problems in the options.
+ #TODO: add -b conflict checking.
for i in ['erase', 'execute']:
for j in ['annotate', 'report', 'combine']:
if settings.get(i) and settings.get(j):
@@ -104,12 +107,13 @@ class CoverageScript:
args_needed = (settings.get('execute')
or settings.get('annotate')
+ or settings.get('html')
or settings.get('report'))
action = (settings.get('erase')
or settings.get('combine')
or args_needed)
if not action:
- help_fn("You must specify at least one of -e, -x, -c, -r, or -a.")
+ help_fn("You must specify at least one of -e, -x, -c, -r, -a, or -b.")
return ERR
if not args_needed and args:
help_fn("Unexpected arguments: %s" % " ".join(args))
@@ -153,7 +157,10 @@ class CoverageScript:
if settings.get('annotate'):
reporter = AnnotateReporter(self.coverage, ignore_errors)
reporter.report(args, directory, omit_prefixes=omit)
-
+ if settings.get('html'):
+ reporter = HtmlReporter(self.coverage, ignore_errors)
+ reporter.report(args, directory, omit_prefixes=omit)
+
return OK
# Main entrypoint. This is installed as the script entrypoint, so don't
diff --git a/coverage/codeunit.py b/coverage/codeunit.py
index ead6e545..cc72f5c0 100644
--- a/coverage/codeunit.py
+++ b/coverage/codeunit.py
@@ -96,3 +96,7 @@ class CodeUnit:
"""
root = os.path.splitdrive(os.path.splitext(self.name)[0])[1]
return root.replace('\\', '_').replace('/', '_')
+
+ def source_file(self):
+ """Return an open file for reading the source of the code unit."""
+ return open(self.filename)
diff --git a/coverage/html.py b/coverage/html.py
new file mode 100644
index 00000000..f0a94474
--- /dev/null
+++ b/coverage/html.py
@@ -0,0 +1,132 @@
+"""HTML reporting for coverage.py"""
+
+import os
+from coverage.report import Reporter
+from coverage.templite import Templite
+
+class HtmlReporter(Reporter):
+ """HTML reporting.
+
+ """
+
+ def __init__(self, coverage, ignore_errors=False):
+ super(HtmlReporter, self).__init__(coverage, ignore_errors)
+ self.directory = None
+ self.source_tmpl = Templite(SOURCE, globals())
+
+ def report(self, morfs, directory=None, omit_prefixes=None):
+ assert directory, "must provide a directory for html reporting"
+ self.report_files(self.html_file, morfs, directory, omit_prefixes)
+
+ def html_file(self, cu, statements, excluded, missing):
+ """Generate an HTML file for one source file."""
+
+ lines = []
+ source = cu.source_file()
+ for lineno, line in enumerate(source.readlines()):
+ lineno += 1
+
+ css_class = ""
+ if lineno in statements:
+ css_class += " s"
+ if lineno not in missing and lineno not in excluded:
+ css_class += " r"
+ if lineno in excluded:
+ css_class += " x"
+ if lineno in missing:
+ css_class += " m"
+
+ lineinfo = {
+ 'text': line,
+ 'number': lineno,
+ 'class': css_class.strip() or "p"
+ }
+ lines.append(lineinfo)
+
+ html_filename = os.path.join(self.directory, cu.flat_rootname()) + ".html"
+ fhtml = open(html_filename, 'w')
+ fhtml.write(self.source_tmpl.render(locals()))
+ fhtml.close()
+
+
+
+# Helpers for templates
+
+def escape(t):
+ """HTML-escape the text in t."""
+ return (
+ t.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
+ .replace("'", "&#39;").replace('"', "&quot;")
+ .replace(" ", "&nbsp; ")
+ )
+
+def not_empty(t):
+ """Make sure HTML content is not completely empty."""
+ return t or "&nbsp;"
+
+
+# Templates
+
+SOURCE = """\
+<html>
+<head>
+<title>Coverage of {{cu.filename|escape}}</title>
+<style>
+* {
+ font-size: 11pt;
+ line-height: 1.1em;
+ }
+.linenos {
+ background: #eee;
+ }
+.linenos p {
+ text-align: right;
+ margin: 0;
+ padding: 0 .5em 0 0;
+ font-family: verdana, sans-serif;
+ }
+.source p {
+ margin: 0;
+ padding: 0 0 0 .5em;
+ font-family: "courier new", monospace;
+ }
+
+.linenos p.m {
+ background: #fcc;
+ }
+.linenos p.r {
+ background: #cfc;
+ }
+.linenos p.x {
+ background: #ddd;
+ }
+
+.source p.m {
+ background: #fee;
+ }
+.source p.r {
+ background: #efe;
+ }
+.source p.x {
+ background: #eee;
+ }
+</style>
+</head>
+<body>
+<table cellspacing='0' cellpadding='0'>
+<tr>
+<td class='linenos' valign='top'>
+ {% for line in lines %}
+ <p class='{{line.class}}'>{{line.number}}</p>
+ {% endfor %}
+</td>
+<td class='source' valign='top'>
+ {% for line in lines %}
+ <p class='{{line.class}}'>{{line.text.rstrip|escape|not_empty}}</p>
+ {% endfor %}
+</td>
+</tr>
+</table>
+</body>
+</html>
+"""
diff --git a/coverage/report.py b/coverage/report.py
index 9859eb96..ef296762 100644
--- a/coverage/report.py
+++ b/coverage/report.py
@@ -1,5 +1,6 @@
"""Reporter foundation for coverage.py"""
+import os
from coverage.codeunit import code_unit_factory
class Reporter(object):
@@ -30,3 +31,29 @@ class Reporter(object):
morfs, self.coverage.file_locator, omit_prefixes
)
self.code_units.sort()
+
+ def report_files(self, report_fn, morfs, directory=None,
+ omit_prefixes=None):
+ """Run a reporting function on a number of morfs.
+
+ `report_fn` is called for each relative morf in `morfs`.
+
+ """
+ self.find_code_units(morfs, omit_prefixes)
+
+ self.directory = directory
+ if self.directory and not os.path.exists(self.directory):
+ os.makedirs(self.directory)
+
+ for cu in self.code_units:
+ try:
+ if not cu.relative:
+ continue
+ statements, excluded, missing, _ = self.coverage.analyze(cu)
+ report_fn(cu, statements, excluded, missing)
+ except KeyboardInterrupt:
+ raise
+ except:
+ if not self.ignore_errors:
+ raise
+
diff --git a/test/test_coverage.py b/test/test_coverage.py
index 07444b54..7d6e866a 100644
--- a/test/test_coverage.py
+++ b/test/test_coverage.py
@@ -1713,7 +1713,7 @@ class CmdLineTest(CoverageTest):
self.assertRaisesMsg(Exception, "You can't specify the 'execute' and 'combine' options at the same time.", self.command_line, ['-x', '-c'])
def testNeedAction(self):
- self.assertRaisesMsg(Exception, "You must specify at least one of -e, -x, -c, -r, or -a.", self.command_line, ['-p'])
+ self.assertRaisesMsg(Exception, "You must specify at least one of -e, -x, -c, -r, -a, or -b.", self.command_line, ['-p'])
def testArglessActions(self):
self.assertRaisesMsg(Exception, "Unexpected arguments: foo bar", self.command_line, ['-e', 'foo', 'bar'])