Skip to content

Commit bbfc8d1

Browse files
authored
conversion of exit codes to enum + exposure (#5420)
conversion of exit codes to enum + exposure
2 parents cf27af7 + ab6ed38 commit bbfc8d1

24 files changed

+131
-114
lines changed

changelog/5125.removal.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
``Session.exitcode`` values are now coded in ``pytest.ExitCode``, an ``IntEnum``. This makes the exit code available for consumer code and are more explicit other than just documentation. User defined exit codes are still valid, but should be used with caution.
2+
3+
The team doesn't expect this change to break test suites or plugins in general, except in esoteric/specific scenarios.
4+
5+
**pytest-xdist** users should upgrade to ``1.29.0`` or later, as ``pytest-xdist`` required a compatibility fix because of this change.

doc/en/reference.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,14 @@ ExceptionInfo
727727
.. autoclass:: _pytest._code.ExceptionInfo
728728
:members:
729729

730+
731+
pytest.ExitCode
732+
~~~~~~~~~~~~~~~
733+
734+
.. autoclass:: _pytest.main.ExitCode
735+
:members:
736+
737+
730738
FixtureDef
731739
~~~~~~~~~~
732740

doc/en/usage.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ Running ``pytest`` can result in six different exit codes:
3333
:Exit code 4: pytest command line usage error
3434
:Exit code 5: No tests were collected
3535

36+
They are repressended by the :class:`_pytest.main.ExitCode` enum.
37+
3638
Getting help on version, option names, environment variables
3739
--------------------------------------------------------------
3840

src/_pytest/config/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def main(args=None, plugins=None):
4848
:arg plugins: list of plugin objects to be auto-registered during
4949
initialization.
5050
"""
51-
from _pytest.main import EXIT_USAGEERROR
51+
from _pytest.main import ExitCode
5252

5353
try:
5454
try:
@@ -78,7 +78,7 @@ def main(args=None, plugins=None):
7878
tw = py.io.TerminalWriter(sys.stderr)
7979
for msg in e.args:
8080
tw.line("ERROR: {}\n".format(msg), red=True)
81-
return EXIT_USAGEERROR
81+
return ExitCode.USAGE_ERROR
8282

8383

8484
class cmdline: # compatibility namespace

src/_pytest/main.py

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
""" core implementation of testing process: init, session, runtest loop. """
2+
import enum
23
import fnmatch
34
import functools
45
import os
@@ -18,13 +19,26 @@
1819
from _pytest.outcomes import exit
1920
from _pytest.runner import collect_one_node
2021

21-
# exitcodes for the command line
22-
EXIT_OK = 0
23-
EXIT_TESTSFAILED = 1
24-
EXIT_INTERRUPTED = 2
25-
EXIT_INTERNALERROR = 3
26-
EXIT_USAGEERROR = 4
27-
EXIT_NOTESTSCOLLECTED = 5
22+
23+
class ExitCode(enum.IntEnum):
24+
"""
25+
Encodes the valid exit codes by pytest.
26+
27+
Currently users and plugins may supply other exit codes as well.
28+
"""
29+
30+
#: tests passed
31+
OK = 0
32+
#: tests failed
33+
TESTS_FAILED = 1
34+
#: pytest was interrupted
35+
INTERRUPTED = 2
36+
#: an internal error got in the way
37+
INTERNAL_ERROR = 3
38+
#: pytest was missused
39+
USAGE_ERROR = 4
40+
#: pytest couldnt find tests
41+
NO_TESTS_COLLECTED = 5
2842

2943

3044
def pytest_addoption(parser):
@@ -188,7 +202,7 @@ def pytest_configure(config):
188202
def wrap_session(config, doit):
189203
"""Skeleton command line program"""
190204
session = Session(config)
191-
session.exitstatus = EXIT_OK
205+
session.exitstatus = ExitCode.OK
192206
initstate = 0
193207
try:
194208
try:
@@ -198,13 +212,13 @@ def wrap_session(config, doit):
198212
initstate = 2
199213
session.exitstatus = doit(config, session) or 0
200214
except UsageError:
201-
session.exitstatus = EXIT_USAGEERROR
215+
session.exitstatus = ExitCode.USAGE_ERROR
202216
raise
203217
except Failed:
204-
session.exitstatus = EXIT_TESTSFAILED
218+
session.exitstatus = ExitCode.TESTS_FAILED
205219
except (KeyboardInterrupt, exit.Exception):
206220
excinfo = _pytest._code.ExceptionInfo.from_current()
207-
exitstatus = EXIT_INTERRUPTED
221+
exitstatus = ExitCode.INTERRUPTED
208222
if isinstance(excinfo.value, exit.Exception):
209223
if excinfo.value.returncode is not None:
210224
exitstatus = excinfo.value.returncode
@@ -217,7 +231,7 @@ def wrap_session(config, doit):
217231
except: # noqa
218232
excinfo = _pytest._code.ExceptionInfo.from_current()
219233
config.notify_exception(excinfo, config.option)
220-
session.exitstatus = EXIT_INTERNALERROR
234+
session.exitstatus = ExitCode.INTERNAL_ERROR
221235
if excinfo.errisinstance(SystemExit):
222236
sys.stderr.write("mainloop: caught unexpected SystemExit!\n")
223237

@@ -243,9 +257,9 @@ def _main(config, session):
243257
config.hook.pytest_runtestloop(session=session)
244258

245259
if session.testsfailed:
246-
return EXIT_TESTSFAILED
260+
return ExitCode.TESTS_FAILED
247261
elif session.testscollected == 0:
248-
return EXIT_NOTESTSCOLLECTED
262+
return ExitCode.NO_TESTS_COLLECTED
249263

250264

251265
def pytest_collection(session):

src/_pytest/pytester.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@
1919
from _pytest.assertion.rewrite import AssertionRewritingHook
2020
from _pytest.capture import MultiCapture
2121
from _pytest.capture import SysCapture
22-
from _pytest.main import EXIT_INTERRUPTED
23-
from _pytest.main import EXIT_OK
22+
from _pytest.main import ExitCode
2423
from _pytest.main import Session
2524
from _pytest.monkeypatch import MonkeyPatch
2625
from _pytest.pathlib import Path
@@ -691,7 +690,7 @@ def getnode(self, config, arg):
691690
p = py.path.local(arg)
692691
config.hook.pytest_sessionstart(session=session)
693692
res = session.perform_collect([str(p)], genitems=False)[0]
694-
config.hook.pytest_sessionfinish(session=session, exitstatus=EXIT_OK)
693+
config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK)
695694
return res
696695

697696
def getpathnode(self, path):
@@ -708,11 +707,11 @@ def getpathnode(self, path):
708707
x = session.fspath.bestrelpath(path)
709708
config.hook.pytest_sessionstart(session=session)
710709
res = session.perform_collect([x], genitems=False)[0]
711-
config.hook.pytest_sessionfinish(session=session, exitstatus=EXIT_OK)
710+
config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK)
712711
return res
713712

714713
def genitems(self, colitems):
715-
"""Generate all test items from a collection node.
714+
"""Generate all test items from a collection node.src/_pytest/main.py
716715
717716
This recurses into the collection node and returns a list of all the
718717
test items contained within.
@@ -841,7 +840,7 @@ class reprec:
841840

842841
# typically we reraise keyboard interrupts from the child run
843842
# because it's our user requesting interruption of the testing
844-
if ret == EXIT_INTERRUPTED and not no_reraise_ctrlc:
843+
if ret == ExitCode.INTERRUPTED and not no_reraise_ctrlc:
845844
calls = reprec.getcalls("pytest_keyboard_interrupt")
846845
if calls and calls[-1].excinfo.type == KeyboardInterrupt:
847846
raise KeyboardInterrupt()

src/_pytest/terminal.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,7 @@
1616

1717
import pytest
1818
from _pytest import nodes
19-
from _pytest.main import EXIT_INTERRUPTED
20-
from _pytest.main import EXIT_NOTESTSCOLLECTED
21-
from _pytest.main import EXIT_OK
22-
from _pytest.main import EXIT_TESTSFAILED
23-
from _pytest.main import EXIT_USAGEERROR
19+
from _pytest.main import ExitCode
2420

2521
REPORT_COLLECTING_RESOLUTION = 0.5
2622

@@ -654,17 +650,17 @@ def pytest_sessionfinish(self, exitstatus):
654650
outcome.get_result()
655651
self._tw.line("")
656652
summary_exit_codes = (
657-
EXIT_OK,
658-
EXIT_TESTSFAILED,
659-
EXIT_INTERRUPTED,
660-
EXIT_USAGEERROR,
661-
EXIT_NOTESTSCOLLECTED,
653+
ExitCode.OK,
654+
ExitCode.TESTS_FAILED,
655+
ExitCode.INTERRUPTED,
656+
ExitCode.USAGE_ERROR,
657+
ExitCode.NO_TESTS_COLLECTED,
662658
)
663659
if exitstatus in summary_exit_codes:
664660
self.config.hook.pytest_terminal_summary(
665661
terminalreporter=self, exitstatus=exitstatus, config=self.config
666662
)
667-
if exitstatus == EXIT_INTERRUPTED:
663+
if exitstatus == ExitCode.INTERRUPTED:
668664
self._report_keyboardinterrupt()
669665
del self._keyboardinterrupt_memo
670666
self.summary_stats()

src/pytest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from _pytest.fixtures import fixture
1616
from _pytest.fixtures import yield_fixture
1717
from _pytest.freeze_support import freeze_includes
18+
from _pytest.main import ExitCode
1819
from _pytest.main import Session
1920
from _pytest.mark import MARK_GEN as mark
2021
from _pytest.mark import param
@@ -57,6 +58,7 @@
5758
"Collector",
5859
"deprecated_call",
5960
"exit",
61+
"ExitCode",
6062
"fail",
6163
"File",
6264
"fixture",

testing/acceptance_test.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88
import py
99

1010
import pytest
11-
from _pytest.main import EXIT_NOTESTSCOLLECTED
12-
from _pytest.main import EXIT_USAGEERROR
11+
from _pytest.main import ExitCode
1312
from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG
1413

1514

@@ -24,7 +23,7 @@ class TestGeneralUsage:
2423
def test_config_error(self, testdir):
2524
testdir.copy_example("conftest_usageerror/conftest.py")
2625
result = testdir.runpytest(testdir.tmpdir)
27-
assert result.ret == EXIT_USAGEERROR
26+
assert result.ret == ExitCode.USAGE_ERROR
2827
result.stderr.fnmatch_lines(["*ERROR: hello"])
2928
result.stdout.fnmatch_lines(["*pytest_unconfigure_called"])
3029

@@ -83,7 +82,7 @@ def pytest_unconfigure():
8382
"""
8483
)
8584
result = testdir.runpytest("-s", "asd")
86-
assert result.ret == 4 # EXIT_USAGEERROR
85+
assert result.ret == ExitCode.USAGE_ERROR
8786
result.stderr.fnmatch_lines(["ERROR: file not found*asd"])
8887
result.stdout.fnmatch_lines(["*---configure", "*---unconfigure"])
8988

