diff options
author | Robert Brewer <fumanchu@aminus.org> | 2005-08-23 21:26:17 +0000 |
---|---|---|
committer | Robert Brewer <fumanchu@aminus.org> | 2005-08-23 21:26:17 +0000 |
commit | 7fd137e340b15e6ecb3b2bf2724ee41414d7c1f0 (patch) | |
tree | 2275370ba566ed6b77a8a4866dc4e79b0ea515c4 /cherrypy/lib/covercp.py | |
parent | f52dc755cf1db028858df69afa284fb322dd8616 (diff) | |
download | cherrypy-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.py | 337 |
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 </td> + <td>%s</td> +</tr>\n""" +TEMPLATE_LOC_NOT_COVERED = """<tr class="notcovered"> + <td class="lineno">%s </td> + <td>%s</td> +</tr>\n""" +TEMPLATE_LOC_EXCLUDED = """<tr class="excluded"> + <td class="lineno">%s </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>" + ("| " * 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(' ',' ') + if pc < float(pct) or pc == -1: + pc_str = "<span class='fail'>%s</span>" % pc_str else: - newpath = name - - yield "<nobr>" + ("| " * 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 = " " + pc_str - if pc < 10: - pc_str = " " + 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" - % ("| " * 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" + % ("| " * (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 ' ' + 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:])) |