From 505c3340bf852f7636eae1490a8638f5ffab266d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Mon, 26 Aug 2019 17:18:46 +0200 Subject: [PATCH 1/4] Fix pytest with mixed up filename casing. --- src/_pytest/config/__init__.py | 19 +++++++++++++------ testing/test_conftest.py | 29 ++++++++++++++++++++++++----- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index b861563e99b..5d77fa983a7 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -36,6 +36,10 @@ hookspec = HookspecMarker("pytest") +def _uniquepath(path): + return type(path)(os.path.normcase(str(path.realpath()))) + + class ConftestImportFailure(Exception): def __init__(self, path, excinfo): Exception.__init__(self, path, excinfo) @@ -366,7 +370,7 @@ def _set_initial_conftests(self, namespace): """ current = py.path.local() self._confcutdir = ( - current.join(namespace.confcutdir, abs=True) + _uniquepath(current.join(namespace.confcutdir, abs=True)) if namespace.confcutdir else None ) @@ -405,19 +409,18 @@ def _getconftestmodules(self, path): else: directory = path + directory = _uniquepath(directory) + # XXX these days we may rather want to use config.rootdir # 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") if conftestpath.isfile(): - # Use realpath to avoid loading the same conftest twice - # with build systems that create build directories containing - # symlinks to actual files. - mod = self._importconftest(conftestpath.realpath()) + mod = self._importconftest(conftestpath) clist.append(mod) self._dirpath2confmods[directory] = clist return clist @@ -432,6 +435,10 @@ def _rget_with_confmod(self, name, path): raise KeyError(name) def _importconftest(self, conftestpath): + # Use realpath to avoid loading the same conftest twice + # with build systems that create build directories containing + # symlinks to actual files. + conftestpath = _uniquepath(conftestpath) try: return self._conftestpath2mod[conftestpath] except KeyError: diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 447416f1076..1f52247d50b 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -3,8 +3,9 @@ import py import pytest -from _pytest.config import PytestPluginManager +from _pytest.config import PytestPluginManager, _uniquepath from _pytest.main import ExitCode +import os.path def ConftestWithSetinitial(path): @@ -141,11 +142,11 @@ def test_conftestcutdir(testdir): # but we can still import a conftest directly conftest._importconftest(conf) values = conftest._getconftestmodules(conf.dirpath()) - assert values[0].__file__.startswith(str(conf)) + assert values[0].__file__.startswith(str(_uniquepath(conf))) # and all sub paths get updated properly values = conftest._getconftestmodules(p) assert len(values) == 1 - assert values[0].__file__.startswith(str(conf)) + assert values[0].__file__.startswith(str(_uniquepath(conf))) def test_conftestcutdir_inplace_considered(testdir): @@ -154,7 +155,7 @@ def test_conftestcutdir_inplace_considered(testdir): conftest_setinitial(conftest, [conf.dirpath()], confcutdir=conf.dirpath()) values = conftest._getconftestmodules(conf.dirpath()) assert len(values) == 1 - assert values[0].__file__.startswith(str(conf)) + assert values[0].__file__.startswith(str(_uniquepath(conf))) @pytest.mark.parametrize("name", "test tests whatever .dotdir".split()) @@ -164,7 +165,7 @@ def test_setinitial_conftest_subdirs(testdir, name): conftest = PytestPluginManager() conftest_setinitial(conftest, [sub.dirpath()], confcutdir=testdir.tmpdir) if name not in ("whatever", ".dotdir"): - assert subconftest in conftest._conftestpath2mod + assert _uniquepath(subconftest) in conftest._conftestpath2mod assert len(conftest._conftestpath2mod) == 1 else: assert subconftest not in conftest._conftestpath2mod @@ -274,6 +275,24 @@ def fixture(): 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(testdir): + """Check conftest.py loading when directory casing is wrong.""" + testdir.tmpdir.mkdir("JenkinsRoot").mkdir("test") + source = { + "setup.py": "", + "test/__init__.py": "", + "test/conftest.py": "" + } + testdir.makepyfile(**{"JenkinsRoot/%s" % k: v for k, v in source.items()}) + + testdir.tmpdir.join("jenkinsroot/test").chdir() + result = testdir.runpytest() + assert result.ret == ExitCode.NO_TESTS_COLLECTED + def test_no_conftest(testdir): testdir.makeconftest("assert 0") From 1aac64573fab858e9c216a8faf9e802fbb1b8303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Tue, 27 Aug 2019 16:16:45 +0200 Subject: [PATCH 2/4] black formatting. --- testing/test_conftest.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 1f52247d50b..a9af649d000 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -1,11 +1,12 @@ +import os.path import textwrap import py import pytest -from _pytest.config import PytestPluginManager, _uniquepath +from _pytest.config import _uniquepath +from _pytest.config import PytestPluginManager from _pytest.main import ExitCode -import os.path def ConftestWithSetinitial(path): @@ -275,18 +276,15 @@ def fixture(): result.stdout.fnmatch_lines(["*conftest_loaded*", "PASSED"]) assert result.ret == ExitCode.OK + @pytest.mark.skipif( - os.path.normcase('x') != os.path.normcase('X'), + os.path.normcase("x") != os.path.normcase("X"), reason="only relevant for case insensitive file systems", ) def test_conftest_badcase(testdir): """Check conftest.py loading when directory casing is wrong.""" testdir.tmpdir.mkdir("JenkinsRoot").mkdir("test") - source = { - "setup.py": "", - "test/__init__.py": "", - "test/conftest.py": "" - } + source = {"setup.py": "", "test/__init__.py": "", "test/conftest.py": ""} testdir.makepyfile(**{"JenkinsRoot/%s" % k: v for k, v in source.items()}) testdir.tmpdir.join("jenkinsroot/test").chdir() From a98270eac078db7d78a50b35f0fcf9bdb8bf888f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Tue, 27 Aug 2019 16:25:24 +0200 Subject: [PATCH 3/4] Document the bugfix. --- AUTHORS | 1 + changelog/5792.bugfix.rst | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 changelog/5792.bugfix.rst diff --git a/AUTHORS b/AUTHORS index 88bbfe3527b..1dbef3d5d15 100644 --- a/AUTHORS +++ b/AUTHORS @@ -55,6 +55,7 @@ Charnjit SiNGH (CCSJ) Chris Lamb Christian Boelsen Christian Fetzer +Christian Neumüller Christian Theunert Christian Tismer Christopher Gilling diff --git a/changelog/5792.bugfix.rst b/changelog/5792.bugfix.rst new file mode 100644 index 00000000000..1ee0364dd53 --- /dev/null +++ b/changelog/5792.bugfix.rst @@ -0,0 +1,3 @@ +Windows: Fix error that occurs in certain circumstances when loading +``conftest.py`` from a working directory that has casing other than the one stored +in the filesystem (e.g., ``c:\test`` instead of ``C:\test``). From 29bb0eda276fc3808264a1b070544339c1eee393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Neum=C3=BCller?= Date: Wed, 28 Aug 2019 09:21:03 +0200 Subject: [PATCH 4/4] Move _uniquepath to pathlib as unique_path. Co-authored-by: Bruno Oliveira --- src/_pytest/config/__init__.py | 11 ++++------- src/_pytest/pathlib.py | 10 ++++++++++ testing/test_conftest.py | 10 +++++----- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 5d77fa983a7..3a0eca546a3 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -30,16 +30,13 @@ from _pytest.compat import importlib_metadata from _pytest.outcomes import fail from _pytest.outcomes import Skipped +from _pytest.pathlib import unique_path from _pytest.warning_types import PytestConfigWarning hookimpl = HookimplMarker("pytest") hookspec = HookspecMarker("pytest") -def _uniquepath(path): - return type(path)(os.path.normcase(str(path.realpath()))) - - class ConftestImportFailure(Exception): def __init__(self, path, excinfo): Exception.__init__(self, path, excinfo) @@ -370,7 +367,7 @@ def _set_initial_conftests(self, namespace): """ current = py.path.local() self._confcutdir = ( - _uniquepath(current.join(namespace.confcutdir, abs=True)) + unique_path(current.join(namespace.confcutdir, abs=True)) if namespace.confcutdir else None ) @@ -409,7 +406,7 @@ def _getconftestmodules(self, path): else: directory = path - directory = _uniquepath(directory) + directory = unique_path(directory) # XXX these days we may rather want to use config.rootdir # and allow users to opt into looking into the rootdir parent @@ -438,7 +435,7 @@ def _importconftest(self, conftestpath): # Use realpath to avoid loading the same conftest twice # with build systems that create build directories containing # symlinks to actual files. - conftestpath = _uniquepath(conftestpath) + conftestpath = unique_path(conftestpath) try: return self._conftestpath2mod[conftestpath] except KeyError: diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 19f9c062f89..0403b694737 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -11,6 +11,7 @@ from os.path import expanduser from os.path import expandvars from os.path import isabs +from os.path import normcase from os.path import sep from posixpath import sep as posix_sep @@ -334,3 +335,12 @@ def fnmatch_ex(pattern, path): def parts(s): parts = s.split(sep) return {sep.join(parts[: i + 1]) or sep for i in range(len(parts))} + + +def unique_path(path): + """Returns a unique path in case-insensitive (but case-preserving) file + systems such as Windows. + + This is needed only for ``py.path.local``; ``pathlib.Path`` handles this + natively with ``resolve()``.""" + return type(path)(normcase(str(path.realpath()))) diff --git a/testing/test_conftest.py b/testing/test_conftest.py index a9af649d000..9888f5457f3 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -4,9 +4,9 @@ import py import pytest -from _pytest.config import _uniquepath from _pytest.config import PytestPluginManager from _pytest.main import ExitCode +from _pytest.pathlib import unique_path def ConftestWithSetinitial(path): @@ -143,11 +143,11 @@ def test_conftestcutdir(testdir): # but we can still import a conftest directly conftest._importconftest(conf) values = conftest._getconftestmodules(conf.dirpath()) - assert values[0].__file__.startswith(str(_uniquepath(conf))) + assert values[0].__file__.startswith(str(unique_path(conf))) # and all sub paths get updated properly values = conftest._getconftestmodules(p) assert len(values) == 1 - assert values[0].__file__.startswith(str(_uniquepath(conf))) + assert values[0].__file__.startswith(str(unique_path(conf))) def test_conftestcutdir_inplace_considered(testdir): @@ -156,7 +156,7 @@ def test_conftestcutdir_inplace_considered(testdir): conftest_setinitial(conftest, [conf.dirpath()], confcutdir=conf.dirpath()) values = conftest._getconftestmodules(conf.dirpath()) assert len(values) == 1 - assert values[0].__file__.startswith(str(_uniquepath(conf))) + assert values[0].__file__.startswith(str(unique_path(conf))) @pytest.mark.parametrize("name", "test tests whatever .dotdir".split()) @@ -166,7 +166,7 @@ def test_setinitial_conftest_subdirs(testdir, name): conftest = PytestPluginManager() conftest_setinitial(conftest, [sub.dirpath()], confcutdir=testdir.tmpdir) if name not in ("whatever", ".dotdir"): - assert _uniquepath(subconftest) in conftest._conftestpath2mod + assert unique_path(subconftest) in conftest._conftestpath2mod assert len(conftest._conftestpath2mod) == 1 else: assert subconftest not in conftest._conftestpath2mod