Merge remote-tracking branch 'upstream/master' into features-assertion-pass-hook-master
# Conflicts: # src/_pytest/assertion/rewrite.py
This commit is contained in:
commit
6f851e6cbb
1
AUTHORS
1
AUTHORS
|
@ -135,6 +135,7 @@ Kale Kundert
|
||||||
Katarzyna Jachim
|
Katarzyna Jachim
|
||||||
Katerina Koukiou
|
Katerina Koukiou
|
||||||
Kevin Cox
|
Kevin Cox
|
||||||
|
Kevin J. Foley
|
||||||
Kodi B. Arfer
|
Kodi B. Arfer
|
||||||
Kostis Anagnostopoulos
|
Kostis Anagnostopoulos
|
||||||
Kristoffer Nordström
|
Kristoffer Nordström
|
||||||
|
|
|
@ -173,7 +173,7 @@ Short version
|
||||||
|
|
||||||
The test environments above are usually enough to cover most cases locally.
|
The test environments above are usually enough to cover most cases locally.
|
||||||
|
|
||||||
#. Write a ``changelog`` entry: ``changelog/2574.bugfix``, use issue id number
|
#. Write a ``changelog`` entry: ``changelog/2574.bugfix.rst``, use issue id number
|
||||||
and one of ``bugfix``, ``removal``, ``feature``, ``vendor``, ``doc`` or
|
and one of ``bugfix``, ``removal``, ``feature``, ``vendor``, ``doc`` or
|
||||||
``trivial`` for the issue type.
|
``trivial`` for the issue type.
|
||||||
#. Unless your change is a trivial or a documentation fix (e.g., a typo or reword of a small section) please
|
#. Unless your change is a trivial or a documentation fix (e.g., a typo or reword of a small section) please
|
||||||
|
@ -264,7 +264,7 @@ Here is a simple overview, with pytest-specific bits:
|
||||||
$ git commit -a -m "<commit message>"
|
$ git commit -a -m "<commit message>"
|
||||||
$ git push -u
|
$ git push -u
|
||||||
|
|
||||||
#. Create a new changelog entry in ``changelog``. The file should be named ``<issueid>.<type>``,
|
#. Create a new changelog entry in ``changelog``. The file should be named ``<issueid>.<type>.rst``,
|
||||||
where *issueid* is the number of the issue related to the change and *type* is one of
|
where *issueid* is the number of the issue related to the change and *type* is one of
|
||||||
``bugfix``, ``removal``, ``feature``, ``vendor``, ``doc`` or ``trivial``.
|
``bugfix``, ``removal``, ``feature``, ``vendor``, ``doc`` or ``trivial``.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
Pytest no longer accepts prefixes of command-line arguments, for example
|
||||||
|
typing ``pytest --doctest-mod`` inplace of ``--doctest-modules``.
|
||||||
|
This was previously allowed where the ``ArgumentParser`` thought it was unambiguous,
|
||||||
|
but this could be incorrect due to delayed parsing of options for plugins.
|
||||||
|
See for example issues `#1149 <https://github.com/pytest-dev/pytest/issues/1149>`__,
|
||||||
|
`#3413 <https://github.com/pytest-dev/pytest/issues/3413>`__, and
|
||||||
|
`#4009 <https://github.com/pytest-dev/pytest/issues/4009>`__.
|
|
@ -0,0 +1 @@
|
||||||
|
Switch from ``imp`` to ``importlib``.
|
|
@ -0,0 +1,2 @@
|
||||||
|
The name of the ``.pyc`` files cached by the assertion writer now includes the pytest version
|
||||||
|
to avoid stale caches.
|
|
@ -0,0 +1 @@
|
||||||
|
Honor PEP 235 on case-insensitive file systems.
|
|
@ -0,0 +1 @@
|
||||||
|
Test module is no longer double-imported when using ``--pyargs``.
|
|
@ -0,0 +1,17 @@
|
||||||
|
Improved comparison of byte strings.
|
||||||
|
|
||||||
|
When comparing bytes, the assertion message used to show the byte numeric value when showing the differences::
|
||||||
|
|
||||||
|
def test():
|
||||||
|
> assert b'spam' == b'eggs'
|
||||||
|
E AssertionError: assert b'spam' == b'eggs'
|
||||||
|
E At index 0 diff: 115 != 101
|
||||||
|
E Use -v to get the full diff
|
||||||
|
|
||||||
|
It now shows the actual ascii representation instead, which is often more useful::
|
||||||
|
|
||||||
|
def test():
|
||||||
|
> assert b'spam' == b'eggs'
|
||||||
|
E AssertionError: assert b'spam' == b'eggs'
|
||||||
|
E At index 0 diff: b's' != b'e'
|
||||||
|
E Use -v to get the full diff
|
|
@ -0,0 +1 @@
|
||||||
|
Prevent "already imported" warnings from assertion rewriter when invoking pytest in-process multiple times.
|
|
@ -0,0 +1 @@
|
||||||
|
Fix assertion rewriting in packages (``__init__.py``).
|
|
@ -0,0 +1,8 @@
|
||||||
|
The `faulthandler <https://docs.python.org/3/library/faulthandler.html>`__ standard library
|
||||||
|
module is now enabled by default to help users diagnose crashes in C modules.
|
||||||
|
|
||||||
|
This functionality was provided by integrating the external
|
||||||
|
`pytest-faulthandler <https://github.com/pytest-dev/pytest-faulthandler>`__ plugin into the core,
|
||||||
|
so users should remove that plugin from their requirements if used.
|
||||||
|
|
||||||
|
For more information see the docs: https://docs.pytest.org/en/latest/usage.html#fault-handler
|
|
@ -0,0 +1,2 @@
|
||||||
|
Fix bug introduced in 4.6.0 causing collection errors when passing
|
||||||
|
more than 2 positional arguments to ``pytest.mark.parametrize``.
|
|
@ -1084,6 +1084,23 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||||
for more details.
|
for more details.
|
||||||
|
|
||||||
|
|
||||||
|
.. confval:: faulthandler_timeout
|
||||||
|
|
||||||
|
Dumps the tracebacks of all threads if a test takes longer than ``X`` seconds to run (including
|
||||||
|
fixture setup and teardown). Implemented using the `faulthandler.dump_traceback_later`_ function,
|
||||||
|
so all caveats there apply.
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
# content of pytest.ini
|
||||||
|
[pytest]
|
||||||
|
faulthandler_timeout=5
|
||||||
|
|
||||||
|
For more information please refer to :ref:`faulthandler`.
|
||||||
|
|
||||||
|
.. _`faulthandler.dump_traceback_later`: https://docs.python.org/3/library/faulthandler.html#faulthandler.dump_traceback_later
|
||||||
|
|
||||||
|
|
||||||
.. confval:: filterwarnings
|
.. confval:: filterwarnings
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -410,7 +410,6 @@ Pytest supports the use of ``breakpoint()`` with the following behaviours:
|
||||||
Profiling test execution duration
|
Profiling test execution duration
|
||||||
-------------------------------------
|
-------------------------------------
|
||||||
|
|
||||||
.. versionadded: 2.2
|
|
||||||
|
|
||||||
To get a list of the slowest 10 test durations:
|
To get a list of the slowest 10 test durations:
|
||||||
|
|
||||||
|
@ -420,6 +419,38 @@ To get a list of the slowest 10 test durations:
|
||||||
|
|
||||||
By default, pytest will not show test durations that are too small (<0.01s) unless ``-vv`` is passed on the command-line.
|
By default, pytest will not show test durations that are too small (<0.01s) unless ``-vv`` is passed on the command-line.
|
||||||
|
|
||||||
|
|
||||||
|
.. _faulthandler:
|
||||||
|
|
||||||
|
Fault Handler
|
||||||
|
-------------
|
||||||
|
|
||||||
|
.. versionadded:: 5.0
|
||||||
|
|
||||||
|
The `faulthandler <https://docs.python.org/3/library/faulthandler.html>`__ standard module
|
||||||
|
can be used to dump Python tracebacks on a segfault or after a timeout.
|
||||||
|
|
||||||
|
The module is automatically enabled for pytest runs, unless the ``-p no:faulthandler`` is given
|
||||||
|
on the command-line.
|
||||||
|
|
||||||
|
Also the :confval:`faulthandler_timeout=X<faulthandler_timeout>` configuration option can be used
|
||||||
|
to dump the traceback of all threads if a test takes longer than ``X``
|
||||||
|
seconds to finish (not available on Windows).
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
This functionality has been integrated from the external
|
||||||
|
`pytest-faulthandler <https://github.com/pytest-dev/pytest-faulthandler>`__ plugin, with two
|
||||||
|
small differences:
|
||||||
|
|
||||||
|
* To disable it, use ``-p no:faulthandler`` instead of ``--no-faulthandler``: the former
|
||||||
|
can be used with any plugin, so it saves one option.
|
||||||
|
|
||||||
|
* The ``--faulthandler-timeout`` command-line option has become the
|
||||||
|
:confval:`faulthandler_timeout` configuration option. It can still be configured from
|
||||||
|
the command-line using ``-o faulthandler_timeout=X``.
|
||||||
|
|
||||||
|
|
||||||
Creating JUnitXML format files
|
Creating JUnitXML format files
|
||||||
----------------------------------------------------
|
----------------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -2,20 +2,19 @@
|
||||||
import ast
|
import ast
|
||||||
import astor
|
import astor
|
||||||
import errno
|
import errno
|
||||||
import imp
|
import importlib.machinery
|
||||||
|
import importlib.util
|
||||||
import itertools
|
import itertools
|
||||||
import marshal
|
import marshal
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import struct
|
import struct
|
||||||
import sys
|
import sys
|
||||||
import types
|
import types
|
||||||
from importlib.util import spec_from_file_location
|
|
||||||
|
|
||||||
import atomicwrites
|
import atomicwrites
|
||||||
import py
|
|
||||||
|
|
||||||
from _pytest._io.saferepr import saferepr
|
from _pytest._io.saferepr import saferepr
|
||||||
|
from _pytest._version import version
|
||||||
from _pytest.assertion import util
|
from _pytest.assertion import util
|
||||||
from _pytest.assertion.util import ( # noqa: F401
|
from _pytest.assertion.util import ( # noqa: F401
|
||||||
format_explanation as _format_explanation,
|
format_explanation as _format_explanation,
|
||||||
|
@ -24,23 +23,13 @@ from _pytest.pathlib import fnmatch_ex
|
||||||
from _pytest.pathlib import PurePath
|
from _pytest.pathlib import PurePath
|
||||||
|
|
||||||
# pytest caches rewritten pycs in __pycache__.
|
# pytest caches rewritten pycs in __pycache__.
|
||||||
if hasattr(imp, "get_tag"):
|
PYTEST_TAG = "{}-pytest-{}".format(sys.implementation.cache_tag, version)
|
||||||
PYTEST_TAG = imp.get_tag() + "-PYTEST"
|
|
||||||
else:
|
|
||||||
if hasattr(sys, "pypy_version_info"):
|
|
||||||
impl = "pypy"
|
|
||||||
else:
|
|
||||||
impl = "cpython"
|
|
||||||
ver = sys.version_info
|
|
||||||
PYTEST_TAG = "{}-{}{}-PYTEST".format(impl, ver[0], ver[1])
|
|
||||||
del ver, impl
|
|
||||||
|
|
||||||
PYC_EXT = ".py" + (__debug__ and "c" or "o")
|
PYC_EXT = ".py" + (__debug__ and "c" or "o")
|
||||||
PYC_TAIL = "." + PYTEST_TAG + PYC_EXT
|
PYC_TAIL = "." + PYTEST_TAG + PYC_EXT
|
||||||
|
|
||||||
|
|
||||||
class AssertionRewritingHook:
|
class AssertionRewritingHook:
|
||||||
"""PEP302 Import hook which rewrites asserts."""
|
"""PEP302/PEP451 import hook which rewrites asserts."""
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
self.config = config
|
self.config = config
|
||||||
|
@ -49,7 +38,6 @@ class AssertionRewritingHook:
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.fnpats = ["test_*.py", "*_test.py"]
|
self.fnpats = ["test_*.py", "*_test.py"]
|
||||||
self.session = None
|
self.session = None
|
||||||
self.modules = {}
|
|
||||||
self._rewritten_names = set()
|
self._rewritten_names = set()
|
||||||
self._must_rewrite = set()
|
self._must_rewrite = set()
|
||||||
# flag to guard against trying to rewrite a pyc file while we are already writing another pyc file,
|
# flag to guard against trying to rewrite a pyc file while we are already writing another pyc file,
|
||||||
|
@ -63,55 +51,53 @@ class AssertionRewritingHook:
|
||||||
self.session = session
|
self.session = session
|
||||||
self._session_paths_checked = False
|
self._session_paths_checked = False
|
||||||
|
|
||||||
def _imp_find_module(self, name, path=None):
|
# Indirection so we can mock calls to find_spec originated from the hook during testing
|
||||||
"""Indirection so we can mock calls to find_module originated from the hook during testing"""
|
_find_spec = importlib.machinery.PathFinder.find_spec
|
||||||
return imp.find_module(name, path)
|
|
||||||
|
|
||||||
def find_module(self, name, path=None):
|
def find_spec(self, name, path=None, target=None):
|
||||||
if self._writing_pyc:
|
if self._writing_pyc:
|
||||||
return None
|
return None
|
||||||
state = self.config._assertstate
|
state = self.config._assertstate
|
||||||
if self._early_rewrite_bailout(name, state):
|
if self._early_rewrite_bailout(name, state):
|
||||||
return None
|
return None
|
||||||
state.trace("find_module called for: %s" % name)
|
state.trace("find_module called for: %s" % name)
|
||||||
names = name.rsplit(".", 1)
|
|
||||||
lastname = names[-1]
|
|
||||||
pth = None
|
|
||||||
if path is not None:
|
|
||||||
# Starting with Python 3.3, path is a _NamespacePath(), which
|
|
||||||
# causes problems if not converted to list.
|
|
||||||
path = list(path)
|
|
||||||
if len(path) == 1:
|
|
||||||
pth = path[0]
|
|
||||||
if pth is None:
|
|
||||||
try:
|
|
||||||
fd, fn, desc = self._imp_find_module(lastname, path)
|
|
||||||
except ImportError:
|
|
||||||
return None
|
|
||||||
if fd is not None:
|
|
||||||
fd.close()
|
|
||||||
tp = desc[2]
|
|
||||||
if tp == imp.PY_COMPILED:
|
|
||||||
if hasattr(imp, "source_from_cache"):
|
|
||||||
try:
|
|
||||||
fn = imp.source_from_cache(fn)
|
|
||||||
except ValueError:
|
|
||||||
# Python 3 doesn't like orphaned but still-importable
|
|
||||||
# .pyc files.
|
|
||||||
fn = fn[:-1]
|
|
||||||
else:
|
|
||||||
fn = fn[:-1]
|
|
||||||
elif tp != imp.PY_SOURCE:
|
|
||||||
# Don't know what this is.
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
fn = os.path.join(pth, name.rpartition(".")[2] + ".py")
|
|
||||||
|
|
||||||
fn_pypath = py.path.local(fn)
|
spec = self._find_spec(name, path)
|
||||||
if not self._should_rewrite(name, fn_pypath, state):
|
if (
|
||||||
|
# the import machinery could not find a file to import
|
||||||
|
spec is None
|
||||||
|
# this is a namespace package (without `__init__.py`)
|
||||||
|
# there's nothing to rewrite there
|
||||||
|
# python3.5 - python3.6: `namespace`
|
||||||
|
# python3.7+: `None`
|
||||||
|
or spec.origin in {None, "namespace"}
|
||||||
|
# we can only rewrite source files
|
||||||
|
or not isinstance(spec.loader, importlib.machinery.SourceFileLoader)
|
||||||
|
# if the file doesn't exist, we can't rewrite it
|
||||||
|
or not os.path.exists(spec.origin)
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
fn = spec.origin
|
||||||
|
|
||||||
|
if not self._should_rewrite(name, fn, state):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
self._rewritten_names.add(name)
|
return importlib.util.spec_from_file_location(
|
||||||
|
name,
|
||||||
|
fn,
|
||||||
|
loader=self,
|
||||||
|
submodule_search_locations=spec.submodule_search_locations,
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_module(self, spec):
|
||||||
|
return None # default behaviour is fine
|
||||||
|
|
||||||
|
def exec_module(self, module):
|
||||||
|
fn = module.__spec__.origin
|
||||||
|
state = self.config._assertstate
|
||||||
|
|
||||||
|
self._rewritten_names.add(module.__name__)
|
||||||
|
|
||||||
# The requested module looks like a test file, so rewrite it. This is
|
# The requested module looks like a test file, so rewrite it. This is
|
||||||
# the most magical part of the process: load the source, rewrite the
|
# the most magical part of the process: load the source, rewrite the
|
||||||
|
@ -122,7 +108,7 @@ class AssertionRewritingHook:
|
||||||
# cached pyc is always a complete, valid pyc. Operations on it must be
|
# cached pyc is always a complete, valid pyc. Operations on it must be
|
||||||
# atomic. POSIX's atomic rename comes in handy.
|
# atomic. POSIX's atomic rename comes in handy.
|
||||||
write = not sys.dont_write_bytecode
|
write = not sys.dont_write_bytecode
|
||||||
cache_dir = os.path.join(fn_pypath.dirname, "__pycache__")
|
cache_dir = os.path.join(os.path.dirname(fn), "__pycache__")
|
||||||
if write:
|
if write:
|
||||||
try:
|
try:
|
||||||
os.mkdir(cache_dir)
|
os.mkdir(cache_dir)
|
||||||
|
@ -133,26 +119,23 @@ class AssertionRewritingHook:
|
||||||
# common case) or it's blocked by a non-dir node. In the
|
# common case) or it's blocked by a non-dir node. In the
|
||||||
# latter case, we'll ignore it in _write_pyc.
|
# latter case, we'll ignore it in _write_pyc.
|
||||||
pass
|
pass
|
||||||
elif e in [errno.ENOENT, errno.ENOTDIR]:
|
elif e in {errno.ENOENT, errno.ENOTDIR}:
|
||||||
# One of the path components was not a directory, likely
|
# One of the path components was not a directory, likely
|
||||||
# because we're in a zip file.
|
# because we're in a zip file.
|
||||||
write = False
|
write = False
|
||||||
elif e in [errno.EACCES, errno.EROFS, errno.EPERM]:
|
elif e in {errno.EACCES, errno.EROFS, errno.EPERM}:
|
||||||
state.trace("read only directory: %r" % fn_pypath.dirname)
|
state.trace("read only directory: %r" % os.path.dirname(fn))
|
||||||
write = False
|
write = False
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
cache_name = fn_pypath.basename[:-3] + PYC_TAIL
|
cache_name = os.path.basename(fn)[:-3] + PYC_TAIL
|
||||||
pyc = os.path.join(cache_dir, cache_name)
|
pyc = os.path.join(cache_dir, cache_name)
|
||||||
# Notice that even if we're in a read-only directory, I'm going
|
# Notice that even if we're in a read-only directory, I'm going
|
||||||
# to check for a cached pyc. This may not be optimal...
|
# to check for a cached pyc. This may not be optimal...
|
||||||
co = _read_pyc(fn_pypath, pyc, state.trace)
|
co = _read_pyc(fn, pyc, state.trace)
|
||||||
if co is None:
|
if co is None:
|
||||||
state.trace("rewriting {!r}".format(fn))
|
state.trace("rewriting {!r}".format(fn))
|
||||||
source_stat, co = _rewrite_test(self.config, fn_pypath)
|
source_stat, co = _rewrite_test(fn)
|
||||||
if co is None:
|
|
||||||
# Probably a SyntaxError in the test.
|
|
||||||
return None
|
|
||||||
if write:
|
if write:
|
||||||
self._writing_pyc = True
|
self._writing_pyc = True
|
||||||
try:
|
try:
|
||||||
|
@ -161,13 +144,11 @@ class AssertionRewritingHook:
|
||||||
self._writing_pyc = False
|
self._writing_pyc = False
|
||||||
else:
|
else:
|
||||||
state.trace("found cached rewritten pyc for {!r}".format(fn))
|
state.trace("found cached rewritten pyc for {!r}".format(fn))
|
||||||
self.modules[name] = co, pyc
|
exec(co, module.__dict__)
|
||||||
return self
|
|
||||||
|
|
||||||
def _early_rewrite_bailout(self, name, state):
|
def _early_rewrite_bailout(self, name, state):
|
||||||
"""
|
"""This is a fast way to get out of rewriting modules. Profiling has
|
||||||
This is a fast way to get out of rewriting modules. Profiling has
|
shown that the call to PathFinder.find_spec (inside of the find_spec
|
||||||
shown that the call to imp.find_module (inside of the find_module
|
|
||||||
from this class) is a major slowdown, so, this method tries to
|
from this class) is a major slowdown, so, this method tries to
|
||||||
filter what we're sure won't be rewritten before getting to it.
|
filter what we're sure won't be rewritten before getting to it.
|
||||||
"""
|
"""
|
||||||
|
@ -202,10 +183,9 @@ class AssertionRewritingHook:
|
||||||
state.trace("early skip of rewriting module: {}".format(name))
|
state.trace("early skip of rewriting module: {}".format(name))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _should_rewrite(self, name, fn_pypath, state):
|
def _should_rewrite(self, name, fn, state):
|
||||||
# always rewrite conftest files
|
# always rewrite conftest files
|
||||||
fn = str(fn_pypath)
|
if os.path.basename(fn) == "conftest.py":
|
||||||
if fn_pypath.basename == "conftest.py":
|
|
||||||
state.trace("rewriting conftest file: {!r}".format(fn))
|
state.trace("rewriting conftest file: {!r}".format(fn))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -218,8 +198,9 @@ class AssertionRewritingHook:
|
||||||
|
|
||||||
# modules not passed explicitly on the command line are only
|
# modules not passed explicitly on the command line are only
|
||||||
# rewritten if they match the naming convention for test files
|
# rewritten if they match the naming convention for test files
|
||||||
|
fn_path = PurePath(fn)
|
||||||
for pat in self.fnpats:
|
for pat in self.fnpats:
|
||||||
if fn_pypath.fnmatch(pat):
|
if fnmatch_ex(pat, fn_path):
|
||||||
state.trace("matched test file {!r}".format(fn))
|
state.trace("matched test file {!r}".format(fn))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -250,9 +231,10 @@ class AssertionRewritingHook:
|
||||||
set(names).intersection(sys.modules).difference(self._rewritten_names)
|
set(names).intersection(sys.modules).difference(self._rewritten_names)
|
||||||
)
|
)
|
||||||
for name in already_imported:
|
for name in already_imported:
|
||||||
|
mod = sys.modules[name]
|
||||||
if not AssertionRewriter.is_rewrite_disabled(
|
if not AssertionRewriter.is_rewrite_disabled(
|
||||||
sys.modules[name].__doc__ or ""
|
mod.__doc__ or ""
|
||||||
):
|
) and not isinstance(mod.__loader__, type(self)):
|
||||||
self._warn_already_imported(name)
|
self._warn_already_imported(name)
|
||||||
self._must_rewrite.update(names)
|
self._must_rewrite.update(names)
|
||||||
self._marked_for_rewrite_cache.clear()
|
self._marked_for_rewrite_cache.clear()
|
||||||
|
@ -269,45 +251,8 @@ class AssertionRewritingHook:
|
||||||
stacklevel=5,
|
stacklevel=5,
|
||||||
)
|
)
|
||||||
|
|
||||||
def load_module(self, name):
|
|
||||||
co, pyc = self.modules.pop(name)
|
|
||||||
if name in sys.modules:
|
|
||||||
# If there is an existing module object named 'fullname' in
|
|
||||||
# sys.modules, the loader must use that existing module. (Otherwise,
|
|
||||||
# the reload() builtin will not work correctly.)
|
|
||||||
mod = sys.modules[name]
|
|
||||||
else:
|
|
||||||
# I wish I could just call imp.load_compiled here, but __file__ has to
|
|
||||||
# be set properly. In Python 3.2+, this all would be handled correctly
|
|
||||||
# by load_compiled.
|
|
||||||
mod = sys.modules[name] = imp.new_module(name)
|
|
||||||
try:
|
|
||||||
mod.__file__ = co.co_filename
|
|
||||||
# Normally, this attribute is 3.2+.
|
|
||||||
mod.__cached__ = pyc
|
|
||||||
mod.__loader__ = self
|
|
||||||
# Normally, this attribute is 3.4+
|
|
||||||
mod.__spec__ = spec_from_file_location(name, co.co_filename, loader=self)
|
|
||||||
exec(co, mod.__dict__)
|
|
||||||
except: # noqa
|
|
||||||
if name in sys.modules:
|
|
||||||
del sys.modules[name]
|
|
||||||
raise
|
|
||||||
return sys.modules[name]
|
|
||||||
|
|
||||||
def is_package(self, name):
|
|
||||||
try:
|
|
||||||
fd, fn, desc = self._imp_find_module(name)
|
|
||||||
except ImportError:
|
|
||||||
return False
|
|
||||||
if fd is not None:
|
|
||||||
fd.close()
|
|
||||||
tp = desc[2]
|
|
||||||
return tp == imp.PKG_DIRECTORY
|
|
||||||
|
|
||||||
def get_data(self, pathname):
|
def get_data(self, pathname):
|
||||||
"""Optional PEP302 get_data API.
|
"""Optional PEP302 get_data API."""
|
||||||
"""
|
|
||||||
with open(pathname, "rb") as f:
|
with open(pathname, "rb") as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
|
@ -315,15 +260,13 @@ class AssertionRewritingHook:
|
||||||
def _write_pyc(state, co, source_stat, pyc):
|
def _write_pyc(state, co, source_stat, pyc):
|
||||||
# Technically, we don't have to have the same pyc format as
|
# Technically, we don't have to have the same pyc format as
|
||||||
# (C)Python, since these "pycs" should never be seen by builtin
|
# (C)Python, since these "pycs" should never be seen by builtin
|
||||||
# import. However, there's little reason deviate, and I hope
|
# import. However, there's little reason deviate.
|
||||||
# sometime to be able to use imp.load_compiled to load them. (See
|
|
||||||
# the comment in load_module above.)
|
|
||||||
try:
|
try:
|
||||||
with atomicwrites.atomic_write(pyc, mode="wb", overwrite=True) as fp:
|
with atomicwrites.atomic_write(pyc, mode="wb", overwrite=True) as fp:
|
||||||
fp.write(imp.get_magic())
|
fp.write(importlib.util.MAGIC_NUMBER)
|
||||||
# as of now, bytecode header expects 32-bit numbers for size and mtime (#4903)
|
# as of now, bytecode header expects 32-bit numbers for size and mtime (#4903)
|
||||||
mtime = int(source_stat.mtime) & 0xFFFFFFFF
|
mtime = int(source_stat.st_mtime) & 0xFFFFFFFF
|
||||||
size = source_stat.size & 0xFFFFFFFF
|
size = source_stat.st_size & 0xFFFFFFFF
|
||||||
# "<LL" stands for 2 unsigned longs, little-ending
|
# "<LL" stands for 2 unsigned longs, little-ending
|
||||||
fp.write(struct.pack("<LL", mtime, size))
|
fp.write(struct.pack("<LL", mtime, size))
|
||||||
fp.write(marshal.dumps(co))
|
fp.write(marshal.dumps(co))
|
||||||
|
@ -336,35 +279,14 @@ def _write_pyc(state, co, source_stat, pyc):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
RN = b"\r\n"
|
def _rewrite_test(fn):
|
||||||
N = b"\n"
|
"""read and rewrite *fn* and return the code object."""
|
||||||
|
stat = os.stat(fn)
|
||||||
cookie_re = re.compile(r"^[ \t\f]*#.*coding[:=][ \t]*[-\w.]+")
|
with open(fn, "rb") as f:
|
||||||
BOM_UTF8 = "\xef\xbb\xbf"
|
source = f.read()
|
||||||
|
tree = ast.parse(source, filename=fn)
|
||||||
|
rewrite_asserts(tree, fn)
|
||||||
def _rewrite_test(config, fn):
|
co = compile(tree, fn, "exec", dont_inherit=True)
|
||||||
"""Try to read and rewrite *fn* and return the code object."""
|
|
||||||
state = config._assertstate
|
|
||||||
try:
|
|
||||||
stat = fn.stat()
|
|
||||||
source = fn.read("rb")
|
|
||||||
except EnvironmentError:
|
|
||||||
return None, None
|
|
||||||
try:
|
|
||||||
tree = ast.parse(source, filename=fn.strpath)
|
|
||||||
except SyntaxError:
|
|
||||||
# Let this pop up again in the real import.
|
|
||||||
state.trace("failed to parse: {!r}".format(fn))
|
|
||||||
return None, None
|
|
||||||
rewrite_asserts(tree, fn, config)
|
|
||||||
try:
|
|
||||||
co = compile(tree, fn.strpath, "exec", dont_inherit=True)
|
|
||||||
except SyntaxError:
|
|
||||||
# It's possible that this error is from some bug in the
|
|
||||||
# assertion rewriting, but I don't know of a fast way to tell.
|
|
||||||
state.trace("failed to compile: {!r}".format(fn))
|
|
||||||
return None, None
|
|
||||||
return stat, co
|
return stat, co
|
||||||
|
|
||||||
|
|
||||||
|
@ -379,8 +301,9 @@ def _read_pyc(source, pyc, trace=lambda x: None):
|
||||||
return None
|
return None
|
||||||
with fp:
|
with fp:
|
||||||
try:
|
try:
|
||||||
mtime = int(source.mtime())
|
stat_result = os.stat(source)
|
||||||
size = source.size()
|
mtime = int(stat_result.st_mtime)
|
||||||
|
size = stat_result.st_size
|
||||||
data = fp.read(12)
|
data = fp.read(12)
|
||||||
except EnvironmentError as e:
|
except EnvironmentError as e:
|
||||||
trace("_read_pyc({}): EnvironmentError {}".format(source, e))
|
trace("_read_pyc({}): EnvironmentError {}".format(source, e))
|
||||||
|
@ -388,7 +311,7 @@ def _read_pyc(source, pyc, trace=lambda x: None):
|
||||||
# Check for invalid or out of date pyc file.
|
# Check for invalid or out of date pyc file.
|
||||||
if (
|
if (
|
||||||
len(data) != 12
|
len(data) != 12
|
||||||
or data[:4] != imp.get_magic()
|
or data[:4] != importlib.util.MAGIC_NUMBER
|
||||||
or struct.unpack("<LL", data[4:]) != (mtime & 0xFFFFFFFF, size & 0xFFFFFFFF)
|
or struct.unpack("<LL", data[4:]) != (mtime & 0xFFFFFFFF, size & 0xFFFFFFFF)
|
||||||
):
|
):
|
||||||
trace("_read_pyc(%s): invalid or out of date pyc" % source)
|
trace("_read_pyc(%s): invalid or out of date pyc" % source)
|
||||||
|
@ -404,9 +327,9 @@ def _read_pyc(source, pyc, trace=lambda x: None):
|
||||||
return co
|
return co
|
||||||
|
|
||||||
|
|
||||||
def rewrite_asserts(mod, module_path=None, config=None):
|
def rewrite_asserts(mod, module_path=None):
|
||||||
"""Rewrite the assert statements in mod."""
|
"""Rewrite the assert statements in mod."""
|
||||||
AssertionRewriter(module_path, config).run(mod)
|
AssertionRewriter(module_path).run(mod)
|
||||||
|
|
||||||
|
|
||||||
def _saferepr(obj):
|
def _saferepr(obj):
|
||||||
|
@ -600,7 +523,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, module_path, config):
|
def __init__(self, module_path):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.module_path = module_path
|
self.module_path = module_path
|
||||||
self.config = config
|
self.config = config
|
||||||
|
@ -780,7 +703,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
"assertion is always true, perhaps remove parentheses?"
|
"assertion is always true, perhaps remove parentheses?"
|
||||||
),
|
),
|
||||||
category=None,
|
category=None,
|
||||||
filename=str(self.module_path),
|
filename=self.module_path,
|
||||||
lineno=assert_.lineno,
|
lineno=assert_.lineno,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -896,7 +819,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
AST_NONE = ast.parse("None").body[0].value
|
AST_NONE = ast.parse("None").body[0].value
|
||||||
val_is_none = ast.Compare(node, [ast.Is()], [AST_NONE])
|
val_is_none = ast.Compare(node, [ast.Is()], [AST_NONE])
|
||||||
send_warning = ast.parse(
|
send_warning = ast.parse(
|
||||||
"""
|
"""\
|
||||||
from _pytest.warning_types import PytestAssertRewriteWarning
|
from _pytest.warning_types import PytestAssertRewriteWarning
|
||||||
from warnings import warn_explicit
|
from warnings import warn_explicit
|
||||||
warn_explicit(
|
warn_explicit(
|
||||||
|
@ -906,7 +829,7 @@ warn_explicit(
|
||||||
lineno={lineno},
|
lineno={lineno},
|
||||||
)
|
)
|
||||||
""".format(
|
""".format(
|
||||||
filename=module_path.strpath, lineno=lineno
|
filename=module_path, lineno=lineno
|
||||||
)
|
)
|
||||||
).body
|
).body
|
||||||
return ast.If(val_is_none, send_warning, [])
|
return ast.If(val_is_none, send_warning, [])
|
||||||
|
@ -930,7 +853,7 @@ warn_explicit(
|
||||||
fail_save = self.expl_stmts
|
fail_save = self.expl_stmts
|
||||||
levels = len(boolop.values) - 1
|
levels = len(boolop.values) - 1
|
||||||
self.push_format_context()
|
self.push_format_context()
|
||||||
# Process each operand, short-circuting if needed.
|
# Process each operand, short-circuiting if needed.
|
||||||
for i, v in enumerate(boolop.values):
|
for i, v in enumerate(boolop.values):
|
||||||
if i:
|
if i:
|
||||||
fail_inner = []
|
fail_inner = []
|
||||||
|
|
|
@ -258,17 +258,38 @@ def _compare_eq_iterable(left, right, verbose=0):
|
||||||
|
|
||||||
|
|
||||||
def _compare_eq_sequence(left, right, verbose=0):
|
def _compare_eq_sequence(left, right, verbose=0):
|
||||||
|
comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes)
|
||||||
explanation = []
|
explanation = []
|
||||||
len_left = len(left)
|
len_left = len(left)
|
||||||
len_right = len(right)
|
len_right = len(right)
|
||||||
for i in range(min(len_left, len_right)):
|
for i in range(min(len_left, len_right)):
|
||||||
if left[i] != right[i]:
|
if left[i] != right[i]:
|
||||||
|
if comparing_bytes:
|
||||||
|
# when comparing bytes, we want to see their ascii representation
|
||||||
|
# instead of their numeric values (#5260)
|
||||||
|
# using a slice gives us the ascii representation:
|
||||||
|
# >>> s = b'foo'
|
||||||
|
# >>> s[0]
|
||||||
|
# 102
|
||||||
|
# >>> s[0:1]
|
||||||
|
# b'f'
|
||||||
|
left_value = left[i : i + 1]
|
||||||
|
right_value = right[i : i + 1]
|
||||||
|
else:
|
||||||
|
left_value = left[i]
|
||||||
|
right_value = right[i]
|
||||||
|
|
||||||
explanation += [
|
explanation += [
|
||||||
"At index {} diff: {!r} != {!r}".format(i, left[i], right[i])
|
"At index {} diff: {!r} != {!r}".format(i, left_value, right_value)
|
||||||
]
|
]
|
||||||
break
|
break
|
||||||
len_diff = len_left - len_right
|
|
||||||
|
|
||||||
|
if comparing_bytes:
|
||||||
|
# when comparing bytes, it doesn't help to show the "sides contain one or more items"
|
||||||
|
# longer explanation, so skip it
|
||||||
|
return explanation
|
||||||
|
|
||||||
|
len_diff = len_left - len_right
|
||||||
if len_diff:
|
if len_diff:
|
||||||
if len_diff > 0:
|
if len_diff > 0:
|
||||||
dir_with_more = "Left"
|
dir_with_more = "Left"
|
||||||
|
|
|
@ -140,6 +140,7 @@ default_plugins = essential_plugins + (
|
||||||
"warnings",
|
"warnings",
|
||||||
"logging",
|
"logging",
|
||||||
"reports",
|
"reports",
|
||||||
|
"faulthandler",
|
||||||
)
|
)
|
||||||
|
|
||||||
builtin_plugins = set(default_plugins)
|
builtin_plugins = set(default_plugins)
|
||||||
|
@ -288,7 +289,7 @@ class PytestPluginManager(PluginManager):
|
||||||
return opts
|
return opts
|
||||||
|
|
||||||
def register(self, plugin, name=None):
|
def register(self, plugin, name=None):
|
||||||
if name in ["pytest_catchlog", "pytest_capturelog"]:
|
if name in _pytest.deprecated.DEPRECATED_EXTERNAL_PLUGINS:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
PytestConfigWarning(
|
PytestConfigWarning(
|
||||||
"{} plugin has been merged into the core, "
|
"{} plugin has been merged into the core, "
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import argparse
|
import argparse
|
||||||
|
import sys
|
||||||
import warnings
|
import warnings
|
||||||
|
from gettext import gettext
|
||||||
|
|
||||||
import py
|
import py
|
||||||
|
|
||||||
|
@ -328,6 +330,7 @@ class MyOptionParser(argparse.ArgumentParser):
|
||||||
usage=parser._usage,
|
usage=parser._usage,
|
||||||
add_help=False,
|
add_help=False,
|
||||||
formatter_class=DropShorterLongHelpFormatter,
|
formatter_class=DropShorterLongHelpFormatter,
|
||||||
|
allow_abbrev=False,
|
||||||
)
|
)
|
||||||
# extra_info is a dict of (param -> value) to display if there's
|
# extra_info is a dict of (param -> value) to display if there's
|
||||||
# an usage error to provide more contextual information to the user
|
# an usage error to provide more contextual information to the user
|
||||||
|
@ -355,6 +358,42 @@ class MyOptionParser(argparse.ArgumentParser):
|
||||||
getattr(args, FILE_OR_DIR).extend(argv)
|
getattr(args, FILE_OR_DIR).extend(argv)
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
if sys.version_info[:2] < (3, 8): # pragma: no cover
|
||||||
|
# Backport of https://github.com/python/cpython/pull/14316 so we can
|
||||||
|
# disable long --argument abbreviations without breaking short flags.
|
||||||
|
def _parse_optional(self, arg_string):
|
||||||
|
if not arg_string:
|
||||||
|
return None
|
||||||
|
if not arg_string[0] in self.prefix_chars:
|
||||||
|
return None
|
||||||
|
if arg_string in self._option_string_actions:
|
||||||
|
action = self._option_string_actions[arg_string]
|
||||||
|
return action, arg_string, None
|
||||||
|
if len(arg_string) == 1:
|
||||||
|
return None
|
||||||
|
if "=" in arg_string:
|
||||||
|
option_string, explicit_arg = arg_string.split("=", 1)
|
||||||
|
if option_string in self._option_string_actions:
|
||||||
|
action = self._option_string_actions[option_string]
|
||||||
|
return action, option_string, explicit_arg
|
||||||
|
if self.allow_abbrev or not arg_string.startswith("--"):
|
||||||
|
option_tuples = self._get_option_tuples(arg_string)
|
||||||
|
if len(option_tuples) > 1:
|
||||||
|
msg = gettext(
|
||||||
|
"ambiguous option: %(option)s could match %(matches)s"
|
||||||
|
)
|
||||||
|
options = ", ".join(option for _, option, _ in option_tuples)
|
||||||
|
self.error(msg % {"option": arg_string, "matches": options})
|
||||||
|
elif len(option_tuples) == 1:
|
||||||
|
option_tuple, = option_tuples
|
||||||
|
return option_tuple
|
||||||
|
if self._negative_number_matcher.match(arg_string):
|
||||||
|
if not self._has_negative_number_optionals:
|
||||||
|
return None
|
||||||
|
if " " in arg_string:
|
||||||
|
return None
|
||||||
|
return None, arg_string, None
|
||||||
|
|
||||||
|
|
||||||
class DropShorterLongHelpFormatter(argparse.HelpFormatter):
|
class DropShorterLongHelpFormatter(argparse.HelpFormatter):
|
||||||
"""shorten help for long options that differ only in extra hyphens
|
"""shorten help for long options that differ only in extra hyphens
|
||||||
|
|
|
@ -14,6 +14,14 @@ from _pytest.warning_types import UnformattedWarning
|
||||||
|
|
||||||
YIELD_TESTS = "yield tests were removed in pytest 4.0 - {name} will be ignored"
|
YIELD_TESTS = "yield tests were removed in pytest 4.0 - {name} will be ignored"
|
||||||
|
|
||||||
|
# set of plugins which have been integrated into the core; we use this list to ignore
|
||||||
|
# them during registration to avoid conflicts
|
||||||
|
DEPRECATED_EXTERNAL_PLUGINS = {
|
||||||
|
"pytest_catchlog",
|
||||||
|
"pytest_capturelog",
|
||||||
|
"pytest_faulthandler",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
FIXTURE_FUNCTION_CALL = (
|
FIXTURE_FUNCTION_CALL = (
|
||||||
'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n'
|
'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n'
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_addoption(parser):
|
||||||
|
help = (
|
||||||
|
"Dump the traceback of all threads if a test takes "
|
||||||
|
"more than TIMEOUT seconds to finish.\n"
|
||||||
|
"Not available on Windows."
|
||||||
|
)
|
||||||
|
parser.addini("faulthandler_timeout", help, default=0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_configure(config):
|
||||||
|
import faulthandler
|
||||||
|
|
||||||
|
# avoid trying to dup sys.stderr if faulthandler is already enabled
|
||||||
|
if faulthandler.is_enabled():
|
||||||
|
return
|
||||||
|
|
||||||
|
stderr_fd_copy = os.dup(_get_stderr_fileno())
|
||||||
|
config.fault_handler_stderr = os.fdopen(stderr_fd_copy, "w")
|
||||||
|
faulthandler.enable(file=config.fault_handler_stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_stderr_fileno():
|
||||||
|
try:
|
||||||
|
return sys.stderr.fileno()
|
||||||
|
except (AttributeError, io.UnsupportedOperation):
|
||||||
|
# python-xdist monkeypatches sys.stderr with an object that is not an actual file.
|
||||||
|
# https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors
|
||||||
|
# This is potentially dangerous, but the best we can do.
|
||||||
|
return sys.__stderr__.fileno()
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_unconfigure(config):
|
||||||
|
import faulthandler
|
||||||
|
|
||||||
|
faulthandler.disable()
|
||||||
|
# close our dup file installed during pytest_configure
|
||||||
|
f = getattr(config, "fault_handler_stderr", None)
|
||||||
|
if f is not None:
|
||||||
|
# re-enable the faulthandler, attaching it to the default sys.stderr
|
||||||
|
# so we can see crashes after pytest has finished, usually during
|
||||||
|
# garbage collection during interpreter shutdown
|
||||||
|
config.fault_handler_stderr.close()
|
||||||
|
del config.fault_handler_stderr
|
||||||
|
faulthandler.enable(file=_get_stderr_fileno())
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.hookimpl(hookwrapper=True)
|
||||||
|
def pytest_runtest_protocol(item):
|
||||||
|
timeout = float(item.config.getini("faulthandler_timeout") or 0.0)
|
||||||
|
if timeout > 0:
|
||||||
|
import faulthandler
|
||||||
|
|
||||||
|
stderr = item.config.fault_handler_stderr
|
||||||
|
faulthandler.dump_traceback_later(timeout, file=stderr)
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
faulthandler.cancel_dump_traceback_later()
|
||||||
|
else:
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.hookimpl(tryfirst=True)
|
||||||
|
def pytest_enter_pdb():
|
||||||
|
"""Cancel any traceback dumping due to timeout before entering pdb.
|
||||||
|
"""
|
||||||
|
import faulthandler
|
||||||
|
|
||||||
|
faulthandler.cancel_dump_traceback_later()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.hookimpl(tryfirst=True)
|
||||||
|
def pytest_exception_interact():
|
||||||
|
"""Cancel any traceback dumping due to an interactive exception being
|
||||||
|
raised.
|
||||||
|
"""
|
||||||
|
import faulthandler
|
||||||
|
|
||||||
|
faulthandler.cancel_dump_traceback_later()
|
|
@ -2,8 +2,8 @@
|
||||||
import enum
|
import enum
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import functools
|
import functools
|
||||||
|
import importlib
|
||||||
import os
|
import os
|
||||||
import pkgutil
|
|
||||||
import sys
|
import sys
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
@ -630,21 +630,15 @@ class Session(nodes.FSCollector):
|
||||||
def _tryconvertpyarg(self, x):
|
def _tryconvertpyarg(self, x):
|
||||||
"""Convert a dotted module name to path."""
|
"""Convert a dotted module name to path."""
|
||||||
try:
|
try:
|
||||||
loader = pkgutil.find_loader(x)
|
spec = importlib.util.find_spec(x)
|
||||||
except ImportError:
|
except (ValueError, ImportError):
|
||||||
return x
|
return x
|
||||||
if loader is None:
|
if spec is None or spec.origin in {None, "namespace"}:
|
||||||
return x
|
return x
|
||||||
# This method is sometimes invoked when AssertionRewritingHook, which
|
elif spec.submodule_search_locations:
|
||||||
# does not define a get_filename method, is already in place:
|
return os.path.dirname(spec.origin)
|
||||||
try:
|
else:
|
||||||
path = loader.get_filename(x)
|
return spec.origin
|
||||||
except AttributeError:
|
|
||||||
# Retrieve path from AssertionRewritingHook:
|
|
||||||
path = loader.modules[x][0].co_filename
|
|
||||||
if loader.is_package(x):
|
|
||||||
path = os.path.dirname(path)
|
|
||||||
return path
|
|
||||||
|
|
||||||
def _parsearg(self, arg):
|
def _parsearg(self, arg):
|
||||||
""" return (fspath, names) tuple after checking the file exists. """
|
""" return (fspath, names) tuple after checking the file exists. """
|
||||||
|
|
|
@ -102,10 +102,7 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
|
||||||
return cls(parameterset, marks=[], id=None)
|
return cls(parameterset, marks=[], id=None)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_parametrize_args(argnames, argvalues, **_):
|
def _parse_parametrize_args(argnames, argvalues, *args, **kwargs):
|
||||||
"""It receives an ignored _ (kwargs) argument so this function can
|
|
||||||
take also calls from parametrize ignoring scope, indirect, and other
|
|
||||||
arguments..."""
|
|
||||||
if not isinstance(argnames, (tuple, list)):
|
if not isinstance(argnames, (tuple, list)):
|
||||||
argnames = [x.strip() for x in argnames.split(",") if x.strip()]
|
argnames = [x.strip() for x in argnames.split(",") if x.strip()]
|
||||||
force_tuple = len(argnames) == 1
|
force_tuple = len(argnames) == 1
|
||||||
|
|
|
@ -149,7 +149,6 @@ def importorskip(modname, minversion=None, reason=None):
|
||||||
|
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
compile(modname, "", "eval") # to catch syntaxerrors
|
compile(modname, "", "eval") # to catch syntaxerrors
|
||||||
import_exc = None
|
|
||||||
|
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
# make sure to ignore ImportWarnings that might happen because
|
# make sure to ignore ImportWarnings that might happen because
|
||||||
|
@ -159,12 +158,9 @@ def importorskip(modname, minversion=None, reason=None):
|
||||||
try:
|
try:
|
||||||
__import__(modname)
|
__import__(modname)
|
||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
# Do not raise chained exception here(#1485)
|
if reason is None:
|
||||||
import_exc = exc
|
reason = "could not import {!r}: {}".format(modname, exc)
|
||||||
if import_exc:
|
raise Skipped(reason, allow_module_level=True) from None
|
||||||
if reason is None:
|
|
||||||
reason = "could not import {!r}: {}".format(modname, import_exc)
|
|
||||||
raise Skipped(reason, allow_module_level=True)
|
|
||||||
mod = sys.modules[modname]
|
mod = sys.modules[modname]
|
||||||
if minversion is None:
|
if minversion is None:
|
||||||
return mod
|
return mod
|
||||||
|
|
|
@ -294,6 +294,8 @@ def fnmatch_ex(pattern, path):
|
||||||
name = path.name
|
name = path.name
|
||||||
else:
|
else:
|
||||||
name = str(path)
|
name = str(path)
|
||||||
|
if path.is_absolute() and not os.path.isabs(pattern):
|
||||||
|
pattern = "*{}{}".format(os.sep, pattern)
|
||||||
return fnmatch.fnmatch(name, pattern)
|
return fnmatch.fnmatch(name, pattern)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
"""(disabled by default) support for testing pytest and pytest plugins."""
|
"""(disabled by default) support for testing pytest and pytest plugins."""
|
||||||
import gc
|
import gc
|
||||||
|
import importlib
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import re
|
import re
|
||||||
|
@ -16,7 +17,6 @@ import py
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest._code import Source
|
from _pytest._code import Source
|
||||||
from _pytest._io.saferepr import saferepr
|
from _pytest._io.saferepr import saferepr
|
||||||
from _pytest.assertion.rewrite import AssertionRewritingHook
|
|
||||||
from _pytest.capture import MultiCapture
|
from _pytest.capture import MultiCapture
|
||||||
from _pytest.capture import SysCapture
|
from _pytest.capture import SysCapture
|
||||||
from _pytest.main import ExitCode
|
from _pytest.main import ExitCode
|
||||||
|
@ -787,6 +787,11 @@ class Testdir:
|
||||||
|
|
||||||
:return: a :py:class:`HookRecorder` instance
|
:return: a :py:class:`HookRecorder` instance
|
||||||
"""
|
"""
|
||||||
|
# (maybe a cpython bug?) the importlib cache sometimes isn't updated
|
||||||
|
# properly between file creation and inline_run (especially if imports
|
||||||
|
# are interspersed with file creation)
|
||||||
|
importlib.invalidate_caches()
|
||||||
|
|
||||||
plugins = list(plugins)
|
plugins = list(plugins)
|
||||||
finalizers = []
|
finalizers = []
|
||||||
try:
|
try:
|
||||||
|
@ -796,18 +801,6 @@ class Testdir:
|
||||||
mp_run.setenv(k, v)
|
mp_run.setenv(k, v)
|
||||||
finalizers.append(mp_run.undo)
|
finalizers.append(mp_run.undo)
|
||||||
|
|
||||||
# When running pytest inline any plugins active in the main test
|
|
||||||
# process are already imported. So this disables the warning which
|
|
||||||
# will trigger to say they can no longer be rewritten, which is
|
|
||||||
# fine as they have already been rewritten.
|
|
||||||
orig_warn = AssertionRewritingHook._warn_already_imported
|
|
||||||
|
|
||||||
def revert_warn_already_imported():
|
|
||||||
AssertionRewritingHook._warn_already_imported = orig_warn
|
|
||||||
|
|
||||||
finalizers.append(revert_warn_already_imported)
|
|
||||||
AssertionRewritingHook._warn_already_imported = lambda *a: None
|
|
||||||
|
|
||||||
# Any sys.module or sys.path changes done while running pytest
|
# Any sys.module or sys.path changes done while running pytest
|
||||||
# inline should be reverted after the test run completes to avoid
|
# inline should be reverted after the test run completes to avoid
|
||||||
# clashing with later inline tests run within the same pytest test,
|
# clashing with later inline tests run within the same pytest test,
|
||||||
|
|
|
@ -76,8 +76,7 @@ def pytest_addoption(parser):
|
||||||
help="show extra test summary info as specified by chars: (f)ailed, "
|
help="show extra test summary info as specified by chars: (f)ailed, "
|
||||||
"(E)rror, (s)kipped, (x)failed, (X)passed, "
|
"(E)rror, (s)kipped, (x)failed, (X)passed, "
|
||||||
"(p)assed, (P)assed with output, (a)ll except passed (p/P), or (A)ll. "
|
"(p)assed, (P)assed with output, (a)ll except passed (p/P), or (A)ll. "
|
||||||
"Warnings are displayed at all times except when "
|
"(w)arnings are enabled by default (see --disable-warnings).",
|
||||||
"--disable-warnings is set.",
|
|
||||||
)
|
)
|
||||||
group._addoption(
|
group._addoption(
|
||||||
"--disable-warnings",
|
"--disable-warnings",
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
"""
|
"""
|
||||||
pytest: unit and functional testing with Python.
|
pytest: unit and functional testing with Python.
|
||||||
"""
|
"""
|
||||||
# else we are imported
|
|
||||||
from _pytest import __version__
|
from _pytest import __version__
|
||||||
from _pytest.assertion import register_assert_rewrite
|
from _pytest.assertion import register_assert_rewrite
|
||||||
from _pytest.config import cmdline
|
from _pytest.config import cmdline
|
||||||
|
|
|
@ -633,6 +633,19 @@ class TestInvocationVariants:
|
||||||
|
|
||||||
result.stdout.fnmatch_lines(["collected*0*items*/*1*errors"])
|
result.stdout.fnmatch_lines(["collected*0*items*/*1*errors"])
|
||||||
|
|
||||||
|
def test_pyargs_only_imported_once(self, testdir):
|
||||||
|
pkg = testdir.mkpydir("foo")
|
||||||
|
pkg.join("test_foo.py").write("print('hello from test_foo')\ndef test(): pass")
|
||||||
|
pkg.join("conftest.py").write(
|
||||||
|
"def pytest_configure(config): print('configuring')"
|
||||||
|
)
|
||||||
|
|
||||||
|
result = testdir.runpytest("--pyargs", "foo.test_foo", "-s", syspathinsert=True)
|
||||||
|
# should only import once
|
||||||
|
assert result.outlines.count("hello from test_foo") == 1
|
||||||
|
# should only configure once
|
||||||
|
assert result.outlines.count("configuring") == 1
|
||||||
|
|
||||||
def test_cmdline_python_package(self, testdir, monkeypatch):
|
def test_cmdline_python_package(self, testdir, monkeypatch):
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
@ -983,7 +996,7 @@ def test_zipimport_hook(testdir, tmpdir):
|
||||||
"app/foo.py": """
|
"app/foo.py": """
|
||||||
import pytest
|
import pytest
|
||||||
def main():
|
def main():
|
||||||
pytest.main(['--pyarg', 'foo'])
|
pytest.main(['--pyargs', 'foo'])
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from _pytest import deprecated
|
||||||
from _pytest.warning_types import PytestDeprecationWarning
|
from _pytest.warning_types import PytestDeprecationWarning
|
||||||
from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG
|
from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG
|
||||||
|
|
||||||
|
@ -69,22 +70,14 @@ def test_terminal_reporter_writer_attr(pytestconfig):
|
||||||
assert terminal_reporter.writer is terminal_reporter._tw
|
assert terminal_reporter.writer is terminal_reporter._tw
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("plugin", ["catchlog", "capturelog"])
|
@pytest.mark.parametrize("plugin", deprecated.DEPRECATED_EXTERNAL_PLUGINS)
|
||||||
@pytest.mark.filterwarnings("default")
|
@pytest.mark.filterwarnings("default")
|
||||||
def test_pytest_catchlog_deprecated(testdir, plugin):
|
def test_external_plugins_integrated(testdir, plugin):
|
||||||
testdir.makepyfile(
|
testdir.syspathinsert()
|
||||||
"""
|
testdir.makepyfile(**{plugin: ""})
|
||||||
def test_func(pytestconfig):
|
|
||||||
pytestconfig.pluginmanager.register(None, 'pytest_{}')
|
with pytest.warns(pytest.PytestConfigWarning):
|
||||||
""".format(
|
testdir.parseconfig("-p", plugin)
|
||||||
plugin
|
|
||||||
)
|
|
||||||
)
|
|
||||||
res = testdir.runpytest()
|
|
||||||
assert res.ret == 0
|
|
||||||
res.stdout.fnmatch_lines(
|
|
||||||
["*pytest-*log plugin has been merged into the core*", "*1 passed, 1 warnings*"]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_raises_message_argument_deprecated():
|
def test_raises_message_argument_deprecated():
|
||||||
|
|
|
@ -1761,3 +1761,16 @@ class TestMarkersWithParametrization:
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
["*test_func_a*0*PASS*", "*test_func_a*2*PASS*", "*test_func_b*10*PASS*"]
|
["*test_func_a*0*PASS*", "*test_func_a*2*PASS*", "*test_func_b*10*PASS*"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_parametrize_positional_args(self, testdir):
|
||||||
|
testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("a", [1], False)
|
||||||
|
def test_foo(a):
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = testdir.runpytest()
|
||||||
|
result.assert_outcomes(passed=1)
|
||||||
|
|
|
@ -137,8 +137,8 @@ class TestImportHookInstallation:
|
||||||
"hamster.py": "",
|
"hamster.py": "",
|
||||||
"test_foo.py": """\
|
"test_foo.py": """\
|
||||||
def test_foo(pytestconfig):
|
def test_foo(pytestconfig):
|
||||||
assert pytestconfig.pluginmanager.rewrite_hook.find_module('ham') is not None
|
assert pytestconfig.pluginmanager.rewrite_hook.find_spec('ham') is not None
|
||||||
assert pytestconfig.pluginmanager.rewrite_hook.find_module('hamster') is None
|
assert pytestconfig.pluginmanager.rewrite_hook.find_spec('hamster') is None
|
||||||
""",
|
""",
|
||||||
}
|
}
|
||||||
testdir.makepyfile(**contents)
|
testdir.makepyfile(**contents)
|
||||||
|
@ -331,6 +331,27 @@ class TestAssert_reprcompare:
|
||||||
assert "- spam" in diff
|
assert "- spam" in diff
|
||||||
assert "+ eggs" in diff
|
assert "+ eggs" in diff
|
||||||
|
|
||||||
|
def test_bytes_diff_normal(self):
|
||||||
|
"""Check special handling for bytes diff (#5260)"""
|
||||||
|
diff = callequal(b"spam", b"eggs")
|
||||||
|
|
||||||
|
assert diff == [
|
||||||
|
"b'spam' == b'eggs'",
|
||||||
|
"At index 0 diff: b's' != b'e'",
|
||||||
|
"Use -v to get the full diff",
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_bytes_diff_verbose(self):
|
||||||
|
"""Check special handling for bytes diff (#5260)"""
|
||||||
|
diff = callequal(b"spam", b"eggs", verbose=True)
|
||||||
|
assert diff == [
|
||||||
|
"b'spam' == b'eggs'",
|
||||||
|
"At index 0 diff: b's' != b'e'",
|
||||||
|
"Full diff:",
|
||||||
|
"- b'spam'",
|
||||||
|
"+ b'eggs'",
|
||||||
|
]
|
||||||
|
|
||||||
def test_list(self):
|
def test_list(self):
|
||||||
expl = callequal([0, 1], [0, 2])
|
expl = callequal([0, 1], [0, 2])
|
||||||
assert len(expl) > 1
|
assert len(expl) > 1
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import ast
|
import ast
|
||||||
import glob
|
import glob
|
||||||
|
import importlib
|
||||||
import os
|
import os
|
||||||
import py_compile
|
import py_compile
|
||||||
import stat
|
import stat
|
||||||
|
@ -117,6 +118,37 @@ class TestAssertionRewrite:
|
||||||
result = testdir.runpytest_subprocess()
|
result = testdir.runpytest_subprocess()
|
||||||
assert "warnings" not in "".join(result.outlines)
|
assert "warnings" not in "".join(result.outlines)
|
||||||
|
|
||||||
|
def test_rewrites_plugin_as_a_package(self, testdir):
|
||||||
|
pkgdir = testdir.mkpydir("plugin")
|
||||||
|
pkgdir.join("__init__.py").write(
|
||||||
|
"import pytest\n"
|
||||||
|
"@pytest.fixture\n"
|
||||||
|
"def special_asserter():\n"
|
||||||
|
" def special_assert(x, y):\n"
|
||||||
|
" assert x == y\n"
|
||||||
|
" return special_assert\n"
|
||||||
|
)
|
||||||
|
testdir.makeconftest('pytest_plugins = ["plugin"]')
|
||||||
|
testdir.makepyfile("def test(special_asserter): special_asserter(1, 2)\n")
|
||||||
|
result = testdir.runpytest()
|
||||||
|
result.stdout.fnmatch_lines(["*assert 1 == 2*"])
|
||||||
|
|
||||||
|
def test_honors_pep_235(self, testdir, monkeypatch):
|
||||||
|
# note: couldn't make it fail on macos with a single `sys.path` entry
|
||||||
|
# note: these modules are named `test_*` to trigger rewriting
|
||||||
|
testdir.tmpdir.join("test_y.py").write("x = 1")
|
||||||
|
xdir = testdir.tmpdir.join("x").ensure_dir()
|
||||||
|
xdir.join("test_Y").ensure_dir().join("__init__.py").write("x = 2")
|
||||||
|
testdir.makepyfile(
|
||||||
|
"import test_y\n"
|
||||||
|
"import test_Y\n"
|
||||||
|
"def test():\n"
|
||||||
|
" assert test_y.x == 1\n"
|
||||||
|
" assert test_Y.x == 2\n"
|
||||||
|
)
|
||||||
|
monkeypatch.syspath_prepend(xdir)
|
||||||
|
testdir.runpytest().assert_outcomes(passed=1)
|
||||||
|
|
||||||
def test_name(self, request):
|
def test_name(self, request):
|
||||||
def f():
|
def f():
|
||||||
assert False
|
assert False
|
||||||
|
@ -748,6 +780,24 @@ def test_rewritten():
|
||||||
|
|
||||||
assert testdir.runpytest().ret == 0
|
assert testdir.runpytest().ret == 0
|
||||||
|
|
||||||
|
def test_cached_pyc_includes_pytest_version(self, testdir, monkeypatch):
|
||||||
|
"""Avoid stale caches (#1671)"""
|
||||||
|
monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False)
|
||||||
|
testdir.makepyfile(
|
||||||
|
test_foo="""
|
||||||
|
def test_foo():
|
||||||
|
assert True
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = testdir.runpytest_subprocess()
|
||||||
|
assert result.ret == 0
|
||||||
|
found_names = glob.glob(
|
||||||
|
"__pycache__/*-pytest-{}.pyc".format(pytest.__version__)
|
||||||
|
)
|
||||||
|
assert found_names, "pyc with expected tag not found in names: {}".format(
|
||||||
|
glob.glob("__pycache__/*.pyc")
|
||||||
|
)
|
||||||
|
|
||||||
@pytest.mark.skipif('"__pypy__" in sys.modules')
|
@pytest.mark.skipif('"__pypy__" in sys.modules')
|
||||||
def test_pyc_vs_pyo(self, testdir, monkeypatch):
|
def test_pyc_vs_pyo(self, testdir, monkeypatch):
|
||||||
testdir.makepyfile(
|
testdir.makepyfile(
|
||||||
|
@ -831,8 +881,9 @@ def test_rewritten():
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
hook, "_warn_already_imported", lambda code, msg: warnings.append(msg)
|
hook, "_warn_already_imported", lambda code, msg: warnings.append(msg)
|
||||||
)
|
)
|
||||||
hook.find_module("test_remember_rewritten_modules")
|
spec = hook.find_spec("test_remember_rewritten_modules")
|
||||||
hook.load_module("test_remember_rewritten_modules")
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
hook.exec_module(module)
|
||||||
hook.mark_rewrite("test_remember_rewritten_modules")
|
hook.mark_rewrite("test_remember_rewritten_modules")
|
||||||
hook.mark_rewrite("test_remember_rewritten_modules")
|
hook.mark_rewrite("test_remember_rewritten_modules")
|
||||||
assert warnings == []
|
assert warnings == []
|
||||||
|
@ -872,33 +923,6 @@ def test_rewritten():
|
||||||
|
|
||||||
|
|
||||||
class TestAssertionRewriteHookDetails:
|
class TestAssertionRewriteHookDetails:
|
||||||
def test_loader_is_package_false_for_module(self, testdir):
|
|
||||||
testdir.makepyfile(
|
|
||||||
test_fun="""
|
|
||||||
def test_loader():
|
|
||||||
assert not __loader__.is_package(__name__)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
result = testdir.runpytest()
|
|
||||||
result.stdout.fnmatch_lines(["* 1 passed*"])
|
|
||||||
|
|
||||||
def test_loader_is_package_true_for_package(self, testdir):
|
|
||||||
testdir.makepyfile(
|
|
||||||
test_fun="""
|
|
||||||
def test_loader():
|
|
||||||
assert not __loader__.is_package(__name__)
|
|
||||||
|
|
||||||
def test_fun():
|
|
||||||
assert __loader__.is_package('fun')
|
|
||||||
|
|
||||||
def test_missing():
|
|
||||||
assert not __loader__.is_package('pytest_not_there')
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
testdir.mkpydir("fun")
|
|
||||||
result = testdir.runpytest()
|
|
||||||
result.stdout.fnmatch_lines(["* 3 passed*"])
|
|
||||||
|
|
||||||
def test_sys_meta_path_munged(self, testdir):
|
def test_sys_meta_path_munged(self, testdir):
|
||||||
testdir.makepyfile(
|
testdir.makepyfile(
|
||||||
"""
|
"""
|
||||||
|
@ -917,7 +941,7 @@ class TestAssertionRewriteHookDetails:
|
||||||
state = AssertionState(config, "rewrite")
|
state = AssertionState(config, "rewrite")
|
||||||
source_path = tmpdir.ensure("source.py")
|
source_path = tmpdir.ensure("source.py")
|
||||||
pycpath = tmpdir.join("pyc").strpath
|
pycpath = tmpdir.join("pyc").strpath
|
||||||
assert _write_pyc(state, [1], source_path.stat(), pycpath)
|
assert _write_pyc(state, [1], os.stat(source_path.strpath), pycpath)
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def atomic_write_failed(fn, mode="r", overwrite=False):
|
def atomic_write_failed(fn, mode="r", overwrite=False):
|
||||||
|
@ -979,7 +1003,7 @@ class TestAssertionRewriteHookDetails:
|
||||||
assert len(contents) > strip_bytes
|
assert len(contents) > strip_bytes
|
||||||
pyc.write(contents[:strip_bytes], mode="wb")
|
pyc.write(contents[:strip_bytes], mode="wb")
|
||||||
|
|
||||||
assert _read_pyc(source, str(pyc)) is None # no error
|
assert _read_pyc(str(source), str(pyc)) is None # no error
|
||||||
|
|
||||||
def test_reload_is_same(self, testdir):
|
def test_reload_is_same(self, testdir):
|
||||||
# A file that will be picked up during collecting.
|
# A file that will be picked up during collecting.
|
||||||
|
@ -1186,14 +1210,17 @@ def test_rewrite_infinite_recursion(testdir, pytestconfig, monkeypatch):
|
||||||
# make a note that we have called _write_pyc
|
# make a note that we have called _write_pyc
|
||||||
write_pyc_called.append(True)
|
write_pyc_called.append(True)
|
||||||
# try to import a module at this point: we should not try to rewrite this module
|
# try to import a module at this point: we should not try to rewrite this module
|
||||||
assert hook.find_module("test_bar") is None
|
assert hook.find_spec("test_bar") is None
|
||||||
return original_write_pyc(*args, **kwargs)
|
return original_write_pyc(*args, **kwargs)
|
||||||
|
|
||||||
monkeypatch.setattr(rewrite, "_write_pyc", spy_write_pyc)
|
monkeypatch.setattr(rewrite, "_write_pyc", spy_write_pyc)
|
||||||
monkeypatch.setattr(sys, "dont_write_bytecode", False)
|
monkeypatch.setattr(sys, "dont_write_bytecode", False)
|
||||||
|
|
||||||
hook = AssertionRewritingHook(pytestconfig)
|
hook = AssertionRewritingHook(pytestconfig)
|
||||||
assert hook.find_module("test_foo") is not None
|
spec = hook.find_spec("test_foo")
|
||||||
|
assert spec is not None
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
hook.exec_module(module)
|
||||||
assert len(write_pyc_called) == 1
|
assert len(write_pyc_called) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@ -1201,11 +1228,11 @@ class TestEarlyRewriteBailout:
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def hook(self, pytestconfig, monkeypatch, testdir):
|
def hook(self, pytestconfig, monkeypatch, testdir):
|
||||||
"""Returns a patched AssertionRewritingHook instance so we can configure its initial paths and track
|
"""Returns a patched AssertionRewritingHook instance so we can configure its initial paths and track
|
||||||
if imp.find_module has been called.
|
if PathFinder.find_spec has been called.
|
||||||
"""
|
"""
|
||||||
import imp
|
import importlib.machinery
|
||||||
|
|
||||||
self.find_module_calls = []
|
self.find_spec_calls = []
|
||||||
self.initial_paths = set()
|
self.initial_paths = set()
|
||||||
|
|
||||||
class StubSession:
|
class StubSession:
|
||||||
|
@ -1214,22 +1241,22 @@ class TestEarlyRewriteBailout:
|
||||||
def isinitpath(self, p):
|
def isinitpath(self, p):
|
||||||
return p in self._initialpaths
|
return p in self._initialpaths
|
||||||
|
|
||||||
def spy_imp_find_module(name, path):
|
def spy_find_spec(name, path):
|
||||||
self.find_module_calls.append(name)
|
self.find_spec_calls.append(name)
|
||||||
return imp.find_module(name, path)
|
return importlib.machinery.PathFinder.find_spec(name, path)
|
||||||
|
|
||||||
hook = AssertionRewritingHook(pytestconfig)
|
hook = AssertionRewritingHook(pytestconfig)
|
||||||
# use default patterns, otherwise we inherit pytest's testing config
|
# use default patterns, otherwise we inherit pytest's testing config
|
||||||
hook.fnpats[:] = ["test_*.py", "*_test.py"]
|
hook.fnpats[:] = ["test_*.py", "*_test.py"]
|
||||||
monkeypatch.setattr(hook, "_imp_find_module", spy_imp_find_module)
|
monkeypatch.setattr(hook, "_find_spec", spy_find_spec)
|
||||||
hook.set_session(StubSession())
|
hook.set_session(StubSession())
|
||||||
testdir.syspathinsert()
|
testdir.syspathinsert()
|
||||||
return hook
|
return hook
|
||||||
|
|
||||||
def test_basic(self, testdir, hook):
|
def test_basic(self, testdir, hook):
|
||||||
"""
|
"""
|
||||||
Ensure we avoid calling imp.find_module when we know for sure a certain module will not be rewritten
|
Ensure we avoid calling PathFinder.find_spec when we know for sure a certain
|
||||||
to optimize assertion rewriting (#3918).
|
module will not be rewritten to optimize assertion rewriting (#3918).
|
||||||
"""
|
"""
|
||||||
testdir.makeconftest(
|
testdir.makeconftest(
|
||||||
"""
|
"""
|
||||||
|
@ -1244,24 +1271,24 @@ class TestEarlyRewriteBailout:
|
||||||
self.initial_paths.add(foobar_path)
|
self.initial_paths.add(foobar_path)
|
||||||
|
|
||||||
# conftest files should always be rewritten
|
# conftest files should always be rewritten
|
||||||
assert hook.find_module("conftest") is not None
|
assert hook.find_spec("conftest") is not None
|
||||||
assert self.find_module_calls == ["conftest"]
|
assert self.find_spec_calls == ["conftest"]
|
||||||
|
|
||||||
# files matching "python_files" mask should always be rewritten
|
# files matching "python_files" mask should always be rewritten
|
||||||
assert hook.find_module("test_foo") is not None
|
assert hook.find_spec("test_foo") is not None
|
||||||
assert self.find_module_calls == ["conftest", "test_foo"]
|
assert self.find_spec_calls == ["conftest", "test_foo"]
|
||||||
|
|
||||||
# file does not match "python_files": early bailout
|
# file does not match "python_files": early bailout
|
||||||
assert hook.find_module("bar") is None
|
assert hook.find_spec("bar") is None
|
||||||
assert self.find_module_calls == ["conftest", "test_foo"]
|
assert self.find_spec_calls == ["conftest", "test_foo"]
|
||||||
|
|
||||||
# file is an initial path (passed on the command-line): should be rewritten
|
# file is an initial path (passed on the command-line): should be rewritten
|
||||||
assert hook.find_module("foobar") is not None
|
assert hook.find_spec("foobar") is not None
|
||||||
assert self.find_module_calls == ["conftest", "test_foo", "foobar"]
|
assert self.find_spec_calls == ["conftest", "test_foo", "foobar"]
|
||||||
|
|
||||||
def test_pattern_contains_subdirectories(self, testdir, hook):
|
def test_pattern_contains_subdirectories(self, testdir, hook):
|
||||||
"""If one of the python_files patterns contain subdirectories ("tests/**.py") we can't bailout early
|
"""If one of the python_files patterns contain subdirectories ("tests/**.py") we can't bailout early
|
||||||
because we need to match with the full path, which can only be found by calling imp.find_module.
|
because we need to match with the full path, which can only be found by calling PathFinder.find_spec
|
||||||
"""
|
"""
|
||||||
p = testdir.makepyfile(
|
p = testdir.makepyfile(
|
||||||
**{
|
**{
|
||||||
|
@ -1273,8 +1300,8 @@ class TestEarlyRewriteBailout:
|
||||||
)
|
)
|
||||||
testdir.syspathinsert(p.dirpath())
|
testdir.syspathinsert(p.dirpath())
|
||||||
hook.fnpats[:] = ["tests/**.py"]
|
hook.fnpats[:] = ["tests/**.py"]
|
||||||
assert hook.find_module("file") is not None
|
assert hook.find_spec("file") is not None
|
||||||
assert self.find_module_calls == ["file"]
|
assert self.find_spec_calls == ["file"]
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
sys.platform.startswith("win32"), reason="cannot remove cwd on Windows"
|
sys.platform.startswith("win32"), reason="cannot remove cwd on Windows"
|
||||||
|
|
|
@ -735,7 +735,7 @@ def test_capture_badoutput_issue412(testdir):
|
||||||
assert 0
|
assert 0
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = testdir.runpytest("--cap=fd")
|
result = testdir.runpytest("--capture=fd")
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
"""
|
"""
|
||||||
*def test_func*
|
*def test_func*
|
||||||
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
def test_enabled(testdir):
|
||||||
|
"""Test single crashing test displays a traceback."""
|
||||||
|
testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
import faulthandler
|
||||||
|
def test_crash():
|
||||||
|
faulthandler._sigabrt()
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = testdir.runpytest_subprocess()
|
||||||
|
result.stderr.fnmatch_lines(["*Fatal Python error*"])
|
||||||
|
assert result.ret != 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_crash_near_exit(testdir):
|
||||||
|
"""Test that fault handler displays crashes that happen even after
|
||||||
|
pytest is exiting (for example, when the interpreter is shutting down).
|
||||||
|
"""
|
||||||
|
testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
import faulthandler
|
||||||
|
import atexit
|
||||||
|
def test_ok():
|
||||||
|
atexit.register(faulthandler._sigabrt)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = testdir.runpytest_subprocess()
|
||||||
|
result.stderr.fnmatch_lines(["*Fatal Python error*"])
|
||||||
|
assert result.ret != 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_disabled(testdir):
|
||||||
|
"""Test option to disable fault handler in the command line.
|
||||||
|
"""
|
||||||
|
testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
import faulthandler
|
||||||
|
def test_disabled():
|
||||||
|
assert not faulthandler.is_enabled()
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = testdir.runpytest_subprocess("-p", "no:faulthandler")
|
||||||
|
result.stdout.fnmatch_lines(["*1 passed*"])
|
||||||
|
assert result.ret == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("enabled", [True, False])
|
||||||
|
def test_timeout(testdir, enabled):
|
||||||
|
"""Test option to dump tracebacks after a certain timeout.
|
||||||
|
If faulthandler is disabled, no traceback will be dumped.
|
||||||
|
"""
|
||||||
|
testdir.makepyfile(
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
def test_timeout():
|
||||||
|
time.sleep(2.0)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
testdir.makeini(
|
||||||
|
"""
|
||||||
|
[pytest]
|
||||||
|
faulthandler_timeout = 1
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
args = ["-p", "no:faulthandler"] if not enabled else []
|
||||||
|
|
||||||
|
result = testdir.runpytest_subprocess(*args)
|
||||||
|
tb_output = "most recent call first"
|
||||||
|
if sys.version_info[:2] == (3, 3):
|
||||||
|
tb_output = "Thread"
|
||||||
|
if enabled:
|
||||||
|
result.stderr.fnmatch_lines(["*%s*" % tb_output])
|
||||||
|
else:
|
||||||
|
assert tb_output not in result.stderr.str()
|
||||||
|
result.stdout.fnmatch_lines(["*1 passed*"])
|
||||||
|
assert result.ret == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("hook_name", ["pytest_enter_pdb", "pytest_exception_interact"])
|
||||||
|
def test_cancel_timeout_on_hook(monkeypatch, pytestconfig, hook_name):
|
||||||
|
"""Make sure that we are cancelling any scheduled traceback dumping due
|
||||||
|
to timeout before entering pdb (pytest-dev/pytest-faulthandler#12) or any other interactive
|
||||||
|
exception (pytest-dev/pytest-faulthandler#14).
|
||||||
|
"""
|
||||||
|
import faulthandler
|
||||||
|
from _pytest import faulthandler as plugin_module
|
||||||
|
|
||||||
|
called = []
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
faulthandler, "cancel_dump_traceback_later", lambda: called.append(1)
|
||||||
|
)
|
||||||
|
|
||||||
|
# call our hook explicitly, we can trust that pytest will call the hook
|
||||||
|
# for us at the appropriate moment
|
||||||
|
hook_func = getattr(plugin_module, hook_name)
|
||||||
|
hook_func()
|
||||||
|
assert called == [1]
|
|
@ -200,7 +200,7 @@ class TestParser:
|
||||||
|
|
||||||
def test_drop_short_helper(self):
|
def test_drop_short_helper(self):
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
formatter_class=parseopt.DropShorterLongHelpFormatter
|
formatter_class=parseopt.DropShorterLongHelpFormatter, allow_abbrev=False
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-t", "--twoword", "--duo", "--two-word", "--two", help="foo"
|
"-t", "--twoword", "--duo", "--two-word", "--two", help="foo"
|
||||||
|
@ -239,10 +239,8 @@ class TestParser:
|
||||||
parser.addoption("--funcarg", "--func-arg", action="store_true")
|
parser.addoption("--funcarg", "--func-arg", action="store_true")
|
||||||
parser.addoption("--abc-def", "--abc-def", action="store_true")
|
parser.addoption("--abc-def", "--abc-def", action="store_true")
|
||||||
parser.addoption("--klm-hij", action="store_true")
|
parser.addoption("--klm-hij", action="store_true")
|
||||||
args = parser.parse(["--funcarg", "--k"])
|
with pytest.raises(UsageError):
|
||||||
assert args.funcarg is True
|
parser.parse(["--funcarg", "--k"])
|
||||||
assert args.abc_def is False
|
|
||||||
assert args.klm_hij is True
|
|
||||||
|
|
||||||
def test_drop_short_2(self, parser):
|
def test_drop_short_2(self, parser):
|
||||||
parser.addoption("--func-arg", "--doit", action="store_true")
|
parser.addoption("--func-arg", "--doit", action="store_true")
|
||||||
|
|
|
@ -21,7 +21,7 @@ class TestPasteCapture:
|
||||||
pytest.skip("")
|
pytest.skip("")
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
reprec = testdir.inline_run(testpath, "--paste=failed")
|
reprec = testdir.inline_run(testpath, "--pastebin=failed")
|
||||||
assert len(pastebinlist) == 1
|
assert len(pastebinlist) == 1
|
||||||
s = pastebinlist[0]
|
s = pastebinlist[0]
|
||||||
assert s.find("def test_fail") != -1
|
assert s.find("def test_fail") != -1
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import os.path
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import py
|
import py
|
||||||
|
@ -53,6 +54,10 @@ class TestPort:
|
||||||
def test_matching(self, match, pattern, path):
|
def test_matching(self, match, pattern, path):
|
||||||
assert match(pattern, path)
|
assert match(pattern, path)
|
||||||
|
|
||||||
|
def test_matching_abspath(self, match):
|
||||||
|
abspath = os.path.abspath(os.path.join("tests/foo.py"))
|
||||||
|
assert match("tests/foo.py", abspath)
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"pattern, path",
|
"pattern, path",
|
||||||
[
|
[
|
||||||
|
|
Loading…
Reference in New Issue