79659281

Date: 2025-06-09 17:35:05
Score: 2
Natty:
Report link

When using pytest and verifying sys.exit() was called, or perhaps a SystemExit was directly raised, e.value.code contains the exit status only in a special case -- not in general.

BTW, we are discussing the "provisional" exit status, because, from https://docs.python.org/3/library/sys.html, trouble during Python cleanup could produce a different exit code.

The special case...
1. if sys.exit or SystemExit was called with a single int argument, like either of these statements
sys.exit(2)
raise SystemExit(2)
then e.value.code will match the exit status.

However, for these other cases, e.value.code WILL NOT match the exit status.
2. These statements, with no args
sys.exit()
raise SystemExit
raise SystemExit()
will produce an exit status of 0, but e.value.code == 0 is false.

3. These statements, with a string arg,
sys.exit('Trouble...')
raise SystemExit('Trouble...')
will produce an exit status of 1, but e.value.code == 1 is false.

Any other usage of sys.exit or raise SystemExit is irregular, but for completeness, for these usages e.value.code WILL NOT match the exit status.
4. passing in a single obj that is neither int or str, or passing
multiple args to the SystemExit call, like
sys.exit((4, 'Tribulation...'))
raise SystemExit(4, 'Tribulation)
will produce an exit status of 1, but e.value.code == 1 is false.

So, if you needed to make an assertion about the exit status, one approach
would be to assert exitstatus(e.value) == ...
and implement python's SystemExit logic in an exitstatus function, like

import re
import sys

import pytest


def exitstatus(value):
    """Determine the exit status based on the SystemExit object's attrs.

    More accurately, this is the *provisional* exit status, because
    trouble during Python cleanup could produce a different exit code.

    Example
        with pytest.raises(SystemExit) as e:
            sys.exit(2)
        assert exitstatus(e.value) == 2

    Example
        with pytest.raises(SystemExit) as e:
            raise SystemExit('Trouble...')
        assert exitstatus(e.value) == 1
    """ 

    if len(value.args) == 0:
        return 0
    elif len(value.args) == 1 and isinstance(value.args[0], int):
        return value.args[0]
    elif len(value.args) == 1 and isinstance(value.args[0], str):
        return 1
    else:
        # Irregular usage of sys.exit() or SystemExit will produce 
        # exit status 1.
        # If you want to trigger on any irregular usage so you can 
        # fix it, you could raise an exception here, to force your
        # unit test to fail.
        # Otherwise, well, the irregular usage does result in exit 
        # status 1, so this is faithful.
        return 1

def test_sysexit_0():
    with pytest.raises(SystemExit) as e:
        sys.exit(0)  # exit status should be 0

    assert exitstatus(e.value) == 0


def test_sysexit_2():
    with pytest.raises(SystemExit) as e:
        sys.exit(2)  # exit status should be 2

    assert exitstatus(e.value) == 2


def test_sysexit_noargs():
    with pytest.raises(SystemExit) as e:
        sys.exit()  # exit status should be 0

    assert exitstatus(e.value) == 0


def test_sysexit_msg():
    with pytest.raises(SystemExit,
            match='cmd: bad usage') as e:
        sys.exit('cmd: bad usage unrecognized option')  # exit status should be 1

    assert exitstatus(e.value) == 1

With Best Regards,
John R.

Reasons:
  • Blacklisted phrase (0.5): Best Regards
  • Blacklisted phrase (1): Regards
  • Long answer (-1):
  • Has code block (-0.5):
  • Self-answer (0.5):
  • Starts with a question (0.5): When
  • Low reputation (1):
Posted by: ruck