Index: tools/telemetry/third_party/coverage/coverage/html.py |
diff --git a/third_party/pycoverage/coverage/html.py b/tools/telemetry/third_party/coverage/coverage/html.py |
similarity index 55% |
copy from third_party/pycoverage/coverage/html.py |
copy to tools/telemetry/third_party/coverage/coverage/html.py |
index 5242236c1ed9826d19f7abe1a7c4499dc90414dd..de18cefa297aa2dc55538b8392ff82c98a427b0e 100644 |
--- a/third_party/pycoverage/coverage/html.py |
+++ b/tools/telemetry/third_party/coverage/coverage/html.py |
@@ -1,11 +1,19 @@ |
-"""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.""" |
+ |
+import datetime |
+import json |
+import os |
+import re |
+import shutil |
import coverage |
-from coverage.backward import pickle |
+from coverage import env |
+from coverage.backward import iitems |
+from coverage.files import flat_rootname |
from coverage.misc import CoverageException, Hasher |
-from coverage.phystokens import source_token_lines, source_encoding |
from coverage.report import Reporter |
from coverage.results import Numbers |
from coverage.templite import Templite |
@@ -20,6 +28,7 @@ STATIC_PATH = [ |
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 @@ def data_filename(fname, pkgdir=""): |
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 @@ class HtmlReporter(Reporter): |
# 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 @@ class HtmlReporter(Reporter): |
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 @@ class HtmlReporter(Reporter): |
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 @@ class HtmlReporter(Reporter): |
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 @@ class HtmlReporter(Reporter): |
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 @@ class HtmlReporter(Reporter): |
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 @@ class HtmlReporter(Reporter): |
}) |
# 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 @@ class HtmlReporter(Reporter): |
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 HtmlReporter(Reporter): |
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 @@ class HtmlStatus(object): |
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 @@ class HtmlStatus(object): |
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 @@ class HtmlStatus(object): |
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 @@ class HtmlStatus(object): |
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. |
@@ -394,3 +439,8 @@ def spaceless(html): |
""" |
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 |