|  | #!/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) | 
|  | ''' | 
|  |  | 
|  | from __future__ import print_function | 
|  |  | 
|  | 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() |