diff options
Diffstat (limited to 'coverage/html.py')
-rw-r--r-- | coverage/html.py | 252 |
1 files changed, 215 insertions, 37 deletions
diff --git a/coverage/html.py b/coverage/html.py index 76e28907..ed8920f2 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -1,16 +1,18 @@ """HTML reporting for Coverage.""" -import os, re, shutil +import os, re, shutil, sys -from coverage import __url__, __version__ # pylint: disable-msg=W0611 -from coverage.misc import CoverageException -from coverage.phystokens import source_token_lines +import coverage +from coverage.backward import pickle +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 # 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 +# pylint: disable=W0612 def data_filename(fname): """Return the path to a data file of ours.""" @@ -18,7 +20,11 @@ def data_filename(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): @@ -28,30 +34,60 @@ class HtmlReporter(Reporter): STATIC_FILES = [ "style.css", "jquery-1.4.3.min.js", - "jquery.tablesorter.min.js", "jquery.hotkeys.js", + "jquery.isonscreen.js", + "jquery.tablesorter.min.js", "coverage_html.js", + "keybd_closed.png", + "keybd_open.png", ] - def __init__(self, coverage, ignore_errors=False): - super(HtmlReporter, self).__init__(coverage, ignore_errors) + 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("htmlfiles/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, config=None): + def report(self, morfs): """Generate an HTML report for `morfs`. - `morfs` is a list of modules or filenames. `config` is a - CoverageConfig instance. + `morfs` is a list of modules or filenames. """ - assert config.html_dir, "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, config, config.html_dir) + self.report_files(self.html_file, morfs, self.config.html_dir) if not self.files: raise CoverageException("No data to report.") @@ -59,22 +95,73 @@ class HtmlReporter(Reporter): # Write the index file. self.index_file() - # Create the once-per-directory files. + 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 in self.STATIC_FILES: shutil.copyfile( data_filename("htmlfiles/" + static), os.path.join(self.directory, static) ) - def html_file(self, cu, analysis): - """Generate an HTML file for one source file.""" + # 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) + ) - source = cu.source_file().read() + 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() + + # 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 # These classes determine which lines are highlighted by default. @@ -99,7 +186,6 @@ class HtmlReporter(Reporter): line_class.append(c_mis) elif self.arcs and lineno in missing_branch_arcs: line_class.append(c_par) - n_par += 1 annlines = [] for b in missing_branch_arcs[lineno]: if b < 0: @@ -134,33 +220,125 @@ class HtmlReporter(Reporter): }) # Write the HTML page for this file. - html_filename = cu.flat_rootname() + ".html" + html_filename = flat_rootname + ".html" html_path = os.path.join(self.directory, html_filename) + extra_css = self.extra_css + html = spaceless(self.source_tmpl.render(locals())) - fhtml = open(html_path, 'w') - fhtml.write(html) - fhtml.close() + if sys.version_info < (3, 0): + html = html.decode(encoding) + 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("htmlfiles/index.html"), self.template_globals + ) files = self.files arcs = self.arcs - totals = sum([f['nums'] for f in files]) - - fhtml = open(os.path.join(self.directory, "index.html"), "w") - fhtml.write(index_tmpl.render(locals())) - fhtml.close() + self.totals = totals = sum([f['nums'] for f in files]) + extra_css = self.extra_css + + html = index_tmpl.render(locals()) + 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 = {} + + 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() + + 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 @@ -185,5 +363,5 @@ def spaceless(html): Get rid of some. """ - html = re.sub(">\s+<p ", ">\n<p ", html) + html = re.sub(r">\s+<p ", ">\n<p ", html) return html |