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