From 44fc35fe2f74187b722dd1a5a5090a53980b4aac Mon Sep 17 00:00:00 2001 From: Tsvika Shapira Date: Sun, 27 Sep 2026 21:15:20 +0300 Subject: [PATCH] test(cyclopts): cover the reported-error exit codes The cyclopts CLI template started new projects at 85% coverage: nothing exercised the FileNotFoundError, PermissionError and ConnectionError branches of `main`, nor `_fail`. A parametrized test checks each exit code and the `error: ...` line, plus a ConnectionError subclass. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GyMoD8tx4oz1bpJ1tEL845 --- ... 'cyclopts' %}test_cli.py{% endif %}.jinja | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/project_name/tests/{% if cli_framework == 'cyclopts' %}test_cli.py{% endif %}.jinja b/project_name/tests/{% if cli_framework == 'cyclopts' %}test_cli.py{% endif %}.jinja index 1876e85..81c2b4a 100644 --- a/project_name/tests/{% if cli_framework == 'cyclopts' %}test_cli.py{% endif %}.jinja +++ b/project_name/tests/{% if cli_framework == 'cyclopts' %}test_cli.py{% endif %}.jinja @@ -1,7 +1,14 @@ import pytest from {{ package_name }} import __version__, cli -from {{ package_name }}.cli import EX_SOFTWARE, app, main +from {{ package_name }}.cli import ( + EX_NOINPUT, + EX_NOPERM, + EX_SOFTWARE, + EX_UNAVAILABLE, + app, + main, +) def test_version(capsys: pytest.CaptureFixture[str]) -> None: @@ -26,6 +33,32 @@ def test_main_usage_error() -> None: assert exc_info.value.code == 2 +@pytest.mark.parametrize( + ("error", "code"), + [ + (FileNotFoundError("missing.txt"), EX_NOINPUT), + (PermissionError("locked.txt"), EX_NOPERM), + (ConnectionError("down"), EX_UNAVAILABLE), + # a subclass lands on its parent's code + (ConnectionRefusedError("refused"), EX_UNAVAILABLE), + ], +) +def test_main_reported_error( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], + error: Exception, + code: int, +) -> None: + def explode(*_args: object, **_kwargs: object) -> None: + raise error + + monkeypatch.setattr(cli, "app", explode) + with pytest.raises(SystemExit) as exc_info: + main() + assert exc_info.value.code == code + assert capsys.readouterr().err == f"error: {error}\n" + + def test_main_unhandled_error(monkeypatch: pytest.MonkeyPatch) -> None: # Accept the call that `main` makes, so that the RuntimeError below is what # reaches it, rather than a TypeError over the signature.