# mypy: allow-untyped-defs from __future__ import annotations import os from pathlib import Path import textwrap from typing import cast from typing import Generator from typing import List from typing import Sequence from _pytest.config import ExitCode from _pytest.config import PytestPluginManager from _pytest.monkeypatch import MonkeyPatch from _pytest.pathlib import symlink_or_skip from _pytest.pytester import Pytester from _pytest.tmpdir import TempPathFactory import pytest def ConftestWithSetinitial(path) -> PytestPluginManager: conftest = PytestPluginManager() conftest_setinitial(conftest, [path]) return conftest def conftest_setinitial( conftest: PytestPluginManager, args: Sequence[str | Path], confcutdir: Path | None = None, ) -> None: conftest._set_initial_conftests( args=args, pyargs=False, noconftest=False, rootpath=Path(args[0]), confcutdir=confcutdir, invocation_dir=Path.cwd(), importmode="prepend", consider_namespace_packages=False, ) @pytest.mark.usefixtures("_sys_snapshot") class TestConftestValueAccessGlobal: @pytest.fixture(scope="module", params=["global", "inpackage"]) def basedir( self, request, tmp_path_factory: TempPathFactory ) -> Generator[Path, None, None]: tmp_path = tmp_path_factory.mktemp("basedir", numbered=True) tmp_path.joinpath("adir/b").mkdir(parents=True) tmp_path.joinpath("adir/conftest.py").write_text( "a=1 ; Directory = 3", encoding="utf-8" ) tmp_path.joinpath("adir/b/conftest.py").write_text( "b=2 ; a = 1.5", encoding="utf-8" ) if request.param == "inpackage": tmp_path.joinpath("adir/__init__.py").touch() tmp_path.joinpath("adir/b/__init__.py").touch() yield tmp_path def test_basic_init(self, basedir: Path) -> None: conftest = PytestPluginManager() p = basedir / "adir" conftest._loadconftestmodules( p, importmode="prepend", rootpath=basedir, consider_namespace_packages=False ) assert conftest._rget_with_confmod("a", p)[1] == 1 def test_immediate_initialization_and_incremental_are_the_same( self, basedir: Path ) -> None: conftest = PytestPluginManager() assert not len(conftest._dirpath2confmods) conftest._loadconftestmodules( basedir, importmode="prepend", rootpath=basedir, consider_namespace_packages=False, ) snap1 = len(conftest._dirpath2confmods) assert snap1 == 1 conftest._loadconftestmodules( basedir / "adir", importmode="prepend", rootpath=basedir, consider_namespace_packages=False, ) assert len(conftest._dirpath2confmods) == snap1 + 1 conftest._loadconftestmodules( basedir / "b", importmode="prepend", rootpath=basedir, consider_namespace_packages=False, ) assert len(conftest._dirpath2confmods) == snap1 + 2 def test_value_access_not_existing(self, basedir: Path) -> None: conftest = ConftestWithSetinitial(basedir) with pytest.raises(KeyError): conftest._rget_with_confmod("a", basedir) def test_value_access_by_path(self, basedir: Path) -> None: conftest = ConftestWithSetinitial(basedir) adir = basedir / "adir" conftest._loadconftestmodules( adir, importmode="prepend", rootpath=basedir, consider_namespace_packages=False, ) assert conftest._rget_with_confmod("a", adir)[1] == 1 conftest._loadconftestmodules( adir / "b", importmode="prepend", rootpath=basedir, consider_namespace_packages=False, ) assert conftest._rget_with_confmod("a", adir / "b")[1] == 1.5 def test_value_access_with_confmod(self, basedir: Path) -> None: startdir = basedir / "adir" / "b" startdir.joinpath("xx").mkdir() conftest = ConftestWithSetinitial(startdir) mod, value = conftest._rget_with_confmod("a", startdir) assert value == 1.5 assert mod.__file__ is not None path = Path(mod.__file__) assert path.parent == basedir / "adir" / "b" assert path.stem == "conftest" def test_conftest_in_nonpkg_with_init(tmp_path: Path, _sys_snapshot) -> None: tmp_path.joinpath("adir-1.0/b").mkdir(parents=True) tmp_path.joinpath("adir-1.0/conftest.py").write_text( "a=1 ; Directory = 3", encoding="utf-8" ) tmp_path.joinpath("adir-1.0/b/conftest.py").write_text( "b=2 ; a = 1.5", encoding="utf-8" ) tmp_path.joinpath("adir-1.0/b/__init__.py").touch() tmp_path.joinpath("adir-1.0/__init__.py").touch() ConftestWithSetinitial(tmp_path.joinpath("adir-1.0", "b")) def test_doubledash_considered(pytester: Pytester) -> None: conf = pytester.mkdir("--option") conf.joinpath("conftest.py").touch() conftest = PytestPluginManager() conftest_setinitial(conftest, [conf.name, conf.name]) values = conftest._getconftestmodules(conf) assert len(values) == 1 def test_issue151_load_all_conftests(pytester: Pytester) -> None: names = "code proj src".split() for name in names: p = pytester.mkdir(name) p.joinpath("conftest.py").touch() pm = PytestPluginManager() conftest_setinitial(pm, names) assert len(set(pm.get_plugins()) - {pm}) == len(names) def test_conftest_global_import(pytester: Pytester) -> None: pytester.makeconftest("x=3") p = pytester.makepyfile( """ from pathlib import Path import pytest from _pytest.config import PytestPluginManager conf = PytestPluginManager() mod = conf._importconftest( Path("conftest.py"), importmode="prepend", rootpath=Path.cwd(), consider_namespace_packages=False, ) assert mod.x == 3 import conftest assert conftest is mod, (conftest, mod) sub = Path("sub") sub.mkdir() subconf = sub / "conftest.py" subconf.write_text("y=4", encoding="utf-8") mod2 = conf._importconftest( subconf, importmode="prepend", rootpath=Path.cwd(), consider_namespace_packages=False, ) assert mod != mod2 assert mod2.y == 4 import conftest assert conftest is mod2, (conftest, mod) """ ) res = pytester.runpython(p) assert res.ret == 0 def test_conftestcutdir(pytester: Pytester) -> None: conf = pytester.makeconftest("") p = pytester.mkdir("x") conftest = PytestPluginManager() conftest_setinitial(conftest, [pytester.path], confcutdir=p) conftest._loadconftestmodules( p, importmode="prepend", rootpath=pytester.path, consider_namespace_packages=False, ) values = conftest._getconftestmodules(p) assert len(values) == 0 conftest._loadconftestmodules( conf.parent, importmode="prepend", rootpath=pytester.path, consider_namespace_packages=False, ) values = conftest._getconftestmodules(conf.parent) assert len(values) == 0 assert not conftest.has_plugin(str(conf)) # but we can still import a conftest directly conftest._importconftest( conf, importmode="prepend", rootpath=pytester.path, consider_namespace_packages=False, ) values = conftest._getconftestmodules(conf.parent) assert values[0].__file__ is not None assert values[0].__file__.startswith(str(conf)) # and all sub paths get updated properly values = conftest._getconftestmodules(p) assert len(values) == 1 assert values[0].__file__ is not None assert values[0].__file__.startswith(str(conf)) def test_conftestcutdir_inplace_considered(pytester: Pytester) -> None: conf = pytester.makeconftest("") conftest = PytestPluginManager() conftest_setinitial(conftest, [conf.parent], confcutdir=conf.parent) values = conftest._getconftestmodules(conf.parent) assert len(values) == 1 assert values[0].__file__ is not None assert values[0].__file__.startswith(str(conf)) @pytest.mark.parametrize("name", "test tests whatever .dotdir".split()) def test_setinitial_conftest_subdirs(pytester: Pytester, name: str) -> None: sub = pytester.mkdir(name) subconftest = sub.joinpath("conftest.py") subconftest.touch() pm = PytestPluginManager() conftest_setinitial(pm, [sub.parent], confcutdir=pytester.path) key = subconftest.resolve() if name not in ("whatever", ".dotdir"): assert pm.has_plugin(str(key)) assert len(set(pm.get_plugins()) - {pm}) == 1 else: assert not pm.has_plugin(str(key)) assert len(set(pm.get_plugins()) - {pm}) == 0 def test_conftest_confcutdir(pytester: Pytester) -> None: pytester.makeconftest("assert 0") x = pytester.mkdir("x") x.joinpath("conftest.py").write_text( textwrap.dedent( """\ def pytest_addoption(parser): parser.addoption("--xyz", action="store_true") """ ), encoding="utf-8", ) result = pytester.runpytest("-h", f"--confcutdir={x}", x) result.stdout.fnmatch_lines(["*--xyz*"]) result.stdout.no_fnmatch_line("*warning: could not load initial*") def test_installed_conftest_is_picked_up(pytester: Pytester, tmp_path: Path) -> None: """When using `--pyargs` to run tests in an installed packages (located e.g. in a site-packages in the PYTHONPATH), conftest files in there are picked up. Regression test for #9767. """ # pytester dir - the source tree. # tmp_path - the simulated site-packages dir (not in source tree). pytester.syspathinsert(tmp_path) pytester.makepyprojecttoml("[tool.pytest.ini_options]") tmp_path.joinpath("foo").mkdir() tmp_path.joinpath("foo", "__init__.py").touch() tmp_path.joinpath("foo", "conftest.py").write_text( textwrap.dedent( """\ import pytest @pytest.fixture def fix(): return None """ ), encoding="utf-8", ) tmp_path.joinpath("foo", "test_it.py").write_text( "def test_it(fix): pass", encoding="utf-8" ) result = pytester.runpytest("--pyargs", "foo") assert result.ret == 0 def test_conftest_symlink(pytester: Pytester) -> None: """`conftest.py` discovery follows normal path resolution and does not resolve symlinks.""" # Structure: # /real # /real/conftest.py # /real/app # /real/app/tests # /real/app/tests/test_foo.py # Links: # /symlinktests -> /real/app/tests (running at symlinktests should fail) # /symlink -> /real (running at /symlink should work) real = pytester.mkdir("real") realtests = real.joinpath("app/tests") realtests.mkdir(parents=True) symlink_or_skip(realtests, pytester.path.joinpath("symlinktests")) symlink_or_skip(real, pytester.path.joinpath("symlink")) pytester.makepyfile( **{ "real/app/tests/test_foo.py": "def test1(fixture): pass", "real/conftest.py": textwrap.dedent( """ import pytest print("conftest_loaded") @pytest.fixture def fixture(): print("fixture_used") """ ), } ) # Should fail because conftest cannot be found from the link structure. result = pytester.runpytest("-vs", "symlinktests") result.stdout.fnmatch_lines(["*fixture 'fixture' not found*"]) assert result.ret == ExitCode.TESTS_FAILED # Should not cause "ValueError: Plugin already registered" (#4174). result = pytester.runpytest("-vs", "symlink") assert result.ret == ExitCode.OK def test_conftest_symlink_files(pytester: Pytester) -> None: """Symlinked conftest.py are found when pytest is executed in a directory with symlinked files.""" real = pytester.mkdir("real") source = { "app/test_foo.py": "def test1(fixture): pass", "app/__init__.py": "", "app/conftest.py": textwrap.dedent( """ import pytest print("conftest_loaded") @pytest.fixture def fixture(): print("fixture_used") """ ), } pytester.makepyfile(**{f"real/{k}": v for k, v in source.items()}) # Create a build directory that contains symlinks to actual files # but doesn't symlink actual directories. build = pytester.mkdir("build") build.joinpath("app").mkdir() for f in source: symlink_or_skip(real.joinpath(f), build.joinpath(f)) os.chdir(build) result = pytester.runpytest("-vs", "app/test_foo.py") result.stdout.fnmatch_lines(["*conftest_loaded*", "PASSED"]) assert result.ret == ExitCode.OK @pytest.mark.skipif( os.path.normcase("x") != os.path.normcase("X"), reason="only relevant for case-insensitive file systems", ) def test_conftest_badcase(pytester: Pytester) -> None: """Check conftest.py loading when directory casing is wrong (#5792).""" pytester.path.joinpath("JenkinsRoot/test").mkdir(parents=True) source = {"setup.py": "", "test/__init__.py": "", "test/conftest.py": ""} pytester.makepyfile(**{f"JenkinsRoot/{k}": v for k, v in source.items()}) os.chdir(pytester.path.joinpath("jenkinsroot/test")) result = pytester.runpytest() assert result.ret == ExitCode.NO_TESTS_COLLECTED def test_conftest_uppercase(pytester: Pytester) -> None: """Check conftest.py whose qualified name contains uppercase characters (#5819)""" source = {"__init__.py": "", "Foo/conftest.py": "", "Foo/__init__.py": ""} pytester.makepyfile(**source) os.chdir(pytester.path) result = pytester.runpytest() assert result.ret == ExitCode.NO_TESTS_COLLECTED def test_no_conftest(pytester: Pytester) -> None: pytester.makeconftest("assert 0") result = pytester.runpytest("--noconftest") assert result.ret == ExitCode.NO_TESTS_COLLECTED result = pytester.runpytest() assert result.ret == ExitCode.USAGE_ERROR def test_conftest_existing_junitxml(pytester: Pytester) -> None: x = pytester.mkdir("tests") x.joinpath("conftest.py").write_text( textwrap.dedent( """\ def pytest_addoption(parser): parser.addoption("--xyz", action="store_true") """ ), encoding="utf-8", ) pytester.makefile(ext=".xml", junit="") # Writes junit.xml result = pytester.runpytest("-h", "--junitxml", "junit.xml") result.stdout.fnmatch_lines(["*--xyz*"]) def test_conftest_import_order(pytester: Pytester, monkeypatch: MonkeyPatch) -> None: ct1 = pytester.makeconftest("") sub = pytester.mkdir("sub") ct2 = sub / "conftest.py" ct2.write_text("", encoding="utf-8") def impct(p, importmode, root, consider_namespace_packages): return p conftest = PytestPluginManager() conftest._confcutdir = pytester.path monkeypatch.setattr(conftest, "_importconftest", impct) conftest._loadconftestmodules( sub, importmode="prepend", rootpath=pytester.path, consider_namespace_packages=False, ) mods = cast(List[Path], conftest._getconftestmodules(sub)) expected = [ct1, ct2] assert mods == expected def test_fixture_dependency(pytester: Pytester) -> None: pytester.makeconftest("") pytester.path.joinpath("__init__.py").touch() sub = pytester.mkdir("sub") sub.joinpath("__init__.py").touch() sub.joinpath("conftest.py").write_text( textwrap.dedent( """\ import pytest @pytest.fixture def not_needed(): assert False, "Should not be called!" @pytest.fixture def foo(): assert False, "Should not be called!" @pytest.fixture def bar(foo): return 'bar' """ ), encoding="utf-8", ) subsub = sub.joinpath("subsub") subsub.mkdir() subsub.joinpath("__init__.py").touch() subsub.joinpath("test_bar.py").write_text( textwrap.dedent( """\ import pytest @pytest.fixture def bar(): return 'sub bar' def test_event_fixture(bar): assert bar == 'sub bar' """ ), encoding="utf-8", ) result = pytester.runpytest("sub") result.stdout.fnmatch_lines(["*1 passed*"]) def test_conftest_found_with_double_dash(pytester: Pytester) -> None: sub = pytester.mkdir("sub") sub.joinpath("conftest.py").write_text( textwrap.dedent( """\ def pytest_addoption(parser): parser.addoption("--hello-world", action="store_true") """ ), encoding="utf-8", ) p = sub.joinpath("test_hello.py") p.write_text("def test_hello(): pass", encoding="utf-8") result = pytester.runpytest(str(p) + "::test_hello", "-h") result.stdout.fnmatch_lines( """ *--hello-world* """ ) class TestConftestVisibility: def _setup_tree(self, pytester: Pytester) -> dict[str, Path]: # for issue616 # example mostly taken from: # https://mail.python.org/pipermail/pytest-dev/2014-September/002617.html runner = pytester.mkdir("empty") package = pytester.mkdir("package") package.joinpath("conftest.py").write_text( textwrap.dedent( """\ import pytest @pytest.fixture def fxtr(): return "from-package" """ ), encoding="utf-8", ) package.joinpath("test_pkgroot.py").write_text( textwrap.dedent( """\ def test_pkgroot(fxtr): assert fxtr == "from-package" """ ), encoding="utf-8", ) swc = package.joinpath("swc") swc.mkdir() swc.joinpath("__init__.py").touch() swc.joinpath("conftest.py").write_text( textwrap.dedent( """\ import pytest @pytest.fixture def fxtr(): return "from-swc" """ ), encoding="utf-8", ) swc.joinpath("test_with_conftest.py").write_text( textwrap.dedent( """\ def test_with_conftest(fxtr): assert fxtr == "from-swc" """ ), encoding="utf-8", ) snc = package.joinpath("snc") snc.mkdir() snc.joinpath("__init__.py").touch() snc.joinpath("test_no_conftest.py").write_text( textwrap.dedent( """\ def test_no_conftest(fxtr): assert fxtr == "from-package" # No local conftest.py, so should # use value from parent dir's """ ), encoding="utf-8", ) print("created directory structure:") for x in pytester.path.glob("**/"): print(" " + str(x.relative_to(pytester.path))) return {"runner": runner, "package": package, "swc": swc, "snc": snc} # N.B.: "swc" stands for "subdir with conftest.py" # "snc" stands for "subdir no [i.e. without] conftest.py" @pytest.mark.parametrize( "chdir,testarg,expect_ntests_passed", [ # Effective target: package/.. ("runner", "..", 3), ("package", "..", 3), ("swc", "../..", 3), ("snc", "../..", 3), # Effective target: package ("runner", "../package", 3), ("package", ".", 3), ("swc", "..", 3), ("snc", "..", 3), # Effective target: package/swc ("runner", "../package/swc", 1), ("package", "./swc", 1), ("swc", ".", 1), ("snc", "../swc", 1), # Effective target: package/snc ("runner", "../package/snc", 1), ("package", "./snc", 1), ("swc", "../snc", 1), ("snc", ".", 1), ], ) def test_parsefactories_relative_node_ids( self, pytester: Pytester, chdir: str, testarg: str, expect_ntests_passed: int ) -> None: """#616""" dirs = self._setup_tree(pytester) print(f"pytest run in cwd: {dirs[chdir].relative_to(pytester.path)}") print(f"pytestarg : {testarg}") print(f"expected pass : {expect_ntests_passed}") os.chdir(dirs[chdir]) reprec = pytester.inline_run( testarg, "-q", "--traceconfig", "--confcutdir", pytester.path, ) reprec.assertoutcome(passed=expect_ntests_passed) @pytest.mark.parametrize( "confcutdir,passed,error", [(".", 2, 0), ("src", 1, 1), (None, 1, 1)] ) def test_search_conftest_up_to_inifile( pytester: Pytester, confcutdir: str, passed: int, error: int ) -> None: """Test that conftest files are detected only up to an ini file, unless an explicit --confcutdir option is given. """ root = pytester.path src = root.joinpath("src") src.mkdir() src.joinpath("pytest.ini").write_text("[pytest]", encoding="utf-8") src.joinpath("conftest.py").write_text( textwrap.dedent( """\ import pytest @pytest.fixture def fix1(): pass """ ), encoding="utf-8", ) src.joinpath("test_foo.py").write_text( textwrap.dedent( """\ def test_1(fix1): pass def test_2(out_of_reach): pass """ ), encoding="utf-8", ) root.joinpath("conftest.py").write_text( textwrap.dedent( """\ import pytest @pytest.fixture def out_of_reach(): pass """ ), encoding="utf-8", ) args = [str(src)] if confcutdir: args = [f"--confcutdir={root.joinpath(confcutdir)}"] result = pytester.runpytest(*args) match = "" if passed: match += "*%d passed*" % passed if error: match += "*%d error*" % error result.stdout.fnmatch_lines(match) def test_issue1073_conftest_special_objects(pytester: Pytester) -> None: pytester.makeconftest( """\ class DontTouchMe(object): def __getattr__(self, x): raise Exception('cant touch me') x = DontTouchMe() """ ) pytester.makepyfile( """\ def test_some(): pass """ ) res = pytester.runpytest() assert res.ret == 0 def test_conftest_exception_handling(pytester: Pytester) -> None: pytester.makeconftest( """\ raise ValueError() """ ) pytester.makepyfile( """\ def test_some(): pass """ ) res = pytester.runpytest() assert res.ret == 4 assert "raise ValueError()" in [line.strip() for line in res.errlines] def test_hook_proxy(pytester: Pytester) -> None: """Session's gethookproxy() would cache conftests incorrectly (#2016). It was decided to remove the cache altogether. """ pytester.makepyfile( **{ "root/demo-0/test_foo1.py": "def test1(): pass", "root/demo-a/test_foo2.py": "def test1(): pass", "root/demo-a/conftest.py": """\ def pytest_ignore_collect(collection_path, config): return True """, "root/demo-b/test_foo3.py": "def test1(): pass", "root/demo-c/test_foo4.py": "def test1(): pass", } ) result = pytester.runpytest() result.stdout.fnmatch_lines( ["*test_foo1.py*", "*test_foo3.py*", "*test_foo4.py*", "*3 passed*"] ) def test_required_option_help(pytester: Pytester) -> None: pytester.makeconftest("assert 0") x = pytester.mkdir("x") x.joinpath("conftest.py").write_text( textwrap.dedent( """\ def pytest_addoption(parser): parser.addoption("--xyz", action="store_true", required=True) """ ), encoding="utf-8", ) result = pytester.runpytest("-h", x) result.stdout.no_fnmatch_line("*argument --xyz is required*") assert "general:" in result.stdout.str()