diff options
Diffstat (limited to 'coverage/html.py')
-rw-r--r-- | coverage/html.py | 160 |
1 files changed, 94 insertions, 66 deletions
diff --git a/coverage/html.py b/coverage/html.py index 0b2cc25c..8dca6323 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -1,6 +1,7 @@ -"""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 -from __future__ import unicode_literals +"""HTML reporting for coverage.py.""" import datetime import json @@ -11,11 +12,14 @@ import shutil import coverage from coverage import env from coverage.backward import iitems -from coverage.misc import CoverageException, Hasher +from coverage.files import flat_rootname +from coverage.misc import CoverageException, Hasher, isolate_module from coverage.report import Reporter from coverage.results import Numbers from coverage.templite import Templite +os = isolate_module(os) + # Static files are looked for in a list of places. STATIC_PATH = [ @@ -26,6 +30,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. @@ -36,15 +41,22 @@ def data_filename(fname, pkgdir=""): 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): @@ -56,18 +68,19 @@ def data(fname): 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.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", ""), - ] + ("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) @@ -81,15 +94,15 @@ class HtmlReporter(Reporter): '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() @@ -98,7 +111,7 @@ class HtmlReporter(Reporter): 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" @@ -128,8 +141,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.""" @@ -138,14 +150,14 @@ 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.""" @@ -164,20 +176,20 @@ class HtmlReporter(Reporter): source = fr.source() # Find out if the file on disk is already correct. - flat_rootname = fr.flat_rootname() + rootname = flat_rootname(fr.relative_filename()) this_hash = self.file_hash(source.encode('utf-8'), fr) - that_hash = self.status.file_hash(flat_rootname) + 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) + 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. @@ -199,23 +211,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": @@ -224,7 +247,7 @@ class HtmlReporter(Reporter): tok_html = escape(tok_text) or ' ' html.append( '<span class="%s">%s</span>' % (tok_type, tok_html) - ) + ) lines.append({ 'html': ''.join(html), @@ -237,13 +260,13 @@ class HtmlReporter(Reporter): # Write the HTML page for this file. 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, + '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)) - html_filename = flat_rootname + ".html" + html_filename = rootname + ".html" html_path = os.path.join(self.directory, html_filename) self.write_html(html_path, html) @@ -251,31 +274,26 @@ class HtmlReporter(Reporter): index_info = { 'nums': nums, 'html_filename': html_filename, - 'name': fr.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) 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, }) - 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) @@ -292,11 +310,11 @@ class HtmlStatus(object): # # { # 'format': 1, - # 'settings': '\x87\x9cc8\x80\xe5\x97\xb16\xfcv\xa2\x8d\x8a\xbb\xcf', + # 'settings': '540ee119c15d52a68a53fe6f0897346d', # 'version': '4.0a1', # 'files': { # 'cogapp___init__': { - # 'hash': '\x99*\x0e\\\x10\x11O\x06WG/gJ\x83\xdd\x99', + # 'hash': 'e45581a5b48f879f301c0f30bf77a50c', # 'index': { # 'html_filename': 'cogapp___init__.html', # 'name': 'cogapp/__init__', @@ -305,7 +323,7 @@ class HtmlStatus(object): # }, # ... # 'cogapp_whiteutils': { - # 'hash': 'o\xfd\x0e+s2="\xb2\x1c\xd6\xa1\xee\x85\x85\xda', + # 'hash': '8504bb427fc488c4176809ded0277d51', # 'index': { # 'html_filename': 'cogapp_whiteutils.html', # 'name': 'cogapp/whiteutils', @@ -361,10 +379,17 @@ class HtmlStatus(object): 'version': coverage.__version__, 'settings': self.settings, '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.""" return self.settings @@ -394,16 +419,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. @@ -415,6 +442,7 @@ 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 |