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

root/trunk/cherrypy/lib/covercp.py

Revision 1814 (checked in by zakj, 10 months ago)

Coerced all path names in covercp.py to lower-case, since some versions of
coverage do so.

  • 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     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):
36         coverage.start()
37 except ImportError:
38     # Setting coverage to None will raise errors
39     # that need to be trapped downstream.
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):
46         pass
47 start.priority = 20
48
49 # Guess initial depth to hide FIXME this doesn't work for non-cherrypy stuff
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&nbsp;</td>
170     <td>%s</td>
171 </tr>\n"""
172 TEMPLATE_LOC_NOT_COVERED = """<tr class="notcovered">
173     <td class="lineno">%s&nbsp;</td>
174     <td>%s</td>
175 </tr>\n"""
176 TEMPLATE_LOC_EXCLUDED = """<tr class="excluded">
177     <td class="lineno">%s&nbsp;</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
183 def _percent(statements, missing):
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     # Show the directory name and any of our children
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     # Now list the files
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                     # Yes, we really want to pass on all errors.
221                     pass
222                 else:
223                     pc = _percent(statements, missing)
224                     pc_str = ("%3d%% " % pc).replace(' ','&nbsp;')
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
233 def _skip_file(path, exclude):
234     if exclude:
235         return bool(re.search(exclude, path))
236
237 def _graft(path, tree):
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
256 def get_tree(base, exclude):
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
267 class CoverStats(object):
268    
269     def index(self):
270         return TEMPLATE_FRAMESET
271     index.exposed = True
272    
273     def menu(self, base="/", pct="50", showpct="",
274              exclude=r'python\d\.\d|test|tut\d|tutorial'):
275        
276         # The coverage module uses all-lower-case names.
277         base = base.lower().rstrip(os.sep)
278        
279         yield TEMPLATE_MENU
280         yield TEMPLATE_FORM % locals()
281        
282         # Start by showing links for parent paths
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         # Then display the tree
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    
308     def annotated_file(self, filename, statements, excluded, missing):
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    
330     def report(self, name):
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
347 def serve(path=localFile, port=8080):
348     if coverage is None:
349         raise ImportError("The coverage module could not be imported.")
350     coverage.cache_default = path
351    
352     import cherrypy
353     cherrypy.config.update({'server.socket_port': port,
354                             'server.thread_pool': 10,
355                             'environment': "production",
356                             })
357     cherrypy.quickstart(CoverStats())
358
359 if __name__ == "__main__":
360     serve(*tuple(sys.argv[1:]))
361
Note: See TracBrowser for help on using the browser.

Hosted by WebFaction

Log in as guest/cpguest to create tickets