Files
lintunes/tests/conftest.py
T
travandClaude Opus 5.5 27abdbdb63 v0.32.1: a stuck Rabbit connection can no longer freeze the window
Opening Connections called find_device() on the GUI thread, which lists the
gvfs-MTP mount; on a wedged mount that blocks forever in an uninterruptible
FUSE wait and the app had to be force-quit. DeviceWatcher now probes on a
daemon thread (one at a time, STUCK after 3 s, late answers still land), the
menu reads its cached answer, and a stuck mount shows "Rabbit isn't
responding…" with a Reset Connection button (kill gvfsd-mtp, remount via
gio). Tests can no longer reach the real mount (conftest patches
find_device), which also takes the suite from ~200 s to ~22 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:58:26 -07:00

139 lines
4.5 KiB
Python

import os
import shutil
import subprocess
import pytest
# Let widget-level tests run headless; harmless for the non-GUI tests.
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
@pytest.fixture(autouse=True)
def _isolate_eventlog(tmp_path, monkeypatch):
"""Keep the control-provenance log out of the real ~/.cache during tests
(Player/MPRIS methods log unconditionally)."""
from lintunes import eventlog
monkeypatch.setattr(eventlog, "_log_path",
lambda: tmp_path / "control-events.log")
@pytest.fixture(autouse=True)
def _no_real_device(monkeypatch):
"""No test may look at the real gvfs mount: a wedged gvfs-MTP mount
blocks the listing uninterruptibly and hangs the suite (Round 71).
Tests that need a device patch their own finder or pass a fake root."""
from lintunes import device_sync
monkeypatch.setattr(device_sync, "find_device", lambda: None)
@pytest.fixture(scope="session")
def qapp():
"""A Qt application so QObject/QTimer and real widgets work in tests.
Prefers a GUI QApplication (offscreen) so widget tests are possible;
QApplication is a QCoreApplication, so existing non-GUI tests are unaffected.
Falls back to QCoreApplication if no GUI platform is available.
"""
from PyQt6.QtCore import QCoreApplication
inst = QCoreApplication.instance()
if inst is not None:
return inst
try:
from PyQt6.QtWidgets import QApplication
return QApplication([])
except Exception:
return QCoreApplication([])
def _generate_audio(path, codec_args):
"""Generate a 1-second sine-wave audio file with ffmpeg."""
if shutil.which("ffmpeg") is None:
pytest.skip("ffmpeg not available to generate audio fixtures")
result = subprocess.run(
["ffmpeg", "-y", "-f", "lavfi", "-i", "sine=frequency=440:duration=1",
*codec_args, str(path)],
capture_output=True,
)
if result.returncode != 0:
pytest.skip(f"ffmpeg could not generate {path.suffix} fixture")
return path
@pytest.fixture
def mp3_file(tmp_path):
return _generate_audio(tmp_path / "test.mp3", ["-codec:a", "libmp3lame", "-b:a", "64k"])
@pytest.fixture
def m4a_file(tmp_path):
return _generate_audio(tmp_path / "test.m4a", ["-codec:a", "aac", "-b:a", "64k"])
@pytest.fixture
def flac_file(tmp_path):
return _generate_audio(tmp_path / "test.flac", ["-codec:a", "flac"])
@pytest.fixture
def jpeg_bytes(tmp_path):
"""A tiny valid JPEG for artwork tests."""
if shutil.which("ffmpeg") is None:
pytest.skip("ffmpeg not available to generate image fixture")
path = tmp_path / "art.jpg"
result = subprocess.run(
["ffmpeg", "-y", "-f", "lavfi", "-i", "color=red:size=8x8",
"-frames:v", "1", str(path)],
capture_output=True,
)
if result.returncode != 0:
pytest.skip("ffmpeg could not generate jpeg fixture")
return path.read_bytes()
# ---- real-Syncthing tests (Cassette) ----
# Two throwaway Syncthing instances on loopback (scripts/cassette_pair.py).
# Slow-ish (a handshake takes Syncthing some seconds), so opt-in:
# python3 -m pytest -m syncthing --syncthing
def pytest_addoption(parser):
parser.addoption("--syncthing", action="store_true",
help="run tests that start two real Syncthing instances")
def pytest_configure(config):
config.addinivalue_line(
"markers", "syncthing: starts two real Syncthing instances "
"(opt in with --syncthing)")
def pytest_collection_modifyitems(config, items):
if config.getoption("--syncthing"):
return
skip = pytest.mark.skip(reason="needs --syncthing")
for item in items:
if "syncthing" in item.keywords:
item.add_marker(skip)
@pytest.fixture
def syncthing_pair(tmp_path):
"""[alice, bob, carol] Instances from scripts/cassette_pair.py, stopped
after."""
if shutil.which("syncthing") is None:
pytest.skip("syncthing isn't installed")
import importlib.util
from pathlib import Path
spec = importlib.util.spec_from_file_location(
"cassette_pair",
Path(__file__).resolve().parent.parent / "scripts" / "cassette_pair.py")
pair = importlib.util.module_from_spec(spec)
import sys
sys.modules[spec.name] = pair # dataclasses look their module up there
spec.loader.exec_module(pair)
out = tmp_path / "pair"
instances = pair.start(out, names=("alice", "bob", "carol"))
try:
yield instances
finally:
pair.stop(out)