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 74class SymcovData: 75 def __init__(self, symcov_json): 76 self.covered_points = frozenset(symcov_json['covered-points']) 77 self.point_symbol_info = symcov_json['point-symbol-info'] 78 self.file_coverage = self.compute_filecoverage() 79 80 def filenames(self): 81 return self.point_symbol_info.keys() 82 83 def has_file(self, filename): 84 return filename in self.point_symbol_info 85 86 def compute_linemap(self, filename): 87 """Build a line_number->css_class map.""" 88 points = self.point_symbol_info.get(filename, dict()) 89 90 line_to_points = dict() 91 for fn, points in points.items(): 92 for point, loc in points.items(): 93 line = int(loc.split(":")[0]) 94 line_to_points.setdefault(line, []).append(point) 95 96 result = dict() 97 for line, points in line_to_points.items(): 98 status = "covered" 99 covered_points = self.covered_points & set(points) 100 if not len(covered_points): 101 status = "not-covered" 102 elif len(covered_points) != len(points): 103 status = "partially-covered" 104 result[line] = status 105 return result 106 107 def compute_filecoverage(self): 108 """Build a filename->pct coverage.""" 109 result = dict() 110 for filename, fns in self.point_symbol_info.items(): 111 file_points = [] 112 for fn, points in fns.items(): 113 file_points.extend(points.keys()) 114 covered_points = self.covered_points & set(file_points) 115 result[filename] = int(math.ceil( 116 len(covered_points) * 100 / len(file_points))) 117 return result 118 119 120def format_pct(pct): 121 pct_str = str(max(0, min(100, pct))) 122 zeroes = '0' * (3 - len(pct_str)) 123 if zeroes: 124 zeroes = '<span class="lz">{0}</span>'.format(zeroes) 125 return zeroes + pct_str 126 127class ServerHandler(http.server.BaseHTTPRequestHandler): 128 symcov_data = None 129 src_path = None 130 131 def do_GET(self): 132 norm_path = os.path.normpath(urllib.parse.unquote(self.path[1:])) 133 if self.path == '/': 134 self.send_response(200) 135 self.send_header("Content-type", "text/html; charset=utf-8") 136 self.end_headers() 137 138 filelist = [] 139 for filename in sorted(self.symcov_data.filenames()): 140 file_coverage = self.symcov_data.file_coverage[filename] 141 if not file_coverage: 142 continue 143 filelist.append( 144 "<tr><td><a href=\"./{name}\">{name}</a></td>" 145 "<td>{coverage}%</td></tr>".format( 146 name=html.escape(filename, quote=True), 147 coverage=format_pct(file_coverage))) 148 149 response = string.Template(INDEX_PAGE_TMPL).safe_substitute( 150 filenames='\n'.join(filelist)) 151 self.wfile.write(response.encode('UTF-8', 'replace')) 152 elif self.symcov_data.has_file(norm_path): 153 filename = norm_path 154 filepath = os.path.join(self.src_path, filename) 155 if not os.path.exists(filepath): 156 self.send_response(404) 157 self.end_headers() 158 return 159 160 self.send_response(200) 161 self.send_header("Content-type", "text/html; charset=utf-8") 162 self.end_headers() 163 164 linemap = self.symcov_data.compute_linemap(filename) 165 166 with open(filepath, 'r', encoding='utf8') as f: 167 content = "\n".join( 168 ["<span class='{cls}'>{line} </span>".format( 169 line=html.escape(line.rstrip()), 170 cls=linemap.get(line_no, "")) 171 for line_no, line in enumerate(f, start=1)]) 172 173 response = string.Template(CONTENT_PAGE_TMPL).safe_substitute( 174 path=self.path[1:], 175 content=content) 176 177 self.wfile.write(response.encode('UTF-8', 'replace')) 178 else: 179 self.send_response(404) 180 self.end_headers() 181 182 183def main(): 184 parser = argparse.ArgumentParser(description="symcov report http server.") 185 parser.add_argument('--host', default='127.0.0.1') 186 parser.add_argument('--port', default=8001) 187 parser.add_argument('--symcov', required=True, type=argparse.FileType('r')) 188 parser.add_argument('--srcpath', required=True) 189 args = parser.parse_args() 190 191 print("Loading coverage...") 192 symcov_json = json.load(args.symcov) 193 ServerHandler.symcov_data = SymcovData(symcov_json) 194 ServerHandler.src_path = args.srcpath 195 196 socketserver.TCPServer.allow_reuse_address = True 197 httpd = socketserver.TCPServer((args.host, args.port), ServerHandler) 198 print("Serving at {host}:{port}".format(host=args.host, port=args.port)) 199 try: 200 httpd.serve_forever() 201 except KeyboardInterrupt: 202 pass 203 httpd.server_close() 204 205if __name__ == '__main__': 206 main() 207