DebugClients/Python/coverage/html.py

changeset 5051
3586ebd9fac8
parent 4491
0d8612e24fef
equal deleted inserted replaced
5047:04e5dfbd3f3d 5051:3586ebd9fac8
4 """HTML reporting for coverage.py.""" 4 """HTML reporting for coverage.py."""
5 5
6 import datetime 6 import datetime
7 import json 7 import json
8 import os 8 import os
9 import re
10 import shutil 9 import shutil
11 10
12 import coverage 11 import coverage
13 from coverage import env 12 from coverage import env
14 from coverage.backward import iitems 13 from coverage.backward import iitems
15 from coverage.files import flat_rootname 14 from coverage.files import flat_rootname
16 from coverage.misc import CoverageException, Hasher 15 from coverage.misc import CoverageException, Hasher, isolate_module
17 from coverage.report import Reporter 16 from coverage.report import Reporter
18 from coverage.results import Numbers 17 from coverage.results import Numbers
19 from coverage.templite import Templite 18 from coverage.templite import Templite
19
20 os = isolate_module(os)
20 21
21 22
22 # Static files are looked for in a list of places. 23 # Static files are looked for in a list of places.
23 STATIC_PATH = [ 24 STATIC_PATH = [
24 # The place Debian puts system Javascript libraries. 25 # The place Debian puts system Javascript libraries.
55 raise CoverageException( 56 raise CoverageException(
56 "Couldn't find static file %r from %r, tried: %r" % (fname, os.getcwd(), tried) 57 "Couldn't find static file %r from %r, tried: %r" % (fname, os.getcwd(), tried)
57 ) 58 )
58 59
59 60
60 def data(fname): 61 def read_data(fname):
61 """Return the contents of a data file of ours.""" 62 """Return the contents of a data file of ours."""
62 with open(data_filename(fname)) as data_file: 63 with open(data_filename(fname)) as data_file:
63 return data_file.read() 64 return data_file.read()
65
66
67 def write_html(fname, html):
68 """Write `html` to `fname`, properly encoded."""
69 with open(fname, "wb") as fout:
70 fout.write(html.encode('ascii', 'xmlcharrefreplace'))
64 71
65 72
66 class HtmlReporter(Reporter): 73 class HtmlReporter(Reporter):
67 """HTML reporting.""" 74 """HTML reporting."""
68 75
91 'pair': pair, 98 'pair': pair,
92 'title': title, 99 'title': title,
93 '__url__': coverage.__url__, 100 '__url__': coverage.__url__,
94 '__version__': coverage.__version__, 101 '__version__': coverage.__version__,
95 } 102 }
96 self.source_tmpl = Templite( 103 self.source_tmpl = Templite(read_data("pyfile.html"), self.template_globals)
97 data("pyfile.html"), self.template_globals
98 )
99 104
100 self.coverage = cov 105 self.coverage = cov
101 106
102 self.files = [] 107 self.files = []
103 self.has_arcs = self.coverage.data.has_arcs() 108 self.has_arcs = self.coverage.data.has_arcs()
155 shutil.copyfile( 160 shutil.copyfile(
156 self.config.extra_css, 161 self.config.extra_css,
157 os.path.join(self.directory, self.extra_css) 162 os.path.join(self.directory, self.extra_css)
158 ) 163 )
159 164
160 def write_html(self, fname, html):
161 """Write `html` to `fname`, properly encoded."""
162 with open(fname, "wb") as fout:
163 fout.write(html.encode('ascii', 'xmlcharrefreplace'))
164
165 def file_hash(self, source, fr): 165 def file_hash(self, source, fr):
166 """Compute a hash that changes if the file needs to be re-reported.""" 166 """Compute a hash that changes if the file needs to be re-reported."""
167 m = Hasher() 167 m = Hasher()
168 m.update(source) 168 m.update(source)
169 self.coverage.data.add_to_hash(fr.filename, m) 169 self.coverage.data.add_to_hash(fr.filename, m)
187 # Get the numbers for this file. 187 # Get the numbers for this file.
188 nums = analysis.numbers 188 nums = analysis.numbers
189 189
190 if self.has_arcs: 190 if self.has_arcs:
191 missing_branch_arcs = analysis.missing_branch_arcs() 191 missing_branch_arcs = analysis.missing_branch_arcs()
192 arcs_executed = analysis.arcs_executed()
192 193
193 # These classes determine which lines are highlighted by default. 194 # These classes determine which lines are highlighted by default.
194 c_run = "run hide_run" 195 c_run = "run hide_run"
195 c_exc = "exc" 196 c_exc = "exc"
196 c_mis = "mis" 197 c_mis = "mis"
200 201
201 for lineno, line in enumerate(fr.source_token_lines(), start=1): 202 for lineno, line in enumerate(fr.source_token_lines(), start=1):
202 # Figure out how to mark this line. 203 # Figure out how to mark this line.
203 line_class = [] 204 line_class = []
204 annotate_html = "" 205 annotate_html = ""
205 annotate_title = "" 206 annotate_long = ""
206 if lineno in analysis.statements: 207 if lineno in analysis.statements:
207 line_class.append("stm") 208 line_class.append("stm")
208 if lineno in analysis.excluded: 209 if lineno in analysis.excluded:
209 line_class.append(c_exc) 210 line_class.append(c_exc)
210 elif lineno in analysis.missing: 211 elif lineno in analysis.missing:
214 shorts = [] 215 shorts = []
215 longs = [] 216 longs = []
216 for b in missing_branch_arcs[lineno]: 217 for b in missing_branch_arcs[lineno]:
217 if b < 0: 218 if b < 0:
218 shorts.append("exit") 219 shorts.append("exit")
219 longs.append("the function exit")
220 else: 220 else:
221 shorts.append(b) 221 shorts.append(b)
222 longs.append("line %d" % b) 222 longs.append(fr.missing_arc_description(lineno, b, arcs_executed))
223 # 202F is NARROW NO-BREAK SPACE. 223 # 202F is NARROW NO-BREAK SPACE.
224 # 219B is RIGHTWARDS ARROW WITH STROKE. 224 # 219B is RIGHTWARDS ARROW WITH STROKE.
225 short_fmt = "%s&#x202F;&#x219B;&#x202F;%s" 225 short_fmt = "%s&#x202F;&#x219B;&#x202F;%s"
226 annotate_html = ",&nbsp;&nbsp; ".join(short_fmt % (lineno, d) for d in shorts) 226 annotate_html = ",&nbsp;&nbsp; ".join(short_fmt % (lineno, d) for d in shorts)
227 annotate_html += " [?]" 227
228
229 annotate_title = "Line %d was executed, but never jumped to " % lineno
230 if len(longs) == 1: 228 if len(longs) == 1:
231 annotate_title += longs[0] 229 annotate_long = longs[0]
232 elif len(longs) == 2:
233 annotate_title += longs[0] + " or " + longs[1]
234 else: 230 else:
235 annotate_title += ", ".join(longs[:-1]) + ", or " + longs[-1] 231 annotate_long = "%d missed branches: %s" % (
232 len(longs),
233 ", ".join("%d) %s" % (num, ann_long)
234 for num, ann_long in enumerate(longs, start=1)),
235 )
236 elif lineno in analysis.statements: 236 elif lineno in analysis.statements:
237 line_class.append(c_run) 237 line_class.append(c_run)
238 238
239 # Build the HTML for the line. 239 # Build the HTML for the line.
240 html = [] 240 html = []
250 lines.append({ 250 lines.append({
251 'html': ''.join(html), 251 'html': ''.join(html),
252 'number': lineno, 252 'number': lineno,
253 'class': ' '.join(line_class) or "pln", 253 'class': ' '.join(line_class) or "pln",
254 'annotate': annotate_html, 254 'annotate': annotate_html,
255 'annotate_title': annotate_title, 255 'annotate_long': annotate_long,
256 }) 256 })
257 257
258 # Write the HTML page for this file. 258 # Write the HTML page for this file.
259 template_values = { 259 html = self.source_tmpl.render({
260 'c_exc': c_exc, 'c_mis': c_mis, 'c_par': c_par, 'c_run': c_run, 260 'c_exc': c_exc,
261 'has_arcs': self.has_arcs, 'extra_css': self.extra_css, 261 'c_mis': c_mis,
262 'fr': fr, 'nums': nums, 'lines': lines, 262 'c_par': c_par,
263 'c_run': c_run,
264 'has_arcs': self.has_arcs,
265 'extra_css': self.extra_css,
266 'fr': fr,
267 'nums': nums,
268 'lines': lines,
263 'time_stamp': self.time_stamp, 269 'time_stamp': self.time_stamp,
264 } 270 })
265 html = spaceless(self.source_tmpl.render(template_values))
266 271
267 html_filename = rootname + ".html" 272 html_filename = rootname + ".html"
268 html_path = os.path.join(self.directory, html_filename) 273 html_path = os.path.join(self.directory, html_filename)
269 self.write_html(html_path, html) 274 write_html(html_path, html)
270 275
271 # Save this file's information for the index file. 276 # Save this file's information for the index file.
272 index_info = { 277 index_info = {
273 'nums': nums, 278 'nums': nums,
274 'html_filename': html_filename, 279 'html_filename': html_filename,
277 self.files.append(index_info) 282 self.files.append(index_info)
278 self.status.set_index_info(rootname, index_info) 283 self.status.set_index_info(rootname, index_info)
279 284
280 def index_file(self): 285 def index_file(self):
281 """Write the index.html file for this report.""" 286 """Write the index.html file for this report."""
282 index_tmpl = Templite(data("index.html"), self.template_globals) 287 index_tmpl = Templite(read_data("index.html"), self.template_globals)
283 288
284 self.totals = sum(f['nums'] for f in self.files) 289 self.totals = sum(f['nums'] for f in self.files)
285 290
286 html = index_tmpl.render({ 291 html = index_tmpl.render({
287 'has_arcs': self.has_arcs, 292 'has_arcs': self.has_arcs,
289 'files': self.files, 294 'files': self.files,
290 'totals': self.totals, 295 'totals': self.totals,
291 'time_stamp': self.time_stamp, 296 'time_stamp': self.time_stamp,
292 }) 297 })
293 298
294 self.write_html(os.path.join(self.directory, "index.html"), html) 299 write_html(os.path.join(self.directory, "index.html"), html)
295 300
296 # Write the latest hashes for next time. 301 # Write the latest hashes for next time.
297 self.status.write(self.directory) 302 self.status.write(self.directory)
298 303
299 304
414 419
415 420
416 # Helpers for templates and generating HTML 421 # Helpers for templates and generating HTML
417 422
418 def escape(t): 423 def escape(t):
419 """HTML-escape the text in `t`.""" 424 """HTML-escape the text in `t`.
420 return ( 425
421 t 426 This is only suitable for HTML text, not attributes.
422 # Convert HTML special chars into HTML entities.
423 .replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
424 .replace("'", "&#39;").replace('"', "&quot;")
425 # Convert runs of spaces: "......" -> "&nbsp;.&nbsp;.&nbsp;."
426 .replace(" ", "&nbsp; ")
427 # To deal with odd-length runs, convert the final pair of spaces
428 # so that "....." -> "&nbsp;.&nbsp;&nbsp;."
429 .replace(" ", "&nbsp; ")
430 )
431
432
433 def spaceless(html):
434 """Squeeze out some annoying extra space from an HTML string.
435
436 Nicely-formatted templates mean lots of extra space in the result.
437 Get rid of some.
438 427
439 """ 428 """
440 html = re.sub(r">\s+<p ", ">\n<p ", html) 429 # Convert HTML special chars into HTML entities.
441 return html 430 return t.replace("&", "&amp;").replace("<", "&lt;")
442 431
443 432
444 def pair(ratio): 433 def pair(ratio):
445 """Format a pair of numbers so JavaScript can read them in an attribute.""" 434 """Format a pair of numbers so JavaScript can read them in an attribute."""
446 return "%s %s" % ratio 435 return "%s %s" % ratio

eric ide

mercurial