Compare commits
49 Commits
Author | SHA1 | Date |
---|---|---|
|
c934de6e1b | |
|
5ff9bf4ea0 | |
|
6702741978 | |
|
03bf27f4df | |
|
48f787ecff | |
|
b322004047 | |
|
2d795dc07b | |
|
2d6b846978 | |
|
703d0f50d8 | |
|
56e6482405 | |
|
589176e9fe | |
|
3734a27733 | |
|
e1a21e46b0 | |
|
551400e8d6 | |
|
b7b729298c | |
|
21ca38b932 | |
|
565f4cb4ad | |
|
af6548a4e7 | |
|
2fb2962df4 | |
|
f838c7b7eb | |
|
25b53c4196 | |
|
fc27171d57 | |
|
d18e426b24 | |
|
e83fa48dd1 | |
|
c53d52c3f2 | |
|
3886c6d735 | |
|
80936b6762 | |
|
5ca08e9b8a | |
|
ba2c49e71e | |
|
da615a4fbe | |
|
32562881d4 | |
|
2a0bbfe63f | |
|
9aed656ec7 | |
|
a600e7a2a4 | |
|
57a95b3a2c | |
|
f5e430fd8f | |
|
38a4c7e56c | |
|
40f02d72b0 | |
|
554f600fb4 | |
|
e3a3c90d94 | |
|
f7327759e8 | |
|
ee1950af77 | |
|
3d0f3baa2b | |
|
8da758b93a | |
|
59e5d1bfbf | |
|
b9e2cd0a81 | |
|
a84fcbf5b2 | |
|
59c1bfada7 | |
|
3267f64724 |
|
@ -39,6 +39,7 @@ jobs:
|
|||
"ubuntu-py37-pluggy",
|
||||
"ubuntu-py37-freeze",
|
||||
"ubuntu-py38",
|
||||
"ubuntu-py39",
|
||||
"ubuntu-pypy3",
|
||||
|
||||
"macos-py37",
|
||||
|
@ -62,7 +63,7 @@ jobs:
|
|||
- name: "windows-py37"
|
||||
python: "3.7"
|
||||
os: windows-latest
|
||||
tox_env: "py37-twisted-numpy"
|
||||
tox_env: "py37-numpy"
|
||||
- name: "windows-py37-pluggy"
|
||||
python: "3.7"
|
||||
os: windows-latest
|
||||
|
@ -70,7 +71,7 @@ jobs:
|
|||
- name: "windows-py38"
|
||||
python: "3.8"
|
||||
os: windows-latest
|
||||
tox_env: "py38"
|
||||
tox_env: "py38-unittestextras"
|
||||
use_coverage: true
|
||||
|
||||
- name: "ubuntu-py35"
|
||||
|
@ -84,7 +85,7 @@ jobs:
|
|||
- name: "ubuntu-py37"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py37-lsof-numpy-oldattrs-pexpect-twisted"
|
||||
tox_env: "py37-lsof-numpy-oldattrs-pexpect"
|
||||
use_coverage: true
|
||||
- name: "ubuntu-py37-pluggy"
|
||||
python: "3.7"
|
||||
|
@ -98,6 +99,10 @@ jobs:
|
|||
python: "3.8"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py38-xdist"
|
||||
- name: "ubuntu-py39"
|
||||
python: "3.8"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py39-xdist"
|
||||
- name: "ubuntu-pypy3"
|
||||
python: "pypy3"
|
||||
os: ubuntu-latest
|
||||
|
@ -133,6 +138,12 @@ jobs:
|
|||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
- name: install python3.9
|
||||
if: matrix.tox_env == 'py39-xdist'
|
||||
run: |
|
||||
sudo add-apt-repository ppa:deadsnakes/nightly
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends python3.9-dev python3.9-distutils
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
|
|
1
AUTHORS
1
AUTHORS
|
@ -267,6 +267,7 @@ Tom Dalton
|
|||
Tom Viner
|
||||
Tomáš Gavenčiak
|
||||
Tomer Keren
|
||||
Tor Colvin
|
||||
Trevor Bekolay
|
||||
Tyler Goodlet
|
||||
Tzu-ping Chung
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
Fix a possible race condition when trying to remove lock files used to control access to folders
|
||||
created by ``tmp_path`` and ``tmpdir``.
|
|
@ -1 +1,6 @@
|
|||
comment: off
|
||||
# reference: https://docs.codecov.io/docs/codecovyml-reference
|
||||
coverage:
|
||||
status:
|
||||
patch: true
|
||||
project: false
|
||||
comment: false
|
||||
|
|
|
@ -6,6 +6,9 @@ Release announcements
|
|||
:maxdepth: 2
|
||||
|
||||
|
||||
release-5.4.3
|
||||
release-5.4.2
|
||||
release-5.4.1
|
||||
release-5.4.0
|
||||
release-5.3.5
|
||||
release-5.3.4
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
pytest-5.4.1
|
||||
=======================================
|
||||
|
||||
pytest 5.4.1 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Bruno Oliveira
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
|
@ -0,0 +1,22 @@
|
|||
pytest-5.4.2
|
||||
=======================================
|
||||
|
||||
pytest 5.4.2 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Anthony Sottile
|
||||
* Bruno Oliveira
|
||||
* Daniel Hahler
|
||||
* Ran Benita
|
||||
* Ronny Pfannschmidt
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
|
@ -0,0 +1,21 @@
|
|||
pytest-5.4.3
|
||||
=======================================
|
||||
|
||||
pytest 5.4.3 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Anthony Sottile
|
||||
* Bruno Oliveira
|
||||
* Ran Benita
|
||||
* Tor Colvin
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
|
@ -28,6 +28,79 @@ with advance notice in the **Deprecations** section of releases.
|
|||
|
||||
.. towncrier release notes start
|
||||
|
||||
pytest 5.4.3 (2020-06-02)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#6428 <https://github.com/pytest-dev/pytest/issues/6428>`_: Paths appearing in error messages are now correct in case the current working directory has
|
||||
changed since the start of the session.
|
||||
|
||||
|
||||
- `#6755 <https://github.com/pytest-dev/pytest/issues/6755>`_: Support deleting paths longer than 260 characters on windows created inside tmpdir.
|
||||
|
||||
|
||||
- `#6956 <https://github.com/pytest-dev/pytest/issues/6956>`_: Prevent pytest from printing ConftestImportFailure traceback to stdout.
|
||||
|
||||
|
||||
- `#7150 <https://github.com/pytest-dev/pytest/issues/7150>`_: Prevent hiding the underlying exception when ``ConfTestImportFailure`` is raised.
|
||||
|
||||
|
||||
- `#7215 <https://github.com/pytest-dev/pytest/issues/7215>`_: Fix regression where running with ``--pdb`` would call the ``tearDown`` methods of ``unittest.TestCase``
|
||||
subclasses for skipped tests.
|
||||
|
||||
|
||||
pytest 5.4.2 (2020-05-08)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#6871 <https://github.com/pytest-dev/pytest/issues/6871>`_: Fix crash with captured output when using the :fixture:`capsysbinary fixture <capsysbinary>`.
|
||||
|
||||
|
||||
- `#6924 <https://github.com/pytest-dev/pytest/issues/6924>`_: Ensure a ``unittest.IsolatedAsyncioTestCase`` is actually awaited.
|
||||
|
||||
|
||||
- `#6925 <https://github.com/pytest-dev/pytest/issues/6925>`_: Fix TerminalRepr instances to be hashable again.
|
||||
|
||||
|
||||
- `#6947 <https://github.com/pytest-dev/pytest/issues/6947>`_: Fix regression where functions registered with ``TestCase.addCleanup`` were not being called on test failures.
|
||||
|
||||
|
||||
- `#6951 <https://github.com/pytest-dev/pytest/issues/6951>`_: Allow users to still set the deprecated ``TerminalReporter.writer`` attribute.
|
||||
|
||||
|
||||
- `#6992 <https://github.com/pytest-dev/pytest/issues/6992>`_: Revert "tmpdir: clean up indirection via config for factories" #6767 as it breaks pytest-xdist.
|
||||
|
||||
|
||||
- `#7110 <https://github.com/pytest-dev/pytest/issues/7110>`_: Fixed regression: ``asyncbase.TestCase`` tests are executed correctly again.
|
||||
|
||||
|
||||
- `#7143 <https://github.com/pytest-dev/pytest/issues/7143>`_: Fix ``File.from_constructor`` so it forwards extra keyword arguments to the constructor.
|
||||
|
||||
|
||||
- `#7145 <https://github.com/pytest-dev/pytest/issues/7145>`_: Classes with broken ``__getattribute__`` methods are displayed correctly during failures.
|
||||
|
||||
|
||||
- `#7180 <https://github.com/pytest-dev/pytest/issues/7180>`_: Fix ``_is_setup_py`` for files encoded differently than locale.
|
||||
|
||||
|
||||
pytest 5.4.1 (2020-03-13)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#6909 <https://github.com/pytest-dev/pytest/issues/6909>`_: Revert the change introduced by `#6330 <https://github.com/pytest-dev/pytest/pull/6330>`_, which required all arguments to ``@pytest.mark.parametrize`` to be explicitly defined in the function signature.
|
||||
|
||||
The intention of the original change was to remove what was expected to be an unintended/surprising behavior, but it turns out many people relied on it, so the restriction has been reverted.
|
||||
|
||||
|
||||
- `#6910 <https://github.com/pytest-dev/pytest/issues/6910>`_: Fix crash when plugins return an unknown stats while using the ``--reportlog`` option.
|
||||
|
||||
|
||||
pytest 5.4.0 (2020-03-12)
|
||||
=========================
|
||||
|
||||
|
|
|
@ -402,9 +402,6 @@ The result of this test will be successful:
|
|||
|
||||
.. regendoc:wipe
|
||||
|
||||
Note, that each argument in `parametrize` list should be explicitly declared in corresponding
|
||||
python test function or via `indirect`.
|
||||
|
||||
Parametrizing test methods through per-class configuration
|
||||
--------------------------------------------------------------
|
||||
|
||||
|
@ -484,11 +481,10 @@ Running it results in some skips if we don't have all the python interpreters in
|
|||
.. code-block:: pytest
|
||||
|
||||
. $ pytest -rs -q multipython.py
|
||||
ssssssssssss...ssssssssssss [100%]
|
||||
ssssssssssss......sss...... [100%]
|
||||
========================= short test summary info ==========================
|
||||
SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:29: 'python3.5' not found
|
||||
SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:29: 'python3.7' not found
|
||||
3 passed, 24 skipped in 0.12s
|
||||
SKIPPED [15] $REGENDOC_TMPDIR/CWD/multipython.py:29: 'python3.5' not found
|
||||
12 passed, 15 skipped in 0.12s
|
||||
|
||||
Indirect parametrization of optional implementations/imports
|
||||
--------------------------------------------------------------------
|
||||
|
|
|
@ -443,9 +443,9 @@ additionally it is possible to copy examples for an example folder before runnin
|
|||
testdir.copy_example("test_example.py")
|
||||
|
||||
test_example.py::test_plugin
|
||||
$PYTHON_PREFIX/lib/python3.8/site-packages/_pytest/terminal.py:287: PytestDeprecationWarning: TerminalReporter.writer attribute is deprecated, use TerminalReporter._tw instead at your own risk.
|
||||
$PYTHON_PREFIX/lib/python3.8/site-packages/_pytest/compat.py:333: PytestDeprecationWarning: The TerminalReporter.writer attribute is deprecated, use TerminalReporter._tw instead at your own risk.
|
||||
See https://docs.pytest.org/en/latest/deprecations.html#terminalreporter-writer for more information.
|
||||
warnings.warn(
|
||||
return getattr(object, name, default)
|
||||
|
||||
-- Docs: https://docs.pytest.org/en/latest/warnings.html
|
||||
====================== 2 passed, 2 warnings in 0.12s =======================
|
||||
|
|
|
@ -126,7 +126,9 @@ def trigger_release(payload_path: Path, token: str) -> None:
|
|||
|
||||
print(f"Branch {Fore.CYAN}{release_branch}{Fore.RESET} created.")
|
||||
|
||||
check_call([sys.executable, "scripts/release.py", version])
|
||||
check_call(
|
||||
[sys.executable, "scripts/release.py", version, "--skip-check-links"]
|
||||
)
|
||||
|
||||
oauth_url = f"https://{token}:x-oauth-basic@github.com/{SLUG}.git"
|
||||
check_call(["git", "push", oauth_url, f"HEAD:{release_branch}", "--force"])
|
||||
|
|
|
@ -26,6 +26,7 @@ classifiers =
|
|||
Programming Language :: Python :: 3.6
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
platforms = unix, linux, osx, cygwin, win32
|
||||
|
||||
[options]
|
||||
|
|
|
@ -32,6 +32,7 @@ import _pytest
|
|||
from _pytest._io import TerminalWriter
|
||||
from _pytest._io.saferepr import safeformat
|
||||
from _pytest._io.saferepr import saferepr
|
||||
from _pytest.compat import ATTRS_EQ_FIELD
|
||||
from _pytest.compat import overload
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
|
||||
|
@ -911,7 +912,7 @@ class FormattedExcinfo:
|
|||
return ExceptionChainRepr(repr_chain)
|
||||
|
||||
|
||||
@attr.s
|
||||
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||
class TerminalRepr:
|
||||
def __str__(self) -> str:
|
||||
# FYI this is called from pytest-xdist's serialization of exception
|
||||
|
@ -928,7 +929,7 @@ class TerminalRepr:
|
|||
raise NotImplementedError()
|
||||
|
||||
|
||||
@attr.s
|
||||
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||
class ExceptionRepr(TerminalRepr):
|
||||
def __attrs_post_init__(self):
|
||||
self.sections = [] # type: List[Tuple[str, str, str]]
|
||||
|
@ -942,7 +943,7 @@ class ExceptionRepr(TerminalRepr):
|
|||
tw.line(content)
|
||||
|
||||
|
||||
@attr.s
|
||||
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||
class ExceptionChainRepr(ExceptionRepr):
|
||||
chain = attr.ib(
|
||||
type=Sequence[
|
||||
|
@ -966,7 +967,7 @@ class ExceptionChainRepr(ExceptionRepr):
|
|||
super().toterminal(tw)
|
||||
|
||||
|
||||
@attr.s
|
||||
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||
class ReprExceptionInfo(ExceptionRepr):
|
||||
reprtraceback = attr.ib(type="ReprTraceback")
|
||||
reprcrash = attr.ib(type="ReprFileLocation")
|
||||
|
@ -976,7 +977,7 @@ class ReprExceptionInfo(ExceptionRepr):
|
|||
super().toterminal(tw)
|
||||
|
||||
|
||||
@attr.s
|
||||
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||
class ReprTraceback(TerminalRepr):
|
||||
reprentries = attr.ib(type=Sequence[Union["ReprEntry", "ReprEntryNative"]])
|
||||
extraline = attr.ib(type=Optional[str])
|
||||
|
@ -1010,7 +1011,7 @@ class ReprTracebackNative(ReprTraceback):
|
|||
self.extraline = None
|
||||
|
||||
|
||||
@attr.s
|
||||
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||
class ReprEntryNative(TerminalRepr):
|
||||
lines = attr.ib(type=Sequence[str])
|
||||
style = "native" # type: _TracebackStyle
|
||||
|
@ -1019,7 +1020,7 @@ class ReprEntryNative(TerminalRepr):
|
|||
tw.write("".join(self.lines))
|
||||
|
||||
|
||||
@attr.s
|
||||
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||
class ReprEntry(TerminalRepr):
|
||||
lines = attr.ib(type=Sequence[str])
|
||||
reprfuncargs = attr.ib(type=Optional["ReprFuncArgs"])
|
||||
|
@ -1093,7 +1094,7 @@ class ReprEntry(TerminalRepr):
|
|||
)
|
||||
|
||||
|
||||
@attr.s
|
||||
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||
class ReprFileLocation(TerminalRepr):
|
||||
path = attr.ib(type=str, converter=str)
|
||||
lineno = attr.ib(type=int)
|
||||
|
@ -1110,7 +1111,7 @@ class ReprFileLocation(TerminalRepr):
|
|||
tw.line(":{}: {}".format(self.lineno, msg))
|
||||
|
||||
|
||||
@attr.s
|
||||
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||
class ReprLocals(TerminalRepr):
|
||||
lines = attr.ib(type=Sequence[str])
|
||||
|
||||
|
@ -1119,7 +1120,7 @@ class ReprLocals(TerminalRepr):
|
|||
tw.line(indent + line)
|
||||
|
||||
|
||||
@attr.s
|
||||
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||
class ReprFuncArgs(TerminalRepr):
|
||||
args = attr.ib(type=Sequence[Tuple[str, object]])
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ def _format_repr_exception(exc: BaseException, obj: Any) -> str:
|
|||
except BaseException as exc:
|
||||
exc_info = "unpresentable exception ({})".format(_try_repr_or_str(exc))
|
||||
return "<[{} raised in repr()] {} object at 0x{:x}>".format(
|
||||
exc_info, obj.__class__.__name__, id(obj)
|
||||
exc_info, type(obj).__name__, id(obj)
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -610,8 +610,6 @@ class FDCaptureBinary:
|
|||
|
||||
def writeorg(self, data):
|
||||
""" write to original file descriptor. """
|
||||
if isinstance(data, str):
|
||||
data = data.encode("utf8") # XXX use encoding of original stream
|
||||
os.write(self.targetfd_save, data)
|
||||
|
||||
|
||||
|
@ -631,6 +629,11 @@ class FDCapture(FDCaptureBinary):
|
|||
res = str(res, enc, "replace")
|
||||
return res
|
||||
|
||||
def writeorg(self, data):
|
||||
""" write to original file descriptor. """
|
||||
data = data.encode("utf-8") # XXX use encoding of original stream
|
||||
os.write(self.targetfd_save, data)
|
||||
|
||||
|
||||
class SysCaptureBinary:
|
||||
|
||||
|
@ -682,8 +685,9 @@ class SysCaptureBinary:
|
|||
self._state = "resumed"
|
||||
|
||||
def writeorg(self, data):
|
||||
self._old.write(data)
|
||||
self._old.flush()
|
||||
self._old.buffer.write(data)
|
||||
self._old.buffer.flush()
|
||||
|
||||
|
||||
class SysCapture(SysCaptureBinary):
|
||||
|
@ -695,6 +699,10 @@ class SysCapture(SysCaptureBinary):
|
|||
self.tmpfile.truncate()
|
||||
return res
|
||||
|
||||
def writeorg(self, data):
|
||||
self._old.write(data)
|
||||
self._old.flush()
|
||||
|
||||
|
||||
class TeeSysCapture(SysCapture):
|
||||
def __init__(self, fd, tmpfile=None):
|
||||
|
|
|
@ -93,6 +93,13 @@ def iscoroutinefunction(func: object) -> bool:
|
|||
return inspect.iscoroutinefunction(func) or getattr(func, "_is_coroutine", False)
|
||||
|
||||
|
||||
def is_async_function(func: object) -> bool:
|
||||
"""Return True if the given function seems to be an async function or async generator"""
|
||||
return iscoroutinefunction(func) or (
|
||||
sys.version_info >= (3, 6) and inspect.isasyncgenfunction(func)
|
||||
)
|
||||
|
||||
|
||||
def getlocation(function, curdir=None) -> str:
|
||||
function = get_real_func(function)
|
||||
fn = py.path.local(inspect.getfile(function))
|
||||
|
|
|
@ -4,6 +4,7 @@ import functools
|
|||
import sys
|
||||
|
||||
from _pytest import outcomes
|
||||
from _pytest.config import ConftestImportFailure
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.config.exceptions import UsageError
|
||||
|
||||
|
@ -272,11 +273,15 @@ class PdbInvoke:
|
|||
class PdbTrace:
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_pyfunc_call(self, pyfuncitem):
|
||||
_test_pytest_function(pyfuncitem)
|
||||
wrap_pytest_function_for_tracing(pyfuncitem)
|
||||
yield
|
||||
|
||||
|
||||
def _test_pytest_function(pyfuncitem):
|
||||
def wrap_pytest_function_for_tracing(pyfuncitem):
|
||||
"""Changes the python function object of the given Function item by a wrapper which actually
|
||||
enters pdb before calling the python function itself, effectively leaving the user
|
||||
in the pdb prompt in the first statement of the function.
|
||||
"""
|
||||
_pdb = pytestPDB._init_pdb("runcall")
|
||||
testfunction = pyfuncitem.obj
|
||||
|
||||
|
@ -291,6 +296,13 @@ def _test_pytest_function(pyfuncitem):
|
|||
pyfuncitem.obj = wrapper
|
||||
|
||||
|
||||
def maybe_wrap_pytest_function_for_tracing(pyfuncitem):
|
||||
"""Wrap the given pytestfunct item for tracing support if --trace was given in
|
||||
the command line"""
|
||||
if pyfuncitem.config.getvalue("trace"):
|
||||
wrap_pytest_function_for_tracing(pyfuncitem)
|
||||
|
||||
|
||||
def _enter_pdb(node, excinfo, rep):
|
||||
# XXX we re-use the TerminalReporter's terminalwriter
|
||||
# because this seems to avoid some encoding related troubles
|
||||
|
@ -327,6 +339,10 @@ def _postmortem_traceback(excinfo):
|
|||
# A doctest.UnexpectedException is not useful for post_mortem.
|
||||
# Use the underlying exception instead:
|
||||
return excinfo.value.exc_info[2]
|
||||
elif isinstance(excinfo.value, ConftestImportFailure):
|
||||
# A config.ConftestImportFailure is not useful for post_mortem.
|
||||
# Use the underlying exception instead:
|
||||
return excinfo.value.excinfo[2]
|
||||
else:
|
||||
return excinfo._excinfo[2]
|
||||
|
||||
|
|
|
@ -54,3 +54,9 @@ COLLECT_DIRECTORY_HOOK = PytestDeprecationWarning(
|
|||
"The pytest_collect_directory hook is not working.\n"
|
||||
"Please use collect_ignore in conftests or pytest_collection_modifyitems."
|
||||
)
|
||||
|
||||
|
||||
TERMINALWRITER_WRITER = PytestDeprecationWarning(
|
||||
"The TerminalReporter.writer attribute is deprecated, use TerminalReporter._tw instead at your own risk.\n"
|
||||
"See https://docs.pytest.org/en/latest/deprecations.html#terminalreporter-writer for more information."
|
||||
)
|
||||
|
|
|
@ -108,20 +108,20 @@ def pytest_unconfigure():
|
|||
RUNNER_CLASS = None
|
||||
|
||||
|
||||
def pytest_collect_file(path, parent):
|
||||
def pytest_collect_file(path: py.path.local, parent):
|
||||
config = parent.config
|
||||
if path.ext == ".py":
|
||||
if config.option.doctestmodules and not _is_setup_py(config, path, parent):
|
||||
if config.option.doctestmodules and not _is_setup_py(path):
|
||||
return DoctestModule.from_parent(parent, fspath=path)
|
||||
elif _is_doctest(config, path, parent):
|
||||
return DoctestTextfile.from_parent(parent, fspath=path)
|
||||
|
||||
|
||||
def _is_setup_py(config, path, parent):
|
||||
def _is_setup_py(path: py.path.local) -> bool:
|
||||
if path.basename != "setup.py":
|
||||
return False
|
||||
contents = path.read()
|
||||
return "setuptools" in contents or "distutils" in contents
|
||||
contents = path.read_binary()
|
||||
return b"setuptools" in contents or b"distutils" in contents
|
||||
|
||||
|
||||
def _is_doctest(config, path, parent):
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import functools
|
||||
import inspect
|
||||
import itertools
|
||||
import sys
|
||||
import warnings
|
||||
from collections import defaultdict
|
||||
|
@ -1277,8 +1278,10 @@ class FixtureManager:
|
|||
else:
|
||||
argnames = ()
|
||||
|
||||
usefixtures = get_use_fixtures_for_node(node)
|
||||
initialnames = usefixtures + argnames
|
||||
usefixtures = itertools.chain.from_iterable(
|
||||
mark.args for mark in node.iter_markers(name="usefixtures")
|
||||
)
|
||||
initialnames = tuple(usefixtures) + argnames
|
||||
fm = node.session._fixturemanager
|
||||
initialnames, names_closure, arg2fixturedefs = fm.getfixtureclosure(
|
||||
initialnames, node, ignore_args=self._get_direct_parametrize_args(node)
|
||||
|
@ -1475,12 +1478,3 @@ class FixtureManager:
|
|||
for fixturedef in fixturedefs:
|
||||
if nodes.ischildnode(fixturedef.baseid, nodeid):
|
||||
yield fixturedef
|
||||
|
||||
|
||||
def get_use_fixtures_for_node(node) -> Tuple[str, ...]:
|
||||
"""Returns the names of all the usefixtures() marks on the given node"""
|
||||
return tuple(
|
||||
str(name)
|
||||
for mark in node.iter_markers(name="usefixtures")
|
||||
for name in mark.args
|
||||
)
|
||||
|
|
|
@ -13,7 +13,6 @@ import os
|
|||
import platform
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
import py
|
||||
|
@ -21,6 +20,7 @@ import py
|
|||
import pytest
|
||||
from _pytest import deprecated
|
||||
from _pytest import nodes
|
||||
from _pytest import timing
|
||||
from _pytest.config import filename_arg
|
||||
from _pytest.store import StoreKey
|
||||
from _pytest.warnings import _issue_warning_captured
|
||||
|
@ -627,14 +627,14 @@ class LogXML:
|
|||
reporter._add_simple(Junit.error, "internal error", excrepr)
|
||||
|
||||
def pytest_sessionstart(self):
|
||||
self.suite_start_time = time.time()
|
||||
self.suite_start_time = timing.time()
|
||||
|
||||
def pytest_sessionfinish(self):
|
||||
dirname = os.path.dirname(os.path.abspath(self.logfile))
|
||||
if not os.path.isdir(dirname):
|
||||
os.makedirs(dirname)
|
||||
logfile = open(self.logfile, "w", encoding="utf-8")
|
||||
suite_stop_time = time.time()
|
||||
suite_stop_time = timing.time()
|
||||
suite_time_delta = suite_stop_time - self.suite_start_time
|
||||
|
||||
numtests = (
|
||||
|
@ -662,7 +662,7 @@ class LogXML:
|
|||
logfile.close()
|
||||
|
||||
def pytest_terminal_summary(self, terminalreporter):
|
||||
terminalreporter.write_sep("-", "generated xml file: %s" % (self.logfile))
|
||||
terminalreporter.write_sep("-", "generated xml file: {}".format(self.logfile))
|
||||
|
||||
def add_global_property(self, name, value):
|
||||
__tracebackhide__ = True
|
||||
|
|
|
@ -19,6 +19,7 @@ from _pytest._code.source import getfslineno
|
|||
from _pytest.compat import cached_property
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import ConftestImportFailure
|
||||
from _pytest.config import PytestPluginManager
|
||||
from _pytest.deprecated import NODE_USE_FROM_PARENT
|
||||
from _pytest.fixtures import FixtureDef
|
||||
|
@ -28,7 +29,7 @@ from _pytest.mark.structures import Mark
|
|||
from _pytest.mark.structures import MarkDecorator
|
||||
from _pytest.mark.structures import NodeKeywords
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.outcomes import Failed
|
||||
from _pytest.pathlib import Path
|
||||
from _pytest.store import Store
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -318,8 +319,10 @@ class Node(metaclass=NodeMeta):
|
|||
pass
|
||||
|
||||
def _repr_failure_py(
|
||||
self, excinfo: ExceptionInfo[Union[Failed, FixtureLookupError]], style=None
|
||||
self, excinfo: ExceptionInfo[BaseException], style=None,
|
||||
) -> Union[str, ReprExceptionInfo, ExceptionChainRepr, FixtureLookupErrorRepr]:
|
||||
if isinstance(excinfo.value, ConftestImportFailure):
|
||||
excinfo = ExceptionInfo(excinfo.value.excinfo)
|
||||
if isinstance(excinfo.value, fail.Exception):
|
||||
if not excinfo.value.pytrace:
|
||||
return str(excinfo.value)
|
||||
|
@ -346,9 +349,14 @@ class Node(metaclass=NodeMeta):
|
|||
else:
|
||||
truncate_locals = True
|
||||
|
||||
# excinfo.getrepr() formats paths relative to the CWD if `abspath` is False.
|
||||
# It is possible for a fixture/test to change the CWD while this code runs, which
|
||||
# would then result in the user seeing confusing paths in the failure message.
|
||||
# To fix this, if the CWD changed, always display the full absolute path.
|
||||
# It will be better to just always display paths relative to invocation_dir, but
|
||||
# this requires a lot of plumbing (#6428).
|
||||
try:
|
||||
os.getcwd()
|
||||
abspath = False
|
||||
abspath = Path(os.getcwd()) != Path(self.config.invocation_dir)
|
||||
except OSError:
|
||||
abspath = True
|
||||
|
||||
|
@ -474,11 +482,11 @@ class FSCollector(Collector):
|
|||
self._norecursepatterns = self.config.getini("norecursedirs")
|
||||
|
||||
@classmethod
|
||||
def from_parent(cls, parent, *, fspath):
|
||||
def from_parent(cls, parent, *, fspath, **kw):
|
||||
"""
|
||||
The public constructor
|
||||
"""
|
||||
return super().from_parent(parent=parent, fspath=fspath)
|
||||
return super().from_parent(parent=parent, fspath=fspath, **kw)
|
||||
|
||||
def _gethookproxy(self, fspath: py.path.local):
|
||||
# check if we have the common case of running
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import atexit
|
||||
import contextlib
|
||||
import fnmatch
|
||||
import itertools
|
||||
import os
|
||||
|
@ -100,10 +101,41 @@ def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool:
|
|||
return True
|
||||
|
||||
|
||||
def ensure_extended_length_path(path: Path) -> Path:
|
||||
"""Get the extended-length version of a path (Windows).
|
||||
|
||||
On Windows, by default, the maximum length of a path (MAX_PATH) is 260
|
||||
characters, and operations on paths longer than that fail. But it is possible
|
||||
to overcome this by converting the path to "extended-length" form before
|
||||
performing the operation:
|
||||
https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation
|
||||
|
||||
On Windows, this function returns the extended-length absolute version of path.
|
||||
On other platforms it returns path unchanged.
|
||||
"""
|
||||
if sys.platform.startswith("win32"):
|
||||
path = path.resolve()
|
||||
path = Path(get_extended_length_path_str(str(path)))
|
||||
return path
|
||||
|
||||
|
||||
def get_extended_length_path_str(path: str) -> str:
|
||||
"""Converts to extended length path as a str"""
|
||||
long_path_prefix = "\\\\?\\"
|
||||
unc_long_path_prefix = "\\\\?\\UNC\\"
|
||||
if path.startswith((long_path_prefix, unc_long_path_prefix)):
|
||||
return path
|
||||
# UNC
|
||||
if path.startswith("\\\\"):
|
||||
return unc_long_path_prefix + path[2:]
|
||||
return long_path_prefix + path
|
||||
|
||||
|
||||
def rm_rf(path: Path) -> None:
|
||||
"""Remove the path contents recursively, even if some elements
|
||||
are read-only.
|
||||
"""
|
||||
path = ensure_extended_length_path(path)
|
||||
onerror = partial(on_rm_rf_error, start_path=path)
|
||||
shutil.rmtree(str(path), onerror=onerror)
|
||||
|
||||
|
@ -220,6 +252,7 @@ def register_cleanup_lock_removal(lock_path: Path, register=atexit.register):
|
|||
|
||||
def maybe_delete_a_numbered_dir(path: Path) -> None:
|
||||
"""removes a numbered directory if its lock can be obtained and it does not seem to be in use"""
|
||||
path = ensure_extended_length_path(path)
|
||||
lock_path = None
|
||||
try:
|
||||
lock_path = create_cleanup_lock(path)
|
||||
|
@ -257,9 +290,13 @@ def ensure_deletable(path: Path, consider_lock_dead_if_created_before: float) ->
|
|||
return False
|
||||
else:
|
||||
if lock_time < consider_lock_dead_if_created_before:
|
||||
# wa want to ignore any errors while trying to remove the lock such as:
|
||||
# - PermissionDenied, like the file permissions have changed since the lock creation
|
||||
# - FileNotFoundError, in case another pytest process got here first.
|
||||
# and any other cause of failure.
|
||||
with contextlib.suppress(OSError):
|
||||
lock.unlink()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ import platform
|
|||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
from fnmatch import fnmatch
|
||||
from io import StringIO
|
||||
|
@ -24,6 +23,7 @@ from weakref import WeakKeyDictionary
|
|||
import py
|
||||
|
||||
import pytest
|
||||
from _pytest import timing
|
||||
from _pytest._code import Source
|
||||
from _pytest.capture import MultiCapture
|
||||
from _pytest.capture import SysCapture
|
||||
|
@ -924,7 +924,7 @@ class Testdir:
|
|||
|
||||
if syspathinsert:
|
||||
self.syspathinsert()
|
||||
now = time.time()
|
||||
now = timing.time()
|
||||
capture = MultiCapture(Capture=SysCapture)
|
||||
capture.start_capturing()
|
||||
try:
|
||||
|
@ -953,7 +953,7 @@ class Testdir:
|
|||
sys.stderr.write(err)
|
||||
|
||||
res = RunResult(
|
||||
reprec.ret, out.splitlines(), err.splitlines(), time.time() - now
|
||||
reprec.ret, out.splitlines(), err.splitlines(), timing.time() - now
|
||||
)
|
||||
res.reprec = reprec # type: ignore
|
||||
return res
|
||||
|
@ -1154,7 +1154,7 @@ class Testdir:
|
|||
f1 = open(str(p1), "w", encoding="utf8")
|
||||
f2 = open(str(p2), "w", encoding="utf8")
|
||||
try:
|
||||
now = time.time()
|
||||
now = timing.time()
|
||||
popen = self.popen(
|
||||
cmdargs,
|
||||
stdin=stdin,
|
||||
|
@ -1201,7 +1201,7 @@ class Testdir:
|
|||
ret = ExitCode(ret)
|
||||
except ValueError:
|
||||
pass
|
||||
return RunResult(ret, out, err, time.time() - now)
|
||||
return RunResult(ret, out, err, timing.time() - now)
|
||||
|
||||
def _dump_lines(self, lines, fp):
|
||||
try:
|
||||
|
|
|
@ -34,8 +34,8 @@ from _pytest.compat import get_default_arg_names
|
|||
from _pytest.compat import get_real_func
|
||||
from _pytest.compat import getimfunc
|
||||
from _pytest.compat import getlocation
|
||||
from _pytest.compat import is_async_function
|
||||
from _pytest.compat import is_generator
|
||||
from _pytest.compat import iscoroutinefunction
|
||||
from _pytest.compat import NOTSET
|
||||
from _pytest.compat import REGEX_TYPE
|
||||
from _pytest.compat import safe_getattr
|
||||
|
@ -159,7 +159,7 @@ def pytest_configure(config):
|
|||
)
|
||||
|
||||
|
||||
def async_warn(nodeid: str) -> None:
|
||||
def async_warn_and_skip(nodeid: str) -> None:
|
||||
msg = "async def functions are not natively supported and have been skipped.\n"
|
||||
msg += (
|
||||
"You need to install a suitable plugin for your async framework, for example:\n"
|
||||
|
@ -175,15 +175,13 @@ def async_warn(nodeid: str) -> None:
|
|||
@hookimpl(trylast=True)
|
||||
def pytest_pyfunc_call(pyfuncitem: "Function"):
|
||||
testfunction = pyfuncitem.obj
|
||||
if iscoroutinefunction(testfunction) or (
|
||||
sys.version_info >= (3, 6) and inspect.isasyncgenfunction(testfunction)
|
||||
):
|
||||
async_warn(pyfuncitem.nodeid)
|
||||
if is_async_function(testfunction):
|
||||
async_warn_and_skip(pyfuncitem.nodeid)
|
||||
funcargs = pyfuncitem.funcargs
|
||||
testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames}
|
||||
result = testfunction(**testargs)
|
||||
if hasattr(result, "__await__") or hasattr(result, "__aiter__"):
|
||||
async_warn(pyfuncitem.nodeid)
|
||||
async_warn_and_skip(pyfuncitem.nodeid)
|
||||
return True
|
||||
|
||||
|
||||
|
@ -936,8 +934,6 @@ class Metafunc:
|
|||
|
||||
arg_values_types = self._resolve_arg_value_types(argnames, indirect)
|
||||
|
||||
self._validate_explicit_parameters(argnames, indirect)
|
||||
|
||||
# Use any already (possibly) generated ids with parametrize Marks.
|
||||
if _param_mark and _param_mark._param_ids_from:
|
||||
generated_ids = _param_mark._param_ids_from._param_ids_generated
|
||||
|
@ -1110,39 +1106,6 @@ class Metafunc:
|
|||
pytrace=False,
|
||||
)
|
||||
|
||||
def _validate_explicit_parameters(
|
||||
self,
|
||||
argnames: typing.Sequence[str],
|
||||
indirect: Union[bool, typing.Sequence[str]],
|
||||
) -> None:
|
||||
"""
|
||||
The argnames in *parametrize* should either be declared explicitly via
|
||||
indirect list or in the function signature
|
||||
|
||||
:param List[str] argnames: list of argument names passed to ``parametrize()``.
|
||||
:param indirect: same ``indirect`` parameter of ``parametrize()``.
|
||||
:raise ValueError: if validation fails
|
||||
"""
|
||||
if isinstance(indirect, bool):
|
||||
parametrized_argnames = [] if indirect else argnames
|
||||
else:
|
||||
parametrized_argnames = [arg for arg in argnames if arg not in indirect]
|
||||
|
||||
if not parametrized_argnames:
|
||||
return
|
||||
|
||||
funcargnames = _pytest.compat.getfuncargnames(self.function)
|
||||
usefixtures = fixtures.get_use_fixtures_for_node(self.definition)
|
||||
|
||||
for arg in parametrized_argnames:
|
||||
if arg not in funcargnames and arg not in usefixtures:
|
||||
func_name = self.function.__name__
|
||||
msg = (
|
||||
'In function "{func_name}":\n'
|
||||
'Parameter "{arg}" should be declared explicitly via indirect or in function itself'
|
||||
).format(func_name=func_name, arg=arg)
|
||||
fail(msg, pytrace=False)
|
||||
|
||||
|
||||
def _find_parametrized_scope(argnames, arg2fixturedefs, indirect):
|
||||
"""Find the most appropriate scope for a parametrized call based on its arguments.
|
||||
|
|
|
@ -77,10 +77,10 @@ class ResultLog:
|
|||
longrepr = ""
|
||||
elif report.passed:
|
||||
longrepr = ""
|
||||
elif report.failed:
|
||||
longrepr = str(report.longrepr)
|
||||
elif report.skipped:
|
||||
longrepr = str(report.longrepr[2])
|
||||
else:
|
||||
longrepr = str(report.longrepr)
|
||||
self.log_outcome(report, code, longrepr)
|
||||
|
||||
def pytest_collectreport(self, report):
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
import bdb
|
||||
import os
|
||||
import sys
|
||||
from time import time
|
||||
from typing import Callable
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
|
@ -14,6 +13,7 @@ import attr
|
|||
from .reports import CollectErrorRepr
|
||||
from .reports import CollectReport
|
||||
from .reports import TestReport
|
||||
from _pytest import timing
|
||||
from _pytest._code.code import ExceptionChainRepr
|
||||
from _pytest._code.code import ExceptionInfo
|
||||
from _pytest.compat import TYPE_CHECKING
|
||||
|
@ -238,8 +238,8 @@ class CallInfo:
|
|||
def from_call(cls, func, when, reraise=None) -> "CallInfo":
|
||||
#: context of invocation: one of "setup", "call",
|
||||
#: "teardown", "memocollect"
|
||||
start = time()
|
||||
excinfo = None
|
||||
start = timing.time()
|
||||
try:
|
||||
result = func()
|
||||
except: # noqa
|
||||
|
@ -247,7 +247,7 @@ class CallInfo:
|
|||
if reraise is not None and excinfo.errisinstance(reraise):
|
||||
raise
|
||||
result = None
|
||||
stop = time()
|
||||
stop = timing.time()
|
||||
return cls(start=start, stop=stop, when=when, result=result, excinfo=excinfo)
|
||||
|
||||
def __repr__(self):
|
||||
|
|
|
@ -7,7 +7,6 @@ import collections
|
|||
import datetime
|
||||
import platform
|
||||
import sys
|
||||
import time
|
||||
import warnings
|
||||
from functools import partial
|
||||
from typing import Any
|
||||
|
@ -26,9 +25,11 @@ from more_itertools import collapse
|
|||
|
||||
import pytest
|
||||
from _pytest import nodes
|
||||
from _pytest import timing
|
||||
from _pytest._io import TerminalWriter
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import ExitCode
|
||||
from _pytest.deprecated import TERMINALWRITER_WRITER
|
||||
from _pytest.main import Session
|
||||
from _pytest.reports import CollectReport
|
||||
from _pytest.reports import TestReport
|
||||
|
@ -284,14 +285,14 @@ class TerminalReporter:
|
|||
|
||||
@property
|
||||
def writer(self) -> TerminalWriter:
|
||||
warnings.warn(
|
||||
pytest.PytestDeprecationWarning(
|
||||
"TerminalReporter.writer attribute is deprecated, use TerminalReporter._tw instead at your own risk.\n"
|
||||
"See https://docs.pytest.org/en/latest/deprecations.html#terminalreporter-writer for more information."
|
||||
)
|
||||
)
|
||||
warnings.warn(TERMINALWRITER_WRITER, stacklevel=2)
|
||||
return self._tw
|
||||
|
||||
@writer.setter
|
||||
def writer(self, value: TerminalWriter):
|
||||
warnings.warn(TERMINALWRITER_WRITER, stacklevel=2)
|
||||
self._tw = value
|
||||
|
||||
def _determine_show_progress_info(self):
|
||||
"""Return True if we should display progress information based on the current config"""
|
||||
# do not show progress if we are not capturing output (#3038)
|
||||
|
@ -553,7 +554,7 @@ class TerminalReporter:
|
|||
if self.isatty:
|
||||
if self.config.option.verbose >= 0:
|
||||
self.write("collecting ... ", bold=True)
|
||||
self._collect_report_last_write = time.time()
|
||||
self._collect_report_last_write = timing.time()
|
||||
elif self.config.option.verbose >= 1:
|
||||
self.write("collecting ... ", bold=True)
|
||||
|
||||
|
@ -573,7 +574,7 @@ class TerminalReporter:
|
|||
|
||||
if not final:
|
||||
# Only write "collecting" report every 0.5s.
|
||||
t = time.time()
|
||||
t = timing.time()
|
||||
if (
|
||||
self._collect_report_last_write is not None
|
||||
and self._collect_report_last_write > t - REPORT_COLLECTING_RESOLUTION
|
||||
|
@ -610,7 +611,7 @@ class TerminalReporter:
|
|||
@pytest.hookimpl(trylast=True)
|
||||
def pytest_sessionstart(self, session: Session) -> None:
|
||||
self._session = session
|
||||
self._sessionstarttime = time.time()
|
||||
self._sessionstarttime = timing.time()
|
||||
if not self.showheader:
|
||||
return
|
||||
self.write_sep("=", "test session starts", bold=True)
|
||||
|
@ -959,7 +960,7 @@ class TerminalReporter:
|
|||
if self.verbosity < -1:
|
||||
return
|
||||
|
||||
session_duration = time.time() - self._sessionstarttime
|
||||
session_duration = timing.time() - self._sessionstarttime
|
||||
(parts, main_color) = self.build_summary_stats_line()
|
||||
line_parts = []
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
"""
|
||||
Indirection for time functions.
|
||||
|
||||
We intentionally grab some "time" functions internally to avoid tests mocking "time" to affect
|
||||
pytest runtime information (issue #185).
|
||||
|
||||
Fixture "mock_timinig" also interacts with this module for pytest's own tests.
|
||||
"""
|
||||
from time import perf_counter
|
||||
from time import sleep
|
||||
from time import time
|
||||
|
||||
__all__ = ["perf_counter", "sleep", "time"]
|
|
@ -14,6 +14,7 @@ from .pathlib import make_numbered_dir
|
|||
from .pathlib import make_numbered_dir_with_cleanup
|
||||
from .pathlib import Path
|
||||
from _pytest.fixtures import FixtureRequest
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
|
||||
|
||||
@attr.s
|
||||
|
@ -134,18 +135,35 @@ def get_user() -> Optional[str]:
|
|||
return None
|
||||
|
||||
|
||||
def pytest_configure(config) -> None:
|
||||
"""Create a TempdirFactory and attach it to the config object.
|
||||
|
||||
This is to comply with existing plugins which expect the handler to be
|
||||
available at pytest_configure time, but ideally should be moved entirely
|
||||
to the tmpdir_factory session fixture.
|
||||
"""
|
||||
mp = MonkeyPatch()
|
||||
tmppath_handler = TempPathFactory.from_config(config)
|
||||
t = TempdirFactory(tmppath_handler)
|
||||
config._cleanup.append(mp.undo)
|
||||
mp.setattr(config, "_tmp_path_factory", tmppath_handler, raising=False)
|
||||
mp.setattr(config, "_tmpdirhandler", t, raising=False)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def tmpdir_factory(tmp_path_factory) -> TempdirFactory:
|
||||
def tmpdir_factory(request: FixtureRequest) -> TempdirFactory:
|
||||
"""Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session.
|
||||
"""
|
||||
return TempdirFactory(tmp_path_factory)
|
||||
# Set dynamically by pytest_configure() above.
|
||||
return request.config._tmpdirhandler # type: ignore
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def tmp_path_factory(request: FixtureRequest) -> TempPathFactory:
|
||||
"""Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session.
|
||||
"""
|
||||
return TempPathFactory.from_config(request.config)
|
||||
# Set dynamically by pytest_configure() above.
|
||||
return request.config._tmp_path_factory # type: ignore
|
||||
|
||||
|
||||
def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path:
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
""" discovery and running of std-library "unittest" style tests. """
|
||||
import functools
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import _pytest._code
|
||||
import pytest
|
||||
from _pytest.compat import getimfunc
|
||||
from _pytest.compat import is_async_function
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.outcomes import exit
|
||||
from _pytest.outcomes import fail
|
||||
|
@ -41,7 +41,7 @@ class UnitTestCase(Class):
|
|||
if not getattr(cls, "__test__", True):
|
||||
return
|
||||
|
||||
skipped = getattr(cls, "__unittest_skip__", False)
|
||||
skipped = _is_skipped(cls)
|
||||
if not skipped:
|
||||
self._inject_setup_teardown_fixtures(cls)
|
||||
self._inject_setup_class_fixture()
|
||||
|
@ -89,7 +89,7 @@ def _make_xunit_fixture(obj, setup_name, teardown_name, scope, pass_self):
|
|||
|
||||
@pytest.fixture(scope=scope, autouse=True)
|
||||
def fixture(self, request):
|
||||
if getattr(self, "__unittest_skip__", None):
|
||||
if _is_skipped(self):
|
||||
reason = self.__unittest_skip_why__
|
||||
pytest.skip(reason)
|
||||
if setup is not None:
|
||||
|
@ -113,15 +113,17 @@ class TestCaseFunction(Function):
|
|||
_testcase = None
|
||||
|
||||
def setup(self):
|
||||
self._needs_explicit_tearDown = False
|
||||
# a bound method to be called during teardown() if set (see 'runtest()')
|
||||
self._explicit_tearDown = None
|
||||
self._testcase = self.parent.obj(self.name)
|
||||
self._obj = getattr(self._testcase, self.name)
|
||||
if hasattr(self, "_request"):
|
||||
self._request._fillfixtures()
|
||||
|
||||
def teardown(self):
|
||||
if self._needs_explicit_tearDown:
|
||||
self._testcase.tearDown()
|
||||
if self._explicit_tearDown is not None:
|
||||
self._explicit_tearDown()
|
||||
self._explicit_tearDown = None
|
||||
self._testcase = None
|
||||
self._obj = None
|
||||
|
||||
|
@ -204,36 +206,31 @@ class TestCaseFunction(Function):
|
|||
return bool(expecting_failure_class or expecting_failure_method)
|
||||
|
||||
def runtest(self):
|
||||
# TODO: move testcase reporter into separate class, this shouldnt be on item
|
||||
import unittest
|
||||
from _pytest.debugging import maybe_wrap_pytest_function_for_tracing
|
||||
|
||||
testMethod = getattr(self._testcase, self._testcase._testMethodName)
|
||||
maybe_wrap_pytest_function_for_tracing(self)
|
||||
|
||||
class _GetOutOf_testPartExecutor(KeyboardInterrupt):
|
||||
"""Helper exception to get out of unittests's testPartExecutor (see TestCase.run)."""
|
||||
# let the unittest framework handle async functions
|
||||
if is_async_function(self.obj):
|
||||
self._testcase(self)
|
||||
else:
|
||||
# when --pdb is given, we want to postpone calling tearDown() otherwise
|
||||
# when entering the pdb prompt, tearDown() would have probably cleaned up
|
||||
# instance variables, which makes it difficult to debug
|
||||
# arguably we could always postpone tearDown(), but this changes the moment where the
|
||||
# TestCase instance interacts with the results object, so better to only do it
|
||||
# when absolutely needed
|
||||
if self.config.getoption("usepdb") and not _is_skipped(self.obj):
|
||||
self._explicit_tearDown = self._testcase.tearDown
|
||||
setattr(self._testcase, "tearDown", lambda *args: None)
|
||||
|
||||
@functools.wraps(testMethod)
|
||||
def wrapped_testMethod(*args, **kwargs):
|
||||
"""Wrap the original method to call into pytest's machinery, so other pytest
|
||||
features can have a chance to kick in (notably --pdb)"""
|
||||
try:
|
||||
self.ihook.pytest_pyfunc_call(pyfuncitem=self)
|
||||
except unittest.SkipTest:
|
||||
raise
|
||||
except Exception as exc:
|
||||
expecting_failure = self._expecting_failure(testMethod)
|
||||
if expecting_failure:
|
||||
raise
|
||||
self._needs_explicit_tearDown = True
|
||||
raise _GetOutOf_testPartExecutor(exc)
|
||||
|
||||
setattr(self._testcase, self._testcase._testMethodName, wrapped_testMethod)
|
||||
# we need to update the actual bound method with self.obj, because
|
||||
# wrap_pytest_function_for_tracing replaces self.obj by a wrapper
|
||||
setattr(self._testcase, self.name, self.obj)
|
||||
try:
|
||||
self._testcase(result=self)
|
||||
except _GetOutOf_testPartExecutor as exc:
|
||||
raise exc.args[0] from exc.args[0]
|
||||
finally:
|
||||
delattr(self._testcase, self._testcase._testMethodName)
|
||||
delattr(self._testcase, self.name)
|
||||
|
||||
def _prunetraceback(self, excinfo):
|
||||
Function._prunetraceback(self, excinfo)
|
||||
|
@ -304,3 +301,8 @@ def check_testcase_implements_trial_reporter(done=[]):
|
|||
|
||||
classImplements(TestCaseFunction, IReporter)
|
||||
done.append(1)
|
||||
|
||||
|
||||
def _is_skipped(obj) -> bool:
|
||||
"""Return True if the given object has been marked with @unittest.skip"""
|
||||
return bool(getattr(obj, "__unittest_skip__", False))
|
||||
|
|
|
@ -895,21 +895,20 @@ class TestInvocationVariants:
|
|||
|
||||
class TestDurations:
|
||||
source = """
|
||||
import time
|
||||
frag = 0.002
|
||||
from _pytest import timing
|
||||
def test_something():
|
||||
pass
|
||||
def test_2():
|
||||
time.sleep(frag*5)
|
||||
timing.sleep(0.010)
|
||||
def test_1():
|
||||
time.sleep(frag)
|
||||
timing.sleep(0.002)
|
||||
def test_3():
|
||||
time.sleep(frag*10)
|
||||
timing.sleep(0.020)
|
||||
"""
|
||||
|
||||
def test_calls(self, testdir):
|
||||
def test_calls(self, testdir, mock_timing):
|
||||
testdir.makepyfile(self.source)
|
||||
result = testdir.runpytest("--durations=10")
|
||||
result = testdir.runpytest_inprocess("--durations=10")
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines_random(
|
||||
["*durations*", "*call*test_3*", "*call*test_2*"]
|
||||
|
@ -918,16 +917,17 @@ class TestDurations:
|
|||
["(0.00 durations hidden. Use -vv to show these durations.)"]
|
||||
)
|
||||
|
||||
def test_calls_show_2(self, testdir):
|
||||
def test_calls_show_2(self, testdir, mock_timing):
|
||||
|
||||
testdir.makepyfile(self.source)
|
||||
result = testdir.runpytest("--durations=2")
|
||||
result = testdir.runpytest_inprocess("--durations=2")
|
||||
assert result.ret == 0
|
||||
lines = result.stdout.get_lines_after("*slowest*durations*")
|
||||
assert "4 passed" in lines[2]
|
||||
|
||||
def test_calls_showall(self, testdir):
|
||||
def test_calls_showall(self, testdir, mock_timing):
|
||||
testdir.makepyfile(self.source)
|
||||
result = testdir.runpytest("--durations=0")
|
||||
result = testdir.runpytest_inprocess("--durations=0")
|
||||
assert result.ret == 0
|
||||
for x in "23":
|
||||
for y in ("call",): # 'setup', 'call', 'teardown':
|
||||
|
@ -937,9 +937,9 @@ class TestDurations:
|
|||
else:
|
||||
raise AssertionError("not found {} {}".format(x, y))
|
||||
|
||||
def test_calls_showall_verbose(self, testdir):
|
||||
def test_calls_showall_verbose(self, testdir, mock_timing):
|
||||
testdir.makepyfile(self.source)
|
||||
result = testdir.runpytest("--durations=0", "-vv")
|
||||
result = testdir.runpytest_inprocess("--durations=0", "-vv")
|
||||
assert result.ret == 0
|
||||
for x in "123":
|
||||
for y in ("call",): # 'setup', 'call', 'teardown':
|
||||
|
@ -949,13 +949,13 @@ class TestDurations:
|
|||
else:
|
||||
raise AssertionError("not found {} {}".format(x, y))
|
||||
|
||||
def test_with_deselected(self, testdir):
|
||||
def test_with_deselected(self, testdir, mock_timing):
|
||||
testdir.makepyfile(self.source)
|
||||
result = testdir.runpytest("--durations=2", "-k test_2")
|
||||
result = testdir.runpytest_inprocess("--durations=2", "-k test_2")
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines(["*durations*", "*call*test_2*"])
|
||||
|
||||
def test_with_failing_collection(self, testdir):
|
||||
def test_with_failing_collection(self, testdir, mock_timing):
|
||||
testdir.makepyfile(self.source)
|
||||
testdir.makepyfile(test_collecterror="""xyz""")
|
||||
result = testdir.runpytest("--durations=2", "-k test_1")
|
||||
|
@ -965,36 +965,35 @@ class TestDurations:
|
|||
# output
|
||||
result.stdout.no_fnmatch_line("*duration*")
|
||||
|
||||
def test_with_not(self, testdir):
|
||||
def test_with_not(self, testdir, mock_timing):
|
||||
testdir.makepyfile(self.source)
|
||||
result = testdir.runpytest("-k not 1")
|
||||
result = testdir.runpytest_inprocess("-k not 1")
|
||||
assert result.ret == 0
|
||||
|
||||
|
||||
class TestDurationWithFixture:
|
||||
source = """
|
||||
import pytest
|
||||
import time
|
||||
frag = 0.01
|
||||
from _pytest import timing
|
||||
|
||||
@pytest.fixture
|
||||
def setup_fixt():
|
||||
time.sleep(frag)
|
||||
timing.sleep(2)
|
||||
|
||||
def test_1(setup_fixt):
|
||||
time.sleep(frag)
|
||||
timing.sleep(5)
|
||||
"""
|
||||
|
||||
def test_setup_function(self, testdir):
|
||||
def test_setup_function(self, testdir, mock_timing):
|
||||
testdir.makepyfile(self.source)
|
||||
result = testdir.runpytest("--durations=10")
|
||||
result = testdir.runpytest_inprocess("--durations=10")
|
||||
assert result.ret == 0
|
||||
|
||||
result.stdout.fnmatch_lines_random(
|
||||
"""
|
||||
*durations*
|
||||
* setup *test_1*
|
||||
* call *test_1*
|
||||
5.00s call *test_1*
|
||||
2.00s setup *test_1*
|
||||
"""
|
||||
)
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import pytest
|
|||
from _pytest._code import Code
|
||||
from _pytest._code import ExceptionInfo
|
||||
from _pytest._code import Frame
|
||||
from _pytest._code.code import ExceptionChainRepr
|
||||
from _pytest._code.code import ReprFuncArgs
|
||||
|
||||
|
||||
|
@ -180,3 +181,20 @@ class TestReprFuncArgs:
|
|||
tw_mock.lines[0]
|
||||
== r"unicode_string = São Paulo, utf8_string = b'S\xc3\xa3o Paulo'"
|
||||
)
|
||||
|
||||
|
||||
def test_ExceptionChainRepr():
|
||||
"""Test ExceptionChainRepr, especially with regard to being hashable."""
|
||||
try:
|
||||
raise ValueError()
|
||||
except ValueError:
|
||||
excinfo1 = ExceptionInfo.from_current()
|
||||
excinfo2 = ExceptionInfo.from_current()
|
||||
|
||||
repr1 = excinfo1.getrepr()
|
||||
repr2 = excinfo2.getrepr()
|
||||
assert repr1 != repr2
|
||||
|
||||
assert isinstance(repr1, ExceptionChainRepr)
|
||||
assert hash(repr1) != hash(repr2)
|
||||
assert repr1 is not excinfo1.getrepr()
|
||||
|
|
|
@ -122,7 +122,8 @@ def test_syntaxerror_rerepresentation() -> None:
|
|||
assert ex is not None
|
||||
assert ex.value.lineno == 1
|
||||
assert ex.value.offset in {5, 7} # cpython: 7, pypy3.6 7.1.1: 5
|
||||
assert ex.value.text == "xyz xyz\n"
|
||||
assert ex.value.text
|
||||
assert ex.value.text.rstrip("\n") == "xyz xyz"
|
||||
|
||||
|
||||
def test_isparseable() -> None:
|
||||
|
@ -521,7 +522,7 @@ def test_getfslineno() -> None:
|
|||
class B:
|
||||
pass
|
||||
|
||||
B.__name__ = "B2"
|
||||
B.__name__ = B.__qualname__ = "B2"
|
||||
assert getfslineno(B)[1] == -1
|
||||
|
||||
co = compile("...", "", "eval")
|
||||
|
|
|
@ -195,3 +195,41 @@ def color_mapping():
|
|||
pytest.skip("doing limited testing because lacking ordered markup")
|
||||
|
||||
return ColorMapping
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_timing(monkeypatch):
|
||||
"""Mocks _pytest.timing with a known object that can be used to control timing in tests
|
||||
deterministically.
|
||||
|
||||
pytest itself should always use functions from `_pytest.timing` instead of `time` directly.
|
||||
|
||||
This then allows us more control over time during testing, if testing code also
|
||||
uses `_pytest.timing` functions.
|
||||
|
||||
Time is static, and only advances through `sleep` calls, thus tests might sleep over large
|
||||
numbers and obtain accurate time() calls at the end, making tests reliable and instant.
|
||||
"""
|
||||
import attr
|
||||
|
||||
@attr.s
|
||||
class MockTiming:
|
||||
|
||||
_current_time = attr.ib(default=1590150050.0)
|
||||
|
||||
def sleep(self, seconds):
|
||||
self._current_time += seconds
|
||||
|
||||
def time(self):
|
||||
return self._current_time
|
||||
|
||||
def patch(self):
|
||||
from _pytest import timing
|
||||
|
||||
monkeypatch.setattr(timing, "sleep", self.sleep)
|
||||
monkeypatch.setattr(timing, "time", self.time)
|
||||
monkeypatch.setattr(timing, "perf_counter", self.time)
|
||||
|
||||
result = MockTiming()
|
||||
result.patch()
|
||||
return result
|
||||
|
|
|
@ -36,8 +36,15 @@ def test_terminal_reporter_writer_attr(pytestconfig):
|
|||
except ImportError:
|
||||
pass
|
||||
terminal_reporter = pytestconfig.pluginmanager.get_plugin("terminalreporter")
|
||||
expected_tw = terminal_reporter._tw
|
||||
|
||||
with pytest.warns(pytest.PytestDeprecationWarning):
|
||||
assert terminal_reporter.writer is terminal_reporter._tw
|
||||
assert terminal_reporter.writer is expected_tw
|
||||
|
||||
with pytest.warns(pytest.PytestDeprecationWarning):
|
||||
terminal_reporter.writer = expected_tw
|
||||
|
||||
assert terminal_reporter._tw is expected_tw
|
||||
|
||||
|
||||
@pytest.mark.parametrize("plugin", sorted(deprecated.DEPRECATED_EXTERNAL_PLUGINS))
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
[pytest]
|
||||
# dummy pytest.ini to ease direct running of example scripts
|
|
@ -0,0 +1,24 @@
|
|||
from unittest import IsolatedAsyncioTestCase # type: ignore
|
||||
|
||||
|
||||
teardowns = []
|
||||
|
||||
|
||||
class AsyncArguments(IsolatedAsyncioTestCase):
|
||||
async def asyncTearDown(self):
|
||||
teardowns.append(None)
|
||||
|
||||
async def test_something_async(self):
|
||||
async def addition(x, y):
|
||||
return x + y
|
||||
|
||||
self.assertEqual(await addition(2, 2), 4)
|
||||
|
||||
async def test_something_async_fails(self):
|
||||
async def addition(x, y):
|
||||
return x + y
|
||||
|
||||
self.assertEqual(await addition(2, 2), 3)
|
||||
|
||||
def test_teardowns(self):
|
||||
assert len(teardowns) == 2
|
|
@ -0,0 +1,22 @@
|
|||
"""Issue #7110"""
|
||||
import asyncio
|
||||
|
||||
import asynctest
|
||||
|
||||
|
||||
teardowns = []
|
||||
|
||||
|
||||
class Test(asynctest.TestCase):
|
||||
async def tearDown(self):
|
||||
teardowns.append(None)
|
||||
|
||||
async def test_error(self):
|
||||
await asyncio.sleep(0)
|
||||
self.fail("failing on purpose")
|
||||
|
||||
async def test_ok(self):
|
||||
await asyncio.sleep(0)
|
||||
|
||||
def test_teardowns(self):
|
||||
assert len(teardowns) == 2
|
|
@ -154,3 +154,20 @@ def test_pformat_dispatch():
|
|||
assert _pformat_dispatch("a") == "'a'"
|
||||
assert _pformat_dispatch("a" * 10, width=5) == "'aaaaaaaaaa'"
|
||||
assert _pformat_dispatch("foo bar", width=5) == "('foo '\n 'bar')"
|
||||
|
||||
|
||||
def test_broken_getattribute():
|
||||
"""saferepr() can create proper representations of classes with
|
||||
broken __getattribute__ (#7145)
|
||||
"""
|
||||
|
||||
class SomeClass:
|
||||
def __getattribute__(self, attr):
|
||||
raise RuntimeError
|
||||
|
||||
def __repr__(self):
|
||||
raise RuntimeError
|
||||
|
||||
assert saferepr(SomeClass()).startswith(
|
||||
"<[RuntimeError() raised in repr()] SomeClass object at 0x"
|
||||
)
|
||||
|
|
|
@ -463,7 +463,7 @@ class TestFunction:
|
|||
return '3'
|
||||
|
||||
@pytest.mark.parametrize('fix2', ['2'])
|
||||
def test_it(fix1, fix2):
|
||||
def test_it(fix1):
|
||||
assert fix1 == '21'
|
||||
assert not fix3_instantiated
|
||||
"""
|
||||
|
@ -1225,7 +1225,7 @@ def test_syntax_error_with_non_ascii_chars(testdir):
|
|||
result.stdout.fnmatch_lines(["*ERROR collecting*", "*SyntaxError*", "*1 error in*"])
|
||||
|
||||
|
||||
def test_collecterror_with_fulltrace(testdir):
|
||||
def test_collect_error_with_fulltrace(testdir):
|
||||
testdir.makepyfile("assert 0")
|
||||
result = testdir.runpytest("--fulltrace")
|
||||
result.stdout.fnmatch_lines(
|
||||
|
@ -1233,15 +1233,12 @@ def test_collecterror_with_fulltrace(testdir):
|
|||
"collected 0 items / 1 error",
|
||||
"",
|
||||
"*= ERRORS =*",
|
||||
"*_ ERROR collecting test_collecterror_with_fulltrace.py _*",
|
||||
"",
|
||||
"*/_pytest/python.py:*: ",
|
||||
"_ _ _ _ _ _ _ _ *",
|
||||
"*_ ERROR collecting test_collect_error_with_fulltrace.py _*",
|
||||
"",
|
||||
"> assert 0",
|
||||
"E assert 0",
|
||||
"",
|
||||
"test_collecterror_with_fulltrace.py:1: AssertionError",
|
||||
"test_collect_error_with_fulltrace.py:1: AssertionError",
|
||||
"*! Interrupted: 1 error during collection !*",
|
||||
]
|
||||
)
|
||||
|
|
|
@ -36,9 +36,6 @@ class TestMetafunc:
|
|||
class DefinitionMock(python.FunctionDefinition):
|
||||
obj = attr.ib()
|
||||
|
||||
def listchain(self):
|
||||
return []
|
||||
|
||||
names = fixtures.getfuncargnames(func)
|
||||
fixtureinfo = FuncFixtureInfoMock(names) # type: Any
|
||||
definition = DefinitionMock._create(func) # type: Any
|
||||
|
@ -1902,51 +1899,3 @@ class TestMarkersWithParametrization:
|
|||
"*= 6 passed in *",
|
||||
]
|
||||
)
|
||||
|
||||
def test_parametrize_explicit_parameters_func(self, testdir: Testdir) -> None:
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fixture(arg):
|
||||
return arg
|
||||
|
||||
@pytest.mark.parametrize("arg", ["baz"])
|
||||
def test_without_arg(fixture):
|
||||
assert "baz" == fixture
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest()
|
||||
result.assert_outcomes(error=1)
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
'*In function "test_without_arg"*',
|
||||
'*Parameter "arg" should be declared explicitly via indirect or in function itself*',
|
||||
]
|
||||
)
|
||||
|
||||
def test_parametrize_explicit_parameters_method(self, testdir: Testdir) -> None:
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
import pytest
|
||||
|
||||
class Test:
|
||||
@pytest.fixture
|
||||
def test_fixture(self, argument):
|
||||
return argument
|
||||
|
||||
@pytest.mark.parametrize("argument", ["foobar"])
|
||||
def test_without_argument(self, test_fixture):
|
||||
assert "foobar" == test_fixture
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest()
|
||||
result.assert_outcomes(error=1)
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
'*In function "test_without_argument"*',
|
||||
'*Parameter "argument" should be declared explicitly via indirect or in function itself*',
|
||||
]
|
||||
)
|
||||
|
|
|
@ -542,18 +542,40 @@ class TestCaptureFixture:
|
|||
reprec.assertoutcome(passed=1)
|
||||
|
||||
def test_capsysbinary(self, testdir):
|
||||
reprec = testdir.inline_runsource(
|
||||
"""\
|
||||
p1 = testdir.makepyfile(
|
||||
r"""
|
||||
def test_hello(capsysbinary):
|
||||
import sys
|
||||
# some likely un-decodable bytes
|
||||
sys.stdout.buffer.write(b'\\xfe\\x98\\x20')
|
||||
|
||||
sys.stdout.buffer.write(b'hello')
|
||||
|
||||
# Some likely un-decodable bytes.
|
||||
sys.stdout.buffer.write(b'\xfe\x98\x20')
|
||||
|
||||
sys.stdout.buffer.flush()
|
||||
|
||||
# Ensure writing in text mode still works and is captured.
|
||||
# https://github.com/pytest-dev/pytest/issues/6871
|
||||
print("world", flush=True)
|
||||
|
||||
out, err = capsysbinary.readouterr()
|
||||
assert out == b'\\xfe\\x98\\x20'
|
||||
assert out == b'hello\xfe\x98\x20world\n'
|
||||
assert err == b''
|
||||
|
||||
print("stdout after")
|
||||
print("stderr after", file=sys.stderr)
|
||||
"""
|
||||
)
|
||||
reprec.assertoutcome(passed=1)
|
||||
result = testdir.runpytest(str(p1), "-rA")
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"*- Captured stdout call -*",
|
||||
"stdout after",
|
||||
"*- Captured stderr call -*",
|
||||
"stderr after",
|
||||
"*= 1 passed in *",
|
||||
]
|
||||
)
|
||||
|
||||
def test_partial_setup_failure(self, testdir):
|
||||
p = testdir.makepyfile(
|
||||
|
@ -977,7 +999,7 @@ class TestFDCapture:
|
|||
cap.start()
|
||||
tmpfile.write(data1)
|
||||
tmpfile.flush()
|
||||
cap.writeorg(data2)
|
||||
cap.writeorg(data2.decode("ascii"))
|
||||
scap = cap.snap()
|
||||
cap.done()
|
||||
assert scap == data1.decode("ascii")
|
||||
|
|
|
@ -1332,3 +1332,24 @@ def test_does_not_put_src_on_path(testdir):
|
|||
)
|
||||
result = testdir.runpytest()
|
||||
assert result.ret == ExitCode.OK
|
||||
|
||||
|
||||
def test_fscollector_from_parent(tmpdir, request):
|
||||
"""Ensure File.from_parent can forward custom arguments to the constructor.
|
||||
|
||||
Context: https://github.com/pytest-dev/pytest-cpp/pull/47
|
||||
"""
|
||||
|
||||
class MyCollector(pytest.File):
|
||||
def __init__(self, fspath, parent, x):
|
||||
super().__init__(fspath, parent)
|
||||
self.x = x
|
||||
|
||||
@classmethod
|
||||
def from_parent(cls, parent, *, fspath, x):
|
||||
return super().from_parent(parent=parent, fspath=fspath, x=x)
|
||||
|
||||
collector = MyCollector.from_parent(
|
||||
parent=request.session, fspath=tmpdir / "foo", x=10
|
||||
)
|
||||
assert collector.x == 10
|
||||
|
|
|
@ -342,6 +342,15 @@ class TestPDB:
|
|||
child.sendeof()
|
||||
self.flush(child)
|
||||
|
||||
def test_pdb_prevent_ConftestImportFailure_hiding_exception(self, testdir):
|
||||
testdir.makepyfile("def test_func(): pass")
|
||||
sub_dir = testdir.tmpdir.join("ns").ensure_dir()
|
||||
sub_dir.join("conftest").new(ext=".py").write("import unknown")
|
||||
sub_dir.join("test_file").new(ext=".py").write("def test_func(): pass")
|
||||
|
||||
result = testdir.runpytest_subprocess("--pdb", ".")
|
||||
result.stdout.fnmatch_lines(["-> import unknown"])
|
||||
|
||||
def test_pdb_interaction_capturing_simple(self, testdir):
|
||||
p1 = testdir.makepyfile(
|
||||
"""
|
||||
|
|
|
@ -5,6 +5,7 @@ import pytest
|
|||
from _pytest.compat import MODULE_NOT_FOUND_ERROR
|
||||
from _pytest.doctest import _get_checker
|
||||
from _pytest.doctest import _is_mocked
|
||||
from _pytest.doctest import _is_setup_py
|
||||
from _pytest.doctest import _patch_unwrap_mock_aware
|
||||
from _pytest.doctest import DoctestItem
|
||||
from _pytest.doctest import DoctestModule
|
||||
|
@ -1487,3 +1488,27 @@ def test_warning_on_unwrap_of_broken_object(stop):
|
|||
with pytest.raises(KeyError):
|
||||
inspect.unwrap(bad_instance, stop=stop)
|
||||
assert inspect.unwrap.__module__ == "inspect"
|
||||
|
||||
|
||||
def test_is_setup_py_not_named_setup_py(tmpdir):
|
||||
not_setup_py = tmpdir.join("not_setup.py")
|
||||
not_setup_py.write('from setuptools import setup; setup(name="foo")')
|
||||
assert not _is_setup_py(not_setup_py)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mod", ("setuptools", "distutils.core"))
|
||||
def test_is_setup_py_is_a_setup_py(tmpdir, mod):
|
||||
setup_py = tmpdir.join("setup.py")
|
||||
setup_py.write('from {} import setup; setup(name="foo")'.format(mod))
|
||||
assert _is_setup_py(setup_py)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mod", ("setuptools", "distutils.core"))
|
||||
def test_is_setup_py_different_encoding(tmpdir, mod):
|
||||
setup_py = tmpdir.join("setup.py")
|
||||
contents = (
|
||||
"# -*- coding: cp1252 -*-\n"
|
||||
'from {} import setup; setup(name="foo", description="€")\n'.format(mod)
|
||||
)
|
||||
setup_py.write_binary(contents.encode("cp1252"))
|
||||
assert _is_setup_py(setup_py)
|
||||
|
|
|
@ -200,23 +200,23 @@ class TestPython:
|
|||
timestamp = datetime.strptime(node["timestamp"], "%Y-%m-%dT%H:%M:%S.%f")
|
||||
assert start_time <= timestamp < datetime.now()
|
||||
|
||||
def test_timing_function(self, testdir, run_and_parse):
|
||||
def test_timing_function(self, testdir, run_and_parse, mock_timing):
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
import time, pytest
|
||||
from _pytest import timing
|
||||
def setup_module():
|
||||
time.sleep(0.01)
|
||||
timing.sleep(1)
|
||||
def teardown_module():
|
||||
time.sleep(0.01)
|
||||
timing.sleep(2)
|
||||
def test_sleep():
|
||||
time.sleep(0.01)
|
||||
timing.sleep(4)
|
||||
"""
|
||||
)
|
||||
result, dom = run_and_parse()
|
||||
node = dom.find_first_by_tag("testsuite")
|
||||
tnode = node.find_first_by_tag("testcase")
|
||||
val = tnode["time"]
|
||||
assert round(float(val), 2) >= 0.03
|
||||
assert float(val) == 7.0
|
||||
|
||||
@pytest.mark.parametrize("duration_report", ["call", "total"])
|
||||
def test_junit_duration_report(
|
||||
|
|
|
@ -58,3 +58,30 @@ def test__check_initialpaths_for_relpath():
|
|||
|
||||
outside = py.path.local("/outside")
|
||||
assert nodes._check_initialpaths_for_relpath(FakeSession, outside) is None
|
||||
|
||||
|
||||
def test_failure_with_changed_cwd(testdir):
|
||||
"""
|
||||
Test failure lines should use absolute paths if cwd has changed since
|
||||
invocation, so the path is correct (#6428).
|
||||
"""
|
||||
p = testdir.makepyfile(
|
||||
"""
|
||||
import os
|
||||
import pytest
|
||||
|
||||
@pytest.fixture
|
||||
def private_dir():
|
||||
out_dir = 'ddd'
|
||||
os.mkdir(out_dir)
|
||||
old_dir = os.getcwd()
|
||||
os.chdir(out_dir)
|
||||
yield out_dir
|
||||
os.chdir(old_dir)
|
||||
|
||||
def test_show_wrong_path(private_dir):
|
||||
assert False
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines([str(p) + ":*: AssertionError", "*1 failed in *"])
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import os.path
|
||||
import sys
|
||||
import unittest.mock
|
||||
|
||||
import py
|
||||
|
||||
import pytest
|
||||
from _pytest.pathlib import ensure_deletable
|
||||
from _pytest.pathlib import fnmatch_ex
|
||||
from _pytest.pathlib import get_extended_length_path_str
|
||||
from _pytest.pathlib import get_lock_path
|
||||
from _pytest.pathlib import maybe_delete_a_numbered_dir
|
||||
from _pytest.pathlib import Path
|
||||
|
@ -89,3 +92,45 @@ def test_access_denied_during_cleanup(tmp_path, monkeypatch):
|
|||
lock_path = get_lock_path(path)
|
||||
maybe_delete_a_numbered_dir(path)
|
||||
assert not lock_path.is_file()
|
||||
|
||||
|
||||
def test_long_path_during_cleanup(tmp_path):
|
||||
"""Ensure that deleting long path works (particularly on Windows (#6775))."""
|
||||
path = (tmp_path / ("a" * 250)).resolve()
|
||||
if sys.platform == "win32":
|
||||
# make sure that the full path is > 260 characters without any
|
||||
# component being over 260 characters
|
||||
assert len(str(path)) > 260
|
||||
extended_path = "\\\\?\\" + str(path)
|
||||
else:
|
||||
extended_path = str(path)
|
||||
os.mkdir(extended_path)
|
||||
assert os.path.isdir(extended_path)
|
||||
maybe_delete_a_numbered_dir(path)
|
||||
assert not os.path.isdir(extended_path)
|
||||
|
||||
|
||||
def test_get_extended_length_path_str():
|
||||
assert get_extended_length_path_str(r"c:\foo") == r"\\?\c:\foo"
|
||||
assert get_extended_length_path_str(r"\\share\foo") == r"\\?\UNC\share\foo"
|
||||
assert get_extended_length_path_str(r"\\?\UNC\share\foo") == r"\\?\UNC\share\foo"
|
||||
assert get_extended_length_path_str(r"\\?\c:\foo") == r"\\?\c:\foo"
|
||||
|
||||
|
||||
def test_suppress_error_removing_lock(tmp_path):
|
||||
"""ensure_deletable should not raise an exception if the lock file cannot be removed (#5456)"""
|
||||
path = tmp_path / "dir"
|
||||
path.mkdir()
|
||||
lock = get_lock_path(path)
|
||||
lock.touch()
|
||||
mtime = lock.stat().st_mtime
|
||||
|
||||
with unittest.mock.patch.object(Path, "unlink", side_effect=OSError):
|
||||
assert not ensure_deletable(
|
||||
path, consider_lock_dead_if_created_before=mtime + 30
|
||||
)
|
||||
assert lock.is_file()
|
||||
|
||||
# check now that we can remove the lock file in normal circumstances
|
||||
assert ensure_deletable(path, consider_lock_dead_if_created_before=mtime + 30)
|
||||
assert not lock.is_file()
|
||||
|
|
|
@ -395,6 +395,14 @@ class TestReportSerialization:
|
|||
# for same reasons as previous test, ensure we don't blow up here
|
||||
loaded_report.longrepr.toterminal(tw_mock)
|
||||
|
||||
def test_report_prevent_ConftestImportFailure_hiding_exception(self, testdir):
|
||||
sub_dir = testdir.tmpdir.join("ns").ensure_dir()
|
||||
sub_dir.join("conftest").new(ext=".py").write("import unknown")
|
||||
|
||||
result = testdir.runpytest_subprocess(".")
|
||||
result.stdout.fnmatch_lines(["E *Error: No module named 'unknown'"])
|
||||
result.stdout.no_fnmatch_line("ERROR - *ConftestImportFailure*")
|
||||
|
||||
|
||||
class TestHooks:
|
||||
"""Test that the hooks are working correctly for plugins"""
|
||||
|
|
|
@ -193,6 +193,42 @@ def test_no_resultlog_on_slaves(testdir):
|
|||
assert resultlog_key not in config._store
|
||||
|
||||
|
||||
def test_unknown_teststatus(testdir):
|
||||
"""Ensure resultlog correctly handles unknown status from pytest_report_teststatus
|
||||
|
||||
Inspired on pytest-rerunfailures.
|
||||
"""
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
def test():
|
||||
assert 0
|
||||
"""
|
||||
)
|
||||
testdir.makeconftest(
|
||||
"""
|
||||
import pytest
|
||||
|
||||
def pytest_report_teststatus(report):
|
||||
if report.outcome == 'rerun':
|
||||
return "rerun", "r", "RERUN"
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_makereport():
|
||||
res = yield
|
||||
report = res.get_result()
|
||||
if report.when == "call":
|
||||
report.outcome = 'rerun'
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest("--resultlog=result.log")
|
||||
result.stdout.fnmatch_lines(
|
||||
["test_unknown_teststatus.py r *[[]100%[]]", "* 1 rerun *"]
|
||||
)
|
||||
|
||||
lines = testdir.tmpdir.join("result.log").readlines(cr=0)
|
||||
assert lines[0] == "r test_unknown_teststatus.py::test"
|
||||
|
||||
|
||||
def test_failure_issue380(testdir):
|
||||
testdir.makeconftest(
|
||||
"""
|
||||
|
|
|
@ -537,28 +537,24 @@ class TestTrialUnittest:
|
|||
)
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"test_trial_error.py::TC::test_four SKIPPED",
|
||||
"test_trial_error.py::TC::test_four FAILED",
|
||||
"test_trial_error.py::TC::test_four ERROR",
|
||||
"test_trial_error.py::TC::test_one FAILED",
|
||||
"test_trial_error.py::TC::test_three FAILED",
|
||||
"test_trial_error.py::TC::test_two SKIPPED",
|
||||
"test_trial_error.py::TC::test_two ERROR",
|
||||
"test_trial_error.py::TC::test_two FAILED",
|
||||
"*ERRORS*",
|
||||
"*_ ERROR at teardown of TC.test_four _*",
|
||||
"NOTE: Incompatible Exception Representation, displaying natively:",
|
||||
"*DelayedCalls*",
|
||||
"*_ ERROR at teardown of TC.test_two _*",
|
||||
"NOTE: Incompatible Exception Representation, displaying natively:",
|
||||
"*DelayedCalls*",
|
||||
"*= FAILURES =*",
|
||||
# "*_ TC.test_four _*",
|
||||
# "*NameError*crash*",
|
||||
"*_ TC.test_four _*",
|
||||
"*NameError*crash*",
|
||||
"*_ TC.test_one _*",
|
||||
"*NameError*crash*",
|
||||
"*_ TC.test_three _*",
|
||||
"NOTE: Incompatible Exception Representation, displaying natively:",
|
||||
"*DelayedCalls*",
|
||||
"*= 2 failed, 2 skipped, 2 errors in *",
|
||||
"*_ TC.test_two _*",
|
||||
"*NameError*crash*",
|
||||
"*= 4 failed, 1 error in *",
|
||||
]
|
||||
)
|
||||
|
||||
|
@ -876,6 +872,37 @@ def test_no_teardown_if_setupclass_failed(testdir):
|
|||
reprec.assertoutcome(passed=1, failed=1)
|
||||
|
||||
|
||||
def test_cleanup_functions(testdir):
|
||||
"""Ensure functions added with addCleanup are always called after each test ends (#6947)"""
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
import unittest
|
||||
|
||||
cleanups = []
|
||||
|
||||
class Test(unittest.TestCase):
|
||||
|
||||
def test_func_1(self):
|
||||
self.addCleanup(cleanups.append, "test_func_1")
|
||||
|
||||
def test_func_2(self):
|
||||
self.addCleanup(cleanups.append, "test_func_2")
|
||||
assert 0
|
||||
|
||||
def test_func_3_check_cleanups(self):
|
||||
assert cleanups == ["test_func_1", "test_func_2"]
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest("-v")
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"*::test_func_1 PASSED *",
|
||||
"*::test_func_2 FAILED *",
|
||||
"*::test_func_3_check_cleanups PASSED *",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_issue333_result_clearing(testdir):
|
||||
testdir.makeconftest(
|
||||
"""
|
||||
|
@ -1129,3 +1156,89 @@ def test_trace(testdir, monkeypatch):
|
|||
result = testdir.runpytest("--trace", str(p1))
|
||||
assert len(calls) == 2
|
||||
assert result.ret == 0
|
||||
|
||||
|
||||
def test_pdb_teardown_called(testdir, monkeypatch):
|
||||
"""Ensure tearDown() is always called when --pdb is given in the command-line.
|
||||
|
||||
We delay the normal tearDown() calls when --pdb is given, so this ensures we are calling
|
||||
tearDown() eventually to avoid memory leaks when using --pdb.
|
||||
"""
|
||||
teardowns = []
|
||||
monkeypatch.setattr(
|
||||
pytest, "test_pdb_teardown_called_teardowns", teardowns, raising=False
|
||||
)
|
||||
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
import unittest
|
||||
import pytest
|
||||
|
||||
class MyTestCase(unittest.TestCase):
|
||||
|
||||
def tearDown(self):
|
||||
pytest.test_pdb_teardown_called_teardowns.append(self.id())
|
||||
|
||||
def test_1(self):
|
||||
pass
|
||||
def test_2(self):
|
||||
pass
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest_inprocess("--pdb")
|
||||
result.stdout.fnmatch_lines("* 2 passed in *")
|
||||
assert teardowns == [
|
||||
"test_pdb_teardown_called.MyTestCase.test_1",
|
||||
"test_pdb_teardown_called.MyTestCase.test_2",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mark", ["@unittest.skip", "@pytest.mark.skip"])
|
||||
def test_pdb_teardown_skipped(testdir, monkeypatch, mark):
|
||||
"""
|
||||
With --pdb, setUp and tearDown should not be called for skipped tests.
|
||||
"""
|
||||
tracked = []
|
||||
monkeypatch.setattr(pytest, "test_pdb_teardown_skipped", tracked, raising=False)
|
||||
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
import unittest
|
||||
import pytest
|
||||
|
||||
class MyTestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
pytest.test_pdb_teardown_skipped.append("setUp:" + self.id())
|
||||
|
||||
def tearDown(self):
|
||||
pytest.test_pdb_teardown_skipped.append("tearDown:" + self.id())
|
||||
|
||||
{mark}("skipped for reasons")
|
||||
def test_1(self):
|
||||
pass
|
||||
|
||||
""".format(
|
||||
mark=mark
|
||||
)
|
||||
)
|
||||
result = testdir.runpytest_inprocess("--pdb")
|
||||
result.stdout.fnmatch_lines("* 1 skipped in *")
|
||||
assert tracked == []
|
||||
|
||||
|
||||
def test_async_support(testdir):
|
||||
pytest.importorskip("unittest.async_case")
|
||||
|
||||
testdir.copy_example("unittest/test_unittest_asyncio.py")
|
||||
reprec = testdir.inline_run()
|
||||
reprec.assertoutcome(failed=1, passed=2)
|
||||
|
||||
|
||||
def test_asynctest_support(testdir):
|
||||
"""Check asynctest support (#7110)"""
|
||||
pytest.importorskip("asynctest")
|
||||
|
||||
testdir.copy_example("unittest/test_unittest_asynctest.py")
|
||||
reprec = testdir.inline_run()
|
||||
reprec.assertoutcome(failed=1, passed=2)
|
||||
|
|
7
tox.ini
7
tox.ini
|
@ -9,9 +9,9 @@ envlist =
|
|||
py36
|
||||
py37
|
||||
py38
|
||||
pypy
|
||||
py39
|
||||
pypy3
|
||||
py37-{pexpect,xdist,twisted,numpy,pluggymaster}
|
||||
py37-{pexpect,xdist,unittestextras,numpy,pluggymaster}
|
||||
doctesting
|
||||
py37-freeze
|
||||
docs
|
||||
|
@ -50,7 +50,8 @@ deps =
|
|||
pexpect: pexpect
|
||||
pluggymaster: git+https://github.com/pytest-dev/pluggy.git@master
|
||||
pygments
|
||||
twisted: twisted
|
||||
unittestextras: twisted
|
||||
unittestextras: asynctest
|
||||
xdist: pytest-xdist>=1.13
|
||||
{env:_PYTEST_TOX_EXTRA_DEP:}
|
||||
|
||||
|
|
Loading…
Reference in New Issue