@@ -229,7 +228,7 @@ def pytest_collect_directory():
229228
"""
230229
)
231230
result = testdir.runpytest()
232-
assert result.ret == EXIT_NOTESTSCOLLECTED
231+
assert result.ret == ExitCode.NO_TESTS_COLLECTED
233232
result.stdout.fnmatch_lines(["*1 skip*"])
234233

235234
def test_issue88_initial_file_multinodes(self, testdir):
@@ -247,7 +246,7 @@ def test_issue93_initialnode_importing_capturing(self, testdir):
247246
"""
248247
)
249248
result = testdir.runpytest()
250-
assert result.ret == EXIT_NOTESTSCOLLECTED
249+
assert result.ret == ExitCode.NO_TESTS_COLLECTED
251250
assert "should not be seen" not in result.stdout.str()
252251
assert "stderr42" not in result.stderr.str()
253252

@@ -290,13 +289,13 @@ def test_issue109_sibling_conftests_not_loaded(self, testdir):
290289
sub2 = testdir.mkdir("sub2")
291290
sub1.join("conftest.py").write("assert 0")
292291
result = testdir.runpytest(sub2)
293-
assert result.ret == EXIT_NOTESTSCOLLECTED
292+
assert result.ret == ExitCode.NO_TESTS_COLLECTED
294293
sub2.ensure("__init__.py")
295294
p = sub2.ensure("test_hello.py")
296295
result = testdir.runpytest(p)
297-
assert result.ret == EXIT_NOTESTSCOLLECTED
296+
assert result.ret == ExitCode.NO_TESTS_COLLECTED
298297
result = testdir.runpytest(sub1)
299-
assert result.ret == EXIT_USAGEERROR
298+
assert result.ret == ExitCode.USAGE_ERROR
300299

