1import os
2import sys
3import types
4from typing import List
5
6import pytest
7from _pytest.config import ExitCode
8from _pytest.config import PytestPluginManager
9from _pytest.config.exceptions import UsageError
10from _pytest.main import Session
11
12
13@pytest.fixture
14def pytestpm() -> PytestPluginManager:
15    return PytestPluginManager()
16
17
18class TestPytestPluginInteractions:
19    def test_addhooks_conftestplugin(self, testdir, _config_for_test):
20        testdir.makepyfile(
21            newhooks="""
22            def pytest_myhook(xyz):
23                "new hook"
24        """
25        )
26        conf = testdir.makeconftest(
27            """
28            import newhooks
29            def pytest_addhooks(pluginmanager):
30                pluginmanager.add_hookspecs(newhooks)
31            def pytest_myhook(xyz):
32                return xyz + 1
33        """
34        )
35        config = _config_for_test
36        pm = config.pluginmanager
37        pm.hook.pytest_addhooks.call_historic(
38            kwargs=dict(pluginmanager=config.pluginmanager)
39        )
40        config.pluginmanager._importconftest(conf, importmode="prepend")
41        # print(config.pluginmanager.get_plugins())
42        res = config.hook.pytest_myhook(xyz=10)
43        assert res == [11]
44
45    def test_addhooks_nohooks(self, testdir):
46        testdir.makeconftest(
47            """
48            import sys
49            def pytest_addhooks(pluginmanager):
50                pluginmanager.add_hookspecs(sys)
51        """
52        )
53        res = testdir.runpytest()
54        assert res.ret != 0
55        res.stderr.fnmatch_lines(["*did not find*sys*"])
56
57    def test_do_option_postinitialize(self, testdir):
58        config = testdir.parseconfigure()
59        assert not hasattr(config.option, "test123")
60        p = testdir.makepyfile(
61            """
62            def pytest_addoption(parser):
63                parser.addoption('--test123', action="store_true",
64                    default=True)
65        """
66        )
67        config.pluginmanager._importconftest(p, importmode="prepend")
68        assert config.option.test123
69
70    def test_configure(self, testdir):
71        config = testdir.parseconfig()
72        values = []
73
74        class A:
75            def pytest_configure(self):
76                values.append(self)
77
78        config.pluginmanager.register(A())
79        assert len(values) == 0
80        config._do_configure()
81        assert len(values) == 1
82        config.pluginmanager.register(A())  # leads to a configured() plugin
83        assert len(values) == 2
84        assert values[0] != values[1]
85
86        config._ensure_unconfigure()
87        config.pluginmanager.register(A())
88        assert len(values) == 2
89
90    def test_hook_tracing(self, _config_for_test) -> None:
91        pytestpm = _config_for_test.pluginmanager  # fully initialized with plugins
92        saveindent = []
93
94        class api1:
95            def pytest_plugin_registered(self):
96                saveindent.append(pytestpm.trace.root.indent)
97
98        class api2:
99            def pytest_plugin_registered(self):
100                saveindent.append(pytestpm.trace.root.indent)
101                raise ValueError()
102
103        values = []  # type: List[str]
104        pytestpm.trace.root.setwriter(values.append)
105        undo = pytestpm.enable_tracing()
106        try:
107            indent = pytestpm.trace.root.indent
108            p = api1()
109            pytestpm.register(p)
110            assert pytestpm.trace.root.indent == indent
111            assert len(values) >= 2
112            assert "pytest_plugin_registered" in values[0]
113            assert "finish" in values[1]
114
115            values[:] = []
116            with pytest.raises(ValueError):
117                pytestpm.register(api2())
118            assert pytestpm.trace.root.indent == indent
119            assert saveindent[0] > indent
120        finally:
121            undo()
122
123    def test_hook_proxy(self, testdir):
124        """Test the gethookproxy function(#2016)"""
125        config = testdir.parseconfig()
126        session = Session.from_config(config)
127        testdir.makepyfile(**{"tests/conftest.py": "", "tests/subdir/conftest.py": ""})
128
129        conftest1 = testdir.tmpdir.join("tests/conftest.py")
130        conftest2 = testdir.tmpdir.join("tests/subdir/conftest.py")
131
132        config.pluginmanager._importconftest(conftest1, importmode="prepend")
133        ihook_a = session.gethookproxy(testdir.tmpdir.join("tests"))
134        assert ihook_a is not None
135        config.pluginmanager._importconftest(conftest2, importmode="prepend")
136        ihook_b = session.gethookproxy(testdir.tmpdir.join("tests"))
137        assert ihook_a is not ihook_b
138
139    def test_hook_with_addoption(self, testdir):
140        """Test that hooks can be used in a call to pytest_addoption"""
141        testdir.makepyfile(
142            newhooks="""
143            import pytest
144            @pytest.hookspec(firstresult=True)
145            def pytest_default_value():
146                pass
147        """
148        )
149        testdir.makepyfile(
150            myplugin="""
151            import newhooks
152            def pytest_addhooks(pluginmanager):
153                pluginmanager.add_hookspecs(newhooks)
154            def pytest_addoption(parser, pluginmanager):
155                default_value = pluginmanager.hook.pytest_default_value()
156                parser.addoption("--config", help="Config, defaults to %(default)s", default=default_value)
157        """
158        )
159        testdir.makeconftest(
160            """
161            pytest_plugins=("myplugin",)
162            def pytest_default_value():
163                return "default_value"
164        """
165        )
166        res = testdir.runpytest("--help")
167        res.stdout.fnmatch_lines(["*--config=CONFIG*default_value*"])
168
169
170def test_default_markers(testdir):
171    result = testdir.runpytest("--markers")
172    result.stdout.fnmatch_lines(["*tryfirst*first*", "*trylast*last*"])
173
174
175def test_importplugin_error_message(testdir, pytestpm):
176    """Don't hide import errors when importing plugins and provide
177    an easy to debug message.
178
179    See #375 and #1998.
180    """
181    testdir.syspathinsert(testdir.tmpdir)
182    testdir.makepyfile(
183        qwe="""\
184        def test_traceback():
185            raise ImportError('Not possible to import: ☺')
186        test_traceback()
187        """
188    )
189    with pytest.raises(ImportError) as excinfo:
190        pytestpm.import_plugin("qwe")
191
192    assert str(excinfo.value).endswith(
193        'Error importing plugin "qwe": Not possible to import: ☺'
194    )
195    assert "in test_traceback" in str(excinfo.traceback[-1])
196
197
198class TestPytestPluginManager:
199    def test_register_imported_modules(self):
200        pm = PytestPluginManager()
201        mod = types.ModuleType("x.y.pytest_hello")
202        pm.register(mod)
203        assert pm.is_registered(mod)
204        values = pm.get_plugins()
205        assert mod in values
206        pytest.raises(ValueError, pm.register, mod)
207        pytest.raises(ValueError, lambda: pm.register(mod))
208        # assert not pm.is_registered(mod2)
209        assert pm.get_plugins() == values
210
211    def test_canonical_import(self, monkeypatch):
212        mod = types.ModuleType("pytest_xyz")
213        monkeypatch.setitem(sys.modules, "pytest_xyz", mod)
214        pm = PytestPluginManager()
215        pm.import_plugin("pytest_xyz")
216        assert pm.get_plugin("pytest_xyz") == mod
217        assert pm.is_registered(mod)
218
219    def test_consider_module(self, testdir, pytestpm: PytestPluginManager) -> None:
220        testdir.syspathinsert()
221        testdir.makepyfile(pytest_p1="#")
222        testdir.makepyfile(pytest_p2="#")
223        mod = types.ModuleType("temp")
224        mod.__dict__["pytest_plugins"] = ["pytest_p1", "pytest_p2"]
225        pytestpm.consider_module(mod)
226        assert pytestpm.get_plugin("pytest_p1").__name__ == "pytest_p1"
227        assert pytestpm.get_plugin("pytest_p2").__name__ == "pytest_p2"
228
229    def test_consider_module_import_module(self, testdir, _config_for_test) -> None:
230        pytestpm = _config_for_test.pluginmanager
231        mod = types.ModuleType("x")
232        mod.__dict__["pytest_plugins"] = "pytest_a"
233        aplugin = testdir.makepyfile(pytest_a="#")
234        reprec = testdir.make_hook_recorder(pytestpm)
235        testdir.syspathinsert(aplugin.dirpath())
236        pytestpm.consider_module(mod)
237        call = reprec.getcall(pytestpm.hook.pytest_plugin_registered.name)
238        assert call.plugin.__name__ == "pytest_a"
239
240        # check that it is not registered twice
241        pytestpm.consider_module(mod)
242        values = reprec.getcalls("pytest_plugin_registered")
243        assert len(values) == 1
244
245    def test_consider_env_fails_to_import(self, monkeypatch, pytestpm):
246        monkeypatch.setenv("PYTEST_PLUGINS", "nonexisting", prepend=",")
247        with pytest.raises(ImportError):
248            pytestpm.consider_env()
249
250    @pytest.mark.filterwarnings("always")
251    def test_plugin_skip(self, testdir, monkeypatch):
252        p = testdir.makepyfile(
253            skipping1="""
254            import pytest
255            pytest.skip("hello", allow_module_level=True)
256        """
257        )
258        p.copy(p.dirpath("skipping2.py"))
259        monkeypatch.setenv("PYTEST_PLUGINS", "skipping2")
260        result = testdir.runpytest("-p", "skipping1", syspathinsert=True)
261        assert result.ret == ExitCode.NO_TESTS_COLLECTED
262        result.stdout.fnmatch_lines(
263            ["*skipped plugin*skipping1*hello*", "*skipped plugin*skipping2*hello*"]
264        )
265
266    def test_consider_env_plugin_instantiation(self, testdir, monkeypatch, pytestpm):
267        testdir.syspathinsert()
268        testdir.makepyfile(xy123="#")
269        monkeypatch.setitem(os.environ, "PYTEST_PLUGINS", "xy123")
270        l1 = len(pytestpm.get_plugins())
271        pytestpm.consider_env()
272        l2 = len(pytestpm.get_plugins())
273        assert l2 == l1 + 1
274        assert pytestpm.get_plugin("xy123")
275        pytestpm.consider_env()
276        l3 = len(pytestpm.get_plugins())
277        assert l2 == l3
278
279    def test_pluginmanager_ENV_startup(self, testdir, monkeypatch):
280        testdir.makepyfile(pytest_x500="#")
281        p = testdir.makepyfile(
282            """
283            import pytest
284            def test_hello(pytestconfig):
285                plugin = pytestconfig.pluginmanager.get_plugin('pytest_x500')
286                assert plugin is not None
287        """
288        )
289        monkeypatch.setenv("PYTEST_PLUGINS", "pytest_x500", prepend=",")
290        result = testdir.runpytest(p, syspathinsert=True)
291        assert result.ret == 0
292        result.stdout.fnmatch_lines(["*1 passed*"])
293
294    def test_import_plugin_importname(self, testdir, pytestpm):
295        pytest.raises(ImportError, pytestpm.import_plugin, "qweqwex.y")
296        pytest.raises(ImportError, pytestpm.import_plugin, "pytest_qweqwx.y")
297
298        testdir.syspathinsert()
299        pluginname = "pytest_hello"
300        testdir.makepyfile(**{pluginname: ""})
301        pytestpm.import_plugin("pytest_hello")
302        len1 = len(pytestpm.get_plugins())
303        pytestpm.import_plugin("pytest_hello")
304        len2 = len(pytestpm.get_plugins())
305        assert len1 == len2
306        plugin1 = pytestpm.get_plugin("pytest_hello")
307        assert plugin1.__name__.endswith("pytest_hello")
308        plugin2 = pytestpm.get_plugin("pytest_hello")
309        assert plugin2 is plugin1
310
311    def test_import_plugin_dotted_name(self, testdir, pytestpm):
312        pytest.raises(ImportError, pytestpm.import_plugin, "qweqwex.y")
313        pytest.raises(ImportError, pytestpm.import_plugin, "pytest_qweqwex.y")
314
315        testdir.syspathinsert()
316        testdir.mkpydir("pkg").join("plug.py").write("x=3")
317        pluginname = "pkg.plug"
318        pytestpm.import_plugin(pluginname)
319        mod = pytestpm.get_plugin("pkg.plug")
320        assert mod.x == 3
321
322    def test_consider_conftest_deps(self, testdir, pytestpm):
323        mod = testdir.makepyfile("pytest_plugins='xyz'").pyimport()
324        with pytest.raises(ImportError):
325            pytestpm.consider_conftest(mod)
326
327
328class TestPytestPluginManagerBootstrapming:
329    def test_preparse_args(self, pytestpm):
330        pytest.raises(
331            ImportError, lambda: pytestpm.consider_preparse(["xyz", "-p", "hello123"])
332        )
333
334        # Handles -p without space (#3532).
335        with pytest.raises(ImportError) as excinfo:
336            pytestpm.consider_preparse(["-phello123"])
337        assert '"hello123"' in excinfo.value.args[0]
338        pytestpm.consider_preparse(["-pno:hello123"])
339
340        # Handles -p without following arg (when used without argparse).
341        pytestpm.consider_preparse(["-p"])
342
343        with pytest.raises(UsageError, match="^plugin main cannot be disabled$"):
344            pytestpm.consider_preparse(["-p", "no:main"])
345
346    def test_plugin_prevent_register(self, pytestpm):
347        pytestpm.consider_preparse(["xyz", "-p", "no:abc"])
348        l1 = pytestpm.get_plugins()
349        pytestpm.register(42, name="abc")
350        l2 = pytestpm.get_plugins()
351        assert len(l2) == len(l1)
352        assert 42 not in l2
353
354    def test_plugin_prevent_register_unregistered_alredy_registered(self, pytestpm):
355        pytestpm.register(42, name="abc")
356        l1 = pytestpm.get_plugins()
357        assert 42 in l1
358        pytestpm.consider_preparse(["xyz", "-p", "no:abc"])
359        l2 = pytestpm.get_plugins()
360        assert 42 not in l2
361
362    def test_plugin_prevent_register_stepwise_on_cacheprovider_unregister(
363        self, pytestpm
364    ):
365        """From PR #4304: The only way to unregister a module is documented at
366        the end of https://docs.pytest.org/en/stable/plugins.html.
367
368        When unregister cacheprovider, then unregister stepwise too.
369        """
370        pytestpm.register(42, name="cacheprovider")
371        pytestpm.register(43, name="stepwise")
372        l1 = pytestpm.get_plugins()
373        assert 42 in l1
374        assert 43 in l1
375        pytestpm.consider_preparse(["xyz", "-p", "no:cacheprovider"])
376        l2 = pytestpm.get_plugins()
377        assert 42 not in l2
378        assert 43 not in l2
379
380    def test_blocked_plugin_can_be_used(self, pytestpm):
381        pytestpm.consider_preparse(["xyz", "-p", "no:abc", "-p", "abc"])
382
383        assert pytestpm.has_plugin("abc")
384        assert not pytestpm.is_blocked("abc")
385        assert not pytestpm.is_blocked("pytest_abc")
386