Skip to content

Commit 322190f

Browse files
fabioznicoddemus
andauthored
Fix issue where working dir becomes wrong on subst drive on Windows. Fixes #5965 (#6523)
Co-authored-by: Bruno Oliveira <[email protected]>
1 parent c17d508 commit 322190f

File tree

10 files changed

+160
-120
lines changed

10 files changed

+160
-120
lines changed

changelog/5965.breaking.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
symlinks are no longer resolved during collection and matching `conftest.py` files with test file paths.
2+
3+
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.
4+
5+
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
6+
`#6523 <https://github.com/pytest-dev/pytest/pull/6523>`__ for details).
7+
8+
This might break test suites which made use of this feature; the fix is to create a symlink
9+
for the entire test tree, and not only to partial files/tress as it was possible previously.

src/_pytest/capture.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,9 @@ def _py36_windowsconsoleio_workaround(stream: TextIO) -> None:
123123
return
124124

125125
buffered = hasattr(stream.buffer, "raw")
126-
raw_stdout = stream.buffer.raw if buffered else stream.buffer
126+
raw_stdout = stream.buffer.raw if buffered else stream.buffer # type: ignore[attr-defined]
127127

128-
if not isinstance(raw_stdout, io._WindowsConsoleIO):
128+
if not isinstance(raw_stdout, io._WindowsConsoleIO): # type: ignore[attr-defined]
129129
return
130130

131131
def _reopen_stdio(f, mode):
@@ -135,7 +135,7 @@ def _reopen_stdio(f, mode):
135135
buffering = -1
136136

