test_ok2/testing/test_pluginmanager.py

467 lines
16 KiB
Python

# mypy: allow-untyped-defs
import os
import shutil
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
@pytest.fixture
def pytestpm() -> PytestPluginManager:
return PytestPluginManager()
class TestPytestPluginInteractions:
def test_addhooks_conftestplugin(
self, pytester: Pytester, _config_for_test: Config
) -> None:
pytester.makepyfile(
newhooks="""
def pytest_myhook(xyz):
"new hook"
"""
)
conf = pytester.makeconftest(
"""
import newhooks
def pytest_addhooks(pluginmanager):
pluginmanager.add_hookspecs(newhooks)
def pytest_myhook(xyz):
return xyz + 1
"""
)
config = _config_for_test
pm = config.pluginmanager
pm.hook.pytest_addhooks.call_historic(
kwargs=dict(pluginmanager=config.pluginmanager)
)
config.pluginmanager._importconftest(
conf, importmode="prepend", rootpath=pytester.path
)
# 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(
"""
import sys
def pytest_addhooks(pluginmanager):
pluginmanager.add_hookspecs(sys)
"""
)
res = pytester.runpytest()
assert res.ret != 0
res.stderr.fnmatch_lines(["*did not find*sys*"])
def test_do_option_postinitialize(self, pytester: Pytester) -> None:
config = pytester.parseconfigure()
assert not hasattr(config.option, "test123")
p = pytester.makepyfile(
"""
def pytest_addoption(parser):
parser.addoption('--test123', action="store_true",
default=True)
"""
)
config.pluginmanager._importconftest(
p, importmode="prepend", rootpath=pytester.path
)
assert config.option.test123
def test_configure(self, pytester: Pytester) -> None:
config = pytester.parseconfig()
values = []
class A:
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,
)
plugin = config.pluginmanager.get_plugin(str(conftest))
assert plugin is mod
mod_uppercase = config.pluginmanager._importconftest(
conftest_upper_case,
importmode="prepend",
rootpath=pytester.path,
)
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 = []
class api1:
def pytest_plugin_registered(self):
saveindent.append(pytestpm.trace.root.indent)
class api2:
def pytest_plugin_registered(self):
saveindent.append(pytestpm.trace.root.indent)
raise ValueError()
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
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:
"""Test the gethookproxy function(#2016)"""
config = pytester.parseconfig()
session = Session.from_config(config)
pytester.makepyfile(**{"tests/conftest.py": "", "tests/subdir/conftest.py": ""})
conftest1 = pytester.path.joinpath("tests/conftest.py")
conftest2 = pytester.path.joinpath("tests/subdir/conftest.py")
config.pluginmanager._importconftest(
conftest1, importmode="prepend", rootpath=pytester.path
)
ihook_a = session.gethookproxy(pytester.path / "tests")
assert ihook_a is not None
config.pluginmanager._importconftest(
conftest2, importmode="prepend", rootpath=pytester.path
)
ihook_b = session.gethookproxy(pytester.path / "tests")
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")
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()
"""
)
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])
class TestPytestPluginManager:
def test_register_imported_modules(self) -> None:
pm = PytestPluginManager()
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):
mod = types.ModuleType("pytest_xyz")
monkeypatch.setitem(sys.modules, "pytest_xyz", mod)
pm = PytestPluginManager()
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="#")
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
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:
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(
skipping1="""
import pytest
pytest.skip("hello", allow_module_level=True)
"""
)
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
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="#")
monkeypatch.setitem(os.environ, "PYTEST_PLUGINS", "xy123")
l1 = len(pytestpm.get_plugins())
pytestpm.consider_env()
l2 = len(pytestpm.get_plugins())
assert l2 == l1 + 1
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(
"""
import pytest
def test_hello(pytestconfig):
plugin = pytestconfig.pluginmanager.get_plugin('pytest_x500')
assert plugin is not None
"""
)
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
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()
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
)
with pytest.raises(ImportError):
pytestpm.consider_conftest(mod, registration_name="unused")
class TestPytestPluginManagerBootstrapming:
def test_preparse_args(self, pytestpm: PytestPluginManager) -> None:
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
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.
When unregister cacheprovider, then unregister stepwise too.
"""
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")