diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index fb17b8e93..a35039587 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-5.3.5 release-5.3.4 release-5.3.3 release-5.3.2 diff --git a/doc/en/announce/release-5.3.5.rst b/doc/en/announce/release-5.3.5.rst new file mode 100644 index 000000000..46095339f --- /dev/null +++ b/doc/en/announce/release-5.3.5.rst @@ -0,0 +1,19 @@ +pytest-5.3.5 +======================================= + +pytest 5.3.5 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: + +* Daniel Hahler +* Ran Benita + + +Happy testing, +The pytest Development Team diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index e0a2495cc..f0ad2b8ec 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -28,6 +28,15 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 5.3.5 (2020-01-29) +========================= + +Bug Fixes +--------- + +- `#6517 `_: Fix regression in pytest 5.3.4 causing an INTERNALERROR due to a wrong assertion. + + pytest 5.3.4 (2020-01-20) ========================= diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index 1570850fc..1a5c5b444 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -461,21 +461,49 @@ an ``incremental`` marker which is to be used on classes: # content of conftest.py - import pytest + # store history of failures per test class name and per index in parametrize (if parametrize used) + _test_failed_incremental: Dict[str, Dict[Tuple[int, ...], str]] = {} def pytest_runtest_makereport(item, call): if "incremental" in item.keywords: + # incremental marker is used if call.excinfo is not None: - parent = item.parent - parent._previousfailed = item + # the test has failed + # retrieve the class name of the test + cls_name = str(item.cls) + # retrieve the index of the test (if parametrize is used in combination with incremental) + parametrize_index = ( + tuple(item.callspec.indices.values()) + if hasattr(item, "callspec") + else () + ) + # retrieve the name of the test function + test_name = item.originalname or item.name + # store in _test_failed_incremental the original name of the failed test + _test_failed_incremental.setdefault(cls_name, {}).setdefault( + parametrize_index, test_name + ) def pytest_runtest_setup(item): if "incremental" in item.keywords: - previousfailed = getattr(item.parent, "_previousfailed", None) - if previousfailed is not None: - pytest.xfail("previous test failed ({})".format(previousfailed.name)) + # retrieve the class name of the test + cls_name = str(item.cls) + # check if a previous test has failed for this class + if cls_name in _test_failed_incremental: + # retrieve the index of the test (if parametrize is used in combination with incremental) + parametrize_index = ( + tuple(item.callspec.indices.values()) + if hasattr(item, "callspec") + else () + ) + # retrieve the name of the first test function to fail for this class name and index + test_name = _test_failed_incremental[cls_name].get(parametrize_index, None) + # if name found, test has failed for the combination of class name & test name + if test_name is not None: + pytest.xfail("previous test failed ({})".format(test_name)) + These two hook implementations work together to abort incremental-marked tests in a class. Here is a test module example: diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 59197d0d7..83b4677e9 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -127,7 +127,7 @@ Once you develop multiple tests, you may want to group them into a class. pytest x = "hello" assert hasattr(x, "check") -``pytest`` discovers all tests following its :ref:`Conventions for Python test discovery `, so it finds both ``test_`` prefixed functions. There is no need to subclass anything. We can simply run the module by passing its filename: +``pytest`` discovers all tests following its :ref:`Conventions for Python test discovery `, so it finds both ``test_`` prefixed functions. There is no need to subclass anything, but make sure to prefix your class with ``Test`` otherwise the class will be skipped. We can simply run the module by passing its filename: .. code-block:: pytest diff --git a/scripts/publish-gh-release-notes.py b/scripts/publish-gh-release-notes.py index f8d8b3986..583b5bfc7 100644 --- a/scripts/publish-gh-release-notes.py +++ b/scripts/publish-gh-release-notes.py @@ -61,7 +61,7 @@ def parse_changelog(tag_name): def convert_rst_to_md(text): - return pypandoc.convert_text(text, "md", format="rst") + return pypandoc.convert_text(text, "md", format="rst", extra_args=["--wrap=none"]) def main(argv): diff --git a/testing/code/test_code.py b/testing/code/test_code.py index f8e1ce17f..826a37708 100644 --- a/testing/code/test_code.py +++ b/testing/code/test_code.py @@ -2,14 +2,17 @@ import sys from types import FrameType from unittest import mock -import _pytest._code import pytest +from _pytest._code import Code +from _pytest._code import ExceptionInfo +from _pytest._code import Frame +from _pytest._code.code import ReprFuncArgs def test_ne() -> None: - code1 = _pytest._code.Code(compile('foo = "bar"', "", "exec")) + code1 = Code(compile('foo = "bar"', "", "exec")) assert code1 == code1 - code2 = _pytest._code.Code(compile('foo = "baz"', "", "exec")) + code2 = Code(compile('foo = "baz"', "", "exec")) assert code2 != code1 @@ -17,7 +20,7 @@ def test_code_gives_back_name_for_not_existing_file() -> None: name = "abc-123" co_code = compile("pass\n", name, "exec") assert co_code.co_filename == name - code = _pytest._code.Code(co_code) + code = Code(co_code) assert str(code.path) == name assert code.fullsource is None @@ -26,7 +29,7 @@ def test_code_with_class() -> None: class A: pass - pytest.raises(TypeError, _pytest._code.Code, A) + pytest.raises(TypeError, Code, A) def x() -> None: @@ -34,13 +37,13 @@ def x() -> None: def test_code_fullsource() -> None: - code = _pytest._code.Code(x) + code = Code(x) full = code.fullsource assert "test_code_fullsource()" in str(full) def test_code_source() -> None: - code = _pytest._code.Code(x) + code = Code(x) src = code.source() expected = """def x() -> None: raise NotImplementedError()""" @@ -51,7 +54,7 @@ def test_frame_getsourcelineno_myself() -> None: def func() -> FrameType: return sys._getframe(0) - f = _pytest._code.Frame(func()) + f = Frame(func()) source, lineno = f.code.fullsource, f.lineno assert source is not None assert source[lineno].startswith(" return sys._getframe(0)") @@ -61,13 +64,13 @@ def test_getstatement_empty_fullsource() -> None: def func() -> FrameType: return sys._getframe(0) - f = _pytest._code.Frame(func()) + f = Frame(func()) with mock.patch.object(f.code.__class__, "fullsource", None): assert f.statement == "" def test_code_from_func() -> None: - co = _pytest._code.Code(test_frame_getsourcelineno_myself) + co = Code(test_frame_getsourcelineno_myself) assert co.firstlineno assert co.path @@ -86,25 +89,25 @@ def test_code_getargs() -> None: def f1(x): raise NotImplementedError() - c1 = _pytest._code.Code(f1) + c1 = Code(f1) assert c1.getargs(var=True) == ("x",) def f2(x, *y): raise NotImplementedError() - c2 = _pytest._code.Code(f2) + c2 = Code(f2) assert c2.getargs(var=True) == ("x", "y") def f3(x, **z): raise NotImplementedError() - c3 = _pytest._code.Code(f3) + c3 = Code(f3) assert c3.getargs(var=True) == ("x", "z") def f4(x, *y, **z): raise NotImplementedError() - c4 = _pytest._code.Code(f4) + c4 = Code(f4) assert c4.getargs(var=True) == ("x", "y", "z") @@ -112,25 +115,25 @@ def test_frame_getargs() -> None: def f1(x) -> FrameType: return sys._getframe(0) - fr1 = _pytest._code.Frame(f1("a")) + fr1 = Frame(f1("a")) assert fr1.getargs(var=True) == [("x", "a")] def f2(x, *y) -> FrameType: return sys._getframe(0) - fr2 = _pytest._code.Frame(f2("a", "b", "c")) + fr2 = Frame(f2("a", "b", "c")) assert fr2.getargs(var=True) == [("x", "a"), ("y", ("b", "c"))] def f3(x, **z) -> FrameType: return sys._getframe(0) - fr3 = _pytest._code.Frame(f3("a", b="c")) + fr3 = Frame(f3("a", b="c")) assert fr3.getargs(var=True) == [("x", "a"), ("z", {"b": "c"})] def f4(x, *y, **z) -> FrameType: return sys._getframe(0) - fr4 = _pytest._code.Frame(f4("a", "b", c="d")) + fr4 = Frame(f4("a", "b", c="d")) assert fr4.getargs(var=True) == [("x", "a"), ("y", ("b",)), ("z", {"c": "d"})] @@ -142,12 +145,12 @@ class TestExceptionInfo: else: assert False except AssertionError: - exci = _pytest._code.ExceptionInfo.from_current() + exci = ExceptionInfo.from_current() assert exci.getrepr() def test_from_current_with_missing(self) -> None: with pytest.raises(AssertionError, match="no current exception"): - _pytest._code.ExceptionInfo.from_current() + ExceptionInfo.from_current() class TestTracebackEntry: @@ -158,7 +161,7 @@ class TestTracebackEntry: else: assert False except AssertionError: - exci = _pytest._code.ExceptionInfo.from_current() + exci = ExceptionInfo.from_current() entry = exci.traceback[0] source = entry.getsource() assert source is not None @@ -168,8 +171,6 @@ class TestTracebackEntry: class TestReprFuncArgs: def test_not_raise_exception_with_mixed_encoding(self, tw_mock) -> None: - from _pytest._code.code import ReprFuncArgs - args = [("unicode_string", "São Paulo"), ("utf8_string", b"S\xc3\xa3o Paulo")] r = ReprFuncArgs(args) diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 030e60676..b5efdb317 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -9,10 +9,11 @@ from typing import Any from typing import Dict from typing import Optional -import py +import py.path import _pytest._code import pytest +from _pytest._code import getfslineno from _pytest._code import Source @@ -496,10 +497,8 @@ def test_findsource() -> None: def test_getfslineno() -> None: - from _pytest._code import getfslineno - def f(x) -> None: - pass + raise NotImplementedError() fspath, lineno = getfslineno(f) @@ -513,6 +512,7 @@ def test_getfslineno() -> None: fspath, lineno = getfslineno(A) _, A_lineno = inspect.findsource(A) + assert isinstance(fspath, py.path.local) assert fspath.basename == "test_source.py" assert lineno == A_lineno diff --git a/testing/test_terminal.py b/testing/test_terminal.py index b3b127cd1..4733469ae 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -33,6 +33,8 @@ COLORS = { } RE_COLORS = {k: re.escape(v) for k, v in COLORS.items()} +TRANS_FNMATCH = str.maketrans({"[": "[[]", "]": "[]]"}) + class Option: def __init__(self, verbosity=0): @@ -1852,14 +1854,19 @@ class TestProgressWithTeardown: [r"test_bar.py (\.E){5}\s+\[ 25%\]", r"test_foo.py (\.E){15}\s+\[100%\]"] ) - def test_teardown_many_verbose(self, testdir, many_files): - output = testdir.runpytest("-v") - output.stdout.re_match_lines( + def test_teardown_many_verbose(self, testdir: Testdir, many_files) -> None: + result = testdir.runpytest("-v") + result.stdout.fnmatch_lines( [ - r"test_bar.py::test_bar\[0\] PASSED\s+\[ 5%\]", - r"test_bar.py::test_bar\[0\] ERROR\s+\[ 5%\]", - r"test_bar.py::test_bar\[4\] PASSED\s+\[ 25%\]", - r"test_bar.py::test_bar\[4\] ERROR\s+\[ 25%\]", + line.translate(TRANS_FNMATCH) + for line in [ + "test_bar.py::test_bar[0] PASSED * [ 5%]", + "test_bar.py::test_bar[0] ERROR * [ 5%]", + "test_bar.py::test_bar[4] PASSED * [ 25%]", + "test_foo.py::test_foo[14] PASSED * [100%]", + "test_foo.py::test_foo[14] ERROR * [100%]", + "=* 20 passed, 20 errors in *", + ] ] )