169 lines
4.9 KiB
Python
169 lines
4.9 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import queue
|
|
import time
|
|
from pathlib import Path
|
|
|
|
from somafm_tui.player import Player
|
|
|
|
FAKE_MPV = '''#!/usr/bin/env python3
|
|
import json, os, socket, sys, time
|
|
|
|
sock_path = None
|
|
volume = 70
|
|
for arg in sys.argv[1:]:
|
|
if arg.startswith("--input-ipc-server="):
|
|
sock_path = arg.split("=", 1)[1]
|
|
elif arg.startswith("--volume="):
|
|
volume = int(arg.split("=", 1)[1])
|
|
|
|
log_path = os.environ.get("FAKE_MPV_LOG")
|
|
def log(line):
|
|
if log_path:
|
|
with open(log_path, "a") as handle:
|
|
handle.write(line + "\\n")
|
|
|
|
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
try:
|
|
os.unlink(sock_path)
|
|
except FileNotFoundError:
|
|
pass
|
|
server.bind(sock_path)
|
|
server.listen(1)
|
|
conn, _ = server.accept()
|
|
stream = conn.makefile("rwb", buffering=0)
|
|
|
|
def send(payload):
|
|
stream.write((json.dumps(payload) + "\\n").encode())
|
|
|
|
while True:
|
|
line = stream.readline()
|
|
if not line:
|
|
break
|
|
try:
|
|
message = json.loads(line)
|
|
except ValueError:
|
|
continue
|
|
command = message.get("command") or []
|
|
if not command:
|
|
continue
|
|
name = command[0]
|
|
if name == "observe_property":
|
|
prop = command[2]
|
|
if prop == "volume":
|
|
send({"event": "property-change", "id": command[1], "name": "volume", "data": volume})
|
|
elif prop == "pause":
|
|
send({"event": "property-change", "id": command[1], "name": "pause", "data": False})
|
|
elif name == "loadfile":
|
|
url = command[1]
|
|
log(url)
|
|
send({"event": "start-file"})
|
|
if "bad" in url:
|
|
time.sleep(0.05)
|
|
send({"event": "end-file", "reason": "error"})
|
|
else:
|
|
time.sleep(0.05)
|
|
send({"event": "file-loaded"})
|
|
send({"event": "property-change", "id": 1, "name": "pause", "data": False})
|
|
elif name == "set_property":
|
|
prop, value = command[1], command[2]
|
|
if prop == "pause":
|
|
send({"event": "property-change", "id": 1, "name": "pause", "data": bool(value)})
|
|
elif prop == "volume":
|
|
volume = int(value)
|
|
send({"event": "property-change", "id": 2, "name": "volume", "data": volume})
|
|
elif name == "stop":
|
|
send({"event": "end-file", "reason": "stop"})
|
|
elif name == "quit":
|
|
break
|
|
|
|
try:
|
|
stream.close()
|
|
except Exception:
|
|
pass
|
|
conn.close()
|
|
server.close()
|
|
'''
|
|
|
|
|
|
def wait_for(player: Player, predicate, timeout: float = 8.0):
|
|
deadline = time.monotonic() + timeout
|
|
seen = []
|
|
while time.monotonic() < deadline:
|
|
try:
|
|
event = player.events.get(timeout=0.1)
|
|
except queue.Empty:
|
|
continue
|
|
seen.append(event)
|
|
if predicate(event):
|
|
return event, seen
|
|
raise AssertionError(f"timed out; last events: {seen[-8:]}")
|
|
|
|
|
|
def install_fake_mpv(tmp_path: Path, monkeypatch, log_path: Path) -> None:
|
|
script = tmp_path / "mpv"
|
|
script.write_text(FAKE_MPV)
|
|
script.chmod(0o755)
|
|
monkeypatch.setenv("PATH", f"{tmp_path}{os.pathsep}{os.environ.get('PATH', '')}")
|
|
monkeypatch.setenv("FAKE_MPV_LOG", str(log_path))
|
|
|
|
|
|
def test_mpv_ipc_play_pause_volume_retry_stop(tmp_path, monkeypatch) -> None:
|
|
log_path = tmp_path / "mpv.log"
|
|
install_fake_mpv(tmp_path, monkeypatch, log_path)
|
|
|
|
player = Player()
|
|
player.start()
|
|
assert player.backend == "mpv"
|
|
player.set_volume(55)
|
|
player.play(
|
|
[
|
|
"https://example.invalid/bad-stream",
|
|
"https://example.invalid/good-stream",
|
|
]
|
|
)
|
|
try:
|
|
event, _ = wait_for(
|
|
player,
|
|
lambda ev: ev.kind == "state"
|
|
and ev.state == "playing"
|
|
and "good-stream" in ev.url,
|
|
timeout=10,
|
|
)
|
|
assert event.state == "playing"
|
|
assert "bad-stream" in log_path.read_text()
|
|
assert "good-stream" in log_path.read_text()
|
|
|
|
player.pause()
|
|
wait_for(player, lambda ev: ev.kind == "state" and ev.state == "paused")
|
|
|
|
player.resume()
|
|
wait_for(player, lambda ev: ev.kind == "state" and ev.state == "playing")
|
|
|
|
player.set_volume(42)
|
|
volume_event, _ = wait_for(
|
|
player, lambda ev: ev.kind == "volume" and ev.volume == 42
|
|
)
|
|
assert volume_event.volume == 42
|
|
|
|
player.stop()
|
|
wait_for(player, lambda ev: ev.kind == "state" and ev.state == "stopped")
|
|
finally:
|
|
player.shutdown()
|
|
if player._thread is not None:
|
|
player._thread.join(timeout=2)
|
|
assert player._thread is None or not player._thread.is_alive()
|
|
|
|
|
|
def test_backend_detection_without_tools(tmp_path, monkeypatch) -> None:
|
|
empty = tmp_path / "empty"
|
|
empty.mkdir()
|
|
monkeypatch.setenv("PATH", str(empty))
|
|
player = Player()
|
|
player.start()
|
|
assert player.backend is None
|
|
event, _ = wait_for(player, lambda ev: ev.kind == "state", timeout=1)
|
|
assert event.state == "error"
|
|
player.shutdown()
|