d780960381
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
163 lines
5.7 KiB
Python
163 lines
5.7 KiB
Python
import asyncio
|
|
import sys
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture
|
|
def listener():
|
|
from whisper_local.hotkey._evdev import EvdevHotkeyListener
|
|
return EvdevHotkeyListener(key_name="KEY_F12")
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform != "linux", reason="evdev only available on Linux")
|
|
class TestHotkeyListener:
|
|
def test_init_stores_key_name(self, listener):
|
|
assert listener.key_name == "KEY_F12"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_key_down_calls_callback(self, listener):
|
|
on_press = AsyncMock()
|
|
listener.on_press = on_press
|
|
|
|
await listener._handle_key_event(key_down=True)
|
|
on_press.assert_awaited_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_key_up_calls_callback(self, listener):
|
|
on_release = AsyncMock()
|
|
listener.on_release = on_release
|
|
|
|
await listener._handle_key_event(key_down=False)
|
|
on_release.assert_awaited_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_no_callback_no_error(self, listener):
|
|
await listener._handle_key_event(key_down=True)
|
|
await listener._handle_key_event(key_down=False)
|
|
|
|
|
|
class TestPynputKeyMapping:
|
|
"""Tests für die Key-Name-Übersetzung von evdev zu pynput."""
|
|
|
|
def test_map_function_key(self):
|
|
from whisper_local.hotkey._pynput import _evdev_to_pynput_key
|
|
from pynput.keyboard import Key
|
|
assert _evdev_to_pynput_key("KEY_F12") == Key.f12
|
|
|
|
def test_map_f1(self):
|
|
from whisper_local.hotkey._pynput import _evdev_to_pynput_key
|
|
from pynput.keyboard import Key
|
|
assert _evdev_to_pynput_key("KEY_F1") == Key.f1
|
|
|
|
def test_map_unknown_key_raises(self):
|
|
from whisper_local.hotkey._pynput import _evdev_to_pynput_key
|
|
with pytest.raises(ValueError, match="Unbekannter Key-Name"):
|
|
_evdev_to_pynput_key("KEY_NONEXISTENT_999")
|
|
|
|
|
|
class TestPynputHotkeyListener:
|
|
def test_init_stores_key_name(self):
|
|
from whisper_local.hotkey._pynput import PynputHotkeyListener
|
|
listener = PynputHotkeyListener("KEY_F12")
|
|
assert listener.key_name == "KEY_F12"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_key_event_press(self):
|
|
from whisper_local.hotkey._pynput import PynputHotkeyListener
|
|
listener = PynputHotkeyListener("KEY_F12")
|
|
on_press = AsyncMock()
|
|
listener.on_press = on_press
|
|
|
|
await listener._handle_key_event(key_down=True)
|
|
on_press.assert_awaited_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_key_event_release(self):
|
|
from whisper_local.hotkey._pynput import PynputHotkeyListener
|
|
listener = PynputHotkeyListener("KEY_F12")
|
|
on_release = AsyncMock()
|
|
listener.on_release = on_release
|
|
|
|
await listener._handle_key_event(key_down=False)
|
|
on_release.assert_awaited_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_no_callback_no_error(self):
|
|
from whisper_local.hotkey._pynput import PynputHotkeyListener
|
|
listener = PynputHotkeyListener("KEY_F12")
|
|
await listener._handle_key_event(key_down=True)
|
|
await listener._handle_key_event(key_down=False)
|
|
|
|
def test_key_repeat_ignored(self):
|
|
from whisper_local.hotkey._pynput import PynputHotkeyListener
|
|
from pynput.keyboard import Key
|
|
listener = PynputHotkeyListener("KEY_F12")
|
|
listener._loop = MagicMock()
|
|
|
|
# Erstes Press — wird durchgelassen
|
|
listener._on_press(Key.f12)
|
|
assert listener._loop.call_soon_threadsafe.call_count == 1
|
|
|
|
# Wiederholte Presses (Key-Repeat) — werden ignoriert
|
|
listener._on_press(Key.f12)
|
|
listener._on_press(Key.f12)
|
|
assert listener._loop.call_soon_threadsafe.call_count == 1
|
|
|
|
# Release — wird durchgelassen
|
|
listener._on_release(Key.f12)
|
|
assert listener._loop.call_soon_threadsafe.call_count == 2
|
|
|
|
# Erneutes Release ohne Press — wird ignoriert
|
|
listener._on_release(Key.f12)
|
|
assert listener._loop.call_soon_threadsafe.call_count == 2
|
|
|
|
|
|
class TestPynputHotkeyListenerStop:
|
|
@pytest.mark.asyncio
|
|
async def test_stop_ends_listen(self):
|
|
from whisper_local.hotkey._pynput import PynputHotkeyListener
|
|
from unittest.mock import patch, MagicMock
|
|
listener = PynputHotkeyListener("KEY_F12")
|
|
|
|
with patch("whisper_local.hotkey._pynput.Listener") as mock_listener_cls:
|
|
mock_listener_cls.return_value = MagicMock()
|
|
listen_task = asyncio.create_task(listener.listen())
|
|
await asyncio.sleep(0) # Loop einen Schritt weiter
|
|
listener.stop()
|
|
await asyncio.wait_for(listen_task, timeout=1.0)
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform != "linux", reason="evdev nur auf Linux")
|
|
class TestEvdevListenerStop:
|
|
@pytest.mark.asyncio
|
|
async def test_stop_cancels_tasks_and_closes_devices(self):
|
|
from whisper_local.hotkey._evdev import EvdevHotkeyListener
|
|
|
|
listener = EvdevHotkeyListener("KEY_F12")
|
|
|
|
async def never_ending(device):
|
|
while True:
|
|
await asyncio.sleep(1)
|
|
|
|
fake_device = MagicMock()
|
|
fake_device.close = MagicMock()
|
|
|
|
with patch(
|
|
"whisper_local.hotkey._evdev.find_keyboard_devices",
|
|
return_value=[fake_device],
|
|
):
|
|
listener._read_device = never_ending
|
|
listen_task = asyncio.create_task(listener.listen())
|
|
await asyncio.sleep(0.05) # Loop-Schritt, damit listen() startet
|
|
|
|
assert len(listener._tasks) == 1
|
|
listener.stop()
|
|
await asyncio.sleep(0.05)
|
|
|
|
assert listener._tasks == []
|
|
fake_device.close.assert_called_once()
|
|
|
|
await asyncio.wait_for(listen_task, timeout=1.0)
|