Merge branch 'master' of https://code.vitaligraf.de/info/whisper-local
This commit is contained in:
@@ -127,3 +127,36 @@ class TestPynputHotkeyListenerStop:
|
||||
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)
|
||||
|
||||
+175
-18
@@ -25,12 +25,12 @@ class TestCreateIcon:
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform != "win32", reason="Tray nur auf Windows")
|
||||
class TestWin32TrayApp:
|
||||
class TestPystrayApp:
|
||||
def test_set_state_updates_icon(self):
|
||||
from unittest.mock import MagicMock, patch
|
||||
from whisper_local.tray._tray import AppState, Win32TrayApp
|
||||
from whisper_local.tray._tray import AppState, PystrayApp
|
||||
|
||||
app = Win32TrayApp(on_settings=MagicMock(), on_quit=MagicMock())
|
||||
app = PystrayApp(on_settings=MagicMock(), on_quit=MagicMock())
|
||||
|
||||
mock_icon = MagicMock()
|
||||
app._icon = mock_icon
|
||||
@@ -41,9 +41,9 @@ class TestWin32TrayApp:
|
||||
|
||||
def test_set_state_before_start_is_safe(self):
|
||||
from unittest.mock import MagicMock
|
||||
from whisper_local.tray._tray import AppState, Win32TrayApp
|
||||
from whisper_local.tray._tray import AppState, PystrayApp
|
||||
|
||||
app = Win32TrayApp(on_settings=MagicMock(), on_quit=MagicMock())
|
||||
app = PystrayApp(on_settings=MagicMock(), on_quit=MagicMock())
|
||||
app.set_state(AppState.WAITING) # kein Fehler, _icon ist None
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ class TestApplySystemTheme:
|
||||
class TestCheckHotkeyConflict:
|
||||
def test_returns_false_when_key_is_free(self):
|
||||
from unittest.mock import patch, MagicMock
|
||||
from whisper_local.tray._settings import check_hotkey_conflict
|
||||
from whisper_local.tray._hotkey_record_pynput import check_hotkey_conflict
|
||||
|
||||
mock_user32 = MagicMock()
|
||||
mock_user32.RegisterHotKey.return_value = 1 # Erfolg
|
||||
@@ -123,7 +123,7 @@ class TestCheckHotkeyConflict:
|
||||
|
||||
def test_returns_true_when_key_is_taken(self):
|
||||
from unittest.mock import patch, MagicMock
|
||||
from whisper_local.tray._settings import check_hotkey_conflict
|
||||
from whisper_local.tray._hotkey_record_pynput import check_hotkey_conflict
|
||||
|
||||
mock_user32 = MagicMock()
|
||||
mock_user32.RegisterHotKey.return_value = 0 # Belegt
|
||||
@@ -134,7 +134,7 @@ class TestCheckHotkeyConflict:
|
||||
mock_user32.UnregisterHotKey.assert_not_called()
|
||||
|
||||
def test_returns_false_for_unknown_key(self):
|
||||
from whisper_local.tray._settings import check_hotkey_conflict
|
||||
from whisper_local.tray._hotkey_record_pynput import check_hotkey_conflict
|
||||
result = check_hotkey_conflict("KEY_NONEXISTENT_999")
|
||||
assert result is False
|
||||
|
||||
@@ -165,22 +165,22 @@ class TestListMicrophones:
|
||||
class TestPynputToEvdevKey:
|
||||
def test_function_key(self):
|
||||
from pynput.keyboard import Key
|
||||
from whisper_local.tray._settings import pynput_to_evdev_key
|
||||
from whisper_local.tray._hotkey_record_pynput import pynput_to_evdev_key
|
||||
assert pynput_to_evdev_key(Key.f12) == "KEY_F12"
|
||||
|
||||
def test_space_key(self):
|
||||
from pynput.keyboard import Key
|
||||
from whisper_local.tray._settings import pynput_to_evdev_key
|
||||
from whisper_local.tray._hotkey_record_pynput import pynput_to_evdev_key
|
||||
assert pynput_to_evdev_key(Key.space) == "KEY_SPACE"
|
||||
|
||||
def test_char_key(self):
|
||||
from pynput.keyboard import KeyCode
|
||||
from whisper_local.tray._settings import pynput_to_evdev_key
|
||||
from whisper_local.tray._hotkey_record_pynput import pynput_to_evdev_key
|
||||
key = KeyCode.from_char("a")
|
||||
assert pynput_to_evdev_key(key) == "KEY_A"
|
||||
|
||||
def test_unknown_returns_empty(self):
|
||||
from whisper_local.tray._settings import pynput_to_evdev_key
|
||||
from whisper_local.tray._hotkey_record_pynput import pynput_to_evdev_key
|
||||
assert pynput_to_evdev_key(None) == ""
|
||||
|
||||
|
||||
@@ -245,17 +245,29 @@ class TestSettingsDialog:
|
||||
|
||||
|
||||
class TestCreateTray:
|
||||
@pytest.mark.skipif(sys.platform != "win32", reason="Win32TrayApp nur auf Windows")
|
||||
def test_returns_win32_tray_on_windows(self):
|
||||
@pytest.mark.skipif(sys.platform != "win32", reason="PystrayApp nur auf Windows")
|
||||
def test_returns_pystray_on_windows(self):
|
||||
from unittest.mock import MagicMock
|
||||
from whisper_local.tray import create_tray
|
||||
from whisper_local.tray._tray import Win32TrayApp
|
||||
from whisper_local.tray._tray import PystrayApp
|
||||
|
||||
tray = create_tray(on_settings=MagicMock(), on_quit=MagicMock())
|
||||
assert isinstance(tray, Win32TrayApp)
|
||||
assert isinstance(tray, PystrayApp)
|
||||
|
||||
@pytest.mark.skipif(sys.platform == "win32", reason="NoOpTray nur auf nicht-Windows")
|
||||
def test_returns_noop_tray_on_non_windows(self):
|
||||
@pytest.mark.skipif(sys.platform != "linux", reason="Linux-only")
|
||||
def test_returns_pystray_on_linux(self):
|
||||
from unittest.mock import MagicMock
|
||||
from whisper_local.tray import create_tray
|
||||
from whisper_local.tray._tray import PystrayApp
|
||||
|
||||
tray = create_tray(on_settings=MagicMock(), on_quit=MagicMock())
|
||||
assert isinstance(tray, PystrayApp)
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.platform in ("win32", "linux"),
|
||||
reason="NoOpTray nur auf Plattformen ohne Tray-Unterstützung",
|
||||
)
|
||||
def test_returns_noop_tray_on_unsupported_platform(self):
|
||||
from unittest.mock import MagicMock
|
||||
from whisper_local.tray import create_tray
|
||||
from whisper_local.tray._tray import NoOpTray
|
||||
@@ -268,3 +280,148 @@ class TestCreateTray:
|
||||
assert AppState.WAITING is not None
|
||||
assert AppState.RECORDING is not None
|
||||
assert AppState.TRANSCRIBING is not None
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform != "linux", reason="evdev-Record nur auf Linux")
|
||||
class TestRecordHotkeyEvdev:
|
||||
def test_first_keydown_triggers_on_result(self):
|
||||
import threading
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
fake_event = MagicMock()
|
||||
fake_event.type = 1 # EV_KEY
|
||||
fake_event.code = 88 # KEY_F12
|
||||
fake_event.value = 1 # Keydown
|
||||
|
||||
fake_device = MagicMock()
|
||||
fake_device.fd = 42
|
||||
fake_device.read.return_value = iter([fake_event])
|
||||
fake_device.close = MagicMock()
|
||||
|
||||
cancel = threading.Event()
|
||||
results: list[tuple[str, bool]] = []
|
||||
|
||||
def on_result(name, conflict):
|
||||
results.append((name, conflict))
|
||||
|
||||
with patch(
|
||||
"whisper_local.tray._hotkey_record_evdev.find_all_keyboards",
|
||||
return_value=[fake_device],
|
||||
), patch(
|
||||
"whisper_local.tray._hotkey_record_evdev.selectors.DefaultSelector"
|
||||
) as mock_sel_cls:
|
||||
mock_sel = MagicMock()
|
||||
mock_sel_cls.return_value = mock_sel
|
||||
sel_key = MagicMock()
|
||||
sel_key.data = fake_device
|
||||
mock_sel.select.return_value = [(sel_key, None)]
|
||||
from whisper_local.tray._hotkey_record_evdev import record_hotkey
|
||||
record_hotkey(on_result, cancel)
|
||||
|
||||
assert results == [("KEY_F12", False)]
|
||||
fake_device.close.assert_called_once()
|
||||
|
||||
def test_cancel_event_stops_recording(self):
|
||||
import threading
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
fake_device = MagicMock()
|
||||
fake_device.fd = 42
|
||||
fake_device.read.return_value = iter([])
|
||||
fake_device.close = MagicMock()
|
||||
|
||||
cancel = threading.Event()
|
||||
cancel.set()
|
||||
|
||||
results: list[tuple[str, bool]] = []
|
||||
|
||||
with patch(
|
||||
"whisper_local.tray._hotkey_record_evdev.find_all_keyboards",
|
||||
return_value=[fake_device],
|
||||
), patch(
|
||||
"whisper_local.tray._hotkey_record_evdev.selectors.DefaultSelector"
|
||||
) as mock_sel_cls:
|
||||
mock_sel = MagicMock()
|
||||
mock_sel_cls.return_value = mock_sel
|
||||
mock_sel.select.return_value = []
|
||||
from whisper_local.tray._hotkey_record_evdev import record_hotkey
|
||||
record_hotkey(lambda n, c: results.append((n, c)), cancel)
|
||||
|
||||
assert results == []
|
||||
fake_device.close.assert_called_once()
|
||||
|
||||
def test_ignores_key_up_events(self):
|
||||
import threading
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
key_up = MagicMock()
|
||||
key_up.type = 1
|
||||
key_up.code = 88
|
||||
key_up.value = 0
|
||||
|
||||
key_down = MagicMock()
|
||||
key_down.type = 1
|
||||
key_down.code = 88
|
||||
key_down.value = 1
|
||||
|
||||
fake_device = MagicMock()
|
||||
fake_device.fd = 42
|
||||
fake_device.read.return_value = iter([key_up, key_down])
|
||||
fake_device.close = MagicMock()
|
||||
|
||||
cancel = threading.Event()
|
||||
results: list[tuple[str, bool]] = []
|
||||
|
||||
with patch(
|
||||
"whisper_local.tray._hotkey_record_evdev.find_all_keyboards",
|
||||
return_value=[fake_device],
|
||||
), patch(
|
||||
"whisper_local.tray._hotkey_record_evdev.selectors.DefaultSelector"
|
||||
) as mock_sel_cls:
|
||||
mock_sel = MagicMock()
|
||||
mock_sel_cls.return_value = mock_sel
|
||||
sel_key = MagicMock()
|
||||
sel_key.data = fake_device
|
||||
mock_sel.select.return_value = [(sel_key, None)]
|
||||
from whisper_local.tray._hotkey_record_evdev import record_hotkey
|
||||
record_hotkey(lambda n, c: results.append((n, c)), cancel)
|
||||
|
||||
assert results == [("KEY_F12", False)]
|
||||
|
||||
def test_skips_unknown_keycodes(self):
|
||||
import threading
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
unknown_event = MagicMock()
|
||||
unknown_event.type = 1 # EV_KEY
|
||||
unknown_event.code = 9999 # unbekannter Keycode
|
||||
unknown_event.value = 1 # Keydown
|
||||
|
||||
known_event = MagicMock()
|
||||
known_event.type = 1 # EV_KEY
|
||||
known_event.code = 88 # KEY_F12
|
||||
known_event.value = 1 # Keydown
|
||||
|
||||
fake_device = MagicMock()
|
||||
fake_device.fd = 42
|
||||
fake_device.read.return_value = iter([unknown_event, known_event])
|
||||
fake_device.close = MagicMock()
|
||||
|
||||
cancel = threading.Event()
|
||||
results: list[tuple[str, bool]] = []
|
||||
|
||||
with patch(
|
||||
"whisper_local.tray._hotkey_record_evdev.find_all_keyboards",
|
||||
return_value=[fake_device],
|
||||
), patch(
|
||||
"whisper_local.tray._hotkey_record_evdev.selectors.DefaultSelector"
|
||||
) as mock_sel_cls:
|
||||
mock_sel = MagicMock()
|
||||
mock_sel_cls.return_value = mock_sel
|
||||
sel_key = MagicMock()
|
||||
sel_key.data = fake_device
|
||||
mock_sel.select.return_value = [(sel_key, None)]
|
||||
from whisper_local.tray._hotkey_record_evdev import record_hotkey
|
||||
record_hotkey(lambda n, c: results.append((n, c)), cancel)
|
||||
|
||||
assert results == [("KEY_F12", False)]
|
||||
|
||||
Reference in New Issue
Block a user