#!/usr/bin/env python # # Copyright (C) 2004, 2005, 2006 Nathaniel Smith # Copyright (C) 2007 Holger Hans Peter Freyther # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of # its contributors may be used to endorse or promote products derived # from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # HTML output inspired by the output of lcov as found on the GStreamer # site. I assume this is not copyrightable. # # # Read all CSV files and # Create an overview file # # import sys import csv import glob import time import os import os.path import datetime import shutil os.environ["TTFPATH"] = ":".join(["/usr/share/fonts/truetype/" + d for d in "ttf-bitstream-vera", "freefont", "msttcorefonts"]) import matplotlib matplotlib.use("Agg") import matplotlib.pylab as m level_LOW = 10 level_MEDIUM = 70 def copy_files(dest_dir): """ Copy the CSS and the png's to the destination directory """ images = ["amber.png", "emerald.png", "glass.png", "ruby.png", "snow.png"] css = "gcov.css" (base_path, name) = os.path.split(__file__) base_path = os.path.abspath(base_path) shutil.copyfile(os.path.join(base_path,css), os.path.join(dest_dir,css)) map(lambda x: shutil.copyfile(os.path.join(base_path,x), os.path.join(dest_dir,x)), images) def sumcov(cov): return "%.2f%% (%s/%s)" % (cov[1] * 100.0 / (cov[0] or 1), cov[1], cov[0]) def create_page(dest_dir, name): index = open(os.path.join(dest_dir, name), "w") index.write(""" WebKit test coverage information """) return index def generate_header(file, last_time, total_lines, total_executed, path, image): product = "WebKit" date = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(last_time)) covered_lines = sumcov((total_lines, total_executed)) file.write("""
GCOV code coverage report
Current view: %(path)s
Test: %(product)s
Date: %(date)s Instrumented lines: %(total_lines)s
Code covered: %(covered_lines)s Executed lines: %(total_executed)s
""" % vars()) # disabled for now def generate_table_item(file, name, total_lines, covered_lines): covered_precise = (covered_lines*100.0)/(total_lines or 1.0) covered = int(round(covered_precise)) remainder = 100-covered (image,perClass,numClass) = coverage_icon(covered_precise) site = "%s.html" % name.replace(os.path.sep,'__') file.write(""" %(name)s
%(covered_precise).2f%(covered_precise).2f
%(covered_precise).2f %% %(covered_lines)s / %(total_lines)s lines """ % vars()) def generate_table_header_start(file): file.write("""
""") def coverage_icon(percent): if percent < level_LOW: return ("ruby.png", "coverPerLo", "coverNumLo") elif percent < level_MEDIUM: return ("amber.png", "coverPerMed", "coverNumMed") else: return ("emerald.png", "coverPerHi", "coverNumHi") def replace(text, *pairs): """ From pydoc... almost identical at least """ from string import split, join while pairs: (a,b) = pairs[0] text = join(split(text, a), b) pairs = pairs[1:] return text def escape(text): """ Escape string to be conform HTML """ return replace(text, ('&', '&'), ('<', '<' ), ('>', '>' ) ) def generate_table_header_end(file): file.write("""

