Background
Follow-up to #2192 (foundation) and PR #2193 (pytest + Zuul infrastructure). Part of Tier 8 (#2199). osism/main.py (38 LOC) defines OsismApp, the cliff App subclass that wires the osism.commands entry-point namespace and replaces loguru's default sink with a custom stderr handler, plus main(), the osism console-script entry point (setup.cfg: osism = osism.main:main).
Scope
Add tests/unit/test_main.py covering OsismApp.__init__ and main() in osism/main.py. No coverage exists yet for this module.
Test targets
OsismApp.__init__() — main.py:13
Patch:
osism.main.logger (the loguru logger as imported in the module) — so the test does not destroy the process-global loguru sink configuration
Logging setup
- Construction calls
logger.remove() exactly once (drops loguru's default handler)
logger.add() is called once with sys.stderr, level="INFO", colorize=True and the <green>{time:...}</green> format string (assert via call_args.kwargs)
cliff wiring
- After construction:
app.command_manager.namespace == "osism.commands" (real CommandManager — cliff registers entry points lazily without importing the command modules, so real construction is cheap)
app.deferred_help is True
app.parser.description == "OSISM manager interface"
- Version is passed through from the package:
osism.__version__ (pbr) may be None when package metadata is unavailable (osism/__init__.py:13 fallback) — patch osism.main.__version__ to None and assert construction still succeeds
app.run(["--version"]) raises SystemExit with code 0 (argparse version action fires before any command module is imported) — cheap smoke test that the option parser is wired
main() — main.py:31
Patch osism.main.OsismApp.
main(["reconciler", "sync"]) constructs the app exactly once and calls app.run(["reconciler", "sync"]) with the argv unchanged
- Return value pass-through: stub
app.run returning 42 → main(...) returns 42; returning 0 → returns 0
- Empty argv list is forwarded as-is (
app.run([])) — only against the mocked app; never call the real app.run([]) in a unit test, cliff would enter interactive shell mode
- Default-argument quirk:
argv=sys.argv[1:] is evaluated once at import time, so the default is frozen — tests must always pass argv explicitly; add a short comment in the test file documenting this
Mocking hints
- Patch
osism.main.logger before instantiating OsismApp; this both isolates the assertion and prevents the test from removing loguru handlers other tests may rely on. Do not configure the real loguru logger in tests.
CommandManager("osism.commands") only registers entry points; commands are imported lazily on find_command, so plain OsismApp() does not pull in the heavy command modules (openstacksdk etc.).
- For the
--version smoke test use pytest.raises(SystemExit) and check excinfo.value.code == 0; capture stdout with capsys if you want to assert the program name appears.
mocker.patch("osism.main.OsismApp") returns the class mock; use mock_app_cls.return_value.run.return_value = 42 and assert mock_app_cls.return_value.run.assert_called_once_with([...]).
Definition of Done
Dependencies
Background
Follow-up to #2192 (foundation) and PR #2193 (pytest + Zuul infrastructure). Part of Tier 8 (#2199).
osism/main.py(38 LOC) definesOsismApp, the cliffAppsubclass that wires theosism.commandsentry-point namespace and replaces loguru's default sink with a custom stderr handler, plusmain(), theosismconsole-script entry point (setup.cfg:osism = osism.main:main).Scope
Add
tests/unit/test_main.pycoveringOsismApp.__init__andmain()inosism/main.py. No coverage exists yet for this module.Test targets
OsismApp.__init__()—main.py:13Patch:
osism.main.logger(the loguru logger as imported in the module) — so the test does not destroy the process-global loguru sink configurationLogging setup
logger.remove()exactly once (drops loguru's default handler)logger.add()is called once withsys.stderr,level="INFO",colorize=Trueand the<green>{time:...}</green>format string (assert viacall_args.kwargs)cliff wiring
app.command_manager.namespace == "osism.commands"(realCommandManager— cliff registers entry points lazily without importing the command modules, so real construction is cheap)app.deferred_help is Trueapp.parser.description == "OSISM manager interface"osism.__version__(pbr) may beNonewhen package metadata is unavailable (osism/__init__.py:13fallback) — patchosism.main.__version__toNoneand assert construction still succeedsapp.run(["--version"])raisesSystemExitwith code 0 (argparse version action fires before any command module is imported) — cheap smoke test that the option parser is wiredmain()—main.py:31Patch
osism.main.OsismApp.main(["reconciler", "sync"])constructs the app exactly once and callsapp.run(["reconciler", "sync"])with the argv unchangedapp.runreturning42→main(...)returns42; returning0→ returns0app.run([])) — only against the mocked app; never call the realapp.run([])in a unit test, cliff would enter interactive shell modeargv=sys.argv[1:]is evaluated once at import time, so the default is frozen — tests must always passargvexplicitly; add a short comment in the test file documenting thisMocking hints
osism.main.loggerbefore instantiatingOsismApp; this both isolates the assertion and prevents the test from removing loguru handlers other tests may rely on. Do not configure the real loguru logger in tests.CommandManager("osism.commands")only registers entry points; commands are imported lazily onfind_command, so plainOsismApp()does not pull in the heavy command modules (openstacksdk etc.).--versionsmoke test usepytest.raises(SystemExit)and checkexcinfo.value.code == 0; capture stdout withcapsysif you want to assert the program name appears.mocker.patch("osism.main.OsismApp")returns the class mock; usemock_app_cls.return_value.run.return_value = 42and assertmock_app_cls.return_value.run.assert_called_once_with([...]).Definition of Done
tests/unit/test_main.pycreatedpytest --cov=osism.mainshows ≥ 95 %pipenv run pytest tests/unit/test_main.pypasses locallyflake8,mypy,python-blackremain greenpython-osism-unit-testspassesDependencies