Merge remote-tracking branch 'upstream/master' into features-assertion-pass-hook-master

# Conflicts:
#	src/_pytest/assertion/rewrite.py
This commit is contained in:
Victor Maryama 2019-06-26 18:12:56 +02:00
commit 6f851e6cbb
37 changed files with 606 additions and 285 deletions

View File

@ -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

View File

@ -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``.

View File

@ -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>`__.

View File

@ -0,0 +1 @@
Switch from ``imp`` to ``importlib``.

View File

@ -0,0 +1,2 @@
The name of the ``.pyc`` files cached by the assertion writer now includes the pytest version
to avoid stale caches.

View File

@ -0,0 +1 @@
Honor PEP 235 on case-insensitive file systems.

View File

@ -0,0 +1 @@
Test module is no longer double-imported when using ``--pyargs``.

17
changelog/5260.bugfix.rst Normal file
View File

@ -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

View File

@ -0,0 +1 @@
Prevent "already imported" warnings from assertion rewriter when invoking pytest in-process multiple times.

View File

@ -0,0 +1 @@
Fix assertion rewriting in packages (``__init__.py``).

View File

@ -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

View File

@ -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``.

View File

@ -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

View File

@ -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
---------------------------------------------------- ----------------------------------------------------

View File

@ -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 = []

View File

@ -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"

View File

@ -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, "

View File

@ -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

View File

@ -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'

View File

@ -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()

View File

@ -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. """

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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,

View File

@ -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",

View File

@ -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

View File

@ -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'])
""" """
} }
) )

View File

@ -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():

View File

@ -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)

View File

@ -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

View File

@ -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"

View File

@ -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*

View File

@ -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]

View File

@ -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")

View File

@ -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

View File

@ -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",
[ [