From 59e6fb94b5e47bcb8db58c7d704ff389a0b0a8a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Stradomski?= Date: Thu, 7 Feb 2019 02:04:06 +0100 Subject: [PATCH 1/2] Fix "ValueError: Plugin already registered" exceptions when running in build directories that symlink to actual source. --- changelog/526.bugfix.rst | 1 + src/_pytest/config/__init__.py | 2 +- testing/test_conftest.py | 36 ++++++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 changelog/526.bugfix.rst diff --git a/changelog/526.bugfix.rst b/changelog/526.bugfix.rst new file mode 100644 index 000000000..022183b88 --- /dev/null +++ b/changelog/526.bugfix.rst @@ -0,0 +1 @@ +Fix "ValueError: Plugin already registered" exceptions when running in build directories that symlink to actual source. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 26999e125..25e1d65bd 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -408,7 +408,7 @@ class PytestPluginManager(PluginManager): continue conftestpath = parent.join("conftest.py") if conftestpath.isfile(): - mod = self._importconftest(conftestpath) + mod = self._importconftest(conftestpath.realpath()) clist.append(mod) self._dirpath2confmods[directory] = clist return clist diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 2b66d8fa7..ac091fed8 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -244,6 +244,42 @@ def test_conftest_symlink(testdir): assert result.ret == EXIT_OK +@pytest.mark.skipif( + not hasattr(py.path.local, "mksymlinkto"), + reason="symlink not available on this platform", +) +def test_conftest_symlink_files(testdir): + """Check conftest.py loading when running in directory with symlinks.""" + real = testdir.tmpdir.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") + """ + ), + } + testdir.makepyfile(**{"real/%s" % 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 = testdir.tmpdir.mkdir("build") + build.mkdir("app") + for f in source: + build.join(f).mksymlinkto(real.join(f)) + build.chdir() + result = testdir.runpytest("-vs", "app/test_foo.py") + result.stdout.fnmatch_lines(["*conftest_loaded*", "PASSED"]) + assert result.ret == EXIT_OK + + def test_no_conftest(testdir): testdir.makeconftest("assert 0") result = testdir.runpytest("--noconftest") From 391dc549c05a3d262a9f7835c8cfa053f0e4e502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Stradomski?= <44680433+pstradomski@users.noreply.github.com> Date: Thu, 7 Feb 2019 12:56:13 +0100 Subject: [PATCH 2/2] Add comment on why realpath is needed --- src/_pytest/config/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 25e1d65bd..3943f8472 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -408,6 +408,9 @@ class PytestPluginManager(PluginManager): 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()) clist.append(mod) self._dirpath2confmods[directory] = clist