1 """Code-coverage tools for CherryPy.
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>`_
9 To turn on coverage tracing, use the following code::
11 cherrypy.engine.subscribe('start', covercp.start)
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.
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.
26 from cherrypy
._cpcompat
import quote_plus
28 localFile
= os
.path
.join(os
.path
.dirname(__file__
), "coverage.cache")
32 from coverage
import coverage
33 the_coverage
= coverage(data_file
=localFile
)
37 # Setting the_coverage to None will raise errors
38 # that need to be trapped downstream.
42 warnings
.warn("No code coverage will be performed; coverage.py could not be imported.")
48 TEMPLATE_MENU
= """<html>
50 <title>CherryPy Coverage Menu</title>
52 body {font: 9pt Arial, serif;}
55 font-family: Andale Mono, monospace;
58 #tree a:active, a:focus {
59 background-color: black;
62 border: 0px solid #9999FF;
63 -moz-outline-style: none;
67 #pct { text-align: right;}
74 input { border: 1px solid #ccc; padding: 2px; }
84 a { text-decoration: none; }
88 font-family: Andale Mono, monospace;
90 background-color: black;
97 border: 1px solid black;
98 background-color: #eee;
104 border: 1px solid #999;
107 background-color: black;
115 <h2>CherryPy Coverage</h2>"""
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='checked' /><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' />
127 <input type='submit' value='Change view' id="submit"/>
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='' />
140 TEMPLATE_COVERAGE
= """<html>
142 <title>Coverage for %(name)s</title>
144 h2 { margin-bottom: .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; }
153 .lineno { background-color: #eee;}
154 .notcovered .lineno { background-color: #000;}
155 table { border-collapse: collapse;
161 <p>Coverage: %(pc)s%%</p>"""
163 TEMPLATE_LOC_COVERED
= """<tr class="covered">
164 <td class="lineno">%s </td>
167 TEMPLATE_LOC_NOT_COVERED
= """<tr class="notcovered">
168 <td class="lineno">%s </td>
171 TEMPLATE_LOC_EXCLUDED
= """<tr class="excluded">
172 <td class="lineno">%s </td>
176 TEMPLATE_ITEM
= "%s%s<a class='file' href='report?name=%s' target='main'>%s</a>\n"
178 def _percent(statements
, missing
):
182 return int(round(100.0 * e
/ s
))
185 def _show_branch(root
, base
, path
, pct
=0, showpct
=False, exclude
="",
186 coverage
=the_coverage
):
188 # Show the directory name and any of our children
189 dirs
= [k
for k
, v
in root
.items() if v
]
192 newpath
= os
.path
.join(path
, name
)
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
)
200 for chunk
in _show_branch(root
[name
], base
, newpath
, pct
, showpct
, exclude
, coverage
=coverage
):
204 if path
.lower().startswith(base
):
205 relpath
= path
[len(base
):]
206 files
= [k
for k
, v
in root
.items() if not v
]
209 newpath
= os
.path
.join(path
, name
)
214 _
, statements
, _
, missing
, _
= coverage
.analysis2(newpath
)
216 # Yes, we really want to pass on all errors.
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
224 pc_str
= "<span class='pass'>%s</span>" % pc_str
226 yield TEMPLATE_ITEM
% ("| " * (relpath
.count(os
.sep
) + 1),
227 pc_str
, newpath
, name
)
229 def _skip_file(path
, exclude
):
231 return bool(re
.search(exclude
, path
))
233 def _graft(path
, tree
):
239 p
, tail
= os
.path
.split(p
)
250 d
= d
.setdefault(node
, {})
252 def get_tree(base
, exclude
, coverage
=the_coverage
):
253 """Return covered module names as a nested dict."""
255 runs
= coverage
.data
.executed_files()
257 if not _skip_file(path
, exclude
) and not os
.path
.isdir(path
):
261 class CoverStats(object):
263 def __init__(self
, coverage
, root
=None):
264 self
.coverage
= coverage
266 # Guess initial depth. Files outside this path will not be
267 # reachable from the web interface.
269 root
= os
.path
.dirname(cherrypy
.__file
__)
273 return TEMPLATE_FRAMESET
% self
.root
.lower()
276 def menu(self
, base
="/", pct
="50", showpct
="",
277 exclude
=r
'python\d\.\d|test|tut\d|tutorial'):
279 # The coverage module uses all-lower-case names.
280 base
= base
.lower().rstrip(os
.sep
)
283 yield TEMPLATE_FORM
% locals()
285 # Start by showing links for parent paths
286 yield "<div id='crumbs'>"
288 atoms
= base
.split(os
.sep
)
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
))
296 yield "<div id='tree'>"
298 # Then display the tree
299 tree
= get_tree(base
, exclude
, self
.coverage
)
301 yield "<p>No modules covered.</p>"
303 for chunk
in _show_branch(tree
, base
, "/", pct
,
304 showpct
=='checked', exclude
, coverage
=self
.coverage
):
308 yield "</body></html>"
311 def annotated_file(self
, filename
, statements
, excluded
, missing
):
312 source
= open(filename
, 'r')
314 for lineno
, line
in enumerate(source
.readlines()):
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
325 empty_the_buffer
= False
326 buffer.append((lineno
, line
))
328 for lno
, pastline
in buffer:
329 yield template
% (lno
, cgi
.escape(pastline
))
331 yield template
% (lineno
, cgi
.escape(line
))
333 def report(self
, name
):
334 filename
, statements
, excluded
, missing
, _
= self
.coverage
.analysis2(name
)
335 pc
= _percent(statements
, missing
)
336 yield TEMPLATE_COVERAGE
% dict(name
=os
.path
.basename(name
),
340 for line
in self
.annotated_file(filename
, statements
, excluded
,
346 report
.exposed
= True
349 def serve(path
=localFile
, port
=8080, root
=None):
351 raise ImportError("The coverage module could not be imported.")
352 from coverage
import coverage
353 cov
= coverage(data_file
= path
)
357 cherrypy
.config
.update({'server.socket_port': int(port
),
358 'server.thread_pool': 10,
359 'environment': "production",
361 cherrypy
.quickstart(CoverStats(cov
, root
))
363 if __name__
== "__main__":
364 serve(*tuple(sys
.argv
[1:]))