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.
15 ./tools/sancov/symcov-report-server.py \
16 --symcov coverage_data.symcov \
17 --srcpath root_src_dir
20 --port port_number - specifies the port to use (8001)
21 --host host_name - host name to bind server to (127.0.0.1)
24 from __future__
import print_function
40 <title>Coverage Report</title>
42 .lz { color: lightgray; }
47 <tr><th>File</th><th>Coverage</th></tr>
48 <tr><td><em>Files with 0 coverage are not shown.</em></td></tr>
55 CONTENT_PAGE_TMPL
= """
60 .covered { background: lightgreen; }
61 .not-covered { background: lightcoral; }
62 .partially-covered { background: navajowhite; }
63 .lz { color: lightgray; }
74 FILE_URI_PREFIX
= "/file/"
78 def __init__(self
, symcov_json
):
79 self
.covered_points
= frozenset(symcov_json
["covered-points"])
80 self
.point_symbol_info
= symcov_json
["point-symbol-info"]
81 self
.file_coverage
= self
.compute_filecoverage()
84 return self
.point_symbol_info
.keys()
86 def has_file(self
, filename
):
87 return filename
in self
.point_symbol_info
89 def compute_linemap(self
, filename
):
90 """Build a line_number->css_class map."""
91 points
= self
.point_symbol_info
.get(filename
, dict())
93 line_to_points
= dict()
94 for fn
, points
in points
.items():
95 for point
, loc
in points
.items():
96 line
= int(loc
.split(":")[0])
97 line_to_points
.setdefault(line
, []).append(point
)
100 for line
, points
in line_to_points
.items():
102 covered_points
= self
.covered_points
& set(points
)
103 if not len(covered_points
):
104 status
= "not-covered"
105 elif len(covered_points
) != len(points
):
106 status
= "partially-covered"
107 result
[line
] = status
110 def compute_filecoverage(self
):
111 """Build a filename->pct coverage."""
113 for filename
, fns
in self
.point_symbol_info
.items():
115 for fn
, points
in fns
.items():
116 file_points
.extend(points
.keys())
117 covered_points
= self
.covered_points
& set(file_points
)
118 result
[filename
] = int(
119 math
.ceil(len(covered_points
) * 100 / len(file_points
))
125 pct_str
= str(max(0, min(100, pct
)))
126 zeroes
= "0" * (3 - len(pct_str
))
128 zeroes
= '<span class="lz">{0}</span>'.format(zeroes
)
129 return zeroes
+ pct_str
132 class ServerHandler(http
.server
.BaseHTTPRequestHandler
):
137 norm_path
= os
.path
.normpath(
138 urllib
.parse
.unquote(self
.path
[len(FILE_URI_PREFIX
) :])
141 self
.send_response(200)
142 self
.send_header("Content-type", "text/html; charset=utf-8")
146 for filename
in sorted(self
.symcov_data
.filenames()):
147 file_coverage
= self
.symcov_data
.file_coverage
[filename
]
148 if not file_coverage
:
151 '<tr><td><a href="{prefix}{name}">{name}</a></td>'
152 "<td>{coverage}%</td></tr>".format(
153 prefix
=FILE_URI_PREFIX
,
154 name
=html
.escape(filename
, quote
=True),
155 coverage
=format_pct(file_coverage
),
159 response
= string
.Template(INDEX_PAGE_TMPL
).safe_substitute(
160 filenames
="\n".join(filelist
)
162 self
.wfile
.write(response
.encode("UTF-8", "replace"))
163 elif self
.symcov_data
.has_file(norm_path
):
165 filepath
= os
.path
.join(self
.src_path
, filename
)
166 if not os
.path
.exists(filepath
):
167 self
.send_response(404)
171 self
.send_response(200)
172 self
.send_header("Content-type", "text/html; charset=utf-8")
175 linemap
= self
.symcov_data
.compute_linemap(filename
)
177 with
open(filepath
, "r", encoding
="utf8") as f
:
180 "<span class='{cls}'>{line} </span>".format(
181 line
=html
.escape(line
.rstrip()),
182 cls
=linemap
.get(line_no
, ""),
184 for line_no
, line
in enumerate(f
, start
=1)
188 response
= string
.Template(CONTENT_PAGE_TMPL
).safe_substitute(
189 path
=self
.path
[1:], content
=content
192 self
.wfile
.write(response
.encode("UTF-8", "replace"))
194 self
.send_response(404)
199 parser
= argparse
.ArgumentParser(description
="symcov report http server.")
200 parser
.add_argument("--host", default
="127.0.0.1")
201 parser
.add_argument("--port", default
=8001)
202 parser
.add_argument("--symcov", required
=True, type=argparse
.FileType("r"))
203 parser
.add_argument("--srcpath", required
=True)
204 args
= parser
.parse_args()
206 print("Loading coverage...")
207 symcov_json
= json
.load(args
.symcov
)
208 ServerHandler
.symcov_data
= SymcovData(symcov_json
)
209 ServerHandler
.src_path
= args
.srcpath
211 socketserver
.TCPServer
.allow_reuse_address
= True
212 httpd
= socketserver
.TCPServer((args
.host
, args
.port
), ServerHandler
)
213 print("Serving at {host}:{port}".format(host
=args
.host
, port
=args
.port
))
215 httpd
.serve_forever()
216 except KeyboardInterrupt:
221 if __name__
== "__main__":