OLD | NEW |
---|---|
(Empty) | |
1 #!/usr/bin/python2.4 | |
John Grabowski
2009/05/29 00:34:43
/usr/bin/env python?
Non-Google machines won't nec
| |
2 # | |
3 # Copyright 2009, Google Inc. | |
John Grabowski
2009/05/29 00:34:43
(c) the Chromium authors?
| |
4 # All rights reserved. | |
5 # | |
6 # Redistribution and use in source and binary forms, with or without | |
7 # modification, are permitted provided that the following conditions are | |
8 # met: | |
9 # | |
10 # * Redistributions of source code must retain the above copyright | |
11 # notice, this list of conditions and the following disclaimer. | |
12 # * Redistributions in binary form must reproduce the above | |
13 # copyright notice, this list of conditions and the following disclaimer | |
14 # in the documentation and/or other materials provided with the | |
15 # distribution. | |
16 # * Neither the name of Google Inc. nor the names of its | |
17 # contributors may be used to endorse or promote products derived from | |
18 # this software without specific prior written permission. | |
19 # | |
20 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
21 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
22 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
23 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
24 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
25 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
26 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
27 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
28 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
29 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
30 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
31 | |
32 """Crocodile HTML output.""" | |
33 | |
34 import os | |
35 import shutil | |
36 import time | |
37 import xml.dom | |
38 | |
39 | |
40 class CrocHtmlError(Exception): | |
41 """Coverage HTML error.""" | |
42 | |
43 | |
44 class HtmlElement(object): | |
45 """Node in a HTML file.""" | |
46 | |
47 def __init__(self, doc, element): | |
48 """Constructor. | |
49 | |
50 Args: | |
51 doc: XML document object. | |
52 element: XML element. | |
53 """ | |
54 self.doc = doc | |
55 self.element = element | |
56 | |
57 def E(self, name, **kwargs): | |
58 """Adds a child element. | |
59 | |
60 Args: | |
61 name: Name of element. | |
62 kwargs: Attributes for element. To use an attribute which is a python | |
63 reserved word (i.e. 'class'), prefix the attribute name with 'e_'. | |
64 | |
65 Returns: | |
66 The child element. | |
67 """ | |
68 he = HtmlElement(self.doc, self.doc.createElement(name)) | |
69 element = he.element | |
70 self.element.appendChild(element) | |
71 | |
72 for k, v in kwargs.iteritems(): | |
73 if k.startswith('e_'): | |
74 # Remove prefix | |
75 element.setAttribute(k[2:], str(v)) | |
76 else: | |
77 element.setAttribute(k, str(v)) | |
78 | |
79 return he | |
80 | |
81 def Text(self, text): | |
82 """Adds a text node. | |
83 | |
84 Args: | |
85 text: Text to add. | |
86 | |
87 Returns: | |
88 self. | |
89 """ | |
90 t = self.doc.createTextNode(str(text)) | |
91 self.element.appendChild(t) | |
92 return self | |
93 | |
94 | |
95 class HtmlFile(object): | |
96 """HTML file.""" | |
97 | |
98 def __init__(self, xml_impl, filename): | |
99 """Constructor. | |
100 | |
101 Args: | |
102 xml_impl: DOMImplementation to use to create document. | |
103 filename: Path to file. | |
104 """ | |
105 self.xml_impl = xml_impl | |
106 doctype = xml_impl.createDocumentType( | |
107 'HTML', '-//W3C//DTD HTML 4.01//EN', | |
108 'http://www.w3.org/TR/html4/strict.dtd') | |
109 self.doc = xml_impl.createDocument(None, 'html', doctype) | |
110 self.filename = filename | |
111 | |
112 # Create head and body elements | |
113 root = HtmlElement(self.doc, self.doc.documentElement) | |
114 self.head = root.E('head') | |
115 self.body = root.E('body') | |
116 | |
117 def Write(self, cleanup=True): | |
118 """Writes the file. | |
119 | |
120 Args: | |
121 cleanup: If True, calls unlink() on the internal xml document. This | |
122 frees up memory, but means that you can't use this file for anything | |
123 else. | |
124 """ | |
125 f = open(self.filename, 'wt') | |
126 self.doc.writexml(f, encoding='UTF-8') | |
127 f.close() | |
128 | |
129 if cleanup: | |
130 self.doc.unlink() | |
131 # Prevent future uses of the doc now that we've unlinked it | |
132 self.doc = None | |
133 | |
134 #------------------------------------------------------------------------------ | |
135 | |
136 COV_TYPE_STRING = {None: 'm', 0: 'i', 1: 'E', 2: ' '} | |
137 COV_TYPE_CLASS = {None: 'missing', 0: 'instr', 1: 'covered', 2: ''} | |
138 | |
139 | |
140 class CrocHtml(object): | |
141 """Crocodile HTML output class.""" | |
142 | |
143 def __init__(self, cov, output_root): | |
144 """Constructor.""" | |
145 self.cov = cov | |
146 self.output_root = output_root | |
147 self.xml_impl = xml.dom.getDOMImplementation() | |
148 self.time_string = 'Coverage information generated %s.' % time.asctime() | |
149 | |
150 def CreateHtmlDoc(self, filename, title): | |
151 """Creates a new HTML document. | |
152 | |
153 Args: | |
154 filename: Filename to write to, relative to self.output_root. | |
155 title: Title of page | |
156 | |
157 Returns: | |
158 The document. | |
159 """ | |
160 f = HtmlFile(self.xml_impl, self.output_root + '/' + filename) | |
161 | |
162 f.head.E('title').Text(title) | |
163 f.head.E( | |
164 'link', rel='stylesheet', type='text/css', | |
165 href='../' * (len(filename.split('/')) - 1) + 'croc.css') | |
166 | |
167 return f | |
168 | |
169 def AddCaptionForFile(self, body, path): | |
170 """Adds a caption for the file, with links to each parent dir. | |
171 | |
172 Args: | |
173 body: Body elemement. | |
174 path: Path to file. | |
175 """ | |
176 # This is slightly different that for subdir, because it needs to have a | |
177 # link to the current directory's index.html. | |
178 hdr = body.E('h2') | |
179 hdr.Text('Coverage for ') | |
180 dirs = [''] + path.split('/') | |
181 num_dirs = len(dirs) | |
182 for i in range(num_dirs - 1): | |
183 hdr.E('a', href=( | |
184 '../' * (num_dirs - i - 2) + 'index.html')).Text(dirs[i] + '/') | |
185 hdr.Text(dirs[-1]) | |
186 | |
187 def AddCaptionForSubdir(self, body, path): | |
188 """Adds a caption for the subdir, with links to each parent dir. | |
189 | |
190 Args: | |
191 body: Body elemement. | |
192 path: Path to subdir. | |
193 """ | |
194 # Link to parent dirs | |
195 hdr = body.E('h2') | |
196 hdr.Text('Coverage for ') | |
197 dirs = [''] + path.split('/') | |
198 num_dirs = len(dirs) | |
199 for i in range(num_dirs - 1): | |
200 hdr.E('a', href=( | |
201 '../' * (num_dirs - i - 1) + 'index.html')).Text(dirs[i] + '/') | |
202 hdr.Text(dirs[-1] + '/') | |
203 | |
204 def AddSectionHeader(self, table, caption, itemtype, is_file=False): | |
205 """Adds a section header to the coverage table. | |
206 | |
207 Args: | |
208 table: Table to add rows to. | |
209 caption: Caption for section, if not None. | |
210 itemtype: Type of items in this section, if not None. | |
211 is_file: Are items in this section files? | |
212 """ | |
213 | |
214 if caption is not None: | |
215 table.E('tr').E('td', e_class='secdesc', colspan=8).Text(caption) | |
216 | |
217 sec_hdr = table.E('tr') | |
218 | |
219 if itemtype is not None: | |
220 sec_hdr.E('td', e_class='section').Text(itemtype) | |
221 | |
222 sec_hdr.E('td', e_class='section').Text('Coverage') | |
223 sec_hdr.E('td', e_class='section', colspan=3).Text( | |
224 'Lines executed / instrumented / missing') | |
225 | |
226 graph = sec_hdr.E('td', e_class='section') | |
227 graph.E('span', style='color:#00FF00').Text('exe') | |
228 graph.Text(' / ') | |
229 graph.E('span', style='color:#FFFF00').Text('inst') | |
230 graph.Text(' / ') | |
231 graph.E('span', style='color:#FF0000').Text('miss') | |
232 | |
233 if is_file: | |
234 sec_hdr.E('td', e_class='section').Text('Language') | |
235 sec_hdr.E('td', e_class='section').Text('Group') | |
236 else: | |
237 sec_hdr.E('td', e_class='section', colspan=2) | |
238 | |
239 def AddItem(self, table, itemname, stats, attrs, link=None): | |
240 """Adds a bar graph to the element. This is a series of <td> elements. | |
241 | |
242 Args: | |
243 table: Table to add item to. | |
244 itemname: Name of item. | |
245 stats: Stats object. | |
246 attrs: Attributes dictionary; if None, no attributes will be printed. | |
247 link: Destination for itemname hyperlink, if not None. | |
248 """ | |
249 row = table.E('tr') | |
250 | |
251 # Add item name | |
252 if itemname is not None: | |
253 item_elem = row.E('td') | |
254 if link is not None: | |
255 item_elem = item_elem.E('a', href=link) | |
256 item_elem.Text(itemname) | |
257 | |
258 # Get stats | |
259 stat_exe = stats.get('lines_executable', 0) | |
260 stat_ins = stats.get('lines_instrumented', 0) | |
261 stat_cov = stats.get('lines_covered', 0) | |
262 | |
263 percent = row.E('td') | |
264 | |
265 # Add text | |
266 row.E('td', e_class='number').Text(stat_cov) | |
267 row.E('td', e_class='number').Text(stat_ins) | |
268 row.E('td', e_class='number').Text(stat_exe - stat_ins) | |
269 | |
270 # Add percent and graph; only fill in if there's something in there | |
271 graph = row.E('td', e_class='graph', width=100) | |
272 if stat_exe: | |
273 percent_cov = 100.0 * stat_cov / stat_exe | |
274 percent_ins = 100.0 * stat_ins / stat_exe | |
275 | |
276 # Color percent based on thresholds | |
277 percent.Text('%.1f%%' % percent_cov) | |
278 if percent_cov >= 80: | |
279 percent.element.setAttribute('class', 'high_pct') | |
280 elif percent_cov >= 60: | |
281 percent.element.setAttribute('class', 'mid_pct') | |
282 else: | |
283 percent.element.setAttribute('class', 'low_pct') | |
284 | |
285 # Graphs use integer values | |
286 percent_cov = int(percent_cov) | |
287 percent_ins = int(percent_ins) | |
288 | |
289 graph.Text('.') | |
290 graph.E('span', style='padding-left:%dpx' % percent_cov, | |
291 e_class='g_covered') | |
292 graph.E('span', style='padding-left:%dpx' % (percent_ins - percent_cov), | |
293 e_class='g_instr') | |
294 graph.E('span', style='padding-left:%dpx' % (100 - percent_ins), | |
295 e_class='g_missing') | |
296 | |
297 if attrs: | |
298 row.E('td', e_class='stat').Text(attrs.get('language')) | |
299 row.E('td', e_class='stat').Text(attrs.get('group')) | |
300 else: | |
301 row.E('td', colspan=2) | |
302 | |
303 def WriteFile(self, cov_file): | |
304 """Writes the HTML for a file. | |
305 | |
306 Args: | |
307 cov_file: croc.CoveredFile to write. | |
308 """ | |
309 print ' ' + cov_file.filename | |
310 title = 'Coverage for ' + cov_file.filename | |
311 | |
312 f = self.CreateHtmlDoc(cov_file.filename + '.html', title) | |
313 body = f.body | |
314 | |
315 # Write header section | |
316 self.AddCaptionForFile(body, cov_file.filename) | |
317 | |
318 # Summary for this file | |
319 table = body.E('table') | |
320 self.AddSectionHeader(table, None, None, is_file=True) | |
321 self.AddItem(table, None, cov_file.stats, cov_file.attrs) | |
322 | |
323 body.E('h2').Text('Line-by-line coverage:') | |
324 | |
325 # Print line-by-line coverage | |
326 if cov_file.local_path: | |
327 code_table = body.E('table').E('tr').E('td').E('pre') | |
328 | |
329 flines = open(cov_file.local_path, 'rt') | |
330 lineno = 0 | |
331 | |
332 for line in flines: | |
333 lineno += 1 | |
334 line_cov = cov_file.lines.get(lineno, 2) | |
335 e_class = COV_TYPE_CLASS.get(line_cov) | |
336 | |
337 code_table.E('span', e_class=e_class).Text('%4d %s : %s\n' % ( | |
338 lineno, | |
339 COV_TYPE_STRING.get(line_cov), | |
340 line.rstrip() | |
341 )) | |
342 | |
343 else: | |
344 body.Text('Line-by-line coverage not available. Make sure the directory' | |
345 ' containing this file has been scanned via ') | |
346 body.E('B').Text('add_files') | |
347 body.Text(' in a configuration file, or the ') | |
348 body.E('B').Text('--addfiles') | |
349 body.Text(' command line option.') | |
350 | |
351 # TODO: if file doesn't have a local path, try to find it by | |
352 # reverse-mapping roots and searching for the file. | |
353 | |
354 body.E('p', e_class='time').Text(self.time_string) | |
355 f.Write() | |
356 | |
357 def WriteSubdir(self, cov_dir): | |
358 """Writes the index.html for a subdirectory. | |
359 | |
360 Args: | |
361 cov_dir: croc.CoveredDir to write. | |
362 """ | |
363 print ' ' + cov_dir.dirpath + '/' | |
364 | |
365 # Create the subdir if it doesn't already exist | |
366 subdir = self.output_root + '/' + cov_dir.dirpath | |
367 if not os.path.exists(subdir): | |
368 os.mkdir(subdir) | |
369 | |
370 if cov_dir.dirpath: | |
371 title = 'Coverage for ' + cov_dir.dirpath + '/' | |
372 f = self.CreateHtmlDoc(cov_dir.dirpath + '/index.html', title) | |
373 else: | |
374 title = 'Coverage summary' | |
375 f = self.CreateHtmlDoc('index.html', title) | |
376 | |
377 body = f.body | |
378 | |
379 # Write header section | |
380 if cov_dir.dirpath: | |
381 self.AddCaptionForSubdir(body, cov_dir.dirpath) | |
382 else: | |
383 body.E('h2').Text(title) | |
384 | |
385 table = body.E('table') | |
386 | |
387 # Coverage by group | |
388 self.AddSectionHeader(table, 'Coverage by Group', 'Group') | |
389 | |
390 for group in sorted(cov_dir.stats_by_group): | |
391 self.AddItem(table, group, cov_dir.stats_by_group[group], None) | |
392 | |
393 # List subdirs | |
394 if cov_dir.subdirs: | |
395 self.AddSectionHeader(table, 'Subdirectories', 'Subdirectory') | |
396 | |
397 for d in sorted(cov_dir.subdirs): | |
398 self.AddItem(table, d + '/', cov_dir.subdirs[d].stats_by_group['all'], | |
399 None, link=d + '/index.html') | |
400 | |
401 # List files | |
402 if cov_dir.files: | |
403 self.AddSectionHeader(table, 'Files in This Directory', 'Filename', | |
404 is_file=True) | |
405 | |
406 for filename in sorted(cov_dir.files): | |
407 cov_file = cov_dir.files[filename] | |
408 self.AddItem(table, filename, cov_file.stats, cov_file.attrs, | |
409 link=filename + '.html') | |
410 | |
411 body.E('p', e_class='time').Text(self.time_string) | |
412 f.Write() | |
413 | |
414 def WriteRoot(self): | |
415 """Writes the files in the output root.""" | |
416 # Find ourselves | |
417 src_dir = os.path.split(self.WriteRoot.func_code.co_filename)[0] | |
418 | |
419 # Files to copy into output root | |
420 copy_files = [ | |
421 'croc.css', | |
422 ] | |
423 | |
424 # Copy files from our directory into the output directory | |
425 for copy_file in copy_files: | |
426 print ' Copying %s' % copy_file | |
427 shutil.copyfile(os.path.join(src_dir, copy_file), | |
428 os.path.join(self.output_root, copy_file)) | |
429 | |
430 def Write(self): | |
431 """Writes HTML output.""" | |
432 | |
433 print 'Writing HTML to %s...' % self.output_root | |
434 | |
435 # Loop through the tree and write subdirs, breadth-first | |
436 # TODO: switch to depth-first and sort values - makes nicer output? | |
437 todo = [self.cov.tree] | |
438 while todo: | |
439 cov_dir = todo.pop(0) | |
440 | |
441 # Append subdirs to todo list | |
442 todo += cov_dir.subdirs.values() | |
443 | |
444 # Write this subdir | |
445 self.WriteSubdir(cov_dir) | |
446 | |
447 # Write files in this subdir | |
448 for cov_file in cov_dir.files.itervalues(): | |
449 self.WriteFile(cov_file) | |
450 | |
451 # Write files in root directory | |
452 self.WriteRoot() | |
453 | |
OLD | NEW |