coverage-report-server.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. #!/usr/bin/env python3
  2. #===- symcov-report-server.py - Coverage Reports HTTP Serve --*- python -*--===#
  3. #
  4. # The LLVM Compiler Infrastructure
  5. #
  6. # This file is distributed under the University of Illinois Open Source
  7. # License. See LICENSE.TXT for details.
  8. #
  9. #===------------------------------------------------------------------------===#
  10. '''(EXPERIMENTAL) HTTP server to browse coverage reports from .symcov files.
  11. Coverage reports for big binaries are too huge, generating them statically
  12. makes no sense. Start the server and go to localhost:8001 instead.
  13. Usage:
  14. ./tools/sancov/symcov-report-server.py \
  15. --symcov coverage_data.symcov \
  16. --srcpath root_src_dir
  17. Other options:
  18. --port port_number - specifies the port to use (8001)
  19. --host host_name - host name to bind server to (127.0.0.1)
  20. '''
  21. from __future__ import print_function
  22. import argparse
  23. import http.server
  24. import json
  25. import socketserver
  26. import time
  27. import html
  28. import os
  29. import string
  30. import math
  31. INDEX_PAGE_TMPL = """
  32. <html>
  33. <head>
  34. <title>Coverage Report</title>
  35. <style>
  36. .lz { color: lightgray; }
  37. </style>
  38. </head>
  39. <body>
  40. <table>
  41. <tr><th>File</th><th>Coverage</th></tr>
  42. <tr><td><em>Files with 0 coverage are not shown.</em></td></tr>
  43. $filenames
  44. </table>
  45. </body>
  46. </html>
  47. """
  48. CONTENT_PAGE_TMPL = """
  49. <html>
  50. <head>
  51. <title>$path</title>
  52. <style>
  53. .covered { background: lightgreen; }
  54. .not-covered { background: lightcoral; }
  55. .partially-covered { background: navajowhite; }
  56. .lz { color: lightgray; }
  57. </style>
  58. </head>
  59. <body>
  60. <pre>
  61. $content
  62. </pre>
  63. </body>
  64. </html>
  65. """
  66. class SymcovData:
  67. def __init__(self, symcov_json):
  68. self.covered_points = frozenset(symcov_json['covered-points'])
  69. self.point_symbol_info = symcov_json['point-symbol-info']
  70. self.file_coverage = self.compute_filecoverage()
  71. def filenames(self):
  72. return self.point_symbol_info.keys()
  73. def has_file(self, filename):
  74. return filename in self.point_symbol_info
  75. def compute_linemap(self, filename):
  76. """Build a line_number->css_class map."""
  77. points = self.point_symbol_info.get(filename, dict())
  78. line_to_points = dict()
  79. for fn, points in points.items():
  80. for point, loc in points.items():
  81. line = int(loc.split(":")[0])
  82. line_to_points.setdefault(line, []).append(point)
  83. result = dict()
  84. for line, points in line_to_points.items():
  85. status = "covered"
  86. covered_points = self.covered_points & set(points)
  87. if not len(covered_points):
  88. status = "not-covered"
  89. elif len(covered_points) != len(points):
  90. status = "partially-covered"
  91. result[line] = status
  92. return result
  93. def compute_filecoverage(self):
  94. """Build a filename->pct coverage."""
  95. result = dict()
  96. for filename, fns in self.point_symbol_info.items():
  97. file_points = []
  98. for fn, points in fns.items():
  99. file_points.extend(points.keys())
  100. covered_points = self.covered_points & set(file_points)
  101. result[filename] = int(math.ceil(
  102. len(covered_points) * 100 / len(file_points)))
  103. return result
  104. def format_pct(pct):
  105. pct_str = str(max(0, min(100, pct)))
  106. zeroes = '0' * (3 - len(pct_str))
  107. if zeroes:
  108. zeroes = '<span class="lz">{0}</span>'.format(zeroes)
  109. return zeroes + pct_str
  110. class ServerHandler(http.server.BaseHTTPRequestHandler):
  111. symcov_data = None
  112. src_path = None
  113. def do_GET(self):
  114. if self.path == '/':
  115. self.send_response(200)
  116. self.send_header("Content-type", "text/html; charset=utf-8")
  117. self.end_headers()
  118. filelist = []
  119. for filename in sorted(self.symcov_data.filenames()):
  120. file_coverage = self.symcov_data.file_coverage[filename]
  121. if not file_coverage:
  122. continue
  123. filelist.append(
  124. "<tr><td><a href=\"./{name}\">{name}</a></td>"
  125. "<td>{coverage}%</td></tr>".format(
  126. name=html.escape(filename, quote=True),
  127. coverage=format_pct(file_coverage)))
  128. response = string.Template(INDEX_PAGE_TMPL).safe_substitute(
  129. filenames='\n'.join(filelist))
  130. self.wfile.write(response.encode('UTF-8', 'replace'))
  131. elif self.symcov_data.has_file(self.path[1:]):
  132. filename = self.path[1:]
  133. filepath = os.path.join(self.src_path, filename)
  134. if not os.path.exists(filepath):
  135. self.send_response(404)
  136. self.end_headers()
  137. return
  138. self.send_response(200)
  139. self.send_header("Content-type", "text/html; charset=utf-8")
  140. self.end_headers()
  141. linemap = self.symcov_data.compute_linemap(filename)
  142. with open(filepath, 'r', encoding='utf8') as f:
  143. content = "\n".join(
  144. ["<span class='{cls}'>{line}&nbsp;</span>".format(
  145. line=html.escape(line.rstrip()),
  146. cls=linemap.get(line_no, ""))
  147. for line_no, line in enumerate(f, start=1)])
  148. response = string.Template(CONTENT_PAGE_TMPL).safe_substitute(
  149. path=self.path[1:],
  150. content=content)
  151. self.wfile.write(response.encode('UTF-8', 'replace'))
  152. else:
  153. self.send_response(404)
  154. self.end_headers()
  155. def main():
  156. parser = argparse.ArgumentParser(description="symcov report http server.")
  157. parser.add_argument('--host', default='127.0.0.1')
  158. parser.add_argument('--port', default=8001)
  159. parser.add_argument('--symcov', required=True, type=argparse.FileType('r'))
  160. parser.add_argument('--srcpath', required=True)
  161. args = parser.parse_args()
  162. print("Loading coverage...")
  163. symcov_json = json.load(args.symcov)
  164. ServerHandler.symcov_data = SymcovData(symcov_json)
  165. ServerHandler.src_path = args.srcpath
  166. socketserver.TCPServer.allow_reuse_address = True
  167. httpd = socketserver.TCPServer((args.host, args.port), ServerHandler)
  168. print("Serving at {host}:{port}".format(host=args.host, port=args.port))
  169. try:
  170. httpd.serve_forever()
  171. except KeyboardInterrupt:
  172. pass
  173. httpd.server_close()
  174. if __name__ == '__main__':
  175. main()