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