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
39 <title>Coverage Report</title>
41 .lz { color: lightgray; }
46 <tr><th>File</th><th>Coverage</th></tr>
47 <tr><td><em>Files with 0 coverage are not shown.</em></td></tr>
54 CONTENT_PAGE_TMPL
= """
59 .covered { background: lightgreen; }
60 .not-covered { background: lightcoral; }
61 .partially-covered { background: navajowhite; }
62 .lz { color: lightgray; }
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()
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
)
96 for line
, points
in line_to_points
.items():
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
106 def compute_filecoverage(self
):
107 """Build a filename->pct coverage."""
109 for filename
, fns
in self
.point_symbol_info
.items():
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
)))
120 pct_str
= str(max(0, min(100, pct
)))
121 zeroes
= '0' * (3 - len(pct_str
))
123 zeroes
= '<span class="lz">{0}</span>'.format(zeroes
)
124 return zeroes
+ pct_str
126 class ServerHandler(http
.server
.BaseHTTPRequestHandler
):
132 self
.send_response(200)
133 self
.send_header("Content-type", "text/html; charset=utf-8")
137 for filename
in sorted(self
.symcov_data
.filenames()):
138 file_coverage
= self
.symcov_data
.file_coverage
[filename
]
139 if not file_coverage
:
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)
158 self
.send_response(200)
159 self
.send_header("Content-type", "text/html; charset=utf-8")
162 linemap
= self
.symcov_data
.compute_linemap(filename
)
164 with
open(filepath
, 'r', encoding
='utf8') as f
:
166 ["<span class='{cls}'>{line} </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(
175 self
.wfile
.write(response
.encode('UTF-8', 'replace'))
177 self
.send_response(404)
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
))
198 httpd
.serve_forever()
199 except KeyboardInterrupt:
203 if __name__
== '__main__':