# Copyright (c) 2012 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """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, base_url=None): """Constructor.""" self.cov = cov self.output_root = output_root self.base_url = base_url 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) if self.base_url: css_href = self.base_url + 'croc.css' base_href = self.base_url + os.path.dirname(filename) if not base_href.endswith('/'): base_href += '/' f.head.E('base', href=base_href) else: css_href = '../' * (len(filename.split('/')) - 1) + 'croc.css' f.head.E('link', rel='stylesheet', type='text/css', href=css_href) 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('th', e_class='secdesc', colspan=8).Text(caption) sec_hdr = table.E('tr') if itemtype is not None: sec_hdr.E('th', e_class='section').Text(itemtype) sec_hdr.E('th', e_class='section').Text('Coverage') sec_hdr.E('th', e_class='section', colspan=3).Text( 'Lines executed / instrumented / missing') graph = sec_hdr.E('th', 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('th', e_class='section').Text('Language') sec_hdr.E('th', e_class='section').Text('Group') else: sec_hdr.E('th', 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 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 dirs = [''] + cov_dir.dirpath.split('/') num_dirs = len(dirs) sort_jsfile = '../' * (num_dirs - 1) + 'sorttable.js' script = body.E('script', src=sort_jsfile) body.E('/script') # Write header section if cov_dir.dirpath: self.AddCaptionForSubdir(body, cov_dir.dirpath) else: body.E('h2').Text(title) table = body.E('table', e_class='sortable') table.E('h3').Text('Coverage by Group') # Coverage by group self.AddSectionHeader(table, None, '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: table = body.E('table', e_class='sortable') table.E('h3').Text('Subdirectories') self.AddSectionHeader(table, None, '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: table = body.E('table', e_class='sortable') table.E('h3').Text('Files in This Directory') self.AddSectionHeader(table, None, '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'] # Third_party files to copy into output root third_party_files = ['sorttable.js'] # 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)) # Copy third party files from third_party directory into # the output directory src_dir = os.path.join(src_dir, 'third_party') for third_party_file in third_party_files: print ' Copying %s' % third_party_file shutil.copyfile(os.path.join(src_dir, third_party_file), os.path.join(self.output_root, third_party_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()