137137
return io.TextIOWrapper(
138-
open(os.dup(f.fileno()), mode, buffering),
138+
open(os.dup(f.fileno()), mode, buffering), # type: ignore[arg-type]
139139
f.encoding,
140140
f.errors,
141141
f.newlines,

src/_pytest/config/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ def get_config(args=None, plugins=None):
232232
config = Config(
233233
pluginmanager,
234234
invocation_params=Config.InvocationParams(
235-
args=args or (), plugins=plugins, dir=Path().resolve()
235+
args=args or (), plugins=plugins, dir=Path.cwd()
236236
),
237237
)
238238

@@ -477,7 +477,7 @@ def _getconftestmodules(self, path):
477477
# and allow users to opt into looking into the rootdir parent
478478
# directories instead of requiring to specify confcutdir
479479
clist = []
480-
for parent in directory.realpath().parts():
480+
for parent in directory.parts():
481481
if self._confcutdir and self._confcutdir.relto(parent):
482482
continue
483483
conftestpath = parent.join("conftest.py")
@@ -798,7 +798,7 @@ def __init__(
798798

799799
if invocation_params is None:
800800
invocation_params = self.InvocationParams(
801-
args=(), plugins=None, dir=Path().resolve()
801+
args=(), plugins=None, dir=Path.cwd()
802802
)
803803

804804
self.option = argparse.Namespace()

src/_pytest/fixtures.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1496,7 +1496,7 @@ def getfixtureinfo(
14961496
def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
14971497
nodeid = None
14981498
try:
1499-
p = py.path.local(plugin.__file__).realpath() # type: ignore[attr-defined] # noqa: F821
1499+
p = py.path.local(plugin.__file__) # type: ignore[attr-defined] # noqa: F821
15001500
except AttributeError:
15011501
pass
15021502
else:

src/_pytest/main.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -665,7 +665,6 @@ def _parsearg(self, arg: str) -> Tuple[py.path.local, List[str]]:
665665
"file or package not found: " + arg + " (missing __init__.py?)"
666666
)
667667
raise UsageError("file not found: " + arg)
668-
fspath = fspath.realpath()
669668
return (fspath, parts)
670669

671670
def matchnodes(

src/_pytest/pathlib.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from typing import TypeVar
1919
from typing import Union
2020

21+
from _pytest.outcomes import skip
2122
from _pytest.warning_types import PytestWarning
2223

2324
if sys.version_info[:2] >= (3, 6):
@@ -397,3 +398,11 @@ def fnmatch_ex(pattern: str, path) -> bool:
397398
def parts(s: str) -> Set[str]:
398399
parts = s.split(sep)
399400
return {sep.join(parts[: i + 1]) or sep for i in range(len(parts))}
401+
402+
403+
def symlink_or_skip(src, dst, **kwargs):
404+
"""Makes a symlink or skips the test in case symlinks are not supported."""
405+
try:
406+
os.symlink(str(src), str(dst), **kwargs)
407+
except OSError as e:
408+
skip("symlinks not supported: {}".format(e))

testing/acceptance_test.py

Lines changed: 16 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import os
22
import sys
3-
import textwrap
43
import types
54

65
import attr
@@ -9,6 +8,7 @@
98
import pytest
109
from _pytest.compat import importlib_metadata
1110
from _pytest.config import ExitCode
11+
from _pytest.pathlib import symlink_or_skip
1212
from _pytest.pytester import Testdir
1313

1414

@@ -266,29 +266,6 @@ def test_conftest_printing_shows_if_error(self, testdir):
266266
assert result.ret != 0
267267
assert "should be seen" in result.stdout.str()
268268

269-
@pytest.mark.skipif(
270-
not hasattr(py.path.local, "mksymlinkto"),
271-
reason="symlink not available on this platform",
272-
)
273-
def test_chdir(self, testdir):
274-
testdir.tmpdir.join("py").mksymlinkto(py._pydir)
275-
p = testdir.tmpdir.join("main.py")
276-
p.write(
277-
textwrap.dedent(
278-
"""\
279-
import sys, os
280-
sys.path.insert(0, '')
281-
import py
282-
print(py.__file__)
283-
print(py.__path__)
284-
os.chdir(os.path.dirname(os.getcwd()))
285-
print(py.log)
286-
"""
287-
)
288-
)
289-
result = testdir.runpython(p)
290-
assert not result.ret
291-
292269
def test_issue109_sibling_conftests_not_loaded(self, testdir):
293270
sub1 = testdir.mkdir("sub1")
294271
sub2 = testdir.mkdir("sub2")
@@ -762,19 +739,9 @@ def test():
762739

763740
def test_cmdline_python_package_symlink(self, testdir, monkeypatch):
764741
"""
765-
test --pyargs option with packages with path containing symlink can
766-
have conftest.py in their package (#2985)
742+
--pyargs with packages with path containing symlink can have conftest.py in
743+
their package (#2985)
767744
"""
768-
# dummy check that we can actually create symlinks: on Windows `os.symlink` is available,
769-
# but normal users require special admin privileges to create symlinks.
770-
if sys.platform == "win32":
771-
try:
772-
os.symlink(
773-
str(testdir.tmpdir.ensure("tmpfile")),
774-
str(testdir.tmpdir.join("tmpfile2")),
775-
)
776-
except OSError as e:
777-
pytest.skip(str(e.args[0]))
778745
monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False)
779746

780747
dirname = "lib"
@@ -790,13 +757,13 @@ def test_cmdline_python_package_symlink(self, testdir, monkeypatch):
790757
"import pytest\n@pytest.fixture\ndef a_fixture():pass"
791758
)
792759

793-
d_local = testdir.mkdir("local")
794-
symlink_location = os.path.join(str(d_local), "lib")
795-
os.symlink(str(d), symlink_location, target_is_directory=True)
760+
d_local = testdir.mkdir("symlink_root")
761+
symlink_location = d_local / "lib"
762+
symlink_or_skip(d, symlink_location, target_is_directory=True)
796763

797764
# The structure of the test directory is now:
798765
# .
799-
# ├── local
766+
# ├── symlink_root
800767
# │ └── lib -> ../lib
801768
# └── lib
802769
# └── foo
@@ -807,32 +774,23 @@ def test_cmdline_python_package_symlink(self, testdir, monkeypatch):
807774
# └── test_bar.py
808775

809776
# NOTE: the different/reversed ordering is intentional here.
810-
search_path = ["lib", os.path.join("local", "lib")]
777+
search_path = ["lib", os.path.join("symlink_root", "lib")]
811778
monkeypatch.setenv("PYTHONPATH", prepend_pythonpath(*search_path))
812779
for p in search_path:
813780
monkeypatch.syspath_prepend(p)
814781

815782
# module picked up in symlink-ed directory:
816-
# It picks up local/lib/foo/bar (symlink) via sys.path.
783+
# It picks up symlink_root/lib/foo/bar (symlink) via sys.path.
817784
result = testdir.runpytest("--pyargs", "-v", "foo.bar")
818785
testdir.chdir()
819786
assert result.ret == 0
820-
if hasattr(py.path.local, "mksymlinkto"):
821-
result.stdout.fnmatch_lines(
822-
[
823-
"lib/foo/bar/test_bar.py::test_bar PASSED*",
824-
"lib/foo/bar/test_bar.py::test_other PASSED*",
825-
"*2 passed*",
826-
]
827-
)
828-
else:
829-
result.stdout.fnmatch_lines(
830-
[
831-
"*lib/foo/bar/test_bar.py::test_bar PASSED*",
832-
"*lib/foo/bar/test_bar.py::test_other PASSED*",
833-
"*2 passed*",
834-
]
835-
)
787+
result.stdout.fnmatch_lines(
788+
[
789+
"symlink_root/lib/foo/bar/test_bar.py::test_bar PASSED*",
790+
"symlink_root/lib/foo/bar/test_bar.py::test_other PASSED*",
791+
"*2 passed*",
792+
]
793+
)
836794

837795
def test_cmdline_python_package_not_exists(self, testdir):
838796
result = testdir.runpytest("--pyargs", "tpkgwhatv")

testing/test_collection.py

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@
33
import sys
44
import textwrap
55

6-
import py
7-
86
import pytest
97
from _pytest.config import ExitCode
108
from _pytest.main import _in_venv
119
from _pytest.main import Session
10+
from _pytest.pathlib import symlink_or_skip
1211
from _pytest.pytester import Testdir
1312

1413

@@ -1164,29 +1163,21 @@ def test_collect_pyargs_with_testpaths(testdir, monkeypatch):
11641163
result.stdout.fnmatch_lines(["*1 passed in*"])
11651164

11661165

1167-
@pytest.mark.skipif(
1168-
not hasattr(py.path.local, "mksymlinkto"),
1169-
reason="symlink not available on this platform",
1170-
)
11711166
def test_collect_symlink_file_arg(testdir):
1172-
"""Test that collecting a direct symlink, where the target does not match python_files works (#4325)."""
1167+
"""Collect a direct symlink works even if it does not match python_files (#4325)."""
11731168
real = testdir.makepyfile(
11741169
real="""
11751170
def test_nodeid(request):
1176-
assert request.node.nodeid == "real.py::test_nodeid"
1171+
assert request.node.nodeid == "symlink.py::test_nodeid"
11771172
"""
11781173
)
11791174
symlink = testdir.tmpdir.join("symlink.py")
1180-
symlink.mksymlinkto(real)
1175+
symlink_or_skip(real, symlink)
11811176
result = testdir.runpytest("-v", symlink)
1182-
result.stdout.fnmatch_lines(["real.py::test_nodeid PASSED*", "*1 passed in*"])
1177+
result.stdout.fnmatch_lines(["symlink.py::test_nodeid PASSED*", "*1 passed in*"])
11831178
assert result.ret == 0
11841179

11851180

1186-
@pytest.mark.skipif(
1187-
not hasattr(py.path.local, "mksymlinkto"),
1188-
reason="symlink not available on this platform",
1189-
)
11901181
def test_collect_symlink_out_of_tree(testdir):
11911182
"""Test collection of symlink via out-of-tree rootdir."""
11921183
sub = testdir.tmpdir.join("sub")
@@ -1204,7 +1195,7 @@ def test_nodeid(request):
12041195

12051196
out_of_tree = testdir.tmpdir.join("out_of_tree").ensure(dir=True)
12061197
symlink_to_sub = out_of_tree.join("symlink_to_sub")
1207-
symlink_to_sub.mksymlinkto(sub)
1198+
symlink_or_skip(sub, symlink_to_sub)
12081199
sub.chdir()
12091200
result = testdir.runpytest("-vs", "--rootdir=%s" % sub, symlink_to_sub)
12101201
result.stdout.fnmatch_lines(
@@ -1270,22 +1261,19 @@ def test_collect_pkg_init_only(testdir):
12701261
result.stdout.fnmatch_lines(["sub/__init__.py::test_init PASSED*", "*1 passed in*"])
12711262

12721263

1273-
@pytest.mark.skipif(
1274-
not hasattr(py.path.local, "mksymlinkto"),
1275-
reason="symlink not available on this platform",
1276-
)
12771264
@pytest.mark.parametrize("use_pkg", (True, False))
12781265
def test_collect_sub_with_symlinks(use_pkg, testdir):
1266+
"""Collection works with symlinked files and broken symlinks"""
12791267
sub = testdir.mkdir("sub")
12801268
if use_pkg:
12811269
sub.ensure("__init__.py")
1282-
sub.ensure("test_file.py").write("def test_file(): pass")
1270+
sub.join("test_file.py").write("def test_file(): pass")
12831271

12841272
# Create a broken symlink.
1285-
sub.join("test_broken.py").mksymlinkto("test_doesnotexist.py")
1273+
symlink_or_skip("test_doesnotexist.py", sub.join("test_broken.py"))
12861274

12871275
# Symlink that gets collected.
1288-
sub.join("test_symlink.py").mksymlinkto("test_file.py")
1276+
symlink_or_skip("test_file.py", sub.join("test_symlink.py"))
12891277

12901278
result = testdir.runpytest("-v", str(sub))
12911279
result.stdout.fnmatch_lines(

0 commit comments

Comments
 (0)