diff options
author | Ned Batchelder <ned@nedbatchelder.com> | 2009-04-20 09:13:14 -0400 |
---|---|---|
committer | Ned Batchelder <ned@nedbatchelder.com> | 2009-04-20 09:13:14 -0400 |
commit | fb133d4b8881bd8368db54039c1dc3d243c83095 (patch) | |
tree | a8474b0fbdbdb56089792b20d2324d0808c3c0c6 | |
parent | c05bb65048278484b59bc71f28feb846748db746 (diff) | |
download | python-coveragepy-git-fb133d4b8881bd8368db54039c1dc3d243c83095.tar.gz |
HTML reporting, phase 0.
-rw-r--r-- | coverage/annotate.py | 22 | ||||
-rw-r--r-- | coverage/cmdline.py | 13 | ||||
-rw-r--r-- | coverage/codeunit.py | 4 | ||||
-rw-r--r-- | coverage/html.py | 132 | ||||
-rw-r--r-- | coverage/report.py | 27 | ||||
-rw-r--r-- | test/test_coverage.py | 2 |
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("&", "&").replace("<", "<").replace(">", ">") + .replace("'", "'").replace('"', """) + .replace(" ", " ") + ) + +def not_empty(t): + """Make sure HTML content is not completely empty.""" + return t or " " + + +# 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']) |