--- a/DebugClients/Python/coverage/html.py Sat Oct 10 12:06:10 2015 +0200 +++ b/DebugClients/Python/coverage/html.py Sat Oct 10 12:44:52 2015 +0200 @@ -1,14 +1,22 @@ -"""HTML reporting for Coverage.""" +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt -import os, re, shutil, sys +"""HTML reporting for coverage.py.""" -from . import coverage -from .backward import pickle -from .misc import CoverageException, Hasher -from .phystokens import source_token_lines, source_encoding -from .report import Reporter -from .results import Numbers -from .templite import Templite +import datetime +import json +import os +import re +import shutil + +import coverage +from coverage import env +from coverage.backward import iitems +from coverage.files import flat_rootname +from coverage.misc import CoverageException, Hasher +from coverage.report import Reporter +from coverage.results import Numbers +from coverage.templite import Templite # Static files are looked for in a list of places. @@ -20,6 +28,7 @@ os.path.join(os.path.dirname(__file__), "htmlfiles"), ] + def data_filename(fname, pkgdir=""): """Return the path to a data file of ours. @@ -27,69 +36,80 @@ is returned. Each directory in `STATIC_PATH` is searched as-is, and also, if `pkgdir` - is provided, at that subdirectory. + is provided, at that sub-directory. """ + tried = [] for static_dir in STATIC_PATH: static_filename = os.path.join(static_dir, fname) if os.path.exists(static_filename): return static_filename + else: + tried.append(static_filename) if pkgdir: static_filename = os.path.join(static_dir, pkgdir, fname) if os.path.exists(static_filename): return static_filename - raise CoverageException("Couldn't find static file %r" % fname) + else: + tried.append(static_filename) + raise CoverageException( + "Couldn't find static file %r from %r, tried: %r" % (fname, os.getcwd(), tried) + ) def data(fname): """Return the contents of a data file of ours.""" - data_file = open(data_filename(fname)) - try: + with open(data_filename(fname)) as data_file: return data_file.read() - finally: - data_file.close() class HtmlReporter(Reporter): """HTML reporting.""" - # These files will be copied from the htmlfiles dir to the output dir. + # These files will be copied from the htmlfiles directory to the output + # directory. STATIC_FILES = [ - ("style.css", ""), - ("jquery.min.js", "jquery"), - ("jquery.hotkeys.js", "jquery-hotkeys"), - ("jquery.isonscreen.js", "jquery-isonscreen"), - ("jquery.tablesorter.min.js", "jquery-tablesorter"), - ("coverage_html.js", ""), - ("keybd_closed.png", ""), - ("keybd_open.png", ""), - ] + ("style.css", ""), + ("jquery.min.js", "jquery"), + ("jquery.debounce.min.js", "jquery-debounce"), + ("jquery.hotkeys.js", "jquery-hotkeys"), + ("jquery.isonscreen.js", "jquery-isonscreen"), + ("jquery.tablesorter.min.js", "jquery-tablesorter"), + ("coverage_html.js", ""), + ("keybd_closed.png", ""), + ("keybd_open.png", ""), + ] def __init__(self, cov, config): super(HtmlReporter, self).__init__(cov, config) self.directory = None + title = self.config.html_title + if env.PY2: + title = title.decode("utf8") self.template_globals = { 'escape': escape, - 'title': self.config.html_title, + 'pair': pair, + 'title': title, '__url__': coverage.__url__, '__version__': coverage.__version__, - } + } self.source_tmpl = Templite( data("pyfile.html"), self.template_globals - ) + ) self.coverage = cov self.files = [] - self.arcs = self.coverage.data.has_arcs() + self.has_arcs = self.coverage.data.has_arcs() self.status = HtmlStatus() self.extra_css = None self.totals = Numbers() + self.time_stamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M') def report(self, morfs): """Generate an HTML report for `morfs`. - `morfs` is a list of modules or filenames. + `morfs` is a list of modules or file names. """ assert self.config.html_dir, "must give a directory for html reporting" @@ -100,7 +120,7 @@ # Check that this run used the same settings as the last run. m = Hasher() m.update(self.config) - these_settings = m.digest() + these_settings = m.hexdigest() if self.status.settings_hash() != these_settings: self.status.reset() self.status.set_settings_hash(these_settings) @@ -119,8 +139,7 @@ self.index_file() self.make_local_static_report_files() - - return self.totals.pc_covered + return self.totals.n_statements and self.totals.pc_covered def make_local_static_report_files(self): """Make local instances of static files for HTML report.""" @@ -129,62 +148,46 @@ shutil.copyfile( data_filename(static, pkgdir), os.path.join(self.directory, static) - ) + ) # The user may have extra CSS they want copied. if self.extra_css: shutil.copyfile( self.config.extra_css, os.path.join(self.directory, self.extra_css) - ) + ) def write_html(self, fname, html): """Write `html` to `fname`, properly encoded.""" - fout = open(fname, "wb") - try: + with open(fname, "wb") as fout: fout.write(html.encode('ascii', 'xmlcharrefreplace')) - finally: - fout.close() - def file_hash(self, source, cu): + def file_hash(self, source, fr): """Compute a hash that changes if the file needs to be re-reported.""" m = Hasher() m.update(source) - self.coverage.data.add_to_hash(cu.filename, m) - return m.digest() + self.coverage.data.add_to_hash(fr.filename, m) + return m.hexdigest() - def html_file(self, cu, analysis): + def html_file(self, fr, analysis): """Generate an HTML file for one source file.""" - source_file = cu.source_file() - try: - source = source_file.read() - finally: - source_file.close() + source = fr.source() # Find out if the file on disk is already correct. - flat_rootname = cu.flat_rootname() - this_hash = self.file_hash(source, cu) - that_hash = self.status.file_hash(flat_rootname) + rootname = flat_rootname(fr.relative_filename()) + this_hash = self.file_hash(source.encode('utf-8'), fr) + that_hash = self.status.file_hash(rootname) if this_hash == that_hash: # Nothing has changed to require the file to be reported again. - self.files.append(self.status.index_info(flat_rootname)) + self.files.append(self.status.index_info(rootname)) return - self.status.set_file_hash(flat_rootname, this_hash) - - # If need be, determine the encoding of the source file. We use it - # later to properly write the HTML. - if sys.version_info < (3, 0): - encoding = source_encoding(source) - # Some UTF8 files have the dreaded UTF8 BOM. If so, junk it. - if encoding.startswith("utf-8") and source[:3] == "\xef\xbb\xbf": - source = source[3:] - encoding = "utf-8" + self.status.set_file_hash(rootname, this_hash) # Get the numbers for this file. nums = analysis.numbers - if self.arcs: + if self.has_arcs: missing_branch_arcs = analysis.missing_branch_arcs() # These classes determine which lines are highlighted by default. @@ -195,8 +198,7 @@ lines = [] - for lineno, line in enumerate(source_token_lines(source)): - lineno += 1 # 1-based line numbers. + for lineno, line in enumerate(fr.source_token_lines(), start=1): # Figure out how to mark this line. line_class = [] annotate_html = "" @@ -207,23 +209,34 @@ line_class.append(c_exc) elif lineno in analysis.missing: line_class.append(c_mis) - elif self.arcs and lineno in missing_branch_arcs: + elif self.has_arcs and lineno in missing_branch_arcs: line_class.append(c_par) - annlines = [] + shorts = [] + longs = [] for b in missing_branch_arcs[lineno]: if b < 0: - annlines.append("exit") + shorts.append("exit") + longs.append("the function exit") else: - annlines.append(str(b)) - annotate_html = " ".join(annlines) - if len(annlines) > 1: - annotate_title = "no jumps to these line numbers" - elif len(annlines) == 1: - annotate_title = "no jump to this line number" + shorts.append(b) + longs.append("line %d" % b) + # 202F is NARROW NO-BREAK SPACE. + # 219B is RIGHTWARDS ARROW WITH STROKE. + short_fmt = "%s ↛ %s" + annotate_html = ", ".join(short_fmt % (lineno, d) for d in shorts) + annotate_html += " [?]" + + annotate_title = "Line %d was executed, but never jumped to " % lineno + if len(longs) == 1: + annotate_title += longs[0] + elif len(longs) == 2: + annotate_title += longs[0] + " or " + longs[1] + else: + annotate_title += ", ".join(longs[:-1]) + ", or " + longs[-1] elif lineno in analysis.statements: line_class.append(c_run) - # Build the HTML for the line + # Build the HTML for the line. html = [] for tok_type, tok_text in line: if tok_type == "ws": @@ -231,8 +244,8 @@ else: tok_html = escape(tok_text) or ' ' html.append( - "<span class='%s'>%s</span>" % (tok_type, tok_html) - ) + '<span class="%s">%s</span>' % (tok_type, tok_html) + ) lines.append({ 'html': ''.join(html), @@ -243,16 +256,15 @@ }) # Write the HTML page for this file. - html = spaceless(self.source_tmpl.render({ + template_values = { 'c_exc': c_exc, 'c_mis': c_mis, 'c_par': c_par, 'c_run': c_run, - 'arcs': self.arcs, 'extra_css': self.extra_css, - 'cu': cu, 'nums': nums, 'lines': lines, - })) + 'has_arcs': self.has_arcs, 'extra_css': self.extra_css, + 'fr': fr, 'nums': nums, 'lines': lines, + 'time_stamp': self.time_stamp, + } + html = spaceless(self.source_tmpl.render(template_values)) - if sys.version_info < (3, 0): - html = html.decode(encoding) - - html_filename = flat_rootname + ".html" + html_filename = rootname + ".html" html_path = os.path.join(self.directory, html_filename) self.write_html(html_path, html) @@ -260,32 +272,26 @@ index_info = { 'nums': nums, 'html_filename': html_filename, - 'name': cu.name, - } + 'relative_filename': fr.relative_filename(), + } self.files.append(index_info) - self.status.set_index_info(flat_rootname, index_info) + self.status.set_index_info(rootname, index_info) def index_file(self): """Write the index.html file for this report.""" - index_tmpl = Templite( - data("index.html"), self.template_globals - ) + index_tmpl = Templite(data("index.html"), self.template_globals) - self.totals = sum([f['nums'] for f in self.files]) + self.totals = sum(f['nums'] for f in self.files) html = index_tmpl.render({ - 'arcs': self.arcs, + 'has_arcs': self.has_arcs, 'extra_css': self.extra_css, 'files': self.files, 'totals': self.totals, + 'time_stamp': self.time_stamp, }) - if sys.version_info < (3, 0): - html = html.decode("utf-8") - self.write_html( - os.path.join(self.directory, "index.html"), - html - ) + self.write_html(os.path.join(self.directory, "index.html"), html) # Write the latest hashes for next time. self.status.write(self.directory) @@ -294,9 +300,37 @@ class HtmlStatus(object): """The status information we keep to support incremental reporting.""" - STATUS_FILE = "status.dat" + STATUS_FILE = "status.json" STATUS_FORMAT = 1 + # pylint: disable=wrong-spelling-in-comment,useless-suppression + # The data looks like: + # + # { + # 'format': 1, + # 'settings': '540ee119c15d52a68a53fe6f0897346d', + # 'version': '4.0a1', + # 'files': { + # 'cogapp___init__': { + # 'hash': 'e45581a5b48f879f301c0f30bf77a50c', + # 'index': { + # 'html_filename': 'cogapp___init__.html', + # 'name': 'cogapp/__init__', + # 'nums': <coverage.results.Numbers object at 0x10ab7ed0>, + # } + # }, + # ... + # 'cogapp_whiteutils': { + # 'hash': '8504bb427fc488c4176809ded0277d51', + # 'index': { + # 'html_filename': 'cogapp_whiteutils.html', + # 'name': 'cogapp/whiteutils', + # 'nums': <coverage.results.Numbers object at 0x10ab7d90>, + # } + # }, + # }, + # } + def __init__(self): self.reset() @@ -310,11 +344,8 @@ usable = False try: status_file = os.path.join(directory, self.STATUS_FILE) - fstatus = open(status_file, "rb") - try: - status = pickle.load(fstatus) - finally: - fstatus.close() + with open(status_file, "r") as fstatus: + status = json.load(fstatus) except (IOError, ValueError): usable = False else: @@ -325,7 +356,10 @@ usable = False if usable: - self.files = status['files'] + self.files = {} + for filename, fileinfo in iitems(status['files']): + fileinfo['index']['nums'] = Numbers(*fileinfo['index']['nums']) + self.files[filename] = fileinfo self.settings = status['settings'] else: self.reset() @@ -333,17 +367,26 @@ def write(self, directory): """Write the current status to `directory`.""" status_file = os.path.join(directory, self.STATUS_FILE) + files = {} + for filename, fileinfo in iitems(self.files): + fileinfo['index']['nums'] = fileinfo['index']['nums'].init_args() + files[filename] = fileinfo + status = { 'format': self.STATUS_FORMAT, 'version': coverage.__version__, 'settings': self.settings, - 'files': self.files, - } - fout = open(status_file, "wb") - try: - pickle.dump(status, fout) - finally: - fout.close() + 'files': files, + } + with open(status_file, "w") as fout: + json.dump(status, fout) + + # Older versions of ShiningPanda look for the old name, status.dat. + # Accomodate them if we are running under Jenkins. + # https://issues.jenkins-ci.org/browse/JENKINS-28428 + if "JENKINS_URL" in os.environ: + with open(os.path.join(directory, "status.dat"), "w") as dat: + dat.write("https://issues.jenkins-ci.org/browse/JENKINS-28428\n") def settings_hash(self): """Get the hash of the coverage.py settings.""" @@ -374,16 +417,18 @@ def escape(t): """HTML-escape the text in `t`.""" - return (t - # Convert HTML special chars into HTML entities. - .replace("&", "&").replace("<", "<").replace(">", ">") - .replace("'", "'").replace('"', """) - # Convert runs of spaces: "......" -> " . . ." - .replace(" ", " ") - # To deal with odd-length runs, convert the final pair of spaces - # so that "....." -> " . ." - .replace(" ", " ") - ) + return ( + t + # Convert HTML special chars into HTML entities. + .replace("&", "&").replace("<", "<").replace(">", ">") + .replace("'", "'").replace('"', """) + # Convert runs of spaces: "......" -> " . . ." + .replace(" ", " ") + # To deal with odd-length runs, convert the final pair of spaces + # so that "....." -> " . ." + .replace(" ", " ") + ) + def spaceless(html): """Squeeze out some annoying extra space from an HTML string. @@ -395,5 +440,10 @@ html = re.sub(r">\s+<p ", ">\n<p ", html) return html + +def pair(ratio): + """Format a pair of numbers so JavaScript can read them in an attribute.""" + return "%s %s" % ratio + # # eflag: FileType = Python2