Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b322004047 | ||
|
|
2d795dc07b | ||
|
|
2d6b846978 | ||
|
|
703d0f50d8 | ||
|
|
56e6482405 | ||
|
|
589176e9fe | ||
|
|
3734a27733 | ||
|
|
e1a21e46b0 | ||
|
|
551400e8d6 | ||
|
|
b7b729298c | ||
|
|
21ca38b932 | ||
|
|
565f4cb4ad | ||
|
|
af6548a4e7 | ||
|
|
2fb2962df4 |
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
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
comment: off
|
||||
# reference: https://docs.codecov.io/docs/codecovyml-reference
|
||||
coverage:
|
||||
status:
|
||||
patch: true
|
||||
project: false
|
||||
comment: false
|
||||
|
||||
@@ -6,6 +6,7 @@ Release announcements
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
release-5.4.3
|
||||
release-5.4.2
|
||||
release-5.4.1
|
||||
release-5.4.0
|
||||
|
||||
21
doc/en/announce/release-5.4.3.rst
Normal file
21
doc/en/announce/release-5.4.3.rst
Normal file
@@ -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,29 @@ 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)
|
||||
=========================
|
||||
|
||||
|
||||
@@ -481,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
|
||||
--------------------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -338,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]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -100,10 +100,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 +251,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)
|
||||
|
||||
@@ -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:
|
||||
@@ -220,7 +220,7 @@ class TestCaseFunction(Function):
|
||||
# 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"):
|
||||
if self.config.getoption("usepdb") and not _is_skipped(self.obj):
|
||||
self._explicit_tearDown = self._testcase.tearDown
|
||||
setattr(self._testcase, "tearDown", lambda *args: None)
|
||||
|
||||
@@ -301,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))
|
||||
|
||||
@@ -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 !*",
|
||||
]
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
"""
|
||||
|
||||
@@ -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 *"])
|
||||
|
||||
@@ -5,6 +5,7 @@ import py
|
||||
|
||||
import pytest
|
||||
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 +90,26 @@ 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"
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -1193,6 +1193,40 @@ def test_pdb_teardown_called(testdir, monkeypatch):
|
||||
]
|
||||
|
||||
|
||||
@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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user