DebugClients/Python/coverage/html.py

branch
Py2 comp.
changeset 3495
fac17a82b431
parent 790
2c0ea0163ef4
child 3499
f2d4b02c7e88
--- a/DebugClients/Python/coverage/html.py	Fri Apr 04 22:57:07 2014 +0200
+++ b/DebugClients/Python/coverage/html.py	Thu Apr 10 23:02:20 2014 +0200
@@ -1,99 +1,217 @@
 """HTML reporting for Coverage."""
 
-import os, re, shutil
+import os, re, shutil, sys
 
-from . import __url__, __version__           # pylint: disable-msg=W0611
-from .phystokens import source_token_lines
+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
 
-# Disable pylint msg W0612, because a bunch of variables look unused, but
-# they're accessed in a templite context via locals().
-# pylint: disable-msg=W0612
+
+# Static files are looked for in a list of places.
+STATIC_PATH = [
+    # The place Debian puts system Javascript libraries.
+    "/usr/share/javascript",
+
+    # Our htmlfiles directory.
+    os.path.join(os.path.dirname(__file__), "htmlfiles"),
+]
+
+def data_filename(fname, pkgdir=""):
+    """Return the path to a data file of ours.
 
-def data_filename(fname):
-    """Return the path to a data file of ours."""
-    return os.path.join(os.path.split(__file__)[0], fname)
+    The file is searched for on `STATIC_PATH`, and the first place it's found,
+    is returned.
+
+    Each directory in `STATIC_PATH` is searched as-is, and also, if `pkgdir`
+    is provided, at that subdirectory.
+
+    """
+    for static_dir in STATIC_PATH:
+        static_filename = os.path.join(static_dir, fname)
+        if os.path.exists(static_filename):
+            return 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)
+
 
 def data(fname):
     """Return the contents of a data file of ours."""
-    return open(data_filename(fname)).read()
+    data_file = open(data_filename(fname))
+    try:
+        return data_file.read()
+    finally:
+        data_file.close()
 
 
 class HtmlReporter(Reporter):
     """HTML reporting."""
 
-    def __init__(self, coverage, ignore_errors=False):
-        super(HtmlReporter, self).__init__(coverage, ignore_errors)
+    # These files will be copied from the htmlfiles dir to the output dir.
+    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", ""),
+            ]
+
+    def __init__(self, cov, config):
+        super(HtmlReporter, self).__init__(cov, config)
         self.directory = None
-        self.source_tmpl = Templite(data("htmlfiles/pyfile.html"), globals())
+        self.template_globals = {
+            'escape': escape,
+            'title': self.config.html_title,
+            '__url__': coverage.__url__,
+            '__version__': coverage.__version__,
+            }
+        self.source_tmpl = Templite(
+            data("pyfile.html"), self.template_globals
+            )
+
+        self.coverage = cov
 
         self.files = []
-        self.arcs = coverage.data.has_arcs()
+        self.arcs = self.coverage.data.has_arcs()
+        self.status = HtmlStatus()
+        self.extra_css = None
+        self.totals = Numbers()
 
-    def report(self, morfs, directory, omit_prefixes=None):
+    def report(self, morfs):
         """Generate an HTML report for `morfs`.
 
-        `morfs` is a list of modules or filenames.  `directory` is where to put
-        the HTML files. `omit_prefixes` is a list of strings, prefixes of
-        modules to omit from the report.
+        `morfs` is a list of modules or filenames.
 
         """
-        assert directory, "must provide a directory for html reporting"
+        assert self.config.html_dir, "must give a directory for html reporting"
+
+        # Read the status data.
+        self.status.read(self.config.html_dir)
+
+        # Check that this run used the same settings as the last run.
+        m = Hasher()
+        m.update(self.config)
+        these_settings = m.digest()
+        if self.status.settings_hash() != these_settings:
+            self.status.reset()
+            self.status.set_settings_hash(these_settings)
+
+        # The user may have extra CSS they want copied.
+        if self.config.extra_css:
+            self.extra_css = os.path.basename(self.config.extra_css)
 
         # Process all the files.
