Switch to upstream source from tarball ‘python-coverage_4.2+dfsg.1.orig.tar.gz’.upstreamupstream/4.2+dfsg.1
authorBen Finney <[email protected]>
Mon, 8 Aug 2016 10:15:59 +0000 (8 20:15 +1000)
committerBen Finney <[email protected]>
Mon, 8 Aug 2016 10:15:59 +0000 (8 20:15 +1000)
64 files changed:
.travis.yml
AUTHORS.txt
CHANGES.rst
PKG-INFO
README.rst
coverage.egg-info/PKG-INFO
coverage.egg-info/SOURCES.txt
coverage/backward.py
coverage/cmdline.py
coverage/collector.py
coverage/config.py
coverage/control.py
coverage/debug.py
coverage/htmlfiles/coverage_html.js
coverage/htmlfiles/keybd_closed.png
coverage/htmlfiles/keybd_open.png
coverage/htmlfiles/style.css
coverage/misc.py
coverage/multiproc.py [moved from coverage/monkey.py with 59% similarity]
coverage/parser.py
coverage/python.py
coverage/report.py
coverage/results.py
coverage/summary.py
coverage/templite.py
coverage/test_helpers.py [deleted file]
coverage/version.py
doc/changes.rst
doc/cmd.rst
doc/conf.py
doc/config.rst
doc/index.rst
doc/python-coverage.1.txt
doc/requirements.pip
doc/trouble.rst
howto.txt
pylintrc
requirements/dev.pip
tests/coveragetest.py
tests/goldtest.py
tests/js/index.html
tests/js/tests.js
tests/modules/process_test/__init__.py [new file with mode: 0644]
tests/modules/process_test/try_execfile.py [moved from tests/try_execfile.py with 91% similarity]
tests/qunit/qunit.css [deleted file]
tests/qunit/qunit.js [deleted file]
tests/test_api.py
tests/test_arcs.py
tests/test_cmdline.py
tests/test_concurrency.py
tests/test_config.py
tests/test_coverage.py
tests/test_execfile.py
tests/test_farm.py
tests/test_files.py
tests/test_html.py
tests/test_parser.py
tests/test_phystokens.py
tests/test_plugins.py
tests/test_process.py
tests/test_summary.py
tests/test_testing.py
tests/test_xml.py
tox.ini

index f3c93a5..1b86b34 100644 (file)
@@ -13,7 +13,6 @@ env:
   - TOXENV=py27
   - TOXENV=py33
   - TOXENV=py34
-  - TOXENV=py35
   - TOXENV=pypy
   - TOXENV=py27 COVERAGE_COVERAGE=yes
 
index 19401a5..4d6e1dc 100644 (file)
@@ -21,6 +21,8 @@ Christian Heimes
 Christine Lytwynec
 Christoph Zwerschke
 Conrad Ho
+Dan Riti
+Dan Wandschneider
 Danek Duvall
 Danny Allen
 David Christian
@@ -43,6 +45,7 @@ JT Olds
 Jessamyn Smith
 Jon Chappell
 Joseph Tate
+Josh Williams
 Julian Berman
 Krystian Kichewko
 Leonardo Pistone
@@ -51,9 +54,11 @@ Marc Abramowitz
 Marcus Cobden
 Mark van der Wal
 Martin Fuzzey
+Matthew Boehm
 Matthew Desmarais
 Max Linke
 Mickie Betz
+Nathan Land
 Noel O'Boyle
 Pablo Carballo
 Patrick Mezard
@@ -62,6 +67,7 @@ Rodrigue Cloutier
 Roger Hu
 Ross Lawley
 Sandra Martocchia
+Scott Belden
 Sigve Tjora
 Stan Hu
 Stefan Behnel
@@ -69,5 +75,6 @@ Steve Leonard
 Steve Peak
 Ted Wexler
 Titus Brown
+Ville Skyttä 
 Yury Selivanov
 Zooko Wilcox-O'Hearn
index a1dec7f..c9f5677 100644 (file)
@@ -5,6 +5,82 @@
 Change history for Coverage.py
 ==============================
 
+Version 4.2 --- 2016-07-26
+--------------------------
+
+- Since ``concurrency=multiprocessing`` uses subprocesses, options specified on
+  the coverage.py command line will not be communicated down to them.  Only
+  options in the configuration file will apply to the subprocesses.
+  Previously, the options didn't apply to the subprocesses, but there was no
+  indication.  Now it is an error to use ``--concurrency=multiprocessing`` and
+  other run-affecting options on the command line.  This prevents
+  failures like those reported in `issue 495`_.
+
+- Filtering the HTML report is now faster, thanks to Ville Skyttä.
+
+.. _issue 495: https://bitbucket.org/ned/coveragepy/issues/495/branch-and-concurrency-are-conflicting
+
+
+Version 4.2b1 --- 2016-07-04
+----------------------------
+
+Work from the PyCon 2016 Sprints!
+
+- BACKWARD INCOMPATIBILITY: the ``coverage combine`` command now ignores an
+  existing ``.coverage`` data file.  It used to include that file in its
+  combining.  This caused confusing results, and extra tox "clean" steps.  If
+  you want the old behavior, use the new ``coverage combine --append`` option.
+
+- The ``concurrency`` option can now take multiple values, to support programs
+  using multiprocessing and another library such as eventlet.  This is only
+  possible in the configuration file, not from the command line. The
+  configuration file is the only way for sub-processes to all run with the same
+  options.  Fixes `issue 484`_.  Thanks to Josh Williams for prototyping.
+
+- Using a ``concurrency`` setting of ``multiprocessing`` now implies
+  ``--parallel`` so that the main program is measured similarly to the
+  sub-processes.
+
+- When using `automatic subprocess measurement`_, running coverage commands
+  would create spurious data files.  This is now fixed, thanks to diagnosis and
+  testing by Dan Riti.  Closes `issue 492`_.
+
+- A new configuration option, ``report:sort``, controls what column of the
+  text report is used to sort the rows.  Thanks to Dan Wandschneider, this
+  closes `issue 199`_.
+
+- The HTML report has a more-visible indicator for which column is being
+  sorted.  Closes `issue 298`_, thanks to Josh Williams.
+
+- If the HTML report cannot find the source for a file, the message now
+  suggests using the ``-i`` flag to allow the report to continue. Closes
+  `issue 231`_, thanks, Nathan Land.
+
+- When reports are ignoring errors, there's now a warning if a file cannot be
+  parsed, rather than being silently ignored.  Closes `issue 396`_. Thanks,
+  Matthew Boehm.
+
+- A new option for ``coverage debug`` is available: ``coverage debug config``
+  shows the current configuration.  Closes `issue 454`_, thanks to Matthew
+  Boehm.
+
+- Running coverage as a module (``python -m coverage``) no longer shows the
+  program name as ``__main__.py``.  Fixes `issue 478`_.  Thanks, Scott Belden.
+
+- The `test_helpers` module has been moved into a separate pip-installable
+  package: `unittest-mixins`_.
+
+.. _automatic subprocess measurement: http://coverage.readthedocs.io/en/latest/subprocess.html
+.. _issue 199: https://bitbucket.org/ned/coveragepy/issues/199/add-a-way-to-sort-the-text-report
+.. _issue 231: https://bitbucket.org/ned/coveragepy/issues/231/various-default-behavior-in-report-phase
+.. _issue 298: https://bitbucket.org/ned/coveragepy/issues/298/show-in-html-report-that-the-columns-are
+.. _issue 396: https://bitbucket.org/ned/coveragepy/issues/396/coverage-xml-shouldnt-bail-out-on-parse
+.. _issue 454: https://bitbucket.org/ned/coveragepy/issues/454/coverage-debug-config-should-be
+.. _issue 478: https://bitbucket.org/ned/coveragepy/issues/478/help-shows-silly-program-name-when-running
+.. _issue 484: https://bitbucket.org/ned/coveragepy/issues/484/multiprocessing-greenlet-concurrency
+.. _issue 492: https://bitbucket.org/ned/coveragepy/issues/492/subprocess-coverage-strange-detection-of
+.. _unittest-mixins: https://pypi.python.org/pypi/unittest-mixins
+
 
 Version 4.1 --- 2016-05-21
 --------------------------
index 967f63b..fc085c4 100644 (file)
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: coverage
-Version: 4.1
+Version: 4.2
 Summary: Code coverage measurement for Python
 Home-page: https://coverage.readthedocs.io
 Author: Ned Batchelder and others
@@ -30,7 +30,9 @@ Description: .. Licensed under the Apache License: http://www.apache.org/license
         Code repository and issue tracker are on `Bitbucket <http://bitbucket.org/ned/coveragepy>`_,
         with a mirrored repository on `GitHub <https://github.com/nedbat/coveragepy>`_.
         
-        **New in 4.1:** much-improved branch coverage.
+        **New in 4.2:** better support for multiprocessing and combining data.
+        
+        New in 4.1: much-improved branch coverage.
         
         New in 4.0: ``--concurrency``, plugins for non-Python files, setup.cfg
         support, --skip-covered, HTML filtering, and more than 50 issues closed.
index 8cb52b4..25f3d3f 100644 (file)
@@ -22,7 +22,9 @@ Documentation is on `Read the Docs <http://coverage.readthedocs.io>`_.
 Code repository and issue tracker are on `Bitbucket <http://bitbucket.org/ned/coveragepy>`_,
 with a mirrored repository on `GitHub <https://github.com/nedbat/coveragepy>`_.
 
-**New in 4.1:** much-improved branch coverage.
+**New in 4.2:** better support for multiprocessing and combining data.
+
+New in 4.1: much-improved branch coverage.
 
 New in 4.0: ``--concurrency``, plugins for non-Python files, setup.cfg
 support, --skip-covered, HTML filtering, and more than 50 issues closed.
index 967f63b..fc085c4 100644 (file)
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: coverage
-Version: 4.1
+Version: 4.2
 Summary: Code coverage measurement for Python
 Home-page: https://coverage.readthedocs.io
 Author: Ned Batchelder and others
@@ -30,7 +30,9 @@ Description: .. Licensed under the Apache License: http://www.apache.org/license
         Code repository and issue tracker are on `Bitbucket <http://bitbucket.org/ned/coveragepy>`_,
         with a mirrored repository on `GitHub <https://github.com/nedbat/coveragepy>`_.
         
-        **New in 4.1:** much-improved branch coverage.
+        **New in 4.2:** better support for multiprocessing and combining data.
+        
+        New in 4.1: much-improved branch coverage.
         
         New in 4.0: ``--concurrency``, plugins for non-Python files, setup.cfg
         support, --skip-covered, HTML filtering, and more than 50 issues closed.
index f386a2e..7857dff 100644 (file)
@@ -38,7 +38,7 @@ coverage/execfile.py
 coverage/files.py
 coverage/html.py
 coverage/misc.py
-coverage/monkey.py
+coverage/multiproc.py
 coverage/parser.py
 coverage/phystokens.py
 coverage/pickle2json.py
@@ -50,7 +50,6 @@ coverage/report.py
 coverage/results.py
 coverage/summary.py
 coverage/templite.py
-coverage/test_helpers.py
 coverage/version.py
 coverage/xmlreport.py
 coverage.egg-info/PKG-INFO
@@ -145,7 +144,6 @@ tests/test_summary.py
 tests/test_templite.py
 tests/test_testing.py
 tests/test_xml.py
-tests/try_execfile.py
 tests/eggsrc/setup.py
 tests/eggsrc/egg1/__init__.py
 tests/eggsrc/egg1/egg1.py
@@ -268,12 +266,12 @@ tests/modules/pkg2/p2b.py
 tests/modules/plugins/__init__.py
 tests/modules/plugins/a_plugin.py
 tests/modules/plugins/another.py
+tests/modules/process_test/__init__.py
+tests/modules/process_test/try_execfile.py
 tests/moremodules/othermods/__init__.py
 tests/moremodules/othermods/othera.py
 tests/moremodules/othermods/otherb.py
 tests/moremodules/othermods/sub/__init__.py
 tests/moremodules/othermods/sub/osa.py
 tests/moremodules/othermods/sub/osb.py
-tests/qunit/jquery.tmpl.min.js
-tests/qunit/qunit.css
-tests/qunit/qunit.js
\ No newline at end of file
+tests/qunit/jquery.tmpl.min.js
\ No newline at end of file
index 50d49a0..700c3eb 100644 (file)
@@ -6,7 +6,7 @@
 # This file does lots of tricky stuff, so disable a bunch of pylint warnings.
 # pylint: disable=redefined-builtin
 # pylint: disable=unused-import
-# pylint: disable=no-name-in-module
+# pxlint: disable=no-name-in-module
 
 import sys
 
@@ -47,7 +47,7 @@ except ImportError:
 try:
     range = xrange
 except NameError:
-    range = range
+    range = range       # pylint: disable=redefined-variable-type
 
 # shlex.quote is new, but there's an undocumented implementation in "pipes",
 # who knew!?
