diff --git a/AUTHORS b/AUTHORS index 3d050a346..087fce8d0 100644 --- a/AUTHORS +++ b/AUTHORS @@ -135,6 +135,7 @@ Kale Kundert Katarzyna Jachim Katerina Koukiou Kevin Cox +Kevin J. Foley Kodi B. Arfer Kostis Anagnostopoulos Kristoffer Nordström diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 57628a34b..ec053c081 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -173,7 +173,7 @@ Short version 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 ``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 @@ -264,7 +264,7 @@ Here is a simple overview, with pytest-specific bits: $ git commit -a -m "" $ git push -u -#. Create a new changelog entry in ``changelog``. The file should be named ``.``, +#. Create a new changelog entry in ``changelog``. The file should be named ``..rst``, where *issueid* is the number of the issue related to the change and *type* is one of ``bugfix``, ``removal``, ``feature``, ``vendor``, ``doc`` or ``trivial``. diff --git a/changelog/1149.removal.rst b/changelog/1149.removal.rst new file mode 100644 index 000000000..f507014d9 --- /dev/null +++ b/changelog/1149.removal.rst @@ -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 `__, +`#3413 `__, and +`#4009 `__. diff --git a/changelog/1403.bugfix.rst b/changelog/1403.bugfix.rst new file mode 100644 index 000000000..3fb748aec --- /dev/null +++ b/changelog/1403.bugfix.rst @@ -0,0 +1 @@ +Switch from ``imp`` to ``importlib``. diff --git a/changelog/1671.bugfix.rst b/changelog/1671.bugfix.rst new file mode 100644 index 000000000..c46eac828 --- /dev/null +++ b/changelog/1671.bugfix.rst @@ -0,0 +1,2 @@ +The name of the ``.pyc`` files cached by the assertion writer now includes the pytest version +to avoid stale caches. diff --git a/changelog/2761.bugfix.rst b/changelog/2761.bugfix.rst new file mode 100644 index 000000000..c63f02ecd --- /dev/null +++ b/changelog/2761.bugfix.rst @@ -0,0 +1 @@ +Honor PEP 235 on case-insensitive file systems. diff --git a/changelog/5078.bugfix.rst b/changelog/5078.bugfix.rst new file mode 100644 index 000000000..8fed85f5d --- /dev/null +++ b/changelog/5078.bugfix.rst @@ -0,0 +1 @@ +Test module is no longer double-imported when using ``--pyargs``. diff --git a/changelog/5260.bugfix.rst b/changelog/5260.bugfix.rst new file mode 100644 index 000000000..484c1438a --- /dev/null +++ b/changelog/5260.bugfix.rst @@ -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 diff --git a/changelog/5432.bugfix.rst b/changelog/5432.bugfix.rst new file mode 100644 index 000000000..44c01c0cf --- /dev/null +++ b/changelog/5432.bugfix.rst @@ -0,0 +1 @@ +Prevent "already imported" warnings from assertion rewriter when invoking pytest in-process multiple times. diff --git a/changelog/5433.bugfix.rst b/changelog/5433.bugfix.rst new file mode 100644 index 000000000..c3a7472bc --- /dev/null +++ b/changelog/5433.bugfix.rst @@ -0,0 +1 @@ +Fix assertion rewriting in packages (``__init__.py``). diff --git a/changelog/5440.feature.rst b/changelog/5440.feature.rst new file mode 100644 index 000000000..d3bb95f58 --- /dev/null +++ b/changelog/5440.feature.rst @@ -0,0 +1,8 @@ +The `faulthandler `__ 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 `__ 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 diff --git a/changelog/5482.bugfix.rst b/changelog/5482.bugfix.rst new file mode 100644 index 000000000..c345458d1 --- /dev/null +++ b/changelog/5482.bugfix.rst @@ -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``. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 3b1d3f262..6750b17f0 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1084,6 +1084,23 @@ passed multiple times. The expected format is ``name=value``. For example:: 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 diff --git a/doc/en/usage.rst b/doc/en/usage.rst index 0d464e207..74cfaa178 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -410,7 +410,6 @@ Pytest supports the use of ``breakpoint()`` with the following behaviours: Profiling test execution duration ------------------------------------- -.. versionadded: 2.2 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. + +.. _faulthandler: + +Fault Handler +------------- + +.. versionadded:: 5.0 + +The `faulthandler `__ 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` 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 `__ 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 ---------------------------------------------------- diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 68fe8fd09..2afe76b82 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -2,20 +2,19 @@ import ast import astor import errno -import imp +import importlib.machinery +import importlib.util import itertools import marshal import os -import re import struct import sys import types -from importlib.util import spec_from_file_location import atomicwrites -import py from _pytest._io.saferepr import saferepr +from _pytest._version import version from _pytest.assertion import util from _pytest.assertion.util import ( # noqa: F401 format_explanation as _format_explanation, @@ -24,23 +23,13 @@ from _pytest.pathlib import fnmatch_ex from _pytest.pathlib import PurePath # pytest caches rewritten pycs in __pycache__. -if hasattr(imp, "get_tag"): - 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 - +PYTEST_TAG = "{}-pytest-{}".format(sys.implementation.cache_tag, version) PYC_EXT = ".py" + (__debug__ and "c" or "o") PYC_TAIL = "." + PYTEST_TAG + PYC_EXT class AssertionRewritingHook: - """PEP302 Import hook which rewrites asserts.""" + """PEP302/PEP451 import hook which rewrites asserts.""" def __init__(self, config): self.config = config @@ -49,7 +38,6 @@ class AssertionRewritingHook: except ValueError: self.fnpats = ["test_*.py", "*_test.py"] self.session = None - self.modules = {} self._rewritten_names = set() self._must_rewrite = set() # 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_paths_checked = False - def _imp_find_module(self, name, path=None): - """Indirection so we can mock calls to find_module originated from the hook during testing""" - return imp.find_module(name, path) + # Indirection so we can mock calls to find_spec originated from the hook during testing + _find_spec = importlib.machinery.PathFinder.find_spec - def find_module(self, name, path=None): + def find_spec(self, name, path=None, target=None): if self._writing_pyc: return None state = self.config._assertstate if self._early_rewrite_bailout(name, state): return None 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) - if not self._should_rewrite(name, fn_pypath, state): + spec = self._find_spec(name, path) + 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 - 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 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 # atomic. POSIX's atomic rename comes in handy. 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: try: os.mkdir(cache_dir) @@ -133,26 +119,23 @@ class AssertionRewritingHook: # common case) or it's blocked by a non-dir node. In the # latter case, we'll ignore it in _write_pyc. 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 # because we're in a zip file. write = False - elif e in [errno.EACCES, errno.EROFS, errno.EPERM]: - state.trace("read only directory: %r" % fn_pypath.dirname) + elif e in {errno.EACCES, errno.EROFS, errno.EPERM}: + state.trace("read only directory: %r" % os.path.dirname(fn)) write = False else: 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) # 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... - co = _read_pyc(fn_pypath, pyc, state.trace) + co = _read_pyc(fn, pyc, state.trace) if co is None: state.trace("rewriting {!r}".format(fn)) - source_stat, co = _rewrite_test(self.config, fn_pypath) - if co is None: - # Probably a SyntaxError in the test. - return None + source_stat, co = _rewrite_test(fn) if write: self._writing_pyc = True try: @@ -161,13 +144,11 @@ class AssertionRewritingHook: self._writing_pyc = False else: state.trace("found cached rewritten pyc for {!r}".format(fn)) - self.modules[name] = co, pyc - return self + exec(co, module.__dict__) def _early_rewrite_bailout(self, name, state): - """ - This is a fast way to get out of rewriting modules. Profiling has - shown that the call to imp.find_module (inside of the find_module + """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 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. """ @@ -202,10 +183,9 @@ class AssertionRewritingHook: state.trace("early skip of rewriting module: {}".format(name)) return True - def _should_rewrite(self, name, fn_pypath, state): + def _should_rewrite(self, name, fn, state): # always rewrite conftest files - fn = str(fn_pypath) - if fn_pypath.basename == "conftest.py": + if os.path.basename(fn) == "conftest.py": state.trace("rewriting conftest file: {!r}".format(fn)) return True @@ -218,8 +198,9 @@ class AssertionRewritingHook: # modules not passed explicitly on the command line are only # rewritten if they match the naming convention for test files + fn_path = PurePath(fn) for pat in self.fnpats: - if fn_pypath.fnmatch(pat): + if fnmatch_ex(pat, fn_path): state.trace("matched test file {!r}".format(fn)) return True @@ -250,9 +231,10 @@ class AssertionRewritingHook: set(names).intersection(sys.modules).difference(self._rewritten_names) ) for name in already_imported: + mod = sys.modules[name] 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._must_rewrite.update(names) self._marked_for_rewrite_cache.clear() @@ -269,45 +251,8 @@ class AssertionRewritingHook: 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): - """Optional PEP302 get_data API. - """ + """Optional PEP302 get_data API.""" with open(pathname, "rb") as f: return f.read() @@ -315,15 +260,13 @@ class AssertionRewritingHook: def _write_pyc(state, co, source_stat, pyc): # Technically, we don't have to have the same pyc format as # (C)Python, since these "pycs" should never be seen by builtin - # import. However, there's little reason deviate, and I hope - # sometime to be able to use imp.load_compiled to load them. (See - # the comment in load_module above.) + # import. However, there's little reason deviate. try: 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) - mtime = int(source_stat.mtime) & 0xFFFFFFFF - size = source_stat.size & 0xFFFFFFFF + mtime = int(source_stat.st_mtime) & 0xFFFFFFFF + size = source_stat.st_size & 0xFFFFFFFF # ">> 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 += [ - "At index {} diff: {!r} != {!r}".format(i, left[i], right[i]) + "At index {} diff: {!r} != {!r}".format(i, left_value, right_value) ] 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 > 0: dir_with_more = "Left" diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index e6de86c36..c1bd2e7eb 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -140,6 +140,7 @@ default_plugins = essential_plugins + ( "warnings", "logging", "reports", + "faulthandler", ) builtin_plugins = set(default_plugins) @@ -288,7 +289,7 @@ class PytestPluginManager(PluginManager): return opts def register(self, plugin, name=None): - if name in ["pytest_catchlog", "pytest_capturelog"]: + if name in _pytest.deprecated.DEPRECATED_EXTERNAL_PLUGINS: warnings.warn( PytestConfigWarning( "{} plugin has been merged into the core, " diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index fb36c7985..d62ed0d03 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -1,5 +1,7 @@ import argparse +import sys import warnings +from gettext import gettext import py @@ -328,6 +330,7 @@ class MyOptionParser(argparse.ArgumentParser): usage=parser._usage, add_help=False, formatter_class=DropShorterLongHelpFormatter, + allow_abbrev=False, ) # extra_info is a dict of (param -> value) to display if there's # 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) 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): """shorten help for long options that differ only in extra hyphens diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 3feae8b43..1c544fd36 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -14,6 +14,14 @@ from _pytest.warning_types import UnformattedWarning 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 "{name}" called directly. Fixtures are not meant to be called directly,\n' diff --git a/src/_pytest/faulthandler.py b/src/_pytest/faulthandler.py new file mode 100644 index 000000000..068bec528 --- /dev/null +++ b/src/_pytest/faulthandler.py @@ -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() diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 118fc784a..73648557e 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -2,8 +2,8 @@ import enum import fnmatch import functools +import importlib import os -import pkgutil import sys import warnings @@ -630,21 +630,15 @@ class Session(nodes.FSCollector): def _tryconvertpyarg(self, x): """Convert a dotted module name to path.""" try: - loader = pkgutil.find_loader(x) - except ImportError: + spec = importlib.util.find_spec(x) + except (ValueError, ImportError): return x - if loader is None: + if spec is None or spec.origin in {None, "namespace"}: return x - # This method is sometimes invoked when AssertionRewritingHook, which - # does not define a get_filename method, is already in place: - try: - path = loader.get_filename(x) - 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 + elif spec.submodule_search_locations: + return os.path.dirname(spec.origin) + else: + return spec.origin def _parsearg(self, arg): """ return (fspath, names) tuple after checking the file exists. """ diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 39cdb57e4..1af7a9b42 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -102,10 +102,7 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): return cls(parameterset, marks=[], id=None) @staticmethod - def _parse_parametrize_args(argnames, argvalues, **_): - """It receives an ignored _ (kwargs) argument so this function can - take also calls from parametrize ignoring scope, indirect, and other - arguments...""" + def _parse_parametrize_args(argnames, argvalues, *args, **kwargs): if not isinstance(argnames, (tuple, list)): argnames = [x.strip() for x in argnames.split(",") if x.strip()] force_tuple = len(argnames) == 1 diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index 749e80f3c..fb4d471b5 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -149,7 +149,6 @@ def importorskip(modname, minversion=None, reason=None): __tracebackhide__ = True compile(modname, "", "eval") # to catch syntaxerrors - import_exc = None with warnings.catch_warnings(): # make sure to ignore ImportWarnings that might happen because @@ -159,12 +158,9 @@ def importorskip(modname, minversion=None, reason=None): try: __import__(modname) except ImportError as exc: - # Do not raise chained exception here(#1485) - import_exc = exc - if import_exc: - if reason is None: - reason = "could not import {!r}: {}".format(modname, import_exc) - raise Skipped(reason, allow_module_level=True) + if reason is None: + reason = "could not import {!r}: {}".format(modname, exc) + raise Skipped(reason, allow_module_level=True) from None mod = sys.modules[modname] if minversion is None: return mod diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 3269c25ed..ecc38eb0f 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -294,6 +294,8 @@ def fnmatch_ex(pattern, path): name = path.name else: name = str(path) + if path.is_absolute() and not os.path.isabs(pattern): + pattern = "*{}{}".format(os.sep, pattern) return fnmatch.fnmatch(name, pattern) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 6b304ad9f..2068761fa 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1,5 +1,6 @@ """(disabled by default) support for testing pytest and pytest plugins.""" import gc +import importlib import os import platform import re @@ -16,7 +17,6 @@ import py import pytest from _pytest._code import Source from _pytest._io.saferepr import saferepr -from _pytest.assertion.rewrite import AssertionRewritingHook from _pytest.capture import MultiCapture from _pytest.capture import SysCapture from _pytest.main import ExitCode @@ -787,6 +787,11 @@ class Testdir: :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) finalizers = [] try: @@ -796,18 +801,6 @@ class Testdir: mp_run.setenv(k, v) 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 # inline should be reverted after the test run completes to avoid # clashing with later inline tests run within the same pytest test, diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 91e373852..bcd6e1f7c 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -76,8 +76,7 @@ def pytest_addoption(parser): help="show extra test summary info as specified by chars: (f)ailed, " "(E)rror, (s)kipped, (x)failed, (X)passed, " "(p)assed, (P)assed with output, (a)ll except passed (p/P), or (A)ll. " - "Warnings are displayed at all times except when " - "--disable-warnings is set.", + "(w)arnings are enabled by default (see --disable-warnings).", ) group._addoption( "--disable-warnings", diff --git a/src/pytest.py b/src/pytest.py index a3fa26084..b4faf4978 100644 --- a/src/pytest.py +++ b/src/pytest.py @@ -2,7 +2,6 @@ """ pytest: unit and functional testing with Python. """ -# else we are imported from _pytest import __version__ from _pytest.assertion import register_assert_rewrite from _pytest.config import cmdline diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 3f339366e..dbdf048a4 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -633,6 +633,19 @@ class TestInvocationVariants: 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): import warnings @@ -983,7 +996,7 @@ def test_zipimport_hook(testdir, tmpdir): "app/foo.py": """ import pytest def main(): - pytest.main(['--pyarg', 'foo']) + pytest.main(['--pyargs', 'foo']) """ } ) diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 177594c4a..5cbb694b1 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -1,6 +1,7 @@ import os import pytest +from _pytest import deprecated from _pytest.warning_types import PytestDeprecationWarning 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 -@pytest.mark.parametrize("plugin", ["catchlog", "capturelog"]) +@pytest.mark.parametrize("plugin", deprecated.DEPRECATED_EXTERNAL_PLUGINS) @pytest.mark.filterwarnings("default") -def test_pytest_catchlog_deprecated(testdir, plugin): - testdir.makepyfile( - """ - def test_func(pytestconfig): - pytestconfig.pluginmanager.register(None, 'pytest_{}') - """.format( - 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_external_plugins_integrated(testdir, plugin): + testdir.syspathinsert() + testdir.makepyfile(**{plugin: ""}) + + with pytest.warns(pytest.PytestConfigWarning): + testdir.parseconfig("-p", plugin) def test_raises_message_argument_deprecated(): diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 6b26be72b..df93d4ef5 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -1761,3 +1761,16 @@ class TestMarkersWithParametrization: result.stdout.fnmatch_lines( ["*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) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index eced5461a..f58d240a5 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -137,8 +137,8 @@ class TestImportHookInstallation: "hamster.py": "", "test_foo.py": """\ def test_foo(pytestconfig): - assert pytestconfig.pluginmanager.rewrite_hook.find_module('ham') is not None - assert pytestconfig.pluginmanager.rewrite_hook.find_module('hamster') is None + assert pytestconfig.pluginmanager.rewrite_hook.find_spec('ham') is not None + assert pytestconfig.pluginmanager.rewrite_hook.find_spec('hamster') is None """, } testdir.makepyfile(**contents) @@ -331,6 +331,27 @@ class TestAssert_reprcompare: assert "- spam" 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): expl = callequal([0, 1], [0, 2]) assert len(expl) > 1 diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 129eca680..0c01be28c 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -1,5 +1,6 @@ import ast import glob +import importlib import os import py_compile import stat @@ -117,6 +118,37 @@ class TestAssertionRewrite: result = testdir.runpytest_subprocess() 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 f(): assert False @@ -748,6 +780,24 @@ def test_rewritten(): 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') def test_pyc_vs_pyo(self, testdir, monkeypatch): testdir.makepyfile( @@ -831,8 +881,9 @@ def test_rewritten(): monkeypatch.setattr( hook, "_warn_already_imported", lambda code, msg: warnings.append(msg) ) - hook.find_module("test_remember_rewritten_modules") - hook.load_module("test_remember_rewritten_modules") + spec = hook.find_spec("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") assert warnings == [] @@ -872,33 +923,6 @@ def test_rewritten(): 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): testdir.makepyfile( """ @@ -917,7 +941,7 @@ class TestAssertionRewriteHookDetails: state = AssertionState(config, "rewrite") source_path = tmpdir.ensure("source.py") 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 def atomic_write_failed(fn, mode="r", overwrite=False): @@ -979,7 +1003,7 @@ class TestAssertionRewriteHookDetails: assert len(contents) > strip_bytes 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): # 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 write_pyc_called.append(True) # 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) monkeypatch.setattr(rewrite, "_write_pyc", spy_write_pyc) monkeypatch.setattr(sys, "dont_write_bytecode", False) 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 @@ -1201,11 +1228,11 @@ class TestEarlyRewriteBailout: @pytest.fixture def hook(self, pytestconfig, monkeypatch, testdir): """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() class StubSession: @@ -1214,22 +1241,22 @@ class TestEarlyRewriteBailout: def isinitpath(self, p): return p in self._initialpaths - def spy_imp_find_module(name, path): - self.find_module_calls.append(name) - return imp.find_module(name, path) + def spy_find_spec(name, path): + self.find_spec_calls.append(name) + return importlib.machinery.PathFinder.find_spec(name, path) hook = AssertionRewritingHook(pytestconfig) # use default patterns, otherwise we inherit pytest's testing config 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()) testdir.syspathinsert() return 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 - to optimize assertion rewriting (#3918). + Ensure we avoid calling PathFinder.find_spec when we know for sure a certain + module will not be rewritten to optimize assertion rewriting (#3918). """ testdir.makeconftest( """ @@ -1244,24 +1271,24 @@ class TestEarlyRewriteBailout: self.initial_paths.add(foobar_path) # conftest files should always be rewritten - assert hook.find_module("conftest") is not None - assert self.find_module_calls == ["conftest"] + assert hook.find_spec("conftest") is not None + assert self.find_spec_calls == ["conftest"] # files matching "python_files" mask should always be rewritten - assert hook.find_module("test_foo") is not None - assert self.find_module_calls == ["conftest", "test_foo"] + assert hook.find_spec("test_foo") is not None + assert self.find_spec_calls == ["conftest", "test_foo"] # file does not match "python_files": early bailout - assert hook.find_module("bar") is None - assert self.find_module_calls == ["conftest", "test_foo"] + assert hook.find_spec("bar") is None + assert self.find_spec_calls == ["conftest", "test_foo"] # file is an initial path (passed on the command-line): should be rewritten - assert hook.find_module("foobar") is not None - assert self.find_module_calls == ["conftest", "test_foo", "foobar"] + assert hook.find_spec("foobar") is not None + assert self.find_spec_calls == ["conftest", "test_foo", "foobar"] def test_pattern_contains_subdirectories(self, testdir, hook): """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( **{ @@ -1273,8 +1300,8 @@ class TestEarlyRewriteBailout: ) testdir.syspathinsert(p.dirpath()) hook.fnpats[:] = ["tests/**.py"] - assert hook.find_module("file") is not None - assert self.find_module_calls == ["file"] + assert hook.find_spec("file") is not None + assert self.find_spec_calls == ["file"] @pytest.mark.skipif( sys.platform.startswith("win32"), reason="cannot remove cwd on Windows" diff --git a/testing/test_capture.py b/testing/test_capture.py index 8d1d33bc7..f5b193597 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -735,7 +735,7 @@ def test_capture_badoutput_issue412(testdir): assert 0 """ ) - result = testdir.runpytest("--cap=fd") + result = testdir.runpytest("--capture=fd") result.stdout.fnmatch_lines( """ *def test_func* diff --git a/testing/test_faulthandler.py b/testing/test_faulthandler.py new file mode 100644 index 000000000..a0cf1d8c1 --- /dev/null +++ b/testing/test_faulthandler.py @@ -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] diff --git a/testing/test_parseopt.py b/testing/test_parseopt.py index 7c581cce1..dd7bc8753 100644 --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -200,7 +200,7 @@ class TestParser: def test_drop_short_helper(self): parser = argparse.ArgumentParser( - formatter_class=parseopt.DropShorterLongHelpFormatter + formatter_class=parseopt.DropShorterLongHelpFormatter, allow_abbrev=False ) parser.add_argument( "-t", "--twoword", "--duo", "--two-word", "--two", help="foo" @@ -239,10 +239,8 @@ class TestParser: parser.addoption("--funcarg", "--func-arg", action="store_true") parser.addoption("--abc-def", "--abc-def", action="store_true") parser.addoption("--klm-hij", action="store_true") - args = parser.parse(["--funcarg", "--k"]) - assert args.funcarg is True - assert args.abc_def is False - assert args.klm_hij is True + with pytest.raises(UsageError): + parser.parse(["--funcarg", "--k"]) def test_drop_short_2(self, parser): parser.addoption("--func-arg", "--doit", action="store_true") diff --git a/testing/test_pastebin.py b/testing/test_pastebin.py index 48dea14bd..fd443ed40 100644 --- a/testing/test_pastebin.py +++ b/testing/test_pastebin.py @@ -21,7 +21,7 @@ class TestPasteCapture: pytest.skip("") """ ) - reprec = testdir.inline_run(testpath, "--paste=failed") + reprec = testdir.inline_run(testpath, "--pastebin=failed") assert len(pastebinlist) == 1 s = pastebinlist[0] assert s.find("def test_fail") != -1 diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index 8ac404070..45daeaed7 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -1,3 +1,4 @@ +import os.path import sys import py @@ -53,6 +54,10 @@ class TestPort: def test_matching(self, 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( "pattern, path", [