Files
Tunetti/tests/test_main.py
NikkeDoy 7a70232239
All checks were successful
SonarQube Code Quality Scan / SonarQube Scan (push) Successful in 1m41s
🐛 | Fix SonarQube issues across codebase
- Add docstrings to all empty Qt stub methods (S1186)
- Fix BLOCKER S1845: rename playbackState to get_playback_state
- Rename enum fields to snake_case with CamelCase aliases (S116)
- Rename stub methods to snake_case (set_source, set_position, etc.) (S100)
- Rename unused timeout param to msecs (S1172)
- Remove unused state variables in tests (S1481)
- Replace 'is' with '==' for enum comparisons (S5795)
- Remove unnecessary list() calls (S7504)
- Rename QTh to qt_cls (S117)
- Remove commented-out code blocks (S125)
- Add docstring to FallbackServiceInterface.emit_properties_changed (S1186)
2026-06-09 18:34:54 +03:00

351 lines
17 KiB
Python

"""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
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 == 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 == 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()