All checks were successful
SonarQube Code Quality Scan / SonarQube Scan (push) Successful in 1m41s
- 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)
351 lines
17 KiB
Python
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()
|