Add capability to dump coverage instead of http server run
authorDmitry Kovalenko <d.kovalenko@samsung.com>
Mon, 10 Jul 2017 09:33:09 +0000 (12:33 +0300)
committerMaria Guseva <m.guseva@samsung.com>
Fri, 14 Jul 2017 03:28:45 +0000 (12:28 +0900)
Use coverage-report-dump.py with --dump=<dest_dir> to dump html's

"$BROWSER <dest_dir>/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 <d.kovalenko@samsung.com>
packaging/libFuzzer.spec
scripts/coverage-report-dump.py [new file with mode: 0755]

index 3abf96d3e4b5c7cbe820459e3c71a47b77c42a09..3a897756e36c21ad8cd71e4b11c88ba64f4bf154 100644 (file)
@@ -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 (executable)
index 0000000..3e12e39
--- /dev/null
@@ -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 = """
+<html>
+<head>
+  <title>Coverage Report</title>
+  <style>
+    .lz { color: lightgray; }
+  </style>
+</head>
+<body>
+    <table>
+      <tr><th>File</th><th>Coverage</th></tr>
+      <tr><td><em>Files with 0 coverage are not shown.</em></td></tr>
+$filenames
+    </table>
+</body>
+</html>
+"""
+
+CONTENT_PAGE_TMPL = """
+<html>
+<head>
+  <title>$path</title>
+  <style>
+    .covered { background: lightgreen; }
+    .not-covered { background: lightcoral; }
+    .partially-covered { background: navajowhite; }
+    .lz { color: lightgray; }
+  </style>
+</head>
+<body>
+<pre>
+$content
+</pre>
+</body>
+</html>
+"""
+
+
+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 = '<span class="lz">{0}</span>'.format(zeroes)
+    return zeroes + pct_str
+
+
+class DumpCov():
+    def DumpCov(self):
+        pass
+
+    html_escape_tbl = {
+        "&": "&amp;",
+        '"': "&quot;",
+        "'": "&apos;",
+        ">": "&gt;",
+        "<": "&lt;"
+        }
+
+    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(
+                ["<span class='{cls}'>{line}&nbsp;</span>".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(
+                "<tr><td><a href=\"./{name}.html\">{name}</a></td>"
+                "<td>{coverage}%</td></tr>".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()