index 508a439..09e8232 100644 (file)
@@ -22,7 +22,7 @@ class Opts(object):
 
     append = optparse.make_option(
         '-a', '--append', action='store_true',
-        help="Append coverage data to .coverage, otherwise it is started clean with each run.",
+        help="Append coverage data to .coverage, otherwise it starts clean each time.",
     )
     branch = optparse.make_option(
         '', '--branch', action='store_true',
@@ -216,7 +216,7 @@ class GlobalOptionParser(CoverageOptionParser):
 class CmdOptionParser(CoverageOptionParser):
     """Parse one of the new-style commands for coverage.py."""
 
-    def __init__(self, action, options=None, defaults=None, usage=None, description=None):
+    def __init__(self, action, options, defaults=None, usage=None, description=None):
         """Create an OptionParser for a coverage.py command.
 
         `action` is the slug to put into `options.action`.
@@ -233,8 +233,7 @@ class CmdOptionParser(CoverageOptionParser):
             description=description,
         )
         self.set_defaults(action=action, **(defaults or {}))
-        if options:
-            self.add_options(options)
+        self.add_options(options)
         self.cmd = action
 
     def __eq__(self, other):
@@ -274,8 +273,10 @@ CMDS = {
 
     'combine': CmdOptionParser(
         "combine",
-        GLOBAL_ARGS,
-        usage="<path1> <path2> ... <pathN>",
+        [
+            Opts.append,
+            ] + GLOBAL_ARGS,
+        usage="[options] <path1> <path2> ... <pathN>",
         description=(
             "Combine data from multiple coverage files collected "
             "with 'run -p'.  The combined results are written to a single "
@@ -299,7 +300,6 @@ CMDS = {
 
     'erase': CmdOptionParser(
         "erase", GLOBAL_ARGS,
-        usage=" ",
         description="Erase previously collected coverage data.",
     ),
 
@@ -399,6 +399,8 @@ class CoverageScript(object):
         self.coverage = None
 
         self.program_name = os.path.basename(sys.argv[0])
+        if self.program_name == '__main__.py':
+            self.program_name = 'coverage'
         if env.WINDOWS:
             # entry_points={'console_scripts':...} on Windows makes files
             # called coverage.exe, coverage3.exe, and coverage-3.5.exe. These
@@ -443,10 +445,6 @@ class CoverageScript(object):
         if self.do_help(options, args, parser):
             return OK
 
-        # Check for conflicts and problems in the options.
-        if not self.args_ok(options, args):
-            return ERR
-
         # We need to be able to import from the current directory, because
         # plugins may try to, for example, to read Django settings.
         sys.path[0] = ''
@@ -482,7 +480,8 @@ class CoverageScript(object):
             return self.do_run(options, args)
 
         elif options.action == "combine":
-            self.coverage.load()
+            if options.append:
+                self.coverage.load()
             data_dirs = args or None
             self.coverage.combine(data_dirs)
             self.coverage.save()
@@ -521,11 +520,9 @@ class CoverageScript(object):
                 self.coverage.set_option("report:fail_under", options.fail_under)
 
             if self.coverage.get_option("report:fail_under"):
-
-                # Total needs to be rounded, but be careful of 0 and 100.
-                if 0 < total < 1:
-                    total = 1
-                elif 99 < total < 100:
+                # Total needs to be rounded, but don't want to report 100
+                # unless it is really 100.
+                if 99 < total < 100:
                     total = 99
                 else:
                     total = round(total)
@@ -591,25 +588,30 @@ class CoverageScript(object):
 
         return False
 
-    def args_ok(self, options, args):
-        """Check for conflicts and problems in the options.
-
-        Returns True if everything is OK, or False if not.
-
-        """
-        if options.action == "run" and not args:
-            self.help_fn("Nothing to do.")
-            return False
-
-        return True
-
     def do_run(self, options, args):
         """Implementation of 'coverage run'."""
 
+        if not args:
+            self.help_fn("Nothing to do.")
+            return ERR
+
         if options.append and self.coverage.get_option("run:parallel"):
             self.help_fn("Can't append to data files in parallel mode.")
             return ERR
 
+        if options.concurrency == "multiprocessing":
+            # Can't set other run-affecting command line options with
+            # multiprocessing.
+            for opt_name in ['branch', 'include', 'omit', 'pylib', 'source', 'timid']:
+                # As it happens, all of these options have no default, meaning
+                # they will be None if they have not been specified.
+                if getattr(options, opt_name) is not None:
+                    self.help_fn(
+                        "Options affecting multiprocessing must be specified "
+                        "in a configuration file."
+                    )
+                    return ERR
+
         if not self.coverage.get_option("run:parallel"):
             if not options.append:
                 self.coverage.erase()
@@ -641,7 +643,7 @@ class CoverageScript(object):
         """Implementation of 'coverage debug'."""
 
         if not args:
-            self.help_fn("What information would you like: data, sys?")
+            self.help_fn("What information would you like: config, data, sys?")
             return ERR
 
         for info in args:
@@ -668,6 +670,11 @@ class CoverageScript(object):
                         print(line)
                 else:
                     print("No data collected")
+            elif info == 'config':
+                print(info_header("config"))
+                config_info = self.coverage.config.__dict__.items()
+                for line in info_formatter(config_info):
+                    print(" %s" % line)
             else:
                 self.help_fn("Don't know what you mean by %r" % info)
                 return ERR
index a296b22..3e28b3b 100644 (file)
@@ -17,7 +17,7 @@ os = isolate_module(os)
 
 try:
     # Use the C extension code when we can, for speed.
-    from coverage.tracer import CTracer, CFileDisposition   # pylint: disable=no-name-in-module
+    from coverage.tracer import CTracer, CFileDisposition
 except ImportError:
     # Couldn't import the C extension, maybe it isn't built.
     if os.getenv('COVERAGE_TEST_TRACER') == 'c':
@@ -65,6 +65,9 @@ class Collector(object):
     # the top, and resumed when they become the top again.
     _collectors = []
 
+    # The concurrency settings we support here.
+    SUPPORTED_CONCURRENCIES = set(["greenlet", "eventlet", "gevent", "thread"])
+
     def __init__(self, should_trace, check_include, timid, branch, warn, concurrency):
         """Create a collector.
 
@@ -86,9 +89,10 @@ class Collector(object):
         `warn` is a warning function, taking a single string message argument,
         to be used if a warning needs to be issued.
 
-        `concurrency` is a string indicating the concurrency library in use.
-        Valid values are "greenlet", "eventlet", "gevent", or "thread" (the
-        default).
+        `concurrency` is a list of strings indicating the concurrency libraries
+        in use.  Valid values are "greenlet", "eventlet", "gevent", or "thread"
+        (the default).  Of these four values, only one can be supplied.  Other
+        values are ignored.
 
         """
         self.should_trace = should_trace
@@ -96,21 +100,26 @@ class Collector(object):
         self.warn = warn
         self.branch = branch
         self.threading = None
-        self.concurrency = concurrency
 
         self.concur_id_func = None
 
+        # We can handle a few concurrency options here, but only one at a time.
+        these_concurrencies = self.SUPPORTED_CONCURRENCIES.intersection(concurrency)
+        if len(these_concurrencies) > 1:
+            raise CoverageException("Conflicting concurrency settings: %s" % concurrency)
+        self.concurrency = these_concurrencies.pop() if these_concurrencies else ''
+
         try:
-            if concurrency == "greenlet":
+            if self.concurrency == "greenlet":
                 import greenlet
                 self.concur_id_func = greenlet.getcurrent
-            elif concurrency == "eventlet":
+            elif self.concurrency == "eventlet":
                 import eventlet.greenthread     # pylint: disable=import-error,useless-suppression
                 self.concur_id_func = eventlet.greenthread.getcurrent
-            elif concurrency == "gevent":
+            elif self.concurrency == "gevent":
                 import gevent                   # pylint: disable=import-error,useless-suppression
                 self.concur_id_func = gevent.getcurrent
-            elif concurrency == "thread" or not concurrency:
+            elif self.concurrency == "thread" or not self.concurrency:
                 # It's important to import threading only if we need it.  If
                 # it's imported early, and the program being measured uses
                 # gevent, then gevent's monkey-patching won't work properly.
@@ -120,7 +129,9 @@ class Collector(object):
                 raise CoverageException("Don't understand concurrency=%s" % concurrency)
         except ImportError:
             raise CoverageException(
-                "Couldn't trace with concurrency=%s, the module isn't installed." % concurrency
+                "Couldn't trace with concurrency=%s, the module isn't installed." % (
+                    self.concurrency,
+                )
             )
 
         # Who-Tests-What is just a hack at the moment, so turn it on with an
index cd66697..d6f5af0 100644 (file)
@@ -9,7 +9,7 @@ import re
 import sys
 
 from coverage.backward import configparser, iitems, string_class
-from coverage.misc import CoverageException, isolate_module
+from coverage.misc import contract, CoverageException, isolate_module
 
 os = isolate_module(os)
 
@@ -191,7 +191,7 @@ class CoverageConfig(object):
         # Options for plugins
         self.plugin_options = {}
 
-    MUST_BE_LIST = ["omit", "include", "debug", "plugins"]
+    MUST_BE_LIST = ["omit", "include", "debug", "plugins", "concurrency"]
 
     def from_args(self, **kwargs):
         """Read config values from `kwargs`."""
@@ -201,6 +201,7 @@ class CoverageConfig(object):
                     v = [v]
                 setattr(self, k, v)
 
+    @contract(filename=str)
     def from_file(self, filename, section_prefix=""):
         """Read configuration from a .rc file.
 
@@ -267,7 +268,7 @@ class CoverageConfig(object):
 
         # [run]
         ('branch', 'run:branch', 'boolean'),
-        ('concurrency', 'run:concurrency'),
+        ('concurrency', 'run:concurrency', 'list'),
         ('cover_pylib', 'run:cover_pylib', 'boolean'),
         ('data_file', 'run:data_file'),
         ('debug', 'run:debug', 'list'),
@@ -290,6 +291,7 @@ class CoverageConfig(object):
         ('precision', 'report:precision', 'int'),
         ('show_missing', 'report:show_missing', 'boolean'),
         ('skip_covered', 'report:skip_covered', 'boolean'),
+        ('sort', 'report:sort'),
 
         # [html]
         ('extra_css', 'html:extra_css'),
index 97d4625..d3e6708 100644 (file)
@@ -24,7 +24,7 @@ from coverage.files import ModuleMatcher, abs_file
 from coverage.html import HtmlReporter
 from coverage.misc import CoverageException, bool_or_none, join_regex
 from coverage.misc import file_be_gone, isolate_module
-from coverage.monkey import patch_multiprocessing
+from coverage.multiproc import patch_multiprocessing
 from coverage.plugin import FileReporter
 from coverage.plugin_support import Plugins
 from coverage.python import PythonFileReporter
@@ -110,12 +110,16 @@ class Coverage(object):
 
         `concurrency` is a string indicating the concurrency library being used
         in the measured code.  Without this, coverage.py will get incorrect
-        results.  Valid strings are "greenlet", "eventlet", "gevent",
-        "multiprocessing", or "thread" (the default).
+        results if these libraries are in use.  Valid strings are "greenlet",
+        "eventlet", "gevent", "multiprocessing", or "thread" (the default).
+        This can also be a list of these strings.
 
         .. versionadded:: 4.0
             The `concurrency` parameter.
 
+        .. versionadded:: 4.2
+            The `concurrency` parameter can now be a list of strings.
+
         """
         # Build our configuration from a number of sources:
         # 1: defaults:
@@ -123,6 +127,7 @@ class Coverage(object):
 
         # 2: from the rcfile, .coveragerc or setup.cfg file:
         if config_file:
+            # pylint: disable=redefined-variable-type
             did_read_rc = False
             # Some API users were specifying ".coveragerc" to mean the same as
             # True, so make it so.
@@ -131,6 +136,7 @@ class Coverage(object):
             specified_file = (config_file is not True)
             if not specified_file:
                 config_file = ".coveragerc"
+            self.config_file = config_file
 
             did_read_rc = self.config.from_file(config_file)
 
@@ -192,6 +198,14 @@ class Coverage(object):
         # Have we measured some data and not harvested it?
         self._measured = False
 
+        # If we have sub-process measurement happening automatically, then we
+        # want any explicit creation of a Coverage object to mean, this process
+        # is already coverage-aware, so don't auto-measure it.  By now, the
+        # auto-creation of a Coverage object has already happened.  But we can
+        # find it and tell it not to save its data.
+        if not env.METACOV:
+            _prevent_sub_process_measurement()
+
     def _init(self):
         """Set all the initial state.
 
@@ -236,10 +250,13 @@ class Coverage(object):
         self.omit = prep_patterns(self.config.omit)
         self.include = prep_patterns(self.config.include)
 
-        concurrency = self.config.concurrency
-        if concurrency == "multiprocessing":
-            patch_multiprocessing()
-            concurrency = None
+        concurrency = self.config.concurrency or []
+        if "multiprocessing" in concurrency:
+            patch_multiprocessing(rcfile=self.config_file)
+            #concurrency = None
+            # Multi-processing uses parallel for the subprocesses, so also use
+            # it for the main process.
+            self.config.parallel = True
 
         self.collector = Collector(
             should_trace=self._should_trace,
@@ -312,7 +329,8 @@ class Coverage(object):
             # When testing, we use PyContracts, which should be considered
             # part of coverage.py, and it uses six. Exclude those directories
             # just as we exclude ourselves.
-            import contracts, six
+            import contracts
+            import six
             for mod in [contracts, six]:
                 self.cover_dirs.append(self._canonical_dir(mod))
 
@@ -911,6 +929,7 @@ class Coverage(object):
                 )
 
         if file_reporter == "python":
+            # pylint: disable=redefined-variable-type
             file_reporter = PythonFileReporter(morf, self)
 
         return file_reporter
@@ -1185,15 +1204,22 @@ def process_startup():
     # https://bitbucket.org/ned/coveragepy/issue/340/keyerror-subpy has more
     # details.
 
-    if hasattr(process_startup, "done"):
+    if hasattr(process_startup, "coverage"):
         # We've annotated this function before, so we must have already
         # started coverage.py in this process.  Nothing to do.
         return None
 
-    process_startup.done = True
     cov = Coverage(config_file=cps, auto_data=True)
+    process_startup.coverage = cov
     cov.start()
     cov._warn_no_data = False
     cov._warn_unimported_source = False
 
     return cov
+
+
+def _prevent_sub_process_measurement():
+    """Stop any subprocess auto-measurement from writing data."""
+    auto_created_coverage = getattr(process_startup, "coverage", None)
+    if auto_created_coverage is not None:
+        auto_created_coverage._auto_data = False
index 719e97b..8ed664c 100644 (file)
@@ -104,3 +104,11 @@ def dump_stack_frames(limit=None, out=None):                # pragma: debugging
     out = out or sys.stdout
     out.write(short_stack(limit=limit))
     out.write("\n")
+
+
+def log(msg, stack=False):                                  # pragma: debugging
+    """Write a log message as forcefully as possible."""
+    with open("/tmp/covlog.txt", "a") as f:
+        f.write("{pid}: {msg}\n".format(pid=os.getpid(), msg=msg))
+        if stack:
+            dump_stack_frames(out=f)
index 51904dd..f6f5de2 100644 (file)
@@ -73,30 +73,28 @@ coverage.wire_up_filter = function () {
         }
         else {
             // Filter table items by value.
-            var hide = $([]);
-            var show = $([]);
+            var hidden = 0;
+            var shown = 0;
 
-            // Compile elements to hide / show.
+            // Hide / show elements.
             $.each(table_row_names, function () {
                 var element = $(this).parents("tr");
 
                 if ($(this).text().indexOf(filter_value) === -1) {
                     // hide
-                    hide = hide.add(element);
+                    element.addClass("hidden");
+                    hidden++;
                 }
                 else {
                     // show
-                    show = show.add(element);
+                    element.removeClass("hidden");
+                    shown++;
                 }
             });
 
-            // Perform DOM manipulation.
-            hide.addClass("hidden");
-            show.removeClass("hidden");
-
             // Show placeholder if no rows will be displayed.
             if (no_rows.length > 0) {
-                if (show.length === 0) {
+                if (shown === 0) {
                     // Show placeholder, hide table.
                     no_rows.show();
                     table.hide();
@@ -109,7 +107,7 @@ coverage.wire_up_filter = function () {
             }
 
             // Manage dynamic header:
-            if (hide.length > 0) {
+            if (hidden > 0) {
                 // Calculate new dynamic sum values based on visible rows.
                 for (var column = 2; column < 20; column++) {
                     // Calculate summed value.
index f2b0418..db11402 100755 (executable)
Binary files a/coverage/htmlfiles/keybd_closed.png and b/coverage/htmlfiles/keybd_closed.png differ
index a77961d..db11402 100755 (executable)
Binary files a/coverage/htmlfiles/keybd_open.png and b/coverage/htmlfiles/keybd_open.png differ
index 136cbb8..86b8209 100644 (file)
@@ -2,6 +2,7 @@
 /* For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt */
 
 /* CSS styles for coverage.py. */
+
 /* Page-wide styles */
 html, body, h1, h2, h3, p, table, td, th {
     margin: 0;
@@ -321,7 +322,15 @@ td.text {
     }
 #index th.headerSortDown, #index th.headerSortUp {
     border-bottom: 1px solid #000;
+    white-space: nowrap;
+    background: #eee;
     }
+#index th.headerSortDown:after {
+    content: " ↓";
+}
+#index th.headerSortUp:after {
+    content: " ↑";
+}
 #index td.name, #index th.name {
     text-align: left;
     width: auto;
index e3723c1..f376346 100644 (file)
@@ -227,6 +227,16 @@ def _needs_to_implement(that, func_name):
         )
 
 
+class SimpleRepr(object):
+    """A mixin implementing a simple __repr__."""
+    def __repr__(self):
+        return "<{klass} @{id:x} {attrs}>".format(
+            klass=self.__class__.__name__,
+            id=id(self) & 0xFFFFFF,
+            attrs=" ".join("{}={!r}".format(k, v) for k, v in self.__dict__.items()),
+            )
+
+
 class CoverageException(Exception):
     """An exception specific to coverage.py."""
     pass
similarity index 59%
rename from coverage/monkey.py
rename to coverage/multiproc.py
index e7517bf..78c680c 100644 (file)
@@ -1,30 +1,38 @@
 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
 
-"""Monkey-patching to make coverage.py work right in some cases."""
+"""Monkey-patching to add multiprocessing support for coverage.py"""
 
 import multiprocessing
 import multiprocessing.process
+import os
 import sys
 
-# An attribute that will be set on modules to indicate that they have been
+from coverage.misc import contract
+
+# An attribute that will be set on the module to indicate that it has been
 # monkey-patched.
 PATCHED_MARKER = "_coverage$patched"
 
+# The environment variable that specifies the rcfile for subprocesses.
+COVERAGE_RCFILE_ENV = "_COVERAGE_RCFILE"
+
+
 if sys.version_info >= (3, 4):
-    klass = multiprocessing.process.BaseProcess
+    OriginalProcess = multiprocessing.process.BaseProcess
 else:
-    klass = multiprocessing.Process
+    OriginalProcess = multiprocessing.Process
 
-original_bootstrap = klass._bootstrap
+original_bootstrap = OriginalProcess._bootstrap
 
-
-class ProcessWithCoverage(klass):
+class ProcessWithCoverage(OriginalProcess):
     """A replacement for multiprocess.Process that starts coverage."""
+
     def _bootstrap(self):
         """Wrapper around _bootstrap to start coverage."""
-        from coverage import Coverage
-        cov = Coverage(data_suffix=True)
+        from coverage import Coverage       # avoid circular import
+        rcfile = os.environ[COVERAGE_RCFILE_ENV]
+        cov = Coverage(data_suffix=True, config_file=rcfile)
         cov.start()
         try:
             return original_bootstrap(self)
@@ -35,28 +43,39 @@ class ProcessWithCoverage(klass):
 
 class Stowaway(object):
     """An object to pickle, so when it is unpickled, it can apply the monkey-patch."""
+    def __init__(self, rcfile):
+        self.rcfile = rcfile
+
     def __getstate__(self):
-        return {}
+        return {'rcfile': self.rcfile}
 
-    def __setstate__(self, state_unused):
-        patch_multiprocessing()
+    def __setstate__(self, state):
+        patch_multiprocessing(state['rcfile'])
 
 
-def patch_multiprocessing():
+@contract(rcfile=str)
+def patch_multiprocessing(rcfile):
     """Monkey-patch the multiprocessing module.
 
     This enables coverage measurement of processes started by multiprocessing.
-    This is wildly experimental!
+    This involves aggressive monkey-patching.
+
+    `rcfile` is the path to the rcfile being used.
 
     """
+
     if hasattr(multiprocessing, PATCHED_MARKER):
         return
 
     if sys.version_info >= (3, 4):
-        klass._bootstrap = ProcessWithCoverage._bootstrap
+        OriginalProcess._bootstrap = ProcessWithCoverage._bootstrap
     else:
         multiprocessing.Process = ProcessWithCoverage
 
+    # Set the value in ProcessWithCoverage that will be pickled into the child
+    # process.
+    os.environ[COVERAGE_RCFILE_ENV] = rcfile
+
     # When spawning processes rather than forking them, we have no state in the
     # new process.  We sneak in there with a Stowaway: we stuff one of our own
     # objects into the data that gets pickled and sent to the sub-process. When
@@ -64,7 +83,7 @@ def patch_multiprocessing():
     # re-applies the monkey-patch.
     # Windows only spawns, so this is needed to keep Windows working.
     try:
-        from multiprocessing import spawn           # pylint: disable=no-name-in-module
+        from multiprocessing import spawn
         original_get_preparation_data = spawn.get_preparation_data
     except (ImportError, AttributeError):
         pass
@@ -72,7 +91,7 @@ def patch_multiprocessing():
         def get_preparation_data_with_stowaway(name):
             """Get the original preparation data, and also insert our stowaway."""
             d = original_get_preparation_data(name)
-            d['stowaway'] = Stowaway()
+            d['stowaway'] = Stowaway(rcfile)
             return d
 
         spawn.get_preparation_data = get_preparation_data_with_stowaway
index 8fb5d89..c3dba83 100644 (file)
@@ -106,6 +106,7 @@ class PythonParser(object):
         """
         combined = join_regex(regexes)
         if env.PY2:
+            # pylint: disable=redefined-variable-type
             combined = combined.decode("utf8")
         regex_c = re.compile(combined)
         matches = set()
index 8e98bea..601318c 100644 (file)
@@ -51,7 +51,9 @@ def get_python_source(filename):
             break
     else:
         # Couldn't find source.
-        raise NoSource("No source for code: '%s'." % filename)
+        exc_msg = "No source for code: '%s'.\n" % (filename,)
+        exc_msg += "Aborting report output, consider using -i."
+        raise NoSource(exc_msg)
 
     # Replace \f because of http://bugs.python.org/issue19035
     source = source.replace(b'\f', b' ')
index 2ffbbaa..609dc2d 100644 (file)
@@ -97,5 +97,8 @@ class Reporter(object):
                 # explicitly suppress those errors.
                 # NotPython is only raised by PythonFileReporter, which has a
                 # should_be_python() method.
-                if fr.should_be_python() and not self.config.ignore_errors:
-                    raise
+                if fr.should_be_python():
+                    if self.config.ignore_errors:
+                        self.coverage._warn("Could not parse Python file {0}".format(fr.filename))
+                    else:
+                        raise
index 7853848..9df5d5b 100644 (file)
@@ -6,7 +6,7 @@
 import collections
 
 from coverage.backward import iitems
-from coverage.misc import format_lines
+from coverage.misc import format_lines, SimpleRepr
 
 
 class Analysis(object):
@@ -157,7 +157,7 @@ class Analysis(object):
         return stats
 
 
-class Numbers(object):
+class Numbers(SimpleRepr):
     """The numerical results of measuring coverage.
 
     This holds the basic statistics from `Analysis`, and is used to roll
index 81844b5..b0fa71a 100644 (file)
@@ -27,7 +27,7 @@ class SummaryReporter(Reporter):
         """
         file_reporters = self.find_file_reporters(morfs)
 
-        # Prepare the formatting strings
+        # Prepare the formatting strings, header, and column sorting.
         max_name = max([len(fr.relative_filename()) for fr in file_reporters] + [5])
         fmt_name = u"%%- %ds  " % max_name
         fmt_err = u"%s   %s: %s"
@@ -46,6 +46,10 @@ class SummaryReporter(Reporter):
             fmt_coverage += u"   %s"
         rule = u"-" * len(header)
 
+        column_order = dict(name=0, stmts=1, miss=2, cover=-1)
+        if self.branches:
+            column_order.update(dict(branch=3, brpart=4))
+
         if outfile is None:
             outfile = sys.stdout
 
@@ -60,6 +64,11 @@ class SummaryReporter(Reporter):
         writeout(header)
         writeout(rule)
 
+        # `lines` is a list of pairs, (line text, line values).  The line text
+        # is a string that will be printed, and line values is a tuple of
+        # sortable values.
+        lines = []
+
         total = Numbers()
         skipped_count = 0
 
@@ -90,7 +99,10 @@ class SummaryReporter(Reporter):
                                 missing_fmtd += ", "
                             missing_fmtd += branches_fmtd
                     args += (missing_fmtd,)
-                writeout(fmt_coverage % args)
+                text = fmt_coverage % args
+                # Add numeric percent coverage so that sorting makes sense.
+                args += (nums.pc_covered,)
+                lines.append((text, args))
             except Exception:
                 report_it = not self.config.ignore_errors
                 if report_it:
@@ -102,6 +114,17 @@ class SummaryReporter(Reporter):
                 if report_it:
                     writeout(fmt_err % (fr.relative_filename(), typ.__name__, msg))
 
+        # Sort the lines and write them out.
+        if getattr(self.config, 'sort', None):
+            position = column_order.get(self.config.sort.lower())
+            if position is None:
+                raise CoverageException("Invalid sorting option: {0!r}".format(self.config.sort))
+            lines.sort(key=lambda l: (l[1][position], l[0]))
+
+        for line in lines:
+            writeout(line[0])
+
+        # Write a TOTAl line if we had more than one file.
         if total.n_files > 1:
             writeout(rule)
             args = ("TOTAL", total.n_statements, total.n_missing)
@@ -112,6 +135,7 @@ class SummaryReporter(Reporter):
                 args += ("",)
             writeout(fmt_coverage % args)
 
+        # Write other final lines.
         if not total.n_files and not skipped_count:
             raise CoverageException("No data to report.")
 
index 4c09c11..9944695 100644 (file)
@@ -170,7 +170,8 @@ class Templite(object):
                     # An expression to evaluate.
                     expr = self._expr_code(token[start:end].strip())
                     buffered.append("to_str(%s)" % expr)
-                elif token.startswith('{%'):
+                else:
+                    # token.startswith('{%')
                     # Action tag: split into words and parse further.
                     flush_output()
 
diff --git a/coverage/test_helpers.py b/coverage/test_helpers.py
deleted file mode 100644 (file)
index a76bed3..0000000
+++ /dev/null
@@ -1,390 +0,0 @@
-# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
-# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
-
-"""Mixin classes to help make good tests."""
-
-import atexit
-import collections
-import contextlib
-import os
-import random
-import shutil
-import sys
-import tempfile
-import textwrap
-
-from coverage.backunittest import TestCase
-from coverage.backward import StringIO, to_bytes
-
-
-class Tee(object):
-    """A file-like that writes to all the file-likes it has."""
-
-    def __init__(self, *files):
-        """Make a Tee that writes to all the files in `files.`"""
-        self._files = files
-        if hasattr(files[0], "encoding"):
-            self.encoding = files[0].encoding
-
-    def write(self, data):
-        """Write `data` to all the files."""
-        for f in self._files:
-            f.write(data)
-
-    def flush(self):
-        """Flush the data on all the files."""
-        for f in self._files:
-            f.flush()
-
-    if 0:
-        # Use this if you need to use a debugger, though it makes some tests
-        # fail, I'm not sure why...
-        def __getattr__(self, name):
-            return getattr(self._files[0], name)
-
-
-def change_dir(new_dir):
-    """Change directory, and then change back.
-
-    Use as a context manager, it will give you the new directory, and later
-    restore the old one.
-
-    """
-    old_dir = os.getcwd()
-    os.chdir(new_dir)
-    try:
-        yield os.getcwd()
-    finally:
-        os.chdir(old_dir)
-
-
-def saved_sys_path():
-    """Save sys.path, and restore it later."""
-    old_syspath = sys.path[:]
-    try:
-        yield
-    finally:
-        sys.path = old_syspath
-
-
-def setup_with_context_manager(testcase, cm):
-    """Use a contextmanager to setUp a test case.
-
-    If you have a context manager you like::
-
-        with ctxmgr(a, b, c) as v:
-            # do something with v
-
-    and you want to have that effect for a test case, call this function from
-    your setUp, and it will start the context manager for your test, and end it
-    when the test is done::
-
-        def setUp(self):
-            self.v = setup_with_context_manager(self, ctxmgr(a, b, c))
-
-        def test_foo(self):
-            # do something with self.v
-
-    """
-    val = cm.__enter__()
-    testcase.addCleanup(cm.__exit__, None, None, None)
-    return val
-
-
-class ModuleAwareMixin(TestCase):
-    """A test case mixin that isolates changes to sys.modules."""
-
-    def setUp(self):
-        super(ModuleAwareMixin, self).setUp()
-
-        # Record sys.modules here so we can restore it in cleanup_modules.
-        self.old_modules = list(sys.modules)
-        self.addCleanup(self.cleanup_modules)
-
-    def cleanup_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]:
-            del sys.modules[m]
-
-
-class SysPathAwareMixin(TestCase):
-    """A test case mixin that isolates changes to sys.path."""
-
-    def setUp(self):
-        super(SysPathAwareMixin, self).setUp()
-        setup_with_context_manager(self, saved_sys_path())
-
-
-class EnvironmentAwareMixin(TestCase):
-    """A test case mixin that isolates changes to the environment."""
-
-    def setUp(self):
-        super(EnvironmentAwareMixin, self).setUp()
-
-        # Record environment variables that we changed with set_environ.
-        self.environ_undos = {}
-
-        self.addCleanup(self.cleanup_environ)
-
-    def set_environ(self, name, value):
-        """Set an environment variable `name` to be `value`.
-
-        The environment variable is set, and record is kept that it was set,
-        so that `cleanup_environ` can restore its original value.
-
-        """
-        if name not in self.environ_undos:
-            self.environ_undos[name] = os.environ.get(name)
-        os.environ[name] = value
-
-    def cleanup_environ(self):
-        """Undo all the changes made by `set_environ`."""
-        for name, value in self.environ_undos.items():
-            if value is None:
-                del os.environ[name]
-            else:
-                os.environ[name] = value
-
-
-class StdStreamCapturingMixin(TestCase):
-    """A test case mixin that captures stdout and stderr."""
-
-    def setUp(self):
-        super(StdStreamCapturingMixin, self).setUp()
-
-        # Capture stdout and stderr so we can examine them in tests.
-        # nose keeps stdout from littering the screen, so we can safely Tee it,
-        # but it doesn't capture stderr, so we don't want to Tee stderr to the
-        # real stderr, since it will interfere with our nice field of dots.
-        old_stdout = sys.stdout
-        self.captured_stdout = StringIO()
-        sys.stdout = Tee(sys.stdout, self.captured_stdout)
-
-        old_stderr = sys.stderr
-        self.captured_stderr = StringIO()
-        sys.stderr = self.captured_stderr
-
-        self.addCleanup(self.cleanup_std_streams, old_stdout, old_stderr)
-
-    def cleanup_std_streams(self, old_stdout, old_stderr):
-        """Restore stdout and stderr."""
-        sys.stdout = old_stdout
-        sys.stderr = old_stderr
-
-    def stdout(self):
-        """Return the data written to stdout during the test."""
-        return self.captured_stdout.getvalue()
-
-    def stderr(self):
-        """Return the data written to stderr during the test."""
-        return self.captured_stderr.getvalue()
-
-
-class DelayedAssertionMixin(TestCase):
-    """A test case mixin that provides a `delayed_assertions` context manager.
-
-    Use it like this::
-
-        with self.delayed_assertions():
-            self.assertEqual(x, y)
-            self.assertEqual(z, w)
-
-    All of the assertions will run.  The failures will be displayed at the end
-    of the with-statement.
-
-    NOTE: this only works with some assertions.  These are known to work:
-
-        - `assertEqual(str, str)`
-
-        - `assertMultilineEqual(str, str)`
-
-    """
-    def __init__(self, *args, **kwargs):
-        super(DelayedAssertionMixin, self).__init__(*args, **kwargs)
-        # This mixin only works with assert methods that call `self.fail`.  In
-        # Python 2.7, `assertEqual` didn't, but we can do what Python 3 does,
-        # and use `assertMultiLineEqual` for comparing strings.
-        self.addTypeEqualityFunc(str, 'assertMultiLineEqual')
-        self._delayed_assertions = None
-
-    @contextlib.contextmanager
-    def delayed_assertions(self):
-        """The context manager: assert that we didn't collect any assertions."""
-        self._delayed_assertions = []
-        old_fail = self.fail
-        self.fail = self._delayed_fail
-        try:
-            yield
-        finally:
-            self.fail = old_fail
-        if self._delayed_assertions:
-            if len(self._delayed_assertions) == 1:
-                self.fail(self._delayed_assertions[0])
-            else:
-                self.fail(
-                    "{0} failed assertions:\n{1}".format(
-                        len(self._delayed_assertions),
-                        "\n".join(self._delayed_assertions),
-                    )
-                )
-
-    def _delayed_fail(self, msg=None):
-        """The stand-in for TestCase.fail during delayed_assertions."""
-        self._delayed_assertions.append(msg)
-
-
-class TempDirMixin(SysPathAwareMixin, ModuleAwareMixin, TestCase):
-    """A test case mixin that creates a temp directory and files in it.
-
-    Includes SysPathAwareMixin and ModuleAwareMixin, because making and using
-    temp directories like this will also need that kind of isolation.
-
-    """
-
-    # Our own setting: most of these tests run in their own temp directory.
-    # Set this to False in your subclass if you don't want a temp directory
-    # created.
-    run_in_temp_dir = True
-
-    # Set this if you aren't creating any files with make_file, but still want
-    # the temp directory.  This will stop the test behavior checker from
-    # complaining.
-    no_files_in_temp_dir = False
-
-    def setUp(self):
-        super(TempDirMixin, self).setUp()
-
-        if self.run_in_temp_dir:
-            # Create a temporary directory.
-            self.temp_dir = self.make_temp_dir("test_cover")
-            self.chdir(self.temp_dir)
-
-            # Modules should be importable from this temp directory.  We don't
-            # use '' because we make lots of different temp directories and
-            # nose's caching importer can get confused.  The full path prevents
-            # problems.
-            sys.path.insert(0, os.getcwd())
-
-        class_behavior = self.class_behavior()
-        class_behavior.tests += 1
-        class_behavior.temp_dir = self.run_in_temp_dir
-        class_behavior.no_files_ok = self.no_files_in_temp_dir
-
-        self.addCleanup(self.check_behavior)
-
-    def make_temp_dir(self, slug="test_cover"):
-        """Make a temp directory that is cleaned up when the test is done."""
-        name = "%s_%08d" % (slug, random.randint(0, 99999999))
-        temp_dir = os.path.join(tempfile.gettempdir(), name)
-        os.makedirs(temp_dir)
-        self.addCleanup(shutil.rmtree, temp_dir)
-        return temp_dir
-
-    def chdir(self, new_dir):
-        """Change directory, and change back when the test is done."""
-        old_dir = os.getcwd()
-        os.chdir(new_dir)
-        self.addCleanup(os.chdir, old_dir)
-
-    def check_behavior(self):
-        """Check that we did the right things."""
-
-        class_behavior = self.class_behavior()
-        if class_behavior.test_method_made_any_files:
-            class_behavior.tests_making_files += 1
-
-    def make_file(self, filename, text="", newline=None):
-        """Create a file for testing.
-
-        `filename` is the relative path to the file, including directories if
-        desired, which will be created if need be.
-
-        `text` is the content to create in the file, a native string (bytes in
-        Python 2, unicode in Python 3).
-
-        If `newline` is provided, it is a string that will be used as the line
-        endings in the created file, otherwise the line endings are as provided
-        in `text`.
-
-        Returns `filename`.
-
-        """
-        # Tests that call `make_file` should be run in a temp environment.
-        assert self.run_in_temp_dir
-        self.class_behavior().test_method_made_any_files = True
-
-        text = textwrap.dedent(text)
-        if newline:
-            text = text.replace("\n", newline)
-
-        # Make sure the directories are available.
-        dirs, _ = os.path.split(filename)
-        if dirs and not os.path.exists(dirs):
-            os.makedirs(dirs)
-
-        # Create the file.
-        with open(filename, 'wb') as f:
-            f.write(to_bytes(text))
-
-        return filename
-
-    # We run some tests in temporary directories, because they may need to make
-    # files for the tests. But this is expensive, so we can change per-class
-    # whether a temp directory is used or not.  It's easy to forget to set that
-    # option properly, so we track information about what the tests did, and
-    # then report at the end of the process on test classes that were set
-    # wrong.
-
-    class ClassBehavior(object):
-        """A value object to store per-class."""
-        def __init__(self):
-            self.tests = 0
-            self.skipped = 0
-            self.temp_dir = True
-            self.no_files_ok = False
-            self.tests_making_files = 0
-            self.test_method_made_any_files = False
-
-    # Map from class to info about how it ran.
-    class_behaviors = collections.defaultdict(ClassBehavior)
-
-    @classmethod
-    def report_on_class_behavior(cls):
-        """Called at process exit to report on class behavior."""
-        for test_class, behavior in cls.class_behaviors.items():
-            bad = ""
-            if behavior.tests <= behavior.skipped:
-                bad = ""
-            elif behavior.temp_dir and behavior.tests_making_files == 0:
-                if not behavior.no_files_ok:
-                    bad = "Inefficient"
-            elif not behavior.temp_dir and behavior.tests_making_files > 0:
-                bad = "Unsafe"
-
-            if bad:
-                if behavior.temp_dir:
-                    where = "in a temp directory"
-                else:
-                    where = "without a temp directory"
-                print(
-                    "%s: %s ran %d tests, %d made files %s" % (
-                        bad,
-                        test_class.__name__,
-                        behavior.tests,
-                        behavior.tests_making_files,
-                        where,
-                    )
-                )
-
-    def class_behavior(self):
-        """Get the ClassBehavior instance for this test."""
-        return self.class_behaviors[self.__class__]
-
-# When the process ends, find out about bad classes.
-atexit.register(TempDirMixin.report_on_class_behavior)
index 23f6704..4ebdc53 100644 (file)
@@ -5,7 +5,7 @@
 # This file is exec'ed in setup.py, don't import anything!
 
 # Same semantics as sys.version_info.
-version_info = (4, 1, 0, 'final', 0)
+version_info = (4, 2, 0, 'final', 0)
 
 
 def _make_version(major, minor, micro, releaselevel, serial):
index 0611204..6bb2cb2 100644 (file)
@@ -49,10 +49,84 @@ history, see the `CHANGES.rst`_ file in the source tree.
 
 .. module:: coverage
 
+.. _changes_42:
+
+Version 4.2 --- 2016-07-26
+--------------------------
+
+Work from the PyCon 2016 Sprints!
+
+- BACKWARD INCOMPATIBILITY: the ``coverage combine`` command now ignores an
+  existing ``.coverage`` data file.  It used to include that file in its
+  combining.  This caused confusing results, and extra tox "clean" steps.  If
+  you want the old behavior, use the new ``coverage combine --append`` option.
+
+- Since ``concurrency=multiprocessing`` uses subprocesses, options specified on
+  the coverage.py command line will not be communicated down to them.  Only
+  options in the configuration file will apply to the subprocesses.
+  Previously, the options didn't apply to the subprocesses, but there was no
+  indication.  Now it is an error to use ``--concurrency=multiprocessing`` and
+  other run-affecting options on the command line.  This prevents
+  failures like those reported in `issue 495`_.
+
+- The ``concurrency`` option can now take multiple values, to support programs
+  using multiprocessing and another library such as eventlet.  This is only
+  possible in the configuration file, not from the command line. The
+  configuration file is the only way for sub-processes to all run with the same
+  options.  Fixes `issue 484`_.  Thanks to Josh Williams for prototyping.
+
+- Using a ``concurrency`` setting of ``multiprocessing`` now implies
+  ``--parallel`` so that the main program is measured similarly to the
+  sub-processes.
+
+- When using `automatic subprocess measurement`_, running coverage commands
+  would create spurious data files.  This is now fixed, thanks to diagnosis and
+  testing by Dan Riti.  Closes `issue 492`_.
+
+- A new configuration option, ``report:sort``, controls what column of the
+  text report is used to sort the rows.  Thanks to Dan Wandschneider, this
+  closes `issue 199`_.
+
+- The HTML report has a more-visible indicator for which column is being
+  sorted.  Closes `issue 298`_, thanks to Josh Williams.
+
+- Filtering the HTML report is now faster, thanks to Ville Skyttä.
+
+- If the HTML report cannot find the source for a file, the message now
+  suggests using the ``-i`` flag to allow the report to continue. Closes
+  `issue 231`_, thanks, Nathan Land.
+
+- When reports are ignoring errors, there's now a warning if a file cannot be
+  parsed, rather than being silently ignored.  Closes `issue 396`_. Thanks,
+  Matthew Boehm.
+
+- A new option for ``coverage debug`` is available: ``coverage debug config``
+  shows the current configuration.  Closes `issue 454`_, thanks to Matthew
+  Boehm.
+
+- Running coverage as a module (``python -m coverage``) no longer shows the
+  program name as ``__main__.py``.  Fixes `issue 478`_.  Thanks, Scott Belden.
+
+- The `test_helpers` module has been moved into a separate pip-installable
+  package: `unittest-mixins`_.
+
+.. _automatic subprocess measurement: http://coverage.readthedocs.io/en/latest/subprocess.html
+.. _issue 199: https://bitbucket.org/ned/coveragepy/issues/199/add-a-way-to-sort-the-text-report
+.. _issue 231: https://bitbucket.org/ned/coveragepy/issues/231/various-default-behavior-in-report-phase
+.. _issue 298: https://bitbucket.org/ned/coveragepy/issues/298/show-in-html-report-that-the-columns-are
+.. _issue 396: https://bitbucket.org/ned/coveragepy/issues/396/coverage-xml-shouldnt-bail-out-on-parse
+.. _issue 454: https://bitbucket.org/ned/coveragepy/issues/454/coverage-debug-config-should-be
+.. _issue 478: https://bitbucket.org/ned/coveragepy/issues/478/help-shows-silly-program-name-when-running
+.. _issue 484: https://bitbucket.org/ned/coveragepy/issues/484/multiprocessing-greenlet-concurrency
+.. _issue 492: https://bitbucket.org/ned/coveragepy/issues/492/subprocess-coverage-strange-detection-of
+.. _issue 495: https://bitbucket.org/ned/coveragepy/issues/495/branch-and-concurrency-are-conflicting
+.. _unittest-mixins: https://pypi.python.org/pypi/unittest-mixins
+
+
 .. _changes_41:
 
-Version 4.1b --- 2016-05-21
----------------------------
+Version 4.1 --- 2016-05-21
+--------------------------
 
 - Branch analysis has been rewritten: it used to be based on bytecode, but now
   uses AST analysis.  This has changed a number of things:
index f0896de..64448b0 100644 (file)
@@ -108,6 +108,11 @@ Give it a value of ``multiprocessing``, ``thread``, ``greenlet``, ``eventlet``,
 or ``gevent``.  Values other than ``thread`` require the :ref:`C extension
 <install_extension>`.
 
+If you are using ``--concurrency=multiprocessing``, you must set other options
+in the configuration file.  Options on the command line will not be passed to
+the processes that multiprocessing creates.  Best practice is to use the
+configuration file for all options.
+
 .. _multiprocessing: https://docs.python.org/2/library/multiprocessing.html
 .. _greenlet: http://greenlet.readthedocs.org/en/latest/
 .. _gevent: http://www.gevent.org/
@@ -212,6 +217,11 @@ suffix.  Here are some examples of data files that can be combined::
     .coverage.20120807T212300
     .coverage.last_good_run.ok
 
+An existing combined data file is ignored and re-written. If you want to use
+**combine** to accumulate results into the .coverage data file over a number of
+runs, use the ``--append`` switch on the **combine** command.  This behavior
+was the default before version 4.2.
+
 The ``run --parallel-mode`` switch automatically creates separate data files
 for each run which can be combined later.  The file names include the machine
 name, the process id, and a random number::
@@ -415,8 +425,11 @@ command can often help::
 
     $ coverage debug sys > please_attach_to_bug_report.txt
 
-Two types of information are available: ``sys`` to show system configuration,
-and ``data`` to show a summary of the collected coverage data.
+Three types of information are available:
+
+* ``config``: show coverage's configuration
+* ``sys``: show system configuration,
+* ``data``: show a summary of the collected coverage data
 
 
 .. _cmd_run_debug:
index 1aae8c8..684628d 100644 (file)
@@ -56,9 +56,9 @@ copyright = u'2009\N{EN DASH}2016, Ned Batchelder'
 # built documents.
 #
 # The short X.Y version.
-version = '4.1'
+version = '4.2'
 # The full version, including alpha/beta/rc tags.
-release = '4.1'
+release = '4.2'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
index 688e899..b149f4f 100644 (file)
@@ -107,8 +107,8 @@ to more than one command.
 ``cover_pylib`` (boolean, default False): whether to measure the Python
 standard library.
 
-``concurrency`` (string, default "thread"): the name of the concurrency library
-in use by the product code.  If your program uses `multiprocessing`_,
+``concurrency`` (multi-string, default "thread"): the name concurrency
+libraries in use by the product code.  If your program uses `multiprocessing`_,
 `gevent`_, `greenlet`_, or `eventlet`_, you must name that library in this
 option, or coverage.py will produce very wrong results.
 
@@ -117,6 +117,8 @@ option, or coverage.py will produce very wrong results.
 .. _gevent: http://www.gevent.org/
 .. _eventlet: http://eventlet.net/
 
+Before version 4.2, this option only accepted a single string.
+
 .. versionadded:: 4.0
 
 ``data_file`` (string, default ".coverage"): the name of the data file to use
@@ -194,7 +196,7 @@ supply the "pragma: no cover" regex if you still want to use it.
 measurement is under this value, then exit with a status code of 2.
 
 ``ignore_errors`` (boolean, default False): ignore source code that can't be
-found.
+found, emitting a warning instead of an exception.
 
 ``include`` (multi-string): a list of file name patterns, the files to include
 in reporting.  See :ref:`source` for details.
@@ -218,6 +220,9 @@ missing lines.  See :ref:`cmd_summary` for more information.
 ``skip_covered`` (boolean, default False): Don't include files in the report
 that are 100% covered files. See :ref:`cmd_summary` for more information.
 
+``sort`` (string, default "Name"): Sort the text report by the named column.
+Allowed values are "Name", "Stmts", "Miss", "Branch", "BrPart", or "Cover".
+
 
 .. _config_html:
 
index 8723748..47d6440 100644 (file)
@@ -47,6 +47,7 @@ Coverage.py
 .. :history: 20160123T171300, updated for 4.1b2
 .. :history: 20160510T125300, updated for 4.1b3
 .. :history: 20160521T074500, udpated for 4.1
+.. :history: 20160726T161300, udpated for 4.2
 
 
 Coverage.py is a tool for measuring code coverage of Python programs. It
@@ -59,7 +60,7 @@ not.
 
 .. ifconfig:: not prerelease
 
-    The latest version is coverage.py 4.1, released May 21st 2016.  It
+    The latest version is coverage.py 4.2, released July 26th 2016.  It
     is supported on:
 
     * Python versions 2.6, 2.7, 3.3, 3.4, 3.5, and 3.6.
@@ -70,7 +71,7 @@ not.
 
 .. ifconfig:: prerelease
 
-    The latest version is coverage.py 4.1b3, released May 10th 2016.  It is
+    The latest version is coverage.py 4.2b1, released July 4th 2016.  It is
     supported on:
 
     * Python versions 2.6, 2.7, 3.3, 3.4, and 3.5.
index 135ccbb..177fca2 100644 (file)
@@ -87,7 +87,7 @@ GLOBAL OPTIONS
 COMMAND REFERENCE
 =================
 
-**annotate**
+**annotate** [ `option` ... ]
 
     Options:
 
@@ -97,7 +97,7 @@ COMMAND REFERENCE
     \-i, --ignore-errors
         Ignore errors while reading source files.
 
-**combine** [ `PATH` ... ]
+**combine** [ `option` ... ] [ `PATH` ... ]
 
     Combine data from multiple coverage files collected with ``run -p``.
     The combined results are written to a single file representing the
@@ -106,6 +106,12 @@ COMMAND REFERENCE
     If `PATH` is specified, they are files or directories containing data to
     be combined.
 
+    Options:
+
+    \--append
+        Append coverage data to .coverage, otherwise it starts clean each
+        time.
+
 **erase**
 
     Erase previously collected coverage data.
index b7ea828..c2f7701 100644 (file)
@@ -3,5 +3,6 @@
 # https://requires.io/github/nedbat/coveragepy/requirements/
 
 pyenchant==1.6.6
-sphinx==1.3.4
+sphinx==1.4.4
 sphinxcontrib-spelling==2.1.2
+sphinx_rtd_theme==0.1.9
index b701198..9fb1a63 100644 (file)
@@ -48,7 +48,7 @@ coverage.py from working properly:
 
 .. _execv: http://docs.python.org/library/os#os.execl
 .. _sys.settrace: http://docs.python.org/library/sys.html#sys.settrace
-.. _thread: http://docs.python.org/library/thread.html
+.. _thread: https://docs.python.org/2/library/thread.html
 .. _threading: http://docs.python.org/library/threading.html
 .. _issue 43: https://bitbucket.org/ned/coveragepy/issue/43/coverage-measurement-fails-on-code
 
index 3aa526f..c8179cd 100644 (file)
--- a/howto.txt
+++ b/howto.txt
@@ -17,7 +17,7 @@ pip install -r requirements/dev.pip
 - Update README.rst, including "New in x.y:"
 - Update docs
     - Version, date, and changes in doc/changes.rst
-    - Version and date in doc/index.rst
+    - Version, date and python versions in doc/index.rst
     - Version and copyright date in doc/conf.py
     - Don't forget the man page: doc/python-coverage.1.txt
     - Check that the docs build correctly:
index f8eb4bf..39d231c 100644 (file)
--- a/pylintrc
+++ b/pylintrc
@@ -18,9 +18,6 @@
 # pygtk.require().
 #init-hook=
 
-# Profiled execution.
-profile=no
-
 # Add <file or directory> to the black list. It should be a base name, not a
 # path. You may set this option multiple times.
 ignore=
@@ -69,8 +66,13 @@ disable=
 # Messages that may be silly:
     no-self-use,
     no-member,
-# formatting stuff
+    using-constant-test,
+    too-many-nested-blocks,
+# Formatting stuff
     superfluous-parens,bad-continuation,
+# I'm fine deciding my own import order,
+    wrong-import-position,
+    wrong-import-order,
 # Messages that are noisy for now, eventually maybe we'll turn them on:
     invalid-name,
     protected-access,
@@ -100,10 +102,6 @@ reports=no
 # (R0004).
 evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
 
-# Add a comment according to your evaluation note. This is used by the global
-# evaluation report (R0004).
-comment=no
-
 # Enable the report(s) with the given id(s).
 #enable-report=
 
@@ -123,9 +121,6 @@ comment=no
 # 
 [BASIC]
 
-# Required attributes for module, separated by a comma
-required-attributes=
-
 # Regular expression which should only match functions or classes name which do
 # not require a docstring
 # Special methods don't: __foo__
@@ -184,10 +179,6 @@ ignore-mixin-members=yes
 # (useful for classes with attributes dynamicaly set).
 ignored-classes=SQLObject
 
-# When zope mode is activated, consider the acquired-members option to ignore
-# access to some undefined attributes.
-zope=no
-
 # List of members which are usually get through zope's acquisition mecanism and
 # so shouldn't trigger E0201 when accessed (need zope=yes to be considered).
 acquired-members=REQUEST,acl_users,aq_parent
@@ -222,10 +213,6 @@ additional-builtins=
 # 
 [CLASSES]
 
-# List of interface methods to ignore, separated by a comma. This is used for
-# instance to not check methods defines in Zope's Interface base class.
-ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
-
 # List of method names used to declare (i.e. assign) instance attributes.
 defining-attr-methods=__init__,__new__,setUp,reset
 
index dbbcfc5..12673d8 100644 (file)
@@ -9,11 +9,12 @@ nose==1.3.7
 -r tox.pip
 
 # for linting.
-greenlet==0.4.9
+greenlet==0.4.10
 mock==2.0.0
 PyContracts==1.7.9
 pyenchant==1.6.6
-pylint==1.4.5
+pylint==1.5.6
+unittest-mixins==1.1.1
 
 # for kitting.
 requests==2.10.0
index 7625ce6..ae2a387 100644 (file)
@@ -13,17 +13,16 @@ import shlex
 import shutil
 import sys
 
+from unittest_mixins import (
+    EnvironmentAwareMixin, StdStreamCapturingMixin, TempDirMixin,
+    DelayedAssertionMixin,
+)
+
 import coverage
 from coverage.backunittest import TestCase
 from coverage.backward import StringIO, import_local_file, string_class, shlex_quote
 from coverage.cmdline import CoverageScript
 from coverage.debug import _TEST_NAME_FILE, DebugControl
-from coverage.test_helpers import (
-    EnvironmentAwareMixin, StdStreamCapturingMixin, TempDirMixin,
-    DelayedAssertionMixin,
-)
-
-from nose.plugins.skip import SkipTest
 
 from tests.helpers import run_command
 
@@ -61,11 +60,6 @@ class CoverageTest(
                     self.__class__.__name__, self._testMethodName,
                 ))
 
-    def skip(self, reason):
-        """Skip this test, and give a reason."""
-        self.class_behavior().skipped += 1
-        raise SkipTest(reason)
-
     def clean_local_file_imports(self):
         """Clean up the results of calls to `import_local_file`.
 
index 39f2bfd..8ebbd59 100644 (file)
@@ -8,7 +8,7 @@ import sys
 
 from tests.coveragetest import CoverageTest
 
-from coverage.test_helpers import change_dir    # pylint: disable=unused-import
+from unittest_mixins import change_dir    # pylint: disable=unused-import
 from tests.test_farm import clean
 # Import helpers, eventually test_farm.py will go away.
 from tests.test_farm import (       # pylint: disable=unused-import
index 5edfced..744014b 100644 (file)
@@ -2,11 +2,12 @@
 <html>
 <head>
     <title>Coverage.py Javascript Test Suite</title>
-    <link rel="stylesheet" href="../qunit/qunit.css" type="text/css" media="screen">
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width">
+    <link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-2.0.1.css">
     <script type="text/javascript" src="../../coverage/htmlfiles/jquery.min.js"></script>
     <script type='text/javascript' src="../../coverage/htmlfiles/jquery.isonscreen.js"></script>
     <script type="text/javascript" src="../../coverage/htmlfiles/coverage_html.js"></script>
-    <script type="text/javascript" src="../qunit/qunit.js"></script>
     <script type="text/javascript" src="../qunit/jquery.tmpl.min.js"></script>
 
     <style>
         <p id='t${number}' class='${klass}'>Hello, world!</p>
     </script>
 
-    <!-- Pull in the tests -->
-    <script type="text/javascript" src="tests.js"></script>
-
 </head>
 <body>
-    <h1 id="qunit-header">Coverage.py Javascript Test Suite</h1>
-    <h2 id="qunit-banner"></h2>
-    <div id="qunit-testrunner-toolbar"></div>
-    <h2 id="qunit-userAgent"></h2>
-    <ol id="qunit-tests"></ol>
+    <div id="qunit"></div>
     <div id="qunit-fixture"></div>
+    <script src="https://code.jquery.com/qunit/qunit-2.0.1.js"></script>
+    <!-- pull in our tests -->
+    <script src="tests.js"></script>
 </body>
 </html>
index 504a158..bf2ca73 100644 (file)
@@ -2,20 +2,20 @@
 /* For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt */
 
 // Tests of coverage.py HTML report chunk navigation.
-/*global coverage, test, module, equals, jQuery, $ */
+/*global coverage, jQuery, $ */
 
 // Test helpers
 
-function selection_is(sel) {
-    raw_selection_is(sel, true);
+function selection_is(assert, sel) {
+    raw_selection_is(assert, sel, true);
 }
 
-function raw_selection_is(sel, check_highlight) {
+function raw_selection_is(assert, sel, check_highlight) {
     var beg = sel[0], end = sel[1];
-    equals(coverage.sel_begin, beg);
-    equals(coverage.sel_end, end);
+    assert.equal(coverage.sel_begin, beg);
+    assert.equal(coverage.sel_end, end);
     if (check_highlight) {
-        equals(coverage.code_container().find(".highlight").length, end-beg);
+        assert.equal(coverage.code_container().find(".highlight").length, end-beg);
     }
 }
 
@@ -34,20 +34,21 @@ function build_fixture(spec) {
 
 // Zero-chunk tests
 
-module("Zero-chunk navigation", {
-    setup: function () {
+QUnit.module("Zero-chunk navigation", {
+    beforeEach: function () {
         build_fixture("wwww");
     }
 });
 
-test("set_sel defaults", function () {
+QUnit.test("set_sel defaults", function (assert) {
     coverage.set_sel(2);
-    equals(coverage.sel_begin, 2);
-    equals(coverage.sel_end, 3);
+    assert.equal(coverage.sel_begin, 2);
+    assert.equal(coverage.sel_end, 3);
 });
 
-test("No first chunk to select", function () {
+QUnit.test("No first chunk to select", function (assert) {
     coverage.to_first_chunk();
+    assert.expect(0);
 });
 
 // One-chunk tests
@@ -64,32 +65,32 @@ $.each([
     var id = params[0];
     var c1 = params[1];
 
-    module("One-chunk navigation - " + id, {
-        setup: function () {
+    QUnit.module("One-chunk navigation - " + id, {
+        beforeEach: function () {
             build_fixture(id);
         }
     });
 
-    test("First chunk", function () {
+    QUnit.test("First chunk", function (assert) {
         coverage.to_first_chunk();
-        selection_is(c1);
+        selection_is(assert, c1);
     });
 
-    test("Next chunk is first chunk", function () {
+    QUnit.test("Next chunk is first chunk", function (assert) {
         coverage.to_next_chunk();
-        selection_is(c1);
+        selection_is(assert, c1);
     });
 
-    test("There is no next chunk", function () {
+    QUnit.test("There is no next chunk", function (assert) {
         coverage.to_first_chunk();
         coverage.to_next_chunk();
-        selection_is(c1);
+        selection_is(assert, c1);
     });
 
-    test("There is no prev chunk", function () {
+    QUnit.test("There is no prev chunk", function (assert) {
         coverage.to_first_chunk();
         coverage.to_prev_chunk();
-        selection_is(c1);
+        selection_is(assert, c1);
     });
 });
 
@@ -109,66 +110,66 @@ $.each([
     var c1 = params[1];
     var c2 = params[2];
 
-    module("Two-chunk navigation - " + id, {
-        setup: function () {
+    QUnit.module("Two-chunk navigation - " + id, {
+        beforeEach: function () {
             build_fixture(id);
         }
     });
 
-    test("First chunk", function () {
+    QUnit.test("First chunk", function (assert) {
         coverage.to_first_chunk();
-        selection_is(c1);
+        selection_is(assert, c1);
     });
 
-    test("Next chunk is first chunk", function () {
+    QUnit.test("Next chunk is first chunk", function (assert) {
         coverage.to_next_chunk();
-        selection_is(c1);
+        selection_is(assert, c1);
     });
 
-    test("Move to next chunk", function () {
+    QUnit.test("Move to next chunk", function (assert) {
         coverage.to_first_chunk();
         coverage.to_next_chunk();
-        selection_is(c2);
+        selection_is(assert, c2);
     });
 
-    test("Move to first chunk", function () {
+    QUnit.test("Move to first chunk", function (assert) {
         coverage.to_first_chunk();
         coverage.to_next_chunk();
         coverage.to_first_chunk();
-        selection_is(c1);
+        selection_is(assert, c1);
     });
 
-    test("Move to previous chunk", function () {
+    QUnit.test("Move to previous chunk", function (assert) {
         coverage.to_first_chunk();
         coverage.to_next_chunk();
         coverage.to_prev_chunk();
-        selection_is(c1);
+        selection_is(assert, c1);
     });
 
-    test("Next doesn't move after last chunk", function () {
+    QUnit.test("Next doesn't move after last chunk", function (assert) {
         coverage.to_first_chunk();
         coverage.to_next_chunk();
         coverage.to_next_chunk();
-        selection_is(c2);
+        selection_is(assert, c2);
     });
 
-    test("Prev doesn't move before first chunk", function () {
+    QUnit.test("Prev doesn't move before first chunk", function (assert) {
         coverage.to_first_chunk();
         coverage.to_next_chunk();
         coverage.to_prev_chunk();
         coverage.to_prev_chunk();
-        selection_is(c1);
+        selection_is(assert, c1);
     });
 
 });
 
-module("Miscellaneous");
+QUnit.module("Miscellaneous");
 
-test("Jump from a line selected", function () {
+QUnit.test("Jump from a line selected", function (assert) {
     build_fixture("rrwwrr");
     coverage.set_sel(3);
     coverage.to_next_chunk();
-    selection_is([5,7]);
+    selection_is(assert, [5,7]);
 });
 
 // Tests of select_line_or_chunk.
@@ -191,17 +192,17 @@ $.each([
     var id = params[0];
     var sels = params[1];
 
-    module("Select line or chunk - " + id, {
-        setup: function () {
+    QUnit.module("Select line or chunk - " + id, {
+        beforeEach: function () {
             build_fixture(id);
         }
     });
 
     $.each(sels, function (i, sel) {
         i++;
-        test("Select line " + i, function () {
+        QUnit.test("Select line " + i, function (assert) {
             coverage.select_line_or_chunk(i);
-            raw_selection_is(sel);
+            raw_selection_is(assert, sel);
         });
     });
 });
diff --git a/tests/modules/process_test/__init__.py b/tests/modules/process_test/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
similarity index 91%
rename from tests/try_execfile.py
rename to tests/modules/process_test/try_execfile.py
index 7090507..bc412fc 100644 (file)
@@ -52,6 +52,11 @@ def without_same_files(filenames):
 
 cleaned_sys_path = [os.path.normcase(p) for p in without_same_files(sys.path)]
 
+# Eggs seems to go in different places for some reason. I'm going to assume
+# it's an OK difference.  Sort eggs to the end of the list to canonicalize
+# them.
+cleaned_sys_path = sorted(cleaned_sys_path, key=lambda p: p.endswith(".egg"))
+
 DATA = "xyzzy"
 
 import __main__
diff --git a/tests/qunit/qunit.css b/tests/qunit/qunit.css
deleted file mode 100644 (file)
index b3c6db5..0000000
+++ /dev/null
@@ -1,225 +0,0 @@
-/**
- * QUnit - A JavaScript Unit Testing Framework
- *
- * http://docs.jquery.com/QUnit
- *
- * Copyright (c) 2011 John Resig, Jörn Zaefferer
- * Dual licensed under the MIT (MIT-LICENSE.txt)
- * or GPL (GPL-LICENSE.txt) licenses.
- */
-
-/** Font Family and Sizes */
-
-#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
-       font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
-}
-
-#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
-#qunit-tests { font-size: smaller; }
-
-
-/** Resets */
-
-#qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult {
-       margin: 0;
-       padding: 0;
-}
-
-
-/** Header */
-
-#qunit-header {
-       padding: 0.5em 0 0.5em 1em;
-
-       color: #8699a4;
-       background-color: #0d3349;
-
-       font-size: 1.5em;
-       line-height: 1em;
-       font-weight: normal;
-
-       border-radius: 15px 15px 0 0;
-       -moz-border-radius: 15px 15px 0 0;
-       -webkit-border-top-right-radius: 15px;
-       -webkit-border-top-left-radius: 15px;
-}
-
-#qunit-header a {
-       text-decoration: none;
-       color: #c2ccd1;
-}
-
-#qunit-header a:hover,
-#qunit-header a:focus {
-       color: #fff;
-}
-
-#qunit-banner {
-       height: 5px;
-}
-
-#qunit-testrunner-toolbar {
-       padding: 0.5em 0 0.5em 2em;
-       color: #5E740B;
-       background-color: #eee;
-}
-
-#qunit-userAgent {
-       padding: 0.5em 0 0.5em 2.5em;
-       background-color: #2b81af;
-       color: #fff;
-       text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
-}
-
-
-/** Tests: Pass/Fail */
-
-#qunit-tests {
-       list-style-position: inside;
-}
-
-#qunit-tests li {
-       padding: 0.4em 0.5em 0.4em 2.5em;
-       border-bottom: 1px solid #fff;
-       list-style-position: inside;
-}
-
-#qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running  {
-       display: none;
-}
-
-#qunit-tests li strong {
-       cursor: pointer;
-}
-
-#qunit-tests li a {
-       padding: 0.5em;
-       color: #c2ccd1;
-       text-decoration: none;
-}
-#qunit-tests li a:hover,
-#qunit-tests li a:focus {
-       color: #000;
-}
-
-#qunit-tests ol {
-       margin-top: 0.5em;
-       padding: 0.5em;
-
-       background-color: #fff;
-
-       border-radius: 15px;
-       -moz-border-radius: 15px;
-       -webkit-border-radius: 15px;
-
-       box-shadow: inset 0px 2px 13px #999;
-       -moz-box-shadow: inset 0px 2px 13px #999;
-       -webkit-box-shadow: inset 0px 2px 13px #999;
-}
-
-#qunit-tests table {
-       border-collapse: collapse;
-       margin-top: .2em;
-}
-
-#qunit-tests th {
-       text-align: right;
-       vertical-align: top;
-       padding: 0 .5em 0 0;
-}
-
-#qunit-tests td {
-       vertical-align: top;
-}
-
-#qunit-tests pre {
-       margin: 0;
-       white-space: pre-wrap;
-       word-wrap: break-word;
-}
-
-#qunit-tests del {
-       background-color: #e0f2be;
-       color: #374e0c;
-       text-decoration: none;
-}
-
-#qunit-tests ins {
-       background-color: #ffcaca;
-       color: #500;
-       text-decoration: none;
-}
-
-/*** Test Counts */
-
-#qunit-tests b.counts                       { color: black; }
-#qunit-tests b.passed                       { color: #5E740B; }
-#qunit-tests b.failed                       { color: #710909; }
-
-#qunit-tests li li {
-       margin: 0.5em;
-       padding: 0.4em 0.5em 0.4em 0.5em;
-       background-color: #fff;
-       border-bottom: none;
-       list-style-position: inside;
-}
-
-/*** Passing Styles */
-
-#qunit-tests li li.pass {
-       color: #5E740B;
-       background-color: #fff;
-       border-left: 26px solid #C6E746;
-}
-
-#qunit-tests .pass                          { color: #528CE0; background-color: #D2E0E6; }
-#qunit-tests .pass .test-name               { color: #366097; }
-
-#qunit-tests .pass .test-actual,
-#qunit-tests .pass .test-expected           { color: #999999; }
-
-#qunit-banner.qunit-pass                    { background-color: #C6E746; }
-
-/*** Failing Styles */
-
-#qunit-tests li li.fail {
-       color: #710909;
-       background-color: #fff;
-       border-left: 26px solid #EE5757;
-}
-
-#qunit-tests > li:last-child {
-       border-radius: 0 0 15px 15px;
-       -moz-border-radius: 0 0 15px 15px;
-       -webkit-border-bottom-right-radius: 15px;
-       -webkit-border-bottom-left-radius: 15px;
-}
-
-#qunit-tests .fail                          { color: #000000; background-color: #EE5757; }
-#qunit-tests .fail .test-name,
-#qunit-tests .fail .module-name             { color: #000000; }
-
-#qunit-tests .fail .test-actual             { color: #EE5757; }
-#qunit-tests .fail .test-expected           { color: green;   }
-
-#qunit-banner.qunit-fail                    { background-color: #EE5757; }
-
-
-/** Result */
-
-#qunit-testresult {
-       padding: 0.5em 0.5em 0.5em 2.5em;
-
-       color: #2b81af;
-       background-color: #D2E0E6;
-
-       border-bottom: 1px solid white;
-}
-
-/** Fixture */
-
-#qunit-fixture {
-       position: absolute;
-       top: -10000px;
-       left: -10000px;
-}
diff --git a/tests/qunit/qunit.js b/tests/qunit/qunit.js
deleted file mode 100644 (file)
index e00cca9..0000000
+++ /dev/null
@@ -1,1448 +0,0 @@
-/**
- * QUnit - A JavaScript Unit Testing Framework
- *
- * http://docs.jquery.com/QUnit
- *
- * Copyright (c) 2011 John Resig, Jörn Zaefferer
- * Dual licensed under the MIT (MIT-LICENSE.txt)
- * or GPL (GPL-LICENSE.txt) licenses.
- */
-
-(function(window) {
-
-var defined = {
-       setTimeout: typeof window.setTimeout !== "undefined",
-       sessionStorage: (function() {
-               try {
-                       return !!sessionStorage.getItem;
-               } catch(e){
-                       return false;
-               }
-  })()
-};
-
-var testId = 0;
-
-var Test = function(name, testName, expected, testEnvironmentArg, async, callback) {
-       this.name = name;
-       this.testName = testName;
-       this.expected = expected;
-       this.testEnvironmentArg = testEnvironmentArg;
-       this.async = async;
-       this.callback = callback;
-       this.assertions = [];
-};
-Test.prototype = {
-       init: function() {
-               var tests = id("qunit-tests");
-               if (tests) {
-                       var b = document.createElement("strong");
-                               b.innerHTML = "Running " + this.name;
-                       var li = document.createElement("li");
-                               li.appendChild( b );
-                               li.className = "running";
-                               li.id = this.id = "test-output" + testId++;
-                       tests.appendChild( li );
-               }
-       },
-       setup: function() {
-               if (this.module != config.previousModule) {
-                       if ( config.previousModule ) {
-                               QUnit.moduleDone( {
-                                       name: config.previousModule,
-                                       failed: config.moduleStats.bad,
-                                       passed: config.moduleStats.all - config.moduleStats.bad,
-                                       total: config.moduleStats.all
-                               } );
-                       }
-                       config.previousModule = this.module;
-                       config.moduleStats = { all: 0, bad: 0 };
-                       QUnit.moduleStart( {
-                               name: this.module
-                       } );
-               }
-
-               config.current = this;
-               this.testEnvironment = extend({
-                       setup: function() {},
-                       teardown: function() {}
-               }, this.moduleTestEnvironment);
-               if (this.testEnvironmentArg) {
-                       extend(this.testEnvironment, this.testEnvironmentArg);
-               }
-
-               QUnit.testStart( {
-                       name: this.testName
-               } );
-
-               // allow utility functions to access the current test environment
-               // TODO why??
-               QUnit.current_testEnvironment = this.testEnvironment;
-
-               try {
-                       if ( !config.pollution ) {
-                               saveGlobal();
-                       }
-
-                       this.testEnvironment.setup.call(this.testEnvironment);
-               } catch(e) {
-                       QUnit.ok( false, "Setup failed on " + this.testName + ": " + e.message );
-               }
-       },
-       run: function() {
-               if ( this.async ) {
-                       QUnit.stop();
-               }
-
-               if ( config.notrycatch ) {
-                       this.callback.call(this.testEnvironment);
-                       return;
-               }
-               try {
-                       this.callback.call(this.testEnvironment);
-               } catch(e) {
-                       fail("Test " + this.testName + " died, exception and test follows", e, this.callback);
-                       QUnit.ok( false, "Died on test #" + (this.assertions.length + 1) + ": " + e.message + " - " + QUnit.jsDump.parse(e) );
-                       // else next test will carry the responsibility
-                       saveGlobal();
-
-                       // Restart the tests if they're blocking
-                       if ( config.blocking ) {
-                               start();
-                       }
-               }
-       },
-       teardown: function() {
-               try {
-                       this.testEnvironment.teardown.call(this.testEnvironment);
-                       checkPollution();
-               } catch(e) {
-                       QUnit.ok( false, "Teardown failed on " + this.testName + ": " + e.message );
-               }
-       },
-       finish: function() {
-               if ( this.expected && this.expected != this.assertions.length ) {
-                       QUnit.ok( false, "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run" );
-               }
-
-               var good = 0, bad = 0,
-                       tests = id("qunit-tests");
-
-               config.stats.all += this.assertions.length;
-               config.moduleStats.all += this.assertions.length;
-
-               if ( tests ) {
-                       var ol  = document.createElement("ol");
-
-                       for ( var i = 0; i < this.assertions.length; i++ ) {
-                               var assertion = this.assertions[i];
-
-                               var li = document.createElement("li");
-                               li.className = assertion.result ? "pass" : "fail";
-                               li.innerHTML = assertion.message || (assertion.result ? "okay" : "failed");
-                               ol.appendChild( li );
-
-                               if ( assertion.result ) {
-                                       good++;
-                               } else {
-                                       bad++;
-                                       config.stats.bad++;
-                                       config.moduleStats.bad++;
-                               }
-                       }
-
-                       // store result when possible
-                       if ( QUnit.config.reorder && defined.sessionStorage ) {
-                               if (bad) {
-                                       sessionStorage.setItem("qunit-" + this.module + "-" + this.testName, bad);
-                               } else {
-                                       sessionStorage.removeItem("qunit-" + this.module + "-" + this.testName);
-                               }
-                       }
-
-                       if (bad == 0) {
-                               ol.style.display = "none";
-                       }
-
-                       var b = document.createElement("strong");
-                       b.innerHTML = this.name + " <b class='counts'>(<b class='failed'>" + bad + "</b>, <b class='passed'>" + good + "</b>, " + this.assertions.length + ")</b>";
-
-                       var a = document.createElement("a");
-                       a.innerHTML = "Rerun";
-                       a.href = QUnit.url({ filter: getText([b]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") });
-
-                       addEvent(b, "click", function() {
-                               var next = b.nextSibling.nextSibling,
-                                       display = next.style.display;
-                               next.style.display = display === "none" ? "block" : "none";
-                       });
-
-                       addEvent(b, "dblclick", function(e) {
-                               var target = e && e.target ? e.target : window.event.srcElement;
-                               if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) {
-                                       target = target.parentNode;
-                               }
-                               if ( window.location && target.nodeName.toLowerCase() === "strong" ) {
-                                       window.location = QUnit.url({ filter: getText([target]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") });
-                               }
-                       });
-
-                       var li = id(this.id);
-                       li.className = bad ? "fail" : "pass";
-                       li.removeChild( li.firstChild );
-                       li.appendChild( b );
-                       li.appendChild( a );
-                       li.appendChild( ol );
-
-               } else {
-                       for ( var i = 0; i < this.assertions.length; i++ ) {
-                               if ( !this.assertions[i].result ) {
-                                       bad++;
-                                       config.stats.bad++;
-                                       config.moduleStats.bad++;
-                               }
-                       }
-               }
-
-               try {
-                       QUnit.reset();
-               } catch(e) {
-                       fail("reset() failed, following Test " + this.testName + ", exception and reset fn follows", e, QUnit.reset);
-               }
-
-               QUnit.testDone( {
-                       name: this.testName,
-                       failed: bad,
-                       passed: this.assertions.length - bad,
-                       total: this.assertions.length
-               } );
-       },
-
-       queue: function() {
-               var test = this;
-               synchronize(function() {
-                       test.init();
-               });
-               function run() {
-                       // each of these can by async
-                       synchronize(function() {
-                               test.setup();
-                       });
-                       synchronize(function() {
-                               test.run();
-                       });
-                       synchronize(function() {
-                               test.teardown();
-                       });
-                       synchronize(function() {
-                               test.finish();
-                       });
-               }
-               // defer when previous test run passed, if storage is available
-               var bad = QUnit.config.reorder && defined.sessionStorage && +sessionStorage.getItem("qunit-" + this.module + "-" + this.testName);
-               if (bad) {
-                       run();
-               } else {
-                       synchronize(run);
-               };
-       }
-
-};
-
-var QUnit = {
-
-       // call on start of module test to prepend name to all tests
-       module: function(name, testEnvironment) {
-               config.currentModule = name;
-               config.currentModuleTestEnviroment = testEnvironment;
-       },
-
-       asyncTest: function(testName, expected, callback) {
-               if ( arguments.length === 2 ) {
-                       callback = expected;
-                       expected = 0;
-               }
-
-               QUnit.test(testName, expected, callback, true);
-       },
-
-       test: function(testName, expected, callback, async) {
-               var name = '<span class="test-name">' + testName + '</span>', testEnvironmentArg;
-
-               if ( arguments.length === 2 ) {
-                       callback = expected;
-                       expected = null;
-               }
-               // is 2nd argument a testEnvironment?
-               if ( expected && typeof expected === 'object') {
-                       testEnvironmentArg =  expected;
-                       expected = null;
-               }
-
-               if ( config.currentModule ) {
-                       name = '<span class="module-name">' + config.currentModule + "</span>: " + name;
-               }
-
-               if ( !validTest(config.currentModule + ": " + testName) ) {
-                       return;
-               }
-
-               var test = new Test(name, testName, expected, testEnvironmentArg, async, callback);
-               test.module = config.currentModule;
-               test.moduleTestEnvironment = config.currentModuleTestEnviroment;
-               test.queue();
-       },
-
-       /**
-        * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through.
-        */
-       expect: function(asserts) {
-               config.current.expected = asserts;
-       },
-
-       /**
-        * Asserts true.
-        * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" );
-        */
-       ok: function(a, msg) {
-               a = !!a;
-               var details = {
-                       result: a,
-                       message: msg
-               };
-               msg = escapeHtml(msg);
-               QUnit.log(details);
-               config.current.assertions.push({
-                       result: a,
-                       message: msg
-               });
-       },
-
-       /**
-        * Checks that the first two arguments are equal, with an optional message.
-        * Prints out both actual and expected values.
-        *
-        * Prefered to ok( actual == expected, message )
-        *
-        * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." );
-        *
-        * @param Object actual
-        * @param Object expected
-        * @param String message (optional)
-        */
-       equal: function(actual, expected, message) {
-               QUnit.push(expected == actual, actual, expected, message);
-       },
-
-       notEqual: function(actual, expected, message) {
-               QUnit.push(expected != actual, actual, expected, message);
-       },
-
-       deepEqual: function(actual, expected, message) {
-               QUnit.push(QUnit.equiv(actual, expected), actual, expected, message);
-       },
-
-       notDeepEqual: function(actual, expected, message) {
-               QUnit.push(!QUnit.equiv(actual, expected), actual, expected, message);
-       },
-
-       strictEqual: function(actual, expected, message) {
-               QUnit.push(expected === actual, actual, expected, message);
-       },
-
-       notStrictEqual: function(actual, expected, message) {
-               QUnit.push(expected !== actual, actual, expected, message);
-       },
-
-       raises: function(block, expected, message) {
-               var actual, ok = false;
-
-               if (typeof expected === 'string') {
-                       message = expected;
-                       expected = null;
-               }
-
-               try {
-                       block();
-               } catch (e) {
-                       actual = e;
-               }
-
-               if (actual) {
-                       // we don't want to validate thrown error
-                       if (!expected) {
-                               ok = true;
-                       // expected is a regexp
-                       } else if (QUnit.objectType(expected) === "regexp") {
-                               ok = expected.test(actual);
-                       // expected is a constructor
-                       } else if (actual instanceof expected) {
-                               ok = true;
-                       // expected is a validation function which returns true is validation passed
-                       } else if (expected.call({}, actual) === true) {
-                               ok = true;
-                       }
-               }
-
-               QUnit.ok(ok, message);
-       },
-
-       start: function() {
-               config.semaphore--;
-               if (config.semaphore > 0) {
-                       // don't start until equal number of stop-calls
-                       return;
-               }
-               if (config.semaphore < 0) {
-                       // ignore if start is called more often then stop
-                       config.semaphore = 0;
-               }
-               // A slight delay, to avoid any current callbacks
-               if ( defined.setTimeout ) {
-                       window.setTimeout(function() {
-                               if ( config.timeout ) {
-                                       clearTimeout(config.timeout);
-                               }
-
-                               config.blocking = false;
-                               process();
-                       }, 13);
-               } else {
-                       config.blocking = false;
-                       process();
-               }
-       },
-
-       stop: function(timeout) {
-               config.semaphore++;
-               config.blocking = true;
-
-               if ( timeout && defined.setTimeout ) {
-                       clearTimeout(config.timeout);
-                       config.timeout = window.setTimeout(function() {
-                               QUnit.ok( false, "Test timed out" );
-                               QUnit.start();
-                       }, timeout);
-               }
-       }
-};
-
-// Backwards compatibility, deprecated
-QUnit.equals = QUnit.equal;
-QUnit.same = QUnit.deepEqual;
-
-// Maintain internal state
-var config = {
-       // The queue of tests to run
-       queue: [],
-
-       // block until document ready
-       blocking: true,
-
-       // by default, run previously failed tests first
-       // very useful in combination with "Hide passed tests" checked
-       reorder: true,
-
-       noglobals: false,
-       notrycatch: false
-};
-
-// Load paramaters
-(function() {
-       var location = window.location || { search: "", protocol: "file:" },
-               params = location.search.slice( 1 ).split( "&" ),
-               length = params.length,
-               urlParams = {},
-               current;
-
-       if ( params[ 0 ] ) {
-               for ( var i = 0; i < length; i++ ) {
-                       current = params[ i ].split( "=" );
-                       current[ 0 ] = decodeURIComponent( current[ 0 ] );
-                       // allow just a key to turn on a flag, e.g., test.html?noglobals
-                       current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true;
-                       urlParams[ current[ 0 ] ] = current[ 1 ];
-                       if ( current[ 0 ] in config ) {
-                               config[ current[ 0 ] ] = current[ 1 ];
-                       }
-               }
-       }
-
-       QUnit.urlParams = urlParams;
-       config.filter = urlParams.filter;
-
-       // Figure out if we're running the tests from a server or not
-       QUnit.isLocal = !!(location.protocol === 'file:');
-})();
-
-// Expose the API as global variables, unless an 'exports'
-// object exists, in that case we assume we're in CommonJS
-if ( typeof exports === "undefined" || typeof require === "undefined" ) {
-       extend(window, QUnit);
-       window.QUnit = QUnit;
-} else {
-       extend(exports, QUnit);
-       exports.QUnit = QUnit;
-}
-
-// define these after exposing globals to keep them in these QUnit namespace only
-extend(QUnit, {
-       config: config,
-
-       // Initialize the configuration options
-       init: function() {
-               extend(config, {
-                       stats: { all: 0, bad: 0 },
-                       moduleStats: { all: 0, bad: 0 },
-                       started: +new Date,
-                       updateRate: 1000,
-                       blocking: false,
-                       autostart: true,
-                       autorun: false,
-                       filter: "",
-                       queue: [],
-                       semaphore: 0
-               });
-
-               var tests = id( "qunit-tests" ),
-                       banner = id( "qunit-banner" ),
-                       result = id( "qunit-testresult" );
-
-               if ( tests ) {
-                       tests.innerHTML = "";
-               }
-
-               if ( banner ) {
-                       banner.className = "";
-               }
-
-               if ( result ) {
-                       result.parentNode.removeChild( result );
-               }
-
-               if ( tests ) {
-                       result = document.createElement( "p" );
-                       result.id = "qunit-testresult";
-                       result.className = "result";
-                       tests.parentNode.insertBefore( result, tests );
-                       result.innerHTML = 'Running...<br/>&nbsp;';
-               }
-       },
-
-       /**
-        * Resets the test setup. Useful for tests that modify the DOM.
-        *
-        * If jQuery is available, uses jQuery's html(), otherwise just innerHTML.
-        */
-       reset: function() {
-               if ( window.jQuery ) {
-                       jQuery( "#qunit-fixture" ).html( config.fixture );
-               } else {
-                       var main = id( 'qunit-fixture' );
-                       if ( main ) {
-                               main.innerHTML = config.fixture;
-                       }
-               }
-       },
-
-       /**
-        * Trigger an event on an element.
-        *
-        * @example triggerEvent( document.body, "click" );
-        *
-        * @param DOMElement elem
-        * @param String type
-        */
-       triggerEvent: function( elem, type, event ) {
-               if ( document.createEvent ) {
-                       event = document.createEvent("MouseEvents");
-                       event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView,
-                               0, 0, 0, 0, 0, false, false, false, false, 0, null);
-                       elem.dispatchEvent( event );
-
-               } else if ( elem.fireEvent ) {
-                       elem.fireEvent("on"+type);
-               }
-       },
-
-       // Safe object type checking
-       is: function( type, obj ) {
-               return QUnit.objectType( obj ) == type;
-       },
-
-       objectType: function( obj ) {
-               if (typeof obj === "undefined") {
-                               return "undefined";
-
-               // consider: typeof null === object
-               }
-               if (obj === null) {
-                               return "null";
-               }
-
-               var type = Object.prototype.toString.call( obj )
-                       .match(/^\[object\s(.*)\]$/)[1] || '';
-
-               switch (type) {
-                               case 'Number':
-                                               if (isNaN(obj)) {
-                                                               return "nan";
-                                               } else {
-                                                               return "number";
-                                               }
-                               case 'String':
-                               case 'Boolean':
-                               case 'Array':
-                               case 'Date':
-                               case 'RegExp':
-                               case 'Function':
-                                               return type.toLowerCase();
-               }
-               if (typeof obj === "object") {
-                               return "object";
-               }
-               return undefined;
-       },
-
-       push: function(result, actual, expected, message) {
-               var details = {
-                       result: result,
-                       message: message,
-                       actual: actual,
-                       expected: expected
-               };
-
-               message = escapeHtml(message) || (result ? "okay" : "failed");
-               message = '<span class="test-message">' + message + "</span>";
-               expected = escapeHtml(QUnit.jsDump.parse(expected));
-               actual = escapeHtml(QUnit.jsDump.parse(actual));
-               var output = message + '<table><tr class="test-expected"><th>Expected: </th><td><pre>' + expected + '</pre></td></tr>';
-               if (actual != expected) {
-                       output += '<tr class="test-actual"><th>Result: </th><td><pre>' + actual + '</pre></td></tr>';
-                       output += '<tr class="test-diff"><th>Diff: </th><td><pre>' + QUnit.diff(expected, actual) +'</pre></td></tr>';
-               }
-               if (!result) {
-                       var source = sourceFromStacktrace();
-                       if (source) {
-                               details.source = source;
-                               output += '<tr class="test-source"><th>Source: </th><td><pre>' + escapeHtml(source) + '</pre></td></tr>';
-                       }
-               }
-               output += "</table>";
-
-               QUnit.log(details);
-
-               config.current.assertions.push({
-                       result: !!result,
-                       message: output
-               });
-       },
-
-       url: function( params ) {
-               params = extend( extend( {}, QUnit.urlParams ), params );
-               var querystring = "?",
-                       key;
-               for ( key in params ) {
-                       querystring += encodeURIComponent( key ) + "=" +
-                               encodeURIComponent( params[ key ] ) + "&";
-               }
-               return window.location.pathname + querystring.slice( 0, -1 );
-       },
-
-       // Logging callbacks; all receive a single argument with the listed properties
-       // run test/logs.html for any related changes
-       begin: function() {},
-       // done: { failed, passed, total, runtime }
-       done: function() {},
-       // log: { result, actual, expected, message }
-       log: function() {},
-       // testStart: { name }
-       testStart: function() {},
-       // testDone: { name, failed, passed, total }
-       testDone: function() {},
-       // moduleStart: { name }
-       moduleStart: function() {},
-       // moduleDone: { name, failed, passed, total }
-       moduleDone: function() {}
-});
-
-if ( typeof document === "undefined" || document.readyState === "complete" ) {
-       config.autorun = true;
-}
-
-addEvent(window, "load", function() {
-       QUnit.begin({});
-
-       // Initialize the config, saving the execution queue
-       var oldconfig = extend({}, config);
-       QUnit.init();
-       extend(config, oldconfig);
-
-       config.blocking = false;
-
-       var userAgent = id("qunit-userAgent");
-       if ( userAgent ) {
-               userAgent.innerHTML = navigator.userAgent;
-       }
-       var banner = id("qunit-header");
-       if ( banner ) {
-               banner.innerHTML = '<a href="' + QUnit.url({ filter: undefined }) + '"> ' + banner.innerHTML + '</a> ' +
-                       '<label><input name="noglobals" type="checkbox"' + ( config.noglobals ? ' checked="checked"' : '' ) + '>noglobals</label>' +
-                       '<label><input name="notrycatch" type="checkbox"' + ( config.notrycatch ? ' checked="checked"' : '' ) + '>notrycatch</label>';
-               addEvent( banner, "change", function( event ) {
-                       var params = {};
-                       params[ event.target.name ] = event.target.checked ? true : undefined;
-                       window.location = QUnit.url( params );
-               });
-       }
-
-       var toolbar = id("qunit-testrunner-toolbar");
-       if ( toolbar ) {
-               var filter = document.createElement("input");
-               filter.type = "checkbox";
-               filter.id = "qunit-filter-pass";
-               addEvent( filter, "click", function() {
-                       var ol = document.getElementById("qunit-tests");
-                       if ( filter.checked ) {
-                               ol.className = ol.className + " hidepass";
-                       } else {
-                               var tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " ";
-                               ol.className = tmp.replace(/ hidepass /, " ");
-                       }
-                       if ( defined.sessionStorage ) {
-                               if (filter.checked) {
-                                       sessionStorage.setItem("qunit-filter-passed-tests",  "true");
-                               } else {
-                                       sessionStorage.removeItem("qunit-filter-passed-tests");
-                               }
-                       }
-               });
-               if ( defined.sessionStorage && sessionStorage.getItem("qunit-filter-passed-tests") ) {
-                       filter.checked = true;
-                       var ol = document.getElementById("qunit-tests");
-                       ol.className = ol.className + " hidepass";
-               }
-               toolbar.appendChild( filter );
-
-               var label = document.createElement("label");
-               label.setAttribute("for", "qunit-filter-pass");
-               label.innerHTML = "Hide passed tests";
-               toolbar.appendChild( label );
-       }
-
-       var main = id('qunit-fixture');
-       if ( main ) {
-               config.fixture = main.innerHTML;
-       }
-
-       if (config.autostart) {
-               QUnit.start();
-       }
-});
-
-function done() {
-       config.autorun = true;
-
-       // Log the last module results
-       if ( config.currentModule ) {
-               QUnit.moduleDone( {
-                       name: config.currentModule,
-                       failed: config.moduleStats.bad,
-                       passed: config.moduleStats.all - config.moduleStats.bad,
-                       total: config.moduleStats.all
-               } );
-       }
-
-       var banner = id("qunit-banner"),
-               tests = id("qunit-tests"),
-               runtime = +new Date - config.started,
-               passed = config.stats.all - config.stats.bad,
-               html = [
-                       'Tests completed in ',
-                       runtime,
-                       ' milliseconds.<br/>',
-                       '<span class="passed">',
-                       passed,
-                       '</span> tests of <span class="total">',
-                       config.stats.all,
-                       '</span> passed, <span class="failed">',
-                       config.stats.bad,
-                       '</span> failed.'
-               ].join('');
-
-       if ( banner ) {
-               banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass");
-       }
-
-       if ( tests ) {
-               id( "qunit-testresult" ).innerHTML = html;
-       }
-
-       if ( typeof document !== "undefined" && document.title ) {
-               // show ✖ for good, ✔ for bad suite result in title
-               // use escape sequences in case file gets loaded with non-utf-8-charset
-               document.title = (config.stats.bad ? "\u2716" : "\u2714") + " " + document.title;
-       }
-
-       QUnit.done( {
-               failed: config.stats.bad,
-               passed: passed,
-               total: config.stats.all,
-               runtime: runtime
-       } );
-}
-
-function validTest( name ) {
-       var filter = config.filter,
-               run = false;
-
-       if ( !filter ) {
-               return true;
-       }
-
-       var not = filter.charAt( 0 ) === "!";
-       if ( not ) {
-               filter = filter.slice( 1 );
-       }
-
-       if ( name.indexOf( filter ) !== -1 ) {
-               return !not;
-       }
-
-       if ( not ) {
-               run = true;
-       }
-
-       return run;
-}
-
-// so far supports only Firefox, Chrome and Opera (buggy)
-// could be extended in the future to use something like https://github.com/csnover/TraceKit
-function sourceFromStacktrace() {
-       try {
-               throw new Error();
-       } catch ( e ) {
-               if (e.stacktrace) {
-                       // Opera
-                       return e.stacktrace.split("\n")[6];
-               } else if (e.stack) {
-                       // Firefox, Chrome
-                       return e.stack.split("\n")[4];
-               }
-       }
-}
-
-function escapeHtml(s) {
-       if (!s) {
-               return "";
-       }
-       s = s + "";
-       return s.replace(/[\&"<>\\]/g, function(s) {
-               switch(s) {
-                       case "&": return "&amp;";
-                       case "\\": return "\\\\";
-                       case '"': return '\"';
-                       case "<": return "&lt;";
-                       case ">": return "&gt;";
-                       default: return s;
-               }
-       });
-}
-
-function synchronize( callback ) {
-       config.queue.push( callback );
-
-       if ( config.autorun && !config.blocking ) {
-               process();
-       }
-}
-
-function process() {
-       var start = (new Date()).getTime();
-
-       while ( config.queue.length && !config.blocking ) {
-               if ( config.updateRate <= 0 || (((new Date()).getTime() - start) < config.updateRate) ) {
-                       config.queue.shift()();
-               } else {
-                       window.setTimeout( process, 13 );
-                       break;
-               }
-       }
-  if (!config.blocking && !config.queue.length) {
-    done();
-  }
-}
-
-function saveGlobal() {
-       config.pollution = [];
-
-       if ( config.noglobals ) {
-               for ( var key in window ) {
-                       config.pollution.push( key );
-               }
-       }
-}
-
-function checkPollution( name ) {
-       var old = config.pollution;
-       saveGlobal();
-
-       var newGlobals = diff( config.pollution, old );
-       if ( newGlobals.length > 0 ) {
-               ok( false, "Introduced global variable(s): " + newGlobals.join(", ") );
-       }
-
-       var deletedGlobals = diff( old, config.pollution );
-       if ( deletedGlobals.length > 0 ) {
-               ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") );
-       }
-}
-
-// returns a new Array with the elements that are in a but not in b
-function diff( a, b ) {
-       var result = a.slice();
-       for ( var i = 0; i < result.length; i++ ) {
-               for ( var j = 0; j < b.length; j++ ) {
-                       if ( result[i] === b[j] ) {
-                               result.splice(i, 1);
-                               i--;
-                               break;
-                       }
-               }
-       }
-       return result;
-}
-
-function fail(message, exception, callback) {
-       if ( typeof console !== "undefined" && console.error && console.warn ) {
-               console.error(message);
-               console.error(exception);
-               console.warn(callback.toString());
-
-       } else if ( window.opera && opera.postError ) {
-               opera.postError(message, exception, callback.toString);
-       }
-}
-
-function extend(a, b) {
-       for ( var prop in b ) {
-               if ( b[prop] === undefined ) {
-                       delete a[prop];
-               } else {
-                       a[prop] = b[prop];
-               }
-       }
-
-       return a;
-}
-
-function addEvent(elem, type, fn) {
-       if ( elem.addEventListener ) {
-               elem.addEventListener( type, fn, false );
-       } else if ( elem.attachEvent ) {
-               elem.attachEvent( "on" + type, fn );
-       } else {
-               fn();
-       }
-}
-
-function id(name) {
-       return !!(typeof document !== "undefined" && document && document.getElementById) &&
-               document.getElementById( name );
-}
-
-// Test for equality any JavaScript type.
-// Discussions and reference: http://philrathe.com/articles/equiv
-// Test suites: http://philrathe.com/tests/equiv
-// Author: Philippe Rathé <[email protected]>
-QUnit.equiv = function () {
-
-    var innerEquiv; // the real equiv function
-    var callers = []; // stack to decide between skip/abort functions
-    var parents = []; // stack to avoiding loops from circular referencing
-
-    // Call the o related callback with the given arguments.
-    function bindCallbacks(o, callbacks, args) {
-        var prop = QUnit.objectType(o);
-        if (prop) {
-            if (QUnit.objectType(callbacks[prop]) === "function") {
-                return callbacks[prop].apply(callbacks, args);
-            } else {
-                return callbacks[prop]; // or undefined
-            }
-        }
-    }
-
-    var callbacks = function () {
-
-        // for string, boolean, number and null
-        function useStrictEquality(b, a) {
-            if (b instanceof a.constructor || a instanceof b.constructor) {
-                // to catch short annotaion VS 'new' annotation of a declaration
-                // e.g. var i = 1;
-                //      var j = new Number(1);
-                return a == b;
-            } else {
-                return a === b;
-            }
-        }
-
-        return {
-            "string": useStrictEquality,
-            "boolean": useStrictEquality,
-            "number": useStrictEquality,
-            "null": useStrictEquality,
-            "undefined": useStrictEquality,
-
-            "nan": function (b) {
-                return isNaN(b);
-            },
-
-            "date": function (b, a) {
-                return QUnit.objectType(b) === "date" && a.valueOf() === b.valueOf();
-            },
-
-            "regexp": function (b, a) {
-                return QUnit.objectType(b) === "regexp" &&
-                    a.source === b.source && // the regex itself
-                    a.global === b.global && // and its modifers (gmi) ...
-                    a.ignoreCase === b.ignoreCase &&
-                    a.multiline === b.multiline;
-            },
-
-            // - skip when the property is a method of an instance (OOP)
-            // - abort otherwise,
-            //   initial === would have catch identical references anyway
-            "function": function () {
-                var caller = callers[callers.length - 1];
-                return caller !== Object &&
-                        typeof caller !== "undefined";
-            },
-
-            "array": function (b, a) {
-                var i, j, loop;
-                var len;
-
-                // b could be an object literal here
-                if ( ! (QUnit.objectType(b) === "array")) {
-                    return false;
-                }
-
-                len = a.length;
-                if (len !== b.length) { // safe and faster
-                    return false;
-                }
-
-                //track reference to avoid circular references
-                parents.push(a);
-                for (i = 0; i < len; i++) {
-                    loop = false;
-                    for(j=0;j<parents.length;j++){
-                        if(parents[j] === a[i]){
-                            loop = true;//dont rewalk array
-                        }
-                    }
-                    if (!loop && ! innerEquiv(a[i], b[i])) {
-                        parents.pop();
-                        return false;
-                    }
-                }
-                parents.pop();
-                return true;
-            },
-
-            "object": function (b, a) {
-                var i, j, loop;
-                var eq = true; // unless we can proove it
-                var aProperties = [], bProperties = []; // collection of strings
-
-                // comparing constructors is more strict than using instanceof
-                if ( a.constructor !== b.constructor) {
-                    return false;
-                }
-
-                // stack constructor before traversing properties
-                callers.push(a.constructor);
-                //track reference to avoid circular references
-                parents.push(a);
-
-                for (i in a) { // be strict: don't ensures hasOwnProperty and go deep
-                    loop = false;
-                    for(j=0;j<parents.length;j++){
-                        if(parents[j] === a[i])
-                            loop = true; //don't go down the same path twice
-                    }
-                    aProperties.push(i); // collect a's properties
-
-                    if (!loop && ! innerEquiv(a[i], b[i])) {
-                        eq = false;
-                        break;
-                    }
-                }
-
-                callers.pop(); // unstack, we are done
-                parents.pop();
-
-                for (i in b) {
-                    bProperties.push(i); // collect b's properties
-                }
-
-                // Ensures identical properties name
-                return eq && innerEquiv(aProperties.sort(), bProperties.sort());
-            }
-        };
-    }();
-
-    innerEquiv = function () { // can take multiple arguments
-        var args = Array.prototype.slice.apply(arguments);
-        if (args.length < 2) {
-            return true; // end transition
-        }
-
-        return (function (a, b) {
-            if (a === b) {
-                return true; // catch the most you can
-            } else if (a === null || b === null || typeof a === "undefined" || typeof b === "undefined" || QUnit.objectType(a) !== QUnit.objectType(b)) {
-                return false; // don't lose time with error prone cases
-            } else {
-                return bindCallbacks(a, callbacks, [b, a]);
-            }
-
-        // apply transition with (1..n) arguments
-        })(args[0], args[1]) && arguments.callee.apply(this, args.splice(1, args.length -1));
-    };
-
-    return innerEquiv;
-
-}();
-
-/**
- * jsDump
- * Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com
- * Licensed under BSD (http://www.opensource.org/licenses/bsd-license.php)
- * Date: 5/15/2008
- * @projectDescription Advanced and extensible data dumping for Javascript.
- * @version 1.0.0
- * @author Ariel Flesler
- * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html}
- */
-QUnit.jsDump = (function() {
-       function quote( str ) {
-               return '"' + str.toString().replace(/"/g, '\\"') + '"';
-       };
-       function literal( o ) {
-               return o + '';
-       };
-       function join( pre, arr, post ) {
-               var s = jsDump.separator(),
-                       base = jsDump.indent(),
-                       inner = jsDump.indent(1);
-               if ( arr.join )
-                       arr = arr.join( ',' + s + inner );
-               if ( !arr )
-                       return pre + post;
-               return [ pre, inner + arr, base + post ].join(s);
-       };
-       function array( arr ) {
-               var i = arr.length,     ret = Array(i);
-               this.up();
-               while ( i-- )
-                       ret[i] = this.parse( arr[i] );
-               this.down();
-               return join( '[', ret, ']' );
-       };
-
-       var reName = /^function (\w+)/;
-
-       var jsDump = {
-               parse:function( obj, type ) { //type is used mostly internally, you can fix a (custom)type in advance
-                       var     parser = this.parsers[ type || this.typeOf(obj) ];
-                       type = typeof parser;
-
-                       return type == 'function' ? parser.call( this, obj ) :
-                                  type == 'string' ? parser :
-                                  this.parsers.error;
-               },
-               typeOf:function( obj ) {
-                       var type;
-                       if ( obj === null ) {
-                               type = "null";
-                       } else if (typeof obj === "undefined") {
-                               type = "undefined";
-                       } else if (QUnit.is("RegExp", obj)) {
-                               type = "regexp";
-                       } else if (QUnit.is("Date", obj)) {
-                               type = "date";
-                       } else if (QUnit.is("Function", obj)) {
-                               type = "function";
-                       } else if (typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined") {
-                               type = "window";
-                       } else if (obj.nodeType === 9) {
-                               type = "document";
-                       } else if (obj.nodeType) {
-                               type = "node";
-                       } else if (typeof obj === "object" && typeof obj.length === "number" && obj.length >= 0) {
-                               type = "array";
-                       } else {
-                               type = typeof obj;
-                       }
-                       return type;
-               },
-               separator:function() {
-                       return this.multiline ? this.HTML ? '<br />' : '\n' : this.HTML ? '&nbsp;' : ' ';
-               },
-               indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing
-                       if ( !this.multiline )
-                               return '';
-                       var chr = this.indentChar;
-                       if ( this.HTML )
-                               chr = chr.replace(/\t/g,'   ').replace(/ /g,'&nbsp;');
-                       return Array( this._depth_ + (extra||0) ).join(chr);
-               },
-               up:function( a ) {
-                       this._depth_ += a || 1;
-               },
-               down:function( a ) {
-                       this._depth_ -= a || 1;
-               },
-               setParser:function( name, parser ) {
-                       this.parsers[name] = parser;
-               },
-               // The next 3 are exposed so you can use them
-               quote:quote,
-               literal:literal,
-               join:join,
-               //
-               _depth_: 1,
-               // This is the list of parsers, to modify them, use jsDump.setParser
-               parsers:{
-                       window: '[Window]',
-                       document: '[Document]',
-                       error:'[ERROR]', //when no parser is found, shouldn't happen
-                       unknown: '[Unknown]',
-                       'null':'null',
-                       'undefined':'undefined',
-                       'function':function( fn ) {
-                               var ret = 'function',
-                                       name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE
-                               if ( name )
-                                       ret += ' ' + name;
-                               ret += '(';
-
-                               ret = [ ret, QUnit.jsDump.parse( fn, 'functionArgs' ), '){'].join('');
-                               return join( ret, QUnit.jsDump.parse(fn,'functionCode'), '}' );
-                       },
-                       array: array,
-                       nodelist: array,
-                       arguments: array,
-                       object:function( map ) {
-                               var ret = [ ];
-                               QUnit.jsDump.up();
-                               for ( var key in map )
-                                       ret.push( QUnit.jsDump.parse(key,'key') + ': ' + QUnit.jsDump.parse(map[key]) );
-                               QUnit.jsDump.down();
-                               return join( '{', ret, '}' );
-                       },
-                       node:function( node ) {
-                               var open = QUnit.jsDump.HTML ? '&lt;' : '<',
-                                       close = QUnit.jsDump.HTML ? '&gt;' : '>';
-
-                               var tag = node.nodeName.toLowerCase(),
-                                       ret = open + tag;
-
-                               for ( var a in QUnit.jsDump.DOMAttrs ) {
-                                       var val = node[QUnit.jsDump.DOMAttrs[a]];
-                                       if ( val )
-                                               ret += ' ' + a + '=' + QUnit.jsDump.parse( val, 'attribute' );
-                               }
-                               return ret + close + open + '/' + tag + close;
-                       },
-                       functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function
-                               var l = fn.length;
-                               if ( !l ) return '';
-
-                               var args = Array(l);
-                               while ( l-- )
-                                       args[l] = String.fromCharCode(97+l);//97 is 'a'
-                               return ' ' + args.join(', ') + ' ';
-                       },
-                       key:quote, //object calls it internally, the key part of an item in a map
-                       functionCode:'[code]', //function calls it internally, it's the content of the function
-                       attribute:quote, //node calls it internally, it's an html attribute value
-                       string:quote,
-                       date:quote,
-                       regexp:literal, //regex
-                       number:literal,
-                       'boolean':literal
-               },
-               DOMAttrs:{//attributes to dump from nodes, name=>realName
-                       id:'id',
-                       name:'name',
-                       'class':'className'
-               },
-               HTML:false,//if true, entities are escaped ( <, >, \t, space and \n )
-               indentChar:'  ',//indentation unit
-               multiline:true //if true, items in a collection, are separated by a \n, else just a space.
-       };
-
-       return jsDump;
-})();
-
-// from Sizzle.js
-function getText( elems ) {
-       var ret = "", elem;
-
-       for ( var i = 0; elems[i]; i++ ) {
-               elem = elems[i];
-
-               // Get the text from text nodes and CDATA nodes
-               if ( elem.nodeType === 3 || elem.nodeType === 4 ) {
-                       ret += elem.nodeValue;
-
-               // Traverse everything else, except comment nodes
-               } else if ( elem.nodeType !== 8 ) {
-                       ret += getText( elem.childNodes );
-               }
-       }
-
-       return ret;
-};
-
-/*
- * Javascript Diff Algorithm
- *  By John Resig (http://ejohn.org/)
- *  Modified by Chu Alan "sprite"
- *
- * Released under the MIT license.
- *
- * More Info:
- *  http://ejohn.org/projects/javascript-diff-algorithm/
- *
- * Usage: QUnit.diff(expected, actual)
- *
- * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the  quick <del>brown </del> fox <del>jumped </del><ins>jumps </ins> over"
- */
-QUnit.diff = (function() {
-       function diff(o, n){
-               var ns = new Object();
-               var os = new Object();
-
-               for (var i = 0; i < n.length; i++) {
-                       if (ns[n[i]] == null)
-                               ns[n[i]] = {
-                                       rows: new Array(),
-                                       o: null
-                               };
-                       ns[n[i]].rows.push(i);
-               }
-
-               for (var i = 0; i < o.length; i++) {
-                       if (os[o[i]] == null)
-                               os[o[i]] = {
-                                       rows: new Array(),
-                                       n: null
-                               };
-                       os[o[i]].rows.push(i);
-               }
-
-               for (var i in ns) {
-                       if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) {
-                               n[ns[i].rows[0]] = {
-                                       text: n[ns[i].rows[0]],
-                                       row: os[i].rows[0]
-                               };
-                               o[os[i].rows[0]] = {
-                                       text: o[os[i].rows[0]],
-                                       row: ns[i].rows[0]
-                               };
-                       }
-               }
-
-               for (var i = 0; i < n.length - 1; i++) {
-                       if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null &&
-                       n[i + 1] == o[n[i].row + 1]) {
-                               n[i + 1] = {
-                                       text: n[i + 1],
-                                       row: n[i].row + 1
-                               };
-                               o[n[i].row + 1] = {
-                                       text: o[n[i].row + 1],
-                                       row: i + 1
-                               };
-                       }
-               }
-
-               for (var i = n.length - 1; i > 0; i--) {
-                       if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null &&
-                       n[i - 1] == o[n[i].row - 1]) {
-                               n[i - 1] = {
-                                       text: n[i - 1],
-                                       row: n[i].row - 1
-                               };
-                               o[n[i].row - 1] = {
-                                       text: o[n[i].row - 1],
-                                       row: i - 1
-                               };
-                       }
-               }
-
-               return {
-                       o: o,
-                       n: n
-               };
-       }
-
-       return function(o, n){
-               o = o.replace(/\s+$/, '');
-               n = n.replace(/\s+$/, '');
-               var out = diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/));
-
-               var str = "";
-
-               var oSpace = o.match(/\s+/g);
-               if (oSpace == null) {
-                       oSpace = [" "];
-               }
-               else {
-                       oSpace.push(" ");
-               }
-               var nSpace = n.match(/\s+/g);
-               if (nSpace == null) {
-                       nSpace = [" "];
-               }
-               else {
-                       nSpace.push(" ");
-               }
-
-               if (out.n.length == 0) {
-                       for (var i = 0; i < out.o.length; i++) {
-                               str += '<del>' + out.o[i] + oSpace[i] + "</del>";
-                       }
-               }
-               else {
-                       if (out.n[0].text == null) {
-                               for (n = 0; n < out.o.length && out.o[n].text == null; n++) {
-                                       str += '<del>' + out.o[n] + oSpace[n] + "</del>";
-                               }
-                       }
-
-                       for (var i = 0; i < out.n.length; i++) {
-                               if (out.n[i].text == null) {
-                                       str += '<ins>' + out.n[i] + nSpace[i] + "</ins>";
-                               }
-                               else {
-                                       var pre = "";
-
-                                       for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) {
-                                               pre += '<del>' + out.o[n] + oSpace[n] + "</del>";
-                                       }
-                                       str += " " + out.n[i].text + nSpace[i] + pre;
-                               }
-                       }
-               }
-
-               return str;
-       };
-})();
-
-})(this);
index f4ab2ba..2709209 100644 (file)
@@ -282,7 +282,7 @@ class ApiTest(CoverageTest):
         self.check_code1_code2(cov)
 
     def test_start_save_stop(self):
-        self.skip("Expected failure: https://bitbucket.org/ned/coveragepy/issue/79")
+        self.skipTest("Expected failure: https://bitbucket.org/ned/coveragepy/issue/79")
         self.make_code1_code2()
         cov = coverage.Coverage()
         cov.start()
@@ -331,7 +331,7 @@ class NamespaceModuleTest(CoverageTest):
     def setUp(self):
         super(NamespaceModuleTest, self).setUp()
         if env.PYVERSION < (3, 3):
-            self.skip("Python before 3.3 doesn't have namespace packages")
+            self.skipTest("Python before 3.3 doesn't have namespace packages")
 
     def test_explicit_namespace_module(self):
         self.make_file("namespace/package/module.py", "VAR = 1\n")
@@ -429,7 +429,7 @@ class SourceOmitIncludeTest(OmitIncludeTestsMixin, CoverageTest):
         """
         cov = coverage.Coverage(**kwargs)
         cov.start()
-        import usepkgs  # pragma: nested   # pylint: disable=import-error,unused-variable
+        import usepkgs  # pragma: nested   # pylint: disable=import-error
         cov.stop()      # pragma: nested
         data = cov.get_data()
         summary = data.line_counts()
@@ -468,7 +468,7 @@ class ReportIncludeOmitTest(OmitIncludeTestsMixin, CoverageTest):
         """Try coverage.report()."""
         cov = coverage.Coverage()
         cov.start()
-        import usepkgs  # pragma: nested   # pylint: disable=import-error,unused-variable
+        import usepkgs  # pragma: nested   # pylint: disable=import-error
         cov.stop()      # pragma: nested
         report = StringIO()
         cov.report(file=report, **kwargs)
@@ -487,7 +487,7 @@ class XmlIncludeOmitTest(OmitIncludeTestsMixin, CoverageTest):
         """Try coverage.xml_report()."""
         cov = coverage.Coverage()
         cov.start()
-        import usepkgs  # pragma: nested   # pylint: disable=import-error,unused-variable
+        import usepkgs  # pragma: nested   # pylint: disable=import-error
         cov.stop()      # pragma: nested
         cov.xml_report(outfile="-", **kwargs)
         return self.stdout()
index 16efbcd..5ea2fe1 100644 (file)
@@ -370,7 +370,7 @@ class LoopArcTest(CoverageTest):
 
     def test_other_comprehensions(self):
         if env.PYVERSION < (2, 7):
-            self.skip("Don't have set or dict comprehensions before 2.7")
+            self.skipTest("Don't have set or dict comprehensions before 2.7")
         # Set comprehension:
         self.check_coverage("""\
             o = ((1,2), (3,4))
@@ -394,7 +394,7 @@ class LoopArcTest(CoverageTest):
 
     def test_multiline_dict_comp(self):
         if env.PYVERSION < (2, 7):
-            self.skip("Don't have set or dict comprehensions before 2.7")
+            self.skipTest("Don't have set or dict comprehensions before 2.7")
         if env.PYVERSION < (3, 5):
             arcz = "-42 2B B-4   2-4"
         else:
@@ -965,7 +965,7 @@ class YieldTest(CoverageTest):
 
     def test_yield_from(self):
         if env.PYVERSION < (3, 3):
-            self.skip("Python before 3.3 doesn't have 'yield from'")
+            self.skipTest("Python before 3.3 doesn't have 'yield from'")
         self.check_coverage("""\
             def gen(inp):
                 i = 2
@@ -1036,7 +1036,7 @@ class MiscArcTest(CoverageTest):
 
     def test_unpacked_literals(self):
         if env.PYVERSION < (3, 5):
-            self.skip("Don't have unpacked literals until 3.5")
+            self.skipTest("Don't have unpacked literals until 3.5")
         self.check_coverage("""\
             d = {
                 'a': 2,
@@ -1302,7 +1302,7 @@ class AsyncTest(CoverageTest):
 
     def setUp(self):
         if env.PYVERSION < (3, 5):
-            self.skip("Async features are new in Python 3.5")
+            self.skipTest("Async features are new in Python 3.5")
         super(AsyncTest, self).setUp()
 
     def test_async(self):
index ee60ed4..1e72c4f 100644 (file)
@@ -190,6 +190,12 @@ class CmdLineTest(BaseCmdLineTest):
         # coverage combine with args
         self.cmd_executes("combine datadir1", """\
             .coverage()
+            .combine(["datadir1"])
+            .save()
+            """)
+        # coverage combine, appending
+        self.cmd_executes("combine --append datadir1", """\
+            .coverage()
             .load()
             .combine(["datadir1"])
             .save()
@@ -197,7 +203,6 @@ class CmdLineTest(BaseCmdLineTest):
         # coverage combine without args
         self.cmd_executes("combine", """\
             .coverage()
-            .load()
             .combine(None)
             .save()
             """)
@@ -206,19 +211,17 @@ class CmdLineTest(BaseCmdLineTest):
         # https://bitbucket.org/ned/coveragepy/issues/385/coverage-combine-doesnt-work-with-rcfile
         self.cmd_executes("combine --rcfile cov.ini", """\
             .coverage(config_file='cov.ini')
-            .load()
             .combine(None)
             .save()
             """)
         self.cmd_executes("combine --rcfile cov.ini data1 data2/more", """\
             .coverage(config_file='cov.ini')
-            .load()
             .combine(["data1", "data2/more"])
             .save()
             """)
 
     def test_debug(self):
-        self.cmd_help("debug", "What information would you like: data, sys?")
+        self.cmd_help("debug", "What information would you like: config, data, sys?")
         self.cmd_help("debug foo", "Don't know what you mean by 'foo'")
 
     def test_debug_sys(self):
@@ -227,6 +230,12 @@ class CmdLineTest(BaseCmdLineTest):
         self.assertIn("version:", out)
         self.assertIn("data_path:", out)
 
+    def test_debug_config(self):
+        self.command_line("debug config")
+        out = self.stdout()
+        self.assertIn("cover_pylib:", out)
+        self.assertIn("skip_covered:", out)
+
     def test_erase(self):
         # coverage erase
         self.cmd_executes("erase", """\
@@ -441,12 +450,38 @@ class CmdLineTest(BaseCmdLineTest):
             .stop()
             .save()
             """)
+        self.cmd_executes("run --concurrency=multiprocessing foo.py", """\
+            .coverage(concurrency='multiprocessing')
+            .erase()
+            .start()
+            .run_python_file('foo.py', ['foo.py'])
+            .stop()
+            .save()
+            """)
 
     def test_bad_concurrency(self):
         self.command_line("run --concurrency=nothing", ret=ERR)
         out = self.stdout()
         self.assertIn("option --concurrency: invalid choice: 'nothing'", out)
 
+    def test_no_multiple_concurrency(self):
+        # You can't use multiple concurrency values on the command line.
+        # I would like to have a better message about not allowing multiple
+        # values for this option, but optparse is not that flexible.
+        self.command_line("run --concurrency=multiprocessing,gevent foo.py", ret=ERR)
+        out = self.stdout()
+        self.assertIn("option --concurrency: invalid choice: 'multiprocessing,gevent'", out)
+
+    def test_multiprocessing_needs_config_file(self):
+        # You can't use command-line args to add options to multiprocessing
+        # runs, since they won't make it to the subprocesses. You need to use a
+        # config file.
+        self.command_line("run --concurrency=multiprocessing --branch foo.py", ret=ERR)
+        self.assertIn(
+            "Options affecting multiprocessing must be specified in a configuration file.",
+            self.stdout()
+        )
+
     def test_run_debug(self):
         self.cmd_executes("run --debug=opt1 foo.py", """\
             .coverage(debug=["opt1"])
@@ -619,6 +654,11 @@ class CmdLineStdoutTest(BaseCmdLineTest):
         self.assertIn("--timid", out)
         self.assertGreater(out.count("\n"), 10)
 
+    def test_unknown_topic(self):
+        # Should probably be an ERR return, but meh.
+        self.command_line("help foobar")
+        self.assertEqual(self.stdout(), "Don't know topic 'foobar'\n")
+
     def test_error(self):
         self.command_line("fooey kablooey", ret=ERR)
         out = self.stdout()
index 04eb985..d438684 100644 (file)
@@ -28,89 +28,166 @@ except ImportError:
 import greenlet
 
 
+def measurable_line(l):
+    """Is this a line of code coverage will measure?
+
+    Not blank, not a comment, and not "else"
+    """
+    l = l.strip()
+    if not l:
+        return False
+    if l.startswith('#'):
+        return False
+    if l.startswith('else:'):
+        return False
+    return True
+
+
 def line_count(s):
-    """How many non-blank non-comment lines are in `s`?"""
-    def code_line(l):
-        """Is this a code line? Not blank, and not a full-line comment."""
-        return l.strip() and not l.strip().startswith('#')
-    return sum(1 for l in s.splitlines() if code_line(l))
+    """How many measurable lines are in `s`?"""
+    return len(list(filter(measurable_line, s.splitlines())))
 
 
-class ConcurrencyTest(CoverageTest):
-    """Tests of the concurrency support in coverage.py."""
+def print_simple_annotation(code, linenos):
+    """Print the lines in `code` with X for each line number in `linenos`."""
+    for lineno, line in enumerate(code.splitlines(), start=1):
+        print(" {0} {1}".format("X" if lineno in linenos else " ", line))
+
+
+class LineCountTest(CoverageTest):
+    """Test the helpers here."""
+
+    run_in_temp_dir = False
+
+    def test_line_count(self):
+        CODE = """
+            # Hey there!
+            x = 1
+            if x:
+                print("hello")
+            else:
+                print("bye")
+
+            print("done")
+            """
 
-    LIMIT = 1000
-
-    # The code common to all the concurrency models.
-    COMMON = """
-        class Producer(threading.Thread):
-            def __init__(self, q):
-                threading.Thread.__init__(self)
-                self.q = q
-
-            def run(self):
-                for i in range({LIMIT}):
-                    self.q.put(i)
-                self.q.put(None)
-
-        class Consumer(threading.Thread):
-            def __init__(self, q):
-                threading.Thread.__init__(self)
-                self.q = q
-
-            def run(self):
-                sum = 0
-                while True:
-                    i = self.q.get()
-                    if i is None:
-                        print(sum)
-                        break
-                    sum += i
+        self.assertEqual(line_count(CODE), 5)
 
+
+# The code common to all the concurrency models.
+SUM_RANGE_Q = """
+    # Above this will be imports defining queue and threading.
+
+    class Producer(threading.Thread):
+        def __init__(self, limit, q):
+            threading.Thread.__init__(self)
+            self.limit = limit
+            self.q = q
+
+        def run(self):
+            for i in range(self.limit):
+                self.q.put(i)
+            self.q.put(None)
+
+    class Consumer(threading.Thread):
+        def __init__(self, q, qresult):
+            threading.Thread.__init__(self)
+            self.q = q
+            self.qresult = qresult
+
+        def run(self):
+            sum = 0
+            while True:
+                i = self.q.get()
+                if i is None:
+                    break
+                sum += i
+            self.qresult.put(sum)
+
+    def sum_range(limit):
         q = queue.Queue()
-        c = Consumer(q)
-        p = Producer(q)
+        qresult = queue.Queue()
+        c = Consumer(q, qresult)
+        p = Producer(limit, q)
         c.start()
         p.start()
 
         p.join()
         c.join()
-        """.format(LIMIT=LIMIT)
-
-    # Import the things to use threads.
-    if env.PY2:
-        THREAD = """\
-        import threading
-        import Queue as queue
-        """ + COMMON
+        return qresult.get()
+
+    # Below this will be something using sum_range.
+    """
+
+PRINT_SUM_RANGE = """
+    print(sum_range({QLIMIT}))
+    """
+
+# Import the things to use threads.
+if env.PY2:
+    THREAD = """
+    import threading
+    import Queue as queue
+    """
+else:
+    THREAD = """
+    import threading
+    import queue
+    """
+
+# Import the things to use eventlet.
+EVENTLET = """
+    import eventlet.green.threading as threading
+    import eventlet.queue as queue
+    """
+
+# Import the things to use gevent.
+GEVENT = """
+    from gevent import monkey
+    monkey.patch_thread()
+    import threading
+    import gevent.queue as queue
+    """
+
+# Uncomplicated code that doesn't use any of the concurrency stuff, to test
+# the simple case under each of the regimes.
+SIMPLE = """
+    total = 0
+    for i in range({QLIMIT}):
+        total += i
+    print(total)
+    """
+
+
+def cant_trace_msg(concurrency, the_module):
+    """What might coverage.py say about a concurrency setting and imported module?"""
+    # In the concurrency choices, "multiprocessing" doesn't count, so remove it.
+    if "multiprocessing" in concurrency:
+        parts = concurrency.split(",")
+        parts.remove("multiprocessing")
+        concurrency = ",".join(parts)
+
+    if the_module is None:
+        # We don't even have the underlying module installed, we expect
+        # coverage to alert us to this fact.
+        expected_out = (
+            "Couldn't trace with concurrency=%s, "
+            "the module isn't installed.\n" % concurrency
+        )
+    elif env.C_TRACER or concurrency == "thread" or concurrency == "":
+        expected_out = None
     else:
-        THREAD = """\
-        import threading
-        import queue
-        """ + COMMON
-
-    # Import the things to use eventlet.
-    EVENTLET = """\
-        import eventlet.green.threading as threading
-        import eventlet.queue as queue
-        """ + COMMON
-
-    # Import the things to use gevent.
-    GEVENT = """\
-        from gevent import monkey
-        monkey.patch_thread()
-        import threading
-        import gevent.queue as queue
-        """ + COMMON
-
-    # Uncomplicated code that doesn't use any of the concurrency stuff, to test
-    # the simple case under each of the regimes.
-    SIMPLE = """\
-        total = 0
-        for i in range({LIMIT}):
-            total += i
-        print(total)
-        """.format(LIMIT=LIMIT)
+        expected_out = (
+            "Can't support concurrency=%s with PyTracer, "
+            "only threads are supported\n" % concurrency
+        )
+    return expected_out
+
+
+class ConcurrencyTest(CoverageTest):
+    """Tests of the concurrency support in coverage.py."""
+
+    QLIMIT = 1000
 
     def try_some_code(self, code, concurrency, the_module, expected_out=None):
         """Run some concurrency testing code and see that it was all covered.
@@ -127,19 +204,16 @@ class ConcurrencyTest(CoverageTest):
         cmd = "coverage run --concurrency=%s try_it.py" % concurrency
         out = self.run_command(cmd)
 
-        if not the_module:
-            # We don't even have the underlying module installed, we expect
-            # coverage to alert us to this fact.
-            expected_out = (
-                "Couldn't trace with concurrency=%s, "
-                "the module isn't installed.\n" % concurrency
-            )
-            self.assertEqual(out, expected_out)
-        elif env.C_TRACER or concurrency == "thread":
+        expected_cant_trace = cant_trace_msg(concurrency, the_module)
+
+        if expected_cant_trace is not None:
+            self.assertEqual(out, expected_cant_trace)
+        else:
             # We can fully measure the code if we are using the C tracer, which
             # can support all the concurrency, or if we are using threads.
             if expected_out is None:
-                expected_out = "%d\n" % (sum(range(self.LIMIT)))
+                expected_out = "%d\n" % (sum(range(self.QLIMIT)))
+            print(code)
             self.assertEqual(out, expected_out)
 
             # Read the coverage file and see that try_it.py has all its lines
@@ -155,30 +229,30 @@ class ConcurrencyTest(CoverageTest):
 
             lines = line_count(code)
             self.assertEqual(data.line_counts()['try_it.py'], lines)
-        else:
-            expected_out = (
-                "Can't support concurrency=%s with PyTracer, "
-                "only threads are supported\n" % concurrency
-            )
-            self.assertEqual(out, expected_out)
 
     def test_threads(self):
-        self.try_some_code(self.THREAD, "thread", threading)
+        code = (THREAD + SUM_RANGE_Q + PRINT_SUM_RANGE).format(QLIMIT=self.QLIMIT)
+        self.try_some_code(code, "thread", threading)
 
     def test_threads_simple_code(self):
-        self.try_some_code(self.SIMPLE, "thread", threading)
+        code = SIMPLE.format(QLIMIT=self.QLIMIT)
+        self.try_some_code(code, "thread", threading)
 
     def test_eventlet(self):
-        self.try_some_code(self.EVENTLET, "eventlet", eventlet)
+        code = (EVENTLET + SUM_RANGE_Q + PRINT_SUM_RANGE).format(QLIMIT=self.QLIMIT)
+        self.try_some_code(code, "eventlet", eventlet)
 
     def test_eventlet_simple_code(self):
-        self.try_some_code(self.SIMPLE, "eventlet", eventlet)
+        code = SIMPLE.format(QLIMIT=self.QLIMIT)
+        self.try_some_code(code, "eventlet", eventlet)
 
     def test_gevent(self):
-        self.try_some_code(self.GEVENT, "gevent", gevent)
+        code = (GEVENT + SUM_RANGE_Q + PRINT_SUM_RANGE).format(QLIMIT=self.QLIMIT)
+        self.try_some_code(code, "gevent", gevent)
 
     def test_gevent_simple_code(self):
-        self.try_some_code(self.SIMPLE, "gevent", gevent)
+        code = SIMPLE.format(QLIMIT=self.QLIMIT)
+        self.try_some_code(code, "gevent", gevent)
 
     def test_greenlet(self):
         GREENLET = """\
@@ -199,7 +273,8 @@ class ConcurrencyTest(CoverageTest):
         self.try_some_code(GREENLET, "greenlet", greenlet, "hello world\n42\n")
 
     def test_greenlet_simple_code(self):
-        self.try_some_code(self.SIMPLE, "greenlet", greenlet)
+        code = SIMPLE.format(QLIMIT=self.QLIMIT)
+        self.try_some_code(code, "greenlet", greenlet)
 
     def test_bug_330(self):
         BUG_330 = """\
@@ -220,40 +295,117 @@ class ConcurrencyTest(CoverageTest):
         self.try_some_code(BUG_330, "eventlet", eventlet, "0\n")
 
 
+SQUARE_OR_CUBE_WORK = """
+    def work(x):
+        # Use different lines in different subprocesses.
+        if x % 2:
+            y = x*x
+        else:
+            y = x*x*x
+        return y
+    """
+
+SUM_RANGE_WORK = """
+    def work(x):
+        return sum_range((x+1)*100)
+    """
+
+MULTI_CODE = """
+    # Above this will be a defintion of work().
+    import multiprocessing
+    import os
+    import time
+    import sys
+
+    def process_worker_main(args):
+        # Need to pause, or the tasks go too quick, and some processes
+        # in the pool don't get any work, and then don't record data.
+        time.sleep(0.02)
+        ret = work(*args)
+        return os.getpid(), ret
+
+    if __name__ == "__main__":      # pragma: no branch
+        # This if is on a single line so we can get 100% coverage
+        # even if we have no arguments.
+        if len(sys.argv) > 1: multiprocessing.set_start_method(sys.argv[1])
+        pool = multiprocessing.Pool({NPROCS})
+        inputs = [(x,) for x in range({UPTO})]
+        outputs = pool.imap_unordered(process_worker_main, inputs)
+        pids = set()
+        total = 0
+        for pid, sq in outputs:
+            pids.add(pid)
+            total += sq
+        print("%d pids, total = %d" % (len(pids), total))
+        pool.close()
+        pool.join()
+    """
+
+
 class MultiprocessingTest(CoverageTest):
     """Test support of the multiprocessing module."""
 
+    def try_multiprocessing_code(
+        self, code, expected_out, the_module, concurrency="multiprocessing"
+    ):
+        """Run code using multiprocessing, it should produce `expected_out`."""
+        self.make_file("multi.py", code)
+        self.make_file(".coveragerc", """\
+            [run]
+            concurrency = %s
+            """ % concurrency)
+
+        if env.PYVERSION >= (3, 4):
+            start_methods = ['fork', 'spawn']
+        else:
+            start_methods = ['']
+
+        for start_method in start_methods:
+            if start_method and start_method not in multiprocessing.get_all_start_methods():
+                continue
+
+            out = self.run_command("coverage run multi.py %s" % (start_method,))
+            expected_cant_trace = cant_trace_msg(concurrency, the_module)
+
+            if expected_cant_trace is not None:
+                self.assertEqual(out, expected_cant_trace)
+            else:
+                self.assertEqual(out.rstrip(), expected_out)
+
+                out = self.run_command("coverage combine")
+                self.assertEqual(out, "")
+                out = self.run_command("coverage report -m")
+
+                last_line = self.squeezed_lines(out)[-1]
+                self.assertRegex(last_line, r"multi.py \d+ 0 100%")
+
     def test_multiprocessing(self):
-        self.make_file("multi.py", """\
-            import multiprocessing
-            import os
-            import time
-            import sys
-
-            def func(x):
-                # Need to pause, or the tasks go too quick, and some processes
-                # in the pool don't get any work, and then don't record data.
-                time.sleep(0.02)
-                # Use different lines in different subprocesses.
-                if x % 2:
-                    y = x*x
-                else:
-                    y = x*x*x
-                return os.getpid(), y
-
-            if __name__ == "__main__":
-                if len(sys.argv) > 1: multiprocessing.set_start_method(sys.argv[1])
-                pool = multiprocessing.Pool(3)
-                inputs = range(30)
-                outputs = pool.imap_unordered(func, inputs)
-                pids = set()
-                total = 0
-                for pid, sq in outputs:
-                    pids.add(pid)
-                    total += sq
-                print("%d pids, total = %d" % (len(pids), total))
-                pool.close()
-                pool.join()
+        nprocs = 3
+        upto = 30
+        code = (SQUARE_OR_CUBE_WORK + MULTI_CODE).format(NPROCS=nprocs, UPTO=upto)
+        total = sum(x*x if x%2 else x*x*x for x in range(upto))
+        expected_out = "{nprocs} pids, total = {total}".format(nprocs=nprocs, total=total)
+        self.try_multiprocessing_code(code, expected_out, threading)
+
+    def test_multiprocessing_and_gevent(self):
+        nprocs = 3
+        upto = 30
+        code = (
+            SUM_RANGE_WORK + EVENTLET + SUM_RANGE_Q + MULTI_CODE
+        ).format(NPROCS=nprocs, UPTO=upto)
+        total = sum(sum(range((x + 1) * 100)) for x in range(upto))
+        expected_out = "{nprocs} pids, total = {total}".format(nprocs=nprocs, total=total)
+        self.try_multiprocessing_code(
+            code, expected_out, eventlet, concurrency="multiprocessing,eventlet"
+        )
+
+    def try_multiprocessing_code_with_branching(self, code, expected_out):
+        """Run code using multiprocessing, it should produce `expected_out`."""
+        self.make_file("multi.py", code)
+        self.make_file("multi.rc", """\
+            [run]
+            concurrency = multiprocessing
+            branch = True
             """)
 
         if env.PYVERSION >= (3, 4):
@@ -265,19 +417,20 @@ class MultiprocessingTest(CoverageTest):
             if start_method and start_method not in multiprocessing.get_all_start_methods():
                 continue
 
-            out = self.run_command(
-                "coverage run --concurrency=multiprocessing multi.py %s" % start_method
-            )
-            total = sum(x*x if x%2 else x*x*x for x in range(30))
-            self.assertEqual(out.rstrip(), "3 pids, total = %d" % total)
+            out = self.run_command("coverage run --rcfile=multi.rc multi.py %s" % (start_method,))
+            self.assertEqual(out.rstrip(), expected_out)
 
-            self.run_command("coverage combine")
+            out = self.run_command("coverage combine")
+            self.assertEqual(out, "")
             out = self.run_command("coverage report -m")
-            last_line = self.squeezed_lines(out)[-1]
-            self.assertEqual(last_line, "multi.py 23 0 100%")
-
 
-def print_simple_annotation(code, linenos):
-    """Print the lines in `code` with X for each line number in `linenos`."""
-    for lineno, line in enumerate(code.splitlines(), start=1):
-        print(" {0} {1}".format("X" if lineno in linenos else " ", line))
+            last_line = self.squeezed_lines(out)[-1]
+            self.assertRegex(last_line, r"multi.py \d+ 0 \d+ 0 100%")
+
+    def test_multiprocessing_with_branching(self):
+        nprocs = 3
+        upto = 30
+        code = (SQUARE_OR_CUBE_WORK + MULTI_CODE).format(NPROCS=nprocs, UPTO=upto)
+        total = sum(x*x if x%2 else x*x*x for x in range(upto))
+        expected_out = "{nprocs} pids, total = {total}".format(nprocs=nprocs, total=total)
+        self.try_multiprocessing_code_with_branching(code, expected_out)
index 2f32c52..cf8a6a7 100644 (file)
@@ -25,10 +25,11 @@ class ConfigTest(CoverageTest):
 
     def test_arguments(self):
         # Arguments to the constructor are applied to the configuration.
-        cov = coverage.Coverage(timid=True, data_file="fooey.dat")
+        cov = coverage.Coverage(timid=True, data_file="fooey.dat", concurrency="multiprocessing")
         self.assertTrue(cov.config.timid)
         self.assertFalse(cov.config.branch)
         self.assertEqual(cov.config.data_file, "fooey.dat")
+        self.assertEqual(cov.config.concurrency, ["multiprocessing"])
 
     def test_config_file(self):
         # A .coveragerc file will be read into the configuration.
@@ -300,7 +301,7 @@ class ConfigFileTest(CoverageTest):
         self.assertTrue(cov.config.branch)
         self.assertTrue(cov.config.cover_pylib)
         self.assertTrue(cov.config.parallel)
-        self.assertEqual(cov.config.concurrency, "thread")
+        self.assertEqual(cov.config.concurrency, ["thread"])
         self.assertEqual(cov.config.source, ["myapp"])
 
         self.assertEqual(cov.get_exclude_list(), ["if 0:", r"pragma:?\s+no cover", "another_tab"])
index a98200a..a52aced 100644 (file)
@@ -326,7 +326,7 @@ class SimpleStatementTest(CoverageTest):
 
     def test_print(self):
         if env.PY3:         # Print statement is gone in Py3k.
-            self.skip("No more print statement in Python 3.")
+            self.skipTest("No more print statement in Python 3.")
 
         self.check_coverage("""\
             print "hello, world!"
@@ -422,7 +422,7 @@ class SimpleStatementTest(CoverageTest):
         # Peephole optimization of jumps to jumps can mean that some statements
         # never hit the line tracer.  The behavior is different in different
         # versions of Python, so don't run this test:
-        self.skip("Expected failure: peephole optimization of jumps to jumps")
+        self.skipTest("Expected failure: peephole optimization of jumps to jumps")
         self.check_coverage("""\
             a = b = c = 0
             for n in range(100):
index 135240f..889d6cf 100644 (file)
@@ -16,15 +16,14 @@ from coverage.misc import NoCode, NoSource
 
 from tests.coveragetest import CoverageTest
 
-HERE = os.path.dirname(__file__)
+TRY_EXECFILE = os.path.join(os.path.dirname(__file__), "modules/process_test/try_execfile.py")
 
 
 class RunFileTest(CoverageTest):
     """Test cases for `run_python_file`."""
 
     def test_run_python_file(self):
-        tryfile = os.path.join(HERE, "try_execfile.py")
-        run_python_file(tryfile, [tryfile, "arg1", "arg2"])
+        run_python_file(TRY_EXECFILE, [TRY_EXECFILE, "arg1", "arg2"])
         mod_globs = json.loads(self.stdout())
 
         # The file should think it is __main__
@@ -44,7 +43,7 @@ class RunFileTest(CoverageTest):
         self.assertEqual(mod_globs['__main__.DATA'], "xyzzy")
 
         # Argv should have the proper values.
-        self.assertEqual(mod_globs['argv'], [tryfile, "arg1", "arg2"])
+        self.assertEqual(mod_globs['argv'], [TRY_EXECFILE, "arg1", "arg2"])
 
         # __builtins__ should have the right values, like open().
         self.assertEqual(mod_globs['__builtins__.has_open'], True)
index 0db0af8..ae9e915 100644 (file)
@@ -15,7 +15,7 @@ import unittest
 
 from nose.plugins.skip import SkipTest
 
-from coverage.test_helpers import ModuleAwareMixin, SysPathAwareMixin, change_dir, saved_sys_path
+from unittest_mixins import ModuleAwareMixin, SysPathAwareMixin, change_dir, saved_sys_path
 from tests.helpers import run_command
 from tests.backtest import execfile         # pylint: disable=redefined-builtin
 
index e735323..eecdfa7 100644 (file)
@@ -271,7 +271,7 @@ class WindowsFileTest(CoverageTest):
 
     def setUp(self):
         if not env.WINDOWS:
-            self.skip("Only need to run Windows tests on Windows.")
+            self.skipTest("Only need to run Windows tests on Windows.")
         super(WindowsFileTest, self).setUp()
 
     def test_actual_path(self):
index e67e4f5..dda2e4b 100644 (file)
@@ -274,6 +274,20 @@ class HtmlWithUnparsableFilesTest(HtmlTestHelpers, CoverageTest):
         self.start_import_stop(cov, "innocuous")
         self.make_file("innocuous.py", "<h1>This isn't python!</h1>")
         cov.html_report(ignore_errors=True)
+        self.assertEqual(
+            len(cov._warnings),
+            1,
+            "Expected a warning to be thrown when an invalid python file is parsed")
+        self.assertIn(
+            "Could not parse Python file",
+            cov._warnings[0],
+            "Warning message should be in 'invalid file' warning"
+        )
+        self.assertIn(
+            "innocuous.py",
+            cov._warnings[0],
+            "Filename should be in 'invalid file' warning"
+        )
         self.assert_exists("htmlcov/index.html")
         # This would be better as a glob, if the HTML layout changes:
         self.assert_doesnt_exist("htmlcov/innocuous.html")
@@ -583,7 +597,7 @@ class HtmlGoldTests(CoverageGoldTest):
         self.output_dir("out/omit_1")
 
         with change_dir("src"):
-            # pylint: disable=import-error, unused-variable
+            # pylint: disable=import-error
             cov = coverage.Coverage(include=["./*"])
             cov.start()
             import main         # pragma: nested
@@ -596,7 +610,7 @@ class HtmlGoldTests(CoverageGoldTest):
         self.output_dir("out/omit_2")
 
         with change_dir("src"):
-            # pylint: disable=import-error, unused-variable
+            # pylint: disable=import-error
             cov = coverage.Coverage(include=["./*"])
             cov.start()
             import main         # pragma: nested
@@ -609,7 +623,7 @@ class HtmlGoldTests(CoverageGoldTest):
         self.output_dir("out/omit_3")
 
         with change_dir("src"):
-            # pylint: disable=import-error, unused-variable
+            # pylint: disable=import-error
             cov = coverage.Coverage(include=["./*"])
             cov.start()
             import main         # pragma: nested
@@ -622,7 +636,7 @@ class HtmlGoldTests(CoverageGoldTest):
         self.output_dir("out/omit_4")
 
         with change_dir("src"):
-            # pylint: disable=import-error, unused-variable
+            # pylint: disable=import-error
             cov = coverage.Coverage(config_file="omit4.ini", include=["./*"])
             cov.start()
             import main         # pragma: nested
@@ -635,7 +649,7 @@ class HtmlGoldTests(CoverageGoldTest):
         self.output_dir("out/omit_5")
 
         with change_dir("src"):
-            # pylint: disable=import-error, unused-variable
+            # pylint: disable=import-error
             cov = coverage.Coverage(config_file="omit5.ini", include=["./*"])
             cov.start()
             import main         # pragma: nested
@@ -648,7 +662,7 @@ class HtmlGoldTests(CoverageGoldTest):
         self.output_dir("out/other")
 
         with change_dir("src"):
-            # pylint: disable=import-error, unused-variable
+            # pylint: disable=import-error
             sys.path.insert(0, "../othersrc")
             cov = coverage.Coverage(include=["./*", "../othersrc/*"])
             cov.start()
index ed18cca..5fee823 100644 (file)
@@ -243,7 +243,7 @@ class ParserMissingArcDescriptionTest(CoverageTest):
     def test_missing_arc_descriptions_for_small_callables(self):
         # We use 2.7 features here, so just skip this test on 2.6
         if env.PYVERSION < (2, 7):
-            self.skip("No dict or set comps in 2.6")
+            self.skipTest("No dict or set comps in 2.6")
 
         parser = self.parse_text(u"""\
             callables = [
index 0bbf8ea..ccbe01b 100644 (file)
@@ -123,7 +123,7 @@ class SourceEncodingTest(CoverageTest):
         if env.PYPY and env.PY3:
             # PyPy3 gets this case wrong. Not sure what I can do about it,
             # so skip the test.
-            self.skip("PyPy3 is wrong about non-comment encoding. Skip it.")
+            self.skipTest("PyPy3 is wrong about non-comment encoding. Skip it.")
         # Should not detect anything here
         source = b'def parse(src, encoding=None):\n    pass'
         self.assertEqual(source_encoding(source), DEF_ENCODING)
index fe43c4c..8ea0a8f 100644 (file)
@@ -248,7 +248,7 @@ class PluginWarningOnPyTracer(CoverageTest):
     """Test that we get a controlled exception with plugins on PyTracer."""
     def test_exception_if_plugins_on_pytracer(self):
         if env.C_TRACER:
-            self.skip("This test is only about PyTracer.")
+            self.skipTest("This test is only about PyTracer.")
 
         self.make_file("simple.py", """a = 1""")
 
@@ -268,7 +268,7 @@ class FileTracerTest(CoverageTest):
     def setUp(self):
         super(FileTracerTest, self).setUp()
         if not env.C_TRACER:
-            self.skip("Plugins are only supported with the C tracer.")
+            self.skipTest("Plugins are only supported with the C tracer.")
 
 
 class GoodPluginTest(FileTracerTest):
index 55dc3b1..5848b9f 100644 (file)
@@ -17,7 +17,7 @@ from coverage.misc import output_encoding
 
 from tests.coveragetest import CoverageTest
 
-HERE = os.path.dirname(__file__)
+TRY_EXECFILE = os.path.join(os.path.dirname(__file__), "modules/process_test/try_execfile.py")
 
 
 class ProcessTest(CoverageTest):
@@ -42,7 +42,7 @@ class ProcessTest(CoverageTest):
         self.assert_exists(".coverage")
 
     def test_environment(self):
-        # Checks that we can import modules from the test directory at all!
+        # Checks that we can import modules from the tests directory at all!
         self.make_file("mycode.py", """\
             import covmod1
             import covmodzip1
@@ -150,7 +150,7 @@ class ProcessTest(CoverageTest):
         self.assertEqual(self.number_of_data_files(), 2)
 
         # Combine the parallel coverage data files into .coverage .
-        self.run_command("coverage combine")
+        self.run_command("coverage combine --append")
         self.assert_exists(".coverage")
 
         # After combining, there should be only the .coverage file.
@@ -425,23 +425,27 @@ class ProcessTest(CoverageTest):
         self.assertEqual(status, status2)
         self.assertEqual(status, 0)
 
+    def assert_execfile_output(self, out):
+        """Assert that the output we got is a successful run of try_execfile.py"""
+        self.assertIn('"DATA": "xyzzy"', out)
+
     def test_coverage_run_is_like_python(self):
-        tryfile = os.path.join(HERE, "try_execfile.py")
-        with open(tryfile) as f:
+        with open(TRY_EXECFILE) as f:
             self.make_file("run_me.py", f.read())
         out_cov = self.run_command("coverage run run_me.py")
         out_py = self.run_command("python run_me.py")
         self.assertMultiLineEqual(out_cov, out_py)
+        self.assert_execfile_output(out_cov)
 
     def test_coverage_run_dashm_is_like_python_dashm(self):
         # These -m commands assume the coverage tree is on the path.
-        out_cov = self.run_command("coverage run -m tests.try_execfile")
-        out_py = self.run_command("python -m tests.try_execfile")
+        out_cov = self.run_command("coverage run -m process_test.try_execfile")
+        out_py = self.run_command("python -m process_test.try_execfile")
         self.assertMultiLineEqual(out_cov, out_py)
+        self.assert_execfile_output(out_cov)
 
     def test_coverage_run_dir_is_like_python_dir(self):
-        tryfile = os.path.join(HERE, "try_execfile.py")
-        with open(tryfile) as f:
+        with open(TRY_EXECFILE) as f:
             self.make_file("with_main/__main__.py", f.read())
         out_cov = self.run_command("coverage run with_main")
         out_py = self.run_command("python with_main")
@@ -458,6 +462,7 @@ class ProcessTest(CoverageTest):
         out_cov = remove_matching_lines(out_cov, ignored)
         out_py = remove_matching_lines(out_py, ignored)
         self.assertMultiLineEqual(out_cov, out_py)
+        self.assert_execfile_output(out_cov)
 
     def test_coverage_run_dashm_equal_to_doubledashsource(self):
         """regression test for #328
@@ -467,19 +472,21 @@ class ProcessTest(CoverageTest):
         """
         # These -m commands assume the coverage tree is on the path.
         out_cov = self.run_command(
-            "coverage run --source tests.try_execfile -m tests.try_execfile"
+            "coverage run --source process_test.try_execfile -m process_test.try_execfile"
         )
-        out_py = self.run_command("python -m tests.try_execfile")
+        out_py = self.run_command("python -m process_test.try_execfile")
         self.assertMultiLineEqual(out_cov, out_py)
+        self.assert_execfile_output(out_cov)
 
     def test_coverage_run_dashm_superset_of_doubledashsource(self):
         """Edge case: --source foo -m foo.bar"""
         # These -m commands assume the coverage tree is on the path.
         out_cov = self.run_command(
-            "coverage run --source tests -m tests.try_execfile"
+            "coverage run --source process_test -m process_test.try_execfile"
         )
-        out_py = self.run_command("python -m tests.try_execfile")
+        out_py = self.run_command("python -m process_test.try_execfile")
         self.assertMultiLineEqual(out_cov, out_py)
+        self.assert_execfile_output(out_cov)
 
         st, out = self.run_command_status("coverage report")
         self.assertEqual(st, 0)
@@ -493,15 +500,16 @@ class ProcessTest(CoverageTest):
         # keeps the test working.
         self.make_file("myscript", """\
             import sys; sys.dont_write_bytecode = True
-            import tests.try_execfile
+            import process_test.try_execfile
             """)
 
         # These -m commands assume the coverage tree is on the path.
         out_cov = self.run_command(
-            "coverage run --source tests myscript"
+            "coverage run --source process_test myscript"
         )
         out_py = self.run_command("python myscript")
         self.assertMultiLineEqual(out_cov, out_py)
+        self.assert_execfile_output(out_cov)
 
         st, out = self.run_command_status("coverage report")
         self.assertEqual(st, 0)
@@ -509,19 +517,19 @@ class ProcessTest(CoverageTest):
 
     def test_coverage_run_dashm_is_like_python_dashm_off_path(self):
         # https://bitbucket.org/ned/coveragepy/issue/242
-        tryfile = os.path.join(HERE, "try_execfile.py")
         self.make_file("sub/__init__.py", "")
-        with open(tryfile) as f:
+        with open(TRY_EXECFILE) as f:
             self.make_file("sub/run_me.py", f.read())
         out_cov = self.run_command("coverage run -m sub.run_me")
         out_py = self.run_command("python -m sub.run_me")
         self.assertMultiLineEqual(out_cov, out_py)
+        self.assert_execfile_output(out_cov)
 
     def test_coverage_run_dashm_is_like_python_dashm_with__main__207(self):
         if sys.version_info < (2, 7):
             # Coverage.py isn't bug-for-bug compatible in the behavior of -m for
             # Pythons < 2.7
-            self.skip("-m doesn't work the same < Python 2.7")
+            self.skipTest("-m doesn't work the same < Python 2.7")
         # https://bitbucket.org/ned/coveragepy/issue/207
         self.make_file("package/__init__.py", "print('init')")
         self.make_file("package/__main__.py", "print('main')")
@@ -531,7 +539,7 @@ class ProcessTest(CoverageTest):
 
     def test_fork(self):
         if not hasattr(os, 'fork'):
-            self.skip("Can't test os.fork since it doesn't exist.")
+            self.skipTest("Can't test os.fork since it doesn't exist.")
 
         self.make_file("fork.py", """\
             import os
@@ -670,10 +678,10 @@ class ProcessTest(CoverageTest):
 
     def test_fullcoverage(self):                        # pragma: not covered
         if env.PY2:             # This doesn't work on Python 2.
-            self.skip("fullcoverage doesn't work on Python 2.")
+            self.skipTest("fullcoverage doesn't work on Python 2.")
         # It only works with the C tracer, and if we aren't measuring ourselves.
         if not env.C_TRACER or env.METACOV:
-            self.skip("fullcoverage only works with the C tracer.")
+            self.skipTest("fullcoverage only works with the C tracer.")
 
         # fullcoverage is a trick to get stdlib modules measured from
         # the very beginning of the process. Here we import os and
@@ -733,6 +741,15 @@ class ProcessTest(CoverageTest):
             "Coverage.py warning: Module foo was previously imported, but not measured.\n"
         )
 
+    def test_module_name(self):
+        if sys.version_info < (2, 7):
+            # Python 2.6 thinks that coverage is a package that can't be
+            # executed
+            self.skipTest("-m doesn't work the same < Python 2.7")
+        # https://bitbucket.org/ned/coveragepy/issues/478/help-shows-silly-program-name-when-running
+        out = self.run_command("python -m coverage")
+        self.assertIn("Use 'coverage help' for help", out)
+
 
 class AliasedCommandTest(CoverageTest):
     """Tests of the version-specific command aliases."""
@@ -896,6 +913,44 @@ class FailUnderEmptyFilesTest(CoverageTest):
         self.assertEqual(st, 2)
 
 
+class FailUnder100Test(CoverageTest):
+    """Tests of the --fail-under switch."""
+
+    def test_99_8(self):
+        self.make_file("ninety_nine_eight.py",
+            "".join("v{i} = {i}\n".format(i=i) for i in range(498)) +
+            "if v0 > 498:\n    v499 = 499\n"
+            )
+        st, _ = self.run_command_status("coverage run ninety_nine_eight.py")
+        self.assertEqual(st, 0)
+        st, out = self.run_command_status("coverage report")
+        self.assertEqual(st, 0)
+        self.assertEqual(
+            self.last_line_squeezed(out),
+            "ninety_nine_eight.py 500 1 99%"
+        )
+
+        st, _ = self.run_command_status("coverage report --fail-under=100")
+        self.assertEqual(st, 2)
+
+
+    def test_100(self):
+        self.make_file("one_hundred.py",
+            "".join("v{i} = {i}\n".format(i=i) for i in range(500))
+            )
+        st, _ = self.run_command_status("coverage run one_hundred.py")
+        self.assertEqual(st, 0)
+        st, out = self.run_command_status("coverage report")
+        self.assertEqual(st, 0)
+        self.assertEqual(
+            self.last_line_squeezed(out),
+            "one_hundred.py 500 0 100%"
+        )
+
+        st, _ = self.run_command_status("coverage report --fail-under=100")
+        self.assertEqual(st, 0)
+
+
 class UnicodeFilePathsTest(CoverageTest):
     """Tests of using non-ascii characters in the names of files."""
 
@@ -928,6 +983,7 @@ class UnicodeFilePathsTest(CoverageTest):
         )
 
         if env.PY2:
+            # pylint: disable=redefined-variable-type
             report_expected = report_expected.encode(output_encoding())
 
         out = self.run_command("coverage report")
@@ -966,6 +1022,7 @@ class UnicodeFilePathsTest(CoverageTest):
         )
 
         if env.PY2:
+            # pylint: disable=redefined-variable-type
             report_expected = report_expected.encode(output_encoding())
 
         out = self.run_command("coverage report")
@@ -1012,9 +1069,8 @@ class ProcessCoverageMixin(object):
 class ProcessStartupTest(ProcessCoverageMixin, CoverageTest):
     """Test that we can measure coverage in sub-processes."""
 
-    def test_subprocess_with_pth_files(self):           # pragma: not covered
-        if env.METACOV:
-            self.skip("Can't test sub-process pth file suppport during metacoverage")
+    def setUp(self):
+        super(ProcessStartupTest, self).setUp()
 
         # Main will run sub.py
         self.make_file("main.py", """\
@@ -1027,12 +1083,17 @@ class ProcessStartupTest(ProcessCoverageMixin, CoverageTest):
             with open("out.txt", "w") as f:
                 f.write("Hello, world!\\n")
             """)
+
+    def test_subprocess_with_pth_files(self):           # pragma: not covered
+        if env.METACOV:
+            self.skipTest("Can't test sub-process pth file suppport during metacoverage")
+
         self.make_file("coverage.ini", """\
             [run]
             data_file = .mycovdata
             """)
         self.set_environ("COVERAGE_PROCESS_START", "coverage.ini")
-        import main             # pylint: disable=import-error,unused-variable
+        import main             # pylint: disable=import-error
 
         with open("out.txt") as f:
             self.assertEqual(f.read(), "Hello, world!\n")
@@ -1043,6 +1104,36 @@ class ProcessStartupTest(ProcessCoverageMixin, CoverageTest):
         data.read_file(".mycovdata")
         self.assertEqual(data.line_counts()['sub.py'], 2)
 
+    def test_subprocess_with_pth_files_and_parallel(self):  # pragma: not covered
+        # https://bitbucket.org/ned/coveragepy/issues/492/subprocess-coverage-strange-detection-of
+        if env.METACOV:
+            self.skipTest("Can't test sub-process pth file suppport during metacoverage")
+
+        self.make_file("coverage.ini", """\
+            [run]
+            parallel = true
+            """)
+
+        self.set_environ("COVERAGE_PROCESS_START", "coverage.ini")
+        self.run_command("coverage run main.py")
+
+        with open("out.txt") as f:
+            self.assertEqual(f.read(), "Hello, world!\n")
+
+        self.run_command("coverage combine")
+
+        # assert that the combined .coverage data file is correct
+        self.assert_exists(".coverage")
+        data = coverage.CoverageData()
+        data.read_file(".coverage")
+        self.assertEqual(data.line_counts()['sub.py'], 2)
+
+        # assert that there are *no* extra data files left over after a combine
+        data_files = glob.glob(os.getcwd() + '/.coverage*')
+        self.assertEquals(len(data_files), 1,
+            "Expected only .coverage after combine, looks like there are " + \
+            "extra data files that were not cleaned up: %r" % data_files)
+
 
 class ProcessStartupWithSourceTest(ProcessCoverageMixin, CoverageTest):
     """Show that we can configure {[run]source} during process-level coverage.
@@ -1076,7 +1167,7 @@ class ProcessStartupWithSourceTest(ProcessCoverageMixin, CoverageTest):
 
         """
         if env.METACOV:
-            self.skip("Can't test sub-process pth file suppport during metacoverage")
+            self.skipTest("Can't test sub-process pth file suppport during metacoverage")
 
         def fullname(modname):
             """What is the full module name for `modname` for this test?"""
index 681e204..c8abd7d 100644 (file)
@@ -14,7 +14,11 @@ import sys
 import coverage
 from coverage import env
 from coverage.backward import StringIO
-from coverage.misc import output_encoding
+from coverage.config import CoverageConfig
+from coverage.control import Coverage
+from coverage.data import CoverageData
+from coverage.misc import CoverageException, output_encoding
+from coverage.summary import SummaryReporter
 
 from tests.coveragetest import CoverageTest
 
@@ -416,6 +420,7 @@ class SummaryTest(CoverageTest):
             u"Couldn't parse 'accented\xe2.py' as Python source: 'error' at line 1"
         )
         if env.PY2:
+            # pylint: disable=redefined-variable-type
             expected = expected.encode(output_encoding())
         self.assertEqual(last, expected)
 
@@ -473,7 +478,7 @@ class SummaryTest(CoverageTest):
             """)
         cov = coverage.Coverage(branch=True, source=["."])
         cov.start()
-        import main     # pragma: nested # pylint: disable=import-error,unused-variable
+        import main     # pragma: nested # pylint: disable=import-error
         cov.stop()      # pragma: nested
         report = self.get_report(cov).splitlines()
         self.assertIn("mybranch.py 5 5 2 0 0%", report)
@@ -482,7 +487,7 @@ class SummaryTest(CoverageTest):
         """A helper for the next few tests."""
         cov = coverage.Coverage()
         cov.start()
-        import TheCode  # pragma: nested # pylint: disable=import-error,unused-variable
+        import TheCode  # pragma: nested # pylint: disable=import-error
         cov.stop()      # pragma: nested
         return self.get_report(cov)
 
@@ -505,7 +510,7 @@ class SummaryTest(CoverageTest):
 
     def test_pyw_files(self):
         if not env.WINDOWS:
-            self.skip(".pyw files are only on Windows.")
+            self.skipTest(".pyw files are only on Windows.")
 
         # https://bitbucket.org/ned/coveragepy/issue/261
         self.make_file("start.pyw", """\
@@ -517,7 +522,7 @@ class SummaryTest(CoverageTest):
             """)
         cov = coverage.Coverage()
         cov.start()
-        import start    # pragma: nested # pylint: disable=import-error,unused-variable
+        import start    # pragma: nested # pylint: disable=import-error
         cov.stop()      # pragma: nested
 
         report = self.get_report(cov)
@@ -537,7 +542,7 @@ class SummaryTest(CoverageTest):
         # Run the program.
         cov = coverage.Coverage()
         cov.start()
-        import main     # pragma: nested # pylint: disable=import-error,unused-variable
+        import main     # pragma: nested # pylint: disable=import-error
         cov.stop()      # pragma: nested
 
         report = self.get_report(cov).splitlines()
@@ -546,7 +551,7 @@ class SummaryTest(CoverageTest):
     def test_missing_py_file_during_run(self):
         # PyPy2 doesn't run bare .pyc files.
         if env.PYPY and env.PY2:
-            self.skip("PyPy2 doesn't run bare .pyc files")
+            self.skipTest("PyPy2 doesn't run bare .pyc files")
 
         # Create two Python files.
         self.make_file("mod.py", "a = 1\n")
@@ -567,7 +572,7 @@ class SummaryTest(CoverageTest):
         # Run the program.
         cov = coverage.Coverage()
         cov.start()
-        import main     # pragma: nested # pylint: disable=import-error,unused-variable
+        import main     # pragma: nested # pylint: disable=import-error
         cov.stop()      # pragma: nested
 
         # Put back the missing Python file.
@@ -595,7 +600,7 @@ class SummaryTest2(CoverageTest):
         # statements, not one statement.
         cov = coverage.Coverage(branch=True)
         cov.start()
-        import usepkgs  # pragma: nested # pylint: disable=import-error,unused-variable
+        import usepkgs  # pragma: nested # pylint: disable=import-error
         cov.stop()      # pragma: nested
 
         repout = StringIO()
@@ -640,3 +645,112 @@ class ReportingReturnValueTest(CoverageTest):
         cov = self.run_coverage()
         val = cov.xml_report(include="*/doit.py")
         self.assertAlmostEqual(val, 85.7, 1)
+
+
+class TestSummaryReporterConfiguration(CoverageTest):
+    """Tests of SummaryReporter."""
+
+    run_in_temp_dir = False
+
+    # We just need some readable files to work with. These will do.
+    HERE = os.path.dirname(__file__)
+
+    LINES_1 = {
+        os.path.join(HERE, "test_api.py"): dict.fromkeys(range(300)),
+        os.path.join(HERE, "test_backward.py"): dict.fromkeys(range(20)),
+        os.path.join(HERE, "test_coverage.py"): dict.fromkeys(range(15)),
+    }
+
+    def get_coverage_data(self, lines):
+        """Get a CoverageData object that includes the requested lines."""
+        data = CoverageData()
+        data.add_lines(lines)
+        return data
+
+    def get_summary_text(self, coverage_data, options):
+        """Get text output from the SummaryReporter."""
+        cov = Coverage()
+        cov.start()
+        cov.stop()              # pragma: nested
+        cov.data = coverage_data
+        printer = SummaryReporter(cov, options)
+        destination = StringIO()
+        printer.report([], destination)
+        return destination.getvalue()
+
+    def test_test_data(self):
+        # We use our own test files as test data. Check that our assumptions
+        # about them are still valid.  We want the three columns of numbers to
+        # sort in three different orders.
+        data = self.get_coverage_data(self.LINES_1)
+        report = self.get_summary_text(data, CoverageConfig())
+        print(report)
+        # Name                     Stmts   Miss  Cover
+        # --------------------------------------------
+        # tests/test_api.py          339    155    54%
+        # tests/test_backward.py      13      3    77%
+        # tests/test_coverage.py     234    228     3%
+        # --------------------------------------------
+        # TOTAL                      586    386    34%
+
+        lines = report.splitlines()[2:-2]
+        self.assertEqual(len(lines), 3)
+        nums = [list(map(int, l.replace('%', '').split()[1:])) for l in lines]
+        # [
+        #  [339, 155, 54],
+        #  [ 13,   3, 77],
+        #  [234, 228,  3]
+        # ]
+        self.assertTrue(nums[1][0] < nums[2][0] < nums[0][0])
+        self.assertTrue(nums[1][1] < nums[0][1] < nums[2][1])
+        self.assertTrue(nums[2][2] < nums[0][2] < nums[1][2])
+
+    def test_defaults(self):
+        """Run the report with no configuration options."""
+        data = self.get_coverage_data(self.LINES_1)
+        opts = CoverageConfig()
+        report = self.get_summary_text(data, opts)
+        self.assertNotIn('Missing', report)
+        self.assertNotIn('Branch', report)
+
+    def test_print_missing(self):
+        """Run the report printing the missing lines."""
+        data = self.get_coverage_data(self.LINES_1)
+        opts = CoverageConfig()
+        opts.from_args(show_missing=True)
+        report = self.get_summary_text(data, opts)
+        self.assertIn('Missing', report)
+        self.assertNotIn('Branch', report)
+
+    def assert_ordering(self, text, *words):
+        """Assert that the `words` appear in order in `text`."""
+        indexes = list(map(text.find, words))
+        self.assertEqual(
+            indexes, sorted(indexes),
+            "The words %r don't appear in order in %r" % (words, text)
+        )
+
+    def test_sort_report_by_stmts(self):
+        # Sort the text report by the Stmts column.
+        data = self.get_coverage_data(self.LINES_1)
+        opts = CoverageConfig()
+        opts.from_args(sort='Stmts')
+        report = self.get_summary_text(data, opts)
+        self.assert_ordering(report, "test_backward.py", "test_coverage.py", "test_api.py")
+
+    def test_sort_report_by_cover(self):
+        # Sort the text report by the Cover column.
+        data = self.get_coverage_data(self.LINES_1)
+        opts = CoverageConfig()
+        opts.from_args(sort='Cover')
+        report = self.get_summary_text(data, opts)
+        self.assert_ordering(report, "test_coverage.py", "test_api.py", "test_backward.py")
+
+    def test_sort_report_by_invalid_option(self):
+        # Sort the text report by a nonsense column.
+        data = self.get_coverage_data(self.LINES_1)
+        opts = CoverageConfig()
+        opts.from_args(sort='Xyzzy')
+        msg = "Invalid sorting option: 'Xyzzy'"
+        with self.assertRaisesRegex(CoverageException, msg):
+            self.get_summary_text(data, opts)
index 1dafdd0..c5858bf 100644 (file)
@@ -6,15 +6,11 @@
 
 import datetime
 import os
-import re
 import sys
-import textwrap
 
 import coverage
 from coverage.backunittest import TestCase
-from coverage.backward import to_bytes
 from coverage.files import actual_path
-from coverage.test_helpers import EnvironmentAwareMixin, TempDirMixin, DelayedAssertionMixin
 
 from tests.coveragetest import CoverageTest
 
@@ -31,113 +27,6 @@ class TestingTest(TestCase):
             self.assertCountEqual(set([1,2,3]), set([4,5,6]))
 
 
-class TempDirMixinTest(TempDirMixin, TestCase):
-    """Test the methods in TempDirMixin."""
-
-    def file_text(self, fname):
-        """Return the text read from a file."""
-        with open(fname, "rb") as f:
-            return f.read().decode('ascii')
-
-    def test_make_file(self):
-        # A simple file.
-        self.make_file("fooey.boo", "Hello there")
-        self.assertEqual(self.file_text("fooey.boo"), "Hello there")
-        # A file in a sub-directory
-        self.make_file("sub/another.txt", "Another")
-        self.assertEqual(self.file_text("sub/another.txt"), "Another")
-        # A second file in that sub-directory
-        self.make_file("sub/second.txt", "Second")
-        self.assertEqual(self.file_text("sub/second.txt"), "Second")
-        # A deeper directory
-        self.make_file("sub/deeper/evenmore/third.txt")
-        self.assertEqual(self.file_text("sub/deeper/evenmore/third.txt"), "")
-
-    def test_make_file_newline(self):
-        self.make_file("unix.txt", "Hello\n")
-        self.assertEqual(self.file_text("unix.txt"), "Hello\n")
-        self.make_file("dos.txt", "Hello\n", newline="\r\n")
-        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_make_file_non_ascii(self):
-        self.make_file("unicode.txt", "tabblo: «ταБЬℓσ»")
-        with open("unicode.txt", "rb") as f:
-            text = f.read()
-        self.assertEqual(text, to_bytes("tabblo: «ταБЬℓσ»"))
-
-
-class EnvironmentAwareMixinTest(EnvironmentAwareMixin, TestCase):
-    """Tests of test_helpers.EnvironmentAwareMixin."""
-
-    def test_setting_and_cleaning_env_vars(self):
-        # The before state.
-        # Not sure what environment variables are available in all of our
-        # different testing environments, so try a bunch.
-        for envvar in ["HOME", "HOMEDIR", "USER", "SYSTEMDRIVE", "TEMP"]:   # pragma: part covered
-            if envvar in os.environ:
-                original_text = os.environ[envvar]
-                new_text = "Some Strange Text"
-                break
-        # pylint: disable=undefined-loop-variable
-        self.assertNotEqual(original_text, new_text)
-        self.assertNotIn("XYZZY_PLUGH", os.environ)
-
-        # Change the environment.
-        self.set_environ(envvar, new_text)
-        self.set_environ("XYZZY_PLUGH", "Vogon")
-
-        self.assertEqual(os.environ[envvar], new_text)
-        self.assertEqual(os.environ["XYZZY_PLUGH"], "Vogon")
-
-        # Do the clean ups early.
-        self.doCleanups()
-
-        # The environment should be restored.
-        self.assertEqual(os.environ[envvar], original_text)
-        self.assertNotIn("XYZZY_PLUGH", os.environ)
-
-
-class DelayedAssertionMixinTest(DelayedAssertionMixin, TestCase):
-    """Test the `delayed_assertions` method."""
-
-    def test_delayed_assertions(self):
-        # Two assertions can be shown at once:
-        msg = re.escape(textwrap.dedent("""\
-            2 failed assertions:
-            'x' != 'y'
-            - x
-            + y
-
-            'w' != 'z'
-            - w
-            + z
-            """))
-        with self.assertRaisesRegex(AssertionError, msg):
-            with self.delayed_assertions():
-                self.assertEqual("x", "y")
-                self.assertEqual("w", "z")
-
-        # It's also OK if only one fails:
-        msg = re.escape(textwrap.dedent("""\
-            'w' != 'z'
-            - w
-            + z
-            """))
-        with self.assertRaisesRegex(AssertionError, msg):
-            with self.delayed_assertions():
-                self.assertEqual("x", "x")
-                self.assertEqual("w", "z")
-
-        # If an error happens, it gets reported immediately, no special
-        # handling:
-        with self.assertRaises(ZeroDivisionError):
-            with self.delayed_assertions():
-                self.assertEqual("x", "y")
-                self.assertEqual("w", 1/0)
-
-
 class CoverageTestTest(CoverageTest):
     """Test the methods in `CoverageTest`."""
 
index 3d521b3..dd14b92 100644 (file)
@@ -165,9 +165,9 @@ class XmlReportTest(XmlTestHelpers, CoverageTest):
         self.make_file("also/over/there/bar.py", "b = 2")
         cov = coverage.Coverage(source=["src/main", "also/over/there", "not/really"])
         cov.start()
-        mod_foo = self.import_local_file("foo", "src/main/foo.py")
-        mod_bar = self.import_local_file("bar", "also/over/there/bar.py")
-        cov.stop()
+        mod_foo = self.import_local_file("foo", "src/main/foo.py")              # pragma: nested
+        mod_bar = self.import_local_file("bar", "also/over/there/bar.py")       # pragma: nested
+        cov.stop()                                                              # pragma: nested
         cov.xml_report([mod_foo, mod_bar], outfile="-")
         xml = self.stdout()
 
diff --git a/tox.ini b/tox.ini
index 9d6c499..970a738 100644 (file)
--- a/tox.ini
+++ b/tox.ini
@@ -14,10 +14,12 @@ deps =
     nose==1.3.7
     mock==2.0.0
     PyContracts==1.7.9
+    unittest-mixins==1.1.1
+    #-egit+/Users/ned/unittest_mixins#egg=unittest-mixins==0.0
     py26: unittest2==1.1.0
     py{27,33,34,35,36}: gevent==1.1.1
     py{26,27,33,34,35,36}: eventlet==0.19.0
-    py{26,27,33,34,35,36}: greenlet==0.4.9
+    py{26,27,33,34,35,36}: greenlet==0.4.10
     # setuptools no longer supports Python 3.2
     pypy3_24: setuptools==21.1.0