Skip to content

Commit

Permalink
[sancov] a simple .symcov coverage report server
Browse files Browse the repository at this point in the history
Coverage reports for gigabyte-sized binaries are huge. There's no
practical reason to generate them statically.

Implementing an experiment http coverage report server. The server
loads .symcov file and serves interactive coverage pages.

git-svn-id: https://llvm.org/svn/llvm-project/llvm/trunk@282637 91177308-0d34-0410-b5e6-96231b3b80d8
  • Loading branch information
aizatsky-chromium committed Sep 28, 2016
1 parent a2d2551 commit 451a26b
Showing 1 changed file with 203 additions and 0 deletions.
203 changes: 203 additions & 0 deletions tools/sancov/symcov-report-server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
#!/usr/bin/python3
#===- symcov-report-server.py - Coverage Reports HTTP Serve --*- python -*--===#
#
# The LLVM Compiler Infrastructure
#
# This file is distributed under the University of Illinois Open Source
# License. See LICENSE.TXT for details.
#
#===------------------------------------------------------------------------===#
'''(EXPERIMENTAL) HTTP server to browse coverage reports from .symcov files.
Coverage reports for big binaries are too huge, generating them statically
makes no sense. Start the server and go to localhost:8001 instead.
Usage:
./tools/sancov/symcov-report-server.py \
--symcov coverage_data.symcov \
--srcpath root_src_dir
Other options:
--port port_number - specifies the port to use (8001)
--host host_name - host name to bind server to (127.0.0.1)
'''

import argparse
import http.server
import json
import socketserver
import time
import html
import os
import string
import math

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 ServerHandler(http.server.BaseHTTPRequestHandler):
symcov_data = None
src_path = None

def do_GET(self):
if self.path == '/':
self.send_response(200)
self.send_header("Content-type", "text/html; charset=utf-8")
self.end_headers()

filelist = []
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}\">{name}</a></td>"
"<td>{coverage}%</td></tr>".format(
name=html.escape(filename, quote=True),
coverage=format_pct(file_coverage)))

response = string.Template(INDEX_PAGE_TMPL).safe_substitute(
filenames='\n'.join(filelist))
self.wfile.write(response.encode('UTF-8', 'replace'))
elif self.symcov_data.has_file(self.path[1:]):
filename = self.path[1:]
filepath = os.path.join(self.src_path, filename)
if not os.path.exists(filepath):
self.send_response(404)
self.end_headers()
return

self.send_response(200)
self.send_header("Content-type", "text/html; charset=utf-8")
self.end_headers()

linemap = self.symcov_data.compute_linemap(filename)

with open(filepath, 'r') as f:
content = "\n".join(
["<span class='{cls}'>{line}&nbsp;</span>".format(
line=html.escape(line.rstrip()),
cls=linemap.get(line_no, ""))
for line_no, line in enumerate(f)])

response = string.Template(CONTENT_PAGE_TMPL).safe_substitute(
path=self.path[1:],
content=content)

self.wfile.write(response.encode('UTF-8', 'replace'))
else:
self.send_response(404)
self.end_headers()


def main():
parser = argparse.ArgumentParser(description="symcov report http server.")
parser.add_argument('--host', default='127.0.0.1')
parser.add_argument('--port', default=8001)
parser.add_argument('--symcov', required=True, type=argparse.FileType('r'))
parser.add_argument('--srcpath', required=True)
args = parser.parse_args()

print("Loading coverage...")
symcov_json = json.load(args.symcov)
ServerHandler.symcov_data = SymcovData(symcov_json)
ServerHandler.src_path = args.srcpath

socketserver.TCPServer.allow_reuse_address = True
httpd = socketserver.TCPServer((args.host, args.port), ServerHandler)
print("Serving at {host}:{port}".format(host=args.host, port=args.port))
try:
httpd.serve_forever()
except KeyboardInterrupt:
pass
httpd.server_close()

if __name__ == '__main__':
main()

0 comments on commit 451a26b

Please sign in to comment.