diff options
Diffstat (limited to 'tools/code_coverage/croc_html.py')
-rw-r--r-- | tools/code_coverage/croc_html.py | 453 |
1 files changed, 453 insertions, 0 deletions
diff --git a/tools/code_coverage/croc_html.py b/tools/code_coverage/croc_html.py new file mode 100644 index 0000000..79ec85d --- /dev/null +++ b/tools/code_coverage/croc_html.py @@ -0,0 +1,453 @@ +#!/usr/bin/python2.4 +# +# Copyright 2009, Google Inc. +# 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() + |