-        self.report_files(self.html_file, morfs, directory, omit_prefixes)
+        self.report_files(self.html_file, morfs, self.config.html_dir)
+
+        if not self.files:
+            raise CoverageException("No data to report.")
 
         # Write the index file.
         self.index_file()
 
-        # Create the once-per-directory files.
-        for static in [
-            "style.css", "coverage_html.js",
-            "jquery-1.3.2.min.js", "jquery.tablesorter.min.js"
-            ]:
+        self.make_local_static_report_files()
+
+        return self.totals.pc_covered
+
+    def make_local_static_report_files(self):
+        """Make local instances of static files for HTML report."""
+        # The files we provide must always be copied.
+        for static, pkgdir in self.STATIC_FILES:
+            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(
-                data_filename("htmlfiles/" + static),
-                os.path.join(directory, static)
+                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:
+            fout.write(html.encode('ascii', 'xmlcharrefreplace'))
+        finally:
+            fout.close()
+
+    def file_hash(self, source, cu):
+        """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()
+
     def html_file(self, cu, analysis):
         """Generate an HTML file for one source file."""
+        source_file = cu.source_file()
+        try:
+            source = source_file.read()
+        finally:
+            source_file.close()
 
-        source = cu.source_file().read()
+        # 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)
+        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))
+            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"
+
+        # Get the numbers for this file.
         nums = analysis.numbers
 
-        missing_branch_arcs = analysis.missing_branch_arcs()
-        n_par = 0   # accumulated below.
-        arcs = self.arcs
+        if self.arcs:
+            missing_branch_arcs = analysis.missing_branch_arcs()
 
         # These classes determine which lines are highlighted by default.
-        c_run = " run hide_run"
-        c_exc = " exc"
-        c_mis = " mis"
-        c_par = " par" + c_run
+        c_run = "run hide_run"
+        c_exc = "exc"
+        c_mis = "mis"
+        c_par = "par " + c_run
 
         lines = []
 
         for lineno, line in enumerate(source_token_lines(source)):
             lineno += 1     # 1-based line numbers.
             # Figure out how to mark this line.
-            line_class = ""
+            line_class = []
             annotate_html = ""
             annotate_title = ""
             if lineno in analysis.statements:
-                line_class += " stm"
+                line_class.append("stm")
             if lineno in analysis.excluded:
-                line_class += c_exc
+                line_class.append(c_exc)
             elif lineno in analysis.missing:
-                line_class += c_mis
+                line_class.append(c_mis)
             elif self.arcs and lineno in missing_branch_arcs:
-                line_class += c_par
-                n_par += 1
+                line_class.append(c_par)
                 annlines = []
                 for b in missing_branch_arcs[lineno]:
-                    if b == -1:
+                    if b < 0:
                         annlines.append("exit")
                     else:
                         annlines.append(str(b))
@@ -103,59 +221,159 @@
                 elif len(annlines) == 1:
                     annotate_title = "no jump to this line number"
             elif lineno in analysis.statements:
-                line_class += c_run
+                line_class.append(c_run)
 
             # Build the HTML for the line
-            html = ""
+            html = []
             for tok_type, tok_text in line:
                 if tok_type == "ws":
-                    html += escape(tok_text)
+                    html.append(escape(tok_text))
                 else:
                     tok_html = escape(tok_text) or '&nbsp;'
-                    html += "<span class='%s'>%s</span>" % (tok_type, tok_html)
+                    html.append(
+                        "<span class='%s'>%s</span>" % (tok_type, tok_html)
+                        )
 
             lines.append({
-                'html': html,
+                'html': ''.join(html),
                 'number': lineno,
-                'class': line_class.strip() or "pln",
+                'class': ' '.join(line_class) or "pln",
                 'annotate': annotate_html,
                 'annotate_title': annotate_title,
             })
 
         # Write the HTML page for this file.
-        html_filename = cu.flat_rootname() + ".html"
+        html = spaceless(self.source_tmpl.render({
+            '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,
+        }))
+
+        if sys.version_info < (3, 0):
+            html = html.decode(encoding)
+
+        html_filename = flat_rootname + ".html"
         html_path = os.path.join(self.directory, html_filename)
-        html = spaceless(self.source_tmpl.render(locals()))
-        fhtml = open(html_path, 'w')
-        fhtml.write(html)
-        fhtml.close()
+        self.write_html(html_path, html)
 
         # Save this file's information for the index file.
-        self.files.append({
+        index_info = {
             'nums': nums,
-            'par': n_par,
             'html_filename': html_filename,
-            'cu': cu,
-            })
+            'name': cu.name,
+            }
+        self.files.append(index_info)
+        self.status.set_index_info(flat_rootname, index_info)
 
     def index_file(self):
         """Write the index.html file for this report."""
-        index_tmpl = Templite(data("htmlfiles/index.html"), 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,
+            'extra_css': self.extra_css,
+            'files': self.files,
+            'totals': self.totals,
+        })
 
-        files = self.files
-        arcs = self.arcs
+        if sys.version_info < (3, 0):
+            html = html.decode("utf-8")
+        self.write_html(
+            os.path.join(self.directory, "index.html"),
+            html
+            )
+
+        # Write the latest hashes for next time.
+        self.status.write(self.directory)
+
+
+class HtmlStatus(object):
+    """The status information we keep to support incremental reporting."""
+
+    STATUS_FILE = "status.dat"
+    STATUS_FORMAT = 1
+
+    def __init__(self):
+        self.reset()
+
+    def reset(self):
+        """Initialize to empty."""
+        self.settings = ''
+        self.files = {}
 
-        totals = sum([f['nums'] for f in files])
+    def read(self, directory):
+        """Read the last status in `directory`."""
+        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()
+        except (IOError, ValueError):
+            usable = False
+        else:
+            usable = True
+            if status['format'] != self.STATUS_FORMAT:
+                usable = False
+            elif status['version'] != coverage.__version__:
+                usable = False
+
+        if usable:
+            self.files = status['files']
+            self.settings = status['settings']
+        else:
+            self.reset()
 
-        fhtml = open(os.path.join(self.directory, "index.html"), "w")
-        fhtml.write(index_tmpl.render(locals()))
-        fhtml.close()
+    def write(self, directory):
+        """Write the current status to `directory`."""
+        status_file = os.path.join(directory, self.STATUS_FILE)
+        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()
+
+    def settings_hash(self):
+        """Get the hash of the coverage.py settings."""
+        return self.settings
+
+    def set_settings_hash(self, settings):
+        """Set the hash of the coverage.py settings."""
+        self.settings = settings
+
+    def file_hash(self, fname):
+        """Get the hash of `fname`'s contents."""
+        return self.files.get(fname, {}).get('hash', '')
+
+    def set_file_hash(self, fname, val):
+        """Set the hash of `fname`'s contents."""
+        self.files.setdefault(fname, {})['hash'] = val
+
+    def index_info(self, fname):
+        """Get the information for index.html for `fname`."""
+        return self.files.get(fname, {}).get('index', {})
+
+    def set_index_info(self, fname, info):
+        """Set the information for index.html for `fname`."""
+        self.files.setdefault(fname, {})['index'] = info
 
 
 # Helpers for templates and generating HTML
 
 def escape(t):
-    """HTML-escape the text in t."""
+    """HTML-escape the text in `t`."""
     return (t
             # Convert HTML special chars into HTML entities.
             .replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
@@ -167,19 +385,12 @@
             .replace("  ", "&nbsp; ")
         )
 
-def format_pct(p):
-    """Format a percentage value for the HTML reports."""
-    return "%.0f" % p
-
 def spaceless(html):
     """Squeeze out some annoying extra space from an HTML string.
 
-    Nicely-formatted templates mean lots of extra space in the result.  Get
-    rid of some.
+    Nicely-formatted templates mean lots of extra space in the result.
+    Get rid of some.
 
     """
-    html = re.sub(">\s+<p ", ">\n<p ", html)
+    html = re.sub(r">\s+<p ", ">\n<p ", html)
     return html
-
-#
-# eflag: FileType = Python2

eric ide

mercurial