From 059ca9ade49f35721008d37037c21ecdd76330c2 Mon Sep 17 00:00:00 2001 From: Dmitry Kovalenko Date: Mon, 10 Jul 2017 12:33:09 +0300 Subject: [PATCH] Add capability to dump coverage instead of http server run Use coverage-report-dump.py with --dump= to dump html's "$BROWSER /index.html" show same info as common HTTP server but now you can easily share coverage Moreover makes zip archive with whole report and print to stdout summary Signed-off-by: Dmitry Kovalenko --- packaging/libFuzzer.spec | 2 + scripts/coverage-report-dump.py | 213 ++++++++++++++++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100755 scripts/coverage-report-dump.py diff --git a/packaging/libFuzzer.spec b/packaging/libFuzzer.spec index 3abf96d..3a89775 100644 --- a/packaging/libFuzzer.spec +++ b/packaging/libFuzzer.spec @@ -25,6 +25,7 @@ cp lib/Fuzzer/libFuzzer.a %{buildroot}%{_libdir} mkdir -p %{buildroot}%{_prefix}/bin cp scripts/sancov.py %{buildroot}%{_prefix}/bin cp scripts/sancov_symbolize.py %{buildroot}%{_prefix}/bin +cp scripts/coverage-report-dump.py %{buildroot}%{_prefix}/bin cp %{SOURCE1} %{SOURCE2} %{buildroot}%{_prefix}/bin %files @@ -70,3 +71,4 @@ Scripts to process coverage information collected with SanitizerCoverage %license LICENSE.TXT %{_prefix}/bin/sancov.py %{_prefix}/bin/sancov_symbolize.py +%{_prefix}/bin/coverage-report-dump.py diff --git a/scripts/coverage-report-dump.py b/scripts/coverage-report-dump.py new file mode 100755 index 0000000..3e12e39 --- /dev/null +++ b/scripts/coverage-report-dump.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python2 +# ===- coverage_report_dump.py - Dump Coverage Reports --*- python -*--===# +# +# TODO: figure correct license +# The LLVM Compiler Infrastructure +# +# This file is distributed under the University of Illinois Open Source +# License. See LICENSE.TXT for details. +# +# ===------------------------------------------------------------------===# +'''Dump coverage reports from .symcov files. + +Usage: + ./coverage-report-dump.py \ + --symcov coverage_data.symcov \ + --srcpath root_src_dir \ + --dump dump-path + +''' + +import argparse +import json +import os +import string +import sys +import math +import shutil + +INDEX_PAGE_TMPL = """ + + + Coverage Report + + + + + + +$filenames +
FileCoverage
Files with 0 coverage are not shown.
+ + +""" + +CONTENT_PAGE_TMPL = """ + + + $path + + + +
+$content
+
+ + +""" + + +class SymcovData: + def __init__(self, symcov_json): + self.covered_points = frozenset(symcov_json['covered-points']) + self.point_symbol_info = symcov_json['point-symbol-info'] + self.file_coverage = self.compute_filecoverage() + + def filenames(self): + return self.point_symbol_info.keys() + + def has_file(self, filename): + return filename in self.point_symbol_info + + def compute_linemap(self, filename): + """Build a line_number->css_class map.""" + points = self.point_symbol_info.get(filename, dict()) + + line_to_points = dict() + for fn, points in points.items(): + for point, loc in points.items(): + line = int(loc.split(":")[0]) + line_to_points.setdefault(line, []).append(point) + + result = dict() + for line, points in line_to_points.items(): + status = "covered" + covered_points = self.covered_points & set(points) + if not len(covered_points): + status = "not-covered" + elif len(covered_points) != len(points): + status = "partially-covered" + result[line] = status + return result + + def compute_filecoverage(self): + """Build a filename->pct coverage.""" + result = dict() + for filename, fns in self.point_symbol_info.items(): + file_points = [] + for fn, points in fns.items(): + file_points.extend(points.keys()) + covered_points = self.covered_points & set(file_points) + result[filename] = int(math.ceil( + len(covered_points) * 100 / len(file_points))) + return result + + +def format_pct(pct): + pct_str = str(max(0, min(100, pct))) + zeroes = '0' * (3 - len(pct_str)) + if zeroes: + zeroes = '{0}'.format(zeroes) + return zeroes + pct_str + + +class DumpCov(): + def DumpCov(self): + pass + + html_escape_tbl = { + "&": "&", + '"': """, + "'": "'", + ">": ">", + "<": "<" + } + + def __html_chars_escape(self, path): + return "".join(self.html_escape_tbl.get(c, c) for c in path) + + def __dump_src_file(self, filename): + if not self.__symcov_data.has_file(filename): + return + + filepath = os.path.join(self.__src_path, filename.lstrip('/')) + if not os.path.exists(filepath): + print >> sys.stderr, ("file not exists " + filepath) + return + + linemap = self.__symcov_data.compute_linemap(filename) + + with open(filepath, 'r') as f: + content = "\n".join( + ["{line} ".format( + line=self.__html_chars_escape(line.rstrip()), + cls=linemap.get(line_no, "")) + for line_no, line in enumerate(f, start=1)]) + + response = string.Template(CONTENT_PAGE_TMPL).safe_substitute( + path=filename, + content=content) + + html_file = os.path.abspath(self.__dump_root + filename + ".html") + src_dir = os.path.dirname(html_file) + if not os.path.exists(src_dir): + os.makedirs(src_dir) + + with open(html_file, 'wb') as f: + f.write(response.encode('UTF-8', 'replace')) + + def do_Dump(self, symcov_data, src_path, dump_path): + filelist = [] + self.__symcov_data = symcov_data + self.__src_path = src_path + self.__dump_path = dump_path + self.__dump_root = dump_path + "/coverage" + + if not os.path.exists(os.path.abspath(self.__dump_root)): + os.makedirs(self.__dump_root) + for filename in sorted(self.__symcov_data.filenames()): + file_coverage = self.__symcov_data.file_coverage[filename] + if not file_coverage: + continue + filelist.append( + "{name}" + "{coverage}%".format( + name=self.__html_chars_escape(filename), + coverage=format_pct(file_coverage))) + # TODO: Print it pretty + print("{:<76} {}%".format(filename, file_coverage)) + self.__dump_src_file(filename) + + response = string.Template(INDEX_PAGE_TMPL).safe_substitute( + filenames='\n'.join(filelist)) + with open(self.__dump_root + '/index.html', 'wb') as f: + f.write(response.encode('UTF-8', 'replace')) + print >> sys.stderr, ("Coverage dumped to " + self.__dump_root) + zip_out = shutil.make_archive(self.__dump_root, + 'zip', self.__dump_root) + print >> sys.stderr, ("Coverage dump Zipped " + zip_out) + + +def main(): + parser = argparse.ArgumentParser(description="symcov report dump.") + parser.add_argument('--symcov', required=True, type=argparse.FileType('r')) + parser.add_argument('--srcpath', required=True) + parser.add_argument('--dump', required=True, + help="Path to dump coverage") + args = parser.parse_args() + + print >> sys.stderr, ("Loading coverage...") + symcov_json = json.load(args.symcov) + + Dumper = DumpCov() + Dumper.do_Dump(SymcovData(symcov_json), args.srcpath, args.dump) + +if __name__ == '__main__': + main() -- 2.34.1