From 6843cd2ea98b4d4cd6edd5fc2bcd7a1a87d05aec Mon Sep 17 00:00:00 2001 From: Christian Berendt Date: Tue, 16 Jun 2026 08:51:12 +0200 Subject: [PATCH 1/2] Add unit tests for OsismApp and main in osism/main.py Cover the osism console-script entry point, which had no tests: - OsismApp.__init__: loguru's default handler is removed and a single stderr sink is added with level INFO, colorize, and the green-time format; the cliff App is wired to the osism.commands namespace, with deferred help, the "OSISM manager interface" description, a version passed through from the package (including the None fallback), and a working --version action that exits 0. - main: constructs the app once, forwards argv unchanged, and returns the app's exit code; the empty-argv edge is checked against a mocked app only. The loguru logger is patched before every construction so the process-global sink configuration is never mutated by the suite. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Christian Berendt --- tests/unit/test_main.py | 127 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 tests/unit/test_main.py diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py new file mode 100644 index 00000000..cb62c681 --- /dev/null +++ b/tests/unit/test_main.py @@ -0,0 +1,127 @@ +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for ``osism/main.py`` — the ``osism`` console-script entry point. + +Two units are covered: + +- ``OsismApp.__init__`` wires loguru's stderr sink and the cliff ``App`` (the + ``osism.commands`` entry-point namespace, deferred help, parser description, + and the ``--version`` action). Every test that constructs ``OsismApp`` + requests the ``mock_logger`` fixture so the process-global loguru + configuration is patched *before* construction and never mutated for real. +- ``main`` constructs the app once, forwards ``argv`` unchanged, and returns + the app's exit code. + +``main``'s ``argv=sys.argv[1:]`` default is evaluated once at import time, so +the default is frozen and reflects the arguments pytest itself was invoked +with. Every test therefore passes ``argv`` explicitly. The ``main`` tests run +only against a mocked ``OsismApp``; the real ``app.run([])`` is never called, +as cliff would drop into its interactive shell on an empty argv. +""" + +import sys + +import pytest + +from osism.main import OsismApp, main, __version__ + + +@pytest.fixture +def mock_logger(mocker): + """Patch the loguru ``logger`` imported by ``osism.main``. + + Returns the mock so tests can assert the sink configuration without + removing the loguru handlers other tests rely on. + """ + return mocker.patch("osism.main.logger") + + +# --------------------------------------------------------------------------- +# OsismApp.__init__ — logging setup +# --------------------------------------------------------------------------- + + +def test_init_removes_default_loguru_handler(mock_logger): + OsismApp() + mock_logger.remove.assert_called_once_with() + + +def test_init_adds_stderr_sink_with_expected_config(mock_logger): + OsismApp() + + mock_logger.add.assert_called_once() + call_args = mock_logger.add.call_args + assert call_args.args[0] is sys.stderr + assert call_args.kwargs["level"] == "INFO" + assert call_args.kwargs["colorize"] is True + assert call_args.kwargs["format"].startswith( + "{time:YYYY-MM-DD HH:mm:ss}" + ) + + +# --------------------------------------------------------------------------- +# OsismApp.__init__ — cliff wiring +# --------------------------------------------------------------------------- + + +def test_init_wires_osism_commands_namespace(mock_logger): + assert OsismApp().command_manager.namespace == "osism.commands" + + +def test_init_enables_deferred_help(mock_logger): + assert OsismApp().deferred_help is True + + +def test_init_sets_parser_description(mock_logger): + assert OsismApp().parser.description == "OSISM manager interface" + + +def test_init_succeeds_when_version_is_none(mock_logger, mocker): + # osism.__version__ (pbr) is None when package metadata is unavailable + # (osism/__init__.py fallback); construction must still succeed. + mocker.patch("osism.main.__version__", None) + assert isinstance(OsismApp(), OsismApp) + + +def test_version_option_exits_zero(mock_logger, capsys): + app = OsismApp() + with pytest.raises(SystemExit) as excinfo: + app.run(["--version"]) + assert excinfo.value.code == 0 + # cliff renders the version line as " "; the prog name comes + # from sys.argv[0] (so it is "pytest" here, "osism" only via the console + # script). Assert on the package version that main.py wires in, which is + # what this entry point actually controls. + assert str(__version__) in capsys.readouterr().out + + +# --------------------------------------------------------------------------- +# main — construct once, forward argv, return the app's exit code +# --------------------------------------------------------------------------- + + +def test_main_constructs_app_once_and_forwards_argv(mocker): + mock_app_cls = mocker.patch("osism.main.OsismApp") + + main(["reconciler", "sync"]) + + mock_app_cls.assert_called_once_with() + mock_app_cls.return_value.run.assert_called_once_with(["reconciler", "sync"]) + + +@pytest.mark.parametrize("rc", [42, 0]) +def test_main_returns_app_run_result(mocker, rc): + mock_app_cls = mocker.patch("osism.main.OsismApp") + mock_app_cls.return_value.run.return_value = rc + + assert main(["reconciler", "sync"]) == rc + + +def test_main_forwards_empty_argv(mocker): + # Only the mocked app sees the empty argv — never the real OsismApp, which + # would enter cliff's interactive shell on an empty argument list. + mock_app_cls = mocker.patch("osism.main.OsismApp") + + main([]) + + mock_app_cls.return_value.run.assert_called_once_with([]) From a30eb6da6f46759785f7c1316d7534e6273f9b73 Mon Sep 17 00:00:00 2001 From: Christian Berendt Date: Tue, 16 Jun 2026 08:51:16 +0200 Subject: [PATCH 2/2] Exclude __main__ entry guard in main.py from coverage The "if __name__ == '__main__'" script entry calls the real main() (and thus app.run with the process argv), so it cannot be exercised from an in-process unit test. Mark it "# pragma: no cover" so "pytest --cov=osism.main" reaches 100% and meets the >= 95% bar. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Christian Berendt --- osism/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osism/main.py b/osism/main.py index 82d26c23..ed072eb5 100644 --- a/osism/main.py +++ b/osism/main.py @@ -34,5 +34,5 @@ def main(argv=sys.argv[1:]): return result -if __name__ == "__main__": +if __name__ == "__main__": # pragma: no cover sys.exit(main(sys.argv[1:]))