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&nbsp;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&nbsp;lines:</td>
111          <td class="headerValue" width="20%%">%(total_lines)s</td>
112        </tr>
113        <tr>
114          <td class="headerItem" width="20%%">Code&nbsp;covered:</td>
115          <td class="headerValue" width="20%%">%(covered_lines)s</td>
116          <td width="20%%"></td>
117          <td class="headerItem" width="20%%">Executed&nbsp;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&nbsp;%%</td>
140      <td class="%(numClass)s">%(covered_lines)s&nbsp;/&nbsp;%(total_lines)s&nbsp;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&nbsp;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                        ('&', '&amp;'),
186                        ('<', '&lt;' ),
187                        ('>', '&gt;' ) )
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