import os import select import time from unittest.mock import ANY from unittest.mock import Mock import pytest from fenrirscreenreader.speechDriver import dectalkDriver from fenrirscreenreader.speechDriver import doubletalkDriver from fenrirscreenreader.speechDriver import litetalkDriver from fenrirscreenreader.speechDriver import tripletalkDriver def build_environment(device): settings_manager = Mock() settings_manager.get_setting.side_effect = ( lambda section, setting: device if (section, setting) == ("speech", "hardware_device") else "" ) settings_manager.get_setting_as_int.side_effect = ( lambda section, setting: 9600 if (section, setting) == ("speech", "hardware_baud_rate") else 0 ) return { "runtime": { "SettingsManager": settings_manager, "DebugManager": Mock(), } } def read_available(fd, expected_length, timeout=1.0): deadline = time.monotonic() + timeout data = b"" while len(data) < expected_length and time.monotonic() < deadline: readable, _, _ = select.select([fd], [], [], 0.05) if readable: data += os.read(fd, 1024) return data @pytest.fixture def serial_pair(): master_fd, slave_fd = os.openpty() try: yield master_fd, os.ttyname(slave_fd) finally: os.close(master_fd) os.close(slave_fd) def initialized_driver(driver_class, serial_pair): master_fd, slave_name = serial_pair speech_driver = driver_class.driver() speech_driver.initialize(build_environment(slave_name)) assert speech_driver._is_initialized return speech_driver, master_fd def test_dectalk_driver_speaks_printable_text(serial_pair): speech_driver, master_fd = initialized_driver(dectalkDriver, serial_pair) try: speech_driver.speak("Hello\nworld ☃") assert read_available(master_fd, 13) == b"Hello world \x01" finally: speech_driver.shutdown() def test_dectalk_driver_writes_settings_and_cancel(serial_pair): speech_driver, master_fd = initialized_driver(dectalkDriver, serial_pair) try: speech_driver.set_rate(1.0) speech_driver.set_pitch(0.0) speech_driver.set_volume(0.5) speech_driver.cancel() assert read_available(master_fd, 33) == ( b"[:ra 650][:dv ap 50][:vo 50]\x18" ) finally: speech_driver.shutdown() def test_litetalk_driver_speaks_printable_text(serial_pair): speech_driver, master_fd = initialized_driver(litetalkDriver, serial_pair) try: speech_driver.speak("Ready") assert read_available(master_fd, 6) == b"Ready\r" finally: speech_driver.shutdown() def test_litetalk_driver_writes_settings_and_cancel(serial_pair): speech_driver, master_fd = initialized_driver(litetalkDriver, serial_pair) try: speech_driver.set_rate(1.0) speech_driver.set_pitch(0.0) speech_driver.set_volume(0.5) speech_driver.cancel() assert read_available(master_fd, 9) == b"\x019S\x010P\x014V\x18" finally: speech_driver.shutdown() def test_configured_device_supports_classic_serial(serial_pair): master_fd, slave_name = serial_pair speech_driver = litetalkDriver.driver() speech_driver.initialize(build_environment(slave_name)) try: assert speech_driver.device == slave_name speech_driver.speak("Serial") assert read_available(master_fd, 7) == b"Serial\r" finally: speech_driver.shutdown() def test_configured_device_strips_inline_comment(serial_pair): master_fd, slave_name = serial_pair device_setting = f"{slave_name} # built-in serial port" speech_driver = litetalkDriver.driver() speech_driver.initialize(build_environment(device_setting)) try: assert speech_driver.device == slave_name speech_driver.speak("Specific") assert read_available(master_fd, 9) == b"Specific\r" finally: speech_driver.shutdown() def test_auto_device_is_rejected(): speech_driver = litetalkDriver.driver() with pytest.raises(RuntimeError, match="hardware speech device"): speech_driver.initialize(build_environment("auto")) debug_manager = speech_driver.env["runtime"]["DebugManager"] debug_manager.write_debug_out.assert_called_with( "Hardware speech requires an explicit serial device", ANY, on_any_level=True, ) def test_hardware_driver_retries_partial_serial_writes(monkeypatch): speech_driver = litetalkDriver.driver() speech_driver.env = build_environment("/dev/ttyUSB0") speech_driver.serial_port = 12 written_chunks = [] def fake_write(port, data): assert port == 12 chunk = data[:2] written_chunks.append(chunk) return len(chunk) monkeypatch.setattr( "fenrirscreenreader.speechDriver.hardwareSerialDriver.os.write", fake_write, ) speech_driver._write_bytes(b"abcdef", "speech") assert written_chunks == [b"ab", b"cd", b"ef"] @pytest.mark.parametrize("driver_class", [doubletalkDriver, tripletalkDriver]) def test_litetalk_compatible_alias_drivers(driver_class, serial_pair): speech_driver, master_fd = initialized_driver(driver_class, serial_pair) try: speech_driver.speak("Alias") speech_driver.set_rate(1.0) assert read_available(master_fd, 10) == b"\x019SAlias\r" finally: speech_driver.shutdown() def test_hardware_driver_ignores_empty_and_non_string_text(serial_pair): speech_driver, master_fd = initialized_driver(dectalkDriver, serial_pair) try: speech_driver.speak("") speech_driver.speak(None) assert read_available(master_fd, 1, timeout=0.2) == b"" finally: speech_driver.shutdown()