Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
This commit is contained in:
		
							parent
							
								
									c17d50829f
								
							
						
					
					
						commit
						322190fd84
					
				|  | @ -0,0 +1,9 @@ | ||||||
|  | symlinks are no longer resolved during collection and matching `conftest.py` files with test file paths. | ||||||
|  | 
 | ||||||
|  | Resolving symlinks for the current directory and during collection was introduced as a bugfix in 3.9.0, but it actually is a new feature which had unfortunate consequences in Windows and surprising results in other platforms. | ||||||
|  | 
 | ||||||
|  | The team decided to step back on resolving symlinks at all, planning to review this in the future with a more solid solution (see discussion in | ||||||
|  | `#6523 <https://github.com/pytest-dev/pytest/pull/6523>`__ for details). | ||||||
|  | 
 | ||||||
|  | This might break test suites which made use of this feature; the fix is to create a symlink | ||||||
|  | for the entire test tree, and not only to partial files/tress as it was possible previously. | ||||||
|  | @ -123,9 +123,9 @@ def _py36_windowsconsoleio_workaround(stream: TextIO) -> None: | ||||||
|         return |         return | ||||||
| 
 | 
 | ||||||
|     buffered = hasattr(stream.buffer, "raw") |     buffered = hasattr(stream.buffer, "raw") | ||||||
|     raw_stdout = stream.buffer.raw if buffered else stream.buffer |     raw_stdout = stream.buffer.raw if buffered else stream.buffer  # type: ignore[attr-defined] | ||||||
| 
 | 
 | ||||||
|     if not isinstance(raw_stdout, io._WindowsConsoleIO): |     if not isinstance(raw_stdout, io._WindowsConsoleIO):  # type: ignore[attr-defined] | ||||||
|         return |         return | ||||||
| 
 | 
 | ||||||
|     def _reopen_stdio(f, mode): |     def _reopen_stdio(f, mode): | ||||||
|  | @ -135,7 +135,7 @@ def _py36_windowsconsoleio_workaround(stream: TextIO) -> None: | ||||||
|             buffering = -1 |             buffering = -1 | ||||||
| 
 | 
 | ||||||
