Add isolated native Rime service and MiSans candidate UI

This commit is contained in:
aria committed 2026-10-28 00:58:38 +09:00
1 parent f21167e1a2
commit 8e9a5838ab
28 files changed
+2381 -17

No files matched your search

+185
View File
@@ -0,0 +1,185 @@
"""Opt-in real Wayland/GTK/Rime integration checks against the build-tree preview.
FERESE_TEST_RIME=1 python3 scripts/tests/test_rime_native_isolated.py
Only disposable fixture entries are modified. The probe refuses host displays.
"""
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")
preview = importlib.util.module_from_spec(spec)
spec.loader.exec_module(preview)
@unittest.skipUnless(os.environ.get("FERESE_TEST_RIME") == "1", "opt-in native Rime validation")
class NativeRimeTest(unittest.TestCase):
def test_native_pipeline_and_process_boundary(self):
preview.start()
preview.start_ime()
results = []
def state():
return json.loads((preview.DATA / "entry-state.json").read_text())
def expect(text):
preview.wait_for(lambda: state()["normal"] == text,
f"Expected {text!r}; latest state {state()}", timeout=4)
def probe(*args):
preview.probe(list(args))
time.sleep(.12)
def clear():
preview.focus_entry()
probe("--key", "Escape")
preview.action("clear")
expect("")
def screenshot(name):
c = preview.context()
p = subprocess.run([str(ROOT / "target/debug/feresectl"), "--socket", c["socket"], "screenshot"], capture_output=True, check=True)
(preview.DATA / name).write_bytes(p.stdout)
clear()
probe("--keys", "nihao")
expect("") # Preedit is displayed, not prematurely committed.
screenshot("candidate-normal.png")
self.assertLessEqual({window["app_id"] for window in preview.windows()},
{"dev.ferese.RimeValidation", "io.github.lgse.Strata"})
probe("--keys", " ")
expect("你好")
results.append("native GTK preedit, candidate popup and space commit")
clear()
probe("--keys", "nihao1")
expect("你好")
results.append("numeric candidate selection")
clear()
probe("--keys", "ni")
pid = preview.prop(preview.IME, "MainPID")
def composition():
lines = preview.run(["journalctl", "--user", f"_PID={pid}", "--no-pager", "-o", "cat"]).splitlines()
return [json.loads(line) for line in lines if line.startswith('{') and '"event":"composition"' in line][-1]
self.assertEqual(composition()["page"], 0)
probe("--keys", "=")
self.assertEqual(composition()["page"], 1)
screenshot("candidate-horizontal-page2.png")
probe("--keys", "-")
self.assertEqual(composition()["page"], 0)
probe("--chord", "Shift_L+equal")
self.assertEqual(composition()["page"], 1)
self.assertFalse(composition()["ascii"])
probe("--keys", "-1")
expect("你")
results.append("horizontal candidates page with number-row minus, equal and Shift+equal")
clear()
probe("--keys", "nihao", "--key", "BackSpace", "--keys", "o ")
expect("你好")
results.append("composition backspace and repair")
clear()
probe("--keys", "nihao", "--key", "Escape")
expect("")
probe("--key", "Shift_L", "--keys", "abc123")
expect("abc123")
time.sleep(.8)
expect("abc123") # Rejected keys must receive paired releases.
probe("--keys", "-=")
expect("abc123-=")
probe("--chord", "Control_L+a", "--key", "BackSpace")
expect("")
probe("--key", "Shift_L", "--keys", "nihao ")
expect("你好")
results.append("escape, Shift mode switching, ASCII and Ctrl+A release pairing")
clear()
preview.action("password")
time.sleep(.2)
pid = preview.prop(preview.IME, "MainPID")
def compositions():
return preview.run(["journalctl", "--user", f"_PID={pid}", "--no-pager", "-o", "cat"]).count('"event":"composition"')
before = compositions()
probe("--keys", "nihao")
preview.wait_for(lambda: state()["password_length"] == 5, "Sensitive field did not receive literal keys")
self.assertEqual(compositions(), before)
screenshot("sensitive-no-candidates.png")
results.append("sensitive field bypasses Rime and has no candidates")
clear()
c = preview.context()
control = [str(ROOT / "target/debug/feresectl"), "--socket", c["socket"]]
subprocess.run(control + ["toggle-maximized"], check=True)
time.sleep(.4)
preview.action("bottom")
time.sleep(.2)
probe("--keys", "nihao")
screenshot("candidate-bottom-flipped.png")
probe("--keys", " ")
expect("你好")
preview.action("top")
subprocess.run(control + ["toggle-maximized"], check=True)
time.sleep(.3)
results.append("bottom-edge native candidate composition and commit; screenshot for placement inspection")
clear()
probe("--keys", "nihao")
theme = json.loads(preview.run(control + ["--json", "theme", "get"]))
original_mode = theme["mode"]
subprocess.run(control + ["theme", "mode", "light"], check=True, capture_output=True)
preview.wait_for(lambda: json.loads(preview.run(control + ["--json", "theme", "get"]))["mode"] == "light", "Light theme did not finish resolving")
time.sleep(.35)
screenshot("candidate-theme-light.png")
subprocess.run(control + ["theme", "mode", original_mode], check=True, capture_output=True)
preview.wait_for(lambda: json.loads(preview.run(control + ["--json", "theme", "get"]))["mode"] == original_mode, "Original theme did not finish resolving")
time.sleep(.35)
results.append("candidate UI follows Ferese light/dark theme through its existing IPC")
clear()
probe("--keys", "nihao")
compositor_pid = preview.prop(preview.MAIN, "MainPID")
subprocess.run(["systemctl", "--user", "kill", "--signal=KILL", preview.IME], check=True)
preview.wait_for(lambda: preview.prop(preview.IME, "ActiveState") != "active", "IME did not terminate")
self.assertEqual(preview.prop(preview.MAIN, "ActiveState"), "active")
self.assertEqual(preview.prop(preview.MAIN, "MainPID"), compositor_pid)
probe("--keys", "abc")
expect("abc")
screenshot("ime-crash-desktop-survives.png")
preview.start_ime()
time.sleep(.2)
probe("--keys", "nihao ")
expect("abc你好")
results.append("IME SIGKILL leaves desktop alive; ASCII works; restarted IME commits Chinese")
# The validation driver must refuse the real host before creating a keyboard.
host = Path(os.environ.get("WAYLAND_DISPLAY", "wayland-0"))
if not host.is_absolute():
host = Path(os.environ["XDG_RUNTIME_DIR"]) / host
p = subprocess.run([str(ROOT / "target/debug/ferese-rime-probe"), "--keys", "x"],
env={**os.environ, "WAYLAND_DISPLAY": str(host), "FERESE_TEST_INPUT": "1"},
text=True, capture_output=True)
self.assertNotEqual(p.returncode, 0)
self.assertIn("not an isolated Ferese", p.stderr)
results.append("test driver refuses host desktop")
pid = preview.prop(preview.IME, "MainPID")
maps = (Path("/proc") / pid / "maps").read_text()
self.assertIn("librime.so", maps)
self.assertNotIn("libibus", maps)
self.assertNotIn("libfcitx", maps)
results.append("independent process links librime, with no IBus or Fcitx library")
clear()
probe("--keys", "nihao")
(preview.DATA / "validation.json").write_text(json.dumps({"status": "passed", "checks": results}, ensure_ascii=False, indent=2) + "\n")
if __name__ == "__main__":
unittest.main()