#!/usr/bin/env python3 #===- symcov-report-server.py - Coverage Reports HTTP Serve --*- python -*--===# # # The LLVM Compiler Infrastructure # # This file is distributed under the University of Illinois Open Source # License. See LICENSE.TXT for details. # #===------------------------------------------------------------------------===# '''(EXPERIMENTAL) HTTP server to browse coverage reports from .symcov files. Coverage reports for big binaries are too huge, generating them statically makes no sense. Start the server and go to localhost:8001 instead. Usage: ./tools/sancov/symcov-report-server.py \ --symcov coverage_data.symcov \ --srcpath root_src_dir Other options: --port port_number - specifies the port to use (8001) --host host_name - host name to bind server to (127.0.0.1) ''' import argparse import http.server import json import socketserver import time import html import os import string import math INDEX_PAGE_TMPL = """ <html> <head> <title>Coverage Report</title> <style> .lz { color: lightgray; } </style> </head> <body> <table> <tr><th>File</th><th>Coverage</th></tr> <tr><td><em>Files with 0 coverage are not shown.</em></td></tr> $filenames </table> </body> </html> """ CONTENT_PAGE_TMPL = """ <html> <head> <title>$path</title> <style> .covered { background: lightgreen; } .not-covered { background: lightcoral; } .partially-covered { background: navajowhite; } .lz { color: lightgray; } </style> </head> <body> <pre> $content </pre> </body> </html> """ class SymcovData: def __init__(self, symcov_json): self.covered_points = frozenset(symcov_json['covered-points']) self.point_symbol_info = symcov_json['point-symbol-info'] self.file_coverage = self.compute_filecoverage() def filenames(self): return self.point_symbol_info.keys() def has_file(self, filename): return filename in self.point_symbol_info def compute_linemap(self, filename): """Build a line_number->css_class map.""" points = self.point_symbol_info.get(filename, dict()) line_to_points = dict() for fn, points in points.items(): for point, loc in points.items(): line = int(loc.split(":")[0]) line_to_points.setdefault(line, []).append(point) result = dict() for line, points in line_to_points.items(): status = "covered" covered_points = self.covered_points & set(points) if not len(covered_points): status = "not-covered" elif len(covered_points) != len(points): status = "partially-covered" result[line] = status return result def compute_filecoverage(self): """Build a filename->pct coverage.""" result = dict() for filename, fns in self.point_symbol_info.items(): file_points = [] for fn, points in fns.items(): file_points.extend(points.keys()) covered_points = self.covered_points & set(file_points) result[filename] = int(math.ceil( len(covered_points) * 100 / len(file_points))) return result def format_pct(pct): pct_str = str(max(0, min(100, pct))) zeroes = '0' * (3 - len(pct_str)) if zeroes: zeroes = '<span class="lz">{0}</span>'.format(zeroes) return zeroes + pct_str class ServerHandler(http.server.BaseHTTPRequestHandler): symcov_data = None src_path = None def do_GET(self): if self.path == '/': self.send_response(200) self.send_header("Content-type", "text/html; charset=utf-8") self.end_headers() filelist = [] for filename in sorted(self.symcov_data.filenames()): file_coverage = self.symcov_data.file_coverage[filename] if not file_coverage: continue filelist.append( "<tr><td><a href=\"./{name}\">{name}</a></td>" "<td>{coverage}%</td></tr>".format( name=html.escape(filename, quote=True), coverage=format_pct(file_coverage))) response = string.Template(INDEX_PAGE_TMPL).safe_substitute( filenames='\n'.join(filelist)) self.wfile.write(response.encode('UTF-8', 'replace')) elif self.symcov_data.has_file(self.path[1:]): filename = self.path[1:] filepath = os.path.join(self.src_path, filename) if not os.path.exists(filepath): self.send_response(404) self.end_headers() return self.send_response(200) self.send_header("Content-type", "text/html; charset=utf-8") self.end_headers() linemap = self.symcov_data.compute_linemap(filename) with open(filepath, 'r', encoding='utf8') as f: content = "\n".join( ["<span class='{cls}'>{line} </span>".format( line=html.escape(line.rstrip()), cls=linemap.get(line_no, "")) for line_no, line in enumerate(f, start=1)]) response = string.Template(CONTENT_PAGE_TMPL).safe_substitute( path=self.path[1:], content=content) self.wfile.write(response.encode('UTF-8', 'replace')) else: self.send_response(404) self.end_headers() def main(): parser = argparse.ArgumentParser(description="symcov report http server.") parser.add_argument('--host', default='127.0.0.1') parser.add_argument('--port', default=8001) parser.add_argument('--symcov', required=True, type=argparse.FileType('r')) parser.add_argument('--srcpath', required=True) args = parser.parse_args() print("Loading coverage...") symcov_json = json.load(args.symcov) ServerHandler.symcov_data = SymcovData(symcov_json) ServerHandler.src_path = args.srcpath socketserver.TCPServer.allow_reuse_address = True httpd = socketserver.TCPServer((args.host, args.port), ServerHandler) print("Serving at {host}:{port}".format(host=args.host, port=args.port)) try: httpd.serve_forever() except KeyboardInterrupt: pass httpd.server_close() if __name__ == '__main__': main()