gn build: Merge r374476
[llvm-complete.git] / tools / sancov / coverage-report-server.py
blob251d8f1b77baca358c17d608b33f7d33ac5471c2
1 #!/usr/bin/env python3
2 #===- symcov-report-server.py - Coverage Reports HTTP Serve --*- python -*--===#
4 # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
5 # See https://llvm.org/LICENSE.txt for license information.
6 # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
8 #===------------------------------------------------------------------------===#
9 '''(EXPERIMENTAL) HTTP server to browse coverage reports from .symcov files.
11 Coverage reports for big binaries are too huge, generating them statically
12 makes no sense. Start the server and go to localhost:8001 instead.
14 Usage:
15 ./tools/sancov/symcov-report-server.py \
16 --symcov coverage_data.symcov \
17 --srcpath root_src_dir
19 Other options:
20 --port port_number - specifies the port to use (8001)
21 --host host_name - host name to bind server to (127.0.0.1)
22 '''
24 from __future__ import print_function
26 import argparse
27 import http.server
28 import json
29 import socketserver
30 import time
31 import html
32 import os
33 import string
34 import math
36 INDEX_PAGE_TMPL = """
37 <html>
38 <head>
39 <title>Coverage Report</title>
40 <style>
41 .lz { color: lightgray; }
42 </style>
43 </head>
44 <body>
45 <table>
46 <tr><th>File</th><th>Coverage</th></tr>
47 <tr><td><em>Files with 0 coverage are not shown.</em></td></tr>
48 $filenames
49 </table>
50 </body>
51 </html>
52 """
54 CONTENT_PAGE_TMPL = """
55 <html>
56 <head>
57 <title>$path</title>
58 <style>
59 .covered { background: lightgreen; }
60 .not-covered { background: lightcoral; }
61 .partially-covered { background: navajowhite; }
62 .lz { color: lightgray; }
63 </style>
64 </head>
65 <body>
66 <pre>
67 $content
68 </pre>
69 </body>
70 </html>
71 """
73 class SymcovData:
74 def __init__(self, symcov_json):
75 self.covered_points = frozenset(symcov_json['covered-points'])
76 self.point_symbol_info = symcov_json['point-symbol-info']
77 self.file_coverage = self.compute_filecoverage()
79 def filenames(self):
80 return self.point_symbol_info.keys()
82 def has_file(self, filename):
83 return filename in self.point_symbol_info
85 def compute_linemap(self, filename):
86 """Build a line_number->css_class map."""
87 points = self.point_symbol_info.get(filename, dict())
89 line_to_points = dict()
90 for fn, points in points.items():
91 for point, loc in points.items():
92 line = int(loc.split(":")[0])
93 line_to_points.setdefault(line, []).append(point)
95 result = dict()
96 for line, points in line_to_points.items():
97 status = "covered"
98 covered_points = self.covered_points & set(points)
99 if not len(covered_points):
100 status = "not-covered"
101 elif len(covered_points) != len(points):
102 status = "partially-covered"
103 result[line] = status
104 return result
106 def compute_filecoverage(self):
107 """Build a filename->pct coverage."""
108 result = dict()
109 for filename, fns in self.point_symbol_info.items():
110 file_points = []
111 for fn, points in fns.items():
112 file_points.extend(points.keys())
113 covered_points = self.covered_points & set(file_points)
114 result[filename] = int(math.ceil(
115 len(covered_points) * 100 / len(file_points)))
116 return result
119 def format_pct(pct):
120 pct_str = str(max(0, min(100, pct)))
121 zeroes = '0' * (3 - len(pct_str))
122 if zeroes:
123 zeroes = '<span class="lz">{0}</span>'.format(zeroes)
124 return zeroes + pct_str
126 class ServerHandler(http.server.BaseHTTPRequestHandler):
127 symcov_data = None
128 src_path = None
130 def do_GET(self):
131 if self.path == '/':
132 self.send_response(200)
133 self.send_header("Content-type", "text/html; charset=utf-8")
134 self.end_headers()
136 filelist = []
137 for filename in sorted(self.symcov_data.filenames()):
138 file_coverage = self.symcov_data.file_coverage[filename]
139 if not file_coverage:
140 continue
141 filelist.append(
142 "<tr><td><a href=\"./{name}\">{name}</a></td>"
143 "<td>{coverage}%</td></tr>".format(
144 name=html.escape(filename, quote=True),
145 coverage=format_pct(file_coverage)))
147 response = string.Template(INDEX_PAGE_TMPL).safe_substitute(
148 filenames='\n'.join(filelist))
149 self.wfile.write(response.encode('UTF-8', 'replace'))
150 elif self.symcov_data.has_file(self.path[1:]):
151 filename = self.path[1:]
152 filepath = os.path.join(self.src_path, filename)
153 if not os.path.exists(filepath):
154 self.send_response(404)
155 self.end_headers()
156 return
158 self.send_response(200)
159 self.send_header("Content-type", "text/html; charset=utf-8")
160 self.end_headers()
162 linemap = self.symcov_data.compute_linemap(filename)
164 with open(filepath, 'r', encoding='utf8') as f:
165 content = "\n".join(
166 ["<span class='{cls}'>{line}&nbsp;</span>".format(
167 line=html.escape(line.rstrip()),
168 cls=linemap.get(line_no, ""))
169 for line_no, line in enumerate(f, start=1)])
171 response = string.Template(CONTENT_PAGE_TMPL).safe_substitute(
172 path=self.path[1:],
173 content=content)
175 self.wfile.write(response.encode('UTF-8', 'replace'))
176 else:
177 self.send_response(404)
178 self.end_headers()
181 def main():
182 parser = argparse.ArgumentParser(description="symcov report http server.")
183 parser.add_argument('--host', default='127.0.0.1')
184 parser.add_argument('--port', default=8001)
185 parser.add_argument('--symcov', required=True, type=argparse.FileType('r'))
186 parser.add_argument('--srcpath', required=True)
187 args = parser.parse_args()
189 print("Loading coverage...")
190 symcov_json = json.load(args.symcov)
191 ServerHandler.symcov_data = SymcovData(symcov_json)
192 ServerHandler.src_path = args.srcpath
194 socketserver.TCPServer.allow_reuse_address = True
195 httpd = socketserver.TCPServer((args.host, args.port), ServerHandler)
196 print("Serving at {host}:{port}".format(host=args.host, port=args.port))
197 try:
198 httpd.serve_forever()
199 except KeyboardInterrupt:
200 pass
201 httpd.server_close()
203 if __name__ == '__main__':
204 main()