普通文本  |  452行  |  13.14 KB

# 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()