After commit c018f5fd29 Fenrir is being switched over to go. If you do not want to be part of the testing process, please switch to master.

This commit is contained in:
Storm Dragon committed 2026-10-27 20:30:55 -04:00
1 parent c018f5fd29
commit 0e50de4c76
1047 files changed
+58364 -63747

No files matched your search

+124
View File
@@ -0,0 +1,124 @@
#!/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()