17757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch"""HTML reporting for Coverage."""
27757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
37757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdochimport os, re, shutil
47757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
57757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdochimport coverage
67757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdochfrom coverage.backward import pickle, write_encoded
77757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdochfrom coverage.misc import CoverageException, Hasher
87757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdochfrom coverage.phystokens import source_token_lines
97757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdochfrom coverage.report import Reporter
107757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdochfrom coverage.templite import Templite
117757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
127757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch# Disable pylint msg W0612, because a bunch of variables look unused, but
137757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch# they're accessed in a Templite context via locals().
147757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch# pylint: disable=W0612
157757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
167757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdochdef data_filename(fname):
177757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    """Return the path to a data file of ours."""
187757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    return os.path.join(os.path.split(__file__)[0], fname)
197757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
207757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdochdef data(fname):
217757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    """Return the contents of a data file of ours."""
227757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    data_file = open(data_filename(fname))
237757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    try:
247757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        return data_file.read()
257757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    finally:
267757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        data_file.close()
277757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
287757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
297757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdochclass HtmlReporter(Reporter):
307757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    """HTML reporting."""
317757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
327757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    # These files will be copied from the htmlfiles dir to the output dir.
337757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    STATIC_FILES = [
347757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            "style.css",
357757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            "jquery-1.4.3.min.js",
367757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            "jquery.hotkeys.js",
377757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            "jquery.isonscreen.js",
387757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            "jquery.tablesorter.min.js",
397757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            "coverage_html.js",
407757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            "keybd_closed.png",
417757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            "keybd_open.png",
427757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            ]
437757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
447757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    def __init__(self, cov, ignore_errors=False):
457757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        super(HtmlReporter, self).__init__(cov, ignore_errors)
467757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.directory = None
477757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.template_globals = {
487757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            'escape': escape,
497757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            '__url__': coverage.__url__,
507757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            '__version__': coverage.__version__,
517757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            }
527757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.source_tmpl = Templite(
537757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            data("htmlfiles/pyfile.html"), self.template_globals
547757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            )
557757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
567757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.coverage = cov
577757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
587757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.files = []
597757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.arcs = self.coverage.data.has_arcs()
607757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.status = HtmlStatus()
617757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
627757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    def report(self, morfs, config=None):
637757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        """Generate an HTML report for `morfs`.
647757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
657757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        `morfs` is a list of modules or filenames.  `config` is a
667757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        CoverageConfig instance.
677757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
687757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        """
697757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        assert config.html_dir, "must provide a directory for html reporting"
707757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
717757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        # Read the status data.
727757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.status.read(config.html_dir)
737757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
747757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        # Check that this run used the same settings as the last run.
757757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        m = Hasher()
767757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        m.update(config)
777757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        these_settings = m.digest()
787757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        if self.status.settings_hash() != these_settings:
797757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            self.status.reset()
807757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            self.status.set_settings_hash(these_settings)
817757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
827757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        # Process all the files.
837757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.report_files(self.html_file, morfs, config, config.html_dir)
847757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
857757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        if not self.files:
867757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            raise CoverageException("No data to report.")
877757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
887757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        # Write the index file.
897757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.index_file()
907757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
917757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.make_local_static_report_files()
927757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
937757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    def make_local_static_report_files(self):
947757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        """Make local instances of static files for HTML report."""
957757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        for static in self.STATIC_FILES:
967757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            shutil.copyfile(
977757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                data_filename("htmlfiles/" + static),
987757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                os.path.join(self.directory, static)
997757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                )
1007757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
1017757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    def write_html(self, fname, html):
1027757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        """Write `html` to `fname`, properly encoded."""
1037757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        write_encoded(fname, html, 'ascii', 'xmlcharrefreplace')
1047757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
1057757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    def file_hash(self, source, cu):
1067757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        """Compute a hash that changes if the file needs to be re-reported."""
1077757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        m = Hasher()
1087757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        m.update(source)
1097757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.coverage.data.add_to_hash(cu.filename, m)
1107757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        return m.digest()
1117757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
1127757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    def html_file(self, cu, analysis):
1137757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        """Generate an HTML file for one source file."""
1147757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        source_file = cu.source_file()
1157757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        try:
1167757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            source = source_file.read()
1177757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        finally:
1187757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            source_file.close()
1197757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
1207757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        # Find out if the file on disk is already correct.
1217757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        flat_rootname = cu.flat_rootname()
1227757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        this_hash = self.file_hash(source, cu)
1237757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        that_hash = self.status.file_hash(flat_rootname)
1247757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        if this_hash == that_hash:
1257757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            # Nothing has changed to require the file to be reported again.
1267757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            self.files.append(self.status.index_info(flat_rootname))
1277757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            return
1287757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
1297757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.status.set_file_hash(flat_rootname, this_hash)
1307757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
1317757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        nums = analysis.numbers
1327757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
1337757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        missing_branch_arcs = analysis.missing_branch_arcs()
1347757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        n_par = 0   # accumulated below.
1357757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        arcs = self.arcs
1367757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
1377757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        # These classes determine which lines are highlighted by default.
1387757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        c_run = "run hide_run"
1397757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        c_exc = "exc"
1407757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        c_mis = "mis"
1417757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        c_par = "par " + c_run
1427757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
1437757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        lines = []
1447757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
1457757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        for lineno, line in enumerate(source_token_lines(source)):
1467757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            lineno += 1     # 1-based line numbers.
1477757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            # Figure out how to mark this line.
1487757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            line_class = []
1497757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            annotate_html = ""
1507757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            annotate_title = ""
1517757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            if lineno in analysis.statements:
1527757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                line_class.append("stm")
1537757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            if lineno in analysis.excluded:
1547757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                line_class.append(c_exc)
1557757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            elif lineno in analysis.missing:
1567757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                line_class.append(c_mis)
1577757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            elif self.arcs and lineno in missing_branch_arcs:
1587757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                line_class.append(c_par)
1597757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                n_par += 1
1607757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                annlines = []
1617757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                for b in missing_branch_arcs[lineno]:
1627757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    if b < 0:
1637757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                        annlines.append("exit")
1647757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    else:
1657757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                        annlines.append(str(b))
1667757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                annotate_html = "&nbsp;&nbsp; ".join(annlines)
1677757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                if len(annlines) > 1:
1687757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    annotate_title = "no jumps to these line numbers"
1697757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                elif len(annlines) == 1:
1707757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    annotate_title = "no jump to this line number"
1717757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            elif lineno in analysis.statements:
1727757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                line_class.append(c_run)
1737757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
1747757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            # Build the HTML for the line
1757757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            html = []
1767757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            for tok_type, tok_text in line:
1777757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                if tok_type == "ws":
1787757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    html.append(escape(tok_text))
1797757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                else:
1807757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    tok_html = escape(tok_text) or '&nbsp;'
1817757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                    html.append(
1827757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                        "<span class='%s'>%s</span>" % (tok_type, tok_html)
1837757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                        )
1847757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
1857757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            lines.append({
1867757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                'html': ''.join(html),
1877757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                'number': lineno,
1887757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                'class': ' '.join(line_class) or "pln",
1897757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                'annotate': annotate_html,
1907757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                'annotate_title': annotate_title,
1917757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            })
1927757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
1937757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        # Write the HTML page for this file.
1947757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        html_filename = flat_rootname + ".html"
1957757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        html_path = os.path.join(self.directory, html_filename)
1967757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
1977757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        html = spaceless(self.source_tmpl.render(locals()))
1987757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.write_html(html_path, html)
1997757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
2007757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        # Save this file's information for the index file.
2017757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        index_info = {
2027757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            'nums': nums,
2037757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            'par': n_par,
2047757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            'html_filename': html_filename,
2057757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            'name': cu.name,
2067757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            }
2077757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.files.append(index_info)
2087757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.status.set_index_info(flat_rootname, index_info)
2097757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
2107757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    def index_file(self):
2117757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        """Write the index.html file for this report."""
2127757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        index_tmpl = Templite(
2137757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            data("htmlfiles/index.html"), self.template_globals
2147757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            )
2157757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
2167757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        files = self.files
2177757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        arcs = self.arcs
2187757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
2197757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        totals = sum([f['nums'] for f in files])
2207757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
2217757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.write_html(
2227757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            os.path.join(self.directory, "index.html"),
2237757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            index_tmpl.render(locals())
2247757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            )
2257757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
2267757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        # Write the latest hashes for next time.
2277757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.status.write(self.directory)
2287757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
2297757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
2307757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdochclass HtmlStatus(object):
2317757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    """The status information we keep to support incremental reporting."""
2327757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
2337757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    STATUS_FILE = "status.dat"
2347757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    STATUS_FORMAT = 1
2357757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
2367757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    def __init__(self):
2377757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.reset()
2387757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
2397757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    def reset(self):
2407757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        """Initialize to empty."""
2417757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.settings = ''
2427757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.files = {}
2437757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
2447757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    def read(self, directory):
2457757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        """Read the last status in `directory`."""
2467757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        usable = False
2477757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        try:
2487757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            status_file = os.path.join(directory, self.STATUS_FILE)
2497757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            status = pickle.load(open(status_file, "rb"))
2507757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        except IOError:
2517757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            usable = False
2527757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        else:
2537757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            usable = True
2547757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            if status['format'] != self.STATUS_FORMAT:
2557757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                usable = False
2567757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            elif status['version'] != coverage.__version__:
2577757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch                usable = False
2587757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
2597757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        if usable:
2607757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            self.files = status['files']
2617757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            self.settings = status['settings']
2627757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        else:
2637757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            self.reset()
2647757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
2657757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    def write(self, directory):
2667757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        """Write the current status to `directory`."""
2677757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        status_file = os.path.join(directory, self.STATUS_FILE)
2687757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        status = {
2697757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            'format': self.STATUS_FORMAT,
2707757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            'version': coverage.__version__,
2717757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            'settings': self.settings,
2727757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            'files': self.files,
2737757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            }
2747757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        fout = open(status_file, "wb")
2757757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        try:
2767757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            pickle.dump(status, fout)
2777757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        finally:
2787757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            fout.close()
2797757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
2807757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    def settings_hash(self):
2817757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        """Get the hash of the coverage.py settings."""
2827757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        return self.settings
2837757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
2847757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    def set_settings_hash(self, settings):
2857757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        """Set the hash of the coverage.py settings."""
2867757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.settings = settings
2877757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
2887757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    def file_hash(self, fname):
2897757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        """Get the hash of `fname`'s contents."""
2907757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        return self.files.get(fname, {}).get('hash', '')
2917757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
2927757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    def set_file_hash(self, fname, val):
2937757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        """Set the hash of `fname`'s contents."""
2947757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.files.setdefault(fname, {})['hash'] = val
2957757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
2967757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    def index_info(self, fname):
2977757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        """Get the information for index.html for `fname`."""
2987757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        return self.files.get(fname, {}).get('index', {})
2997757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
3007757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    def set_index_info(self, fname, info):
3017757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        """Set the information for index.html for `fname`."""
3027757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        self.files.setdefault(fname, {})['index'] = info
3037757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
3047757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
3057757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch# Helpers for templates and generating HTML
3067757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
3077757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdochdef escape(t):
3087757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    """HTML-escape the text in `t`."""
3097757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    return (t
3107757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            # Convert HTML special chars into HTML entities.
3117757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            .replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
3127757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            .replace("'", "&#39;").replace('"', "&quot;")
3137757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            # Convert runs of spaces: "......" -> "&nbsp;.&nbsp;.&nbsp;."
3147757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            .replace("  ", "&nbsp; ")
3157757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            # To deal with odd-length runs, convert the final pair of spaces
3167757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            # so that "....." -> "&nbsp;.&nbsp;&nbsp;."
3177757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch            .replace("  ", "&nbsp; ")
3187757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch        )
3197757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
3207757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdochdef spaceless(html):
3217757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    """Squeeze out some annoying extra space from an HTML string.
3227757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
3237757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    Nicely-formatted templates mean lots of extra space in the result.
3247757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    Get rid of some.
3257757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch
3267757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    """
3277757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    html = re.sub(">\s+<p ", ">\n<p ", html)
3287757ec2eadfa2dd8ac2aeed0a4399e9b07ec38cbBen Murdoch    return html
329