"""Pytest configuration: mock platform-dependent libraries for unit tests. Pure helper-function tests (``test_player_helpers``, ``test_gui_helpers``) import modules that unconditionally load PySide6 / Qt native libraries at the top level. In CI environments (Docker without PulseAudio, EGL, etc.) these imports fail with ``ImportError`` for ``libpulse.so.0`` or ``libEGL.so.1``. Strategy -------- 1. First try to import PySide6/Qt for real. 2. If that succeeds → Qt is available (local dev machine). The try/except guards in ``player.py`` / ``gui.py`` will hit the ``try`` branch. No stubs needed. 3. If the real import fails → we install lightweight stubs in ``sys.modules`` so the modules can be imported and pure helper functions tested. The ``except`` branches in the production guards will be exercised instead. This way **both** branches of the guarded imports get coverage depending on the runtime environment, and ``new_coverage`` on SonarQube stops complaining. """ from __future__ import annotations import sys from unittest import mock # ═══════════════════════════════════════════════════════════════════════════ # Stub definitions (used only when real Qt is unavailable) # ═══════════════════════════════════════════════════════════════════════════ class _StubQObject: """Minimal replacement for PySide6.QtCore.QObject. Used as a base class so that ``class Foo(QObject)`` produces a real class with the methods defined in its body, not a MagicMock. """ pass class _StubQThread: """Minimal replacement for PySide6.QtCore.QThread.""" def start(self) -> None: """Stub — no-op; real QThread would start the event loop.""" pass def quit(self) -> None: pass def wait(self, timeout: int = 3000) -> bool: return True def _stub_signal(*types): """Minimal replacement for PySide6.QtCore.Signal.""" class _Signal: def __init__(self): self._handlers = set() def connect(self, slot): self._handlers.add(slot) def disconnect(self, slot=None): if slot: self._handlers.discard(slot) else: self._handlers.clear() def emit(self, *args, **kwargs): for handler in self._handlers: handler(*args, **kwargs) def __call__(self, *args, **kwargs): return self.emit(*args, **kwargs) return _Signal() def _stub_slot(*types): """Minimal replacement for PySide6.QtCore.Slot. Handles ``@Slot``, ``@Slot()``, and ``@Slot(int, dict)`` usage. """ if len(types) == 1 and callable(types[0]) and not isinstance(types[0], type): return types[0] return lambda f: f def _install_stubs() -> None: """Populate sys.modules with stubs for platform-dependent packages. Called only when PySide6 failed to import for real — guarantees that pure helper-function tests can still import ``player.py``/``gui.py`` and access their standalone utility functions. """ def _stub(name: str) -> mock.MagicMock: m = mock.MagicMock() m.__name__ = name return m MOD_QTCORE = "PySide6.QtCore" MOD_QTGUI = "PySide6.QtGui" MOD_QTMULTIMEDIA = "PySide6.QtMultimedia" MOD_QTNETWORK = "PySide6.QtNetwork" MOD_QTWIDGETS = "PySide6.QtWidgets" _PYSIDE_STUBS = { (MOD_QTCORE, "QObject"): _StubQObject, (MOD_QTCORE, "QThread"): _StubQThread, (MOD_QTCORE, "Signal"): _stub_signal, (MOD_QTCORE, "Slot"): _stub_slot, (MOD_QTCORE, "QUrl"): _stub("QUrl"), (MOD_QTCORE, "Qt"): _stub("Qt"), (MOD_QTCORE, "QTimer"): _stub("QTimer"), (MOD_QTCORE, "QRectF"): _stub("QRectF"), (MOD_QTCORE, "QPropertyAnimation"): _stub("QPropertyAnimation"), (MOD_QTCORE, "QEasingCurve"): _stub("QEasingCurve"), (MOD_QTCORE, "QLoggingCategory"): _stub("QLoggingCategory"), (MOD_QTCORE, "QEvent"): _stub("QEvent"), (MOD_QTGUI, "QFont"): _stub("QFont"), (MOD_QTGUI, "QPixmap"): _stub("QPixmap"), (MOD_QTGUI, "QPainter"): _stub("QPainter"), (MOD_QTGUI, "QColor"): _stub("QColor"), (MOD_QTGUI, "QBrush"): _stub("QBrush"), (MOD_QTMULTIMEDIA, "QMediaPlayer"): _stub("QMediaPlayer"), (MOD_QTMULTIMEDIA, "QAudioOutput"): _stub("QAudioOutput"), (MOD_QTMULTIMEDIA, "QAudioBufferOutput"): _stub("QAudioBufferOutput"), (MOD_QTNETWORK, "QNetworkAccessManager"): _stub("QNetworkAccessManager"), (MOD_QTNETWORK, "QNetworkRequest"): _stub("QNetworkRequest"), (MOD_QTNETWORK, "QNetworkReply"): _stub("QNetworkReply"), (MOD_QTWIDGETS, "QApplication"): _stub("QApplication"), (MOD_QTWIDGETS, "QCheckBox"): _stub("QCheckBox"), (MOD_QTWIDGETS, "QDialog"): _stub("QDialog"), (MOD_QTWIDGETS, "QHBoxLayout"): _stub("QHBoxLayout"), (MOD_QTWIDGETS, "QLabel"): _stub("QLabel"), (MOD_QTWIDGETS, "QLineEdit"): _stub("QLineEdit"), (MOD_QTWIDGETS, "QListWidget"): _stub("QListWidget"), (MOD_QTWIDGETS, "QListWidgetItem"): _stub("QListWidgetItem"), (MOD_QTWIDGETS, "QMainWindow"): _stub("QMainWindow"), (MOD_QTWIDGETS, "QMenu"): _stub("QMenu"), (MOD_QTWIDGETS, "QPushButton"): _stub("QPushButton"), (MOD_QTWIDGETS, "QScrollArea"): _stub("QScrollArea"), (MOD_QTWIDGETS, "QSlider"): _stub("QSlider"), (MOD_QTWIDGETS, "QStackedWidget"): _stub("QStackedWidget"), (MOD_QTWIDGETS, "QVBoxLayout"): _stub("QVBoxLayout"), (MOD_QTWIDGETS, "QWidget"): _stub("QWidget"), (MOD_QTWIDGETS, "QFrame"): _stub("QFrame"), (MOD_QTWIDGETS, "QToolButton"): _stub("QToolButton"), (MOD_QTWIDGETS, "QButtonGroup"): _stub("QButtonGroup"), (MOD_QTWIDGETS, "QSizePolicy"): _stub("QSizePolicy"), } for (mod_name, attr_name), stub_value in _PYSIDE_STUBS.items(): if mod_name not in sys.modules: mod = mock.MagicMock(__name__=mod_name.rsplit(".", 1)[-1]) sys.modules[mod_name] = mod setattr(sys.modules[mod_name], attr_name, stub_value) # ── yt-dlp ───────────────────────────────────────────────────────── if "yt_dlp" not in sys.modules: yt_dlp_mock = mock.MagicMock(__name__="yt_dlp") ydl_mock = mock.MagicMock() yt_dlp_mock.YoutubeDL.return_value.__enter__.return_value = ydl_mock sys.modules["yt_dlp"] = yt_dlp_mock # ── pypresence ───────────────────────────────────────────────────── if "pypresence" not in sys.modules: pypresence_mock = mock.MagicMock(__name__="pypresence") pypresence_mock.DiscordNotFound = type("DiscordNotFound", (Exception,), {}) sys.modules["pypresence"] = pypresence_mock if "pypresence.types" not in sys.modules: types_mock = mock.MagicMock(__name__="types") types_mock.ActivityType.LISTENING = "listening" sys.modules["pypresence.types"] = types_mock # ═══════════════════════════════════════════════════════════════════════════ # Bootstrap: try real Qt first, fall back to stubs only if that fails. # This ensures the try/except guards in player.py / gui.py are exercised # in both Qt-available and Qt-unavailable environments. # ═══════════════════════════════════════════════════════════════════════════ _QT_AVAILABLE = False try: # Attempt real import — this will fail in headless CI. import PySide6 # noqa: F401 import PySide6.QtCore # noqa: F401 import PySide6.QtMultimedia # noqa: F401 _QT_AVAILABLE = True except (ImportError, OSError): pass if not _QT_AVAILABLE: _install_stubs() # ═══════════════════════════════════════════════════════════════════════════ # Pytest hooks # ═══════════════════════════════════════════════════════════════════════════ def pytest_configure(config): config.addinivalue_line( "markers", "qt: mark test as requiring a real Qt runtime (skipped if PySide6 unavailable).", )