1#!/usr/bin/env python3 2# ===- symcov-report-server.py - Coverage Reports HTTP Serve --*- python -*--===# 3# 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 7# 8# ===------------------------------------------------------------------------===# 9"""(EXPERIMENTAL) HTTP server to browse coverage reports from .symcov files. 10 11Coverage reports for big binaries are too huge, generating them statically 12makes no sense. Start the server and go to localhost:8001 instead. 13 14Usage: 15 ./tools/sancov/symcov-report-server.py \ 16 --symcov coverage_data.symcov \ 17 --srcpath root_src_dir 18 19Other 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""" 23 24from __future__ import print_function 25 26import argparse 27import http.server 28import json 29import socketserver 30import time 31import html 32import os 33import string 34import math 35import urllib 36 37INDEX_PAGE_TMPL = """ 38<html> 39<head> 40 <title>Coverage Report</title> 41 <style> 42 .lz { color: lightgray; } 43 </style> 44</head> 45<body> 46 <table> 47 <tr><th>File</th><th>Coverage</th></tr> 48 <tr><td><em>Files with 0 coverage are not shown.</em></td></tr> 49$filenames 50 </table> 51</body> 52</html> 53""" 54 55CONTENT_PAGE_TMPL = """ 56<html> 57<head> 58 <title>$path</title> 59 <style> 60 .covered { background: lightgreen; } 61 .not-covered { background: lightcoral; } 62 .partially-covered { background: navajowhite; } 63 .lz { color: lightgray; } 64 </style> 65</head> 66<body> 67<pre> 68$content 69</pre> 70</body> 71</html> 72""" 73 74FILE_URI_PREFIX = "/file/" 75 76 77class SymcovData: 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() 82 83 def filenames(self): 84 return self.point_symbol_info.keys() 85 86 def has_file(self, filename): 87 return filename in self.point_symbol_info 88 89 def compute_linemap(self, filename): 90 """Build a line_number->css_class map.""" 91 points = self.point_symbol_info.get(filename, dict()) 92 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) 98 99 result = dict() 100 for line, points in line_to_points.items(): 101 status = "covered" 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 108 return result 109 110 def compute_filecoverage(self): 111 """Build a filename->pct coverage.""" 112 result = dict() 113 for filename, fns in self.point_symbol_info.items(): 114 file_points = [] 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)) 120 ) 121 return result 122 123 124def format_pct(pct): 125 pct_str = str(max(0, min(100, pct))) 126 zeroes = "0" * (3 - len(pct_str)) 127 if zeroes: 128 zeroes = '<span class="lz">{0}</span>'.format(zeroes) 129 return zeroes + pct_str 130 131 132class ServerHandler(http.server.BaseHTTPRequestHandler): 133 symcov_data = None 134 src_path = None 135 136 def do_GET(self): 137 norm_path = os.path.normpath( 138 urllib.parse.unquote(self.path[len(FILE_URI_PREFIX) :]) 139 ) 140 if self.path == "/": 141 self.send_response(200) 142 self.send_header("Content-type", "text/html; charset=utf-8") 143 self.end_headers() 144 145 filelist = [] 146 for filename in sorted(self.symcov_data.filenames()): 147 file_coverage = self.symcov_data.file_coverage[filename] 148 if not file_coverage: 149 continue 150 filelist.append( 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), 156 ) 157 ) 158 159 response = string.Template(INDEX_PAGE_TMPL).safe_substitute( 160 filenames="\n".join(filelist) 161 ) 162 self.wfile.write(response.encode("UTF-8", "replace")) 163 elif self.symcov_data.has_file(norm_path): 164 filename = norm_path 165 filepath = os.path.join(self.src_path, filename) 166 if not os.path.exists(filepath): 167 self.send_response(404) 168 self.end_headers() 169 return 170 171 self.send_response(200) 172 self.send_header("Content-type", "text/html; charset=utf-8") 173 self.end_headers() 174 175 linemap = self.symcov_data.compute_linemap(filename) 176 177 with open(filepath, "r", encoding="utf8") as f: 178 content = "\n".join( 179 [ 180 "<span class='{cls}'>{line} </span>".format( 181 line=html.escape(line.rstrip()), 182 cls=linemap.get(line_no, ""), 183 ) 184 for line_no, line in enumerate(f, start=1) 185 ] 186 ) 187 188 response = string.Template(CONTENT_PAGE_TMPL).safe_substitute( 189 path=self.path[1:], content=content 190 ) 191 192 self.wfile.write(response.encode("UTF-8", "replace")) 193 else: 194 self.send_response(404) 195 self.end_headers() 196 197 198def main(): 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() 205 206 print("Loading coverage...") 207 symcov_json = json.load(args.symcov) 208 ServerHandler.symcov_data = SymcovData(symcov_json) 209 ServerHandler.src_path = args.srcpath 210 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)) 214 try: 215 httpd.serve_forever() 216 except KeyboardInterrupt: 217 pass 218 httpd.server_close() 219 220 221if __name__ == "__main__": 222 main() 223