103 lines
5.3 KiB
Python
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()
|