452 lines
13 KiB
Python
452 lines
13 KiB
Python
|
# 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 <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
|
||
|
|
||
|
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()
|