summaryrefslogtreecommitdiff
path: root/cherrypy/lib/covercp.py
diff options
context:
space:
mode:
authorRobert Brewer <fumanchu@aminus.org>2005-08-23 21:26:17 +0000
committerRobert Brewer <fumanchu@aminus.org>2005-08-23 21:26:17 +0000
commit7fd137e340b15e6ecb3b2bf2724ee41414d7c1f0 (patch)
tree2275370ba566ed6b77a8a4866dc4e79b0ea515c4 /cherrypy/lib/covercp.py
parentf52dc755cf1db028858df69afa284fb322dd8616 (diff)
downloadcherrypy-git-7fd137e340b15e6ecb3b2bf2724ee41414d7c1f0.tar.gz
Upgrades to coverage browser (thanks to Keir Mierle). See ticket #262.
Diffstat (limited to 'cherrypy/lib/covercp.py')
-rw-r--r--cherrypy/lib/covercp.py337
1 files changed, 204 insertions, 133 deletions
diff --git a/cherrypy/lib/covercp.py b/cherrypy/lib/covercp.py
index 9c1a727c..288626b0 100644
--- a/cherrypy/lib/covercp.py
+++ b/cherrypy/lib/covercp.py
@@ -43,6 +43,8 @@ If you run this module from the command line, it will call serve() for you.
import re
import sys
+import cgi
+import urllib
import os, os.path
localFile = os.path.join(os.path.dirname(__file__), "coverage.cache")
@@ -51,7 +53,6 @@ try:
except ImportError:
import StringIO
-
try:
from coverage import the_coverage as coverage
def start():
@@ -67,22 +68,11 @@ except ImportError:
def start():
pass
+# Guess initial depth to hide FIXME this doesn't work for non-cherrypy stuff
+import cherrypy
+initial_base = os.path.dirname(cherrypy.__file__)
-class CoverStats(object):
-
- def index(self):
- return """<html>
- <head><title>CherryPy coverage data</title></head>
- <frameset cols='250, 1*'>
- <frame src='menu' />
- <frame name='main' src='' />
- </frameset>
- </html>
- """
- index.exposed = True
-
- def menu(self, base="", pct=""):
- yield """<html>
+TEMPLATE_MENU = """<html>
<head>
<title>CherryPy Coverage Menu</title>
<style>
@@ -95,149 +85,231 @@ class CoverStats(object):
-moz-outline-style: none;
}
.fail {color: red;}
+ .pass {color: #888;}
#pct {text-align: right;}
+ h3 { font-size: small; font-weight: bold; font-style: italic; margin-top: 5px;}
+ input { border: 1px solid #ccc; padding: 2px; }
</style>
</head>
<body>
<h2>CherryPy Coverage</h2>"""
+
+TEMPLATE_FORM = """
+<form action='menu' method=GET>
+ <input type='hidden' name='base' value='%(base)s' />
+ <h3>Options</h3>
+ <input type='checkbox' %(showpct)s name='showpct' value='checked'/>
+ show percentages <br />
+ Hide files over <input type='text' id='pct' name='pct' value='%(pct)s' size='3' />%%<br />
+ Exclude files matching<br />
+ <input type='text' id='exclude' name='exclude' value='%(exclude)s' size='20' />
+ <br />
+
+ <input type='submit' value='Change view' />
+</form>"""
+
+TEMPLATE_FRAMESET = """<html>
+<head><title>CherryPy coverage data</title></head>
+<frameset cols='250, 1*'>
+ <frame src='menu?base=%s' />
+ <frame name='main' src='' />
+</frameset>
+</html>
+""" % initial_base.lower()
+
+TEMPLATE_COVERAGE = """<html>
+<head>
+ <title>Coverage for %(name)s</title>
+ <style>
+ h2 { margin-bottom: .25em; }
+ p { margin: .25em; }
+ .covered { color: #000; background-color: #fff; }
+ .notcovered { color: #fee; background-color: #500; }
+ .excluded { color: #00f; background-color: #fff; }
+ table .covered, table .notcovered, table .excluded
+ { font-family: Andale Mono, monospace;
+ font-size: 10pt; white-space: pre; }
+
+ .lineno { background-color: #eee;}
+ .notcovered .lineno { background-color: #000;}
+ table { border-collapse: collapse;
+ </style>
+</head>
+<body>
+<h2>%(name)s</h2>
+<p>%(fullpath)s</p>
+<p>Coverage: %(pc)s%%</p>"""
+
+TEMPLATE_LOC_COVERED = """<tr class="covered">
+ <td class="lineno">%s&nbsp;</td>
+ <td>%s</td>
+</tr>\n"""
+TEMPLATE_LOC_NOT_COVERED = """<tr class="notcovered">
+ <td class="lineno">%s&nbsp;</td>
+ <td>%s</td>
+</tr>\n"""
+TEMPLATE_LOC_EXCLUDED = """<tr class="excluded">
+ <td class="lineno">%s&nbsp;</td>
+ <td>%s</td>
+</tr>\n"""
+
+
+def _skip_file(path, exclude):
+ if exclude:
+ return bool(re.search(exclude, path))
+
+def _percent(statements, missing):
+ s = len(statements)
+ e = s - len(missing)
+ if s > 0:
+ return int(round(100.0 * e / s))
+ return 0
+
+def _show_branch(root, base="", path="", pct=0, showpct=False, exclude=""):
+
+ # Show the directory name and any of our children
+ dirs = [k for k, v in root.iteritems() if v is not None]
+ dirs.sort()
+ for name in dirs:
+ if path:
+ newpath = os.sep.join((path, name))
+ else:
+ newpath = name
- coverage.get_ready()
- runs = coverage.cexecuted.keys()
- if runs:
- yield """<form action='menu'>
- <input type='hidden' name='base' value='%s' />
- <input type='submit' value='Show %%' />
- threshold: <input type='text' id='pct' name='pct' value='%s' size='3' />%%
-</form>""" % (base, pct or "50")
+ if newpath.startswith(base):
+ relpath = newpath[len(base):]
+ yield "<nobr>" + ("|&nbsp;" * relpath.count(os.sep)) + "<b>"
+ yield ("<a href='menu?base=%s&exclude=%s'>%s</a>" %
+ (newpath, urllib.quote_plus(exclude), name))
+ yield "</b></nobr><br />\n"
+
+ for chunk in _show_branch(root[name], base, newpath, pct, showpct, exclude):
+ yield chunk
+
+ # Now list the files
+ if path.startswith(base):
+ relpath = path[len(base):]
+ files = [k for k, v in root.iteritems() if v is None]
+ files.sort()
+ for name in files:
+ if path:
+ newpath = os.sep.join((path, name))
+ else:
+ newpath = name
- yield "<div id='tree'>"
- tree = {}
- def graft(path):
- b, n = os.path.split(path)
- if n:
- return graft(b).setdefault(n, {})
+ pc_str = ""
+ if showpct:
+ try:
+ _, statements, _, missing, _ = coverage.analysis2(newpath)
+ except:
+ # Yes, we really want to pass on all errors.
+ pass
else:
- return tree.setdefault(b.strip(r"\/"), {})
- for path in runs:
- if not os.path.isdir(path):
- b, n = os.path.split(path)
- if b.startswith(base):
- graft(b)[n] = None
-
- def show(root, depth=0, path=""):
- dirs = [k for k, v in root.iteritems() if v is not None]
- dirs.sort()
- for name in dirs:
- if path:
- newpath = os.sep.join((path, name))
+ pc = _percent(statements, missing)
+ pc_str = ("%3d%% " % pc).replace(' ','&nbsp;')
+ if pc < float(pct) or pc == -1:
+ pc_str = "<span class='fail'>%s</span>" % pc_str
else:
- newpath = name
-
- yield "<nobr>" + ("|&nbsp;" * depth) + "<b>"
- yield "<a href='menu?base=%s'>%s</a>" % (newpath, name)
- yield "</b></nobr><br />\n"
- for chunk in show(root[name], depth + 1, newpath):
- yield chunk
-
- files = [k for k, v in root.iteritems() if v is None]
- files.sort()
- for name in files:
- if path:
- newpath = os.sep.join((path, name))
- else:
- newpath = name
-
- pc_str = ""
- if pct:
- try:
- _, statements, _, missing, _ = coverage.analysis2(newpath)
- except:
- # Yes, we really want to pass on all errors.
- pass
- else:
- s = len(statements)
- e = s - len(missing)
- if s > 0:
- pc = 100.0 * e / s
- pc_str = "%d%% " % pc
- if pc < 100:
- pc_str = "&nbsp;" + pc_str
- if pc < 10:
- pc_str = "&nbsp;" + pc_str
- if pc < float(pct):
- pc_str = "<span class='fail'>%s</span>" % pc_str
- yield ("<nobr>%s%s<a href='report?name=%s' target='main'>%s</a></nobr><br />\n"
- % ("|&nbsp;" * depth, pc_str, newpath, name))
-
- for chunk in show(tree):
- yield chunk
+ pc_str = "<span class='pass'>%s</span>" % pc_str
- yield "</div>"
- else:
+ yield ("<nobr>%s%s<a href='report?name=%s' target='main'>%s</a></nobr><br />\n"
+ % ("|&nbsp;" * (relpath.count(os.sep) + 1), pc_str, newpath, name))
+
+def get_tree(base, exclude):
+ """Return covered module names as a nested dict."""
+ tree = {}
+ coverage.get_ready()
+ runs = coverage.cexecuted.keys()
+ if runs:
+ tree = {}
+ def graft(path):
+ head, tail = os.path.split(path)
+ if tail:
+ return graft(head).setdefault(tail, {})
+ else:
+ return tree.setdefault(head.strip(r"\/"), {})
+
+ for path in runs:
+ if not _skip_file(path, exclude) and not os.path.isdir(path):
+ head, tail = os.path.split(path)
+ if head.startswith(base):
+ graft(head)[tail] = None
+ return tree
+
+
+class CoverStats(object):
+
+ def index(self):
+ return TEMPLATE_FRAMESET
+ index.exposed = True
+
+ def menu(self, base="", pct="50", showpct="",
+ exclude=r'python\d\.\d|test|tut\d|tutorial'):
+
+ # The coverage module uses all-lower-case names.
+ base = base.lower().rstrip(os.sep)
+
+ yield TEMPLATE_MENU
+ yield TEMPLATE_FORM % locals()
+
+ yield "<div id='tree'>"
+
+ # Start by showing links for parent paths
+ path = ""
+ atoms = base.split(os.sep)
+ atoms.pop()
+ for atom in atoms:
+ path += atom + os.sep
+ yield ("<nobr><b><a href='menu?base=%s&exclude=%s'>%s</a></b></nobr>%s\n"
+ % (path, urllib.quote_plus(exclude), atom, os.sep))
+
+ tree = get_tree(base, exclude)
+ if not tree:
yield "<p>No modules covered.</p>"
+ else:
+ # Now show all visible branches
+ yield "<br />"
+ for chunk in _show_branch(tree, base, "", pct, showpct=='checked', exclude):
+ yield chunk
+
+ yield "</div>"
yield "</body></html>"
menu.exposed = True
def annotated_file(self, filename, statements, excluded, missing):
source = open(filename, 'r')
- dest = StringIO.StringIO()
lineno = 0
- i = 0
- j = 0
- covered = 1
while 1:
line = source.readline()
if line == '':
break
+ line = line[:-1]
lineno = lineno + 1
- while i < len(statements) and statements[i] < lineno:
- i = i + 1
- while j < len(missing) and missing[j] < lineno:
- j = j + 1
- if i < len(statements) and statements[i] == lineno:
- covered = j >= len(missing) or missing[j] > lineno
- if coverage.blank_re.match(line):
- dest.write(' ')
- elif coverage.else_re.match(line):
- # Special logic for lines containing only
- # 'else:'. See [GDR 2001-12-04b, 3.2].
- if i >= len(statements) and j >= len(missing):
- dest.write('! ')
- elif i >= len(statements) or j >= len(missing):
- dest.write('> ')
- elif statements[i] == missing[j]:
- dest.write('! ')
- else:
- dest.write('> ')
- elif lineno in excluded:
- dest.write('- ')
- elif covered:
- dest.write('> ')
+ if line == '':
+ yield '&nbsp;'
+ continue
+ if lineno in excluded:
+ template = TEMPLATE_LOC_EXCLUDED
+ elif lineno in missing:
+ template = TEMPLATE_LOC_NOT_COVERED
else:
- dest.write('! ')
- dest.write(line)
- source.close()
- result = dest.getvalue()
- dest.close()
- return result
+ template = TEMPLATE_LOC_COVERED
+ yield template % (lineno, cgi.escape(line))
def report(self, name):
- import cherrypy
- cherrypy.response.headerMap['Content-Type'] = 'text/plain'
-
- yield name
- yield "\n"
-
coverage.get_ready()
filename, statements, excluded, missing, _ = coverage.analysis2(name)
- s = len(statements)
- e = s - len(missing)
- if s > 0:
- pc = 100.0 * e / s
- yield "%2d%% covered\n" % pc
-
- yield "\n"
- yield self.annotated_file(filename, statements, excluded, missing)
+ pc = _percent(statements, missing)
+ yield TEMPLATE_COVERAGE % dict(name=os.path.basename(name),
+ fullpath=name,
+ pc=pc)
+ yield '<table>\n'
+ for line in self.annotated_file(filename, statements, excluded,
+ missing):
+ yield line
+ yield '</table>'
+ yield '</body>'
+ yield '</html>'
report.exposed = True
@@ -254,7 +326,6 @@ def serve(path=localFile, port=8080):
})
cherrypy.server.start()
-
if __name__ == "__main__":
serve(*tuple(sys.argv[1:]))