Files
feresaria/scripts/tests/test_rime_resilience_isolated.py
T

103 lines
5.3 KiB
Python

"""Opt-in multi-window preferences and real service fault recovery.
FERESE_TEST_RIME=1 python3 scripts/tests/test_rime_resilience_isolated.py
Uses disposable native GTK windows only; never writes user chat/profile data.
"""
import importlib.util
import json
import os
from pathlib import Path
import subprocess
import time
import unittest
ROOT = Path(__file__).resolve().parents[2]
spec = importlib.util.spec_from_file_location("preview", ROOT / "scripts/rime-preview.py")
p = importlib.util.module_from_spec(spec)
spec.loader.exec_module(p)
ALT = "ferese-rime-second-20261028.service"
@unittest.skipUnless(os.environ.get("FERESE_TEST_RIME") == "1", "opt-in Rime resilience validation")
class Resilience(unittest.TestCase):
def test_preferences_and_recovery(self):
p.start()
env = {**os.environ, **p.child_environment()}
results = []
def ctl(*args):
return json.loads(p.run([str(ROOT / "target/debug/ferese-rime"), *args], env=env))["state"]
def keys(*args):
p.probe(list(args)); time.sleep(.15)
def text(name="entry-state.json"):
return json.loads((p.DATA / name).read_text())["normal"]
def expect(value, name="entry-state.json"):
p.wait_for(lambda: text(name) == value, f"Expected {value!r} in {name}", timeout=5)
def ready_after(pid, timeout=20):
def ready():
try:
state = ctl("--status")
return state["pid"] != int(pid) and state["active"] and state["context"]
except (RuntimeError, ValueError):
return False
p.wait_for(ready, "Input service did not automatically recover", timeout=timeout)
p.focus_entry(); p.action("clear")
keys("--key", "Escape")
ctl("--select-schema", "luna_pinyin_simp"); ctl("--mode", "rime")
keys("--keys", "nihao "); expect("你好")
try:
p.start_unit(ALT, ["/usr/bin/gjs", str(ROOT / "scripts/tests/fixtures/rime-entry.js"),
str(p.DATA / "second-state.json"), "dev.ferese.RimeSecond"], p.child_environment(), "512M")
p.wait_for(lambda: any(w["focused"] and w["app_id"] == "dev.ferese.RimeSecond" for w in p.windows()), "Second window did not focus")
p.wait_for(lambda: ctl("--status")["context"]["app_id"] == "dev.ferese.RimeSecond", "Second input context missing")
ctl("--mode", "ascii"); keys("--keys", "abc"); expect("abc", "second-state.json")
p.focus_entry(); p.action("normal"); time.sleep(.25)
self.assertFalse(ctl("--status")["profile"]["ascii"])
p.action("clear"); keys("--keys", "nihao "); expect("你好")
results.append("two windows retain independent ASCII/Rime modes on focus changes")
ctl("--select-schema", "luna_pinyin")
p.action("clear"); keys("--keys", "hanzi "); expect("漢字")
# Reopening the same application must use its persisted preference.
ctl("--mode", "ascii")
subprocess.run(["systemctl", "--user", "stop", p.APP], check=True)
p.wait_for(lambda: not any(w["app_id"] == "dev.ferese.RimeValidation" for w in p.windows()), "First window did not close")
p.start_unit(p.APP, ["/usr/bin/gjs", str(ROOT / "scripts/tests/fixtures/rime-entry.js"),
str(p.DATA / "entry-state.json")], p.child_environment(), "512M")
p.focus_entry()
p.wait_for(lambda: (ctl("--status")["context"] or {}).get("app_id") == "dev.ferese.RimeValidation", "Reopened context missing")
self.assertEqual(ctl("--status")["profile"], {"schema":"luna_pinyin", "ascii":True})
keys("--keys", "abc"); expect("abc")
results.append("closed/reopened application restores mode and Rime schema")
ctl("--mode", "rime"); p.action("clear"); keys("--keys", "hanzi")
desktop = p.prop(p.MAIN, "MainPID"); old = p.prop(p.IME, "MainPID")
started = time.monotonic()
subprocess.run(["systemctl", "--user", "kill", "--signal=KILL", p.IME], check=True)
ready_after(old)
self.assertEqual(p.prop(p.MAIN, "MainPID"), desktop)
self.assertEqual(ctl("--status")["profile"], {"schema":"luna_pinyin", "ascii":False})
keys("--keys", "hanzi "); expect("漢字")
results.append(f"SIGKILL recovery restores current-window schema in {time.monotonic()-started:.2f}s without stale composition")
p.action("clear"); old = p.prop(p.IME, "MainPID"); started=time.monotonic()
subprocess.run(["systemctl", "--user", "kill", "--signal=STOP", p.IME], check=True)
ready_after(old, timeout=20)
self.assertEqual(p.prop(p.MAIN, "MainPID"), desktop)
keys("--keys", "hanzi "); expect("漢字")
results.append(f"stalled input loop is killed by watchdog and restored in {time.monotonic()-started:.2f}s")
finally:
subprocess.run(["systemctl", "--user", "stop", ALT], capture_output=True)
p.focus_entry()
ctl("--select-schema", "luna_pinyin_simp"); ctl("--mode", "rime")
(p.DATA / "resilience-validation.json").write_text(json.dumps({"status":"passed","checks":results}, ensure_ascii=False,indent=2)+"\n")
print(json.dumps(results, ensure_ascii=False))
if __name__ == "__main__":
unittest.main()