Fixed python_path problem.
[smonitor.git] / lib / cherrypy / lib / covercp.py
blob9b701b560d53031c3545e9bc192efa7163481f6d
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.
21 """
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")
30 the_coverage = None
31 try:
32 from coverage import coverage
33 the_coverage = coverage(data_file=localFile)
34 def start():
35 the_coverage.start()
36 except ImportError:
37 # Setting the_coverage to None will raise errors
38 # that need to be trapped downstream.
39 the_coverage = None
41 import warnings
42 warnings.warn("No code coverage will be performed; coverage.py could not be imported.")
44 def start():
45 pass
46 start.priority = 20
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;
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;
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;
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;
81 .file {
82 color: #400;
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;
92 #crumbs a {
93 color: #f88;
95 #options {
96 line-height: 2.3em;
97 border: 1px solid black;
98 background-color: #eee;
99 padding: 4px;
101 #exclude {
102 width: 100%;
103 margin-bottom: 3px;
104 border: 1px solid #999;
106 #submit {
107 background-color: black;
108 color: white;
109 border: 0;
110 margin-bottom: -9px;
112 </style>
113 </head>
114 <body>
115 <h2>CherryPy Coverage</h2>"""
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 />
127 <input type='submit' value='Change view' id="submit"/>
128 </form>
129 </div>"""
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>
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; }
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>"""
163 TEMPLATE_LOC_COVERED = """<tr class="covered">
164 <td class="lineno">%s&nbsp;</td>
165 <td>%s</td>
166 </tr>\n"""
167 TEMPLATE_LOC_NOT_COVERED = """<tr class="notcovered">
168 <td class="lineno">%s&nbsp;</td>
169 <td>%s</td>
170 </tr>\n"""
171 TEMPLATE_LOC_EXCLUDED = """<tr class="excluded">
172 <td class="lineno">%s&nbsp;</td>
173 <td>%s</td>
174 </tr>\n"""
176 TEMPLATE_ITEM = "%s%s<a class='file' href='report?name=%s' target='main'>%s</a>\n"
178 def _percent(statements, missing):
179 s = len(statements)
180 e = s - len(missing)
181 if s > 0:
182 return int(round(100.0 * e / s))
183 return 0
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]
190 dirs.sort()
191 for name in dirs:
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):
201 yield chunk
203 # Now list the files
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)
211 pc_str = ""
212 if showpct:
213 try:
214 _, statements, _, missing, _ = coverage.analysis2(newpath)
215 except:
216 # Yes, we really want to pass on all errors.
217 pass
218 else:
219 pc = _percent(statements, missing)
220 pc_str = ("%3d%% " % pc).replace(' ','&nbsp;')
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
226 yield TEMPLATE_ITEM % ("| " * (relpath.count(os.sep) + 1),
227 pc_str, newpath, name)
229 def _skip_file(path, exclude):
230 if exclude:
231 return bool(re.search(exclude, path))
233 def _graft(path, tree):
234 d = tree
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("/")
247 atoms.reverse()
248 for node in atoms:
249 if node:
250 d = d.setdefault(node, {})
252 def get_tree(base, exclude, coverage=the_coverage):
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
261 class CoverStats(object):
263 def __init__(self, coverage, root=None):
264 self.coverage = coverage
265 if root is None:
266 # Guess initial depth. Files outside this path will not be
267 # reachable from the web interface.
268 import cherrypy
269 root = os.path.dirname(cherrypy.__file__)
270 self.root = root
272 def index(self):
273 return TEMPLATE_FRAMESET % self.root.lower()
274 index.exposed = True
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)
282 yield TEMPLATE_MENU
283 yield TEMPLATE_FORM % locals()
285 # Start by showing links for parent paths
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>"
296 yield "<div id='tree'>"
298 # Then display the tree
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
307 yield "</div>"
308 yield "</body></html>"
309 menu.exposed = True
311 def annotated_file(self, filename, statements, excluded, missing):
312 source = open(filename, 'r')
313 buffer = []
314 for lineno, line in enumerate(source.readlines()):
315 lineno += 1
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
324 else:
325 empty_the_buffer = False
326 buffer.append((lineno, line))
327 if empty_the_buffer:
328 for lno, pastline in buffer:
329 yield template % (lno, cgi.escape(pastline))
330 buffer = []
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),
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
349 def serve(path=localFile, port=8080, root=None):
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()
356 import cherrypy
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:]))