Skip to content

Commit

Permalink
Fix issue where working dir becomes wrong on symlinks. Fixes pytest-d…
Browse files Browse the repository at this point in the history
…ev#5965

This addresses subst drive on Windows and symlinks on Linux.
  • Loading branch information
fabioz committed Feb 13, 2020
1 parent dbae5a7 commit 129687d
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 29 deletions.
1 change: 1 addition & 0 deletions changelog/5965.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
No longer resolve symlinks so that the current directory remains correct with drives mapped with subst on Windows or symlinks on Linux.
6 changes: 3 additions & 3 deletions src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ def get_config(args=None, plugins=None):
config = Config(
pluginmanager,
invocation_params=Config.InvocationParams(
args=args or (), plugins=plugins, dir=Path().resolve()
args=args or (), plugins=plugins, dir=Path.cwd()
),
)

Expand Down Expand Up @@ -462,7 +462,7 @@ def _getconftestmodules(self, path):
# and allow users to opt into looking into the rootdir parent
# directories instead of requiring to specify confcutdir
clist = []
for parent in directory.realpath().parts():
for parent in directory.parts():
if self._confcutdir and self._confcutdir.relto(parent):
continue
conftestpath = parent.join("conftest.py")
Expand Down Expand Up @@ -771,7 +771,7 @@ def __init__(self, pluginmanager, *, invocation_params=None) -> None:

if invocation_params is None:
invocation_params = self.InvocationParams(
args=(), plugins=None, dir=Path().resolve()
args=(), plugins=None, dir=Path.cwd()
)

self.option = argparse.Namespace()
Expand Down
2 changes: 1 addition & 1 deletion src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -1288,7 +1288,7 @@ def getfixtureinfo(self, node, func, cls, funcargs=True):
def pytest_plugin_registered(self, plugin):
nodeid = None
try:
p = py.path.local(plugin.__file__).realpath()
p = py.path.local(plugin.__file__)
except AttributeError:
pass
else:
Expand Down
1 change: 0 additions & 1 deletion src/_pytest/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -642,7 +642,6 @@ def _parsearg(self, arg):
"file or package not found: " + arg + " (missing __init__.py?)"
)
raise UsageError("file not found: " + arg)
fspath = fspath.realpath()
return (fspath, parts)

def matchnodes(self, matching, names):
Expand Down
4 changes: 2 additions & 2 deletions testing/acceptance_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -822,8 +822,8 @@ def test_cmdline_python_package_symlink(self, testdir, monkeypatch):
if hasattr(py.path.local, "mksymlinkto"):
result.stdout.fnmatch_lines(
[
"lib/foo/bar/test_bar.py::test_bar PASSED*",
"lib/foo/bar/test_bar.py::test_other PASSED*",
"local/lib/foo/bar/test_bar.py::test_bar PASSED*",
"local/lib/foo/bar/test_bar.py::test_other PASSED*",
"*2 passed*",
]
)
Expand Down
4 changes: 2 additions & 2 deletions testing/test_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -1171,13 +1171,13 @@ def test_collect_symlink_file_arg(testdir):
real = testdir.makepyfile(
real="""
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.mksymlinkto(real)
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


Expand Down
39 changes: 19 additions & 20 deletions testing/test_conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,21 @@ def pytest_addoption(parser):
reason="symlink not available on this platform",
)
def test_conftest_symlink(testdir):
"""Ensure that conftest.py is used for resolved symlinks."""
"""
Ensure that conftest.py is not found unless it's not a parent in the current
directory structure (i.e.: symlinks are not resolved).
"""
# 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")
realtests = real.mkdir("app").mkdir("tests")
testdir.tmpdir.join("symlinktests").mksymlinkto(realtests)
Expand All @@ -216,31 +230,16 @@ def fixture():
),
}
)

# Should fail because conftest cannot be found from the link structure.
result = testdir.runpytest("-vs", "symlinktests")
result.stdout.fnmatch_lines(
[
"*conftest_loaded*",
"real/app/tests/test_foo.py::test1 fixture_used",
"PASSED",
]
)
assert result.ret == ExitCode.OK
result.stdout.fnmatch_lines(["*fixture 'fixture' not found*"])
assert result.ret == ExitCode.TESTS_FAILED

# Should not cause "ValueError: Plugin already registered" (#4174).
result = testdir.runpytest("-vs", "symlink")
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"),
Expand Down
85 changes: 85 additions & 0 deletions testing/test_link_resolve.py
Original file line number Diff line number Diff line change
@@ -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])

0 comments on commit 129687d

Please sign in to comment.