• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
7import os
8import shutil
9import time
10import xml.dom
11
12
13class CrocHtmlError(Exception):
14  """Coverage HTML error."""
15
16
17class 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
68class 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
109COV_TYPE_STRING = {None: 'm', 0: 'i', 1: 'E', 2: ' '}
110COV_TYPE_CLASS = {None: 'missing', 0: 'instr', 1: 'covered', 2: ''}
111
112
113class 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