|
13 | 13 | import setuptools |
14 | 14 |
|
15 | 15 | from _pytest.config import ExitCode |
| 16 | +from _pytest.monkeypatch import MonkeyPatch |
16 | 17 | from _pytest.pathlib import symlink_or_skip |
17 | 18 | from _pytest.pytester import Pytester |
18 | 19 | import pytest |
@@ -510,9 +511,10 @@ def test_plugins_given_as_strings( |
510 | 511 | ) -> None: |
511 | 512 | """Test that str values passed to main() as `plugins` arg are |
512 | 513 | interpreted as module names to be imported and registered (#855).""" |
513 | | - with pytest.raises(ImportError) as excinfo: |
514 | | - pytest.main([str(pytester.path)], plugins=["invalid.module"]) |
515 | | - assert "invalid" in str(excinfo.value) |
| 514 | + # A plugin which cannot be found is a usage error, reported through the |
| 515 | + # return value rather than raised out of pytest.main() (#993). |
| 516 | + ret = pytest.main([str(pytester.path)], plugins=["invalid.module"]) |
| 517 | + assert ret == ExitCode.USAGE_ERROR |
516 | 518 |
|
517 | 519 | p = pytester.path.joinpath("test_test_plugins_given_as_strings.py") |
518 | 520 | p.write_text("def test_foo(): pass", encoding="utf-8") |
@@ -1088,6 +1090,128 @@ def main(): |
1088 | 1090 | result.stdout.no_fnmatch_line("*INTERNALERROR>*") |
1089 | 1091 |
|
1090 | 1092 |
|
| 1093 | +class TestStartupPluginImportErrors: |
| 1094 | + """Exit codes for plugins which fail to load at startup (#993). |
| 1095 | +
|
| 1096 | + A plugin which cannot be found means pytest was pointed at something which |
| 1097 | + is not there, which is a usage error; a plugin which is found but blows up |
| 1098 | + while importing is a defect in the plugin, reported as an internal error. |
| 1099 | + """ |
| 1100 | + |
| 1101 | + @pytest.fixture |
| 1102 | + def broken_plugin(self, pytester: Pytester) -> Pytester: |
| 1103 | + pytester.syspathinsert() |
| 1104 | + pytester.makepyfile(myplugin="raise ValueError('plugin is broken')") |
| 1105 | + pytester.makepyfile("def test_foo(): pass") |
| 1106 | + return pytester |
| 1107 | + |
| 1108 | + @pytest.fixture |
| 1109 | + def missing_plugin(self, pytester: Pytester) -> Pytester: |
| 1110 | + pytester.syspathinsert() |
| 1111 | + pytester.makepyfile("def test_foo(): pass") |
| 1112 | + return pytester |
| 1113 | + |
| 1114 | + def test_missing_via_cmdline(self, missing_plugin: Pytester) -> None: |
| 1115 | + result = missing_plugin.runpytest("-p", "nosuchplugin") |
| 1116 | + assert result.ret == ExitCode.USAGE_ERROR |
| 1117 | + result.stderr.fnmatch_lines(['*Error importing plugin "nosuchplugin"*']) |
| 1118 | + |
| 1119 | + def test_missing_via_conftest(self, missing_plugin: Pytester) -> None: |
| 1120 | + missing_plugin.makeconftest("pytest_plugins = ['nosuchplugin']") |
| 1121 | + result = missing_plugin.runpytest() |
| 1122 | + assert result.ret == ExitCode.USAGE_ERROR |
| 1123 | + |
| 1124 | + def test_missing_via_env( |
| 1125 | + self, missing_plugin: Pytester, monkeypatch: MonkeyPatch |
| 1126 | + ) -> None: |
| 1127 | + monkeypatch.setenv("PYTEST_PLUGINS", "nosuchplugin") |
| 1128 | + result = missing_plugin.runpytest() |
| 1129 | + assert result.ret == ExitCode.USAGE_ERROR |
| 1130 | + |
| 1131 | + def test_broken_via_cmdline(self, broken_plugin: Pytester) -> None: |
| 1132 | + result = broken_plugin.runpytest("-p", "myplugin") |
| 1133 | + assert result.ret == ExitCode.INTERNAL_ERROR |
| 1134 | + result.stderr.fnmatch_lines( |
| 1135 | + [ |
| 1136 | + 'Error while loading plugin "myplugin".', |
| 1137 | + "*myplugin.py:1: in <module>*", |
| 1138 | + "E*ValueError: plugin is broken", |
| 1139 | + ] |
| 1140 | + ) |
| 1141 | + |
| 1142 | + def test_broken_via_conftest(self, broken_plugin: Pytester) -> None: |
| 1143 | + broken_plugin.makeconftest("pytest_plugins = ['myplugin']") |
| 1144 | + result = broken_plugin.runpytest() |
| 1145 | + assert result.ret == ExitCode.INTERNAL_ERROR |
| 1146 | + |
| 1147 | + def test_broken_via_env( |
| 1148 | + self, broken_plugin: Pytester, monkeypatch: MonkeyPatch |
| 1149 | + ) -> None: |
| 1150 | + monkeypatch.setenv("PYTEST_PLUGINS", "myplugin") |
| 1151 | + result = broken_plugin.runpytest() |
| 1152 | + assert result.ret == ExitCode.INTERNAL_ERROR |
| 1153 | + |
| 1154 | + def test_broken_via_entry_point( |
| 1155 | + self, pytester: Pytester, monkeypatch: MonkeyPatch |
| 1156 | + ) -> None: |
| 1157 | + monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) |
| 1158 | + |
| 1159 | + class DummyEntryPoint: |
| 1160 | + name = "myplugin" |
| 1161 | + group = "pytest11" |
| 1162 | + |
| 1163 | + def load(self): |
| 1164 | + raise ValueError("plugin is broken") |
| 1165 | + |
| 1166 | + class Distribution: |
| 1167 | + version = "1.0" |
| 1168 | + files = ("foo.txt",) |
| 1169 | + metadata = {"name": "foo"} |
| 1170 | + entry_points = (DummyEntryPoint(),) |
| 1171 | + |
| 1172 | + monkeypatch.setattr( |
| 1173 | + importlib.metadata, "distributions", lambda: (Distribution(),) |
| 1174 | + ) |
| 1175 | + pytester.makepyfile("def test_foo(): pass") |
| 1176 | + result = pytester.runpytest() |
| 1177 | + assert result.ret == ExitCode.INTERNAL_ERROR |
| 1178 | + |
| 1179 | + def test_import_error_without_args(self, pytester: Pytester) -> None: |
| 1180 | + """A bare ``raise ImportError`` used to crash with an IndexError (#993).""" |
| 1181 | + pytester.syspathinsert() |
| 1182 | + pytester.makepyfile(myplugin="raise ImportError") |
| 1183 | + pytester.makepyfile("def test_foo(): pass") |
| 1184 | + result = pytester.runpytest("-p", "myplugin") |
| 1185 | + assert result.ret == ExitCode.INTERNAL_ERROR |
| 1186 | + result.stderr.no_fnmatch_line("*IndexError*") |
| 1187 | + result.stderr.fnmatch_lines(['Error while loading plugin "myplugin".']) |
| 1188 | + |
| 1189 | + def test_missing_dependency_is_not_a_usage_error(self, pytester: Pytester) -> None: |
| 1190 | + """The plugin was found; one of *its* imports is unsatisfied (#993).""" |
| 1191 | + pytester.syspathinsert() |
| 1192 | + pytester.makepyfile(myplugin="import nosuchdependency") |
| 1193 | + pytester.makepyfile("def test_foo(): pass") |
| 1194 | + result = pytester.runpytest("-p", "myplugin") |
| 1195 | + assert result.ret == ExitCode.INTERNAL_ERROR |
| 1196 | + |
| 1197 | + def test_missing_submodule_of_existing_package(self, pytester: Pytester) -> None: |
| 1198 | + """The package exists but the requested plugin module within it does not.""" |
| 1199 | + pytester.syspathinsert() |
| 1200 | + pytester.mkpydir("mypkg") |
| 1201 | + pytester.makepyfile("def test_foo(): pass") |
| 1202 | + result = pytester.runpytest("-p", "mypkg.nosuchmodule") |
| 1203 | + assert result.ret == ExitCode.USAGE_ERROR |
| 1204 | + |
| 1205 | + def test_conftest_import_failure_stays_a_usage_error( |
| 1206 | + self, pytester: Pytester |
| 1207 | + ) -> None: |
| 1208 | + """conftest.py is not a plugin; it keeps reporting a usage error (#993).""" |
| 1209 | + pytester.makeconftest("raise ValueError('conftest is broken')") |
| 1210 | + pytester.makepyfile("def test_foo(): pass") |
| 1211 | + result = pytester.runpytest() |
| 1212 | + assert result.ret == ExitCode.USAGE_ERROR |
| 1213 | + |
| 1214 | + |
1091 | 1215 | def test_import_plugin_unicode_name(pytester: Pytester) -> None: |
1092 | 1216 | pytester.makepyfile(myplugin="") |
1093 | 1217 | pytester.makepyfile("def test(): pass") |
|
0 commit comments