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.subscribe('start', covercp.start)
14 cherrypy.engine.subscribe('start_thread', 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):
37 except ImportError:
38
39
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):
47 start.priority = 20
48
49
50 import cherrypy
51 initial_base = os.path.dirname(cherrypy.__file__)
52
53 TEMPLATE_MENU = """<html>
54 <head>
55 <title>CherryPy Coverage Menu</title>
56 <style>
57 body {font: 9pt Arial, serif;}
58 #tree {
59 font-size: 8pt;
60 font-family: Andale Mono, monospace;
61 white-space: pre;
62 }
63 #tree a:active, a:focus {
64 background-color: black;
65 padding: 1px;
66 color: white;
67 border: 0px solid #9999FF;
68 -moz-outline-style: none;
69 }
70 .fail { color: red;}
71 .pass { color: #888;}
72 #pct { text-align: right;}
73 h3 {
74 font-size: small;
75 font-weight: bold;
76 font-style: italic;
77 margin-top: 5px;
78 }
79 input { border: 1px solid #ccc; padding: 2px; }
80 .directory {
81 color: #933;
82 font-style: italic;
83 font-weight: bold;
84 font-size: 10pt;
85 }
86 .file {
87 color: #400;
88 }
89 a { text-decoration: none; }
90 #crumbs {
91 color: white;
92 font-size: 8pt;
93 font-family: Andale Mono, monospace;
94 width: 100%;
95 background-color: black;
96 }
97 #crumbs a {
98 color: #f88;
99 }
100 #options {
101 line-height: 2.3em;
102 border: 1px solid black;
103 background-color: #eee;
104 padding: 4px;
105 }
106 #exclude {
107 width: 100%;
108 margin-bottom: 3px;
109 border: 1px solid #999;
110 }
111 #submit {
112 background-color: black;
113 color: white;
114 border: 0;
115 margin-bottom: -9px;
116 }
117 </style>
118 </head>
119 <body>
120 <h2>CherryPy Coverage</h2>"""
121
122 TEMPLATE_FORM = """
123 <div id="options">
124 <form action='menu' method=GET>
125 <input type='hidden' name='base' value='%(base)s' />
126 Show percentages <input type='checkbox' %(showpct)s name='showpct' value='checked' /><br />
127 Hide files over <input type='text' id='pct' name='pct' value='%(pct)s' size='3' />%%<br />
128 Exclude files matching<br />
129 <input type='text' id='exclude' name='exclude' value='%(exclude)s' size='20' />
130 <br />
131
132 <input type='submit' value='Change view' id="submit"/>
133 </form>
134 </div>"""
135
136 TEMPLATE_FRAMESET = """<html>
137 <head><title>CherryPy coverage data</title></head>
138 <frameset cols='250, 1*'>
139 <frame src='menu?base=%s' />
140 <frame name='main' src='' />
141 </frameset>
142 </html>
143 """ % initial_base.lower()
144
145 TEMPLATE_COVERAGE = """<html>
146 <head>
147 <title>Coverage for %(name)s</title>
148 <style>
149 h2 { margin-bottom: .25em; }
150 p { margin: .25em; }
151 .covered { color: #000; background-color: #fff; }
152 .notcovered { color: #fee; background-color: #500; }
153 .excluded { color: #00f; background-color: #fff; }
154 table .covered, table .notcovered, table .excluded
155 { font-family: Andale Mono, monospace;
156 font-size: 10pt; white-space: pre; }
157
158 .lineno { background-color: #eee;}
159 .notcovered .lineno { background-color: #000;}
160 table { border-collapse: collapse;
161 </style>
162 </head>
163 <body>
164 <h2>%(name)s</h2>
165 <p>%(fullpath)s</p>
166 <p>Coverage: %(pc)s%%</p>"""
167
168 TEMPLATE_LOC_COVERED = """<tr class="covered">
169 <td class="lineno">%s </td>
170 <td>%s</td>
171 </tr>\n"""
172 TEMPLATE_LOC_NOT_COVERED = """<tr class="notcovered">
173 <td class="lineno">%s </td>
174 <td>%s</td>
175 </tr>\n"""
176 TEMPLATE_LOC_EXCLUDED = """<tr class="excluded">
177 <td class="lineno">%s </td>
178 <td>%s</td>
179 </tr>\n"""
180
181 TEMPLATE_ITEM = "%s%s<a class='file' href='report?name=%s' target='main'>%s</a>\n"
182
184 s = len(statements)
185 e = s - len(missing)
186 if s > 0:
187 return int(round(100.0 * e / s))
188 return 0
189
190 -def _show_branch(root, base, path, pct=0, showpct=False, exclude=""):
191
192
193 dirs = [k for k, v in root.iteritems() if v]
194 dirs.sort()
195 for name in dirs:
196 newpath = os.path.join(path, name)
197
198 if newpath.lower().startswith(base):
199 relpath = newpath[len(base):]
200 yield "| " * relpath.count(os.sep)
201 yield "<a class='directory' href='menu?base=%s&exclude=%s'>%s</a>\n" % \
202 (newpath, urllib.quote_plus(exclude), name)
203
204 for chunk in _show_branch(root[name], base, newpath, pct, showpct, exclude):
205 yield chunk
206
207
208 if path.lower().startswith(base):
209 relpath = path[len(base):]
210 files = [k for k, v in root.iteritems() if not v]
211 files.sort()
212 for name in files:
213 newpath = os.path.join(path, name)
214
215 pc_str = ""
216 if showpct:
217 try:
218 _, statements, _, missing, _ = coverage.analysis2(newpath)
219 except:
220
221 pass
222 else:
223 pc = _percent(statements, missing)
224 pc_str = ("%3d%% " % pc).replace(' ',' ')
225 if pc < float(pct) or pc == -1:
226 pc_str = "<span class='fail'>%s</span>" % pc_str
227 else:
228 pc_str = "<span class='pass'>%s</span>" % pc_str
229
230 yield TEMPLATE_ITEM % ("| " * (relpath.count(os.sep) + 1),
231 pc_str, newpath, name)
232
234 if exclude:
235 return bool(re.search(exclude, path))
236
238 d = tree
239
240 p = path
241 atoms = []
242 while True:
243 p, tail = os.path.split(p)
244 if not tail:
245 break
246 atoms.append(tail)
247 atoms.append(p)
248 if p != "/":
249 atoms.append("/")
250
251 atoms.reverse()
252 for node in atoms:
253 if node:
254 d = d.setdefault(node, {})
255
257 """Return covered module names as a nested dict."""
258 tree = {}
259 coverage.get_ready()
260 runs = coverage.cexecuted.keys()
261 if runs:
262 for path in runs:
263 if not _skip_file(path, exclude) and not os.path.isdir(path):
264 _graft(path, tree)
265 return tree
266
268
271 index.exposed = True
272
275
276
277 base = base.lower().rstrip(os.sep)
278
279 yield TEMPLATE_MENU
280 yield TEMPLATE_FORM % locals()
281
282
283 yield "<div id='crumbs'>"
284 path = ""
285 atoms = base.split(os.sep)
286 atoms.pop()
287 for atom in atoms:
288 path += atom + os.sep
289 yield ("<a href='menu?base=%s&exclude=%s'>%s</a> %s"
290 % (path, urllib.quote_plus(exclude), atom, os.sep))
291 yield "</div>"
292
293 yield "<div id='tree'>"
294
295
296 tree = get_tree(base, exclude)
297 if not tree:
298 yield "<p>No modules covered.</p>"
299 else:
300 for chunk in _show_branch(tree, base, "/", pct,
301 showpct=='checked', exclude):
302 yield chunk
303
304 yield "</div>"
305 yield "</body></html>"
306 menu.exposed = True
307
309 source = open(filename, 'r')
310 buffer = []
311 for lineno, line in enumerate(source.readlines()):
312 lineno += 1
313 line = line.strip("\n\r")
314 empty_the_buffer = True
315 if lineno in excluded:
316 template = TEMPLATE_LOC_EXCLUDED
317 elif lineno in missing:
318 template = TEMPLATE_LOC_NOT_COVERED
319 elif lineno in statements:
320 template = TEMPLATE_LOC_COVERED
321 else:
322 empty_the_buffer = False
323 buffer.append((lineno, line))
324 if empty_the_buffer:
325 for lno, pastline in buffer:
326 yield template % (lno, cgi.escape(pastline))
327 buffer = []
328 yield template % (lineno, cgi.escape(line))
329
331 coverage.get_ready()
332 filename, statements, excluded, missing, _ = coverage.analysis2(name)
333 pc = _percent(statements, missing)
334 yield TEMPLATE_COVERAGE % dict(name=os.path.basename(name),
335 fullpath=name,
336 pc=pc)
337 yield '<table>\n'
338 for line in self.annotated_file(filename, statements, excluded,
339 missing):
340 yield line
341 yield '</table>'
342 yield '</body>'
343 yield '</html>'
344 report.exposed = True
345
346
358
359 if __name__ == "__main__":
360 serve(*tuple(sys.argv[1:]))
361