test_ok2/testing/test_pluginmanager.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

483 lines
17 KiB
Python
Raw Permalink Normal View History

# mypy: allow-untyped-defs
import os
import shutil
2017-12-27 11:47:26 +08:00
import sys
import types
from typing import List
from _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.config import PytestPluginManager
from _pytest.config.exceptions import UsageError
from _pytest.main import Session
from _pytest.monkeypatch import MonkeyPatch
from _pytest.pathlib import import_path
from _pytest.pytester import Pytester
import pytest
2016-11-21 22:50:21 +08:00
@pytest.fixture
def pytestpm() -> PytestPluginManager:
return PytestPluginManager()
2019-06-03 06:32:00 +08:00
class TestPytestPluginInteractions:
def test_addhooks_conftestplugin(
self, pytester: Pytester, _config_for_test: Config
) -> None:
pytester.makepyfile(
2018-05-23 22:48:46 +08:00
newhooks="""
def pytest_myhook(xyz):
"new hook"
2018-05-23 22:48:46 +08:00
"""
)
conf = pytester.makeconftest(
2018-05-23 22:48:46 +08:00
"""
import newhooks
def pytest_addhooks(pluginmanager):
pluginmanager.add_hookspecs(newhooks)
def pytest_myhook(xyz):
return xyz + 1
2018-05-23 22:48:46 +08:00
"""
)
config = _config_for_test
pm = config.pluginmanager
pm.hook.pytest_addhooks.call_historic(
2018-05-23 22:48:46 +08:00
kwargs=dict(pluginmanager=config.pluginmanager)
)
config.pluginmanager._importconftest(
conf,
importmode="prepend",
rootpath=pytester.path,
consider_namespace_packages=False,
)
# print(config.pluginmanager.get_plugins())
res = config.hook.pytest_myhook(xyz=10)
assert res == [11]
def test_addhooks_nohooks(self, pytester: Pytester) -> None:
pytester.makeconftest(
2018-05-23 22:48:46 +08:00
"""
import sys
def pytest_addhooks(pluginmanager):
pluginmanager.add_hookspecs(sys)
2018-05-23 22:48:46 +08:00
"""
)
res = pytester.runpytest()
assert res.ret != 0
2018-05-23 22:48:46 +08:00
res.stderr.fnmatch_lines(["*did not find*sys*"])
def test_do_option_postinitialize(self, pytester: Pytester) -> None:
config = pytester.parseconfigure()
2018-05-23 22:48:46 +08:00
assert not hasattr(config.option, "test123")
p = pytester.makepyfile(
2018-05-23 22:48:46 +08:00
"""
def pytest_addoption(parser):
parser.addoption('--test123', action="store_true",
default=True)
2018-05-23 22:48:46 +08:00
"""
)
config.pluginmanager._importconftest(
p,
importmode="prepend",
rootpath=pytester.path,
consider_namespace_packages=False,
)
assert config.option.test123
def test_configure(self, pytester: Pytester) -> None:
config = pytester.parseconfig()
values = []
2019-06-03 06:32:00 +08:00
class A:
2020-01-17 02:42:29 +08:00
def pytest_configure(self):
values.append(self)
config.pluginmanager.register(A())
assert len(values) == 0
config._do_configure()
assert len(values) == 1
config.pluginmanager.register(A()) # leads to a configured() plugin
assert len(values) == 2
assert values[0] != values[1]
config._ensure_unconfigure()
config.pluginmanager.register(A())
assert len(values) == 2
@pytest.mark.skipif(
not sys.platform.startswith("win"),
reason="requires a case-insensitive file system",
)
def test_conftestpath_case_sensitivity(self, pytester: Pytester) -> None:
"""Unit test for issue #9765."""
config = pytester.parseconfig()
pytester.makepyfile(**{"tests/conftest.py": ""})
conftest = pytester.path.joinpath("tests/conftest.py")
conftest_upper_case = pytester.path.joinpath("TESTS/conftest.py")
mod = config.pluginmanager._importconftest(
conftest,
importmode="prepend",
rootpath=pytester.path,
consider_namespace_packages=False,
)
plugin = config.pluginmanager.get_plugin(str(conftest))
assert plugin is mod
mod_uppercase = config.pluginmanager._importconftest(
conftest_upper_case,
importmode="prepend",
rootpath=pytester.path,
consider_namespace_packages=False,
)
plugin_uppercase = config.pluginmanager.get_plugin(str(conftest_upper_case))
assert plugin_uppercase is mod_uppercase
# No str(conftestpath) normalization so conftest should be imported
# twice and modules should be different objects
assert mod is not mod_uppercase
def test_hook_tracing(self, _config_for_test: Config) -> None:
pytestpm = _config_for_test.pluginmanager # fully initialized with plugins
saveindent = []
2019-06-03 06:32:00 +08:00
class api1:
def pytest_plugin_registered(self):
saveindent.append(pytestpm.trace.root.indent)
2019-06-03 06:32:00 +08:00
class api2:
def pytest_plugin_registered(self):
saveindent.append(pytestpm.trace.root.indent)
raise ValueError()
2020-10-06 09:13:05 +08:00
values: List[str] = []
pytestpm.trace.root.setwriter(values.append)
undo = pytestpm.enable_tracing()
try:
indent = pytestpm.trace.root.indent
p = api1()
pytestpm.register(p)
assert pytestpm.trace.root.indent == indent
assert len(values) >= 2
2018-05-23 22:48:46 +08:00
assert "pytest_plugin_registered" in values[0]
assert "finish" in values[1]
values[:] = []
with pytest.raises(ValueError):
pytestpm.register(api2())
assert pytestpm.trace.root.indent == indent
assert saveindent[0] > indent
finally:
undo()
def test_hook_proxy(self, pytester: Pytester) -> None:
2016-11-21 22:50:21 +08:00
"""Test the gethookproxy function(#2016)"""
config = pytester.parseconfig()
session = Session.from_config(config)
pytester.makepyfile(**{"tests/conftest.py": "", "tests/subdir/conftest.py": ""})
2016-11-21 22:50:21 +08:00
conftest1 = pytester.path.joinpath("tests/conftest.py")
conftest2 = pytester.path.joinpath("tests/subdir/conftest.py")
2016-11-21 22:50:21 +08:00
config.pluginmanager._importconftest(
conftest1,
importmode="prepend",
rootpath=pytester.path,
consider_namespace_packages=False,
)
ihook_a = session.gethookproxy(pytester.path / "tests")
2016-11-21 22:50:21 +08:00
assert ihook_a is not None
config.pluginmanager._importconftest(
conftest2,
importmode="prepend",
rootpath=pytester.path,
consider_namespace_packages=False,
)
ihook_b = session.gethookproxy(pytester.path / "tests")
2016-11-21 22:50:21 +08:00
assert ihook_a is not ihook_b
def test_hook_with_addoption(self, pytester: Pytester) -> None:
"""Test that hooks can be used in a call to pytest_addoption"""
pytester.makepyfile(
newhooks="""
import pytest
@pytest.hookspec(firstresult=True)
def pytest_default_value():
pass
"""
)
pytester.makepyfile(
myplugin="""
import newhooks
def pytest_addhooks(pluginmanager):
pluginmanager.add_hookspecs(newhooks)
def pytest_addoption(parser, pluginmanager):
default_value = pluginmanager.hook.pytest_default_value()
parser.addoption("--config", help="Config, defaults to %(default)s", default=default_value)
"""
)
pytester.makeconftest(
"""
pytest_plugins=("myplugin",)
def pytest_default_value():
return "default_value"
"""
)
res = pytester.runpytest("--help")
res.stdout.fnmatch_lines(["*--config=CONFIG*default_value*"])
def test_default_markers(pytester: Pytester) -> None:
result = pytester.runpytest("--markers")
2018-05-23 22:48:46 +08:00
result.stdout.fnmatch_lines(["*tryfirst*first*", "*trylast*last*"])
def test_importplugin_error_message(
pytester: Pytester, pytestpm: PytestPluginManager
) -> None:
"""Don't hide import errors when importing plugins and provide
an easy to debug message.
See #375 and #1998.
"""
pytester.syspathinsert(pytester.path)
pytester.makepyfile(
qwe="""\
def test_traceback():
raise ImportError('Not possible to import: ☺')
test_traceback()
"""
2018-05-23 22:48:46 +08:00
)
with pytest.raises(ImportError) as excinfo:
pytestpm.import_plugin("qwe")
assert str(excinfo.value).endswith(
'Error importing plugin "qwe": Not possible to import: ☺'
)
assert "in test_traceback" in str(excinfo.traceback[-1])
2019-06-03 06:32:00 +08:00
class TestPytestPluginManager:
def test_register_imported_modules(self) -> None:
pm = PytestPluginManager()
2017-12-27 11:47:26 +08:00
mod = types.ModuleType("x.y.pytest_hello")
pm.register(mod)
assert pm.is_registered(mod)
values = pm.get_plugins()
assert mod in values
pytest.raises(ValueError, pm.register, mod)
pytest.raises(ValueError, lambda: pm.register(mod))
# assert not pm.is_registered(mod2)
assert pm.get_plugins() == values
def test_canonical_import(self, monkeypatch):
2017-12-27 11:47:26 +08:00
mod = types.ModuleType("pytest_xyz")
2018-05-23 22:48:46 +08:00
monkeypatch.setitem(sys.modules, "pytest_xyz", mod)
pm = PytestPluginManager()
2018-05-23 22:48:46 +08:00
pm.import_plugin("pytest_xyz")
assert pm.get_plugin("pytest_xyz") == mod
assert pm.is_registered(mod)
def test_consider_module(
self, pytester: Pytester, pytestpm: PytestPluginManager
) -> None:
pytester.syspathinsert()
pytester.makepyfile(pytest_p1="#")
pytester.makepyfile(pytest_p2="#")
2017-12-27 11:47:26 +08:00
mod = types.ModuleType("temp")
mod.__dict__["pytest_plugins"] = ["pytest_p1", "pytest_p2"]
pytestpm.consider_module(mod)
p1 = pytestpm.get_plugin("pytest_p1")
assert p1 is not None
assert p1.__name__ == "pytest_p1"
p2 = pytestpm.get_plugin("pytest_p2")
assert p2 is not None
assert p2.__name__ == "pytest_p2"
def test_consider_module_import_module(
self, pytester: Pytester, _config_for_test: Config
) -> None:
pytestpm = _config_for_test.pluginmanager
2017-12-27 11:47:26 +08:00
mod = types.ModuleType("x")
mod.__dict__["pytest_plugins"] = "pytest_a"
aplugin = pytester.makepyfile(pytest_a="#")
reprec = pytester.make_hook_recorder(pytestpm)
pytester.syspathinsert(aplugin.parent)
pytestpm.consider_module(mod)
call = reprec.getcall(pytestpm.hook.pytest_plugin_registered.name)
assert call.plugin.__name__ == "pytest_a"
# check that it is not registered twice
pytestpm.consider_module(mod)
values = reprec.getcalls("pytest_plugin_registered")
assert len(values) == 1
def test_consider_env_fails_to_import(
self, monkeypatch: MonkeyPatch, pytestpm: PytestPluginManager
) -> None:
2018-05-23 22:48:46 +08:00
monkeypatch.setenv("PYTEST_PLUGINS", "nonexisting", prepend=",")
with pytest.raises(ImportError):
pytestpm.consider_env()
@pytest.mark.filterwarnings("always")
def test_plugin_skip(self, pytester: Pytester, monkeypatch: MonkeyPatch) -> None:
p = pytester.makepyfile(
2018-05-23 22:48:46 +08:00
skipping1="""
import pytest
pytest.skip("hello", allow_module_level=True)
2018-05-23 22:48:46 +08:00
"""
)
shutil.copy(p, p.with_name("skipping2.py"))
monkeypatch.setenv("PYTEST_PLUGINS", "skipping2")
result = pytester.runpytest("-p", "skipping1", syspathinsert=True)
assert result.ret == ExitCode.NO_TESTS_COLLECTED
2018-05-23 22:48:46 +08:00
result.stdout.fnmatch_lines(
["*skipped plugin*skipping1*hello*", "*skipped plugin*skipping2*hello*"]
)
def test_consider_env_plugin_instantiation(
self,
pytester: Pytester,
monkeypatch: MonkeyPatch,
pytestpm: PytestPluginManager,
) -> None:
pytester.syspathinsert()
pytester.makepyfile(xy123="#")
2018-05-23 22:48:46 +08:00
monkeypatch.setitem(os.environ, "PYTEST_PLUGINS", "xy123")
l1 = len(pytestpm.get_plugins())
pytestpm.consider_env()
l2 = len(pytestpm.get_plugins())
assert l2 == l1 + 1
2018-05-23 22:48:46 +08:00
assert pytestpm.get_plugin("xy123")
pytestpm.consider_env()
l3 = len(pytestpm.get_plugins())
assert l2 == l3
def test_pluginmanager_ENV_startup(
self, pytester: Pytester, monkeypatch: MonkeyPatch
) -> None:
pytester.makepyfile(pytest_x500="#")
p = pytester.makepyfile(
2018-05-23 22:48:46 +08:00
"""
import pytest
def test_hello(pytestconfig):
plugin = pytestconfig.pluginmanager.get_plugin('pytest_x500')
assert plugin is not None
2018-05-23 22:48:46 +08:00
"""
)
monkeypatch.setenv("PYTEST_PLUGINS", "pytest_x500", prepend=",")
result = pytester.runpytest(p, syspathinsert=True)
assert result.ret == 0
result.stdout.fnmatch_lines(["*1 passed*"])
def test_import_plugin_importname(
self, pytester: Pytester, pytestpm: PytestPluginManager
) -> None:
pytest.raises(ImportError, pytestpm.import_plugin, "qweqwex.y")
pytest.raises(ImportError, pytestpm.import_plugin, "pytest_qweqwx.y")
pytester.syspathinsert()
pluginname = "pytest_hello"
pytester.makepyfile(**{pluginname: ""})
pytestpm.import_plugin("pytest_hello")
len1 = len(pytestpm.get_plugins())
pytestpm.import_plugin("pytest_hello")
len2 = len(pytestpm.get_plugins())
assert len1 == len2
plugin1 = pytestpm.get_plugin("pytest_hello")
assert plugin1 is not None
2018-05-23 22:48:46 +08:00
assert plugin1.__name__.endswith("pytest_hello")
plugin2 = pytestpm.get_plugin("pytest_hello")
assert plugin2 is plugin1
def test_import_plugin_dotted_name(
self, pytester: Pytester, pytestpm: PytestPluginManager
) -> None:
pytest.raises(ImportError, pytestpm.import_plugin, "qweqwex.y")
pytest.raises(ImportError, pytestpm.import_plugin, "pytest_qweqwex.y")
pytester.syspathinsert()
2023-06-20 19:55:40 +08:00
pytester.mkpydir("pkg").joinpath("plug.py").write_text("x=3", encoding="utf-8")
pluginname = "pkg.plug"
pytestpm.import_plugin(pluginname)
mod = pytestpm.get_plugin("pkg.plug")
assert mod is not None
assert mod.x == 3
def test_consider_conftest_deps(
self,
pytester: Pytester,
pytestpm: PytestPluginManager,
) -> None:
mod = import_path(
pytester.makepyfile("pytest_plugins='xyz'"),
root=pytester.path,
consider_namespace_packages=False,
)
with pytest.raises(ImportError):
pytestpm.consider_conftest(mod, registration_name="unused")
2019-06-03 06:32:00 +08:00
class TestPytestPluginManagerBootstrapming:
def test_preparse_args(self, pytestpm: PytestPluginManager) -> None:
2018-05-23 22:48:46 +08:00
pytest.raises(
ImportError, lambda: pytestpm.consider_preparse(["xyz", "-p", "hello123"])
)
# Handles -p without space (#3532).
with pytest.raises(ImportError) as excinfo:
pytestpm.consider_preparse(["-phello123"])
assert '"hello123"' in excinfo.value.args[0]
pytestpm.consider_preparse(["-pno:hello123"])
# Handles -p without following arg (when used without argparse).
pytestpm.consider_preparse(["-p"])
with pytest.raises(UsageError, match="^plugin main cannot be disabled$"):
pytestpm.consider_preparse(["-p", "no:main"])
def test_plugin_prevent_register(self, pytestpm: PytestPluginManager) -> None:
pytestpm.consider_preparse(["xyz", "-p", "no:abc"])
l1 = pytestpm.get_plugins()
pytestpm.register(42, name="abc")
l2 = pytestpm.get_plugins()
assert len(l2) == len(l1)
assert 42 not in l2
def test_plugin_prevent_register_unregistered_alredy_registered(
self, pytestpm: PytestPluginManager
) -> None:
pytestpm.register(42, name="abc")
l1 = pytestpm.get_plugins()
assert 42 in l1
pytestpm.consider_preparse(["xyz", "-p", "no:abc"])
l2 = pytestpm.get_plugins()
assert 42 not in l2
2018-11-08 04:32:23 +08:00
def test_plugin_prevent_register_stepwise_on_cacheprovider_unregister(
self, pytestpm: PytestPluginManager
) -> None:
"""From PR #4304: The only way to unregister a module is documented at
the end of https://docs.pytest.org/en/stable/how-to/plugins.html.
2018-11-08 04:32:23 +08:00
When unregister cacheprovider, then unregister stepwise too.
2018-11-05 07:14:35 +08:00
"""
pytestpm.register(42, name="cacheprovider")
pytestpm.register(43, name="stepwise")
l1 = pytestpm.get_plugins()
assert 42 in l1
assert 43 in l1
pytestpm.consider_preparse(["xyz", "-p", "no:cacheprovider"])
l2 = pytestpm.get_plugins()
assert 42 not in l2
assert 43 not in l2
def test_blocked_plugin_can_be_used(self, pytestpm: PytestPluginManager) -> None:
pytestpm.consider_preparse(["xyz", "-p", "no:abc", "-p", "abc"])
assert pytestpm.has_plugin("abc")
assert not pytestpm.is_blocked("abc")
assert not pytestpm.is_blocked("pytest_abc")