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