Files
fenrir/tests/unit/test_output_manager.py
2026-06-04 14:21:49 -04:00

277 lines
8.3 KiB
Python

import importlib.util
import threading
import time
from pathlib import Path
from unittest.mock import Mock
import pytest
from fenrirscreenreader.core.outputManager import OutputManager
def build_output_manager():
settings_manager = Mock()
settings_manager.get_setting_as_bool.return_value = True
settings_manager.get_setting_as_float.return_value = 1.0
sound_driver = Mock()
speech_driver = Mock()
output_manager = OutputManager()
output_manager.env = {
"soundIcons": {
"ACCEPT": "/tmp/Accept.wav",
"ERRORSCREEN": "/tmp/ErrorScreen.wav",
},
"runtime": {
"SettingsManager": settings_manager,
"SoundDriver": sound_driver,
"SpeechDriver": speech_driver,
"DebugManager": Mock(write_debug_out=Mock()),
"TextManager": Mock(
replace_head_lines=Mock(side_effect=lambda text: text)
),
"PunctuationManager": Mock(
proceed_punctuation=Mock(
side_effect=lambda text, _ignore_punctuation: text
)
),
},
}
return output_manager, sound_driver, speech_driver
def load_key_interrupt_module():
module_path = (
Path(__file__).resolve().parents[2]
/ "src"
/ "fenrirscreenreader"
/ "commands"
/ "onKeyInput"
/ "10000-shut_up.py"
)
spec = importlib.util.spec_from_file_location(
"fenrir_key_interrupt", module_path
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
@pytest.mark.unit
def test_present_text_allows_sound_only_feedback():
output_manager, sound_driver, _speech_driver = build_output_manager()
output_manager.present_text("", sound_icon="Accept", interrupt=False)
sound_driver.play_sound_file.assert_called_once_with(
"/tmp/Accept.wav", False
)
@pytest.mark.unit
def test_present_text_sound_icon_with_interrupt_cancels_speech():
output_manager, sound_driver, speech_driver = build_output_manager()
output_manager.present_text(
"end of screen", sound_icon="Accept", interrupt=True
)
speech_driver.cancel.assert_called_once_with()
sound_driver.play_sound_file.assert_called_once_with(
"/tmp/Accept.wav", True
)
speech_driver.speak.assert_not_called()
@pytest.mark.unit
def test_play_sound_supports_error_alias():
output_manager, sound_driver, _speech_driver = build_output_manager()
assert output_manager.play_sound("Error") is True
sound_driver.play_sound_file.assert_called_once_with(
"/tmp/ErrorScreen.wav", True
)
@pytest.mark.unit
def test_interrupt_output_async_does_not_block_on_slow_cancel():
output_manager, _sound_driver, speech_driver = build_output_manager()
interrupt_started = threading.Event()
release_interrupt = threading.Event()
def slow_cancel():
interrupt_started.set()
release_interrupt.wait(timeout=1.0)
speech_driver.cancel.side_effect = slow_cancel
start_time = time.monotonic()
output_manager.interrupt_output_async()
elapsed = time.monotonic() - start_time
try:
assert interrupt_started.wait(timeout=0.2)
assert elapsed < 0.2
output_manager.interrupt_output_async()
assert speech_driver.cancel.call_count == 1
finally:
release_interrupt.set()
output_manager.interrupt_thread.join(timeout=1.0)
@pytest.mark.unit
def test_interrupt_output_waits_only_briefly_for_slow_cancel():
output_manager, _sound_driver, speech_driver = build_output_manager()
interrupt_started = threading.Event()
release_interrupt = threading.Event()
def slow_cancel():
interrupt_started.set()
release_interrupt.wait(timeout=1.0)
speech_driver.cancel.side_effect = slow_cancel
start_time = time.monotonic()
output_manager.interrupt_output()
elapsed = time.monotonic() - start_time
try:
assert interrupt_started.wait(timeout=0.2)
assert elapsed < 0.2
output_manager.interrupt_output()
assert speech_driver.cancel.call_count == 1
finally:
release_interrupt.set()
output_manager.interrupt_thread.join(timeout=1.0)
@pytest.mark.unit
def test_cancel_speech_skips_when_speech_driver_is_busy():
output_manager, _sound_driver, speech_driver = build_output_manager()
output_manager.speech_driver_lock_timeout = 0.01
output_manager.speech_driver_lock.acquire()
try:
start_time = time.monotonic()
output_manager.cancel_speech()
elapsed = time.monotonic() - start_time
finally:
output_manager.speech_driver_lock.release()
assert elapsed < 0.2
speech_driver.cancel.assert_not_called()
@pytest.mark.unit
def test_speak_text_drops_speech_when_cancel_holds_driver_lock():
output_manager, _sound_driver, speech_driver = build_output_manager()
output_manager.speech_driver_lock_timeout = 0.01
output_manager.speech_driver_lock.acquire()
try:
start_time = time.monotonic()
output_manager.speak_text("hello", interrupt=False, flush=False)
elapsed = time.monotonic() - start_time
finally:
output_manager.speech_driver_lock.release()
assert elapsed < 0.2
speech_driver.speak.assert_not_called()
@pytest.mark.unit
def test_present_text_records_speech_history_when_enabled():
output_manager, _sound_driver, speech_driver = build_output_manager()
speech_history_manager = Mock(add_text=Mock())
output_manager.env["runtime"]["SpeechHistoryManager"] = (
speech_history_manager
)
output_manager.present_text("hello history", interrupt=False)
speech_history_manager.add_text.assert_called_once_with("hello history")
speech_driver.speak.assert_called_once()
@pytest.mark.unit
def test_present_text_does_not_record_when_speech_disabled():
output_manager, _sound_driver, speech_driver = build_output_manager()
speech_history_manager = Mock(add_text=Mock())
output_manager.env["runtime"]["SpeechHistoryManager"] = (
speech_history_manager
)
def _get_setting_as_bool(section, setting):
if (section, setting) == ("speech", "enabled"):
return False
return True
output_manager.env["runtime"][
"SettingsManager"
].get_setting_as_bool.side_effect = _get_setting_as_bool
output_manager.present_text("hello history", interrupt=False)
speech_history_manager.add_text.assert_not_called()
speech_driver.speak.assert_not_called()
@pytest.mark.unit
def test_key_interrupt_command_uses_nonblocking_interrupt():
module = load_key_interrupt_module()
settings_manager = Mock()
settings_manager.get_setting_as_bool.return_value = True
settings_manager.get_setting.return_value = ""
output_manager = Mock()
env = {
"input": {
"curr_input": ["KEY_A"],
"prev_input": [],
},
"runtime": {
"InputManager": Mock(no_key_pressed=Mock(return_value=False)),
"OutputManager": output_manager,
"ScreenManager": Mock(is_screen_change=Mock(return_value=False)),
"SettingsManager": settings_manager,
},
}
command = module.command()
command.initialize(env)
command.run()
output_manager.interrupt_output_async.assert_called_once_with()
output_manager.interrupt_output.assert_not_called()
@pytest.mark.unit
def test_key_interrupt_command_ignores_fenrir_shortcuts():
module = load_key_interrupt_module()
settings_manager = Mock()
settings_manager.get_setting_as_bool.return_value = True
settings_manager.get_setting.return_value = ""
input_manager = Mock(
no_key_pressed=Mock(return_value=False),
get_curr_shortcut=Mock(return_value=[1, ["KEY_KP9"]]),
get_command_for_shortcut=Mock(return_value="REVIEW_NEXT_LINE"),
)
output_manager = Mock()
env = {
"input": {
"curr_input": ["KEY_KP9"],
"prev_input": [],
},
"runtime": {
"InputManager": input_manager,
"OutputManager": output_manager,
"ScreenManager": Mock(is_screen_change=Mock(return_value=False)),
"SettingsManager": settings_manager,
},
}
command = module.command()
command.initialize(env)
command.run()
output_manager.interrupt_output_async.assert_not_called()