| 1 |
"""Code-coverage tools for CherryPy. |
|---|
| 2 |
|
|---|
| 3 |
To use this module, or the coverage tools in the test suite, |
|---|
| 4 |
you need to download 'coverage.py', either Gareth Rees' original |
|---|
| 5 |
implementation: |
|---|
| 6 |
http://www.garethrees.org/2001/12/04/python-coverage/ |
|---|
| 7 |
|
|---|
| 8 |
or Ned Batchelder's enhanced version: |
|---|
| 9 |
http://www.nedbatchelder.com/code/modules/coverage.html |
|---|
| 10 |
|
|---|
| 11 |
To turn on coverage tracing, use the following code: |
|---|
| 12 |
|
|---|
| 13 |
cherrypy.engine.subscribe('start', covercp.start) |
|---|
| 14 |
|
|---|
| 15 |
DO NOT subscribe anything on the 'start_thread' channel, as previously |
|---|
| 16 |
recommended. Calling start once in the main thread should be sufficient |
|---|
| 17 |
to start coverage on all threads. Calling start again in each thread |
|---|
| 18 |
effectively clears any coverage data gathered up to that point. |
|---|
| 19 |
|
|---|
| 20 |
Run your code, then use the covercp.serve() function to browse the |
|---|
| 21 |
results in a web browser. If you run this module from the command line, |
|---|
| 22 |
it will call serve() for you. |
|---|
| 23 |
""" |
|---|
| 24 |
|
|---|
| 25 |
import re |
|---|
| 26 |
import sys |
|---|
| 27 |
import cgi |
|---|
| 28 |
from urllib import quote_plus |
|---|
| 29 |
import os, os.path |
|---|
| 30 |
localFile = os.path.join(os.path.dirname(__file__), "coverage.cache") |
|---|
| 31 |
|
|---|
| 32 |
the_coverage = None |
|---|
| 33 |
try: |
|---|
| 34 |
from coverage import coverage |
|---|
| 35 |
the_coverage = coverage(data_file=localFile) |
|---|
| 36 |
def start(): |
|---|
| 37 |
the_coverage.start() |
|---|
| 38 |
except ImportError: |
|---|
| 39 |
|
|---|
| 40 |
|
|---|
| 41 |
the_coverage = None |
|---|
| 42 |
|
|---|
| 43 |
import warnings |
|---|
| 44 |
warnings.warn("No code coverage will be performed; coverage.py could not be imported.") |
|---|
| 45 |
|
|---|
| 46 |
def start(): |
|---|
| 47 |
pass |
|---|
| 48 |
start.priority = 20 |
|---|
| 49 |
|
|---|
| 50 |
TEMPLATE_MENU = """<html> |
|---|
| 51 |
<head> |
|---|
| 52 |
<title>CherryPy Coverage Menu</title> |
|---|
| 53 |
<style> |
|---|
| 54 |
body {font: 9pt Arial, serif;} |
|---|
| 55 |
#tree { |
|---|
| 56 |
font-size: 8pt; |
|---|
| 57 |
font-family: Andale Mono, monospace; |
|---|
| 58 |
white-space: pre; |
|---|
| 59 |
} |
|---|
| 60 |
#tree a:active, a:focus { |
|---|
| 61 |
background-color: black; |
|---|
| 62 |
padding: 1px; |
|---|
| 63 |
color: white; |
|---|
| 64 |
border: 0px solid #9999FF; |
|---|
| 65 |
-moz-outline-style: none; |
|---|
| 66 |
} |
|---|
| 67 |
.fail { color: red;} |
|---|
| 68 |
.pass { color: #888;} |
|---|
| 69 |
#pct { text-align: right;} |
|---|
| 70 |
h3 { |
|---|
| 71 |
font-size: small; |
|---|
| 72 |
font-weight: bold; |
|---|
| 73 |
font-style: italic; |
|---|
| 74 |
margin-top: 5px; |
|---|
| 75 |
} |
|---|
| 76 |
input { border: 1px solid #ccc; padding: 2px; } |
|---|
| 77 |
.directory { |
|---|
| 78 |
color: #933; |
|---|
| 79 |
font-style: italic; |
|---|
| 80 |
font-weight: bold; |
|---|
| 81 |
font-size: 10pt; |
|---|
| 82 |
} |
|---|
| 83 |
.file { |
|---|
| 84 |
color: #400; |
|---|
| 85 |
} |
|---|
| 86 |
a { text-decoration: none; } |
|---|
| 87 |
#crumbs { |
|---|
| 88 |
color: white; |
|---|
| 89 |
font-size: 8pt; |
|---|
| 90 |
font-family: Andale Mono, monospace; |
|---|
| 91 |
width: 100%; |
|---|
| 92 |
background-color: black; |
|---|
| 93 |
} |
|---|
| 94 |
#crumbs a { |
|---|
| 95 |
color: #f88; |
|---|
| 96 |
} |
|---|
| 97 |
#options { |
|---|
| 98 |
line-height: 2.3em; |
|---|
| 99 |
border: 1px solid black; |
|---|
| 100 |
background-color: #eee; |
|---|
| 101 |
padding: 4px; |
|---|
| 102 |
} |
|---|
| 103 |
#exclude { |
|---|
| 104 |
width: 100%; |
|---|
| 105 |
margin-bottom: 3px; |
|---|
| 106 |
border: 1px solid #999; |
|---|
| 107 |
} |
|---|
| 108 |
#submit { |
|---|
| 109 |
background-color: black; |
|---|
| 110 |
color: white; |
|---|
| 111 |
border: 0; |
|---|
| 112 |
margin-bottom: -9px; |
|---|
| 113 |
} |
|---|
| 114 |
</style> |
|---|
| 115 |
</head> |
|---|
| 116 |
<body> |
|---|
| 117 |
<h2>CherryPy Coverage</h2>""" |
|---|
| 118 |
|
|---|
| 119 |
TEMPLATE_FORM = """ |
|---|
| 120 |
<div id="options"> |
|---|
| 121 |
<form action='menu' method=GET> |
|---|
| 122 |
<input type='hidden' name='base' value='%(base)s' /> |
|---|
| 123 |
Show percentages <input type='checkbox' %(showpct)s name='showpct' value='checked' /><br /> |
|---|
| 124 |
Hide files over <input type='text' id='pct' name='pct' value='%(pct)s' size='3' />%%<br /> |
|---|
| 125 |
Exclude files matching<br /> |
|---|
| 126 |
<input type='text' id='exclude' name='exclude' value='%(exclude)s' size='20' /> |
|---|
| 127 |
<br /> |
|---|
| 128 |
|
|---|
| 129 |
<input type='submit' value='Change view' id="submit"/> |
|---|
| 130 |
</form> |
|---|
| 131 |
</div>""" |
|---|
| 132 |
|
|---|
| 133 |
TEMPLATE_FRAMESET = """<html> |
|---|
| 134 |
<head><title>CherryPy coverage data</title></head> |
|---|
| 135 |
<frameset cols='250, 1*'> |
|---|
| 136 |
<frame src='menu?base=%s' /> |
|---|
| 137 |
<frame name='main' src='' /> |
|---|
| 138 |
</frameset> |
|---|
| 139 |
</html> |
|---|
| 140 |
""" |
|---|
| 141 |
|
|---|
| 142 |
TEMPLATE_COVERAGE = """<html> |
|---|
| 143 |
<head> |
|---|
| 144 |
<title>Coverage for %(name)s</title> |
|---|
| 145 |
<style> |
|---|
| 146 |
h2 { margin-bottom: .25em; } |
|---|
| 147 |
p { margin: .25em; } |
|---|
| 148 |
.covered { color: #000; background-color: #fff; } |
|---|
| 149 |
.notcovered { color: #fee; background-color: #500; } |
|---|
| 150 |
.excluded { color: #00f; background-color: #fff; } |
|---|
| 151 |
table .covered, table .notcovered, table .excluded |
|---|
| 152 |
{ font-family: Andale Mono, monospace; |
|---|
| 153 |
font-size: 10pt; white-space: pre; } |
|---|
| 154 |
|
|---|
| 155 |
.lineno { background-color: #eee;} |
|---|
| 156 |
.notcovered .lineno { background-color: #000;} |
|---|
| 157 |
table { border-collapse: collapse; |
|---|
| 158 |
</style> |
|---|
| 159 |
</head> |
|---|
| 160 |
<body> |
|---|
| 161 |
<h2>%(name)s</h2> |
|---|
| 162 |
<p>%(fullpath)s</p> |
|---|
| 163 |
<p>Coverage: %(pc)s%%</p>""" |
|---|
| 164 |
|
|---|
| 165 |
TEMPLATE_LOC_COVERED = """<tr class="covered"> |
|---|
| 166 |
<td class="lineno">%s </td> |
|---|
| 167 |
<td>%s</td> |
|---|
| 168 |
</tr>\n""" |
|---|
| 169 |
TEMPLATE_LOC_NOT_COVERED = """<tr class="notcovered"> |
|---|
| 170 |
<td class="lineno">%s </td> |
|---|
| 171 |
<td>%s</td> |
|---|
| 172 |
</tr>\n""" |
|---|
| 173 |
TEMPLATE_LOC_EXCLUDED = """<tr class="excluded"> |
|---|
| 174 |
<td class="lineno">%s </td> |
|---|
| 175 |
<td>%s</td> |
|---|
| 176 |
</tr>\n""" |
|---|
| 177 |
|
|---|
| 178 |
TEMPLATE_ITEM = "%s%s<a class='file' href='report?name=%s' target='main'>%s</a>\n" |
|---|
| 179 |
|
|---|
| 180 |
def _percent(statements, missing): |
|---|
| 181 |
s = len(statements) |
|---|
| 182 |
e = s - len(missing) |
|---|
| 183 |
if s > 0: |
|---|
| 184 |
return int(round(100.0 * e / s)) |
|---|
| 185 |
return 0 |
|---|
| 186 |
|
|---|
| 187 |
def _show_branch(root, base, path, pct=0, showpct=False, exclude="", |
|---|
| 188 |
coverage=the_coverage): |
|---|
| 189 |
|
|---|
| 190 |
|
|---|
| 191 |
dirs = [k for k, v in root.items() if v] |
|---|
| 192 |
dirs.sort() |
|---|
| 193 |
for name in dirs: |
|---|
| 194 |
newpath = os.path.join(path, name) |
|---|
| 195 |
|
|---|
| 196 |
if newpath.lower().startswith(base): |
|---|
| 197 |
relpath = newpath[len(base):] |
|---|
| 198 |
yield "| " * relpath.count(os.sep) |
|---|
| 199 |
yield "<a class='directory' href='menu?base=%s&exclude=%s'>%s</a>\n" % \ |
|---|
| 200 |
(newpath, quote_plus(exclude), name) |
|---|
| 201 |
|
|---|
| 202 |
for chunk in _show_branch(root[name], base, newpath, pct, showpct, exclude, coverage=coverage): |
|---|
| 203 |
yield chunk |
|---|
| 204 |
|
|---|
| 205 |
|
|---|
| 206 |
if path.lower().startswith(base): |
|---|
| 207 |
relpath = path[len(base):] |
|---|
| 208 |
files = [k for k, v in root.items() if not v] |
|---|
| 209 |
files.sort() |
|---|
| 210 |
for name in files: |
|---|
| 211 |
newpath = os.path.join(path, name) |
|---|
| 212 |
|
|---|
| 213 |
pc_str = "" |
|---|
| 214 |
if showpct: |
|---|
| 215 |
try: |
|---|
| 216 |
_, statements, _, missing, _ = coverage.analysis2(newpath) |
|---|
| 217 |
except: |
|---|
| 218 |
|
|---|
| 219 |
pass |
|---|
| 220 |
else: |
|---|
| 221 |
pc = _percent(statements, missing) |
|---|
| 222 |
pc_str = ("%3d%% " % pc).replace(' ',' ') |
|---|
| 223 |
if pc < float(pct) or pc == -1: |
|---|
| 224 |
pc_str = "<span class='fail'>%s</span>" % pc_str |
|---|
| 225 |
else: |
|---|
| 226 |
pc_str = "<span class='pass'>%s</span>" % pc_str |
|---|
| 227 |
|
|---|
| 228 |
yield TEMPLATE_ITEM % ("| " * (relpath.count(os.sep) + 1), |
|---|
| 229 |
pc_str, newpath, name) |
|---|
| 230 |
|
|---|
| 231 |
def _skip_file(path, exclude): |
|---|
| 232 |
if exclude: |
|---|
| 233 |
return bool(re.search(exclude, path)) |
|---|
| 234 |
|
|---|
| 235 |
def _graft(path, tree): |
|---|
| 236 |
d = tree |
|---|
| 237 |
|
|---|
| 238 |
p = path |
|---|
| 239 |
atoms = [] |
|---|
| 240 |
while True: |
|---|
| 241 |
p, tail = os.path.split(p) |
|---|
| 242 |
if not tail: |
|---|
| 243 |
break |
|---|
| 244 |
atoms.append(tail) |
|---|
| 245 |
atoms.append(p) |
|---|
| 246 |
if p != "/": |
|---|
| 247 |
atoms.append("/") |
|---|
| 248 |
|
|---|
| 249 |
atoms.reverse() |
|---|
| 250 |
for node in atoms: |
|---|
| 251 |
if node: |
|---|
| 252 |
d = d.setdefault(node, {}) |
|---|
| 253 |
|
|---|
| 254 |
def get_tree(base, exclude, coverage=the_coverage): |
|---|
| 255 |
"""Return covered module names as a nested dict.""" |
|---|
| 256 |
tree = {} |
|---|
| 257 |
runs = coverage.data.executed_files() |
|---|
| 258 |
for path in runs: |
|---|
| 259 |
if not _skip_file(path, exclude) and not os.path.isdir(path): |
|---|
| 260 |
_graft(path, tree) |
|---|
| 261 |
return tree |
|---|
| 262 |
|
|---|
| 263 |
class CoverStats(object): |
|---|
| 264 |
|
|---|
| 265 |
def __init__(self, coverage, root=None): |
|---|
| 266 |
self.coverage = coverage |
|---|
| 267 |
if root is None: |
|---|
| 268 |
|
|---|
| 269 |
|
|---|
| 270 |
import cherrypy |
|---|
| 271 |
root = os.path.dirname(cherrypy.__file__) |
|---|
| 272 |
self.root = root |
|---|
| 273 |
|
|---|
| 274 |
def index(self): |
|---|
| 275 |
return TEMPLATE_FRAMESET % self.root.lower() |
|---|
| 276 |
index.exposed = True |
|---|
| 277 |
|
|---|
| 278 |
def menu(self, base="/", pct="50", showpct="", |
|---|
| 279 |
exclude=r'python\d\.\d|test|tut\d|tutorial'): |
|---|
| 280 |
|
|---|
| 281 |
|
|---|
| 282 |
base = base.lower().rstrip(os.sep) |
|---|
| 283 |
|
|---|
| 284 |
yield TEMPLATE_MENU |
|---|
| 285 |
yield TEMPLATE_FORM % locals() |
|---|
| 286 |
|
|---|
| 287 |
|
|---|
| 288 |
yield "<div id='crumbs'>" |
|---|
| 289 |
path = "" |
|---|
| 290 |
atoms = base.split(os.sep) |
|---|
| 291 |
atoms.pop() |
|---|
| 292 |
for atom in atoms: |
|---|
| 293 |
path += atom + os.sep |
|---|
| 294 |
yield ("<a href='menu?base=%s&exclude=%s'>%s</a> %s" |
|---|
| 295 |
% (path, quote_plus(exclude), atom, os.sep)) |
|---|
| 296 |
yield "</div>" |
|---|
| 297 |
|
|---|
| 298 |
yield "<div id='tree'>" |
|---|
| 299 |
|
|---|
| 300 |
|
|---|
| 301 |
tree = get_tree(base, exclude, self.coverage) |
|---|
| 302 |
if not tree: |
|---|
| 303 |
yield "<p>No modules covered.</p>" |
|---|
| 304 |
else: |
|---|
| 305 |
for chunk in _show_branch(tree, base, "/", pct, |
|---|
| 306 |
showpct=='checked', exclude, coverage=self.coverage): |
|---|
| 307 |
yield chunk |
|---|
| 308 |
|
|---|
| 309 |
yield "</div>" |
|---|
| 310 |
yield "</body></html>" |
|---|
| 311 |
menu.exposed = True |
|---|
| 312 |
|
|---|
| 313 |
def annotated_file(self, filename, statements, excluded, missing): |
|---|
| 314 |
source = open(filename, 'r') |
|---|
| 315 |
buffer = [] |
|---|
| 316 |
for lineno, line in enumerate(source.readlines()): |
|---|
| 317 |
lineno += 1 |
|---|
| 318 |
line = line.strip("\n\r") |
|---|
| 319 |
empty_the_buffer = True |
|---|
| 320 |
if lineno in excluded: |
|---|
| 321 |
template = TEMPLATE_LOC_EXCLUDED |
|---|
| 322 |
elif lineno in missing: |
|---|
| 323 |
template = TEMPLATE_LOC_NOT_COVERED |
|---|
| 324 |
elif lineno in statements: |
|---|
| 325 |
template = TEMPLATE_LOC_COVERED |
|---|
| 326 |
else: |
|---|
| 327 |
empty_the_buffer = False |
|---|
| 328 |
buffer.append((lineno, line)) |
|---|
| 329 |
if empty_the_buffer: |
|---|
| 330 |
for lno, pastline in buffer: |
|---|
| 331 |
yield template % (lno, cgi.escape(pastline)) |
|---|
| 332 |
buffer = [] |
|---|
| 333 |
yield template % (lineno, cgi.escape(line)) |
|---|
| 334 |
|
|---|
| 335 |
def report(self, name): |
|---|
| 336 |
filename, statements, excluded, missing, _ = self.coverage.analysis2(name) |
|---|
| 337 |
pc = _percent(statements, missing) |
|---|
| 338 |
yield TEMPLATE_COVERAGE % dict(name=os.path.basename(name), |
|---|
| 339 |
fullpath=name, |
|---|
| 340 |
pc=pc) |
|---|
| 341 |
yield '<table>\n' |
|---|
| 342 |
for line in self.annotated_file(filename, statements, excluded, |
|---|
| 343 |
missing): |
|---|
| 344 |
yield line |
|---|
| 345 |
yield '</table>' |
|---|
| 346 |
yield '</body>' |
|---|
| 347 |
yield '</html>' |
|---|
| 348 |
report.exposed = True |
|---|
| 349 |
|
|---|
| 350 |
|
|---|
| 351 |
def serve(path=localFile, port=8080, root=None): |
|---|
| 352 |
if coverage is None: |
|---|
| 353 |
raise ImportError("The coverage module could not be imported.") |
|---|
| 354 |
from coverage import coverage |
|---|
| 355 |
cov = coverage(data_file = path) |
|---|
| 356 |
cov.load() |
|---|
| 357 |
|
|---|
| 358 |
import cherrypy |
|---|
| 359 |
cherrypy.config.update({'server.socket_port': int(port), |
|---|
| 360 |
'server.thread_pool': 10, |
|---|
| 361 |
'environment': "production", |
|---|
| 362 |
}) |
|---|
| 363 |
cherrypy.quickstart(CoverStats(cov, root)) |
|---|
| 364 |
|
|---|
| 365 |
if __name__ == "__main__": |
|---|
| 366 |
serve(*tuple(sys.argv[1:])) |
|---|
| 367 |
|
|---|