This is an automated email from the git hooks/post-receive script.
git pushed a commit to branch wl/browser-all
in repository enlightenment.
View the commit online.
commit 1f5682fb4a146f86182b627b968e1193cc21e2a8
Author: Cedric BAIL <[email protected]>
AuthorDate: Wed Aug 19 14:12:52 2026 -0600
tests - the input method loop, against a real fcitx5
Every other test of this code plays both ends. This one runs fcitx5 -
the only input method implementing zwp_input_method_v2 and
zwp_virtual_keyboard_v1 - and plays only the application, so the input
method's half is software nobody here wrote.
It answers the two questions left open when the protocols landed, and
both answers are yes.
**fcitx5 tolerates E's narrowed keyboard grab.** E gives the grab keys
only while a text input is enabled, where the protocol describes a grab
exclusive over the whole seat; the narrowing exists so that an input
method which stops answering costs text fields rather than the whole
keyboard. It was the most likely thing for a real IME to object to, and
it does not.
**The return path works.** fcitx5's default engine passes Latin keys
straight through: it takes the key from the grab, decides it is not
composing, and hands it back with zwp_virtual_keyboard_v1. So an
ordinary "a" makes the whole round trip - compositor to input method to
compositor to application - and arrives as wl_keyboard.key with evdev 30
intact. That is the failure this exists to catch: a grab whose way back
is broken swallows every keystroke, and looks from inside the compositor
exactly like a grab that works.
The default engine is the right one to test with. A composing engine
exercises the loop only on the keys it decides to eat; the pass-through
engine exercises it on every keystroke.
How the test knows fcitx5 has arrived is worth noting: it tries to
become the input method itself and waits to be refused. E sends
`unavailable` to the newcomer when the seat is taken, so an unavailable
means fcitx5 got there first - which incidentally checks the
one-input-method-per-seat rule against a real client rather than a
written-to-match one.
5 runs, 5 passes. Breaking e_comp_wl_key_inject makes it fail on the
round trip, checked against a build with it broken.
Also recorded, because both look alarming in fcitx5's log and neither is
a fault: the SIGTERM backtrace it prints when the harness cleans up
(usually inside xkb_keymap_new_from_buffer, which is what it spends
startup doing), and the portal Settings D-Bus errors from the private
bus.
Test-only: no compositor code touched, so wlcs cannot move and was not
re-run. In-tree 35 -> 36, with fcitx5 in its own `im` suite so a machine
without it skips rather than fails.
---
src/tests/wayland/fcitx5-run.sh | 63 ++++++++++
src/tests/wayland/meson.build | 25 ++++
src/tests/wayland/test_fcitx5.c | 269 ++++++++++++++++++++++++++++++++++++++++
3 files changed, 357 insertions(+)
diff --git a/src/tests/wayland/fcitx5-run.sh b/src/tests/wayland/fcitx5-run.sh
new file mode 100755
index 000000000..ef7b93903
--- /dev/null
+++ b/src/tests/wayland/fcitx5-run.sh
@@ -0,0 +1,63 @@
+#!/bin/sh
+# Launch fcitx5 as E_TEST_APP inside run-nested.sh's session.
+#
+# fcitx5-run.sh
+#
+# Exits 77 when fcitx5 is not installed, which meson reports as a skip. A
+# machine without an input method should not have a red suite.
+#
+# fcitx5 is the only input method that implements the protocols this tests -
+# zwp_input_method_v2 and zwp_virtual_keyboard_v1. ibus does not: it speaks
+# zwp_input_method_v1 only, which is the pair E's older wl_text_input module
+# implements and which no browser speaks. Checked by grepping every binary
+# either project ships, because "it has Wayland support" is not the same
+# question as "it has this Wayland support".
+#
+# What the flags are for:
+#
+# * a private D-Bus. fcitx5 refuses to start twice on one bus, and the
+# developer running this almost certainly has one already. Inheriting
+# theirs would either fail immediately or - worse - reconfigure the input
+# method they are using to read this.
+#
+# * --disable=xcb,xim. There is no X server in a nested buffer-backend
+# session. fcitx5 survives their absence anyway, but it spends startup
+# retrying, and startup is what the test is waiting on.
+#
+# * -d is deliberately NOT used. Daemonising detaches fcitx5 from the process
+# group run-nested.sh kills, and it would outlive the session it was
+# started for - which on this machine means an input method quietly holding
+# a seat in every later run.
+#
+# Two things in fcitx5's log look alarming and are not:
+#
+# * "Get Signal No.: 15" followed by a backtrace. That is SIGTERM - the
+# harness cleaning up at the end of the run - and fcitx5's signal handler
+# dumps a stack wherever it happened to be. It is usually somewhere inside
+# xkb_keymap_new_from_buffer, because parsing the keymap the compositor
+# sent it is what it spends its startup doing. Not a crash, and not a
+# protocol error.
+#
+# * "No such interface org.freedesktop.portal.Settings". There is no desktop
+# portal on the private bus, by design; fcitx5 asks and carries on.
+
+set -eu
+
+if ! command -v fcitx5 >/dev/null 2>&1; then
+ echo "fcitx5-run.sh: fcitx5 is not installed" >&2
+ exit 77
+fi
+
+# A configuration of its own, under the private HOME run-nested.sh made. Left
+# empty on purpose: fcitx5 writes its defaults, which is the "keyboard" engine
+# passing keys straight through. That is the configuration worth testing - it
+# exercises the grab and the virtual keyboard on every keystroke, where a
+# composing engine only exercises them on the keys it decides to eat.
+export XDG_CONFIG_HOME="${HOME}/.config"
+export XDG_DATA_HOME="${HOME}/.local/share"
+mkdir -p "$XDG_CONFIG_HOME" "$XDG_DATA_HOME"
+
+if command -v dbus-run-session >/dev/null 2>&1; then
+ exec dbus-run-session -- fcitx5 --disable=xcb,xim
+fi
+exec fcitx5 --disable=xcb,xim
diff --git a/src/tests/wayland/meson.build b/src/tests/wayland/meson.build
index 313760531..6bb7f9ca2 100644
--- a/src/tests/wayland/meson.build
+++ b/src/tests/wayland/meson.build
@@ -226,6 +226,31 @@ foreach b: ['firefox', 'chromium', 'brave']
)
endforeach
+# The input method tier. Its own suite for the same reasons the browser tier
+# has one: it depends on software this tree does not ship, and an input method
+# that has changed its mind about something must never hold up the protocol
+# tests. A missing fcitx5 is a skip (exit 77), not a failure.
+#
+# fcitx5 rather than ibus, and that is not a preference. ibus speaks
+# zwp_input_method_v1 only - no binary it ships mentions the v2 manager or the
+# virtual keyboard - so it cannot exercise any of this. fcitx5 is the only
+# implementation of the pair E now speaks.
+test('input-method-fcitx5',
+ find_program('run-nested.sh'),
+ args : [executable('test_wl_fcitx5',
+ ['test_fcitx5.c', tk_src, test_proto_src],
+ dependencies: [dependency('wayland-client')],
+ )],
+ env : [
+ 'E_TEST_BIN=' + e_test_bin,
+ 'E_TEST_MODULE_SO=' + wl_test_module_so,
+ 'E_TEST_MODULE_ARCH=' + module_arch,
+ 'E_TEST_APP=' + meson.current_source_dir() / 'fcitx5-run.sh',
+ ],
+ suite : 'im',
+ timeout: 300,
+)
+
# The X11 second opinion.
#
# e_client.c is shared between the X11 compositor and the Wayland one, and the
diff --git a/src/tests/wayland/test_fcitx5.c b/src/tests/wayland/test_fcitx5.c
new file mode 100644
index 000000000..abe0b234f
--- /dev/null
+++ b/src/tests/wayland/test_fcitx5.c
@@ -0,0 +1,269 @@
+/* E, a real input method, and a text field: the whole loop.
+ *
+ * Every other test of this code plays both ends itself. This one runs fcitx5 -
+ * the only input method that implements zwp_input_method_v2 and
+ * zwp_virtual_keyboard_v1 - and plays only the application, so the input
+ * method's half is software nobody here wrote.
+ *
+ * What that buys is the two things a test client cannot get wrong by
+ * construction, because it was written against the same reading of the
+ * protocol as the compositor:
+ *
+ * * whether a real input method tolerates E's **narrowed keyboard grab**.
+ * The protocol describes a grab exclusive over the whole seat; E gives it
+ * keys only while a text input is enabled, so that an input method which
+ * stops answering costs text fields rather than the whole keyboard. If
+ * fcitx5 could not live with that, this is where it would show.
+ *
+ * * whether the **return path** works against a real client. fcitx5's
+ * default engine passes Latin keys straight through: it takes the key from
+ * the grab, decides it is not composing, and sends it back with
+ * zwp_virtual_keyboard_v1. So an ordinary "a" makes the entire round trip
+ * - compositor to input method to compositor to application - and arrives
+ * as a wl_keyboard.key the application can see. If E's grab worked and its
+ * virtual keyboard did not, the key would simply vanish, which is the
+ * failure this is built to catch.
+ *
+ * The default engine is the right one to test with for exactly that reason. A
+ * composing engine exercises the loop only on the keys it decides to eat; the
+ * pass-through engine exercises it on every keystroke.
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "e_wl_testkit.h"
+#include "text-input-unstable-v3-client-protocol.h"
+#include "input-method-unstable-v2-client-protocol.h"
+
+#define PROG "test-fcitx5"
+#define EVDEV_KEY_A 30
+
+/* fcitx5 has a D-Bus session, a config directory and several addons to bring
+ * up before it binds anything. Generous, and bounded. */
+#define IM_APPEAR_MS 40000
+#define KEY_MS 10000
+
+typedef struct
+{
+ struct wl_surface *entered;
+ uint32_t last_key, last_state;
+ int keys;
+} App;
+
+static void
+_kmap(void *d, struct wl_keyboard *k, uint32_t f, int32_t fd, uint32_t s)
+{ (void)d; (void)k; (void)f; (void)s; if (fd >= 0) close(fd); }
+static void
+_enter(void *data, struct wl_keyboard *k, uint32_t se, struct wl_surface *s, struct wl_array *a)
+{ App *p = data; (void)k; (void)se; (void)a; p->entered = s; }
+static void
+_leave(void *data, struct wl_keyboard *k, uint32_t se, struct wl_surface *s)
+{ App *p = data; (void)k; (void)se; if (p->entered == s) p->entered = NULL; }
+static void
+_key(void *data, struct wl_keyboard *k, uint32_t se, uint32_t t, uint32_t key, uint32_t st)
+{
+ App *p = data;
+
+ (void)k; (void)se; (void)t;
+ p->last_key = key;
+ p->last_state = st;
+ p->keys++;
+}
+static void
+_mods(void *d, struct wl_keyboard *k, uint32_t se, uint32_t a, uint32_t b, uint32_t c, uint32_t g)
+{ (void)d; (void)k; (void)se; (void)a; (void)b; (void)c; (void)g; }
+static void
+_rep(void *d, struct wl_keyboard *k, int32_t r, int32_t dl)
+{ (void)d; (void)k; (void)r; (void)dl; }
+
+static const struct wl_keyboard_listener _kbd_listener =
+{ _kmap, _enter, _leave, _key, _mods, _rep };
+
+/* The text input side. Nothing is asserted through it here - the pass-through
+ * engine commits no text - but a listener has to exist for the object to be
+ * legal, and preedit arriving would be worth seeing in the log. */
+static int _preedits, _commits;
+
+static void
+_ti_enter(void *d, struct zwp_text_input_v3 *t, struct wl_surface *s)
+{ (void)d; (void)t; (void)s; }
+static void
+_ti_leave(void *d, struct zwp_text_input_v3 *t, struct wl_surface *s)
+{ (void)d; (void)t; (void)s; }
+static void
+_ti_preedit(void *d, struct zwp_text_input_v3 *t, const char *x, int32_t a, int32_t b)
+{ (void)d; (void)t; (void)x; (void)a; (void)b; _preedits++; }
+static void
+_ti_commit(void *d, struct zwp_text_input_v3 *t, const char *x)
+{ (void)d; (void)t; (void)x; _commits++; }
+static void
+_ti_delete(void *d, struct zwp_text_input_v3 *t, uint32_t a, uint32_t b)
+{ (void)d; (void)t; (void)a; (void)b; }
+static void
+_ti_done(void *d, struct zwp_text_input_v3 *t, uint32_t s)
+{ (void)d; (void)t; (void)s; }
+
+static const struct zwp_text_input_v3_listener _ti_listener =
+{ _ti_enter, _ti_leave, _ti_preedit, _ti_commit, _ti_delete, _ti_done };
+
+/* Has fcitx5 taken the seat's input method yet?
+ *
+ * Asked by trying to become the input method and seeing whether the compositor
+ * refuses. E sends `unavailable` to the *newcomer* when one is already held,
+ * so an unavailable here means fcitx5 got there first - which is both the
+ * signal this test is waiting for and, incidentally, a check that the
+ * one-input-method-per-seat rule holds against a real client.
+ *
+ * The object is destroyed either way. If fcitx5 has not connected yet this
+ * test would otherwise become the input method itself and fcitx5 would get the
+ * unavailable, which is the opposite of what is wanted. */
+static int _unavailable;
+
+static void
+_im_unavailable(void *d, struct zwp_input_method_v2 *im)
+{ (void)d; (void)im; _unavailable = 1; }
+static void
+_im_nop0(void *d, struct zwp_input_method_v2 *im)
+{ (void)d; (void)im; }
+static void
+_im_nop_s(void *d, struct zwp_input_method_v2 *im, const char *t, uint32_t c, uint32_t a)
+{ (void)d; (void)im; (void)t; (void)c; (void)a; }
+static void
+_im_nop_u(void *d, struct zwp_input_method_v2 *im, uint32_t c)
+{ (void)d; (void)im; (void)c; }
+static void
+_im_nop_uu(void *d, struct zwp_input_method_v2 *im, uint32_t a, uint32_t b)
+{ (void)d; (void)im; (void)a; (void)b; }
+
+static const struct zwp_input_method_v2_listener _probe_listener =
+{ _im_nop0, _im_nop0, _im_nop_s, _im_nop_u, _im_nop_uu, _im_nop0,
+ _im_unavailable };
+
+static int
+_input_method_taken(Tk *tk, struct zwp_input_method_manager_v2 *mgr,
+ struct wl_seat *seat)
+{
+ struct zwp_input_method_v2 *probe;
+
+ _unavailable = 0;
+ probe = zwp_input_method_manager_v2_get_input_method(mgr, seat);
+ zwp_input_method_v2_add_listener(probe, &_probe_listener, NULL);
+ tk_sync(tk);
+ tk_sync(tk);
+ zwp_input_method_v2_destroy(probe);
+ tk_sync(tk);
+ return _unavailable;
+}
+
+int
+main(void)
+{
+ Tk *tk;
+ Tk_Toplevel *top;
+ struct wl_seat *seat;
+ struct wl_keyboard *kbd;
+ struct zwp_text_input_manager_v3 *ti_mgr;
+ struct zwp_text_input_v3 *ti;
+ struct zwp_input_method_manager_v2 *im_mgr;
+ App app = { 0 };
+ uint32_t v;
+ int i, keys_before;
+
+ tk = tk_connect(PROG);
+
+ v = tk_global_version(tk, "wl_seat");
+ if (!v) tk_fail(tk, "no wl_seat");
+ seat = tk_bind(tk, &wl_seat_interface, v);
+ kbd = wl_seat_get_keyboard(seat);
+ if (!kbd) tk_fail(tk, "seat has no keyboard");
+ wl_keyboard_add_listener(kbd, &_kbd_listener, &app);
+
+ if (tk_global_version(tk, "zwp_text_input_manager_v3") < 1)
+ tk_fail(tk, "no zwp_text_input_manager_v3");
+ if (tk_global_version(tk, "zwp_input_method_manager_v2") < 1)
+ tk_fail(tk, "no zwp_input_method_manager_v2");
+ if (tk_global_version(tk, "zwp_virtual_keyboard_manager_v1") < 1)
+ tk_fail(tk, "no zwp_virtual_keyboard_manager_v1 - fcitx5 needs it to hand "
+ "back the keys it does not consume, and refuses to use the "
+ "wayland input method without it");
+ ti_mgr = tk_bind(tk, &zwp_text_input_manager_v3_interface, 1);
+ im_mgr = tk_bind(tk, &zwp_input_method_manager_v2_interface, 1);
+ if ((!ti_mgr) || (!im_mgr)) tk_fail(tk, "managers would not bind");
+
+ top = tk_toplevel_new(tk, "fcitx5-app", "fcitx5 app", 300, 200);
+ tk_settle(tk);
+ tk_sync(tk);
+ if (app.entered != tk_toplevel_surface(top))
+ tk_fail(tk, "the window did not take keyboard focus");
+
+ /* ------------------------------------------------ wait for fcitx5 */
+
+ for (i = 0; i * 500 < IM_APPEAR_MS; i++)
+ {
+ if (_input_method_taken(tk, im_mgr, seat)) break;
+ usleep(500 * 1000);
+ }
+ if (!_input_method_taken(tk, im_mgr, seat))
+ tk_fail(tk, "fcitx5 never took the seat's input method within %d ms. It "
+ "is started as E_TEST_APP and its output is in the app log "
+ "printed below this; the usual causes are a missing D-Bus "
+ "session or the waylandim addon being disabled",
+ IM_APPEAR_MS);
+ printf(PROG ": fcitx5 holds the seat's input method\n");
+
+ /* --------------------------------- a key with no text input enabled */
+
+ /* E's grab is narrowed to an enabled text input, so this key must reach the
+ * window directly - it never goes near fcitx5. Establishing that first
+ * means the interesting case below is a comparison rather than a single
+ * observation. */
+ keys_before = app.keys;
+ tk_key(tk, "a", 1);
+ tk_key(tk, "a", 0);
+ tk_sync(tk);
+ if (app.keys != keys_before + 2)
+ tk_fail(tk, "with no text input enabled the window received %d keys "
+ "instead of 2 - E's grab is supposed to be inactive here and "
+ "something took them", app.keys - keys_before);
+ printf(PROG ": with no field enabled, keys go straight to the window\n");
+
+ /* ------------------------------- and now with one, through fcitx5 */
+
+ ti = zwp_text_input_manager_v3_get_text_input(ti_mgr, seat);
+ zwp_text_input_v3_add_listener(ti, &_ti_listener, NULL);
+ zwp_text_input_v3_enable(ti);
+ zwp_text_input_v3_commit(ti);
+ tk_sync(tk);
+ tk_sync(tk);
+
+ keys_before = app.keys;
+ app.last_key = 0;
+ tk_key(tk, "a", 1);
+ for (i = 0; (i * 200 < KEY_MS) && (app.keys == keys_before); i++)
+ tk_sync(tk);
+ tk_key(tk, "a", 0);
+ for (i = 0; (i * 200 < KEY_MS) && (app.keys < keys_before + 2); i++)
+ tk_sync(tk);
+
+ if (app.keys == keys_before)
+ tk_fail(tk, "with a text input enabled and fcitx5 holding the keyboard "
+ "grab, a key press never came back to the window. E handed "
+ "it to fcitx5 and nothing returned it - which is either the "
+ "virtual keyboard not delivering, or fcitx5 declining to use "
+ "it. This is the failure this test exists for: the grab "
+ "without the way back swallows every keystroke");
+
+ if (app.last_key != EVDEV_KEY_A)
+ tk_fail(tk, "the key came back as keycode %u, expected %d - it made the "
+ "round trip through fcitx5 and something rewrote it on the "
+ "way", app.last_key, EVDEV_KEY_A);
+
+ printf(PROG ": ok - \"a\" went to fcitx5 through the grab and came back to "
+ "the window as keycode %u (%d keys, %d preedit, %d commit_string)\n",
+ app.last_key, app.keys - keys_before, _preedits, _commits);
+
+ tk_disconnect(tk);
+ return 0;
+}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.