Chromium Code Reviews| Index: tools/code_coverage/croc_html.py |
| =================================================================== |
| --- tools/code_coverage/croc_html.py (revision 0) |
| +++ tools/code_coverage/croc_html.py (revision 0) |
| @@ -0,0 +1,453 @@ |
| +#!/usr/bin/python2.4 |
|
John Grabowski
2009/05/29 00:34:43
/usr/bin/env python?
Non-Google machines won't nec
|
| +# |
| +# Copyright 2009, Google Inc. |
|
John Grabowski
2009/05/29 00:34:43
(c) the Chromium authors?
|
| +# All rights reserved. |
| +# |
| +# Redistribution and use in source and binary forms, with or without |
| +# modification, are permitted provided that the following conditions are |
| +# met: |
| +# |
| +# * Redistributions of source code must retain the above copyright |
| +# notice, this list of conditions and the following disclaimer. |
| +# * Redistributions in binary form must reproduce the above |
| +# copyright notice, this list of conditions and the following disclaimer |
| +# in the documentation and/or other materials provided with the |
| +# distribution. |
| +# * Neither the name of Google Inc. nor the names of its |
| +# contributors may be used to endorse or promote products derived from |
| +# this software without specific prior written permission. |
| +# |
| +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| + |
| +"""Crocodile HTML output.""" |
| + |
| +import os |
| +import shutil |
| +import time |
| +import xml.dom |
| + |
| + |
| +class CrocHtmlError(Exception): |
| + """Coverage HTML error.""" |
| + |
| + |
| +class HtmlElement(object): |
| + """Node in a HTML file.""" |
| + |
| + def __init__(self, doc, element): |
| + """Constructor. |
| + |
| + Args: |
| + doc: XML document object. |
| + element: XML element. |
| + """ |
| + self.doc = doc |
| + self.element = element |
| + |
| + def E(self, name, **kwargs): |
| + """Adds a child element. |
| + |
| + Args: |
| + name: Name of element. |
| + kwargs: Attributes for element. To use an attribute which is a python |
| + reserved word (i.e. 'class'), prefix the attribute name with 'e_'. |
| + |
| + Returns: |
| + The child element. |
| + """ |
| + he = HtmlElement(self.doc, self.doc.createElement(name)) |
| + element = he.element |
| + self.element.appendChild(element) |
| + |
| + for k, v in kwargs.iteritems(): |
| + if k.startswith('e_'): |
| + # Remove prefix |
| + element.setAttribute(k[2:], str(v)) |
| + else: |
| + element.setAttribute(k, str(v)) |
| + |
| + return he |
| + |
| + def Text(self, text): |
| + """Adds a text node. |
| + |
| + Args: |
| + text: Text to add. |
| + |
| + Returns: |
| + self. |
| + """ |
| + t = self.doc.createTextNode(str(text)) |
| + self.element.appendChild(t) |
| + return self |
| + |
| + |
| +class HtmlFile(object): |
| + """HTML file.""" |
| + |
| + def __init__(self, xml_impl, filename): |
| + """Constructor. |
| + |
| + Args: |
| + xml_impl: DOMImplementation to use to create document. |
| + filename: Path to file. |
| + """ |
| + self.xml_impl = xml_impl |
| + doctype = xml_impl.createDocumentType( |
| + 'HTML', '-//W3C//DTD HTML 4.01//EN', |
| + 'http://www.w3.org/TR/html4/strict.dtd') |
| + self.doc = xml_impl.createDocument(None, 'html', doctype) |
| + self.filename = filename |
| + |
| + # Create head and body elements |
| + root = HtmlElement(self.doc, self.doc.documentElement) |
| + self.head = root.E('head') |
| + self.body = root.E('body') |
| + |
| + def Write(self, cleanup=True): |
| + """Writes the file. |
| + |
| + Args: |
| + cleanup: If True, calls unlink() on the internal xml document. This |
| + frees up memory, but means that you can't use this file for anything |
| + else. |
| + """ |
| + f = open(self.filename, 'wt') |
| + self.doc.writexml(f, encoding='UTF-8') |
| + f.close() |
| + |
| + if cleanup: |
| + self.doc.unlink() |
| + # Prevent future uses of the doc now that we've unlinked it |
| + self.doc = None |
| + |
| +#------------------------------------------------------------------------------ |
| + |
| +COV_TYPE_STRING = {None: 'm', 0: 'i', 1: 'E', 2: ' '} |
| +COV_TYPE_CLASS = {None: 'missing', 0: 'instr', 1: 'covered', 2: ''} |
| + |
| + |
| +class CrocHtml(object): |
| + """Crocodile HTML output class.""" |
| + |
| + def __init__(self, cov, output_root): |
| + """Constructor.""" |
| + self.cov = cov |
| + self.output_root = output_root |
| + self.xml_impl = xml.dom.getDOMImplementation() |
| + self.time_string = 'Coverage information generated %s.' % time.asctime() |
| + |
| + def CreateHtmlDoc(self, filename, title): |
| + """Creates a new HTML document. |
| + |
| + Args: |
| + filename: Filename to write to, relative to self.output_root. |
| + title: Title of page |
| + |
| + Returns: |
| + The document. |
| + """ |
| + f = HtmlFile(self.xml_impl, self.output_root + '/' + filename) |
| + |
| + f.head.E('title').Text(title) |
| + f.head.E( |
| + 'link', rel='stylesheet', type='text/css', |
| + href='../' * (len(filename.split('/')) - 1) + 'croc.css') |
| + |
| + return f |
| + |
| + def AddCaptionForFile(self, body, path): |
| + """Adds a caption for the file, with links to each parent dir. |
| + |
| + Args: |
| + body: Body elemement. |
| + path: Path to file. |
| + """ |
| + # This is slightly different that for subdir, because it needs to have a |
| + # link to the current directory's index.html. |
| + hdr = body.E('h2') |
| + hdr.Text('Coverage for ') |
| + dirs = [''] + path.split('/') |
| + num_dirs = len(dirs) |
| + for i in range(num_dirs - 1): |
| + hdr.E('a', href=( |
| + '../' * (num_dirs - i - 2) + 'index.html')).Text(dirs[i] + '/') |
| + hdr.Text(dirs[-1]) |
| + |
| + def AddCaptionForSubdir(self, body, path): |
| + """Adds a caption for the subdir, with links to each parent dir. |
| + |
| + Args: |
| + body: Body elemement. |
| + path: Path to subdir. |
| + """ |
| + # Link to parent dirs |
| + hdr = body.E('h2') |
| + hdr.Text('Coverage for ') |
| + dirs = [''] + path.split('/') |
| + num_dirs = len(dirs) |
| + for i in range(num_dirs - 1): |
| + hdr.E('a', href=( |
| + '../' * (num_dirs - i - 1) + 'index.html')).Text(dirs[i] + '/') |
| + hdr.Text(dirs[-1] + '/') |
| + |
| + def AddSectionHeader(self, table, caption, itemtype, is_file=False): |
| + """Adds a section header to the coverage table. |
| + |
| + Args: |
| + table: Table to add rows to. |
| + caption: Caption for section, if not None. |
| + itemtype: Type of items in this section, if not None. |
| + is_file: Are items in this section files? |
| + """ |
| + |
| + if caption is not None: |
| + table.E('tr').E('td', e_class='secdesc', colspan=8).Text(caption) |
| + |
| + sec_hdr = table.E('tr') |
| + |
| + if itemtype is not None: |
| + sec_hdr.E('td', e_class='section').Text(itemtype) |
| + |
| + sec_hdr.E('td', e_class='section').Text('Coverage') |
| + sec_hdr.E('td', e_class='section', colspan=3).Text( |
| + 'Lines executed / instrumented / missing') |
| + |
| + graph = sec_hdr.E('td', e_class='section') |
| + graph.E('span', style='color:#00FF00').Text('exe') |
| + graph.Text(' / ') |
| + graph.E('span', style='color:#FFFF00').Text('inst') |
| + graph.Text(' / ') |
| + graph.E('span', style='color:#FF0000').Text('miss') |
| + |
| + if is_file: |
| + sec_hdr.E('td', e_class='section').Text('Language') |
| + sec_hdr.E('td', e_class='section').Text('Group') |
| + else: |
| + sec_hdr.E('td', e_class='section', colspan=2) |
| + |
| + def AddItem(self, table, itemname, stats, attrs, link=None): |
| + """Adds a bar graph to the element. This is a series of <td> elements. |
| + |
| + Args: |
| + table: Table to add item to. |
| + itemname: Name of item. |
| + stats: Stats object. |
| + attrs: Attributes dictionary; if None, no attributes will be printed. |
| + link: Destination for itemname hyperlink, if not None. |
| + """ |
| + row = table.E('tr') |
| + |
| + # Add item name |
| + if itemname is not None: |
| + item_elem = row.E('td') |
| + if link is not None: |
| + item_elem = item_elem.E('a', href=link) |
| + item_elem.Text(itemname) |
| + |
| + # Get stats |
| + stat_exe = stats.get('lines_executable', 0) |
| + stat_ins = stats.get('lines_instrumented', 0) |
| + stat_cov = stats.get('lines_covered', 0) |
| + |
| + percent = row.E('td') |
| + |
| + # Add text |
| + row.E('td', e_class='number').Text(stat_cov) |
| + row.E('td', e_class='number').Text(stat_ins) |
| + row.E('td', e_class='number').Text(stat_exe - stat_ins) |
| + |
| + # Add percent and graph; only fill in if there's something in there |
| + graph = row.E('td', e_class='graph', width=100) |
| + if stat_exe: |
| + percent_cov = 100.0 * stat_cov / stat_exe |
| + percent_ins = 100.0 * stat_ins / stat_exe |
| + |
| + # Color percent based on thresholds |
| + percent.Text('%.1f%%' % percent_cov) |
| + if percent_cov >= 80: |
| + percent.element.setAttribute('class', 'high_pct') |
| + elif percent_cov >= 60: |
| + percent.element.setAttribute('class', 'mid_pct') |
| + else: |
| + percent.element.setAttribute('class', 'low_pct') |
| + |
| + # Graphs use integer values |
| + percent_cov = int(percent_cov) |
| + percent_ins = int(percent_ins) |
| + |
| + graph.Text('.') |
| + graph.E('span', style='padding-left:%dpx' % percent_cov, |
| + e_class='g_covered') |
| + graph.E('span', style='padding-left:%dpx' % (percent_ins - percent_cov), |
| + e_class='g_instr') |
| + graph.E('span', style='padding-left:%dpx' % (100 - percent_ins), |
| + e_class='g_missing') |
| + |
| + if attrs: |
| + row.E('td', e_class='stat').Text(attrs.get('language')) |
| + row.E('td', e_class='stat').Text(attrs.get('group')) |
| + else: |
| + row.E('td', colspan=2) |
| + |
| + def WriteFile(self, cov_file): |
| + """Writes the HTML for a file. |
| + |
| + Args: |
| + cov_file: croc.CoveredFile to write. |
| + """ |
| + print ' ' + cov_file.filename |
| + title = 'Coverage for ' + cov_file.filename |
| + |
| + f = self.CreateHtmlDoc(cov_file.filename + '.html', title) |
| + body = f.body |
| + |
| + # Write header section |
| + self.AddCaptionForFile(body, cov_file.filename) |
| + |
| + # Summary for this file |
| + table = body.E('table') |
| + self.AddSectionHeader(table, None, None, is_file=True) |
| + self.AddItem(table, None, cov_file.stats, cov_file.attrs) |
| + |
| + body.E('h2').Text('Line-by-line coverage:') |
| + |
| + # Print line-by-line coverage |
| + if cov_file.local_path: |
| + code_table = body.E('table').E('tr').E('td').E('pre') |
| + |
| + flines = open(cov_file.local_path, 'rt') |
| + lineno = 0 |
| + |
| + for line in flines: |
| + lineno += 1 |
| + line_cov = cov_file.lines.get(lineno, 2) |
| + e_class = COV_TYPE_CLASS.get(line_cov) |
| + |
| + code_table.E('span', e_class=e_class).Text('%4d %s : %s\n' % ( |
| + lineno, |
| + COV_TYPE_STRING.get(line_cov), |
| + line.rstrip() |
| + )) |
| + |
| + else: |
| + body.Text('Line-by-line coverage not available. Make sure the directory' |
| + ' containing this file has been scanned via ') |
| + body.E('B').Text('add_files') |
| + body.Text(' in a configuration file, or the ') |
| + body.E('B').Text('--addfiles') |
| + body.Text(' command line option.') |
| + |
| + # TODO: if file doesn't have a local path, try to find it by |
| + # reverse-mapping roots and searching for the file. |
| + |
| + body.E('p', e_class='time').Text(self.time_string) |
| + f.Write() |
| + |
| + def WriteSubdir(self, cov_dir): |
| + """Writes the index.html for a subdirectory. |
| + |
| + Args: |
| + cov_dir: croc.CoveredDir to write. |
| + """ |
| + print ' ' + cov_dir.dirpath + '/' |
| + |
| + # Create the subdir if it doesn't already exist |
| + subdir = self.output_root + '/' + cov_dir.dirpath |
| + if not os.path.exists(subdir): |
| + os.mkdir(subdir) |
| + |
| + if cov_dir.dirpath: |
| + title = 'Coverage for ' + cov_dir.dirpath + '/' |
| + f = self.CreateHtmlDoc(cov_dir.dirpath + '/index.html', title) |
| + else: |
| + title = 'Coverage summary' |
| + f = self.CreateHtmlDoc('index.html', title) |
| + |
| + body = f.body |
| + |
| + # Write header section |
| + if cov_dir.dirpath: |
| + self.AddCaptionForSubdir(body, cov_dir.dirpath) |
| + else: |
| + body.E('h2').Text(title) |
| + |
| + table = body.E('table') |
| + |
| + # Coverage by group |
| + self.AddSectionHeader(table, 'Coverage by Group', 'Group') |
| + |
| + for group in sorted(cov_dir.stats_by_group): |
| + self.AddItem(table, group, cov_dir.stats_by_group[group], None) |
| + |
| + # List subdirs |
| + if cov_dir.subdirs: |
| + self.AddSectionHeader(table, 'Subdirectories', 'Subdirectory') |
| + |
| + for d in sorted(cov_dir.subdirs): |
| + self.AddItem(table, d + '/', cov_dir.subdirs[d].stats_by_group['all'], |
| + None, link=d + '/index.html') |
| + |
| + # List files |
| + if cov_dir.files: |
| + self.AddSectionHeader(table, 'Files in This Directory', 'Filename', |
| + is_file=True) |
| + |
| + for filename in sorted(cov_dir.files): |
| + cov_file = cov_dir.files[filename] |
| + self.AddItem(table, filename, cov_file.stats, cov_file.attrs, |
| + link=filename + '.html') |
| + |
| + body.E('p', e_class='time').Text(self.time_string) |
| + f.Write() |
| + |
| + def WriteRoot(self): |
| + """Writes the files in the output root.""" |
| + # Find ourselves |
| + src_dir = os.path.split(self.WriteRoot.func_code.co_filename)[0] |
| + |
| + # Files to copy into output root |
| + copy_files = [ |
| + 'croc.css', |
| + ] |
| + |
| + # Copy files from our directory into the output directory |
| + for copy_file in copy_files: |
| + print ' Copying %s' % copy_file |
| + shutil.copyfile(os.path.join(src_dir, copy_file), |
| + os.path.join(self.output_root, copy_file)) |
| + |
| + def Write(self): |
| + """Writes HTML output.""" |
| + |
| + print 'Writing HTML to %s...' % self.output_root |
| + |
| + # Loop through the tree and write subdirs, breadth-first |
| + # TODO: switch to depth-first and sort values - makes nicer output? |
| + todo = [self.cov.tree] |
| + while todo: |
| + cov_dir = todo.pop(0) |
| + |
| + # Append subdirs to todo list |
| + todo += cov_dir.subdirs.values() |
| + |
| + # Write this subdir |
| + self.WriteSubdir(cov_dir) |
| + |
| + # Write files in this subdir |
| + for cov_file in cov_dir.files.itervalues(): |
| + self.WriteFile(cov_file) |
| + |
| + # Write files in root directory |
| + self.WriteRoot() |
| + |