Package cherrypy :: Package lib :: Module covercp
[hide private]
[frames] | no frames]

Source Code for Module cherrypy.lib.covercp

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