301300
def test_directory_skipped(self, testdir):
302301
testdir.makeconftest(
@@ -308,7 +307,7 @@ def pytest_ignore_collect():
308307
)
309308
testdir.makepyfile("def test_hello(): pass")
310309
result = testdir.runpytest()
311-
assert result.ret == EXIT_NOTESTSCOLLECTED
310+
assert result.ret == ExitCode.NO_TESTS_COLLECTED
312311
result.stdout.fnmatch_lines(["*1 skipped*"])
313312

314313
def test_multiple_items_per_collector_byid(self, testdir):
@@ -410,18 +409,18 @@ def test_a():
410409
def test_report_all_failed_collections_initargs(self, testdir):
411410
testdir.makeconftest(
412411
"""
413-
from _pytest.main import EXIT_USAGEERROR
412+
from _pytest.main import ExitCode
414413
415414
def pytest_sessionfinish(exitstatus):
416-
assert exitstatus == EXIT_USAGEERROR
415+
assert exitstatus == ExitCode.USAGE_ERROR
417416
print("pytest_sessionfinish_called")
418417
"""
419418
)
420419
testdir.makepyfile(test_a="def", test_b="def")
421420
result = testdir.runpytest("test_a.py::a", "test_b.py::b")
422421
result.stderr.fnmatch_lines(["*ERROR*test_a.py::a*", "*ERROR*test_b.py::b*"])
423422
result.stdout.fnmatch_lines(["pytest_sessionfinish_called"])
424-
assert result.ret == EXIT_USAGEERROR
423+
assert result.ret == ExitCode.USAGE_ERROR
425424

