"""Tests for the main entry point (main.py). Strategy -------- - Module-level constants (``VERBOSE``, ``_LOG_LEVEL``, ``_LOG_FORMAT``, ``_QT_LOG_RULES``) are tested by clearing/modifying ``os.environ`` and then calling ``importlib.reload(main)`` so the top-level code re-evaluates. - The ``main()`` function is tested by mocking ``gui.run_gui`` and ``main.VERBOSE`` / ``main._QT_LOG_RULES`` so we never need a real Qt runtime. - The ``if __name__ == "__main__":`` block is tested via AST extraction so we can execute only that ``if``-block (not all module-level code) with a mocked ``main``. """ import importlib import logging import sys from pathlib import Path from unittest import mock import pytest # ═══════════════════════════════════════════════════════════════════════════ # Helpers # ═══════════════════════════════════════════════════════════════════════════ def _reload_main(): """Re-import the ``main`` module with a fresh module object. ``importlib.reload`` re-executes the module-level code but preserves the existing module object (and its identity in ``sys.modules``). We remove it entirely first so that even the import machinery runs from scratch — every time. This avoids stale state leaking across tests (e.g. the ``logging`` handler check inside ``basicConfig``, which is a no-op on subsequent calls). """ sys.modules.pop("main", None) import main as m # noqa: F811 return m # ═══════════════════════════════════════════════════════════════════════════ # Tests for the ``VERBOSE`` / ``TUNETTI_VERBOSE`` env-var # ═══════════════════════════════════════════════════════════════════════════ class TestVerboseEnvVar: """Module-level ``VERBOSE`` flag is driven by ``TUNETTI_VERBOSE``.""" # ── absent / empty ──────────────────────────────────────────────────── def test_verbose_disabled_when_unset(self): """``TUNETTI_VERBOSE`` absent → ``main.VERBOSE is False``.""" with mock.patch.dict("os.environ", {}, clear=True): main_mod = _reload_main() assert main_mod.VERBOSE is False def test_verbose_disabled_when_empty(self): """``TUNETTI_VERBOSE=""`` → ``main.VERBOSE is False``.""" with mock.patch.dict("os.environ", {"TUNETTI_VERBOSE": ""}, clear=True): main_mod = _reload_main() assert main_mod.VERBOSE is False # ── truthy ─────────────────────────────────────────────────────────── def test_verbose_enabled_with_1(self): """``TUNETTI_VERBOSE=1`` → ``main.VERBOSE is True``.""" with mock.patch.dict("os.environ", {"TUNETTI_VERBOSE": "1"}, clear=True): main_mod = _reload_main() assert main_mod.VERBOSE is True def test_verbose_enabled_with_true(self): """``TUNETTI_VERBOSE=true`` → ``main.VERBOSE is True``.""" with mock.patch.dict("os.environ", {"TUNETTI_VERBOSE": "true"}, clear=True): main_mod = _reload_main() assert main_mod.VERBOSE is True def test_verbose_enabled_with_yes(self): """``TUNETTI_VERBOSE=yes`` → ``main.VERBOSE is True``.""" with mock.patch.dict("os.environ", {"TUNETTI_VERBOSE": "yes"}, clear=True): main_mod = _reload_main() assert main_mod.VERBOSE is True def test_verbose_strips_whitespace(self): """``TUNETTI_VERBOSE=" 1 "`` (whitespace) → ``main.VERBOSE is True``.""" with mock.patch.dict("os.environ", {"TUNETTI_VERBOSE": " 1 "}, clear=True): main_mod = _reload_main() assert main_mod.VERBOSE is True # ── falsy ──────────────────────────────────────────────────────────── @pytest.mark.parametrize("val", ["0", "false", "no"]) def test_verbose_falsy_values(self, val): """``TUNETTI_VERBOSE={val}`` → ``main.VERBOSE is False``.""" with mock.patch.dict("os.environ", {"TUNETTI_VERBOSE": val}, clear=True): main_mod = _reload_main() assert main_mod.VERBOSE is False, f"expected False for {val!r}" # ═══════════════════════════════════════════════════════════════════════════ # Tests for derived module-level constants # ═══════════════════════════════════════════════════════════════════════════ class TestModuleConstants: """Constants derived from ``VERBOSE`` (``_LOG_LEVEL``, ``_LOG_FORMAT``, ``_QT_LOG_RULES``).""" # ── _LOG_LEVEL ─────────────────────────────────────────────────────── def test_log_level_info_by_default(self): """``_LOG_LEVEL`` is ``logging.INFO`` when not verbose.""" with mock.patch.dict("os.environ", {}, clear=True): main_mod = _reload_main() assert main_mod._LOG_LEVEL is logging.INFO def test_log_level_debug_when_verbose(self): """``_LOG_LEVEL`` is ``logging.DEBUG`` when verbose.""" with mock.patch.dict("os.environ", {"TUNETTI_VERBOSE": "1"}, clear=True): main_mod = _reload_main() assert main_mod._LOG_LEVEL is logging.DEBUG # ── _LOG_FORMAT ────────────────────────────────────────────────────── def test_log_format_includes_timestamp_in_verbose(self): """``_LOG_FORMAT`` contains ``%%(asctime)s`` in verbose mode.""" with mock.patch.dict("os.environ", {"TUNETTI_VERBOSE": "1"}, clear=True): main_mod = _reload_main() assert "%(asctime)s" in main_mod._LOG_FORMAT def test_log_format_omits_timestamp_in_normal(self): """``_LOG_FORMAT`` does **not** contain ``%%(asctime)s`` normally.""" with mock.patch.dict("os.environ", {}, clear=True): main_mod = _reload_main() assert "%(asctime)s" not in main_mod._LOG_FORMAT def test_log_format_differs_by_verbosity(self): """The two log-format strings are different (checks they actually vary).""" with mock.patch.dict("os.environ", {}, clear=True): normal_mod = _reload_main() normal_fmt = normal_mod._LOG_FORMAT with mock.patch.dict("os.environ", {"TUNETTI_VERBOSE": "1"}, clear=True): verbose_mod = _reload_main() assert verbose_mod._LOG_FORMAT != normal_fmt # ── _QT_LOG_RULES ──────────────────────────────────────────────────── def test_qt_rules_suppressed_by_default(self): """``_QT_LOG_RULES`` contains FFmpeg filter rules in normal mode.""" with mock.patch.dict("os.environ", {}, clear=True): main_mod = _reload_main() assert "qt.multimedia.ffmpeg" in main_mod._QT_LOG_RULES def test_qt_rules_empty_when_verbose(self): """``_QT_LOG_RULES`` is an empty string in verbose mode.""" with mock.patch.dict("os.environ", {"TUNETTI_VERBOSE": "1"}, clear=True): main_mod = _reload_main() assert main_mod._QT_LOG_RULES == "" def test_qt_rules_multiline(self): """``_QT_LOG_RULES`` spans multiple lines (one rule per line).""" with mock.patch.dict("os.environ", {}, clear=True): main_mod = _reload_main() lines = [ln for ln in main_mod._QT_LOG_RULES.splitlines() if ln.strip()] assert len(lines) >= 4 # ═══════════════════════════════════════════════════════════════════════════ # Tests for the ``main()`` function # ═══════════════════════════════════════════════════════════════════════════ class TestMainFunction: """``main()`` function behaviour in normal and verbose modes.""" # ── Normal mode ───────────────────────────────────────────────────── def test_passes_qt_rules_to_run_gui(self): """``main()`` forwards ``_QT_LOG_RULES`` to ``run_gui`` normally.""" main_mod = _reload_main() with ( mock.patch.object(main_mod, "VERBOSE", False), mock.patch.object(main_mod, "_QT_LOG_RULES", "qt.foo=false\nqt.bar=false"), mock.patch("gui.run_gui") as mock_run_gui, ): main_mod.main() mock_run_gui.assert_called_once_with( extra_qt_log_rules="qt.foo=false\nqt.bar=false", ) def test_does_not_touch_player_verbose_in_normal_mode(self): """Normal mode should **not** set ``player.VERBOSE``.""" main_mod = _reload_main() player_mock = mock.MagicMock() player_mock.VERBOSE = False with ( mock.patch.object(main_mod, "VERBOSE", False), mock.patch.object(main_mod, "_QT_LOG_RULES", ""), mock.patch("gui.run_gui"), mock.patch.dict("sys.modules", {"player": player_mock}), ): main_mod.main() assert player_mock.VERBOSE is False def test_does_not_log_debug_in_normal_mode(self): """Normal mode should **not** emit a debug log message.""" main_mod = _reload_main() with ( mock.patch.object(main_mod, "VERBOSE", False), mock.patch("gui.run_gui"), mock.patch("logging.getLogger") as mock_get_logger, ): mock_logger = mock.MagicMock() mock_get_logger.return_value = mock_logger main_mod.main() mock_logger.debug.assert_not_called() # ── Verbose mode ──────────────────────────────────────────────────── def test_passes_empty_rules_in_verbose_mode(self): """``main()`` passes empty ``_QT_LOG_RULES`` to ``run_gui`` when verbose.""" main_mod = _reload_main() with ( mock.patch.object(main_mod, "VERBOSE", True), mock.patch.object(main_mod, "_QT_LOG_RULES", ""), mock.patch("gui.run_gui") as mock_run_gui, ): main_mod.main() mock_run_gui.assert_called_once_with(extra_qt_log_rules="") def test_sets_player_verbose_in_verbose_mode(self): """Verbose mode sets ``player.VERBOSE = True``.""" main_mod = _reload_main() player_mock = mock.MagicMock() player_mock.VERBOSE = False with ( mock.patch.object(main_mod, "VERBOSE", True), mock.patch("gui.run_gui"), mock.patch.dict("sys.modules", {"player": player_mock}), ): main_mod.main() assert player_mock.VERBOSE is True def test_emits_debug_log_in_verbose_mode(self): """Verbose mode logs a debug message about verbose mode being active.""" main_mod = _reload_main() with ( mock.patch.object(main_mod, "VERBOSE", True), mock.patch("gui.run_gui"), mock.patch("logging.getLogger") as mock_get_logger, ): mock_logger = mock.MagicMock() mock_get_logger.return_value = mock_logger main_mod.main() mock_logger.debug.assert_called_once() (msg,) = mock_logger.debug.call_args[0] assert "Verbose mode enabled" in msg assert "yt-dlp" in msg assert "FFmpeg" in msg # ── Edge cases ────────────────────────────────────────────────────── def test_handles_empty_qt_rules_gracefully(self): """``run_gui`` is called even when ``_QT_LOG_RULES`` is empty.""" main_mod = _reload_main() with ( mock.patch.object(main_mod, "VERBOSE", False), mock.patch.object(main_mod, "_QT_LOG_RULES", ""), mock.patch("gui.run_gui") as mock_run_gui, ): main_mod.main() mock_run_gui.assert_called_once_with(extra_qt_log_rules="") def test_handles_very_large_qt_rules(self): """``main()`` passes a large multi-line rules string without issues.""" main_mod = _reload_main() large_rules = "\n".join(f"qt.category{i}=false" for i in range(50)) with ( mock.patch.object(main_mod, "VERBOSE", False), mock.patch.object(main_mod, "_QT_LOG_RULES", large_rules), mock.patch("gui.run_gui") as mock_run_gui, ): main_mod.main() mock_run_gui.assert_called_once_with(extra_qt_log_rules=large_rules) def test_run_gui_exception_propagates(self): """If ``run_gui`` raises, ``main()`` lets the exception propagate.""" main_mod = _reload_main() with ( mock.patch.object(main_mod, "VERBOSE", False), mock.patch.object(main_mod, "_QT_LOG_RULES", ""), mock.patch("gui.run_gui", side_effect=RuntimeError("gui failed")), ): with pytest.raises(RuntimeError, match="gui failed"): main_mod.main() # ═══════════════════════════════════════════════════════════════════════════ # Tests for the ``if __name__ == "__main__"`` block # ═══════════════════════════════════════════════════════════════════════════ class TestMainBlock: """``if __name__ == "__main__":`` entry point guard.""" def test_name_is_main_when_imported(self): """Module ``__name__`` is ``"main"`` (not ``"__main__"``) on import.""" main_mod = _reload_main() assert main_mod.__name__ == "main" def test_block_not_executed_on_import(self): """The ``__main__`` block does **not** execute when imported normally. We verify this by patching ``main()`` to raise an exception — if the block ran, the exception would be raised. """ main_mod = _reload_main() with mock.patch.object( main_mod, "main", side_effect=RuntimeError("main() was called!"), ): # A plain re-import should not trigger the error because __name__ # is "main", not "__main__". importlib.reload(main_mod) def test_source_contains_main_guard(self): """The source code contains the ``if __name__ == "__main__":`` guard. This verifies the guard exists without relying on AST compilation (which varies across Python versions). The guard is a Python language feature — we just need to confirm it's present. """ main_mod = _reload_main() source = Path(main_mod.__file__).read_text() assert 'if __name__ == "__main__":' in source, ( "Missing ``if __name__ == '__main__':`` guard. " "This guard must be present so ``main()`` is only called " "when the module is executed as a script." ) assert "main()" in source.split('if __name__ == "__main__":')[1] def test_main_block_executes_when_run_as_script(self): """When ``__name__ == '__main__'``, the guard calls ``main()``. We re-execute the module source with ``__name__`` set to ``'__main__'``, mocking ``gui.run_gui`` so the call does nothing. This covers line 62 (the actual ``main()`` call) for the coverage report. """ main_mod = _reload_main() with mock.patch("gui.run_gui") as mock_run_gui: file_path = Path(main_mod.__file__).resolve() code = compile(file_path.read_text(), str(file_path), "exec") ns = {"__name__": "__main__", "__file__": str(file_path)} exec(code, ns) mock_run_gui.assert_called_once()