"""Round 71: a wedged Rabbit mount must never freeze the window. The Connections menu used to call ``find_device()`` on the GUI thread, and on a wedged gvfs-MTP mount that listing blocks forever in an uninterruptible FUSE wait — trav had to force-quit. ``DeviceWatcher`` does the looking on a daemon thread and gives up waiting (not the thread) after a timeout. """ import threading import time from PyQt6.QtCore import QCoreApplication from lintunes import device_sync from lintunes.device_sync import ( ABSENT, PRESENT, STUCK, Device, DeviceWatcher, mtp_uris, reset_mtp_connection, ) def _pump(until, timeout=3.0): deadline = time.monotonic() + timeout while time.monotonic() < deadline: QCoreApplication.processEvents() if until(): return True time.sleep(0.01) return False def _device(tmp_path): return Device("Rabbit R1", tmp_path, tmp_path / "Music") class TestWatcher: def test_a_hung_finder_is_reported_stuck_without_blocking(self, qapp): release = threading.Event() watcher = DeviceWatcher(finder=lambda: release.wait() and None, timeout_ms=50) started = time.monotonic() watcher.probe() assert time.monotonic() - started < 0.5 # probe() returns at once assert _pump(lambda: watcher.state == STUCK) release.set() def test_one_probe_at_a_time(self, qapp): release = threading.Event() calls = [] def finder(): calls.append(1) release.wait() watcher = DeviceWatcher(finder=finder, timeout_ms=50) watcher.probe() watcher.probe() watcher.probe() assert _pump(lambda: watcher.state == STUCK) watcher.probe() assert len(calls) == 1 release.set() def test_a_late_answer_recovers(self, qapp, tmp_path): release = threading.Event() device = _device(tmp_path) watcher = DeviceWatcher(finder=lambda: release.wait() and device, timeout_ms=50) watcher.probe() assert _pump(lambda: watcher.state == STUCK) release.set() assert _pump(lambda: watcher.state == PRESENT) assert watcher.device is device def test_when_ready_fires_once_with_a_fresh_answer(self, qapp, tmp_path): watcher = DeviceWatcher(finder=lambda: None) seen = [] watcher.when_ready(lambda d, s: seen.append((d, s))) assert _pump(lambda: seen) assert seen == [(None, ABSENT)] _pump(lambda: False, timeout=0.1) assert len(seen) == 1 def test_when_ready_while_stuck_answers_immediately(self, qapp): release = threading.Event() watcher = DeviceWatcher(finder=lambda: release.wait(), timeout_ms=50) watcher.probe() assert _pump(lambda: watcher.state == STUCK) seen = [] watcher.when_ready(lambda d, s: seen.append(s)) assert seen == [STUCK] release.set() class TestReset: GIO = """Volume(0): Rabbit R1 Type: GProxyVolume (GProxyVolumeMonitorMTP) Mount(0): Rabbit R1 -> mtp://unknown_Rabbit_R1_9191/ Mount(1): mtp -> mtp://unknown_Rabbit_R1_9191/ Mount(2): Phone -> mtp://Google_Pixel_1234/ """ def test_uris_come_from_gio_not_the_mount(self): assert mtp_uris(self.GIO) == ["mtp://unknown_Rabbit_R1_9191/"] def test_reset_kills_then_remounts(self, monkeypatch): monkeypatch.setattr(time, "sleep", lambda s: None) commands = [] class R: returncode, stderr = 0, "" stdout = TestReset.GIO def run(cmd, **kw): commands.append(cmd) return R() assert reset_mtp_connection(run) is None assert commands == [["gio", "mount", "-l"], ["pkill", "-x", "gvfsd-mtp"], ["gio", "mount", "mtp://unknown_Rabbit_R1_9191/"]] class TestMenu: def test_menu_open_never_waits_on_the_mount(self, qapp, monkeypatch, tmp_path): from lintunes.gui.main_window import MainWindow from lintunes.library_manager import LibraryManager from lintunes.models import Library from lintunes.preferences import Preferences release = threading.Event() monkeypatch.setattr(device_sync, "find_device", lambda: release.wait() and None) manager = LibraryManager(Library(), tmp_path / "data") window = MainWindow(manager, Preferences(tmp_path / "data")) window._devices._timer.setInterval(50) try: started = time.monotonic() window._refresh_connection_actions() assert time.monotonic() - started < 0.5 assert not window._andtunes_action.isEnabled() assert _pump(lambda: window._andtunes_action.text() == "Rabbit isn't responding…") assert window._andtunes_action.isEnabled() finally: release.set() assert _pump(lambda: window._devices.state == ABSENT) assert window._andtunes_action.text() == "Sync to Rabbit" window.close()