426425
@pytest.mark.usefixtures("recwarn")
427426
def test_namespace_import_doesnt_confuse_import_hook(self, testdir):
@@ -612,7 +611,7 @@ def test_invoke_with_invalid_type(self, capsys):
612611

613612
def test_invoke_with_path(self, tmpdir, capsys):
614613
retcode = pytest.main(tmpdir)
615-
assert retcode == EXIT_NOTESTSCOLLECTED
614+
assert retcode == ExitCode.NO_TESTS_COLLECTED
616615
out, err = capsys.readouterr()
617616

618617
def test_invoke_plugin_api(self, testdir, capsys):
@@ -1160,7 +1159,7 @@ def test_fixture_mock_integration(testdir):
11601159

11611160
def test_usage_error_code(testdir):
11621161
result = testdir.runpytest("-unknown-option-")
1163-
assert result.ret == EXIT_USAGEERROR
1162+
assert result.ret == ExitCode.USAGE_ERROR
11641163

11651164

11661165
@pytest.mark.filterwarnings("default")

testing/python/collect.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import _pytest._code
66
import pytest
7-
from _pytest.main import EXIT_NOTESTSCOLLECTED
7+
from _pytest.main import ExitCode
88
from _pytest.nodes import Collector
99

1010

@@ -246,7 +246,7 @@ def prop(self):
246246
"""
247247
)
248248
result = testdir.runpytest()
249-
assert result.ret == EXIT_NOTESTSCOLLECTED
249+
assert result.ret == ExitCode.NO_TESTS_COLLECTED
250250

251251

252252
class TestFunction:
@@ -1140,7 +1140,7 @@ class Test(object):
11401140
)
11411141
result = testdir.runpytest()
11421142
assert "TypeError" not in result.stdout.str()
1143-
assert result.ret == EXIT_NOTESTSCOLLECTED
1143+
assert result.ret == ExitCode.NO_TESTS_COLLECTED
11441144

11451145

11461146
def test_collect_functools_partial(testdir):

testing/test_assertrewrite.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from _pytest.assertion.rewrite import AssertionRewritingHook
1616
from _pytest.assertion.rewrite import PYTEST_TAG
1717
from _pytest.assertion.rewrite import rewrite_asserts
18-
from _pytest.main import EXIT_NOTESTSCOLLECTED
18+
from _pytest.main import ExitCode
1919

2020

2121
def setup_module(mod):
@@ -692,7 +692,7 @@ def test_zipfile(self, testdir):
692692
import test_gum.test_lizard"""
693693
% (z_fn,)
694694
)
695-
assert testdir.runpytest().ret == EXIT_NOTESTSCOLLECTED
695+
assert testdir.runpytest().ret == ExitCode.NO_TESTS_COLLECTED
696696

697697
def test_readonly(self, testdir):
698698
sub = testdir.mkdir("testing")
@@ -792,7 +792,7 @@ def test_package_without__init__py(self, testdir):
792792
pkg = testdir.mkdir("a_package_without_init_py")
793793
pkg.join("module.py").ensure()
794794
testdir.makepyfile("import a_package_without_init_py.module")
795-
assert testdir.runpytest().ret == EXIT_NOTESTSCOLLECTED
795+
assert testdir.runpytest().ret == ExitCode.NO_TESTS_COLLECTED
796796

797797
def test_rewrite_warning(self, testdir):
798798
testdir.makeconftest(

0 commit comments

Comments
 (0)