You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
392 lines
11 KiB
392 lines
11 KiB
"""Code-coverage tools for CherryPy.
|
|
|
|
To use this module, or the coverage tools in the test suite,
|
|
you need to download 'coverage.py', either Gareth Rees' `original
|
|
implementation <http://www.garethrees.org/2001/12/04/python-coverage/>`_
|
|
or Ned Batchelder's `enhanced version:
|
|
<http://www.nedbatchelder.com/code/modules/coverage.html>`_
|
|
|
|
To turn on coverage tracing, use the following code::
|
|
|
|
cherrypy.engine.subscribe('start', covercp.start)
|
|
|
|
DO NOT subscribe anything on the 'start_thread' channel, as previously
|
|
recommended. Calling start once in the main thread should be sufficient
|
|
to start coverage on all threads. Calling start again in each thread
|
|
effectively clears any coverage data gathered up to that point.
|
|
|
|
Run your code, then use the ``covercp.serve()`` function to browse the
|
|
results in a web browser. If you run this module from the command line,
|
|
it will call ``serve()`` for you.
|
|
"""
|
|
|
|
import re
|
|
import sys
|
|
import cgi
|
|
import os
|
|
import os.path
|
|
|
|
import cherrypy
|
|
from cherrypy._cpcompat import quote_plus
|
|
|
|
|
|
localFile = os.path.join(os.path.dirname(__file__), 'coverage.cache')
|
|
|
|
the_coverage = None
|
|
try:
|
|
from coverage import coverage
|
|
the_coverage = coverage(data_file=localFile)
|
|
|
|
def start():
|
|
the_coverage.start()
|
|
except ImportError:
|
|
# Setting the_coverage to None will raise errors
|
|
# that need to be trapped downstream.
|
|
the_coverage = None
|
|
|
|
import warnings
|
|
warnings.warn(
|
|
'No code coverage will be performed; '
|
|
'coverage.py could not be imported.')
|
|
|
|
def start():
|
|
pass
|
|
start.priority = 20
|
|
|
|
TEMPLATE_MENU = """<html>
|
|
<head>
|
|
<title>CherryPy Coverage Menu</title>
|
|
<style>
|
|
body {font: 9pt Arial, serif;}
|
|
#tree {
|
|
font-size: 8pt;
|
|
font-family: Andale Mono, monospace;
|
|
white-space: pre;
|
|
}
|
|
#tree a:active, a:focus {
|
|
background-color: black;
|
|
padding: 1px;
|
|
color: white;
|
|
border: 0px solid #9999FF;
|
|
-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; }
|
|
.directory {
|
|
color: #933;
|
|
font-style: italic;
|
|
font-weight: bold;
|
|
font-size: 10pt;
|
|
}
|
|
.file {
|
|
color: #400;
|
|
}
|
|
a { text-decoration: none; }
|
|
#crumbs {
|
|
color: white;
|
|
font-size: 8pt;
|
|
font-family: Andale Mono, monospace;
|
|
width: 100%;
|
|
background-color: black;
|
|
}
|
|
#crumbs a {
|
|
color: #f88;
|
|
}
|
|
#options {
|
|
line-height: 2.3em;
|
|
border: 1px solid black;
|
|
background-color: #eee;
|
|
padding: 4px;
|
|
}
|
|
#exclude {
|
|
width: 100%;
|
|
margin-bottom: 3px;
|
|
border: 1px solid #999;
|
|
}
|
|
#submit {
|
|
background-color: black;
|
|
color: white;
|
|
border: 0;
|
|
margin-bottom: -9px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h2>CherryPy Coverage</h2>"""
|
|
|
|
TEMPLATE_FORM = """
|
|
<div id="options">
|
|
<form action='menu' method=GET>
|
|
<input type='hidden' name='base' value='%(base)s' />
|
|
Show percentages
|
|
<input type='checkbox' %(showpct)s name='showpct' value='checked' /><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' id="submit"/>
|
|
</form>
|
|
</div>"""
|
|
|
|
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>
|
|
"""
|
|
|
|
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"""
|
|
|
|
TEMPLATE_ITEM = (
|
|
"%s%s<a class='file' href='report?name=%s' target='main'>%s</a>\n"
|
|
)
|
|
|
|
|
|
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='',
|
|
coverage=the_coverage):
|
|
|
|
# Show the directory name and any of our children
|
|
dirs = [k for k, v in root.items() if v]
|
|
dirs.sort()
|
|
for name in dirs:
|
|
newpath = os.path.join(path, name)
|
|
|
|
if newpath.lower().startswith(base):
|
|
relpath = newpath[len(base):]
|
|
yield '| ' * relpath.count(os.sep)
|
|
yield (
|
|
"<a class='directory' "
|
|
"href='menu?base=%s&exclude=%s'>%s</a>\n" %
|
|
(newpath, quote_plus(exclude), name)
|
|
)
|
|
|
|
for chunk in _show_branch(
|
|
root[name], base, newpath, pct, showpct,
|
|
exclude, coverage=coverage
|
|
):
|
|
yield chunk
|
|
|
|
# Now list the files
|
|
if path.lower().startswith(base):
|
|
relpath = path[len(base):]
|
|
files = [k for k, v in root.items() if not v]
|
|
files.sort()
|
|
for name in files:
|
|
newpath = os.path.join(path, name)
|
|
|
|
pc_str = ''
|
|
if showpct:
|
|
try:
|
|
_, statements, _, missing, _ = coverage.analysis2(newpath)
|
|
except:
|
|
# Yes, we really want to pass on all errors.
|
|
pass
|
|
else:
|
|
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:
|
|
pc_str = "<span class='pass'>%s</span>" % pc_str
|
|
|
|
yield TEMPLATE_ITEM % ('| ' * (relpath.count(os.sep) + 1),
|
|
pc_str, newpath, name)
|
|
|
|
|
|
def _skip_file(path, exclude):
|
|
if exclude:
|
|
return bool(re.search(exclude, path))
|
|
|
|
|
|
def _graft(path, tree):
|
|
d = tree
|
|
|
|
p = path
|
|
atoms = []
|
|
while True:
|
|
p, tail = os.path.split(p)
|
|
if not tail:
|
|
break
|
|
atoms.append(tail)
|
|
atoms.append(p)
|
|
if p != '/':
|
|
atoms.append('/')
|
|
|
|
atoms.reverse()
|
|
for node in atoms:
|
|
if node:
|
|
d = d.setdefault(node, {})
|
|
|
|
|
|
def get_tree(base, exclude, coverage=the_coverage):
|
|
"""Return covered module names as a nested dict."""
|
|
tree = {}
|
|
runs = coverage.data.executed_files()
|
|
for path in runs:
|
|
if not _skip_file(path, exclude) and not os.path.isdir(path):
|
|
_graft(path, tree)
|
|
return tree
|
|
|
|
|
|
class CoverStats(object):
|
|
|
|
def __init__(self, coverage, root=None):
|
|
self.coverage = coverage
|
|
if root is None:
|
|
# Guess initial depth. Files outside this path will not be
|
|
# reachable from the web interface.
|
|
import cherrypy
|
|
root = os.path.dirname(cherrypy.__file__)
|
|
self.root = root
|
|
|
|
@cherrypy.expose
|
|
def index(self):
|
|
return TEMPLATE_FRAMESET % self.root.lower()
|
|
|
|
@cherrypy.expose
|
|
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()
|
|
|
|
# Start by showing links for parent paths
|
|
yield "<div id='crumbs'>"
|
|
path = ''
|
|
atoms = base.split(os.sep)
|
|
atoms.pop()
|
|
for atom in atoms:
|
|
path += atom + os.sep
|
|
yield ("<a href='menu?base=%s&exclude=%s'>%s</a> %s"
|
|
% (path, quote_plus(exclude), atom, os.sep))
|
|
yield '</div>'
|
|
|
|
yield "<div id='tree'>"
|
|
|
|
# Then display the tree
|
|
tree = get_tree(base, exclude, self.coverage)
|
|
if not tree:
|
|
yield '<p>No modules covered.</p>'
|
|
else:
|
|
for chunk in _show_branch(tree, base, '/', pct,
|
|
showpct == 'checked', exclude,
|
|
coverage=self.coverage):
|
|
yield chunk
|
|
|
|
yield '</div>'
|
|
yield '</body></html>'
|
|
|
|
def annotated_file(self, filename, statements, excluded, missing):
|
|
source = open(filename, 'r')
|
|
buffer = []
|
|
for lineno, line in enumerate(source.readlines()):
|
|
lineno += 1
|
|
line = line.strip('\n\r')
|
|
empty_the_buffer = True
|
|
if lineno in excluded:
|
|
template = TEMPLATE_LOC_EXCLUDED
|
|
elif lineno in missing:
|
|
template = TEMPLATE_LOC_NOT_COVERED
|
|
elif lineno in statements:
|
|
template = TEMPLATE_LOC_COVERED
|
|
else:
|
|
empty_the_buffer = False
|
|
buffer.append((lineno, line))
|
|
if empty_the_buffer:
|
|
for lno, pastline in buffer:
|
|
yield template % (lno, cgi.escape(pastline))
|
|
buffer = []
|
|
yield template % (lineno, cgi.escape(line))
|
|
|
|
@cherrypy.expose
|
|
def report(self, name):
|
|
filename, statements, excluded, missing, _ = self.coverage.analysis2(
|
|
name)
|
|
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>'
|
|
|
|
|
|
def serve(path=localFile, port=8080, root=None):
|
|
if coverage is None:
|
|
raise ImportError('The coverage module could not be imported.')
|
|
from coverage import coverage
|
|
cov = coverage(data_file=path)
|
|
cov.load()
|
|
|
|
import cherrypy
|
|
cherrypy.config.update({'server.socket_port': int(port),
|
|
'server.thread_pool': 10,
|
|
'environment': 'production',
|
|
})
|
|
cherrypy.quickstart(CoverStats(cov, root))
|
|
|
|
if __name__ == '__main__':
|
|
serve(*tuple(sys.argv[1:]))
|