summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2011-04-10 08:42:31 -0400
committerNed Batchelder <ned@nedbatchelder.com>2011-04-10 08:42:31 -0400
commit52e07c90b77b1d07ed95a6195ec5d895f0988224 (patch)
tree325346a20aedac88b3297919f5c1c89f79de7aed
parentd4dd809d27ad7f31431a4ef61309b6951ad41d9b (diff)
parent822b7c82f58bbd6f2b38cc98c7881cc405d0c69e (diff)
downloadpython-coveragepy-52e07c90b77b1d07ed95a6195ec5d895f0988224.tar.gz
Merge Brett's __main__.py file for the tree.
-rw-r--r--CHANGES.txt21
-rw-r--r--TODO.txt6
-rwxr-xr-xalltests.sh2
-rw-r--r--coverage/backward.py27
-rw-r--r--coverage/control.py30
-rw-r--r--coverage/data.py5
-rw-r--r--coverage/execfile.py8
-rw-r--r--coverage/files.py5
-rw-r--r--coverage/html.py147
-rw-r--r--coverage/htmlfiles/coverage_html.js105
-rw-r--r--coverage/htmlfiles/index.html2
-rw-r--r--coverage/htmlfiles/jquery.isonscreen.js53
-rw-r--r--coverage/htmlfiles/pyfile.html1
-rw-r--r--coverage/misc.py46
-rw-r--r--coverage/parser.py5
-rw-r--r--coverage/report.py12
-rw-r--r--doc/source.rst5
-rw-r--r--lab/trace_sample.py3
-rw-r--r--setup.py2
-rw-r--r--test/coveragetest.py48
-rw-r--r--test/modules/usepkgs.py2
-rw-r--r--test/othermods/__init__.py0
-rw-r--r--test/othermods/othera.py2
-rw-r--r--test/othermods/otherb.py2
-rw-r--r--test/othermods/sub/__init__.py0
-rw-r--r--test/othermods/sub/osa.py2
-rw-r--r--test/othermods/sub/osb.py2
-rw-r--r--test/test_api.py103
-rw-r--r--test/test_coverage.py29
-rw-r--r--test/test_html.py155
-rw-r--r--test/test_misc.py28
-rw-r--r--test/test_process.py26
-rw-r--r--test/test_summary.py20
-rw-r--r--test/test_testing.py9
34 files changed, 804 insertions, 109 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index e9d9128..7134c13 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -9,7 +9,15 @@ Version 3.5
- The HTML report now has hotkeys. Try ``n``, ``s``, ``m``, ``x``, ``b``,
``p``, and ``c`` on the overview page to change the column sorting.
On a file page, ``r``, ``m``, ``x``, and ``p`` toggle the run, missing,
- excluded, and partial line markings.
+ excluded, and partial line markings. You can navigate the highlighted
+ sections of code by using the ``j`` and ``k`` keys for next and previous.
+ The ``1`` (one) key jumps to the first highlighted section in the file,
+ and ``0`` (zero) scrolls to the top of the file.
+
+- The ``--omit`` and ``--include`` switches now interpret their values more
+ usefully. If the value starts with a wildcard character, it is used as-is.
+ If it does not, it is interpreted relative to the current directory.
+ Closes `issue 121`.
- Modules can now be run directly using ``coverage run -m modulename``, to
mirror Python's ``-m`` flag. Closes `issue 95_`, thanks, Brandon Rhodes.
@@ -17,6 +25,10 @@ Version 3.5
- A little bit of Jython support: `coverage run` can now measure Jython
execution by adapting when $py.class files are traced. Thanks, Adi Roiban.
+- HTML reporting is now incremental: a record is kept of the data that
+ produced the HTML reports, and only files whose data has changed will
+ be generated. This should make most HTML reporting faster.
+
- Pathological code execution could disable the trace function behind our
backs, leading to incorrect code measurement. Now if this happens,
coverage.py will issue a warning, at least alerting you to the problem.
@@ -26,8 +38,11 @@ Version 3.5
possible, to get the best handling of Python source files with encodings.
Closes `issue 107`, thanks, Brett Cannon.
+- Syntax errors in supposed Python files can now be ignored during reporting
+ with the ``-i`` switch just like other source errors. Closes `issue 115`.
+
- Installation from source now succeeds on machines without a C compiler,
- closing `issue 80`.
+ closing `issue 80`.
- Internally, files are now closed explicitly, fixing `issue 104`. Thanks,
Brett Cannon.
@@ -37,6 +52,8 @@ Version 3.5
.. _issue 95: https://bitbucket.org/ned/coveragepy/issue/95/run-subcommand-should-take-a-module-name
.. _issue 104: https://bitbucket.org/ned/coveragepy/issue/104/explicitly-close-files
.. _issue 107: https://bitbucket.org/ned/coveragepy/issue/107/codeparser-not-opening-source-files-with
+.. _issue 115: https://bitbucket.org/ned/coveragepy/issue/115/fail-gracefully-when-reporting-on-file
+.. _issue 121: https://bitbucket.org/ned/coveragepy/issue/121/filename-patterns-are-applied-stupidly
Version 3.4 --- 19 September 2010
diff --git a/TODO.txt b/TODO.txt
index 347936a..6673328 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -12,7 +12,7 @@ Coverage TODO
- while TRUE claims to be partial.
- A way to mark lines as partial branches, with a regex?
- Default to "while True:", "while 1:"
-- HTML keyboard short cuts
++ HTML keyboard short cuts
* 3.2
@@ -111,8 +111,8 @@ x Why can't you specify execute (-x) and report (-r) in the same invocation?
- Rolled-up statistics.
- Some way to focus in on red and yellow
- Show only lines near highlights?
- - Jump to next highlight?
- - Keyboard navigation: N and P
+ + Jump to next highlight?
+ + Keyboard navigation: j and k.
- Cookie for changes to pyfile.html state.
+ Clickable column headers on the index page.
+ Syntax coloring in HTML report.
diff --git a/alltests.sh b/alltests.sh
index be19929..853ca7b 100755
--- a/alltests.sh
+++ b/alltests.sh
@@ -11,7 +11,7 @@ echo "Testing in $ve"
source $ve/26/bin/activate
make --quiet testdata
-for v in 24 25 26 27 31 32 # 23
+for v in 23 24 25 26 27 31 32
do
source $ve/$v/bin/activate
python setup.py -q develop
diff --git a/coverage/backward.py b/coverage/backward.py
index c363f21..f0a34ac 100644
--- a/coverage/backward.py
+++ b/coverage/backward.py
@@ -81,3 +81,30 @@ except AttributeError:
"""Open a source file the best way."""
return open(fname, "rU")
+# Python 3.x is picky about bytes and strings, so provide methods to
+# get them right, and make them no-ops in 2.x
+if sys.version_info >= (3, 0):
+ def to_bytes(s):
+ """Convert string `s` to bytes."""
+ return s.encode('utf8')
+
+ def to_string(b):
+ """Convert bytes `b` to a string."""
+ return b.decode('utf8')
+
+else:
+ def to_bytes(s):
+ """Convert string `s` to bytes (no-op in 2.x)."""
+ return s
+
+ def to_string(b):
+ """Convert bytes `b` to a string (no-op in 2.x)."""
+ return b
+
+# Md5 is available in different places.
+try:
+ import hashlib
+ md5 = hashlib.md5
+except ImportError:
+ import md5
+ md5 = md5.new
diff --git a/coverage/control.py b/coverage/control.py
index 514f23d..dd65661 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -118,8 +118,8 @@ class coverage(object):
else:
self.source_pkgs.append(src)
- self.omit = self._abs_files(self.config.omit)
- self.include = self._abs_files(self.config.include)
+ self.omit = self._prep_patterns(self.config.omit)
+ self.include = self._prep_patterns(self.config.include)
self.collector = Collector(
self._should_trace, timid=self.config.timid,
@@ -226,8 +226,8 @@ class coverage(object):
self._check_for_packages()
# Compiled Python files have two filenames: frame.f_code.co_filename is
- # the filename at the time the .pyc was compiled. The second name
- # is __file__, which is where the .pyc was actually loaded from. Since
+ # the filename at the time the .pyc was compiled. The second name is
+ # __file__, which is where the .pyc was actually loaded from. Since
# .pyc files can be moved after compilation (for example, by being
# installed), we look for __file__ in the frame and prefer it to the
# co_filename value.
@@ -280,10 +280,24 @@ class coverage(object):
self._warnings.append(msg)
sys.stderr.write("Coverage.py warning: %s\n" % msg)
- def _abs_files(self, files):
- """Return a list of absolute file names for the names in `files`."""
- files = files or []
- return [self.file_locator.abs_file(f) for f in files]
+ def _prep_patterns(self, patterns):
+ """Prepare the file patterns for use in a `FnmatchMatcher`.
+
+ If a pattern starts with a wildcard, it is used as a pattern
+ as-is. If it does not start with a wildcard, then it is made
+ absolute with the current directory.
+
+ If `patterns` is None, an empty list is returned.
+
+ """
+ patterns = patterns or []
+ prepped = []
+ for p in patterns or []:
+ if p.startswith("*") or p.startswith("?"):
+ prepped.append(p)
+ else:
+ prepped.append(self.file_locator.abs_file(p))
+ return prepped
def _check_for_packages(self):
"""Update the source_match matcher with latest imported packages."""
diff --git a/coverage/data.py b/coverage/data.py
index 5d482ea..3263cb3 100644
--- a/coverage/data.py
+++ b/coverage/data.py
@@ -228,6 +228,11 @@ class CoverageData(object):
"""A map containing all the arcs executed in `filename`."""
return self.arcs.get(filename) or {}
+ def add_to_hash(self, filename, hasher):
+ """Contribute `filename`'s data to the Md5Hash `hasher`."""
+ hasher.update(self.executed_lines(filename))
+ hasher.update(self.executed_arcs(filename))
+
def summary(self, fullpath=False):
"""Return a dict summarizing the coverage data.
diff --git a/coverage/execfile.py b/coverage/execfile.py
index a32957c..01788bb 100644
--- a/coverage/execfile.py
+++ b/coverage/execfile.py
@@ -14,6 +14,12 @@ except KeyError:
BUILTINS = sys.modules['builtins']
+def rsplit1(s, sep):
+ """The same as s.rsplit(sep, 1), but works in 2.3"""
+ parts = s.split(sep)
+ return sep.join(parts[:-1]), parts[-1]
+
+
def run_python_module(modulename, args):
"""Run a python module, as though with ``python -m name args...``.
@@ -29,7 +35,7 @@ def run_python_module(modulename, args):
# Search for the module - inside its parent package, if any - using
# standard import mechanics.
if '.' in modulename:
- packagename, name = modulename.rsplit('.', 1)
+ packagename, name = rsplit1(modulename, '.')
package = __import__(packagename, glo, loc, ['__path__'])
searchpath = package.__path__
else:
diff --git a/coverage/files.py b/coverage/files.py
index 9a8ac56..a68a0a7 100644
--- a/coverage/files.py
+++ b/coverage/files.py
@@ -1,5 +1,6 @@
"""File wrangling."""
+from coverage.backward import to_string
import fnmatch, os, sys
class FileLocator(object):
@@ -72,9 +73,7 @@ class FileLocator(object):
data = zi.get_data(parts[1])
except IOError:
continue
- if sys.version_info >= (3, 0):
- data = data.decode('utf8') # TODO: How to do this properly?
- return data
+ return to_string(data)
return None
diff --git a/coverage/html.py b/coverage/html.py
index 87edad4..802327d 100644
--- a/coverage/html.py
+++ b/coverage/html.py
@@ -2,8 +2,9 @@
import os, re, shutil
-from coverage import __url__, __version__ # pylint: disable=W0611
-from coverage.misc import CoverageException
+import coverage
+from coverage.backward import pickle
+from coverage.misc import CoverageException, Hasher
from coverage.phystokens import source_token_lines
from coverage.report import Reporter
from coverage.templite import Templite
@@ -32,18 +33,29 @@ 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",
]
- def __init__(self, coverage, ignore_errors=False):
- super(HtmlReporter, self).__init__(coverage, ignore_errors)
+ def __init__(self, cov, ignore_errors=False):
+ super(HtmlReporter, self).__init__(cov, ignore_errors)
self.directory = None
- self.source_tmpl = Templite(data("htmlfiles/pyfile.html"), globals())
+ self.template_globals = {
+ 'escape': escape,
+ '__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()
def report(self, morfs, config=None):
"""Generate an HTML report for `morfs`.
@@ -54,6 +66,17 @@ class HtmlReporter(Reporter):
"""
assert config.html_dir, "must provide a directory for html reporting"
+ # Read the status data.
+ self.status.read(config.html_dir)
+
+ # Check that this run used the same settings as the last run.
+ m = Hasher()
+ m.update(config)
+ these_settings = m.digest()
+ if self.status.settings_hash() != these_settings:
+ self.status.reset()
+ self.status.set_settings_hash(these_settings)
+
# Process all the files.
self.report_files(self.html_file, morfs, config, config.html_dir)
@@ -70,6 +93,13 @@ class HtmlReporter(Reporter):
os.path.join(self.directory, static)
)
+ 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()
@@ -78,6 +108,17 @@ class HtmlReporter(Reporter):
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)
+
nums = analysis.numbers
missing_branch_arcs = analysis.missing_branch_arcs()
@@ -141,7 +182,7 @@ 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)
html = spaceless(self.source_tmpl.render(locals()))
fhtml = open(html_path, 'w')
@@ -151,16 +192,20 @@ class HtmlReporter(Reporter):
fhtml.close()
# 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
@@ -173,6 +218,84 @@ class HtmlReporter(Reporter):
finally:
fhtml.close()
+ # 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)
+ status = pickle.load(open(status_file, "rb"))
+ except IOError:
+ 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
diff --git a/coverage/htmlfiles/coverage_html.js b/coverage/htmlfiles/coverage_html.js
index 7b6db42..10482fa 100644
--- a/coverage/htmlfiles/coverage_html.js
+++ b/coverage/htmlfiles/coverage_html.js
@@ -90,8 +90,17 @@ coverage.pyfile_ready = function($) {
var frag = location.hash;
if (frag.length > 2 && frag[1] === 'n') {
$(frag).addClass('highlight');
+ coverage.sel_begin = parseInt(frag.substr(2));
+ coverage.sel_end = coverage.sel_begin + 1;
}
+ $(document)
+ .bind('keydown', 'j', coverage.to_next_chunk)
+ .bind('keydown', 'k', coverage.to_prev_chunk)
+ .bind('keydown', '0', coverage.to_top)
+ .bind('keydown', '1', coverage.to_first_chunk)
+ ;
+
coverage.assign_shortkeys();
};
@@ -108,3 +117,99 @@ coverage.toggle_lines = function(btn, cls) {
}
};
+// The first line selected, and the next line not selected.
+coverage.sel_begin = 0;
+coverage.sel_end = 1;
+
+coverage.to_top = function() {
+ coverage.sel_begin = 0;
+ coverage.sel_end = 1;
+ $("html").animate({scrollTop: 0}, 200);
+};
+
+coverage.to_first_chunk = function() {
+ coverage.sel_begin = 0;
+ coverage.sel_end = 1;
+ coverage.to_next_chunk();
+};
+
+coverage.to_next_chunk = function() {
+ var c = coverage;
+
+ // Find the start of the next colored chunk.
+ var probe = c.sel_end;
+ while (true) {
+ var probe_line = $("#t" + probe);
+ if (probe_line.length === 0) {
+ return;
+ }
+ var color = probe_line.css("background-color");
+ if (color !== "transparent") {
+ break;
+ }
+ probe += 1;
+ }
+
+ // There's a next chunk, `probe` points to it.
+ c.sel_begin = probe;
+
+ // Find the end of this chunk.
+ var next_color = color;
+ while (next_color === color) {
+ probe += 1;
+ next_color = $("#t" + probe).css("background-color");
+ }
+ c.sel_end = probe;
+ coverage.show_selected_chunk();
+};
+
+coverage.to_prev_chunk = function() {
+ var c = coverage;
+
+ // Find the end of the prev colored chunk.
+ var probe = c.sel_begin-1;
+ var color = $("#t" + probe).css("background-color");
+ while (probe > 0 && color === "transparent") {
+ probe -= 1;
+ var probe_line = $("#t" + probe);
+ if (probe_line.length === 0) {
+ return;
+ }
+ color = probe_line.css("background-color");
+ }
+
+ // There's a prev chunk, `probe` points to its last line.
+ c.sel_end = probe+1;
+
+ // Find the beginning of this chunk.
+ var prev_color = color;
+ while (prev_color === color) {
+ probe -= 1;
+ prev_color = $("#t" + probe).css("background-color");
+ }
+ c.sel_begin = probe+1;
+ coverage.show_selected_chunk();
+};
+
+coverage.show_selected_chunk = function() {
+ var c = coverage;
+
+ // Highlight the lines in the chunk
+ $(".linenos p").removeClass("highlight");
+ var probe = c.sel_begin;
+ while (probe > 0 && probe < c.sel_end) {
+ $("#n" + probe).addClass("highlight");
+ probe += 1;
+ }
+
+ // Scroll the page if the chunk isn't fully visible.
+ var top = $("#t" + c.sel_begin);
+ var next = $("#t" + c.sel_end);
+
+ if (!top.isOnScreen() || !next.isOnScreen()) {
+ // Need to move the page.
+ var top_pos = parseInt(top.offset().top);
+ $("html").animate({scrollTop: top_pos-30}, 300);
+ }
+};
+
diff --git a/coverage/htmlfiles/index.html b/coverage/htmlfiles/index.html
index f03c325..fb37d40 100644
--- a/coverage/htmlfiles/index.html
+++ b/coverage/htmlfiles/index.html
@@ -55,7 +55,7 @@
<tbody>
{% for file in files %}
<tr class='file'>
- <td class='name left'><a href='{{file.html_filename}}'>{{file.cu.name}}</a></td>
+ <td class='name left'><a href='{{file.html_filename}}'>{{file.name}}</a></td>
<td>{{file.nums.n_statements}}</td>
<td>{{file.nums.n_missing}}</td>
<td>{{file.nums.n_excluded}}</td>
diff --git a/coverage/htmlfiles/jquery.isonscreen.js b/coverage/htmlfiles/jquery.isonscreen.js
new file mode 100644
index 0000000..b147aff
--- /dev/null
+++ b/coverage/htmlfiles/jquery.isonscreen.js
@@ -0,0 +1,53 @@
+/* Copyright (c) 2010
+ * @author Laurence Wheway
+ * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
+ * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
+ *
+ * @version 1.2.0
+ */
+(function($) {
+ jQuery.extend({
+ isOnScreen: function(box, container) {
+ //ensure numbers come in as intgers (not strings) and remove 'px' is it's there
+ for(var i in box){box[i] = parseFloat(box[i])};
+ for(var i in container){container[i] = parseFloat(container[i])};
+
+ if(!container){
+ container = {
+ left: $(window).scrollLeft(),
+ top: $(window).scrollTop(),
+ width: $(window).width(),
+ height: $(window).height()
+ }
+ }
+
+ if( box.left+box.width-container.left > 0 &&
+ box.left < container.width+container.left &&
+ box.top+box.height-container.top > 0 &&
+ box.top < container.height+container.top
+ ) return true;
+ return false;
+ }
+ })
+
+
+ jQuery.fn.isOnScreen = function (container) {
+ for(var i in container){container[i] = parseFloat(container[i])};
+
+ if(!container){
+ container = {
+ left: $(window).scrollLeft(),
+ top: $(window).scrollTop(),
+ width: $(window).width(),
+ height: $(window).height()
+ }
+ }
+
+ if( $(this).offset().left+$(this).width()-container.left > 0 &&
+ $(this).offset().left < container.width+container.left &&
+ $(this).offset().top+$(this).height()-container.top > 0 &&
+ $(this).offset().top < container.height+container.top
+ ) return true;
+ return false;
+ }
+})(jQuery);
diff --git a/coverage/htmlfiles/pyfile.html b/coverage/htmlfiles/pyfile.html
index d9d0e4c..6f99e6a 100644
--- a/coverage/htmlfiles/pyfile.html
+++ b/coverage/htmlfiles/pyfile.html
@@ -9,6 +9,7 @@
<link rel='stylesheet' href='style.css' type='text/css'>
<script type='text/javascript' src='jquery-1.4.3.min.js'></script>
<script type='text/javascript' src='jquery.hotkeys.js'></script>
+ <script type='text/javascript' src='jquery.isonscreen.js'></script>
<script type='text/javascript' src='coverage_html.js'></script>
<script type='text/javascript' charset='utf-8'>
jQuery(document).ready(coverage.pyfile_ready);
diff --git a/coverage/misc.py b/coverage/misc.py
index 4218536..4f3748f 100644
--- a/coverage/misc.py
+++ b/coverage/misc.py
@@ -1,5 +1,10 @@
"""Miscellaneous stuff for Coverage."""
+import inspect
+from coverage.backward import md5, sorted # pylint: disable=W0622
+from coverage.backward import string_class, to_bytes
+
+
def nice_pair(pair):
"""Make a nice string representation of a pair of numbers.
@@ -68,12 +73,51 @@ def bool_or_none(b):
return bool(b)
+class Hasher(object):
+ """Hashes Python data into md5."""
+ def __init__(self):
+ self.md5 = md5()
+
+ def update(self, v):
+ """Add `v` to the hash, recursively if needed."""
+ self.md5.update(to_bytes(str(type(v))))
+ if isinstance(v, string_class):
+ self.md5.update(to_bytes(v))
+ elif isinstance(v, (int, float)):
+ self.update(str(v))
+ elif isinstance(v, (tuple, list)):
+ for e in v:
+ self.update(e)
+ elif isinstance(v, dict):
+ keys = v.keys()
+ for k in sorted(keys):
+ self.update(k)
+ self.update(v[k])
+ else:
+ for k in dir(v):
+ if k.startswith('__'):
+ continue
+ a = getattr(v, k)
+ if inspect.isroutine(a):
+ continue
+ self.update(k)
+ self.update(a)
+
+ def digest(self):
+ """Retrieve the digest of the hash."""
+ return self.md5.digest()
+
+
class CoverageException(Exception):
"""An exception specific to Coverage."""
pass
class NoSource(CoverageException):
- """Used to indicate we couldn't find the source for a module."""
+ """We couldn't find the source for a module."""
+ pass
+
+class NotPython(CoverageException):
+ """A source file turned out not to be parsable Python."""
pass
class ExceptionDuringRun(CoverageException):
diff --git a/coverage/parser.py b/coverage/parser.py
index 8ad4e05..d033f6d 100644
--- a/coverage/parser.py
+++ b/coverage/parser.py
@@ -5,7 +5,8 @@ import glob, opcode, os, re, sys, token, tokenize
from coverage.backward import set, sorted, StringIO # pylint: disable=W0622
from coverage.backward import open_source
from coverage.bytecode import ByteCodes, CodeObjects
-from coverage.misc import nice_pair, CoverageException, NoSource, expensive
+from coverage.misc import nice_pair, expensive
+from coverage.misc import CoverageException, NoSource, NotPython
class CodeParser(object):
@@ -316,7 +317,7 @@ class ByteParser(object):
self.code = compile(text + '\n', filename, "exec")
except SyntaxError:
_, synerr, _ = sys.exc_info()
- raise CoverageException(
+ raise NotPython(
"Couldn't parse '%s' as Python source: '%s' at line %d" %
(filename, synerr.msg, synerr.lineno)
)
diff --git a/coverage/report.py b/coverage/report.py
index 0fb353a..6c5510a 100644
--- a/coverage/report.py
+++ b/coverage/report.py
@@ -2,7 +2,7 @@
import fnmatch, os
from coverage.codeunit import code_unit_factory
-from coverage.misc import CoverageException, NoSource
+from coverage.misc import CoverageException, NoSource, NotPython
class Reporter(object):
"""A base class for all reporters."""
@@ -61,7 +61,13 @@ class Reporter(object):
def report_files(self, report_fn, morfs, config, directory=None):
"""Run a reporting function on a number of morfs.
- `report_fn` is called for each relative morf in `morfs`.
+ `report_fn` is called for each relative morf in `morfs`. It is called
+ as::
+
+ report_fn(code_unit, analysis)
+
+ where `code_unit` is the `CodeUnit` for the morf, and `analysis` is
+ the `Analysis` for the morf.
`config` is a CoverageConfig instance.
@@ -78,6 +84,6 @@ class Reporter(object):
for cu in self.code_units:
try:
report_fn(cu, self.coverage._analyze(cu))
- except NoSource:
+ except (NoSource, NotPython):
if not self.ignore_errors:
raise
diff --git a/doc/source.rst b/doc/source.rst
index 3f0a156..8700bcb 100644
--- a/doc/source.rst
+++ b/doc/source.rst
@@ -43,9 +43,8 @@ the set.
The ``include`` and ``omit`` filename patterns follow typical shell syntax:
``*`` matches any number of characters and ``?`` matches a single character.
-The full semantics are specified in the `fnmatch docs`_.
-
-.. _fnmatch docs: http://docs.python.org/library/fnmatch.html
+Patterns that start with a wildcard character are used as-is, other patterns
+are interpreted relative to the current directory.
The ``source``, ``include``, and ``omit`` values all work together to determine
the source that will be measured.
diff --git a/lab/trace_sample.py b/lab/trace_sample.py
index 6d616a5..2fec942 100644
--- a/lab/trace_sample.py
+++ b/lab/trace_sample.py
@@ -7,12 +7,11 @@ def trace(frame, event, arg):
#if event == 'line':
global nest
- print "%s%s %s %d (%r)" % (
+ print "%s%s %s %d" % (
" " * nest,
event,
os.path.basename(frame.f_code.co_filename),
frame.f_lineno,
- arg
)
if event == 'call':
diff --git a/setup.py b/setup.py
index 5a39ad2..3b4854c 100644
--- a/setup.py
+++ b/setup.py
@@ -116,7 +116,7 @@ if sys.version_info >= (3, 0):
# extension. Try it with, and if it fails, try it without.
try:
setup(**setup_args)
-except:
+except: # pylint: disable=W0702
if 'ext_modules' not in setup_args:
raise
msg = "Couldn't install with extension module, trying without it..."
diff --git a/test/coveragetest.py b/test/coveragetest.py
index ebff65a..93cffa8 100644
--- a/test/coveragetest.py
+++ b/test/coveragetest.py
@@ -1,9 +1,10 @@
"""Base test case class for coverage testing."""
-import imp, os, random, shlex, shutil, sys, tempfile, textwrap
+import glob, imp, os, random, shlex, shutil, sys, tempfile, textwrap
import coverage
from coverage.backward import sorted, StringIO # pylint: disable=W0622
+from coverage.backward import to_bytes
from backtest import run_command
from backunittest import TestCase
@@ -31,6 +32,9 @@ class CoverageTest(TestCase):
run_in_temp_dir = True
def setUp(self):
+ # tearDown will restore the original sys.path
+ self.old_syspath = sys.path[:]
+
if self.run_in_temp_dir:
# Create a temporary directory.
self.noise = str(random.random())[2:]
@@ -40,9 +44,7 @@ class CoverageTest(TestCase):
self.old_dir = os.getcwd()
os.chdir(self.temp_dir)
-
# Modules should be importable from this temp directory.
- self.old_syspath = sys.path[:]
sys.path.insert(0, '')
# Keep a counter to make every call to check_coverage unique.
@@ -66,10 +68,10 @@ class CoverageTest(TestCase):
self.old_modules = dict(sys.modules)
def tearDown(self):
- if self.run_in_temp_dir:
- # Restore the original sys.path.
- sys.path = self.old_syspath
+ # Restore the original sys.path.
+ sys.path = self.old_syspath
+ if self.run_in_temp_dir:
# Get rid of the temporary directory.
os.chdir(self.old_dir)
shutil.rmtree(self.temp_root)
@@ -81,6 +83,9 @@ class CoverageTest(TestCase):
sys.stdout = self.old_stdout
sys.stderr = self.old_stderr
+ self.clean_modules()
+
+ def clean_modules(self):
# Remove any new modules imported during the test run. This lets us
# import the same source files for more than one test.
for m in [m for m in sys.modules if m not in self.old_modules]:
@@ -145,14 +150,31 @@ class CoverageTest(TestCase):
os.makedirs(dirs)
# Create the file.
- f = open(filename, 'w')
+ f = open(filename, 'wb')
try:
- f.write(text)
+ f.write(to_bytes(text))
finally:
f.close()
return filename
+ def clean_local_file_imports(self):
+ """Clean up the results of calls to `import_local_file`.
+
+ Use this if you need to `import_local_file` the same file twice in
+ one test.
+
+ """
+ # So that we can re-import files, clean them out first.
+ self.clean_modules()
+ # Also have to clean out the .pyc file, since the timestamp
+ # resolution is only one second, a changed file might not be
+ # picked up.
+ for pyc in glob.glob('*.pyc'):
+ os.remove(pyc)
+ if os.path.exists("__pycache__"):
+ shutil.rmtree("__pycache__")
+
def import_local_file(self, modname):
"""Import a local file as a module.
@@ -338,6 +360,16 @@ class CoverageTest(TestCase):
flist2_nice = [self.nice_file(f) for f in flist2]
self.assertSameElements(flist1_nice, flist2_nice)
+ def assert_exists(self, fname):
+ """Assert that `fname` is a file that exists."""
+ msg = "File %r should exist" % fname
+ self.assert_(os.path.exists(fname), msg)
+
+ def assert_doesnt_exist(self, fname):
+ """Assert that `fname` is a file that doesn't exist."""
+ msg = "File %r shouldn't exist" % fname
+ self.assert_(not os.path.exists(fname), msg)
+
def command_line(self, args, ret=OK, _covpkg=None):
"""Run `args` through the command line.
diff --git a/test/modules/usepkgs.py b/test/modules/usepkgs.py
index 208e3f3..93c7d90 100644
--- a/test/modules/usepkgs.py
+++ b/test/modules/usepkgs.py
@@ -1,2 +1,4 @@
import pkg1.p1a, pkg1.p1b
import pkg2.p2a, pkg2.p2b
+import othermods.othera, othermods.otherb
+import othermods.sub.osa, othermods.sub.osb
diff --git a/test/othermods/__init__.py b/test/othermods/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/othermods/__init__.py
diff --git a/test/othermods/othera.py b/test/othermods/othera.py
new file mode 100644
index 0000000..7889692
--- /dev/null
+++ b/test/othermods/othera.py
@@ -0,0 +1,2 @@
+o = 1
+p = 2
diff --git a/test/othermods/otherb.py b/test/othermods/otherb.py
new file mode 100644
index 0000000..2bd8a44
--- /dev/null
+++ b/test/othermods/otherb.py
@@ -0,0 +1,2 @@
+q = 3
+r = 4
diff --git a/test/othermods/sub/__init__.py b/test/othermods/sub/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/othermods/sub/__init__.py
diff --git a/test/othermods/sub/osa.py b/test/othermods/sub/osa.py
new file mode 100644
index 0000000..0139d28
--- /dev/null
+++ b/test/othermods/sub/osa.py
@@ -0,0 +1,2 @@
+s = 5
+t = 6
diff --git a/test/othermods/sub/osb.py b/test/othermods/sub/osb.py
new file mode 100644
index 0000000..b024b14
--- /dev/null
+++ b/test/othermods/sub/osb.py
@@ -0,0 +1,2 @@
+u = 7
+v = 8
diff --git a/test/test_api.py b/test/test_api.py
index aee0734..31d8988 100644
--- a/test/test_api.py
+++ b/test/test_api.py
@@ -304,7 +304,14 @@ class SourceOmitIncludeTest(CoverageTest):
def setUp(self):
super(SourceOmitIncludeTest, self).setUp()
# Parent class saves and restores sys.path, we can just modify it.
- sys.path.append(self.nice_file(os.path.dirname(__file__), 'modules'))
+ #sys.path.append(self.nice_file(os.path.dirname(__file__), 'modules'))
+ self.old_dir = os.getcwd()
+ os.chdir(self.nice_file(os.path.dirname(__file__), 'modules'))
+ sys.path.append(".")
+
+ def tearDown(self):
+ os.chdir(self.old_dir)
+ super(SourceOmitIncludeTest, self).tearDown()
def coverage_usepkgs_summary(self, **kwargs):
"""Run coverage on usepkgs and return the line summary.
@@ -318,52 +325,94 @@ class SourceOmitIncludeTest(CoverageTest):
cov.stop()
return cov.data.summary()
+ def filenames_in_summary(self, summary, filenames):
+ """Assert the `filenames` are in the keys of `summary`."""
+ for filename in filenames.split():
+ self.assert_(filename in summary,
+ "%s should be in %r" % (filename, summary)
+ )
+
+ def filenames_not_in_summary(self, summary, filenames):
+ """Assert the `filenames` are not in the keys of `summary`."""
+ for filename in filenames.split():
+ self.assert_(filename not in summary,
+ "%s should not be in %r" % (filename, summary)
+ )
+
def test_nothing_specified(self):
lines = self.coverage_usepkgs_summary()
- self.assertEqual(lines['p1a.py'], 3)
- self.assertEqual(lines['p1b.py'], 3)
- self.assertEqual(lines['p2a.py'], 3)
- self.assertEqual(lines['p2b.py'], 3)
+ self.filenames_in_summary(lines,
+ "p1a.py p1b.py p2a.py p2b.py othera.py otherb.py osa.py osb.py"
+ )
+ self.filenames_not_in_summary(lines,
+ "p1c.py"
+ )
# Because there was no source= specified, we don't search for
# unexecuted files.
- self.assert_('p1c.py' not in lines)
def test_source_package(self):
lines = self.coverage_usepkgs_summary(source=["pkg1"])
- self.assertEqual(lines['p1a.py'], 3)
- self.assertEqual(lines['p1b.py'], 3)
- self.assert_('p2a.py' not in lines)
- self.assert_('p2b.py' not in lines)
+ self.filenames_in_summary(lines,
+ "p1a.py p1b.py"
+ )
+ self.filenames_not_in_summary(lines,
+ "p2a.py p2b.py othera.py otherb.py osa.py osb.py"
+ )
# Because source= was specified, we do search for unexecuted files.
self.assertEqual(lines['p1c.py'], 0)
def test_source_package_dotted(self):
lines = self.coverage_usepkgs_summary(source=["pkg1.p1b"])
- self.assert_('p1a.py' not in lines)
- self.assertEqual(lines['p1b.py'], 3)
- self.assert_('p2a.py' not in lines)
- self.assert_('p2b.py' not in lines)
- self.assert_('p1c.py' not in lines)
+ self.filenames_in_summary(lines,
+ "p1b.py"
+ )
+ self.filenames_not_in_summary(lines,
+ "p1a.py p1c.py p2a.py p2b.py othera.py otherb.py osa.py osb.py"
+ )
def test_include(self):
lines = self.coverage_usepkgs_summary(include=["*/p1a.py"])
- self.assertEqual(lines['p1a.py'], 3)
- self.assert_('p1b.py' not in lines)
- self.assert_('p2a.py' not in lines)
- self.assert_('p2b.py' not in lines)
+ self.filenames_in_summary(lines,
+ "p1a.py"
+ )
+ self.filenames_not_in_summary(lines,
+ "p1b.py p1c.py p2a.py p2b.py othera.py otherb.py osa.py osb.py"
+ )
+
+ def test_include_2(self):
+ lines = self.coverage_usepkgs_summary(include=["*a.py"])
+ self.filenames_in_summary(lines,
+ "p1a.py p2a.py othera.py osa.py"
+ )
+ self.filenames_not_in_summary(lines,
+ "p1b.py p1c.py p2b.py otherb.py osb.py"
+ )
def test_omit(self):
lines = self.coverage_usepkgs_summary(omit=["*/p1a.py"])
- self.assert_('p1a.py' not in lines)
- self.assertEqual(lines['p1b.py'], 3)
- self.assertEqual(lines['p2a.py'], 3)
- self.assertEqual(lines['p2b.py'], 3)
+ self.filenames_in_summary(lines,
+ "p1b.py p2a.py p2b.py"
+ )
+ self.filenames_not_in_summary(lines,
+ "p1a.py p1c.py"
+ )
+
+ def test_omit_2(self):
+ lines = self.coverage_usepkgs_summary(omit=["*a.py"])
+ self.filenames_in_summary(lines,
+ "p1b.py p2b.py otherb.py osb.py"
+ )
+ self.filenames_not_in_summary(lines,
+ "p1a.py p1c.py p2a.py othera.py osa.py"
+ )
def test_omit_and_include(self):
lines = self.coverage_usepkgs_summary(
include=["*/p1*"], omit=["*/p1a.py"]
)
- self.assert_('p1a.py' not in lines)
- self.assertEqual(lines['p1b.py'], 3)
- self.assert_('p2a.py' not in lines)
- self.assert_('p2b.py' not in lines)
+ self.filenames_in_summary(lines,
+ "p1b.py"
+ )
+ self.filenames_not_in_summary(lines,
+ "p1a.py p1c.py p2a.py p2b.py"
+ )
diff --git a/test/test_coverage.py b/test/test_coverage.py
index 31f3aa1..fe81da7 100644
--- a/test/test_coverage.py
+++ b/test/test_coverage.py
@@ -140,12 +140,33 @@ class SimpleStatementTest(CoverageTest):
"""Testing simple single-line statements."""
def test_expression(self):
+ # Bare expressions as statements are tricky: some implementations
+ # optimize some of them away. All implementations seem to count
+ # the implicit return at the end as executable.
+ self.check_coverage("""\
+ 12
+ 23
+ """,
+ ([1,2],[2]), "")
+ self.check_coverage("""\
+ 12
+ 23
+ a = 3
+ """,
+ ([1,2,3],[3]), "")
self.check_coverage("""\
1 + 2
1 + \\
2
""",
- [1,2], "")
+ ([1,2], [2]), "")
+ self.check_coverage("""\
+ 1 + 2
+ 1 + \\
+ 2
+ a = 4
+ """,
+ ([1,2,4], [4]), "")
def test_assert(self):
self.check_coverage("""\
@@ -560,7 +581,7 @@ class SimpleStatementTest(CoverageTest):
c = 6
assert (a,b,c) == (1,3,6)
""",
- ([1,3,5,6,7], [1,3,4,5,6,7]), "")
+ ([1,3,6,7], [1,3,5,6,7], [1,3,4,5,6,7]), "")
class CompoundStatementTest(CoverageTest):
@@ -1683,7 +1704,7 @@ class ReportingTest(CoverageTest):
CoverageException, "No data to report.",
self.command_line, "annotate -d ann"
)
- self.assertFalse(os.path.exists("ann"))
+ self.assert_doesnt_exist("ann")
def test_no_data_to_report_on_html(self):
# Reporting with no data produces a nice message and no output dir.
@@ -1691,7 +1712,7 @@ class ReportingTest(CoverageTest):
CoverageException, "No data to report.",
self.command_line, "html -d htmlcov"
)
- self.assertFalse(os.path.exists("htmlcov"))
+ self.assert_doesnt_exist("htmlcov")
def test_no_data_to_report_on_xml(self):
# Reporting with no data produces a nice message.
diff --git a/test/test_html.py b/test/test_html.py
new file mode 100644
index 0000000..c7b5657
--- /dev/null
+++ b/test/test_html.py
@@ -0,0 +1,155 @@
+"""Tests that HTML generation is awesome."""
+
+import os.path, sys
+import coverage
+sys.path.insert(0, os.path.split(__file__)[0]) # Force relative import for Py3k
+from coveragetest import CoverageTest
+
+class HtmlTest(CoverageTest):
+ """HTML!"""
+
+ def setUp(self):
+ super(HtmlTest, self).setUp()
+
+ # At least one of our tests monkey-patches the version of coverage,
+ # so grab it here to restore it later.
+ self.real_coverage_version = coverage.__version__
+
+ def tearDown(self):
+ coverage.__version__ = self.real_coverage_version
+ super(HtmlTest, self).tearDown()
+
+ def create_initial_files(self):
+ """Create the source files we need to run these tests."""
+ self.make_file("main_file.py", """\
+ import helper1, helper2
+ helper1.func1(12)
+ helper2.func2(12)
+ """)
+ self.make_file("helper1.py", """\
+ def func1(x):
+ if x % 2:
+ print("odd")
+ """)
+ self.make_file("helper2.py", """\
+ def func2(x):
+ print("x is %d" % x)
+ """)
+
+ def run_coverage(self, **kwargs):
+ """Run coverage on main_file.py, and create an HTML report."""
+ self.clean_local_file_imports()
+ cov = coverage.coverage(**kwargs)
+ cov.start()
+ self.import_local_file("main_file")
+ cov.stop()
+ cov.html_report()
+
+ def remove_html_files(self):
+ """Remove the HTML files created as part of the HTML report."""
+ os.remove("htmlcov/index.html")
+ os.remove("htmlcov/main_file.html")
+ os.remove("htmlcov/helper1.html")
+ os.remove("htmlcov/helper2.html")
+
+ def test_html_created(self):
+ # Test basic HTML generation: files should be created.
+ self.create_initial_files()
+ self.run_coverage()
+
+ self.assert_exists("htmlcov/index.html")
+ self.assert_exists("htmlcov/main_file.html")
+ self.assert_exists("htmlcov/helper1.html")
+ self.assert_exists("htmlcov/helper2.html")
+ self.assert_exists("htmlcov/style.css")
+ self.assert_exists("htmlcov/coverage_html.js")
+
+ def test_html_delta_from_source_change(self):
+ # HTML generation can create only the files that have changed.
+ # In this case, helper1 changes because its source is different.
+ self.create_initial_files()
+ self.run_coverage()
+ index1 = open("htmlcov/index.html").read()
+ self.remove_html_files()
+
+ # Now change a file and do it again
+ self.make_file("helper1.py", """\
+ def func1(x): # A nice function
+ if x % 2:
+ print("odd")
+ """)
+
+ self.run_coverage()
+
+ # Only the changed files should have been created.
+ self.assert_exists("htmlcov/index.html")
+ self.assert_exists("htmlcov/helper1.html")
+ self.assert_doesnt_exist("htmlcov/main_file.html")
+ self.assert_doesnt_exist("htmlcov/helper2.html")
+ index2 = open("htmlcov/index.html").read()
+ self.assertMultiLineEqual(index1, index2)
+
+ def test_html_delta_from_coverage_change(self):
+ # HTML generation can create only the files that have changed.
+ # In this case, helper1 changes because its coverage is different.
+ self.create_initial_files()
+ self.run_coverage()
+ self.remove_html_files()
+
+ # Now change a file and do it again
+ self.make_file("main_file.py", """\
+ import helper1, helper2
+ helper1.func1(23)
+ helper2.func2(23)
+ """)
+
+ self.run_coverage()
+
+ # Only the changed files should have been created.
+ self.assert_exists("htmlcov/index.html")
+ self.assert_exists("htmlcov/helper1.html")
+ self.assert_exists("htmlcov/main_file.html")
+ self.assert_doesnt_exist("htmlcov/helper2.html")
+
+ def test_html_delta_from_settings_change(self):
+ # HTML generation can create only the files that have changed.
+ # In this case, everything changes because the coverage settings have
+ # changed.
+ self.create_initial_files()
+ self.run_coverage()
+ index1 = open("htmlcov/index.html").read()
+ self.remove_html_files()
+
+ self.run_coverage(timid=True)
+
+ # All the files have been reported again.
+ self.assert_exists("htmlcov/index.html")
+ self.assert_exists("htmlcov/helper1.html")
+ self.assert_exists("htmlcov/main_file.html")
+ self.assert_exists("htmlcov/helper2.html")
+ index2 = open("htmlcov/index.html").read()
+ self.assertMultiLineEqual(index1, index2)
+
+ def test_html_delta_from_coverage_version_change(self):
+ # HTML generation can create only the files that have changed.
+ # In this case, everything changes because the coverage version has
+ # changed.
+ self.create_initial_files()
+ self.run_coverage()
+ index1 = open("htmlcov/index.html").read()
+ self.remove_html_files()
+
+ # "Upgrade" coverage.py!
+ coverage.__version__ = "XYZZY"
+
+ self.run_coverage()
+
+ # All the files have been reported again.
+ self.assert_exists("htmlcov/index.html")
+ self.assert_exists("htmlcov/helper1.html")
+ self.assert_exists("htmlcov/main_file.html")
+ self.assert_exists("htmlcov/helper2.html")
+ index2 = open("htmlcov/index.html").read()
+ fixed_index2 = index2.replace("XYZZY", self.real_coverage_version)
+ self.assertMultiLineEqual(index1, fixed_index2)
+
diff --git a/test/test_misc.py b/test/test_misc.py
new file mode 100644
index 0000000..72f5caa
--- /dev/null
+++ b/test/test_misc.py
@@ -0,0 +1,28 @@
+"""Tests of miscellaneous stuff."""
+
+import os, sys
+
+from coverage.misc import Hasher
+sys.path.insert(0, os.path.split(__file__)[0]) # Force relative import for Py3k
+from coveragetest import CoverageTest
+
+class HasherTest(CoverageTest):
+ """Test our wrapper of md5 hashing."""
+
+ def test_string_hashing(self):
+ h1 = Hasher()
+ h1.update("Hello, world!")
+ h2 = Hasher()
+ h2.update("Goodbye!")
+ h3 = Hasher()
+ h3.update("Hello, world!")
+ self.assertNotEqual(h1.digest(), h2.digest())
+ self.assertEqual(h1.digest(), h3.digest())
+
+ def test_dict_hashing(self):
+ h1 = Hasher()
+ h1.update({'a': 17, 'b': 23})
+ h2 = Hasher()
+ h2.update({'b': 23, 'a': 17})
+ self.assertEqual(h1.digest(), h2.digest())
+
diff --git a/test/test_process.py b/test/test_process.py
index 917abc7..47f3a08 100644
--- a/test/test_process.py
+++ b/test/test_process.py
@@ -24,9 +24,9 @@ class ProcessTest(CoverageTest):
w = "world"
""")
- self.assertFalse(os.path.exists(".coverage"))
+ self.assert_doesnt_exist(".coverage")
self.run_command("coverage -x mycode.py")
- self.assertTrue(os.path.exists(".coverage"))
+ self.assert_exists(".coverage")
def test_environment(self):
# Checks that we can import modules from the test directory at all!
@@ -37,9 +37,9 @@ class ProcessTest(CoverageTest):
print ('done')
""")
- self.assertFalse(os.path.exists(".coverage"))
+ self.assert_doesnt_exist(".coverage")
out = self.run_command("coverage -x mycode.py")
- self.assertTrue(os.path.exists(".coverage"))
+ self.assert_exists(".coverage")
self.assertEqual(out, 'done\n')
def test_combine_parallel_data(self):
@@ -56,18 +56,18 @@ class ProcessTest(CoverageTest):
out = self.run_command("coverage -x -p b_or_c.py b")
self.assertEqual(out, 'done\n')
- self.assertFalse(os.path.exists(".coverage"))
+ self.assert_doesnt_exist(".coverage")
out = self.run_command("coverage -x -p b_or_c.py c")
self.assertEqual(out, 'done\n')
- self.assertFalse(os.path.exists(".coverage"))
+ self.assert_doesnt_exist(".coverage")
# After two -p runs, there should be two .coverage.machine.123 files.
self.assertEqual(self.number_of_data_files(), 2)
# Combine the parallel coverage data files into .coverage .
self.run_command("coverage -c")
- self.assertTrue(os.path.exists(".coverage"))
+ self.assert_exists(".coverage")
# After combining, there should be only the .coverage file.
self.assertEqual(self.number_of_data_files(), 1)
@@ -97,19 +97,19 @@ class ProcessTest(CoverageTest):
out = self.run_command("coverage run b_or_c.py b")
self.assertEqual(out, 'done\n')
- self.assertFalse(os.path.exists(".coverage"))
+ self.assert_doesnt_exist(".coverage")
out = self.run_command("coverage run b_or_c.py c")
self.assertEqual(out, 'done\n')
- self.assertFalse(os.path.exists(".coverage"))
+ self.assert_doesnt_exist(".coverage")
# After two runs, there should be two .coverage.machine.123 files.
self.assertEqual(self.number_of_data_files(), 2)
# Combine the parallel coverage data files into .coverage .
self.run_command("coverage combine")
- self.assertTrue(os.path.exists(".coverage"))
- self.assertTrue(os.path.exists(".coveragerc"))
+ self.assert_exists(".coverage")
+ self.assert_exists(".coveragerc")
# After combining, there should be only the .coverage file.
self.assertEqual(self.number_of_data_files(), 1)
@@ -242,7 +242,7 @@ class ProcessTest(CoverageTest):
out = self.run_command("coverage run -p fork.py")
self.assertEqual(out, 'Child!\n')
- self.assertFalse(os.path.exists(".coverage"))
+ self.assert_doesnt_exist(".coverage")
# After running the forking program, there should be two
# .coverage.machine.123 files.
@@ -250,7 +250,7 @@ class ProcessTest(CoverageTest):
# Combine the parallel coverage data files into .coverage .
self.run_command("coverage -c")
- self.assertTrue(os.path.exists(".coverage"))
+ self.assert_exists(".coverage")
# After combining, there should be only the .coverage file.
self.assertEqual(self.number_of_data_files(), 1)
diff --git a/test/test_summary.py b/test/test_summary.py
index 5a68912..b4b9938 100644
--- a/test/test_summary.py
+++ b/test/test_summary.py
@@ -1,6 +1,6 @@
"""Test text-based summary reporting for coverage.py"""
-import os, re, sys, textwrap
+import os, re, sys
import coverage
from coverage.backward import StringIO
@@ -141,6 +141,8 @@ class SummaryTest2(CoverageTest):
sys.path.append(self.nice_file(os.path.dirname(__file__), 'modules'))
def test_empty_files(self):
+ # Shows that empty files like __init__.py are listed as having zero
+ # statements, not one statement.
cov = coverage.coverage()
cov.start()
import usepkgs # pylint: disable=F0401,W0612
@@ -150,16 +152,6 @@ class SummaryTest2(CoverageTest):
cov.report(file=repout, show_missing=False)
report = repout.getvalue().replace('\\', '/')
- self.assertMultiLineEqual(report, textwrap.dedent("""\
- Name Stmts Miss Cover
- ------------------------------------------------
- test/modules/pkg1/__init__ 1 0 100%
- test/modules/pkg1/p1a 3 0 100%
- test/modules/pkg1/p1b 3 0 100%
- test/modules/pkg2/__init__ 0 0 100%
- test/modules/pkg2/p2a 3 0 100%
- test/modules/pkg2/p2b 3 0 100%
- test/modules/usepkgs 2 0 100%
- ------------------------------------------------
- TOTAL 15 0 100%
- """))
+ report = re.sub(r"\s+", " ", report)
+ self.assert_("test/modules/pkg1/__init__ 1 0 100%" in report)
+ self.assert_("test/modules/pkg2/__init__ 0 0 100%" in report)
diff --git a/test/test_testing.py b/test/test_testing.py
index 2461a08..bcf7d28 100644
--- a/test/test_testing.py
+++ b/test/test_testing.py
@@ -121,3 +121,12 @@ class CoverageTestTest(CoverageTest):
self.assertEqual(self.file_text("dos.txt"), "Hello\r\n")
self.make_file("mac.txt", "Hello\n", newline="\r")
self.assertEqual(self.file_text("mac.txt"), "Hello\r")
+
+ def test_file_exists(self):
+ self.make_file("whoville.txt", "We are here!")
+ self.assert_exists("whoville.txt")
+ self.assert_doesnt_exist("shadow.txt")
+ self.assertRaises(AssertionError, self.assert_doesnt_exist,
+ "whoville.txt")
+ self.assertRaises(AssertionError, self.assert_exists, "shadow.txt")
+