|         return io.TextIOWrapper( |         return io.TextIOWrapper( | ||||||
|             open(os.dup(f.fileno()), mode, buffering), |             open(os.dup(f.fileno()), mode, buffering),  # type: ignore[arg-type] | ||||||
|             f.encoding, |             f.encoding, | ||||||
|             f.errors, |             f.errors, | ||||||
|             f.newlines, |             f.newlines, | ||||||
|  |  | ||||||
|  | @ -232,7 +232,7 @@ def get_config(args=None, plugins=None): | ||||||
|     config = Config( |     config = Config( | ||||||
|         pluginmanager, |         pluginmanager, | ||||||
|         invocation_params=Config.InvocationParams( |         invocation_params=Config.InvocationParams( | ||||||
|             args=args or (), plugins=plugins, dir=Path().resolve() |             args=args or (), plugins=plugins, dir=Path.cwd() | ||||||
|         ), |         ), | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|  | @ -477,7 +477,7 @@ class PytestPluginManager(PluginManager): | ||||||
|         # and allow users to opt into looking into the rootdir parent |         # and allow users to opt into looking into the rootdir parent | ||||||
|         # directories instead of requiring to specify confcutdir |         # directories instead of requiring to specify confcutdir | ||||||
|         clist = [] |         clist = [] | ||||||
|         for parent in directory.realpath().parts(): |         for parent in directory.parts(): | ||||||
|             if self._confcutdir and self._confcutdir.relto(parent): |             if self._confcutdir and self._confcutdir.relto(parent): | ||||||
|                 continue |                 continue | ||||||
|             conftestpath = parent.join("conftest.py") |             conftestpath = parent.join("conftest.py") | ||||||
|  | @ -798,7 +798,7 @@ class Config: | ||||||
| 
 | 
 | ||||||
|         if invocation_params is None: |         if invocation_params is None: | ||||||
|             invocation_params = self.InvocationParams( |             invocation_params = self.InvocationParams( | ||||||
|                 args=(), plugins=None, dir=Path().resolve() |                 args=(), plugins=None, dir=Path.cwd() | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|         self.option = argparse.Namespace() |         self.option = argparse.Namespace() | ||||||
|  |  | ||||||
|  | @ -1496,7 +1496,7 @@ class FixtureManager: | ||||||
|     def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: |     def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: | ||||||
|         nodeid = None |         nodeid = None | ||||||
|         try: |         try: | ||||||
|             p = py.path.local(plugin.__file__).realpath()  # type: ignore[attr-defined] # noqa: F821 |             p = py.path.local(plugin.__file__)  # type: ignore[attr-defined] # noqa: F821 | ||||||
|         except AttributeError: |         except AttributeError: | ||||||
|             pass |             pass | ||||||
|         else: |         else: | ||||||
|  |  | ||||||
|  | @ -665,7 +665,6 @@ class Session(nodes.FSCollector): | ||||||
|                     "file or package not found: " + arg + " (missing __init__.py?)" |                     "file or package not found: " + arg + " (missing __init__.py?)" | ||||||
|                 ) |                 ) | ||||||
|             raise UsageError("file not found: " + arg) |             raise UsageError("file not found: " + arg) | ||||||
|         fspath = fspath.realpath() |  | ||||||
|         return (fspath, parts) |         return (fspath, parts) | ||||||
| 
 | 
 | ||||||
|     def matchnodes( |     def matchnodes( | ||||||
|  |  | ||||||
|  | @ -18,6 +18,7 @@ from typing import Set | ||||||
| from typing import TypeVar | from typing import TypeVar | ||||||
| from typing import Union | from typing import Union | ||||||
| 
 | 
 | ||||||
|  | from _pytest.outcomes import skip | ||||||
| from _pytest.warning_types import PytestWarning | from _pytest.warning_types import PytestWarning | ||||||
| 
 | 
 | ||||||
| if sys.version_info[:2] >= (3, 6): | if sys.version_info[:2] >= (3, 6): | ||||||
|  | @ -397,3 +398,11 @@ def fnmatch_ex(pattern: str, path) -> bool: | ||||||
| def parts(s: str) -> Set[str]: | def parts(s: str) -> Set[str]: | ||||||
|     parts = s.split(sep) |     parts = s.split(sep) | ||||||
|     return {sep.join(parts[: i + 1]) or sep for i in range(len(parts))} |     return {sep.join(parts[: i + 1]) or sep for i in range(len(parts))} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def symlink_or_skip(src, dst, **kwargs): | ||||||
|  |     """Makes a symlink or skips the test in case symlinks are not supported.""" | ||||||
|  |     try: | ||||||
|  |         os.symlink(str(src), str(dst), **kwargs) | ||||||
|  |     except OSError as e: | ||||||
|  |         skip("symlinks not supported: {}".format(e)) | ||||||
|  |  | ||||||
|  | @ -1,6 +1,5 @@ | ||||||
| import os | import os | ||||||
| import sys | import sys | ||||||
| import textwrap |  | ||||||
| import types | import types | ||||||
| 
 | 
 | ||||||
| import attr | import attr | ||||||
|  | @ -9,6 +8,7 @@ import py | ||||||
| import pytest | import pytest | ||||||
| from _pytest.compat import importlib_metadata | from _pytest.compat import importlib_metadata | ||||||
| from _pytest.config import ExitCode | from _pytest.config import ExitCode | ||||||
|  | from _pytest.pathlib import symlink_or_skip | ||||||
| from _pytest.pytester import Testdir | from _pytest.pytester import Testdir | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -266,29 +266,6 @@ class TestGeneralUsage: | ||||||
|         assert result.ret != 0 |         assert result.ret != 0 | ||||||
|         assert "should be seen" in result.stdout.str() |         assert "should be seen" in result.stdout.str() | ||||||
| 
 | 
 | ||||||
|     @pytest.mark.skipif( |  | ||||||
|         not hasattr(py.path.local, "mksymlinkto"), |  | ||||||
|         reason="symlink not available on this platform", |  | ||||||
|     ) |  | ||||||
|     def test_chdir(self, testdir): |  | ||||||
|         testdir.tmpdir.join("py").mksymlinkto(py._pydir) |  | ||||||
|         p = testdir.tmpdir.join("main.py") |  | ||||||
|         p.write( |  | ||||||
|             textwrap.dedent( |  | ||||||
|                 """\ |  | ||||||
|                 import sys, os |  | ||||||
|                 sys.path.insert(0, '') |  | ||||||
|                 import py |  | ||||||
|                 print(py.__file__) |  | ||||||
|                 print(py.__path__) |  | ||||||
|                 os.chdir(os.path.dirname(os.getcwd())) |  | ||||||
|                 print(py.log) |  | ||||||
|                 """ |  | ||||||
|             ) |  | ||||||
|         ) |  | ||||||
|         result = testdir.runpython(p) |  | ||||||
|         assert not result.ret |  | ||||||
| 
 |  | ||||||
|     def test_issue109_sibling_conftests_not_loaded(self, testdir): |     def test_issue109_sibling_conftests_not_loaded(self, testdir): | ||||||
|         sub1 = testdir.mkdir("sub1") |         sub1 = testdir.mkdir("sub1") | ||||||
|         sub2 = testdir.mkdir("sub2") |         sub2 = testdir.mkdir("sub2") | ||||||
|  | @ -762,19 +739,9 @@ class TestInvocationVariants: | ||||||
| 
 | 
 | ||||||
|     def test_cmdline_python_package_symlink(self, testdir, monkeypatch): |     def test_cmdline_python_package_symlink(self, testdir, monkeypatch): | ||||||
|         """ |         """ | ||||||
|         test --pyargs option with packages with path containing symlink can |         --pyargs with packages with path containing symlink can have conftest.py in | ||||||
|         have conftest.py in their package (#2985) |         their package (#2985) | ||||||
|         """ |         """ | ||||||
|         # dummy check that we can actually create symlinks: on Windows `os.symlink` is available, |  | ||||||
|         # but normal users require special admin privileges to create symlinks. |  | ||||||
|         if sys.platform == "win32": |  | ||||||
|             try: |  | ||||||
|                 os.symlink( |  | ||||||
|                     str(testdir.tmpdir.ensure("tmpfile")), |  | ||||||
|                     str(testdir.tmpdir.join("tmpfile2")), |  | ||||||
|                 ) |  | ||||||
|             except OSError as e: |  | ||||||
|                 pytest.skip(str(e.args[0])) |  | ||||||
|         monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False) |         monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False) | ||||||
| 
 | 
 | ||||||
|         dirname = "lib" |         dirname = "lib" | ||||||
|  | @ -790,13 +757,13 @@ class TestInvocationVariants: | ||||||
|             "import pytest\n@pytest.fixture\ndef a_fixture():pass" |             "import pytest\n@pytest.fixture\ndef a_fixture():pass" | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         d_local = testdir.mkdir("local") |         d_local = testdir.mkdir("symlink_root") | ||||||
|         symlink_location = os.path.join(str(d_local), "lib") |         symlink_location = d_local / "lib" | ||||||
|         os.symlink(str(d), symlink_location, target_is_directory=True) |         symlink_or_skip(d, symlink_location, target_is_directory=True) | ||||||
| 
 | 
 | ||||||
|         # The structure of the test directory is now: |         # The structure of the test directory is now: | ||||||
|         # . |         # . | ||||||
|         # ├── local |         # ├── symlink_root | ||||||
|         # │   └── lib -> ../lib |         # │   └── lib -> ../lib | ||||||
|         # └── lib |         # └── lib | ||||||
|         #     └── foo |         #     └── foo | ||||||
|  | @ -807,32 +774,23 @@ class TestInvocationVariants: | ||||||
|         #             └── test_bar.py |         #             └── test_bar.py | ||||||
| 
 | 
 | ||||||
|         # NOTE: the different/reversed ordering is intentional here. |         # NOTE: the different/reversed ordering is intentional here. | ||||||
|         search_path = ["lib", os.path.join("local", "lib")] |         search_path = ["lib", os.path.join("symlink_root", "lib")] | ||||||
|         monkeypatch.setenv("PYTHONPATH", prepend_pythonpath(*search_path)) |         monkeypatch.setenv("PYTHONPATH", prepend_pythonpath(*search_path)) | ||||||
|         for p in search_path: |         for p in search_path: | ||||||
|             monkeypatch.syspath_prepend(p) |             monkeypatch.syspath_prepend(p) | ||||||
| 
 | 
 | ||||||
|         # module picked up in symlink-ed directory: |         # module picked up in symlink-ed directory: | ||||||
|         # It picks up local/lib/foo/bar (symlink) via sys.path. |         # It picks up symlink_root/lib/foo/bar (symlink) via sys.path. | ||||||
|         result = testdir.runpytest("--pyargs", "-v", "foo.bar") |         result = testdir.runpytest("--pyargs", "-v", "foo.bar") | ||||||
|         testdir.chdir() |         testdir.chdir() | ||||||
|         assert result.ret == 0 |         assert result.ret == 0 | ||||||
|         if hasattr(py.path.local, "mksymlinkto"): |         result.stdout.fnmatch_lines( | ||||||
|             result.stdout.fnmatch_lines( |             [ | ||||||
|                 [ |                 "symlink_root/lib/foo/bar/test_bar.py::test_bar PASSED*", | ||||||
|                     "lib/foo/bar/test_bar.py::test_bar PASSED*", |                 "symlink_root/lib/foo/bar/test_bar.py::test_other PASSED*", | ||||||
|                     "lib/foo/bar/test_bar.py::test_other PASSED*", |                 "*2 passed*", | ||||||
|                     "*2 passed*", |             ] | ||||||
|                 ] |         ) | ||||||
|             ) |  | ||||||
|         else: |  | ||||||
|             result.stdout.fnmatch_lines( |  | ||||||
|                 [ |  | ||||||
|                     "*lib/foo/bar/test_bar.py::test_bar PASSED*", |  | ||||||
|                     "*lib/foo/bar/test_bar.py::test_other PASSED*", |  | ||||||
|                     "*2 passed*", |  | ||||||
|                 ] |  | ||||||
|             ) |  | ||||||
| 
 | 
 | ||||||
|     def test_cmdline_python_package_not_exists(self, testdir): |     def test_cmdline_python_package_not_exists(self, testdir): | ||||||
|         result = testdir.runpytest("--pyargs", "tpkgwhatv") |         result = testdir.runpytest("--pyargs", "tpkgwhatv") | ||||||
|  |  | ||||||
|  | @ -3,12 +3,11 @@ import pprint | ||||||
| import sys | import sys | ||||||
| import textwrap | import textwrap | ||||||
| 
 | 
 | ||||||
| import py |  | ||||||
| 
 |  | ||||||
| import pytest | import pytest | ||||||
| from _pytest.config import ExitCode | from _pytest.config import ExitCode | ||||||
| from _pytest.main import _in_venv | from _pytest.main import _in_venv | ||||||
| from _pytest.main import Session | from _pytest.main import Session | ||||||
|  | from _pytest.pathlib import symlink_or_skip | ||||||
| from _pytest.pytester import Testdir | from _pytest.pytester import Testdir | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -1164,29 +1163,21 @@ def test_collect_pyargs_with_testpaths(testdir, monkeypatch): | ||||||
|     result.stdout.fnmatch_lines(["*1 passed in*"]) |     result.stdout.fnmatch_lines(["*1 passed in*"]) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.skipif( |  | ||||||
|     not hasattr(py.path.local, "mksymlinkto"), |  | ||||||
|     reason="symlink not available on this platform", |  | ||||||
| ) |  | ||||||
| def test_collect_symlink_file_arg(testdir): | def test_collect_symlink_file_arg(testdir): | ||||||
|     """Test that collecting a direct symlink, where the target does not match python_files works (#4325).""" |     """Collect a direct symlink works even if it does not match python_files (#4325).""" | ||||||
|     real = testdir.makepyfile( |     real = testdir.makepyfile( | ||||||
|         real=""" |         real=""" | ||||||
|         def test_nodeid(request): |         def test_nodeid(request): | ||||||
|             assert request.node.nodeid == "real.py::test_nodeid" |             assert request.node.nodeid == "symlink.py::test_nodeid" | ||||||
|         """ |         """ | ||||||
|     ) |     ) | ||||||
|     symlink = testdir.tmpdir.join("symlink.py") |     symlink = testdir.tmpdir.join("symlink.py") | ||||||
|     symlink.mksymlinkto(real) |     symlink_or_skip(real, symlink) | ||||||
|     result = testdir.runpytest("-v", symlink) |     result = testdir.runpytest("-v", symlink) | ||||||
|     result.stdout.fnmatch_lines(["real.py::test_nodeid PASSED*", "*1 passed in*"]) |     result.stdout.fnmatch_lines(["symlink.py::test_nodeid PASSED*", "*1 passed in*"]) | ||||||
|     assert result.ret == 0 |     assert result.ret == 0 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.skipif( |  | ||||||
|     not hasattr(py.path.local, "mksymlinkto"), |  | ||||||
|     reason="symlink not available on this platform", |  | ||||||
| ) |  | ||||||
| def test_collect_symlink_out_of_tree(testdir): | def test_collect_symlink_out_of_tree(testdir): | ||||||
|     """Test collection of symlink via out-of-tree rootdir.""" |     """Test collection of symlink via out-of-tree rootdir.""" | ||||||
|     sub = testdir.tmpdir.join("sub") |     sub = testdir.tmpdir.join("sub") | ||||||
|  | @ -1204,7 +1195,7 @@ def test_collect_symlink_out_of_tree(testdir): | ||||||
| 
 | 
 | ||||||
|     out_of_tree = testdir.tmpdir.join("out_of_tree").ensure(dir=True) |     out_of_tree = testdir.tmpdir.join("out_of_tree").ensure(dir=True) | ||||||
|     symlink_to_sub = out_of_tree.join("symlink_to_sub") |     symlink_to_sub = out_of_tree.join("symlink_to_sub") | ||||||
|     symlink_to_sub.mksymlinkto(sub) |     symlink_or_skip(sub, symlink_to_sub) | ||||||
|     sub.chdir() |     sub.chdir() | ||||||
|     result = testdir.runpytest("-vs", "--rootdir=%s" % sub, symlink_to_sub) |     result = testdir.runpytest("-vs", "--rootdir=%s" % sub, symlink_to_sub) | ||||||
|     result.stdout.fnmatch_lines( |     result.stdout.fnmatch_lines( | ||||||
|  | @ -1270,22 +1261,19 @@ def test_collect_pkg_init_only(testdir): | ||||||
|     result.stdout.fnmatch_lines(["sub/__init__.py::test_init PASSED*", "*1 passed in*"]) |     result.stdout.fnmatch_lines(["sub/__init__.py::test_init PASSED*", "*1 passed in*"]) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.skipif( |  | ||||||
|     not hasattr(py.path.local, "mksymlinkto"), |  | ||||||
|     reason="symlink not available on this platform", |  | ||||||
| ) |  | ||||||
| @pytest.mark.parametrize("use_pkg", (True, False)) | @pytest.mark.parametrize("use_pkg", (True, False)) | ||||||
| def test_collect_sub_with_symlinks(use_pkg, testdir): | def test_collect_sub_with_symlinks(use_pkg, testdir): | ||||||
|  |     """Collection works with symlinked files and broken symlinks""" | ||||||
|     sub = testdir.mkdir("sub") |     sub = testdir.mkdir("sub") | ||||||
|     if use_pkg: |     if use_pkg: | ||||||
|         sub.ensure("__init__.py") |         sub.ensure("__init__.py") | ||||||
|     sub.ensure("test_file.py").write("def test_file(): pass") |     sub.join("test_file.py").write("def test_file(): pass") | ||||||
| 
 | 
 | ||||||
|     # Create a broken symlink. |     # Create a broken symlink. | ||||||
|     sub.join("test_broken.py").mksymlinkto("test_doesnotexist.py") |     symlink_or_skip("test_doesnotexist.py", sub.join("test_broken.py")) | ||||||
| 
 | 
 | ||||||
|     # Symlink that gets collected. |     # Symlink that gets collected. | ||||||
|     sub.join("test_symlink.py").mksymlinkto("test_file.py") |     symlink_or_skip("test_file.py", sub.join("test_symlink.py")) | ||||||
| 
 | 
 | ||||||
|     result = testdir.runpytest("-v", str(sub)) |     result = testdir.runpytest("-v", str(sub)) | ||||||
|     result.stdout.fnmatch_lines( |     result.stdout.fnmatch_lines( | ||||||
|  |  | ||||||
|  | @ -7,6 +7,7 @@ import pytest | ||||||
| from _pytest.config import ExitCode | from _pytest.config import ExitCode | ||||||
| from _pytest.config import PytestPluginManager | from _pytest.config import PytestPluginManager | ||||||
| from _pytest.pathlib import Path | from _pytest.pathlib import Path | ||||||
|  | from _pytest.pathlib import symlink_or_skip | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def ConftestWithSetinitial(path): | def ConftestWithSetinitial(path): | ||||||
|  | @ -190,16 +191,25 @@ def test_conftest_confcutdir(testdir): | ||||||
|     result.stdout.no_fnmatch_line("*warning: could not load initial*") |     result.stdout.no_fnmatch_line("*warning: could not load initial*") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.skipif( |  | ||||||
|     not hasattr(py.path.local, "mksymlinkto"), |  | ||||||
|     reason="symlink not available on this platform", |  | ||||||
| ) |  | ||||||
| def test_conftest_symlink(testdir): | def test_conftest_symlink(testdir): | ||||||
|     """Ensure that conftest.py is used for resolved symlinks.""" |     """ | ||||||
|  |     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 = testdir.tmpdir.mkdir("real") |     real = testdir.tmpdir.mkdir("real") | ||||||
|     realtests = real.mkdir("app").mkdir("tests") |     realtests = real.mkdir("app").mkdir("tests") | ||||||
|     testdir.tmpdir.join("symlinktests").mksymlinkto(realtests) |     symlink_or_skip(realtests, testdir.tmpdir.join("symlinktests")) | ||||||
|     testdir.tmpdir.join("symlink").mksymlinkto(real) |     symlink_or_skip(real, testdir.tmpdir.join("symlink")) | ||||||
|     testdir.makepyfile( |     testdir.makepyfile( | ||||||
|         **{ |         **{ | ||||||
|             "real/app/tests/test_foo.py": "def test1(fixture): pass", |             "real/app/tests/test_foo.py": "def test1(fixture): pass", | ||||||
|  | @ -216,38 +226,20 @@ def test_conftest_symlink(testdir): | ||||||
|             ), |             ), | ||||||
|         } |         } | ||||||
|     ) |     ) | ||||||
|  | 
 | ||||||
|  |     # Should fail because conftest cannot be found from the link structure. | ||||||
|     result = testdir.runpytest("-vs", "symlinktests") |     result = testdir.runpytest("-vs", "symlinktests") | ||||||
|     result.stdout.fnmatch_lines( |     result.stdout.fnmatch_lines(["*fixture 'fixture' not found*"]) | ||||||
|         [ |     assert result.ret == ExitCode.TESTS_FAILED | ||||||
|             "*conftest_loaded*", |  | ||||||
|             "real/app/tests/test_foo.py::test1 fixture_used", |  | ||||||
|             "PASSED", |  | ||||||
|         ] |  | ||||||
|     ) |  | ||||||
|     assert result.ret == ExitCode.OK |  | ||||||
| 
 | 
 | ||||||
|     # Should not cause "ValueError: Plugin already registered" (#4174). |     # Should not cause "ValueError: Plugin already registered" (#4174). | ||||||
|     result = testdir.runpytest("-vs", "symlink") |     result = testdir.runpytest("-vs", "symlink") | ||||||
|     assert result.ret == ExitCode.OK |     assert result.ret == ExitCode.OK | ||||||
| 
 | 
 | ||||||
|     realtests.ensure("__init__.py") |  | ||||||
|     result = testdir.runpytest("-vs", "symlinktests/test_foo.py::test1") |  | ||||||
|     result.stdout.fnmatch_lines( |  | ||||||
|         [ |  | ||||||
|             "*conftest_loaded*", |  | ||||||
|             "real/app/tests/test_foo.py::test1 fixture_used", |  | ||||||
|             "PASSED", |  | ||||||
|         ] |  | ||||||
|     ) |  | ||||||
|     assert result.ret == ExitCode.OK |  | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| @pytest.mark.skipif( |  | ||||||
|     not hasattr(py.path.local, "mksymlinkto"), |  | ||||||
|     reason="symlink not available on this platform", |  | ||||||
| ) |  | ||||||
| def test_conftest_symlink_files(testdir): | def test_conftest_symlink_files(testdir): | ||||||
|     """Check conftest.py loading when running in directory with symlinks.""" |     """Symlinked conftest.py are found when pytest is executed in a directory with symlinked | ||||||
|  |     files.""" | ||||||
|     real = testdir.tmpdir.mkdir("real") |     real = testdir.tmpdir.mkdir("real") | ||||||
|     source = { |     source = { | ||||||
|         "app/test_foo.py": "def test1(fixture): pass", |         "app/test_foo.py": "def test1(fixture): pass", | ||||||
|  | @ -271,7 +263,7 @@ def test_conftest_symlink_files(testdir): | ||||||
|     build = testdir.tmpdir.mkdir("build") |     build = testdir.tmpdir.mkdir("build") | ||||||
|     build.mkdir("app") |     build.mkdir("app") | ||||||
|     for f in source: |     for f in source: | ||||||
|         build.join(f).mksymlinkto(real.join(f)) |         symlink_or_skip(real.join(f), build.join(f)) | ||||||
|     build.chdir() |     build.chdir() | ||||||
|     result = testdir.runpytest("-vs", "app/test_foo.py") |     result = testdir.runpytest("-vs", "app/test_foo.py") | ||||||
|     result.stdout.fnmatch_lines(["*conftest_loaded*", "PASSED"]) |     result.stdout.fnmatch_lines(["*conftest_loaded*", "PASSED"]) | ||||||
|  |  | ||||||
|  | @ -0,0 +1,85 @@ | ||||||
|  | import os.path | ||||||
|  | import subprocess | ||||||
|  | import sys | ||||||
|  | import textwrap | ||||||
|  | from contextlib import contextmanager | ||||||
|  | from string import ascii_lowercase | ||||||
|  | 
 | ||||||
|  | import py.path | ||||||
|  | 
 | ||||||
|  | from _pytest import pytester | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @contextmanager | ||||||
|  | def subst_path_windows(filename): | ||||||
|  |     for c in ascii_lowercase[7:]:  # Create a subst drive from H-Z. | ||||||
|  |         c += ":" | ||||||
|  |         if not os.path.exists(c): | ||||||
|  |             drive = c | ||||||
|  |             break | ||||||
|  |     else: | ||||||
|  |         raise AssertionError("Unable to find suitable drive letter for subst.") | ||||||
|  | 
 | ||||||
|  |     directory = filename.dirpath() | ||||||
|  |     basename = filename.basename | ||||||
|  | 
 | ||||||
|  |     args = ["subst", drive, str(directory)] | ||||||
|  |     subprocess.check_call(args) | ||||||
|  |     assert os.path.exists(drive) | ||||||
|  |     try: | ||||||
|  |         filename = py.path.local(drive) / basename | ||||||
|  |         yield filename | ||||||
|  |     finally: | ||||||
|  |         args = ["subst", "/D", drive] | ||||||
|  |         subprocess.check_call(args) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @contextmanager | ||||||
|  | def subst_path_linux(filename): | ||||||
|  |     directory = filename.dirpath() | ||||||
|  |     basename = filename.basename | ||||||
|  | 
 | ||||||
|  |     target = directory / ".." / "sub2" | ||||||
|  |     os.symlink(str(directory), str(target), target_is_directory=True) | ||||||
|  |     try: | ||||||
|  |         filename = target / basename | ||||||
|  |         yield filename | ||||||
|  |     finally: | ||||||
|  |         # We don't need to unlink (it's all in the tempdir). | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_link_resolve(testdir: pytester.Testdir) -> None: | ||||||
|  |     """ | ||||||
|  |     See: https://github.com/pytest-dev/pytest/issues/5965 | ||||||
|  |     """ | ||||||
|  |     sub1 = testdir.mkpydir("sub1") | ||||||
|  |     p = sub1.join("test_foo.py") | ||||||
|  |     p.write( | ||||||
|  |         textwrap.dedent( | ||||||
|  |             """ | ||||||
|  |         import pytest | ||||||
|  |         def test_foo(): | ||||||
|  |             raise AssertionError() | ||||||
|  |         """ | ||||||
|  |         ) | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     subst = subst_path_linux | ||||||
|  |     if sys.platform == "win32": | ||||||
|  |         subst = subst_path_windows | ||||||
|  | 
 | ||||||
|  |     with subst(p) as subst_p: | ||||||
|  |         result = testdir.runpytest(str(subst_p), "-v") | ||||||
|  |         # i.e.: Make sure that the error is reported as a relative path, not as a | ||||||
|  |         # resolved path. | ||||||
|  |         # See: https://github.com/pytest-dev/pytest/issues/5965 | ||||||
|  |         stdout = result.stdout.str() | ||||||
|  |         assert "sub1/test_foo.py" not in stdout | ||||||
|  | 
 | ||||||
|  |         # i.e.: Expect drive on windows because we just have drive:filename, whereas | ||||||
|  |         # we expect a relative path on Linux. | ||||||
|  |         expect = ( | ||||||
|  |             "*{}*".format(subst_p) if sys.platform == "win32" else "*sub2/test_foo.py*" | ||||||
|  |         ) | ||||||
|  |         result.stdout.fnmatch_lines([expect]) | ||||||
		Loading…
	
		Reference in New Issue