diff options
| author | Ned Batchelder <ned@nedbatchelder.com> | 2011-04-10 08:42:31 -0400 |
|---|---|---|
| committer | Ned Batchelder <ned@nedbatchelder.com> | 2011-04-10 08:42:31 -0400 |
| commit | 52e07c90b77b1d07ed95a6195ec5d895f0988224 (patch) | |
| tree | 325346a20aedac88b3297919f5c1c89f79de7aed | |
| parent | d4dd809d27ad7f31431a4ef61309b6951ad41d9b (diff) | |
| parent | 822b7c82f58bbd6f2b38cc98c7881cc405d0c69e (diff) | |
| download | python-coveragepy-52e07c90b77b1d07ed95a6195ec5d895f0988224.tar.gz | |
Merge Brett's __main__.py file for the tree.
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 @@ -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':
@@ -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") + |
