Add ICU message format support
[chromium-blink-merge.git] / tools / code_coverage / croc_html.py
blob7866f472f7bf105154cbea2a32039a893b62d032
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.
5 """Crocodile HTML output."""
7 import os
8 import shutil
9 import time
10 import xml.dom
13 class CrocHtmlError(Exception):
14 """Coverage HTML error."""
17 class HtmlElement(object):
18 """Node in a HTML file."""
20 def __init__(self, doc, element):
21 """Constructor.
23 Args:
24 doc: XML document object.
25 element: XML element.
26 """
27 self.doc = doc
28 self.element = element
30 def E(self, name, **kwargs):
31 """Adds a child element.
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_'.
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)
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))
52 return he
54 def Text(self, text):
55 """Adds a text node.
57 Args:
58 text: Text to add.
60 Returns:
61 self.
62 """
63 t = self.doc.createTextNode(str(text))
64 self.element.appendChild(t)
65 return self
68 class HtmlFile(object):
69 """HTML file."""
71 def __init__(self, xml_impl, filename):
72 """Constructor.
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
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')
90 def Write(self, cleanup=True):
91 """Writes the file.
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()
102 if cleanup:
103 self.doc.unlink()
104 # Prevent future uses of the doc now that we've unlinked it
105 self.doc = None
107 #------------------------------------------------------------------------------
109 COV_TYPE_STRING = {None: 'm', 0: 'i', 1: 'E', 2: ' '}
110 COV_TYPE_CLASS = {None: 'missing', 0: 'instr', 1: 'covered', 2: ''}
113 class CrocHtml(object):
114 """Crocodile HTML output class."""
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()
124 def CreateHtmlDoc(self, filename, title):
125 """Creates a new HTML document.
127 Args:
128 filename: Filename to write to, relative to self.output_root.
129 title: Title of page
131 Returns:
132 The document.
134 f = HtmlFile(self.xml_impl, self.output_root + '/' + filename)
136 f.head.E('title').Text(title)
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'
147 f.head.E('link', rel='stylesheet', type='text/css', href=css_href)
149 return f
151 def AddCaptionForFile(self, body, path):
152 """Adds a caption for the file, with links to each parent dir.
154 Args:
155 body: Body elemement.
156 path: Path to file.
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])
169 def AddCaptionForSubdir(self, body, path):
170 """Adds a caption for the subdir, with links to each parent dir.
172 Args:
173 body: Body elemement.
174 path: Path to subdir.
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] + '/')
186 def AddSectionHeader(self, table, caption, itemtype, is_file=False):
187 """Adds a section header to the coverage table.
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?
196 if caption is not None:
197 table.E('tr').E('th', e_class='secdesc', colspan=8).Text(caption)
199 sec_hdr = table.E('tr')
201 if itemtype is not None:
202 sec_hdr.E('th', e_class='section').Text(itemtype)
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')
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')
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)
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.
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.
231 row = table.E('tr')
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)
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)
245 percent = row.E('td')
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)
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
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')
267 # Graphs use integer values
268 percent_cov = int(percent_cov)
269 percent_ins = int(percent_ins)
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')
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)
285 def WriteFile(self, cov_file):
286 """Writes the HTML for a file.
288 Args:
289 cov_file: croc.CoveredFile to write.
291 print ' ' + cov_file.filename
292 title = 'Coverage for ' + cov_file.filename
294 f = self.CreateHtmlDoc(cov_file.filename + '.html', title)
295 body = f.body
297 # Write header section
298 self.AddCaptionForFile(body, cov_file.filename)
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)
305 body.E('h2').Text('Line-by-line coverage:')
307 # Print line-by-line coverage
308 if cov_file.local_path:
309 code_table = body.E('table').E('tr').E('td').E('pre')
311 flines = open(cov_file.local_path, 'rt')
312 lineno = 0
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)
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()
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.')
333 # TODO: if file doesn't have a local path, try to find it by
334 # reverse-mapping roots and searching for the file.
336 body.E('p', e_class='time').Text(self.time_string)
337 f.Write()
339 def WriteSubdir(self, cov_dir):
340 """Writes the index.html for a subdirectory.
342 Args:
343 cov_dir: croc.CoveredDir to write.
345 print ' ' + cov_dir.dirpath + '/'
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)
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)
359 body = f.body
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')
367 # Write header section
368 if cov_dir.dirpath:
369 self.AddCaptionForSubdir(body, cov_dir.dirpath)
370 else:
371 body.E('h2').Text(title)
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')
378 for group in sorted(cov_dir.stats_by_group):
379 self.AddItem(table, group, cov_dir.stats_by_group[group], None)
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')
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')
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)
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')
403 body.E('p', e_class='time').Text(self.time_string)
404 f.Write()
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]
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']
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))
429 def Write(self):
430 """Writes HTML output."""
432 print 'Writing HTML to %s...' % self.output_root
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)
440 # Append subdirs to todo list
441 todo += cov_dir.subdirs.values()
443 # Write this subdir
444 self.WriteSubdir(cov_dir)
446 # Write files in this subdir
447 for cov_file in cov_dir.files.itervalues():
448 self.WriteFile(cov_file)
450 # Write files in root directory
451 self.WriteRoot()