Directory name Coverage
""") def write_title_page(dest_dir,plot_files, last_time, last_tot_lines, last_tot_covered, dir_series): """ Write the index.html with a overview of each directory """ index= create_page(dest_dir, "index.html") generate_header(index, last_time, last_tot_lines, last_tot_covered, "directory", "images/Total.png") # Create the directory overview generate_table_header_start(index) dirs = dir_series.keys() dirs.sort() for dir in dirs: (dir_files, total_lines, covered_lines,_) = dir_series[dir][-1] generate_table_item(index, dir, total_lines, covered_lines) generate_table_header_end(index) index.write("""""") index.close() def write_directory_site(dest_dir, plot_files, dir_name, last_time, dir_series, file_series): escaped_dir = dir_name.replace(os.path.sep,'__') site = create_page(dest_dir, "%s.html" % escaped_dir) (_,tot_lines,tot_covered,files) = dir_series[dir_name][-1] generate_header(site, last_time, tot_lines, tot_covered, "directory - %s" % dir_name, "images/%s.png" % escaped_dir) files.sort() generate_table_header_start(site) for file in files: (lines,covered) = file_series[file][-1] generate_table_item(site, file, lines, covered) generate_table_header_end(site) site.write("""""") site.close() def write_file_site(dest_dir, plot_files, file_name, last_time, data_dir, last_id, file_series): escaped_name = file_name.replace(os.path.sep,'__') site = create_page(dest_dir, "%s.html" % escaped_name) (tot_lines,tot_covered) = file_series[file_name][-1] generate_header(site, last_time, tot_lines, tot_covered, "file - %s" % file_name, "images/%s.png" % escaped_name) path = "%s/%s.annotated%s" % (data_dir,last_id,file_name) # In contrast to the lcov we want to show files that have been compiled # but have not been tested at all. This means we have sourcefiles with 0 # lines covered in the path but they are not lcov files. # To identify them we check the first line now. If we see that we can # continue # -: 0:Source: try: file = open(path, "r") except: return all_lines = file.read().split("\n") # Convert the gcov file to HTML if we have a chanche to do so # Scan each line and see if it was covered or not and escape the # text if len(all_lines) == 0 or not "-: 0:Source:" in all_lines[0]: site.write("

The file was not excercised

") else: site.write("""

    """)
        for line in all_lines:
            split_line = line.split(':',2)
            # e.g. at the EOF
            if len(split_line) == 1:
                continue
            line_number = split_line[1].strip()
            if line_number == "0":
                continue
            covered = 15*" "
            end = ""
            if "#####" in split_line[0]:
                covered = '%15s' % "0"
                end = ""
            elif split_line[0].strip() != "-":
                covered = '%15s' % split_line[0].strip()
                end = ""

            escaped_line = escape(split_line[2])
            str = '%(line_number)10s %(covered)s: %(escaped_line)s%(end)s\n' % vars()
            site.write(str)
        site.write("
") site.write("") site.close() def main(progname, args): if len(args) != 2: sys.exit("Usage: %s DATADIR OUTDIR" % progname) branch = "WebKit from trunk" datadir, outdir = args # First, load in all data from the data directory. data = [] for datapath in glob.glob(os.path.join(datadir, "*.csv")): data.append(read_csv(datapath)) # Sort by time data.sort() # Calculate time series for each file. times = [sample[0] for sample in data] times = [datetime.datetime.utcfromtimestamp(t) for t in times] times = m.date2num(times) all_files = {} all_dirs = {} for sample in data: t, i, tot_line, tot_cover, per_file, per_dir = sample all_files.update(per_file) all_dirs.update(per_dir) total_series = [] file_serieses = dict([[k, [(0, 0)] * len(times)] for k in all_files.keys()]) dir_serieses = dict([[k, [(0, 0, 0, [])] * len(times)] for k in all_dirs.keys()]) data_idx = 0 for sample in data: t, i, tot_line, tot_cover, per_file, per_dir = sample total_series.append([tot_line, tot_cover]) for f, covinfo in per_file.items(): file_serieses[f][data_idx] = covinfo for f, covinfo in per_dir.items(): dir_serieses[f][data_idx] = covinfo data_idx += 1 # Okay, ready to start outputting. First make sure our directories # exist. if not os.path.exists(outdir): os.makedirs(outdir) rel_imgdir = "images" imgdir = os.path.join(outdir, rel_imgdir) if not os.path.exists(imgdir): os.makedirs(imgdir) # Now plot the actual graphs plot_files = {} #plot_files["Total"] = plot_coverage(times, total_series, imgdir, "Total") #for dir, series in dir_serieses.items(): # plot_files[dir] = plot_coverage(times, map(lambda (a,b,c,d):(b,c), series), imgdir, dir) #for f, series in file_serieses.items(): # plot_files[f] = plot_coverage(times, series, imgdir, f) # And look up the latest revision id, and coverage information last_time, last_id, last_tot_lines, last_tot_covered = data[-1][:4] # Now start generating our html file copy_files(outdir) write_title_page(outdir, plot_files, last_time, last_tot_lines, last_tot_covered, dir_serieses) dir_keys = dir_serieses.keys() dir_keys.sort() for dir_name in dir_keys: write_directory_site(outdir, plot_files, dir_name, last_time, dir_serieses, file_serieses) file_keys = file_serieses.keys() for file_name in file_keys: write_file_site(outdir, plot_files, file_name, last_time, datadir, last_id, file_serieses) def read_csv(path): r = csv.reader(open(path, "r")) # First line is id, time for row in r: id, time_str = row break time = int(float(time_str)) # Rest of lines are path, total_lines, covered_lines per_file = {} per_dir = {} grand_total_lines, grand_covered_lines = 0, 0 for row in r: path, total_lines_str, covered_lines_str = row total_lines = int(total_lines_str) covered_lines = int(covered_lines_str) grand_total_lines += total_lines grand_covered_lines += covered_lines per_file[path] = [total_lines, covered_lines] # Update dir statistics dirname = os.path.dirname(path) if not dirname in per_dir: per_dir[dirname] = (0,0,0,[]) (dir_files,dir_total_lines,dir_covered_lines, files) = per_dir[dirname] dir_files += 1 dir_total_lines += total_lines dir_covered_lines += covered_lines files.append(path) per_dir[dirname] = (dir_files,dir_total_lines,dir_covered_lines,files) return [time, id, grand_total_lines, grand_covered_lines, per_file, per_dir] def plot_coverage(times, series, imgdir, name): percentages = [cov * 100.0 / (tot or 1) for tot, cov in series] m.plot_date(times, percentages, "b-") m.plot_date(times, percentages, "bo") m.title(name) m.ylim(0, 100) m.xlabel("Date") m.ylabel("Statement Coverage (%)") outfile_base = name.replace("/", "__") + ".png" outfile = os.path.join(imgdir, outfile_base) m.savefig(outfile, dpi=75) m.close() return outfile_base if __name__ == "__main__": import sys main(sys.argv[0], sys.argv[1:])