|
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 |
|
43 import sys |
|
44 import csv |
|
45 import glob |
|
46 import time |
|
47 import os |
|
48 import os.path |
|
49 import datetime |
|
50 import shutil |
|
51 |
|
52 os.environ["TTFPATH"] = ":".join(["/usr/share/fonts/truetype/" + d |
|
53 for d in "ttf-bitstream-vera", |
|
54 "freefont", |
|
55 "msttcorefonts"]) |
|
56 import matplotlib |
|
57 matplotlib.use("Agg") |
|
58 import matplotlib.pylab as m |
|
59 |
|
60 level_LOW = 10 |
|
61 level_MEDIUM = 70 |
|
62 |
|
63 def copy_files(dest_dir): |
|
64 """ |
|
65 Copy the CSS and the png's to the destination directory |
|
66 """ |
|
67 images = ["amber.png", "emerald.png", "glass.png", "ruby.png", "snow.png"] |
|
68 css = "gcov.css" |
|
69 (base_path, name) = os.path.split(__file__) |
|
70 base_path = os.path.abspath(base_path) |
|
71 |
|
72 shutil.copyfile(os.path.join(base_path,css), os.path.join(dest_dir,css)) |
|
73 map(lambda x: shutil.copyfile(os.path.join(base_path,x), os.path.join(dest_dir,x)), images) |
|
74 |
|
75 def sumcov(cov): |
|
76 return "%.2f%% (%s/%s)" % (cov[1] * 100.0 / (cov[0] or 1), cov[1], cov[0]) |
|
77 |
|
78 def create_page(dest_dir, name): |
|
79 index = open(os.path.join(dest_dir, name), "w") |
|
80 index.write("""<HTML> |
|
81 <HEAD> |
|
82 <TITLE>WebKit test coverage information</TITLE> |
|
83 <link rel="stylesheet" type="text/css" href="gcov.css"> |
|
84 </HEAD> |
|
85 <BODY> |
|
86 """) |
|
87 return index |
|
88 |
|
89 def generate_header(file, last_time, total_lines, total_executed, path, image): |
|
90 product = "WebKit" |
|
91 date = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(last_time)) |
|
92 covered_lines = sumcov((total_lines, total_executed)) |
|
93 |
|
94 file.write("""<table width="100%%" border=0 cellspacing=0 cellpadding=0> |
|
95 <tr><td class="title">GCOV code coverage report</td></tr> |
|
96 <tr><td class="ruler"><img src="glass.png" width=3 height=3 alt=""></td></tr> |
|
97 |
|
98 <tr> |
|
99 <td width="100%%"> |
|
100 <table cellpadding=1 border=0 width="100%%"> |
|
101 <tr> |
|
102 <td class="headerItem" width="20%%">Current view:</td> |
|
103 <td class="headerValue" width="80%%" colspan=4>%(path)s</td> |
|
104 </tr> |
|
105 <tr> |
|
106 <td class="headerItem" width="20%%">Test:</td> |
|
107 <td class="headerValue" width="80%%" colspan=4>%(product)s</td> |
|
108 </tr> |
|
109 <tr> |
|
110 <td class="headerItem" width="20%%">Date:</td> |
|
111 <td class="headerValue" width="20%%">%(date)s</td> |
|
112 <td width="20%%"></td> |
|
113 <td class="headerItem" width="20%%">Instrumented lines:</td> |
|
114 <td class="headerValue" width="20%%">%(total_lines)s</td> |
|
115 </tr> |
|
116 <tr> |
|
117 <td class="headerItem" width="20%%">Code covered:</td> |
|
118 <td class="headerValue" width="20%%">%(covered_lines)s</td> |
|
119 <td width="20%%"></td> |
|
120 <td class="headerItem" width="20%%">Executed lines:</td> |
|
121 <td class="headerValue" width="20%%">%(total_executed)s</td> |
|
122 </tr> |
|
123 </table> |
|
124 </td> |
|
125 </tr> |
|
126 <tr><td class="ruler"><img src="glass.png" width=3 height=3 alt=""></td></tr> |
|
127 </table>""" % vars()) |
|
128 # disabled for now <tr><td><img src="%(image)s"></td></tr> |
|
129 |
|
130 def generate_table_item(file, name, total_lines, covered_lines): |
|
131 covered_precise = (covered_lines*100.0)/(total_lines or 1.0) |
|
132 covered = int(round(covered_precise)) |
|
133 remainder = 100-covered |
|
134 (image,perClass,numClass) = coverage_icon(covered_precise) |
|
135 site = "%s.html" % name.replace(os.path.sep,'__') |
|
136 file.write(""" |
|
137 <tr> |
|
138 <td class="coverFile"><a href="%(site)s">%(name)s</a></td> |
|
139 <td class="coverBar" align="center"> |
|
140 <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> |
|
141 </td> |
|
142 <td class="%(perClass)s">%(covered_precise).2f %%</td> |
|
143 <td class="%(numClass)s">%(covered_lines)s / %(total_lines)s lines</td> |
|
144 </tr> |
|
145 """ % vars()) |
|
146 |
|
147 def generate_table_header_start(file): |
|
148 file.write("""<center> |
|
149 <table width="80%%" cellpadding=2 cellspacing=1 border=0> |
|
150 |
|
151 <tr> |
|
152 <td width="50%%"><br></td> |
|
153 <td width="15%%"></td> |
|
154 <td width="15%%"></td> |
|
155 <td width="20%%"></td> |
|
156 </tr> |
|
157 |
|
158 <tr> |
|
159 <td class="tableHead">Directory name</td> |
|
160 <td class="tableHead" colspan=3>Coverage</td> |
|
161 </tr> |
|
162 """) |
|
163 |
|
164 def coverage_icon(percent): |
|
165 if percent < level_LOW: |
|
166 return ("ruby.png", "coverPerLo", "coverNumLo") |
|
167 elif percent < level_MEDIUM: |
|
168 return ("amber.png", "coverPerMed", "coverNumMed") |
|
169 else: |
|
170 return ("emerald.png", "coverPerHi", "coverNumHi") |
|
171 |
|
172 def replace(text, *pairs): |
|
173 """ |
|
174 From pydoc... almost identical at least |
|
175 """ |
|
176 from string import split, join |
|
177 while pairs: |
|
178 (a,b) = pairs[0] |
|
179 text = join(split(text, a), b) |
|
180 pairs = pairs[1:] |
|
181 return text |
|
182 |
|
183 def escape(text): |
|
184 """ |
|
185 Escape string to be conform HTML |
|
186 """ |
|
187 return replace(text, |
|
188 ('&', '&'), |
|
189 ('<', '<' ), |
|
190 ('>', '>' ) ) |
|
191 |
|
192 def generate_table_header_end(file): |
|
193 file.write("""</table> |
|
194 </center>""") |
|
195 |
|
196 def write_title_page(dest_dir,plot_files, last_time, last_tot_lines, last_tot_covered, dir_series): |
|
197 """ |
|
198 Write the index.html with a overview of each directory |
|
199 """ |
|
200 index= create_page(dest_dir, "index.html") |
|
201 generate_header(index, last_time, last_tot_lines, last_tot_covered, "directory", "images/Total.png") |
|
202 # Create the directory overview |
|
203 generate_table_header_start(index) |
|
204 dirs = dir_series.keys() |
|
205 dirs.sort() |
|
206 for dir in dirs: |
|
207 (dir_files, total_lines, covered_lines,_) = dir_series[dir][-1] |
|
208 generate_table_item(index, dir, total_lines, covered_lines) |
|
209 generate_table_header_end(index) |
|
210 |
|
211 index.write("""</BODY></HTML>""") |
|
212 index.close() |
|
213 |
|
214 def write_directory_site(dest_dir, plot_files, dir_name, last_time, dir_series, file_series): |
|
215 escaped_dir = dir_name.replace(os.path.sep,'__') |
|
216 site = create_page(dest_dir, "%s.html" % escaped_dir) |
|
217 (_,tot_lines,tot_covered,files) = dir_series[dir_name][-1] |
|
218 generate_header(site, last_time, tot_lines, tot_covered, "directory - %s" % dir_name, "images/%s.png" % escaped_dir) |
|
219 |
|
220 files.sort() |
|
221 |
|
222 generate_table_header_start(site) |
|
223 for file in files: |
|
224 (lines,covered) = file_series[file][-1] |
|
225 generate_table_item(site, file, lines, covered) |
|
226 |
|
227 generate_table_header_end(site) |
|
228 site.write("""</BODY></HTML>""") |
|
229 site.close() |
|
230 |
|
231 def write_file_site(dest_dir, plot_files, file_name, last_time, data_dir, last_id, file_series): |
|
232 escaped_name = file_name.replace(os.path.sep,'__') |
|
233 site = create_page(dest_dir, "%s.html" % escaped_name) |
|
234 (tot_lines,tot_covered) = file_series[file_name][-1] |
|
235 generate_header(site, last_time, tot_lines, tot_covered, "file - %s" % file_name, "images/%s.png" % escaped_name) |
|
236 |
|
237 path = "%s/%s.annotated%s" % (data_dir,last_id,file_name) |
|
238 |
|
239 # In contrast to the lcov we want to show files that have been compiled |
|
240 # but have not been tested at all. This means we have sourcefiles with 0 |
|
241 # lines covered in the path but they are not lcov files. |
|
242 # To identify them we check the first line now. If we see that we can |
|
243 # continue |
|
244 # -: 0:Source: |
|
245 try: |
|
246 file = open(path, "r") |
|
247 except: |
|
248 return |
|
249 all_lines = file.read().split("\n") |
|
250 |
|
251 # Convert the gcov file to HTML if we have a chanche to do so |
|
252 # Scan each line and see if it was covered or not and escape the |
|
253 # text |
|
254 if len(all_lines) == 0 or not "-: 0:Source:" in all_lines[0]: |
|
255 site.write("<p>The file was not excercised</p>") |
|
256 else: |
|
257 site.write("""</br><table cellpadding=0 cellspacing=0 border=0> |
|
258 <tr> |
|
259 <td><br></td> |
|
260 </tr> |
|
261 <tr> |
|
262 <td><pre class="source"> |
|
263 """) |
|
264 for line in all_lines: |
|
265 split_line = line.split(':',2) |
|
266 # e.g. at the EOF |
|
267 if len(split_line) == 1: |
|
268 continue |
|
269 line_number = split_line[1].strip() |
|
270 if line_number == "0": |
|
271 continue |
|
272 covered = 15*" " |
|
273 end = "" |
|
274 if "#####" in split_line[0]: |
|
275 covered = '<span class="lineNoCov">%15s' % "0" |
|
276 end = "</span>" |
|
277 elif split_line[0].strip() != "-": |
|
278 covered = '<span class="lineCov">%15s' % split_line[0].strip() |
|
279 end = "</span>" |
|
280 |
|
281 escaped_line = escape(split_line[2]) |
|
282 str = '<span class="lineNum">%(line_number)10s </span>%(covered)s: %(escaped_line)s%(end)s\n' % vars() |
|
283 site.write(str) |
|
284 site.write("</pre></td></tr></table>") |
|
285 site.write("</BODY></HTML>") |
|
286 site.close() |
|
287 |
|
288 def main(progname, args): |
|
289 if len(args) != 2: |
|
290 sys.exit("Usage: %s DATADIR OUTDIR" % progname) |
|
291 |
|
292 branch = "WebKit from trunk" |
|
293 datadir, outdir = args |
|
294 |
|
295 # First, load in all data from the data directory. |
|
296 data = [] |
|
297 for datapath in glob.glob(os.path.join(datadir, "*.csv")): |
|
298 data.append(read_csv(datapath)) |
|
299 # Sort by time |
|
300 data.sort() |
|
301 |
|
302 # Calculate time series for each file. |
|
303 times = [sample[0] for sample in data] |
|
304 times = [datetime.datetime.utcfromtimestamp(t) for t in times] |
|
305 times = m.date2num(times) |
|
306 all_files = {} |
|
307 all_dirs = {} |
|
308 for sample in data: |
|
309 t, i, tot_line, tot_cover, per_file, per_dir = sample |
|
310 all_files.update(per_file) |
|
311 all_dirs.update(per_dir) |
|
312 total_series = [] |
|
313 file_serieses = dict([[k, [(0, 0)] * len(times)] for k in all_files.keys()]) |
|
314 dir_serieses = dict([[k, [(0, 0, 0, [])] * len(times)] for k in all_dirs.keys()]) |
|
315 data_idx = 0 |
|
316 for sample in data: |
|
317 t, i, tot_line, tot_cover, per_file, per_dir = sample |
|
318 total_series.append([tot_line, tot_cover]) |
|
319 for f, covinfo in per_file.items(): |
|
320 file_serieses[f][data_idx] = covinfo |
|
321 for f, covinfo in per_dir.items(): |
|
322 dir_serieses[f][data_idx] = covinfo |
|
323 data_idx += 1 |
|
324 |
|
325 |
|
326 # Okay, ready to start outputting. First make sure our directories |
|
327 # exist. |
|
328 if not os.path.exists(outdir): |
|
329 os.makedirs(outdir) |
|
330 rel_imgdir = "images" |
|
331 imgdir = os.path.join(outdir, rel_imgdir) |
|
332 if not os.path.exists(imgdir): |
|
333 os.makedirs(imgdir) |
|
334 |
|
335 # Now plot the actual graphs |
|
336 plot_files = {} |
|
337 #plot_files["Total"] = plot_coverage(times, total_series, imgdir, "Total") |
|
338 #for dir, series in dir_serieses.items(): |
|
339 # plot_files[dir] = plot_coverage(times, map(lambda (a,b,c,d):(b,c), series), imgdir, dir) |
|
340 #for f, series in file_serieses.items(): |
|
341 # plot_files[f] = plot_coverage(times, series, imgdir, f) |
|
342 |
|
343 # And look up the latest revision id, and coverage information |
|
344 last_time, last_id, last_tot_lines, last_tot_covered = data[-1][:4] |
|
345 |
|
346 # Now start generating our html file |
|
347 copy_files(outdir) |
|
348 write_title_page(outdir, plot_files, last_time, last_tot_lines, last_tot_covered, dir_serieses) |
|
349 |
|
350 dir_keys = dir_serieses.keys() |
|
351 dir_keys.sort() |
|
352 for dir_name in dir_keys: |
|
353 write_directory_site(outdir, plot_files, dir_name, last_time, dir_serieses, file_serieses) |
|
354 |
|
355 file_keys = file_serieses.keys() |
|
356 for file_name in file_keys: |
|
357 write_file_site(outdir, plot_files, file_name, last_time, datadir, last_id, file_serieses) |
|
358 |
|
359 def read_csv(path): |
|
360 r = csv.reader(open(path, "r")) |
|
361 # First line is id, time |
|
362 for row in r: |
|
363 id, time_str = row |
|
364 break |
|
365 time = int(float(time_str)) |
|
366 # Rest of lines are path, total_lines, covered_lines |
|
367 per_file = {} |
|
368 per_dir = {} |
|
369 grand_total_lines, grand_covered_lines = 0, 0 |
|
370 for row in r: |
|
371 path, total_lines_str, covered_lines_str = row |
|
372 total_lines = int(total_lines_str) |
|
373 covered_lines = int(covered_lines_str) |
|
374 grand_total_lines += total_lines |
|
375 grand_covered_lines += covered_lines |
|
376 per_file[path] = [total_lines, covered_lines] |
|
377 |
|
378 # Update dir statistics |
|
379 dirname = os.path.dirname(path) |
|
380 if not dirname in per_dir: |
|
381 per_dir[dirname] = (0,0,0,[]) |
|
382 (dir_files,dir_total_lines,dir_covered_lines, files) = per_dir[dirname] |
|
383 dir_files += 1 |
|
384 dir_total_lines += total_lines |
|
385 dir_covered_lines += covered_lines |
|
386 files.append(path) |
|
387 per_dir[dirname] = (dir_files,dir_total_lines,dir_covered_lines,files) |
|
388 return [time, id, grand_total_lines, grand_covered_lines, per_file, per_dir] |
|
389 |
|
390 |
|
391 def plot_coverage(times, series, imgdir, name): |
|
392 percentages = [cov * 100.0 / (tot or 1) for tot, cov in series] |
|
393 m.plot_date(times, percentages, "b-") |
|
394 m.plot_date(times, percentages, "bo") |
|
395 m.title(name) |
|
396 m.ylim(0, 100) |
|
397 m.xlabel("Date") |
|
398 m.ylabel("Statement Coverage (%)") |
|
399 outfile_base = name.replace("/", "__") + ".png" |
|
400 outfile = os.path.join(imgdir, outfile_base) |
|
401 m.savefig(outfile, dpi=75) |
|
402 m.close() |
|
403 return outfile_base |
|
404 |
|
405 |
|
406 if __name__ == "__main__": |
|
407 import sys |
|
408 main(sys.argv[0], sys.argv[1:]) |