eric7/DebugClients/Python/coverage/html.py

branch
eric7
changeset 8775
0802ae193343
parent 8527
2bd1325d727e
child 8929
fcca2fa618bf
equal deleted inserted replaced
8774:d728227e8ebb 8775:0802ae193343
6 import datetime 6 import datetime
7 import json 7 import json
8 import os 8 import os
9 import re 9 import re
10 import shutil 10 import shutil
11 import types
11 12
12 import coverage 13 import coverage
13 from coverage import env
14 from coverage.backward import iitems, SimpleNamespace, format_local_datetime
15 from coverage.data import add_data_to_hash 14 from coverage.data import add_data_to_hash
15 from coverage.exceptions import CoverageException
16 from coverage.files import flat_rootname 16 from coverage.files import flat_rootname
17 from coverage.misc import CoverageException, ensure_dir, file_be_gone, Hasher, isolate_module 17 from coverage.misc import ensure_dir, file_be_gone, Hasher, isolate_module, format_local_datetime
18 from coverage.misc import human_sorted
18 from coverage.report import get_analysis_to_report 19 from coverage.report import get_analysis_to_report
19 from coverage.results import Numbers 20 from coverage.results import Numbers
20 from coverage.templite import Templite 21 from coverage.templite import Templite
21 22
22 os = isolate_module(os) 23 os = isolate_module(os)
23 24
24 25
25 # Static files are looked for in a list of places. 26 def data_filename(fname):
26 STATIC_PATH = [ 27 """Return the path to an "htmlfiles" data file of ours.
27 # The place Debian puts system Javascript libraries.
28 "/usr/share/javascript",
29
30 # Our htmlfiles directory.
31 os.path.join(os.path.dirname(__file__), "htmlfiles"),
32 ]
33
34
35 def data_filename(fname, pkgdir=""):
36 """Return the path to a data file of ours.
37
38 The file is searched for on `STATIC_PATH`, and the first place it's found,
39 is returned.
40
41 Each directory in `STATIC_PATH` is searched as-is, and also, if `pkgdir`
42 is provided, at that sub-directory.
43
44 """ 28 """
45 tried = [] 29 static_dir = os.path.join(os.path.dirname(__file__), "htmlfiles")
46 for static_dir in STATIC_PATH: 30 static_filename = os.path.join(static_dir, fname)
47 static_filename = os.path.join(static_dir, fname) 31 return static_filename
48 if os.path.exists(static_filename):
49 return static_filename
50 else:
51 tried.append(static_filename)
52 if pkgdir:
53 static_filename = os.path.join(static_dir, pkgdir, fname)
54 if os.path.exists(static_filename):
55 return static_filename
56 else:
57 tried.append(static_filename)
58 raise CoverageException(
59 "Couldn't find static file %r from %r, tried: %r" % (fname, os.getcwd(), tried)
60 )
61 32
62 33
63 def read_data(fname): 34 def read_data(fname):
64 """Return the contents of a data file of ours.""" 35 """Return the contents of a data file of ours."""
65 with open(data_filename(fname)) as data_file: 36 with open(data_filename(fname)) as data_file:
71 html = re.sub(r"(\A\s+)|(\s+$)", "", html, flags=re.MULTILINE) + "\n" 42 html = re.sub(r"(\A\s+)|(\s+$)", "", html, flags=re.MULTILINE) + "\n"
72 with open(fname, "wb") as fout: 43 with open(fname, "wb") as fout:
73 fout.write(html.encode('ascii', 'xmlcharrefreplace')) 44 fout.write(html.encode('ascii', 'xmlcharrefreplace'))
74 45
75 46
76 class HtmlDataGeneration(object): 47 class HtmlDataGeneration:
77 """Generate structured data to be turned into HTML reports.""" 48 """Generate structured data to be turned into HTML reports."""
78 49
79 EMPTY = "(empty)" 50 EMPTY = "(empty)"
80 51
81 def __init__(self, cov): 52 def __init__(self, cov):
121 category = 'run' 92 category = 'run'
122 93
123 contexts = contexts_label = None 94 contexts = contexts_label = None
124 context_list = None 95 context_list = None
125 if category and self.config.show_contexts: 96 if category and self.config.show_contexts:
126 contexts = sorted(c or self.EMPTY for c in contexts_by_lineno[lineno]) 97 contexts = human_sorted(c or self.EMPTY for c in contexts_by_lineno.get(lineno, ()))
127 if contexts == [self.EMPTY]: 98 if contexts == [self.EMPTY]:
128 contexts_label = self.EMPTY 99 contexts_label = self.EMPTY
129 else: 100 else:
130 contexts_label = "{} ctx".format(len(contexts)) 101 contexts_label = f"{len(contexts)} ctx"
131 context_list = contexts 102 context_list = contexts
132 103
133 lines.append(SimpleNamespace( 104 lines.append(types.SimpleNamespace(
134 tokens=tokens, 105 tokens=tokens,
135 number=lineno, 106 number=lineno,
136 category=category, 107 category=category,
137 statement=(lineno in analysis.statements), 108 statement=(lineno in analysis.statements),
138 contexts=contexts, 109 contexts=contexts,
140 context_list=context_list, 111 context_list=context_list,
141 short_annotations=short_annotations, 112 short_annotations=short_annotations,
142 long_annotations=long_annotations, 113 long_annotations=long_annotations,
143 )) 114 ))
144 115
145 file_data = SimpleNamespace( 116 file_data = types.SimpleNamespace(
146 relative_filename=fr.relative_filename(), 117 relative_filename=fr.relative_filename(),
147 nums=analysis.numbers, 118 nums=analysis.numbers,
148 lines=lines, 119 lines=lines,
149 ) 120 )
150 121
151 return file_data 122 return file_data
152 123
153 124
154 class HtmlReporter(object): 125 class HtmlReporter:
155 """HTML reporting.""" 126 """HTML reporting."""
156 127
157 # These files will be copied from the htmlfiles directory to the output 128 # These files will be copied from the htmlfiles directory to the output
158 # directory. 129 # directory.
159 STATIC_FILES = [ 130 STATIC_FILES = [
160 ("style.css", ""), 131 "style.css",
161 ("jquery.min.js", "jquery"), 132 "coverage_html.js",
162 ("jquery.ba-throttle-debounce.min.js", "jquery-throttle-debounce"), 133 "keybd_closed.png",
163 ("jquery.hotkeys.js", "jquery-hotkeys"), 134 "keybd_open.png",
164 ("jquery.isonscreen.js", "jquery-isonscreen"), 135 "favicon_32.png",
165 ("jquery.tablesorter.min.js", "jquery-tablesorter"),
166 ("coverage_html.js", ""),
167 ("keybd_closed.png", ""),
168 ("keybd_open.png", ""),
169 ("favicon_32.png", ""),
170 ] 136 ]
171 137
172 def __init__(self, cov): 138 def __init__(self, cov):
173 self.coverage = cov 139 self.coverage = cov
174 self.config = self.coverage.config 140 self.config = self.coverage.config
177 self.skip_covered = self.config.html_skip_covered 143 self.skip_covered = self.config.html_skip_covered
178 if self.skip_covered is None: 144 if self.skip_covered is None:
179 self.skip_covered = self.config.skip_covered 145 self.skip_covered = self.config.skip_covered
180 self.skip_empty = self.config.html_skip_empty 146 self.skip_empty = self.config.html_skip_empty
181 if self.skip_empty is None: 147 if self.skip_empty is None:
182 self.skip_empty= self.config.skip_empty 148 self.skip_empty = self.config.skip_empty
149 self.skipped_covered_count = 0
150 self.skipped_empty_count = 0
183 151
184 title = self.config.html_title 152 title = self.config.html_title
185 if env.PY2:
186 title = title.decode("utf8")
187 153
188 if self.config.extra_css: 154 if self.config.extra_css:
189 self.extra_css = os.path.basename(self.config.extra_css) 155 self.extra_css = os.path.basename(self.config.extra_css)
190 else: 156 else:
191 self.extra_css = None 157 self.extra_css = None
195 161
196 self.file_summaries = [] 162 self.file_summaries = []
197 self.all_files_nums = [] 163 self.all_files_nums = []
198 self.incr = IncrementalChecker(self.directory) 164 self.incr = IncrementalChecker(self.directory)
199 self.datagen = HtmlDataGeneration(self.coverage) 165 self.datagen = HtmlDataGeneration(self.coverage)
200 self.totals = Numbers() 166 self.totals = Numbers(precision=self.config.precision)
201 167
202 self.template_globals = { 168 self.template_globals = {
203 # Functions available in the templates. 169 # Functions available in the templates.
204 'escape': escape, 170 'escape': escape,
205 'pair': pair, 171 'pair': pair,
253 return self.totals.n_statements and self.totals.pc_covered 219 return self.totals.n_statements and self.totals.pc_covered
254 220
255 def make_local_static_report_files(self): 221 def make_local_static_report_files(self):
256 """Make local instances of static files for HTML report.""" 222 """Make local instances of static files for HTML report."""
257 # The files we provide must always be copied. 223 # The files we provide must always be copied.
258 for static, pkgdir in self.STATIC_FILES: 224 for static in self.STATIC_FILES:
259 shutil.copyfile( 225 shutil.copyfile(data_filename(static), os.path.join(self.directory, static))
260 data_filename(static, pkgdir), 226
261 os.path.join(self.directory, static) 227 # .gitignore can't be copied from the source tree because it would
262 ) 228 # prevent the static files from being checked in.
229 with open(os.path.join(self.directory, ".gitignore"), "w") as fgi:
230 fgi.write("# Created by coverage.py\n*\n")
263 231
264 # The user may have extra CSS they want copied. 232 # The user may have extra CSS they want copied.
265 if self.extra_css: 233 if self.extra_css:
266 shutil.copyfile( 234 shutil.copyfile(self.config.extra_css, os.path.join(self.directory, self.extra_css))
267 self.config.extra_css,
268 os.path.join(self.directory, self.extra_css)
269 )
270 235
271 def html_file(self, fr, analysis): 236 def html_file(self, fr, analysis):
272 """Generate an HTML file for one source file.""" 237 """Generate an HTML file for one source file."""
273 rootname = flat_rootname(fr.relative_filename()) 238 rootname = flat_rootname(fr.relative_filename())
274 html_filename = rootname + ".html" 239 html_filename = rootname + ".html"
284 no_missing_lines = (nums.n_missing == 0) 249 no_missing_lines = (nums.n_missing == 0)
285 no_missing_branches = (nums.n_partial_branches == 0) 250 no_missing_branches = (nums.n_partial_branches == 0)
286 if no_missing_lines and no_missing_branches: 251 if no_missing_lines and no_missing_branches:
287 # If there's an existing file, remove it. 252 # If there's an existing file, remove it.
288 file_be_gone(html_path) 253 file_be_gone(html_path)
254 self.skipped_covered_count += 1
289 return 255 return
290 256
291 if self.skip_empty: 257 if self.skip_empty:
292 # Don't report on empty files. 258 # Don't report on empty files.
293 if nums.n_statements == 0: 259 if nums.n_statements == 0:
294 file_be_gone(html_path) 260 file_be_gone(html_path)
261 self.skipped_empty_count += 1
295 return 262 return
296 263
297 # Find out if the file on disk is already correct. 264 # Find out if the file on disk is already correct.
298 if self.incr.can_skip_file(self.data, fr, rootname): 265 if self.incr.can_skip_file(self.data, fr, rootname):
299 self.file_summaries.append(self.incr.index_info(rootname)) 266 self.file_summaries.append(self.incr.index_info(rootname))
308 if tok_type == "ws": 275 if tok_type == "ws":
309 html.append(escape(tok_text)) 276 html.append(escape(tok_text))
310 else: 277 else:
311 tok_html = escape(tok_text) or ' ' 278 tok_html = escape(tok_text) or ' '
312 html.append( 279 html.append(
313 u'<span class="{}">{}</span>'.format(tok_type, tok_html) 280 f'<span class="{tok_type}">{tok_html}</span>'
314 ) 281 )
315 ldata.html = ''.join(html) 282 ldata.html = ''.join(html)
316 283
317 if ldata.short_annotations: 284 if ldata.short_annotations:
318 # 202F is NARROW NO-BREAK SPACE. 285 # 202F is NARROW NO-BREAK SPACE.
319 # 219B is RIGHTWARDS ARROW WITH STROKE. 286 # 219B is RIGHTWARDS ARROW WITH STROKE.
320 ldata.annotate = u",&nbsp;&nbsp; ".join( 287 ldata.annotate = ",&nbsp;&nbsp; ".join(
321 u"{}&#x202F;&#x219B;&#x202F;{}".format(ldata.number, d) 288 f"{ldata.number}&#x202F;&#x219B;&#x202F;{d}"
322 for d in ldata.short_annotations 289 for d in ldata.short_annotations
323 ) 290 )
324 else: 291 else:
325 ldata.annotate = None 292 ldata.annotate = None
326 293
327 if ldata.long_annotations: 294 if ldata.long_annotations:
328 longs = ldata.long_annotations 295 longs = ldata.long_annotations
329 if len(longs) == 1: 296 if len(longs) == 1:
330 ldata.annotate_long = longs[0] 297 ldata.annotate_long = longs[0]
331 else: 298 else:
332 ldata.annotate_long = u"{:d} missed branches: {}".format( 299 ldata.annotate_long = "{:d} missed branches: {}".format(
333 len(longs), 300 len(longs),
334 u", ".join( 301 ", ".join(
335 u"{:d}) {}".format(num, ann_long) 302 f"{num:d}) {ann_long}"
336 for num, ann_long in enumerate(longs, start=1) 303 for num, ann_long in enumerate(longs, start=1)
337 ), 304 ),
338 ) 305 )
339 else: 306 else:
340 ldata.annotate_long = None 307 ldata.annotate_long = None
358 325
359 def index_file(self): 326 def index_file(self):
360 """Write the index.html file for this report.""" 327 """Write the index.html file for this report."""
361 index_tmpl = Templite(read_data("index.html"), self.template_globals) 328 index_tmpl = Templite(read_data("index.html"), self.template_globals)
362 329
330 skipped_covered_msg = skipped_empty_msg = ""
331 if self.skipped_covered_count:
332 msg = "{} {} skipped due to complete coverage."
333 skipped_covered_msg = msg.format(
334 self.skipped_covered_count,
335 "file" if self.skipped_covered_count == 1 else "files",
336 )
337 if self.skipped_empty_count:
338 msg = "{} empty {} skipped."
339 skipped_empty_msg = msg.format(
340 self.skipped_empty_count,
341 "file" if self.skipped_empty_count == 1 else "files",
342 )
343
363 html = index_tmpl.render({ 344 html = index_tmpl.render({
364 'files': self.file_summaries, 345 'files': self.file_summaries,
365 'totals': self.totals, 346 'totals': self.totals,
347 'skipped_covered_msg': skipped_covered_msg,
348 'skipped_empty_msg': skipped_empty_msg,
366 }) 349 })
367 350
368 write_html(os.path.join(self.directory, "index.html"), html) 351 index_file = os.path.join(self.directory, "index.html")
352 write_html(index_file, html)
353 self.coverage._message(f"Wrote HTML report to {index_file}")
369 354
370 # Write the latest hashes for next time. 355 # Write the latest hashes for next time.
371 self.incr.write() 356 self.incr.write()
372 357
373 358
374 class IncrementalChecker(object): 359 class IncrementalChecker:
375 """Logic and data to support incremental reporting.""" 360 """Logic and data to support incremental reporting."""
376 361
377 STATUS_FILE = "status.json" 362 STATUS_FILE = "status.json"
378 STATUS_FORMAT = 2 363 STATUS_FORMAT = 2
379 364
419 usable = False 404 usable = False
420 try: 405 try:
421 status_file = os.path.join(self.directory, self.STATUS_FILE) 406 status_file = os.path.join(self.directory, self.STATUS_FILE)
422 with open(status_file) as fstatus: 407 with open(status_file) as fstatus:
423 status = json.load(fstatus) 408 status = json.load(fstatus)
424 except (IOError, ValueError): 409 except (OSError, ValueError):
425 usable = False 410 usable = False
426 else: 411 else:
427 usable = True 412 usable = True
428 if status['format'] != self.STATUS_FORMAT: 413 if status['format'] != self.STATUS_FORMAT:
429 usable = False 414 usable = False
430 elif status['version'] != coverage.__version__: 415 elif status['version'] != coverage.__version__:
431 usable = False 416 usable = False
432 417
433 if usable: 418 if usable:
434 self.files = {} 419 self.files = {}
435 for filename, fileinfo in iitems(status['files']): 420 for filename, fileinfo in status['files'].items():
436 fileinfo['index']['nums'] = Numbers(*fileinfo['index']['nums']) 421 fileinfo['index']['nums'] = Numbers(*fileinfo['index']['nums'])
437 self.files[filename] = fileinfo 422 self.files[filename] = fileinfo
438 self.globals = status['globals'] 423 self.globals = status['globals']
439 else: 424 else:
440 self.reset() 425 self.reset()
441 426
442 def write(self): 427 def write(self):
443 """Write the current status.""" 428 """Write the current status."""
444 status_file = os.path.join(self.directory, self.STATUS_FILE) 429 status_file = os.path.join(self.directory, self.STATUS_FILE)
445 files = {} 430 files = {}
446 for filename, fileinfo in iitems(self.files): 431 for filename, fileinfo in self.files.items():
447 fileinfo['index']['nums'] = fileinfo['index']['nums'].init_args() 432 fileinfo['index']['nums'] = fileinfo['index']['nums'].init_args()
448 files[filename] = fileinfo 433 files[filename] = fileinfo
449 434
450 status = { 435 status = {
451 'format': self.STATUS_FORMAT, 436 'format': self.STATUS_FORMAT,

eric ide

mercurial