223 lines · python
1#!/usr/bin/env python32# ===- 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-exception7#8# ===------------------------------------------------------------------------===#9"""(EXPERIMENTAL) HTTP server to browse coverage reports from .symcov files.10 11Coverage reports for big binaries are too huge, generating them statically12makes 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_dir18 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_function25 26import argparse27import http.server28import json29import socketserver30import time31import html32import os33import string34import math35import urllib36 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$filenames50 </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$content69</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_info88 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] = status108 return result109 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 result122 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_str130 131 132class ServerHandler(http.server.BaseHTTPRequestHandler):133 symcov_data = None134 src_path = None135 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 continue150 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_path165 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 return170 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=content190 )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.srcpath210 211 socketserver.TCPServer.allow_reuse_address = True212 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 pass218 httpd.server_close()219 220 221if __name__ == "__main__":222 main()223