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
38 except ImportError:
39
40
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
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 </td>
172 <td>%s</td>
173 </tr>\n"""
174 TEMPLATE_LOC_NOT_COVERED = """<tr class="notcovered">
175 <td class="lineno">%s </td>
176 <td>%s</td>
177 </tr>\n"""
178 TEMPLATE_LOC_EXCLUDED = """<tr class="excluded">
179 <td class="lineno">%s </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
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
198
199
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
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
234 pass
235 else:
236 pc = _percent(statements, missing)
237 pc_str = ("%3d%% " % pc).replace(' ', ' ')
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
248 if exclude:
249 return bool(re.search(exclude, path))
250
251
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
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
283
284 - def __init__(self, coverage, root=None):
292
295 index.exposed = True
296
299
300
301 base = base.lower().rstrip(os.sep)
302
303 yield TEMPLATE_MENU
304 yield TEMPLATE_FORM % locals()
305
306
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
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
354
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
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