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: 
  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.on_start_engine_list.insert(0, covercp.start) 
 14      cherrypy.engine.on_start_thread_list.insert(0, covercp.start) 
 15   
 16  Run your code, then use the covercp.serve() function to browse the 
 17  results in a web browser. If you run this module from the command line, 
 18  it will call serve() for you. 
 19  """ 
 20   
 21  import re 
 22  import sys 
 23  import cgi 
 24  import urllib 
 25  import os, os.path 
 26  localFile = os.path.join(os.path.dirname(__file__), "coverage.cache") 
 27   
 28  try: 
 29      import cStringIO as StringIO 
 30  except ImportError: 
 31      import StringIO 
 32   
 33  try: 
 34      from coverage import the_coverage as coverage 
35 - def start(threadid=None):
36 coverage.start()
37 except ImportError: 38 # Setting coverage to None will raise errors 39 # that need to be trapped downstream. 40 coverage = None 41 42 import warnings 43 warnings.warn("No code coverage will be performed; coverage.py could not be imported.") 44
45 - def start(threadid=None):
46 pass
47 48 # Guess initial depth to hide FIXME this doesn't work for non-cherrypy stuff 49 import cherrypy 50 initial_base = os.path.dirname(cherrypy.__file__) 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 <input type='checkbox' %(showpct)s name='showpct' value='checked' /><br /> 126 Hide files over <input type='text' id='pct' name='pct' value='%(pct)s' size='3' />%%<br /> 127 Exclude files matching<br /> 128 <input type='text' id='exclude' name='exclude' value='%(exclude)s' size='20' /> 129 <br /> 130 131 <input type='submit' value='Change view' id="submit"/> 132 </form> 133 </div>""" 134 135 TEMPLATE_FRAMESET = """<html> 136 <head><title>CherryPy coverage data</title></head> 137 <frameset cols='250, 1*'> 138 <frame src='menu?base=%s' /> 139 <frame name='main' src='' /> 140 </frameset> 141 </html> 142 """ % initial_base.lower() 143 144 TEMPLATE_COVERAGE = """<html> 145 <head> 146 <title>Coverage for %(name)s</title> 147 <style> 148 h2 { margin-bottom: .25em; } 149 p { margin: .25em; } 150 .covered { color: #000; background-color: #fff; } 151 .notcovered { color: #fee; background-color: #500; } 152 .excluded { color: #00f; background-color: #fff; } 153 table .covered, table .notcovered, table .excluded 154 { font-family: Andale Mono, monospace; 155 font-size: 10pt; white-space: pre; } 156 157 .lineno { background-color: #eee;} 158 .notcovered .lineno { background-color: #000;} 159 table { border-collapse: collapse; 160 </style> 161 </head> 162 <body> 163 <h2>%(name)s</h2> 164 <p>%(fullpath)s</p> 165 <p>Coverage: %(pc)s%%</p>""" 166 167 TEMPLATE_LOC_COVERED = """<tr class="covered"> 168 <td class="lineno">%s&nbsp;</td> 169 <td>%s</td> 170 </tr>\n""" 171 TEMPLATE_LOC_NOT_COVERED = """<tr class="notcovered"> 172 <td class="lineno">%s&nbsp;</td> 173 <td>%s</td> 174 </tr>\n""" 175 TEMPLATE_LOC_EXCLUDED = """<tr class="excluded"> 176 <td class="lineno">%s&nbsp;</td> 177 <td>%s</td> 178 </tr>\n""" 179 180 TEMPLATE_ITEM = "%s%s<a class='file' href='report?name=%s' target='main'>%s</a>\n" 181
182 -def _percent(statements, missing):
183 s = len(statements) 184 e = s - len(missing) 185 if s > 0: 186 return int(round(100.0 * e / s)) 187 return 0
188
189 -def _show_branch(root, base, path, pct=0, showpct=False, exclude=""):
190 191 # Show the directory name and any of our children 192 dirs = [k for k, v in root.iteritems() if v] 193 dirs.sort() 194 for name in dirs: 195 newpath = os.path.join(path, name) 196 197 if newpath.startswith(base): 198 relpath = newpath[len(base):] 199 yield "| " * relpath.count(os.sep) 200 yield "<a class='directory' href='menu?base=%s&exclude=%s'>%s</a>\n" % \ 201 (newpath, urllib.quote_plus(exclude), name) 202 203 for chunk in _show_branch(root[name], base, newpath, pct, showpct, exclude): 204 yield chunk 205 206 # Now list the files 207 if path.startswith(base): 208 relpath = path[len(base):] 209 files = [k for k, v in root.iteritems() if not v] 210 files.sort() 211 for name in files: 212 newpath = os.path.join(path, name) 213 214 pc_str = "" 215 if showpct: 216 try: 217 _, statements, _, missing, _ = coverage.analysis2(newpath) 218 except: 219 # Yes, we really want to pass on all errors. 220 pass 221 else: 222 pc = _percent(statements, missing) 223 pc_str = ("%3d%% " % pc).replace(' ','&nbsp;') 224 if pc < float(pct) or pc == -1: 225 pc_str = "<span class='fail'>%s</span>" % pc_str 226 else: 227 pc_str = "<span class='pass'>%s</span>" % pc_str 228 229 yield TEMPLATE_ITEM % ("| " * (relpath.count(os.sep) + 1), 230 pc_str, newpath, name)
231
232 -def _skip_file(path, exclude):
233 if exclude: 234 return bool(re.search(exclude, path))
235
236 -def _graft(path, tree):
237 d = tree 238 239 p = path 240 atoms = [] 241 while True: 242 p, tail = os.path.split(p) 243 if not tail: 244 break 245 atoms.append(tail) 246 atoms.append(p) 247 if p != "/": 248 atoms.append("/") 249 250 atoms.reverse() 251 for node in atoms: 252 if node: 253 d = d.setdefault(node, {})
254
255 -def get_tree(base, exclude):
256 """Return covered module names as a nested dict.""" 257 tree = {} 258 coverage.get_ready() 259 runs = coverage.cexecuted.keys() 260 if runs: 261 for path in runs: 262 if not _skip_file(path, exclude) and not os.path.isdir(path): 263 _graft(path, tree) 264 return tree
265
266 -class CoverStats(object):
267
268 - def index(self):
269 return TEMPLATE_FRAMESET
270 index.exposed = True 271
272 - def menu(self, base="/", pct="50", showpct="", 273 exclude=r'python\d\.\d|test|tut\d|tutorial'):
274 275 # The coverage module uses all-lower-case names. 276 base = base.lower().rstrip(os.sep) 277 278 yield TEMPLATE_MENU 279 yield TEMPLATE_FORM % locals() 280 281 # Start by showing links for parent paths 282 yield "<div id='crumbs'>" 283 path = "" 284 atoms = base.split(os.sep) 285 atoms.pop() 286 for atom in atoms: 287 path += atom + os.sep 288 yield ("<a href='menu?base=%s&exclude=%s'>%s</a> %s" 289 % (path, urllib.quote_plus(exclude), atom, os.sep)) 290 yield "</div>" 291 292 yield "<div id='tree'>" 293 294 # Then display the tree 295 tree = get_tree(base, exclude) 296 if not tree: 297 yield "<p>No modules covered.</p>" 298 else: 299 for chunk in _show_branch(tree, base, "/", pct, 300 showpct=='checked', exclude): 301 yield chunk 302 303 yield "</div>" 304 yield "</body></html>"
305 menu.exposed = True 306
307 - def annotated_file(self, filename, statements, excluded, missing):
308 source = open(filename, 'r') 309 buffer = [] 310 for lineno, line in enumerate(source.readlines()): 311 lineno += 1 312 line = line.strip("\n\r") 313 empty_the_buffer = True 314 if lineno in excluded: 315 template = TEMPLATE_LOC_EXCLUDED 316 elif lineno in missing: 317 template = TEMPLATE_LOC_NOT_COVERED 318 elif lineno in statements: 319 template = TEMPLATE_LOC_COVERED 320 else: 321 empty_the_buffer = False 322 buffer.append((lineno, line)) 323 if empty_the_buffer: 324 for lno, pastline in buffer: 325 yield template % (lno, cgi.escape(pastline)) 326 buffer = [] 327 yield template % (lineno, cgi.escape(line))
328
329 - def report(self, name):
330 coverage.get_ready() 331 filename, statements, excluded, missing, _ = coverage.analysis2(name) 332 pc = _percent(statements, missing) 333 yield TEMPLATE_COVERAGE % dict(name=os.path.basename(name), 334 fullpath=name, 335 pc=pc) 336 yield '<table>\n' 337 for line in self.annotated_file(filename, statements, excluded, 338 missing): 339 yield line 340 yield '</table>' 341 yield '</body>' 342 yield '</html>'
343 report.exposed = True
344 345
346 -def serve(path=localFile, port=8080):
347 if coverage is None: 348 raise ImportError("The coverage module could not be imported.") 349 coverage.cache_default = path 350 351 import cherrypy 352 cherrypy.config.update({'server.socket_port': port, 353 'server.thread_pool': 10, 354 'environment': "production", 355 }) 356 cherrypy.quickstart(CoverStats())
357 358 if __name__ == "__main__": 359 serve(*tuple(sys.argv[1:])) 360