2016-10-04 03:22:05 +08:00
|
|
|
#!/usr/bin/env python3
|
2016-09-29 05:27:58 +08:00
|
|
|
#===- symcov-report-server.py - Coverage Reports HTTP Serve --*- python -*--===#
|
|
|
|
#
|
2019-01-19 16:50:56 +08:00
|
|
|
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
|
|
|
# See https://llvm.org/LICENSE.txt for license information.
|
|
|
|
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
2016-09-29 05:27:58 +08:00
|
|
|
#
|
|
|
|
#===------------------------------------------------------------------------===#
|
|
|
|
'''(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)
|
|
|
|
'''
|
|
|
|
|
2019-01-03 22:11:33 +08:00
|
|
|
from __future__ import print_function
|
|
|
|
|
2016-09-29 05:27:58 +08:00
|
|
|
import argparse
|
|
|
|
import http.server
|
|
|
|
import json
|
|
|
|
import socketserver
|
|
|
|
import time
|
|
|
|
import html
|
|
|
|
import os
|
|
|
|
import string
|
|
|
|
import math
|
2020-05-27 05:27:22 +08:00
|
|
|
import urllib
|
2016-09-29 05:27:58 +08:00
|
|
|
|
|
|
|
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):
|
2020-05-27 05:27:22 +08:00
|
|
|
norm_path = os.path.normpath(urllib.parse.unquote(self.path[1:]))
|
2016-09-29 05:27:58 +08:00
|
|
|
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(
|
2017-03-20 22:06:04 +08:00
|
|
|
"<tr><td><a href=\"./{name}\">{name}</a></td>"
|
2016-09-29 05:27:58 +08:00
|
|
|
"<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'))
|
2020-05-27 05:27:22 +08:00
|
|
|
elif self.symcov_data.has_file(norm_path):
|
|
|
|
filename = norm_path
|
2016-09-29 05:27:58 +08:00
|
|
|
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)
|
|
|
|
|
[sancov] coverage-report-server.py: ServerHandler(): open file as UTF8
Summary:
This is nessesary in Python3. Everywhere else we assume that
encoding is UTF8. If we don't specify it here, the defaults
from the environment will be used, which may result in ASCII
decoder being used. And if the file is non-ASCII, then it
will crash:
```
File "/usr/local/bin/coverage-report-server.py", line 168, in do_GET
for line_no, line in enumerate(f, start=1)])
File "/usr/local/bin/coverage-report-server.py", line 165, in <listcomp>
["<span class='{cls}'>{line} </span>".format(
File "/usr/lib/python3.5/encodings/ascii.py", line 26, in decode
return codecs.ascii_decode(input, self.errors)[0]
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 106: ordinal not in range(128)
```
Fixes https://bugs.llvm.org/show_bug.cgi?id=33548
Now, how would i add a testcase here?
Reviewers: m.ostapenko, kcc
Reviewed By: kcc
Subscribers: kcc, llvm-commits
Tags: #sanitizers
Differential Revision: https://reviews.llvm.org/D37661
llvm-svn: 313063
2017-09-13 02:59:21 +08:00
|
|
|
with open(filepath, 'r', encoding='utf8') as f:
|
2016-09-29 05:27:58 +08:00
|
|
|
content = "\n".join(
|
|
|
|
["<span class='{cls}'>{line} </span>".format(
|
|
|
|
line=html.escape(line.rstrip()),
|
|
|
|
cls=linemap.get(line_no, ""))
|
2017-03-20 22:06:04 +08:00
|
|
|
for line_no, line in enumerate(f, start=1)])
|
2016-09-29 05:27:58 +08:00
|
|
|
|
|
|
|
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()
|