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."""
13 class CrocHtmlError(Exception):
14 """Coverage HTML error."""
17 class HtmlElement(object):
18 """Node in a HTML file."""
20 def __init__(self
, doc
, element
):
24 doc: XML document object.
28 self
.element
= element
30 def E(self
, name
, **kwargs
):
31 """Adds a child element.
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_'.
41 he
= HtmlElement(self
.doc
, self
.doc
.createElement(name
))
43 self
.element
.appendChild(element
)
45 for k
, v
in kwargs
.iteritems():
46 if k
.startswith('e_'):
48 element
.setAttribute(k
[2:], str(v
))
50 element
.setAttribute(k
, str(v
))
63 t
= self
.doc
.createTextNode(str(text
))
64 self
.element
.appendChild(t
)
68 class HtmlFile(object):
71 def __init__(self
, xml_impl
, filename
):
75 xml_impl: DOMImplementation to use to create document.
76 filename: Path to file.
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):
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
98 f
= open(self
.filename
, 'wt')
99 self
.doc
.writexml(f
, encoding
='UTF-8')
104 # Prevent future uses of the doc now that we've unlinked it
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):
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.
128 filename: Filename to write to, relative to self.output_root.
134 f
= HtmlFile(self
.xml_impl
, self
.output_root
+ '/' + filename
)
136 f
.head
.E('title').Text(title
)
139 css_href
= self
.base_url
+ 'croc.css'
140 base_href
= self
.base_url
+ os
.path
.dirname(filename
)
141 if not base_href
.endswith('/'):
143 f
.head
.E('base', href
=base_href
)
145 css_href
= '../' * (len(filename
.split('/')) - 1) + 'croc.css'
147 f
.head
.E('link', rel
='stylesheet', type='text/css', href
=css_href
)
151 def AddCaptionForFile(self
, body
, path
):
152 """Adds a caption for the file, with links to each parent dir.
155 body: Body elemement.
158 # This is slightly different that for subdir, because it needs to have a
159 # link to the current directory's index.html.
161 hdr
.Text('Coverage for ')
162 dirs
= [''] + path
.split('/')
164 for i
in range(num_dirs
- 1):
166 '../' * (num_dirs
- i
- 2) + 'index.html')).Text(dirs
[i
] + '/')
169 def AddCaptionForSubdir(self
, body
, path
):
170 """Adds a caption for the subdir, with links to each parent dir.
173 body: Body elemement.
174 path: Path to subdir.
176 # Link to parent dirs
178 hdr
.Text('Coverage for ')
179 dirs
= [''] + path
.split('/')
181 for i
in range(num_dirs
- 1):
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.
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')
211 graph
.E('span', style
='color:#FFFF00').Text('inst')
213 graph
.E('span', style
='color:#FF0000').Text('miss')
216 sec_hdr
.E('th', e_class
='section').Text('Language')
217 sec_hdr
.E('th', e_class
='section').Text('Group')
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.
225 table: Table to add item to.
226 itemname: Name of item.
228 attrs: Attributes dictionary; if None, no attributes will be printed.
229 link: Destination for itemname hyperlink, if not None.
234 if itemname
is not None:
235 item_elem
= row
.E('td')
237 item_elem
= item_elem
.E('a', href
=link
)
238 item_elem
.Text(itemname
)
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')
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)
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')
265 percent
.element
.setAttribute('class', 'low_pct')
267 # Graphs use integer values
268 percent_cov
= int(percent_cov
)
269 percent_ins
= int(percent_ins
)
272 graph
.E('span', style
='padding-left:%dpx' % percent_cov
,
274 graph
.E('span', style
='padding-left:%dpx' % (percent_ins
- percent_cov
),
276 graph
.E('span', style
='padding-left:%dpx' % (100 - percent_ins
),
280 row
.E('td', e_class
='stat').Text(attrs
.get('language'))
281 row
.E('td', e_class
='stat').Text(attrs
.get('group'))
283 row
.E('td', colspan
=2)
285 def WriteFile(self
, cov_file
):
286 """Writes the HTML for a file.
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
)
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')
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' % (
321 COV_TYPE_STRING
.get(line_cov
),
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
)
339 def WriteSubdir(self
, cov_dir
):
340 """Writes the index.html for a subdirectory.
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
):
353 title
= 'Coverage for ' + cov_dir
.dirpath
+ '/'
354 f
= self
.CreateHtmlDoc(cov_dir
.dirpath
+ '/index.html', title
)
356 title
= 'Coverage summary'
357 f
= self
.CreateHtmlDoc('index.html', title
)
361 dirs
= [''] + cov_dir
.dirpath
.split('/')
363 sort_jsfile
= '../' * (num_dirs
- 1) + 'sorttable.js'
364 script
= body
.E('script', src
=sort_jsfile
)
367 # Write header section
369 self
.AddCaptionForSubdir(body
, cov_dir
.dirpath
)
371 body
.E('h2').Text(title
)
373 table
= body
.E('table', e_class
='sortable')
374 table
.E('h3').Text('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)
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')
393 table
= body
.E('table', e_class
='sortable')
394 table
.E('h3').Text('Files in This Directory')
395 self
.AddSectionHeader(table
, None, 'Filename',
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
)
407 """Writes the files in the output root."""
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
))
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
]
438 cov_dir
= todo
.pop(0)
440 # Append subdirs to todo list
441 todo
+= cov_dir
.subdirs
.values()
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