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)
36 except ImportError:
37
38
39 the_coverage = None
40
41 import warnings
42 warnings.warn("No code coverage will be performed; coverage.py could not be imported.")
43
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='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' />
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
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
187
188
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, exclude, coverage=coverage):
201 yield chunk
202
203
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
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
230 if exclude:
231 return bool(re.search(exclude, path))
232
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
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
262
263 - def __init__(self, coverage, root=None):
271
274 index.exposed = True
275
278
279
280 base = base.lower().rstrip(os.sep)
281
282 yield TEMPLATE_MENU
283 yield TEMPLATE_FORM % locals()
284
285
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
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
332
334 filename, statements, excluded, missing, _ = self.coverage.analysis2(name)
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
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