1#!/usr/bin/env python 2# 3# Copyright (C) 2004, 2005, 2006 Nathaniel Smith 4# Copyright (C) 2007 Holger Hans Peter Freyther 5# 6# Redistribution and use in source and binary forms, with or without 7# modification, are permitted provided that the following conditions 8# are met: 9# 10# 1. Redistributions of source code must retain the above copyright 11# notice, this list of conditions and the following disclaimer. 12# 2. Redistributions in binary form must reproduce the above copyright 13# notice, this list of conditions and the following disclaimer in the 14# documentation and/or other materials provided with the distribution. 15# 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of 16# its contributors may be used to endorse or promote products derived 17# from this software without specific prior written permission. 18# 19# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY 20# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY 23# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 26# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 28# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 30# 31# HTML output inspired by the output of lcov as found on the GStreamer 32# site. I assume this is not copyrightable. 33# 34 35 36# 37# Read all CSV files and 38# Create an overview file 39# 40# 41 42 43import sys 44import csv 45import glob 46import time 47import os 48import os.path 49import datetime 50import shutil 51 52os.environ["TTFPATH"] = ":".join(["/usr/share/fonts/truetype/" + d 53 for d in "ttf-bitstream-vera", 54 "freefont", 55 "msttcorefonts"]) 56 57level_LOW = 10 58level_MEDIUM = 70 59 60def copy_files(dest_dir): 61 """ 62 Copy the CSS and the png's to the destination directory 63 """ 64 images = ["amber.png", "emerald.png", "glass.png", "ruby.png", "snow.png"] 65 css = "gcov.css" 66 (base_path, name) = os.path.split(__file__) 67 base_path = os.path.abspath(base_path) 68 69 shutil.copyfile(os.path.join(base_path,css), os.path.join(dest_dir,css)) 70 map(lambda x: shutil.copyfile(os.path.join(base_path,x), os.path.join(dest_dir,x)), images) 71 72def sumcov(cov): 73 return "%.2f%% (%s/%s)" % (cov[1] * 100.0 / (cov[0] or 1), cov[1], cov[0]) 74 75def create_page(dest_dir, name): 76 index = open(os.path.join(dest_dir, name), "w") 77 index.write("""<HTML> 78 <HEAD> 79 <TITLE>WebKit test coverage information</TITLE> 80 <link rel="stylesheet" type="text/css" href="gcov.css"> 81 </HEAD> 82 <BODY> 83 """) 84 return index 85 86def generate_header(file, last_time, total_lines, total_executed, path, image): 87 product = "WebKit" 88 date = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(last_time)) 89 covered_lines = sumcov((total_lines, total_executed)) 90 91 file.write("""<table width="100%%" border=0 cellspacing=0 cellpadding=0> 92 <tr><td class="title">GCOV code coverage report</td></tr> 93 <tr><td class="ruler"><img src="glass.png" width=3 height=3 alt=""></td></tr> 94 95 <tr> 96 <td width="100%%"> 97 <table cellpadding=1 border=0 width="100%%"> 98 <tr> 99 <td class="headerItem" width="20%%">Current view:</td> 100 <td class="headerValue" width="80%%" colspan=4>%(path)s</td> 101 </tr> 102 <tr> 103 <td class="headerItem" width="20%%">Test:</td> 104 <td class="headerValue" width="80%%" colspan=4>%(product)s</td> 105 </tr> 106 <tr> 107 <td class="headerItem" width="20%%">Date:</td> 108 <td class="headerValue" width="20%%">%(date)s</td> 109 <td width="20%%"></td> 110 <td class="headerItem" width="20%%">Instrumented lines:</td> 111 <td class="headerValue" width="20%%">%(total_lines)s</td> 112 </tr> 113 <tr> 114 <td class="headerItem" width="20%%">Code covered:</td> 115 <td class="headerValue" width="20%%">%(covered_lines)s</td> 116 <td width="20%%"></td> 117 <td class="headerItem" width="20%%">Executed lines:</td> 118 <td class="headerValue" width="20%%">%(total_executed)s</td> 119 </tr> 120 </table> 121 </td> 122 </tr> 123 <tr><td class="ruler"><img src="glass.png" width=3 height=3 alt=""></td></tr> 124 </table>""" % vars()) 125 # disabled for now <tr><td><img src="%(image)s"></td></tr> 126 127def generate_table_item(file, name, total_lines, covered_lines): 128 covered_precise = (covered_lines*100.0)/(total_lines or 1.0) 129 covered = int(round(covered_precise)) 130 remainder = 100-covered 131 (image,perClass,numClass) = coverage_icon(covered_precise) 132 site = "%s.html" % name.replace(os.path.sep,'__') 133 file.write(""" 134 <tr> 135 <td class="coverFile"><a href="%(site)s">%(name)s</a></td> 136 <td class="coverBar" align="center"> 137 <table border=0 cellspacing=0 cellpadding=1><tr><td class="coverBarOutline"><img src="%(image)s" width=%(covered)s height=10 alt="%(covered_precise).2f"><img src="snow.png" width=%(remainder)s height=10 alt="%(covered_precise).2f"></td></tr></table> 138 </td> 139 <td class="%(perClass)s">%(covered_precise).2f %%</td> 140 <td class="%(numClass)s">%(covered_lines)s / %(total_lines)s lines</td> 141 </tr> 142 """ % vars()) 143 144def generate_table_header_start(file): 145 file.write("""<center> 146 <table width="80%%" cellpadding=2 cellspacing=1 border=0> 147 148 <tr> 149 <td width="50%%"><br></td> 150 <td width="15%%"></td> 151 <td width="15%%"></td> 152 <td width="20%%"></td> 153 </tr> 154 155 <tr> 156 <td class="tableHead">Directory name</td> 157 <td class="tableHead" colspan=3>Coverage</td> 158 </tr> 159 """) 160 161def coverage_icon(percent): 162 if percent < level_LOW: 163 return ("ruby.png", "coverPerLo", "coverNumLo") 164 elif percent < level_MEDIUM: 165 return ("amber.png", "coverPerMed", "coverNumMed") 166 else: 167 return ("emerald.png", "coverPerHi", "coverNumHi") 168 169def replace(text, *pairs): 170 """ 171 From pydoc... almost identical at least 172 """ 173 from string import split, join 174 while pairs: 175 (a,b) = pairs[0] 176 text = join(split(text, a), b) 177 pairs = pairs[1:] 178 return text 179 180def escape(text): 181 """ 182 Escape string to be conform HTML 183 """ 184 return replace(text, 185 ('&', '&'), 186 ('<', '<' ), 187 ('>', '>' ) ) 188 189def generate_table_header_end(file): 190 file.write("""</table> 191 </center>""") 192 193def write_title_page(dest_dir, last_time, last_tot_lines, last_tot_covered, dir_series): 194 """ 195 Write the index.html with a overview of each directory 196 """ 197 index= create_page(dest_dir, "index.html") 198 generate_header(index, last_time, last_tot_lines, last_tot_covered, "directory", "images/Total.png") 199 # Create the directory overview 200 generate_table_header_start(index) 201 dirs = dir_series.keys() 202 dirs.sort() 203 for dir in dirs: 204 (dir_files, total_lines, covered_lines,_) = dir_series[dir][-1] 205 generate_table_item(index, dir, total_lines, covered_lines) 206 generate_table_header_end(index) 207 208 index.write("""</BODY></HTML>""") 209 index.close() 210 211def write_directory_site(dest_dir, dir_name, last_time, dir_series, file_series): 212 escaped_dir = dir_name.replace(os.path.sep,'__') 213 site = create_page(dest_dir, "%s.html" % escaped_dir) 214 (_,tot_lines,tot_covered,files) = dir_series[dir_name][-1] 215 generate_header(site, last_time, tot_lines, tot_covered, "directory - %s" % dir_name, "images/%s.png" % escaped_dir) 216 217 files.sort() 218 219 generate_table_header_start(site) 220 for file in files: 221 (lines,covered) = file_series[file][-1] 222 generate_table_item(site, file, lines, covered) 223 224 generate_table_header_end(site) 225 site.write("""</BODY></HTML>""") 226 site.close() 227 228def write_file_site(dest_dir, file_name, last_time, data_dir, last_id, file_series): 229 escaped_name = file_name.replace(os.path.sep,'__') 230 site = create_page(dest_dir, "%s.html" % escaped_name) 231 (tot_lines,tot_covered) = file_series[file_name][-1] 232 generate_header(site, last_time, tot_lines, tot_covered, "file - %s" % file_name, "images/%s.png" % escaped_name) 233 234 path = "%s/%s.annotated%s" % (data_dir,last_id,file_name) 235 236 # In contrast to the lcov we want to show files that have been compiled 237 # but have not been tested at all. This means we have sourcefiles with 0 238 # lines covered in the path but they are not lcov files. 239 # To identify them we check the first line now. If we see that we can 240 # continue 241 # -: 0:Source: 242 try: 243 file = open(path, "r") 244 except: 245 return 246 all_lines = file.read().split("\n") 247 248 # Convert the gcov file to HTML if we have a chanche to do so 249 # Scan each line and see if it was covered or not and escape the 250 # text 251 if len(all_lines) == 0 or not "-: 0:Source:" in all_lines[0]: 252 site.write("<p>The file was not excercised</p>") 253 else: 254 site.write("""</br><table cellpadding=0 cellspacing=0 border=0> 255 <tr> 256 <td><br></td> 257 </tr> 258 <tr> 259 <td><pre class="source"> 260 """) 261 for line in all_lines: 262 split_line = line.split(':',2) 263 # e.g. at the EOF 264 if len(split_line) == 1: 265 continue 266 line_number = split_line[1].strip() 267 if line_number == "0": 268 continue 269 covered = 15*" " 270 end = "" 271 if "#####" in split_line[0]: 272 covered = '<span class="lineNoCov">%15s' % "0" 273 end = "</span>" 274 elif split_line[0].strip() != "-": 275 covered = '<span class="lineCov">%15s' % split_line[0].strip() 276 end = "</span>" 277 278 escaped_line = escape(split_line[2]) 279 str = '<span class="lineNum">%(line_number)10s </span>%(covered)s: %(escaped_line)s%(end)s\n' % vars() 280 site.write(str) 281 site.write("</pre></td></tr></table>") 282 site.write("</BODY></HTML>") 283 site.close() 284 285def main(progname, args): 286 if len(args) != 2: 287 sys.exit("Usage: %s DATADIR OUTDIR" % progname) 288 289 branch = "WebKit from trunk" 290 datadir, outdir = args 291 292 # First, load in all data from the data directory. 293 data = [] 294 for datapath in glob.glob(os.path.join(datadir, "*.csv")): 295 data.append(read_csv(datapath)) 296 # Sort by time 297 data.sort() 298 299 # Calculate time series for each file. 300 times = [sample[0] for sample in data] 301 times = [datetime.datetime.utcfromtimestamp(t) for t in times] 302 303 all_files = {} 304 all_dirs = {} 305 for sample in data: 306 t, i, tot_line, tot_cover, per_file, per_dir = sample 307 all_files.update(per_file) 308 all_dirs.update(per_dir) 309 total_series = [] 310 file_serieses = dict([[k, [(0, 0)] * len(times)] for k in all_files.keys()]) 311 dir_serieses = dict([[k, [(0, 0, 0, [])] * len(times)] for k in all_dirs.keys()]) 312 data_idx = 0 313 for sample in data: 314 t, i, tot_line, tot_cover, per_file, per_dir = sample 315 total_series.append([tot_line, tot_cover]) 316 for f, covinfo in per_file.items(): 317 file_serieses[f][data_idx] = covinfo 318 for f, covinfo in per_dir.items(): 319 dir_serieses[f][data_idx] = covinfo 320 data_idx += 1 321 322 323 # Okay, ready to start outputting. First make sure our directories 324 # exist. 325 if not os.path.exists(outdir): 326 os.makedirs(outdir) 327 rel_imgdir = "images" 328 imgdir = os.path.join(outdir, rel_imgdir) 329 if not os.path.exists(imgdir): 330 os.makedirs(imgdir) 331 332 333 # And look up the latest revision id, and coverage information 334 last_time, last_id, last_tot_lines, last_tot_covered = data[-1][:4] 335 336 # Now start generating our html file 337 copy_files(outdir) 338 write_title_page(outdir, last_time, last_tot_lines, last_tot_covered, dir_serieses) 339 340 dir_keys = dir_serieses.keys() 341 dir_keys.sort() 342 for dir_name in dir_keys: 343 write_directory_site(outdir, dir_name, last_time, dir_serieses, file_serieses) 344 345 file_keys = file_serieses.keys() 346 for file_name in file_keys: 347 write_file_site(outdir, file_name, last_time, datadir, last_id, file_serieses) 348 349def read_csv(path): 350 r = csv.reader(open(path, "r")) 351 # First line is id, time 352 for row in r: 353 id, time_str = row 354 break 355 time = int(float(time_str)) 356 # Rest of lines are path, total_lines, covered_lines 357 per_file = {} 358 per_dir = {} 359 grand_total_lines, grand_covered_lines = 0, 0 360 for row in r: 361 path, total_lines_str, covered_lines_str = row 362 total_lines = int(total_lines_str) 363 covered_lines = int(covered_lines_str) 364 grand_total_lines += total_lines 365 grand_covered_lines += covered_lines 366 per_file[path] = [total_lines, covered_lines] 367 368 # Update dir statistics 369 dirname = os.path.dirname(path) 370 if not dirname in per_dir: 371 per_dir[dirname] = (0,0,0,[]) 372 (dir_files,dir_total_lines,dir_covered_lines, files) = per_dir[dirname] 373 dir_files += 1 374 dir_total_lines += total_lines 375 dir_covered_lines += covered_lines 376 files.append(path) 377 per_dir[dirname] = (dir_files,dir_total_lines,dir_covered_lines,files) 378 return [time, id, grand_total_lines, grand_covered_lines, per_file, per_dir] 379 380if __name__ == "__main__": 381 import sys 382 main(sys.argv[0], sys.argv[1:]) 383