1import os
2import textwrap
3
4import py
5
6import pytest
7from _pytest.config import ExitCode
8from _pytest.config import PytestPluginManager
9from _pytest.pathlib import Path
10from _pytest.pathlib import symlink_or_skip
11
12
13def ConftestWithSetinitial(path):
14    conftest = PytestPluginManager()
15    conftest_setinitial(conftest, [path])
16    return conftest
17
18
19def conftest_setinitial(conftest, args, confcutdir=None):
20    class Namespace:
21        def __init__(self):
22            self.file_or_dir = args
23            self.confcutdir = str(confcutdir)
24            self.noconftest = False
25            self.pyargs = False
26            self.importmode = "prepend"
27
28    conftest._set_initial_conftests(Namespace())
29
30
31@pytest.mark.usefixtures("_sys_snapshot")
32class TestConftestValueAccessGlobal:
33    @pytest.fixture(scope="module", params=["global", "inpackage"])
34    def basedir(self, request, tmpdir_factory):
35        tmpdir = tmpdir_factory.mktemp("basedir", numbered=True)
36        tmpdir.ensure("adir/conftest.py").write("a=1 ; Directory = 3")
37        tmpdir.ensure("adir/b/conftest.py").write("b=2 ; a = 1.5")
38        if request.param == "inpackage":
39            tmpdir.ensure("adir/__init__.py")
40            tmpdir.ensure("adir/b/__init__.py")
41
42        yield tmpdir
43
44    def test_basic_init(self, basedir):
45        conftest = PytestPluginManager()
46        p = basedir.join("adir")
47        assert conftest._rget_with_confmod("a", p, importmode="prepend")[1] == 1
48
49    def test_immediate_initialiation_and_incremental_are_the_same(self, basedir):
50        conftest = PytestPluginManager()
51        assert not len(conftest._dirpath2confmods)
52        conftest._getconftestmodules(basedir, importmode="prepend")
53        snap1 = len(conftest._dirpath2confmods)
54        assert snap1 == 1
55        conftest._getconftestmodules(basedir.join("adir"), importmode="prepend")
56        assert len(conftest._dirpath2confmods) == snap1 + 1
57        conftest._getconftestmodules(basedir.join("b"), importmode="prepend")
58        assert len(conftest._dirpath2confmods) == snap1 + 2
59
60    def test_value_access_not_existing(self, basedir):
61        conftest = ConftestWithSetinitial(basedir)
62        with pytest.raises(KeyError):
63            conftest._rget_with_confmod("a", basedir, importmode="prepend")
64
65    def test_value_access_by_path(self, basedir):
66        conftest = ConftestWithSetinitial(basedir)
67        adir = basedir.join("adir")
68        assert conftest._rget_with_confmod("a", adir, importmode="prepend")[1] == 1
69        assert (
70            conftest._rget_with_confmod("a", adir.join("b"), importmode="prepend")[1]
71            == 1.5
72        )
73
74    def test_value_access_with_confmod(self, basedir):
75        startdir = basedir.join("adir", "b")
76        startdir.ensure("xx", dir=True)
77        conftest = ConftestWithSetinitial(startdir)
78        mod, value = conftest._rget_with_confmod("a", startdir, importmode="prepend")
79        assert value == 1.5
80        path = py.path.local(mod.__file__)
81        assert path.dirpath() == basedir.join("adir", "b")
82        assert path.purebasename.startswith("conftest")
83
84
85def test_conftest_in_nonpkg_with_init(tmpdir, _sys_snapshot):
86    tmpdir.ensure("adir-1.0/conftest.py").write("a=1 ; Directory = 3")
87    tmpdir.ensure("adir-1.0/b/conftest.py").write("b=2 ; a = 1.5")
88    tmpdir.ensure("adir-1.0/b/__init__.py")
89    tmpdir.ensure("adir-1.0/__init__.py")
90    ConftestWithSetinitial(tmpdir.join("adir-1.0", "b"))
91
92
93def test_doubledash_considered(testdir):
94    conf = testdir.mkdir("--option")
95    conf.ensure("conftest.py")
96    conftest = PytestPluginManager()
97    conftest_setinitial(conftest, [conf.basename, conf.basename])
98    values = conftest._getconftestmodules(conf, importmode="prepend")
99    assert len(values) == 1
100
101
102def test_issue151_load_all_conftests(testdir):
103    names = "code proj src".split()
104    for name in names:
105        p = testdir.mkdir(name)
106        p.ensure("conftest.py")
107
108    conftest = PytestPluginManager()
109    conftest_setinitial(conftest, names)
110    d = list(conftest._conftestpath2mod.values())
111    assert len(d) == len(names)
112
113
114def test_conftest_global_import(testdir):
115    testdir.makeconftest("x=3")
116    p = testdir.makepyfile(
117        """
118        import py, pytest
119        from _pytest.config import PytestPluginManager
120        conf = PytestPluginManager()
121        mod = conf._importconftest(py.path.local("conftest.py"), importmode="prepend")
122        assert mod.x == 3
123        import conftest
124        assert conftest is mod, (conftest, mod)
125        subconf = py.path.local().ensure("sub", "conftest.py")
126        subconf.write("y=4")
127        mod2 = conf._importconftest(subconf, importmode="prepend")
128        assert mod != mod2
129        assert mod2.y == 4
130        import conftest
131        assert conftest is mod2, (conftest, mod)
132    """
133    )
134    res = testdir.runpython(p)
135    assert res.ret == 0
136
137
138def test_conftestcutdir(testdir):
139    conf = testdir.makeconftest("")
140    p = testdir.mkdir("x")
141    conftest = PytestPluginManager()
142    conftest_setinitial(conftest, [testdir.tmpdir], confcutdir=p)
143    values = conftest._getconftestmodules(p, importmode="prepend")
144    assert len(values) == 0
145    values = conftest._getconftestmodules(conf.dirpath(), importmode="prepend")
146    assert len(values) == 0
147    assert conf not in conftest._conftestpath2mod
148    # but we can still import a conftest directly
149    conftest._importconftest(conf, importmode="prepend")
150    values = conftest._getconftestmodules(conf.dirpath(), importmode="prepend")
151    assert values[0].__file__.startswith(str(conf))
152    # and all sub paths get updated properly
153    values = conftest._getconftestmodules(p, importmode="prepend")
154    assert len(values) == 1
155    assert values[0].__file__.startswith(str(conf))
156
157
158def test_conftestcutdir_inplace_considered(testdir):
159    conf = testdir.makeconftest("")
160    conftest = PytestPluginManager()
161    conftest_setinitial(conftest, [conf.dirpath()], confcutdir=conf.dirpath())
162    values = conftest._getconftestmodules(conf.dirpath(), importmode="prepend")
163    assert len(values) == 1
164    assert values[0].__file__.startswith(str(conf))
165
166
167@pytest.mark.parametrize("name", "test tests whatever .dotdir".split())
168def test_setinitial_conftest_subdirs(testdir, name):
169    sub = testdir.mkdir(name)
170    subconftest = sub.ensure("conftest.py")
171    conftest = PytestPluginManager()
172    conftest_setinitial(conftest, [sub.dirpath()], confcutdir=testdir.tmpdir)
173    key = Path(str(subconftest)).resolve()
174    if name not in ("whatever", ".dotdir"):
175        assert key in conftest._conftestpath2mod
176        assert len(conftest._conftestpath2mod) == 1
177    else:
178        assert key not in conftest._conftestpath2mod
179        assert len(conftest._conftestpath2mod) == 0
180
181
182def test_conftest_confcutdir(testdir):
183    testdir.makeconftest("assert 0")
184    x = testdir.mkdir("x")
185    x.join("conftest.py").write(
186        textwrap.dedent(
187            """\
188            def pytest_addoption(parser):
189                parser.addoption("--xyz", action="store_true")
190            """
191        )
192    )
193    result = testdir.runpytest("-h", "--confcutdir=%s" % x, x)
194    result.stdout.fnmatch_lines(["*--xyz*"])
195    result.stdout.no_fnmatch_line("*warning: could not load initial*")
196
197
198def test_conftest_symlink(testdir):
199    """`conftest.py` discovery follows normal path resolution and does not resolve symlinks."""
200    # Structure:
201    # /real
202    # /real/conftest.py
203    # /real/app
204    # /real/app/tests
205    # /real/app/tests/test_foo.py
206
207    # Links:
208    # /symlinktests -> /real/app/tests (running at symlinktests should fail)
209    # /symlink -> /real (running at /symlink should work)
210
211    real = testdir.tmpdir.mkdir("real")
212    realtests = real.mkdir("app").mkdir("tests")
213    symlink_or_skip(realtests, testdir.tmpdir.join("symlinktests"))
214    symlink_or_skip(real, testdir.tmpdir.join("symlink"))
215    testdir.makepyfile(
216        **{
217            "real/app/tests/test_foo.py": "def test1(fixture): pass",
218            "real/conftest.py": textwrap.dedent(
219                """
220                import pytest
221
222                print("conftest_loaded")
223
224                @pytest.fixture
225                def fixture():
226                    print("fixture_used")
227                """
228            ),
229        }
230    )
231
232    # Should fail because conftest cannot be found from the link structure.
233    result = testdir.runpytest("-vs", "symlinktests")
234    result.stdout.fnmatch_lines(["*fixture 'fixture' not found*"])
235    assert result.ret == ExitCode.TESTS_FAILED
236
237    # Should not cause "ValueError: Plugin already registered" (#4174).
238    result = testdir.runpytest("-vs", "symlink")
239    assert result.ret == ExitCode.OK
240
241
242def test_conftest_symlink_files(testdir):
243    """Symlinked conftest.py are found when pytest is executed in a directory with symlinked
244    files."""
245    real = testdir.tmpdir.mkdir("real")
246    source = {
247        "app/test_foo.py": "def test1(fixture): pass",
248        "app/__init__.py": "",
249        "app/conftest.py": textwrap.dedent(
250            """
251            import pytest
252
253            print("conftest_loaded")
254
255            @pytest.fixture
256            def fixture():
257                print("fixture_used")
258            """
259        ),
260    }
261    testdir.makepyfile(**{"real/%s" % k: v for k, v in source.items()})
262
263    # Create a build directory that contains symlinks to actual files
264    # but doesn't symlink actual directories.
265    build = testdir.tmpdir.mkdir("build")
266    build.mkdir("app")
267    for f in source:
268        symlink_or_skip(real.join(f), build.join(f))
269    build.chdir()
270    result = testdir.runpytest("-vs", "app/test_foo.py")
271    result.stdout.fnmatch_lines(["*conftest_loaded*", "PASSED"])
272    assert result.ret == ExitCode.OK
273
274
275@pytest.mark.skipif(
276    os.path.normcase("x") != os.path.normcase("X"),
277    reason="only relevant for case insensitive file systems",
278)
279def test_conftest_badcase(testdir):
280    """Check conftest.py loading when directory casing is wrong (#5792)."""
281    testdir.tmpdir.mkdir("JenkinsRoot").mkdir("test")
282    source = {"setup.py": "", "test/__init__.py": "", "test/conftest.py": ""}
283    testdir.makepyfile(**{"JenkinsRoot/%s" % k: v for k, v in source.items()})
284
285    testdir.tmpdir.join("jenkinsroot/test").chdir()
286    result = testdir.runpytest()
287    assert result.ret == ExitCode.NO_TESTS_COLLECTED
288
289
290def test_conftest_uppercase(testdir):
291    """Check conftest.py whose qualified name contains uppercase characters (#5819)"""
292    source = {"__init__.py": "", "Foo/conftest.py": "", "Foo/__init__.py": ""}
293    testdir.makepyfile(**source)
294
295    testdir.tmpdir.chdir()
296    result = testdir.runpytest()
297    assert result.ret == ExitCode.NO_TESTS_COLLECTED
298
299
300def test_no_conftest(testdir):
301    testdir.makeconftest("assert 0")
302    result = testdir.runpytest("--noconftest")
303    assert result.ret == ExitCode.NO_TESTS_COLLECTED
304
305    result = testdir.runpytest()
306    assert result.ret == ExitCode.USAGE_ERROR
307
308
309def test_conftest_existing_junitxml(testdir):
310    x = testdir.mkdir("tests")
311    x.join("conftest.py").write(
312        textwrap.dedent(
313            """\
314            def pytest_addoption(parser):
315                parser.addoption("--xyz", action="store_true")
316            """
317        )
318    )
319    testdir.makefile(ext=".xml", junit="")  # Writes junit.xml
320    result = testdir.runpytest("-h", "--junitxml", "junit.xml")
321    result.stdout.fnmatch_lines(["*--xyz*"])
322
323
324def test_conftest_import_order(testdir, monkeypatch):
325    ct1 = testdir.makeconftest("")
326    sub = testdir.mkdir("sub")
327    ct2 = sub.join("conftest.py")
328    ct2.write("")
329
330    def impct(p, importmode):
331        return p
332
333    conftest = PytestPluginManager()
334    conftest._confcutdir = testdir.tmpdir
335    monkeypatch.setattr(conftest, "_importconftest", impct)
336    assert conftest._getconftestmodules(sub, importmode="prepend") == [ct1, ct2]
337
338
339def test_fixture_dependency(testdir):
340    ct1 = testdir.makeconftest("")
341    ct1 = testdir.makepyfile("__init__.py")
342    ct1.write("")
343    sub = testdir.mkdir("sub")
344    sub.join("__init__.py").write("")
345    sub.join("conftest.py").write(
346        textwrap.dedent(
347            """\
348            import pytest
349
350            @pytest.fixture
351            def not_needed():
352                assert False, "Should not be called!"
353
354            @pytest.fixture
355            def foo():
356                assert False, "Should not be called!"
357
358            @pytest.fixture
359            def bar(foo):
360                return 'bar'
361            """
362        )
363    )
364    subsub = sub.mkdir("subsub")
365    subsub.join("__init__.py").write("")
366    subsub.join("test_bar.py").write(
367        textwrap.dedent(
368            """\
369            import pytest
370
371            @pytest.fixture
372            def bar():
373                return 'sub bar'
374
375            def test_event_fixture(bar):
376                assert bar == 'sub bar'
377            """
378        )
379    )
380    result = testdir.runpytest("sub")
381    result.stdout.fnmatch_lines(["*1 passed*"])
382
383
384def test_conftest_found_with_double_dash(testdir):
385    sub = testdir.mkdir("sub")
386    sub.join("conftest.py").write(
387        textwrap.dedent(
388            """\
389            def pytest_addoption(parser):
390                parser.addoption("--hello-world", action="store_true")
391            """
392        )
393    )
394    p = sub.join("test_hello.py")
395    p.write("def test_hello(): pass")
396    result = testdir.runpytest(str(p) + "::test_hello", "-h")
397    result.stdout.fnmatch_lines(
398        """
399        *--hello-world*
400    """
401    )
402
403
404class TestConftestVisibility:
405    def _setup_tree(self, testdir):  # for issue616
406        # example mostly taken from:
407        # https://mail.python.org/pipermail/pytest-dev/2014-September/002617.html
408        runner = testdir.mkdir("empty")
409        package = testdir.mkdir("package")
410
411        package.join("conftest.py").write(
412            textwrap.dedent(
413                """\
414                import pytest
415                @pytest.fixture
416                def fxtr():
417                    return "from-package"
418                """
419            )
420        )
421        package.join("test_pkgroot.py").write(
422            textwrap.dedent(
423                """\
424                def test_pkgroot(fxtr):
425                    assert fxtr == "from-package"
426                """
427            )
428        )
429
430        swc = package.mkdir("swc")
431        swc.join("__init__.py").ensure()
432        swc.join("conftest.py").write(
433            textwrap.dedent(
434                """\
435                import pytest
436                @pytest.fixture
437                def fxtr():
438                    return "from-swc"
439                """
440            )
441        )
442        swc.join("test_with_conftest.py").write(
443            textwrap.dedent(
444                """\
445                def test_with_conftest(fxtr):
446                    assert fxtr == "from-swc"
447                """
448            )
449        )
450
451        snc = package.mkdir("snc")
452        snc.join("__init__.py").ensure()
453        snc.join("test_no_conftest.py").write(
454            textwrap.dedent(
455                """\
456                def test_no_conftest(fxtr):
457                    assert fxtr == "from-package"   # No local conftest.py, so should
458                                                    # use value from parent dir's
459                """
460            )
461        )
462        print("created directory structure:")
463        tmppath = Path(str(testdir.tmpdir))
464        for x in tmppath.rglob(""):
465            print("   " + str(x.relative_to(tmppath)))
466
467        return {"runner": runner, "package": package, "swc": swc, "snc": snc}
468
469    # N.B.: "swc" stands for "subdir with conftest.py"
470    #       "snc" stands for "subdir no [i.e. without] conftest.py"
471    @pytest.mark.parametrize(
472        "chdir,testarg,expect_ntests_passed",
473        [
474            # Effective target: package/..
475            ("runner", "..", 3),
476            ("package", "..", 3),
477            ("swc", "../..", 3),
478            ("snc", "../..", 3),
479            # Effective target: package
480            ("runner", "../package", 3),
481            ("package", ".", 3),
482            ("swc", "..", 3),
483            ("snc", "..", 3),
484            # Effective target: package/swc
485            ("runner", "../package/swc", 1),
486            ("package", "./swc", 1),
487            ("swc", ".", 1),
488            ("snc", "../swc", 1),
489            # Effective target: package/snc
490            ("runner", "../package/snc", 1),
491            ("package", "./snc", 1),
492            ("swc", "../snc", 1),
493            ("snc", ".", 1),
494        ],
495    )
496    def test_parsefactories_relative_node_ids(
497        self, testdir, chdir, testarg, expect_ntests_passed
498    ):
499        """#616"""
500        dirs = self._setup_tree(testdir)
501        print("pytest run in cwd: %s" % (dirs[chdir].relto(testdir.tmpdir)))
502        print("pytestarg        : %s" % (testarg))
503        print("expected pass    : %s" % (expect_ntests_passed))
504        with dirs[chdir].as_cwd():
505            reprec = testdir.inline_run(testarg, "-q", "--traceconfig")
506            reprec.assertoutcome(passed=expect_ntests_passed)
507
508
509@pytest.mark.parametrize(
510    "confcutdir,passed,error", [(".", 2, 0), ("src", 1, 1), (None, 1, 1)]
511)
512def test_search_conftest_up_to_inifile(testdir, confcutdir, passed, error):
513    """Test that conftest files are detected only up to an ini file, unless
514    an explicit --confcutdir option is given.
515    """
516    root = testdir.tmpdir
517    src = root.join("src").ensure(dir=1)
518    src.join("pytest.ini").write("[pytest]")
519    src.join("conftest.py").write(
520        textwrap.dedent(
521            """\
522            import pytest
523            @pytest.fixture
524            def fix1(): pass
525            """
526        )
527    )
528    src.join("test_foo.py").write(
529        textwrap.dedent(
530            """\
531            def test_1(fix1):
532                pass
533            def test_2(out_of_reach):
534                pass
535            """
536        )
537    )
538    root.join("conftest.py").write(
539        textwrap.dedent(
540            """\
541            import pytest
542            @pytest.fixture
543            def out_of_reach(): pass
544            """
545        )
546    )
547
548    args = [str(src)]
549    if confcutdir:
550        args = ["--confcutdir=%s" % root.join(confcutdir)]
551    result = testdir.runpytest(*args)
552    match = ""
553    if passed:
554        match += "*%d passed*" % passed
555    if error:
556        match += "*%d error*" % error
557    result.stdout.fnmatch_lines(match)
558
559
560def test_issue1073_conftest_special_objects(testdir):
561    testdir.makeconftest(
562        """\
563        class DontTouchMe(object):
564            def __getattr__(self, x):
565                raise Exception('cant touch me')
566
567        x = DontTouchMe()
568        """
569    )
570    testdir.makepyfile(
571        """\
572        def test_some():
573            pass
574        """
575    )
576    res = testdir.runpytest()
577    assert res.ret == 0
578
579
580def test_conftest_exception_handling(testdir):
581    testdir.makeconftest(
582        """\
583        raise ValueError()
584        """
585    )
586    testdir.makepyfile(
587        """\
588        def test_some():
589            pass
590        """
591    )
592    res = testdir.runpytest()
593    assert res.ret == 4
594    assert "raise ValueError()" in [line.strip() for line in res.errlines]
595
596
597def test_hook_proxy(testdir):
598    """Session's gethookproxy() would cache conftests incorrectly (#2016).
599    It was decided to remove the cache altogether.
600    """
601    testdir.makepyfile(
602        **{
603            "root/demo-0/test_foo1.py": "def test1(): pass",
604            "root/demo-a/test_foo2.py": "def test1(): pass",
605            "root/demo-a/conftest.py": """\
606            def pytest_ignore_collect(path, config):
607                return True
608            """,
609            "root/demo-b/test_foo3.py": "def test1(): pass",
610            "root/demo-c/test_foo4.py": "def test1(): pass",
611        }
612    )
613    result = testdir.runpytest()
614    result.stdout.fnmatch_lines(
615        ["*test_foo1.py*", "*test_foo3.py*", "*test_foo4.py*", "*3 passed*"]
616    )
617
618
619def test_required_option_help(testdir):
620    testdir.makeconftest("assert 0")
621    x = testdir.mkdir("x")
622    x.join("conftest.py").write(
623        textwrap.dedent(
624            """\
625            def pytest_addoption(parser):
626                parser.addoption("--xyz", action="store_true", required=True)
627            """
628        )
629    )
630    result = testdir.runpytest("-h", x)
631    result.stdout.no_fnmatch_line("*argument --xyz is required*")
632    assert "general:" in result.stdout.str()
633