From 2a3c21645e5c303a71694c0ff68d0a56c2d734d5 Mon Sep 17 00:00:00 2001 From: Gleb Nikonorov Date: Sat, 6 Jun 2020 02:38:18 -0400 Subject: [PATCH] Commit solution thus far, needs to be polished up pre PR --- doc/en/reference.rst | 10 +++++ src/_pytest/config/__init__.py | 33 +++++++++++++--- testing/test_config.py | 71 ++++++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 6 deletions(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 7348636a2..d84d9d405 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1604,3 +1604,13 @@ passed multiple times. The expected format is ``name=value``. For example:: [pytest] xfail_strict = True + + +.. confval:: required_plugins + + A space seperated list of plugins that must be present for pytest to run + + .. code-block:: ini + + [pytest] + require_plugins = pluginA pluginB pluginC diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 27083900d..83878a486 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -952,6 +952,12 @@ class Config: self._parser.extra_info["inifile"] = self.inifile self._parser.addini("addopts", "extra command line options", "args") self._parser.addini("minversion", "minimally required pytest version") + self._parser.addini( + "require_plugins", + "plugins that must be present for pytest to run", + type="args", + default=[], + ) self._override_ini = ns.override_ini or () def _consider_importhook(self, args: Sequence[str]) -> None: @@ -1035,7 +1041,8 @@ class Config: self.known_args_namespace = ns = self._parser.parse_known_args( args, namespace=copy.copy(self.option) ) - self._validatekeys() + self._validate_keys() + self._validate_plugins() if self.known_args_namespace.confcutdir is None and self.inifile: confcutdir = py.path.local(self.inifile).dirname self.known_args_namespace.confcutdir = confcutdir @@ -1078,12 +1085,26 @@ class Config: ) ) - def _validatekeys(self): + def _validate_keys(self) -> None: for key in sorted(self._get_unknown_ini_keys()): - message = "Unknown config ini key: {}\n".format(key) - if self.known_args_namespace.strict_config: - fail(message, pytrace=False) - sys.stderr.write("WARNING: {}".format(message)) + self._emit_warning_or_fail("Unknown config ini key: {}\n".format(key)) + + def _validate_plugins(self) -> None: + # so iterate over all required plugins and see if pluginmanager hasplugin + # NOTE: This also account for -p no: ( e.g: -p no:celery ) + # raise ValueError(self._parser._inidict['requiredplugins']) + # raise ValueError(self.getini("requiredplugins")) + # raise ValueError(self.pluginmanager.hasplugin('debugging')) + for plugin in self.getini("require_plugins"): + if not self.pluginmanager.hasplugin(plugin): + self._emit_warning_or_fail( + "Missing required plugin: {}\n".format(plugin) + ) + + def _emit_warning_or_fail(self, message: str) -> None: + if self.known_args_namespace.strict_config: + fail(message, pytrace=False) + sys.stderr.write("WARNING: {}".format(message)) def _get_unknown_ini_keys(self) -> List[str]: parser_inicfg = self._parser._inidict diff --git a/testing/test_config.py b/testing/test_config.py index 867012e93..f88a9a0ce 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -212,6 +212,77 @@ class TestParseIni: with pytest.raises(pytest.fail.Exception, match=exception_text): testdir.runpytest("--strict-config") + @pytest.mark.parametrize( + "ini_file_text, stderr_output, exception_text", + [ + ( + """ + [pytest] + require_plugins = fakePlugin1 fakePlugin2 + """, + [ + "WARNING: Missing required plugin: fakePlugin1", + "WARNING: Missing required plugin: fakePlugin2", + ], + "Missing required plugin: fakePlugin1", + ), + ( + """ + [pytest] + require_plugins = a monkeypatch z + """, + [ + "WARNING: Missing required plugin: a", + "WARNING: Missing required plugin: z", + ], + "Missing required plugin: a", + ), + ( + """ + [pytest] + require_plugins = a monkeypatch z + addopts = -p no:monkeypatch + """, + [ + "WARNING: Missing required plugin: a", + "WARNING: Missing required plugin: monkeypatch", + "WARNING: Missing required plugin: z", + ], + "Missing required plugin: a", + ), + ( + """ + [some_other_header] + require_plugins = wont be triggered + [pytest] + minversion = 5.0.0 + """, + [], + "", + ), + ( + """ + [pytest] + minversion = 5.0.0 + """, + [], + "", + ), + ], + ) + def test_missing_required_plugins( + self, testdir, ini_file_text, stderr_output, exception_text + ): + testdir.tmpdir.join("pytest.ini").write(textwrap.dedent(ini_file_text)) + testdir.parseconfig() + + result = testdir.runpytest() + result.stderr.fnmatch_lines(stderr_output) + + if stderr_output: + with pytest.raises(pytest.fail.Exception, match=exception_text): + testdir.runpytest("--strict-config") + class TestConfigCmdlineParsing: def test_parsing_again_fails(self, testdir):