Files
fenrir/tests/native_hardware_speech.py

125 lines
7.2 KiB
Python

#!/usr/bin/env python3
"""Private X11 hardware-speech integration using PTYs and fake libusb only."""
import fcntl
import json
import os
from pathlib import Path
import select
import subprocess
import sys
import tempfile
import tomllib
from native_terminal import KeyboardFixture, wait_for
def main():
if not os.environ.get("DISPLAY") or os.environ.get("FENRIR_PRIVATE_X11") != "1":
raise SystemExit("Use xvfb-run -a env FENRIR_PRIVATE_X11=1 python3 tests/native_hardware_speech.py")
repository = Path(__file__).resolve().parents[1]
fixture = str(repository / "tests/native_terminal.py")
binary = os.environ.get("FENRIR_TEST_BINARY", str(repository / "fenrir"))
with tempfile.TemporaryDirectory(prefix="fenrir-hardware-", dir="/tmp") as directory:
suiteRoot = Path(directory)
libraryRoot = suiteRoot / "usb"
libraryRoot.mkdir()
subprocess.run(["cc", "-shared", "-fPIC", "-o", str(libraryRoot / "libusb-1.0.so.0"),
str(repository / "internal/speech/testdata/usb_fixture.c")], check=True)
keyboard = KeyboardFixture()
try:
for driver in ("dectalk", "dectalk-usb", "litetalk", "doubletalk", "tripletalk"):
for layout in ("desktop", "laptop"):
root = suiteRoot / (driver + "-" + layout)
root.mkdir()
(root / "home").mkdir()
(root / "keyboard").mkdir()
layoutName = "laptop.toml" if layout == "laptop" else "custom.toml"
(root / "keyboard" / layoutName).write_bytes((repository / "config/keyboard" / (layout + ".toml")).read_bytes())
if layout == "laptop":
(root / "mode").write_text("laptop")
shell = root / "shell"
shell.write_text(f'#!{sys.executable}\nimport runpy, sys\nsys.argv = [{fixture!r}, "--fixture-shell"]\nrunpy.run_path({fixture!r}, run_name="__main__")\n')
shell.chmod(0o700)
master, slave = os.openpty()
fcntl.fcntl(master, fcntl.F_SETFL, os.O_NONBLOCK)
settings = root / "settings.toml"
settings.write_text('version=3\n[presentation.sound]\nenabled=false\n'
'[presentation.speech]\n' + f'driver="{driver}"\nhardware_device="{os.ttyname(slave)}"\nrate=0\n'
'[reading]\npunctuation_level="none"\nspellcheck=false\n'
f'[remote]\nsocket_file="{root / "remote.sock"}"\n')
environment = dict(os.environ, FENRIR_TEST_BINARY=binary, FENRIR_FIXTURE_DIR=str(root),
HOME=str(root / "home"), TMPDIR=str(root), LOGNAME="Username", USER="Username",
SPEECHD_ADDRESS="unix_socket:" + str(root / "unused-speech.sock"),
LD_LIBRARY_PATH=str(libraryRoot), USB_FIXTURE_MODE="ok", USB_FIXTURE_LOG=str(root / "usb.log"))
terminal = subprocess.Popen(["xterm", "-geometry", "80x25", "-e", sys.executable, fixture, "--outer", str(root)], env=environment)
captured = bytearray()
def output():
if driver == "dectalk-usb":
log = root / "usb.log"
return b"".join(bytes.fromhex(line.partition(":")[2]) for line in log.read_text().splitlines() if line.startswith("write-02:")) if log.exists() else b""
while select.select([master], [], [], 0)[0]:
captured.extend(os.read(master, 65536))
return bytes(captured)
def expect(text, start=0):
wait_for(lambda: text in output()[start:], f"{driver}/{layout}: {text!r}; errors=" + ((root / "reader-errors").read_text() if (root / "reader-errors").exists() else ""))
try:
wait_for(lambda: (root / "started").exists(), "reader startup")
state = json.loads((root / "started").read_text())
subprocess.run(["xdotool", "windowfocus", "--sync", state["window"]], check=True)
expect(b"prompt")
start = len(output())
keyboard.key("Insert+Right", "Insert+Right", "Insert+Right")
expect(b"speech rate 0", start) # Hardware wraps after volume.
start = len(output())
keyboard.key("Insert+Up", "Insert+ctrl+s")
expect(b"Settings saved", start)
wait_for(lambda: tomllib.loads(settings.read_text())["presentation"]["speech"]["rate"] == 10, "hardware quick save")
start = len(output())
keyboard.key("Insert+f")
expect(b"Fenrir Menu", start)
terminator = b"\x0b" if driver.startswith("dectalk") else b"\r"
for _ in range(24):
start = len(output())
keyboard.key("Tab")
wait_for(lambda: terminator in output()[start:], "hardware form control")
if b"Test synthesizer button" in output()[start:]:
break
else:
raise AssertionError("Hardware test button not reachable")
start = len(output())
keyboard.key("Return")
expect(b"Voice test: The quick brown fox jumps over the lazy dog.", start)
start = len(output())
keyboard.key("Escape")
expect(b"Fenrir Menu closed", start)
start = len(output())
keyboard.key("Insert+i" if layout == "laptop" else "KP_Up")
expect(b"prompt", start)
assert not (root / "input").exists() or not (root / "input").read_bytes(), "reader chords leaked to the child"
keyboard.key("Insert+q")
wait_for(lambda: (root / "finished").exists(), "reader shutdown")
result = json.loads((root / "finished").read_text())
assert result == {"status": 0, "restored": True, "flagsRestored": True}, result
if driver == "dectalk-usb":
assert (root / "usb.log").read_text().endswith("release\nclose\nexit\n")
print(f"PASS: {driver}/{layout}: startup, quick controls, save, owned-connection preview, review, input isolation and cleanup", flush=True)
finally:
terminal.terminate()
try:
terminal.wait(timeout=3)
except subprocess.TimeoutExpired:
terminal.kill()
terminal.wait()
os.close(master)
os.close(slave)
finally:
keyboard.close()
if __name__ == "__main__":
main()