467 lines
16 KiB
Python
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")
|