forked from llvm-mirror/llvm
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[sancov] a simple .symcov coverage report server
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
1 parent
a2d2551
commit 451a26b
Showing
1 changed file
with
203 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} </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() |