Download Install Tutorial Docs FAQ Tools WikiLicense Team IRC Planet Involvement Shop Book

root/trunk/cherrypy/lib/covercp.py

Revision 2568 (checked in by jtate, 7 months ago)

Update covercp and test suite to use coverage 3.2. Should be faster. Note no branch coverage yet.

  • Property svn:eol-style set to native
Line 
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
15 DO NOT subscribe anything on the 'start_thread' channel, as previously
16 recommended. Calling start once in the main thread should be sufficient
17 to start coverage on all threads. Calling start again in each thread
18 effectively clears any coverage data gathered up to that point.
19
20 Run your code, then use the covercp.serve() function to browse the
21 results in a web browser. If you run this module from the command line,
22 it will call serve() for you.
23 """
24
25 import re
26 import sys
27 import cgi
28 from urllib import quote_plus
29 import os, os.path
30 localFile = os.path.join(os.path.dirname(__file__), "coverage.cache")
31
32 the_coverage = None
33 try:
34     from coverage import coverage
35     the_coverage = coverage(data_file=localFile)
36     def start():
37         the_coverage.start()
38 except ImportError:
39     # Setting the_coverage to None will raise errors
40     # that need to be trapped downstream.
41     the_coverage = None
42    
43     import warnings
44     warnings.warn("No code coverage will be performed; coverage.py could not be imported.")
45    
46     def start():
47         pass
48 start.priority = 20
49
50 TEMPLATE_MENU = """<html>
51 <head>
52     <title>CherryPy Coverage Menu</title>
53     <style>
54         body {font: 9pt Arial, serif;}
55         #tree {
56             font-size: 8pt;
57             font-family: Andale Mono, monospace;
58             white-space: pre;
59             }
60         #tree a:active, a:focus {
61             background-color: black;
62             padding: 1px;
63             color: white;
64             border: 0px solid #9999FF;
65             -moz-outline-style: none;
66             }
67         .fail { color: red;}
68         .pass { color: #888;}
69         #pct { text-align: right;}
70         h3 {
71             font-size: small;
72             font-weight: bold;
73             font-style: italic;
74             margin-top: 5px;
75             }
76         input { border: 1px solid #ccc; padding: 2px; }
77         .directory {
78             color: #933;
79             font-style: italic;
80             font-weight: bold;
81             font-size: 10pt;
82             }
83         .file {
84             color: #400;
85             }
86         a { text-decoration: none; }
87         #crumbs {
88             color: white;
89             font-size: 8pt;
90             font-family: Andale Mono, monospace;
91             width: 100%;
92             background-color: black;
93             }
94         #crumbs a {
95             color: #f88;
96             }
97         #options {
98             line-height: 2.3em;
99             border: 1px solid black;
100             background-color: #eee;
101             padding: 4px;
102             }
103         #exclude {
104             width: 100%;
105             margin-bottom: 3px;
106             border: 1px solid #999;
107             }
108         #submit {
109             background-color: black;
110             color: white;
111             border: 0;
112             margin-bottom: -9px;
113             }
114     </style>
115 </head>
116 <body>
117 <h2>CherryPy Coverage</h2>"""
118
119 TEMPLATE_FORM = """
120 <div id="options">
121 <form action='menu' method=GET>
122     <input type='hidden' name='base' value='%(base)s' />
123     Show percentages <input type='checkbox' %(showpct)s name='showpct' value='checked' /><br />
124     Hide files over <input type='text' id='pct' name='pct' value='%(pct)s' size='3' />%%<br />
125     Exclude files matching<br />
126     <input type='text' id='exclude' name='exclude' value='%(exclude)s' size='20' />
127     <br />
128
129     <input type='submit' value='Change view' id="submit"/>
130 </form>
131 </div>""" 
132
133 TEMPLATE_FRAMESET = """<html>
134 <head><title>CherryPy coverage data</title></head>
135 <frameset cols='250, 1*'>
136     <frame src='menu?base=%s' />
137     <frame name='main' src='' />
138 </frameset>
139 </html>
140 """
141
142 TEMPLATE_COVERAGE = """<html>
143 <head>
144     <title>Coverage for %(name)s</title>
145     <style>
146         h2 { margin-bottom: .25em; }
147         p { margin: .25em; }
148         .covered { color: #000; background-color: #fff; }
149         .notcovered { color: #fee; background-color: #500; }
150         .excluded { color: #00f; background-color: #fff; }
151          table .covered, table .notcovered, table .excluded
152              { font-family: Andale Mono, monospace;
153                font-size: 10pt; white-space: pre; }
154
155          .lineno { background-color: #eee;}
156          .notcovered .lineno { background-color: #000;}
157          table { border-collapse: collapse;
158     </style>
159 </head>
160 <body>
161 <h2>%(name)s</h2>
162 <p>%(fullpath)s</p>
163 <p>Coverage: %(pc)s%%</p>"""
164
165 TEMPLATE_LOC_COVERED = """<tr class="covered">
166     <td class="lineno">%s&nbsp;</td>
167     <td>%s</td>
168 </tr>\n"""
169 TEMPLATE_LOC_NOT_COVERED = """<tr class="notcovered">
170     <td class="lineno">%s&nbsp;</td>
171     <td>%s</td>
172 </tr>\n"""
173 TEMPLATE_LOC_EXCLUDED = """<tr class="excluded">
174     <td class="lineno">%s&nbsp;</td>
175     <td>%s</td>
176 </tr>\n"""
177
178 TEMPLATE_ITEM = "%s%s<a class='file' href='report?name=%s' target='main'>%s</a>\n"
179
180 def _percent(statements, missing):
181     s = len(statements)
182     e = s - len(missing)
183     if s > 0:
184         return int(round(100.0 * e / s))
185     return 0
186
187 def _show_branch(root, base, path, pct=0, showpct=False, exclude="",
188                  coverage=the_coverage):
189    
190     # Show the directory name and any of our children
191     dirs = [k for k, v in root.items() if v]
192     dirs.sort()
193     for name in dirs:
194         newpath = os.path.join(path, name)
195        
196         if newpath.lower().startswith(base):
197             relpath = newpath[len(base):]
198             yield "| " * relpath.count(os.sep)
199             yield "<a class='directory' href='menu?base=%s&exclude=%s'>%s</a>\n" % \
200                    (newpath, quote_plus(exclude), name)
201        
202         for chunk in _show_branch(root[name], base, newpath, pct, showpct, exclude, coverage=coverage):
203             yield chunk
204    
205     # Now list the files
206     if path.lower().startswith(base):
207         relpath = path[len(base):]
208         files = [k for k, v in root.items() if not v]
209         files.sort()
210         for name in files:
211             newpath = os.path.join(path, name)
212            
213             pc_str = ""
214             if showpct:
215                 try:
216                     _, statements, _, missing, _ = coverage.analysis2(newpath)
217                 except:
218                     # Yes, we really want to pass on all errors.
219                     pass
220                 else:
221                     pc = _percent(statements, missing)
222                     pc_str = ("%3d%% " % pc).replace(' ','&nbsp;')
223                     if pc < float(pct) or pc == -1:
224                         pc_str = "<span class='fail'>%s</span>" % pc_str
225                     else:
226                         pc_str = "<span class='pass'>%s</span>" % pc_str
227            
228             yield TEMPLATE_ITEM % ("| " * (relpath.count(os.sep) + 1),
229                                    pc_str, newpath, name)
230
231 def _skip_file(path, exclude):
232     if exclude:
233         return bool(re.search(exclude, path))
234
235 def _graft(path, tree):
236     d = tree
237    
238     p = path
239     atoms = []
240     while True:
241         p, tail = os.path.split(p)
242         if not tail:
243             break
244         atoms.append(tail)
245     atoms.append(p)
246     if p != "/":
247         atoms.append("/")
248    
249     atoms.reverse()
250     for node in atoms:
251         if node:
252             d = d.setdefault(node, {})
253
254 def get_tree(base, exclude, coverage=the_coverage):
255     """Return covered module names as a nested dict."""
256     tree = {}
257     runs = coverage.data.executed_files()
258     for path in runs:
259         if not _skip_file(path, exclude) and not os.path.isdir(path):
260             _graft(path, tree)
261     return tree
262
263 class CoverStats(object):
264    
265     def __init__(self, coverage, root=None):
266         self.coverage = coverage
267         if root is None:
268             # Guess initial depth. Files outside this path will not be
269             # reachable from the web interface.
270             import cherrypy
271             root = os.path.dirname(cherrypy.__file__)
272         self.root = root
273    
274     def index(self):
275         return TEMPLATE_FRAMESET % self.root.lower()
276     index.exposed = True
277    
278     def menu(self, base="/", pct="50", showpct="",
279              exclude=r'python\d\.\d|test|tut\d|tutorial'):
280        
281         # The coverage module uses all-lower-case names.
282         base = base.lower().rstrip(os.sep)
283        
284         yield TEMPLATE_MENU
285         yield TEMPLATE_FORM % locals()
286        
287         # Start by showing links for parent paths
288         yield "<div id='crumbs'>"
289         path = ""
290         atoms = base.split(os.sep)
291         atoms.pop()
292         for atom in atoms:
293             path += atom + os.sep
294             yield ("<a href='menu?base=%s&exclude=%s'>%s</a> %s"
295                    % (path, quote_plus(exclude), atom, os.sep))
296         yield "</div>"
297        
298         yield "<div id='tree'>"
299        
300         # Then display the tree
301         tree = get_tree(base, exclude, self.coverage)
302         if not tree:
303             yield "<p>No modules covered.</p>"
304         else:
305             for chunk in _show_branch(tree, base, "/", pct,
306                                       showpct=='checked', exclude, coverage=self.coverage):
307                 yield chunk
308        
309         yield "</div>"
310         yield "</body></html>"
311     menu.exposed = True
312    
313     def annotated_file(self, filename, statements, excluded, missing):
314         source = open(filename, 'r')
315         buffer = []
316         for lineno, line in enumerate(source.readlines()):
317             lineno += 1
318             line = line.strip("\n\r")
319             empty_the_buffer = True
320             if lineno in excluded:
321                 template = TEMPLATE_LOC_EXCLUDED
322             elif lineno in missing:
323                 template = TEMPLATE_LOC_NOT_COVERED
324             elif lineno in statements:
325                 template = TEMPLATE_LOC_COVERED
326             else:
327                 empty_the_buffer = False
328                 buffer.append((lineno, line))
329             if empty_the_buffer:
330                 for lno, pastline in buffer:
331                     yield template % (lno, cgi.escape(pastline))
332                 buffer = []
333                 yield template % (lineno, cgi.escape(line))
334    
335     def report(self, name):
336         filename, statements, excluded, missing, _ = self.coverage.analysis2(name)
337         pc = _percent(statements, missing)
338         yield TEMPLATE_COVERAGE % dict(name=os.path.basename(name),
339                                        fullpath=name,
340                                        pc=pc)
341         yield '<table>\n'
342         for line in self.annotated_file(filename, statements, excluded,
343                                         missing):
344             yield line
345         yield '</table>'
346         yield '</body>'
347         yield '</html>'
348     report.exposed = True
349
350
351 def serve(path=localFile, port=8080, root=None):
352     if coverage is None:
353         raise ImportError("The coverage module could not be imported.")
354     from coverage import coverage
355     cov = coverage(data_file = path)
356     cov.load()
357    
358     import cherrypy
359     cherrypy.config.update({'server.socket_port': int(port),
360                             'server.thread_pool': 10,
361                             'environment': "production",
362                             })
363     cherrypy.quickstart(CoverStats(cov, root))
364
365 if __name__ == "__main__":
366     serve(*tuple(sys.argv[1:]))
367
Note: See TracBrowser for help on using the browser.

Hosted by WebFaction

Log in as guest/cpguest to create tickets