diff --git a/AUTHORS b/AUTHORS index ee275083a..14c465571 100644 --- a/AUTHORS +++ b/AUTHORS @@ -135,6 +135,7 @@ Jordan Guymon Jordan Moldow Jordan Speicher Joseph Hunkeler +Josh Karpel Joshua Bronson Jurko Gospodnetić Justyna Janczyszyn @@ -264,6 +265,7 @@ Virgil Dupras Vitaly Lashmanov Vlad Dragos Volodymyr Piskun +Wei Lin Wil Cooley William Lee Wim Glenn diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b1a988de8..e9ac09c8e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1901,7 +1901,8 @@ Features live-logging is enabled and/or when they are logged to a file. -- `#3985 `_: Introduce ``tmp_path`` as a fixture providing a Path object. +- `#3985 `_: Introduce ``tmp_path`` as a fixture providing a Path object. Also introduce ``tmp_path_factory`` as + a session-scoped fixture for creating arbitrary temporary directories from any other fixture or test. - `#4013 `_: Deprecation warnings are now shown even if you customize the warnings filters yourself. In the previous version diff --git a/changelog/2049.bugfix.rst b/changelog/2049.bugfix.rst new file mode 100644 index 000000000..395396bd3 --- /dev/null +++ b/changelog/2049.bugfix.rst @@ -0,0 +1 @@ +Fix ``-setup-plan`` showing inaccurate information about fixture lifetimes. diff --git a/changelog/6189.bugfix.rst b/changelog/6189.bugfix.rst new file mode 100644 index 000000000..060a2260a --- /dev/null +++ b/changelog/6189.bugfix.rst @@ -0,0 +1 @@ +Fix incorrect result of ``getmodpath`` method. diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 4702e0659..bf0812339 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -287,8 +287,7 @@ class PyobjMixin(PyobjContext): break parts.append(name) parts.reverse() - s = ".".join(parts) - return s.replace(".[", "[") + return ".".join(parts) def reportinfo(self) -> Tuple[str, int, str]: # XXX caching? diff --git a/src/_pytest/setupplan.py b/src/_pytest/setupplan.py index 697746f20..6fdd3aed0 100644 --- a/src/_pytest/setupplan.py +++ b/src/_pytest/setupplan.py @@ -16,7 +16,8 @@ def pytest_addoption(parser): def pytest_fixture_setup(fixturedef, request): # Will return a dummy fixture if the setuponly option is provided. if request.config.option.setupplan: - fixturedef.cached_result = (None, None, None) + my_cache_key = fixturedef.cache_key(request) + fixturedef.cached_result = (None, my_cache_key, None) return fixturedef.cached_result diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 578ab45eb..8f7be14be 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -760,7 +760,6 @@ class TestInvocationVariants: result = testdir.runpytest(str(p) + "::test", "--doctest-modules") result.stdout.fnmatch_lines(["*1 passed*"]) - @pytest.mark.skipif(not hasattr(os, "symlink"), reason="requires symlinks") def test_cmdline_python_package_symlink(self, testdir, monkeypatch): """ test --pyargs option with packages with path containing symlink can diff --git a/testing/test_collection.py b/testing/test_collection.py index 303738d67..b791ac6f9 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -685,6 +685,8 @@ class Test_genitems: def test_example_items1(self, testdir): p = testdir.makepyfile( """ + import pytest + def testone(): pass @@ -693,19 +695,24 @@ class Test_genitems: pass class TestY(TestX): - pass + @pytest.mark.parametrize("arg0", [".["]) + def testmethod_two(self, arg0): + pass """ ) items, reprec = testdir.inline_genitems(p) - assert len(items) == 3 + assert len(items) == 4 assert items[0].name == "testone" assert items[1].name == "testmethod_one" assert items[2].name == "testmethod_one" + assert items[3].name == "testmethod_two[.[]" # let's also test getmodpath here assert items[0].getmodpath() == "testone" assert items[1].getmodpath() == "TestX.testmethod_one" assert items[2].getmodpath() == "TestY.testmethod_one" + # PR #6202: Fix incorrect result of getmodpath method. (Resolves issue #6189) + assert items[3].getmodpath() == "TestY.testmethod_two[.[]" s = items[0].getmodpath(stopatmodule=False) assert s.endswith("test_example_items1.testone") diff --git a/testing/test_setupplan.py b/testing/test_setupplan.py index e323ba240..a44474dd1 100644 --- a/testing/test_setupplan.py +++ b/testing/test_setupplan.py @@ -17,3 +17,94 @@ def test_show_fixtures_and_test(testdir, dummy_yaml_custom_test): result.stdout.fnmatch_lines( ["*SETUP F arg*", "*test_arg (fixtures used: arg)", "*TEARDOWN F arg*"] ) + + +def test_show_multi_test_fixture_setup_and_teardown_correctly_simple(testdir): + """ + Verify that when a fixture lives for longer than a single test, --setup-plan + correctly displays the SETUP/TEARDOWN indicators the right number of times. + + As reported in https://github.com/pytest-dev/pytest/issues/2049 + --setup-plan was showing SETUP/TEARDOWN on every test, even when the fixture + should persist through multiple tests. + + (Note that this bug never affected actual test execution, which used the + correct fixture lifetimes. It was purely a display bug for --setup-plan, and + did not affect the related --setup-show or --setup-only.) + """ + testdir.makepyfile( + """ + import pytest + @pytest.fixture(scope = 'class') + def fix(): + return object() + class TestClass: + def test_one(self, fix): + assert False + def test_two(self, fix): + assert False + """ + ) + + result = testdir.runpytest("--setup-plan") + assert result.ret == 0 + + setup_fragment = "SETUP C fix" + setup_count = 0 + + teardown_fragment = "TEARDOWN C fix" + teardown_count = 0 + + for line in result.stdout.lines: + if setup_fragment in line: + setup_count += 1 + if teardown_fragment in line: + teardown_count += 1 + + # before the fix this tests, there would have been a setup/teardown + # message for each test, so the counts would each have been 2 + assert setup_count == 1 + assert teardown_count == 1 + + +def test_show_multi_test_fixture_setup_and_teardown_same_as_setup_show(testdir): + """ + Verify that SETUP/TEARDOWN messages match what comes out of --setup-show. + """ + testdir.makepyfile( + """ + import pytest + @pytest.fixture(scope = 'session') + def sess(): + return True + @pytest.fixture(scope = 'module') + def mod(): + return True + @pytest.fixture(scope = 'class') + def cls(): + return True + @pytest.fixture(scope = 'function') + def func(): + return True + def test_outside(sess, mod, cls, func): + assert True + class TestCls: + def test_one(self, sess, mod, cls, func): + assert True + def test_two(self, sess, mod, cls, func): + assert True + """ + ) + + plan_result = testdir.runpytest("--setup-plan") + show_result = testdir.runpytest("--setup-show") + + # the number and text of these lines should be identical + plan_lines = [ + l for l in plan_result.stdout.lines if "SETUP" in l or "TEARDOWN" in l + ] + show_lines = [ + l for l in show_result.stdout.lines if "SETUP" in l or "TEARDOWN" in l + ] + + assert plan_lines == show_lines