This is an automated email from the git hooks/post-receive script.
git pushed a commit to reference refs/pull/146/head
in repository enlightenment.
View the commit online.
commit e26475481e2c6739c5717fe7969e8c01e59a4f86
Author: Cedric BAIL <[email protected]>
AuthorDate: Sat Aug 8 18:42:56 2026 -0600
wl_test - a test-only back door into the compositor
Enlightenment is a single large main(), not a library. Weston, Mutter, KWin
and Mir all build their compositor as a library with a thin main(), so
their test suites link it in, construct one per test case and reach
straight into its state. E cannot do that.
But E has modules, and a module is dlopen'd into E's own process with full
access to e_comp, e_comp_wl, the E_Client list and e_zone. That recovers
most of what being a library would have given us. This is that module,
plus src/protocol/wl-test.xml, the private interface an out-of-process
test uses to talk to it.
Built only with -Dtests=true and never installed in a release build.
Loaded via E_MODULE_FORCE_LOAD rather than listed in a profile, so the
wltest profile stays a description of the compositor under test rather
than of the harness.
What it is for, in order of how much it unlocks:
- Asking what E *believes*, not just what E put on the wire. A test can
then assert the two agree. That is the bug class - correct protocol,
wrong internal state - that black-box protocol testing cannot see at
all, and several planned tests (border counts, inhibit counters,
whether a key binding fired) have no other way to observe their
subject.
- zone_add. The headless backend has exactly one zone, so every
multi-monitor behaviour was untestable: xdg-output logical geometry,
per-output surface tracking, whether wl_output.name is actually unique.
- Input, via evas_event_feed_*. E's wayland pointer handlers are evas
callbacks, so synthesised evas events reach clients exactly as real
ones do - no uinput, no libinput, no seat, no root.
- Client serials, assigned from a wl_display client_created listener.
wlcs's position_window_absolute is handed a client-side wl_surface
belonging to a different connection and has to ask the compositor to
move it; connection order decided server-side is what makes that
answerable without guessing.
Two deliberate omissions. There is no buffer_scale in surface_info:
set_buffer_scale is an empty stub and E stores nothing, so the module
would have to invent a value, and a test asserting on it would be
asserting on the module rather than on the compositor. And the interface
carries nothing a public protocol could already answer.
sync is not wl_display.sync: it replies from an ecore job, so one main
loop iteration has run first. Most of what E does in response to a request
happens in a job or an idler, so a test that asked "did that land?"
straight after would race.
Named wl_test rather than e_test to sit with the other wl_* modules and to
avoid colliding with the existing src/bin/e_test.c.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01FtoiXoSKUmZb6Aix6U3GZS
---
meson_options.txt | 4 +
src/modules/meson.build | 2 +
src/modules/wl_test/e_mod_main.c | 284 +++++++++++++++++++++++++++++++++++
src/modules/wl_test/meson.build | 14 ++
src/protocol/wl-test.xml | 173 ++++++++++++++++++++++
src/tests/wayland/e_test_globals.c | 6 +
src/tests/wayland/meson.build | 30 ++++
src/tests/wayland/run-nested.sh | 19 ++-
src/tests/wayland/test_wl_test.c | 293 +++++++++++++++++++++++++++++++++++++
9 files changed, 824 insertions(+), 1 deletion(-)
diff --git a/meson_options.txt b/meson_options.txt
index 6e81f6551..829fc1345 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -334,3 +334,7 @@ option('tests',
type: 'boolean',
value: false,
description: 'build the test suites: (default=false)')
+option('wl-test',
+ type: 'boolean',
+ value: true,
+ description: 'build the test-only compositor back door; also needs -Dtests=true: (default=true)')
diff --git a/src/modules/meson.build b/src/modules/meson.build
index 68dfc263a..b711fe49f 100644
--- a/src/modules/meson.build
+++ b/src/modules/meson.build
@@ -70,6 +70,8 @@ mods = [
'wl_text_input',
'wl_desktop_shell',
'wl_weekeyboard',
+# test-only back door; its own meson.build disables it unless -Dtests=true
+ 'wl_test',
### XXX: disabled for now
# 'wl_fb'
'pants',
diff --git a/src/modules/wl_test/e_mod_main.c b/src/modules/wl_test/e_mod_main.c
new file mode 100644
index 000000000..1acc2d3b4
--- /dev/null
+++ b/src/modules/wl_test/e_mod_main.c
@@ -0,0 +1,284 @@
+/* Test-only back door into the compositor.
+ *
+ * Built only with -Dtests=true and never installed in a release build. See
+ * src/protocol/e-test.xml for what it exposes and why.
+ *
+ * Everything here runs inside E's process, which is the whole point: it can
+ * answer what E *believes* about a surface, not merely what E put on the
+ * wire. It is also, for the same reason, coupled to E's internals and will
+ * need updating when they move. Keep the surface area small.
+ */
+#include "e.h"
+#include "wl-test-server-protocol.h"
+
+E_API E_Module_Api e_modapi = { E_MODULE_API_VERSION, "Wl_Test" };
+
+static struct wl_global *_wl_test_global = NULL;
+static struct wl_listener _client_created_listener;
+static Eina_Hash *_client_serials = NULL; /* wl_client* -> serial */
+static unsigned int _next_serial = 0;
+
+/* Pending sync requests, drained from an ecore job so that one main loop
+ * iteration has run before sync_done goes out. */
+static Eina_List *_pending_syncs = NULL;
+static Ecore_Job *_sync_job = NULL;
+
+static void
+_wl_test_cb_client_created(struct wl_listener *listener EINA_UNUSED, void *data)
+{
+ struct wl_client *client = data;
+ unsigned int serial = ++_next_serial;
+
+ /* Connection order decided here, server-side, rather than inferred by
+ * whoever opened the socket. wlcs needs to name a surface belonging to a
+ * client other than itself, and counting its own connect() calls would be
+ * a guess about ordering the compositor already knows for certain. */
+ eina_hash_add(_client_serials, &client, (void *)(uintptr_t)serial);
+}
+
+static unsigned int
+_wl_test_client_serial_get(struct wl_client *client)
+{
+ return (unsigned int)(uintptr_t)eina_hash_find(_client_serials, &client);
+}
+
+static E_Client *
+_wl_test_ec_get(struct wl_resource *surface)
+{
+ E_Client *ec;
+
+ if (!surface) return NULL;
+ ec = wl_resource_get_user_data(surface);
+ if (!ec) return NULL;
+ if (e_object_is_del(E_OBJECT(ec))) return NULL;
+ return ec;
+}
+
+static void
+_wl_test_cb_destroy(struct wl_client *client EINA_UNUSED, struct wl_resource *resource)
+{
+ wl_resource_destroy(resource);
+}
+
+static void
+_wl_test_cb_get_client_serial(struct wl_client *client, struct wl_resource *resource)
+{
+ wl_test_send_client_serial(resource, _wl_test_client_serial_get(client));
+}
+
+static void
+_wl_test_cb_get_surface_info(struct wl_client *client EINA_UNUSED, struct wl_resource *resource, struct wl_resource *surface)
+{
+ E_Client *ec;
+
+ ec = _wl_test_ec_get(surface);
+ if (!ec)
+ {
+ /* Normal before the first commit: the surface exists on the wire but
+ * has no E_Client behind it yet. Say so rather than guessing. */
+ wl_test_send_surface_unknown(resource, surface);
+ return;
+ }
+
+ wl_test_send_surface_info(resource, surface,
+ ec->x, ec->y, ec->w, ec->h,
+ evas_object_visible_get(ec->frame) ? 1 : 0,
+ (e_client_focused_get() == ec) ? 1 : 0);
+}
+
+static void
+_wl_test_cb_move_surface(struct wl_client *client EINA_UNUSED, struct wl_resource *resource EINA_UNUSED, struct wl_resource *surface, int32_t x, int32_t y)
+{
+ E_Client *ec;
+
+ ec = _wl_test_ec_get(surface);
+ if (!ec) return;
+
+ evas_object_move(ec->frame, x, y);
+}
+
+static void
+_wl_test_cb_zone_add(struct wl_client *client EINA_UNUSED, struct wl_resource *resource EINA_UNUSED, int32_t x, int32_t y, int32_t w, int32_t h)
+{
+ E_Zone *zone;
+ int num = eina_list_count(e_comp->zones);
+ char id[32];
+
+ snprintf(id, sizeof(id), "e-test-%d", num);
+
+ zone = e_zone_new(num, num, x, y, w, h);
+ if (!zone)
+ {
+ ERR("wl_test: could not create zone");
+ return;
+ }
+
+ /* e_comp_wl_output_init() finds the zone with e_zone_for_id_get(), which
+ * matches on randr2_id. A zone made outside the randr path has none, so
+ * set one or the output is silently dropped. */
+ zone->randr2_id = strdup(id);
+
+ /* Give the new zone a wl_output, the way the backends do for the first
+ * one. Without this a second zone is invisible to any client. */
+ if (!e_comp_wl_output_init(id, NULL, NULL, x, y, w, h, 0, 0, 0, 0, 0, num))
+ ERR("wl_test: could not create wl_output for zone %d", num);
+}
+
+static void
+_wl_test_cb_pointer_warp(struct wl_client *client EINA_UNUSED, struct wl_resource *resource EINA_UNUSED, int32_t x, int32_t y)
+{
+ /* Feeding evas is enough: E's wayland pointer handlers are evas callbacks
+ * on the client's frame object, so a synthesised evas event reaches the
+ * client exactly as a real one would. No uinput, no libinput, no seat. */
+ evas_event_feed_mouse_move(e_comp->evas, x, y, 0, NULL);
+}
+
+static void
+_wl_test_cb_pointer_button(struct wl_client *client EINA_UNUSED, struct wl_resource *resource EINA_UNUSED, uint32_t button, uint32_t pressed)
+{
+ /* evas buttons are 1-based; the protocol speaks BTN_* like the seat. */
+ int b = 1;
+
+ switch (button)
+ {
+ case BTN_LEFT: b = 1; break;
+ case BTN_MIDDLE: b = 2; break;
+ case BTN_RIGHT: b = 3; break;
+ default: b = (int)(button - BTN_LEFT) + 1; break;
+ }
+
+ if (pressed)
+ evas_event_feed_mouse_down(e_comp->evas, b, EVAS_BUTTON_NONE, 0, NULL);
+ else
+ evas_event_feed_mouse_up(e_comp->evas, b, EVAS_BUTTON_NONE, 0, NULL);
+}
+
+static void
+_wl_test_cb_pointer_axis(struct wl_client *client EINA_UNUSED, struct wl_resource *resource EINA_UNUSED, uint32_t direction, int32_t steps)
+{
+ evas_event_feed_mouse_wheel(e_comp->evas, direction ? 1 : 0, steps, 0, NULL);
+}
+
+static void
+_wl_test_cb_key(struct wl_client *client EINA_UNUSED, struct wl_resource *resource EINA_UNUSED, const char *keyname, uint32_t pressed)
+{
+ if (pressed)
+ evas_event_feed_key_down(e_comp->evas, keyname, keyname, NULL, NULL, 0, NULL);
+ else
+ evas_event_feed_key_up(e_comp->evas, keyname, keyname, NULL, NULL, 0, NULL);
+}
+
+static void
+_wl_test_cb_sync_job(void *data EINA_UNUSED)
+{
+ struct wl_resource *resource;
+
+ _sync_job = NULL;
+ EINA_LIST_FREE(_pending_syncs, resource)
+ wl_test_send_sync_done(resource);
+}
+
+static void
+_wl_test_cb_sync(struct wl_client *client EINA_UNUSED, struct wl_resource *resource)
+{
+ /* Answer from a job rather than inline. wl_display.sync only proves the
+ * requests were read; most of what E does in response happens in a job or
+ * an idler, so a test that asked "did the move land?" immediately after
+ * would race. Deferring by one loop iteration makes the answer mean
+ * something. */
+ _pending_syncs = eina_list_append(_pending_syncs, resource);
+ if (!_sync_job) _sync_job = ecore_job_add(_wl_test_cb_sync_job, NULL);
+}
+
+static const struct wl_test_interface _wl_test_implementation =
+{
+ _wl_test_cb_destroy,
+ _wl_test_cb_get_client_serial,
+ _wl_test_cb_get_surface_info,
+ _wl_test_cb_move_surface,
+ _wl_test_cb_zone_add,
+ _wl_test_cb_pointer_warp,
+ _wl_test_cb_pointer_button,
+ _wl_test_cb_pointer_axis,
+ _wl_test_cb_key,
+ _wl_test_cb_sync,
+};
+
+static void
+_wl_test_cb_unbind(struct wl_resource *resource)
+{
+ _pending_syncs = eina_list_remove(_pending_syncs, resource);
+}
+
+static void
+_wl_test_cb_bind(struct wl_client *client, void *data EINA_UNUSED, uint32_t version, uint32_t id)
+{
+ struct wl_resource *resource;
+
+ resource = wl_resource_create(client, &wl_test_interface, version, id);
+ if (!resource)
+ {
+ wl_client_post_no_memory(client);
+ return;
+ }
+
+ wl_resource_set_implementation(resource, &_wl_test_implementation, NULL,
+ _wl_test_cb_unbind);
+}
+
+E_API void *
+e_modapi_init(E_Module *m)
+{
+ if (!e_comp_wl)
+ {
+ ERR("wl_test: no wayland compositor data - wayland-only module");
+ return NULL;
+ }
+
+ _client_serials = eina_hash_pointer_new(NULL);
+
+ _client_created_listener.notify = _wl_test_cb_client_created;
+ wl_display_add_client_created_listener(e_comp_wl->wl.disp,
+ &_client_created_listener);
+
+ _wl_test_global = wl_global_create(e_comp_wl->wl.disp, &wl_test_interface, 1,
+ NULL, _wl_test_cb_bind);
+ if (!_wl_test_global)
+ {
+ ERR("wl_test: could not create global");
+ return NULL;
+ }
+
+ INF("wl_test: test interface active - this build is not for production");
+
+ return m;
+}
+
+E_API int
+e_modapi_shutdown(E_Module *m EINA_UNUSED)
+{
+ if (_sync_job)
+ {
+ ecore_job_del(_sync_job);
+ _sync_job = NULL;
+ }
+ _pending_syncs = eina_list_free(_pending_syncs);
+
+ if (_wl_test_global)
+ {
+ wl_global_destroy(_wl_test_global);
+ _wl_test_global = NULL;
+ }
+
+ wl_list_remove(&_client_created_listener.link);
+
+ E_FREE_FUNC(_client_serials, eina_hash_free);
+
+ return 1;
+}
+
+E_API int
+e_modapi_save(E_Module *m EINA_UNUSED)
+{
+ return 1;
+}
diff --git a/src/modules/wl_test/meson.build b/src/modules/wl_test/meson.build
new file mode 100644
index 000000000..f54e77136
--- /dev/null
+++ b/src/modules/wl_test/meson.build
@@ -0,0 +1,14 @@
+# Test-only module. Never built unless -Dtests=true, so a release build
+# cannot ship the back door even by accident.
+if get_option('wl') != true or get_option('tests') != true
+ disable = true
+else
+ src = ""
+ 'e_mod_main.c'
+ )
+
+ src += gen_scanner_server.process('../../protocol/wl-test.xml')
+ src += gen_scanner_impl.process('../../protocol/wl-test.xml')
+
+ no_icon = true
+endif
diff --git a/src/protocol/wl-test.xml b/src/protocol/wl-test.xml
new file mode 100644
index 000000000..e373aa299
--- /dev/null
+++ b/src/protocol/wl-test.xml
@@ -0,0 +1,173 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="wl_test">
+
+ <copyright>
+ Copyright © 2026 Enlightenment contributors
+
+ Permission is hereby granted, free of charge, to any person obtaining a
+ copy of this software and associated documentation files (the "Software"),
+ to deal in the Software without restriction, including without limitation
+ the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ and/or sell copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ DEALINGS IN THE SOFTWARE.
+ </copyright>
+
+ <interface name="wl_test" version="1">
+ <description summary="private interface for the compositor test suite">
+ A back door into the compositor for its own test suite. It exposes
+ internal state and synthesises input, so it is deliberately NOT built
+ unless -Dtests=true and it is never installed in a release build.
+
+ Enlightenment is a single large main() rather than a library, so tests
+ cannot construct a compositor per case and reach into it the way KWin's
+ or Weston's suites do. A test-only module is the equivalent: it is
+ dlopen'd into E's own process and can see everything, and this protocol
+ is how an out-of-process test asks it questions.
+
+ Do not add anything here that a test could get from a public protocol.
+ The point of this interface is the state the wire does not carry - what
+ E *believes* - so that a test can assert the compositor's own view
+ agrees with what it told the client. That is the bug class black-box
+ protocol testing cannot see.
+ </description>
+
+ <request name="destroy" type="destructor">
+ <description summary="release the test interface"/>
+ </request>
+
+ <request name="get_client_serial">
+ <description summary="ask which connection this is">
+ The compositor numbers client connections in the order they arrive,
+ starting at 1. A test harness that opens the sockets itself can use
+ the serial to name a surface belonging to a *different* client, which
+ is what wlcs's position_window_absolute needs: it is handed a
+ client-side wl_surface pointer from another connection and has to ask
+ the compositor to move it.
+
+ Assigned server-side from a client_created listener, so the ordering
+ is the compositor's own and not something the caller has to infer.
+ </description>
+ </request>
+
+ <event name="client_serial">
+ <description summary="reply to get_client_serial"/>
+ <arg name="serial" type="uint" summary="1-based connection order"/>
+ </event>
+
+ <request name="get_surface_info">
+ <description summary="ask what the compositor believes about a surface">
+ Answered with a surface_info event, or surface_unknown if there is no
+ E_Client behind the surface.
+
+ Note E makes the E_Client in wl_compositor.create_surface rather than
+ at the first commit, so a surface is normally known immediately.
+ surface_unknown means the client is being torn down, not that it is
+ merely uncommitted.
+ </description>
+ <arg name="surface" type="object" interface="wl_surface"/>
+ </request>
+
+ <event name="surface_info">
+ <description summary="the compositor's own view of a surface">
+ Frame geometry in compositor coordinates, i.e. what E would use to
+ paint it, not what the client last requested.
+ </description>
+ <arg name="surface" type="object" interface="wl_surface"/>
+ <arg name="x" type="int"/>
+ <arg name="y" type="int"/>
+ <arg name="w" type="int"/>
+ <arg name="h" type="int"/>
+ <arg name="visible" type="uint" summary="non-zero if E considers it visible"/>
+ <arg name="focused" type="uint" summary="non-zero if it holds the focus"/>
+ </event>
+
+ <!-- Deliberately no buffer_scale here. set_buffer_scale is an empty stub
+ and E stores nothing, so the module would have to invent a value -
+ which is worse than no value, because a test asserting on it would
+ be asserting on the module rather than on the compositor. Add it
+ when E-17 gives it somewhere real to come from. -->
+
+ <event name="surface_unknown">
+ <description summary="the surface has no E_Client"/>
+ <arg name="surface" type="object" interface="wl_surface"/>
+ </event>
+
+ <request name="move_surface">
+ <description summary="place a surface in compositor coordinates">
+ Moves the frame, so the coordinates match those reported by
+ surface_info.
+ </description>
+ <arg name="surface" type="object" interface="wl_surface"/>
+ <arg name="x" type="int"/>
+ <arg name="y" type="int"/>
+ </request>
+
+ <request name="zone_add">
+ <description summary="add a zone, i.e. a second monitor">
+ The headless backend has exactly one zone, so anything multi-monitor -
+ xdg-output logical geometry, per-output surface tracking, wl_output
+ name uniqueness - is untestable without this.
+ </description>
+ <arg name="x" type="int"/>
+ <arg name="y" type="int"/>
+ <arg name="w" type="int"/>
+ <arg name="h" type="int"/>
+ </request>
+
+ <request name="pointer_warp">
+ <description summary="place the pointer in compositor coordinates"/>
+ <arg name="x" type="int"/>
+ <arg name="y" type="int"/>
+ </request>
+
+ <request name="pointer_button">
+ <description summary="press or release a pointer button">
+ Button numbers are the linux/input.h BTN_* codes the seat already
+ speaks.
+ </description>
+ <arg name="button" type="uint"/>
+ <arg name="pressed" type="uint"/>
+ </request>
+
+ <request name="pointer_axis">
+ <description summary="scroll">
+ Direction 0 is vertical, 1 is horizontal, matching wl_pointer.axis.
+ </description>
+ <arg name="direction" type="uint"/>
+ <arg name="steps" type="int" summary="detents; negative scrolls back"/>
+ </request>
+
+ <request name="key">
+ <description summary="press or release a key by keysym name">
+ Named rather than by keycode so a test does not have to reproduce the
+ active keymap.
+ </description>
+ <arg name="keyname" type="string"/>
+ <arg name="pressed" type="uint"/>
+ </request>
+
+ <request name="sync">
+ <description summary="round-trip through the compositor's own event loop">
+ wl_display.sync only proves the compositor read the requests. This
+ additionally lets one main loop iteration run, so effects that E
+ applies from a job or an idler - which is most of them - have landed
+ before the done event arrives.
+ </description>
+ </request>
+
+ <event name="sync_done">
+ <description summary="reply to sync"/>
+ </event>
+ </interface>
+</protocol>
diff --git a/src/tests/wayland/e_test_globals.c b/src/tests/wayland/e_test_globals.c
index 75b0f473b..0221a688c 100644
--- a/src/tests/wayland/e_test_globals.c
+++ b/src/tests/wayland/e_test_globals.c
@@ -28,6 +28,12 @@ _global_add(void *data, struct wl_registry *reg, uint32_t id,
{
(void)data; (void)reg; (void)id;
+ /* The test module's own back door is not part of E's protocol surface -
+ * it exists only in a -Dtests=true build and is never installed. Listing
+ * it in globals.expected would make that file describe the harness rather
+ * than the compositor. */
+ if (!strcmp(iface, "wl_test")) return;
+
if (globals_count == globals_alloc)
{
globals_alloc = globals_alloc ? globals_alloc * 2 : 32;
diff --git a/src/tests/wayland/meson.build b/src/tests/wayland/meson.build
index 6b8d23d96..f8bcaf00a 100644
--- a/src/tests/wayland/meson.build
+++ b/src/tests/wayland/meson.build
@@ -24,6 +24,17 @@ e_test_globals = executable('e_test_globals',
dependencies: [dependency('wayland-client')],
)
+# Client-side bindings for the private test protocol and for xdg-shell, which
+# a test needs to map a surface before the compositor has an E_Client for it.
+test_proto_src = []
+foreach p: [
+ '../../protocol/wl-test.xml',
+ '@0@/stable/xdg-shell/xdg-shell.xml'.format(dir_wayland_protocols),
+]
+ test_proto_src += gen_scanner_client.process(p)
+ test_proto_src += gen_scanner_impl.process(p)
+endforeach
+
# The golden-globals test. Cheap, and the one test every protocol branch has
# to touch: it makes E's advertised protocol surface a reviewable file.
test('wl-globals',
@@ -32,3 +43,22 @@ test('wl-globals',
env: wl_test_env,
timeout: 120,
)
+
+# Protocol tests. Each is a plain wayland-client program that exits non-zero
+# with an explanatory message; run-nested.sh supplies the compositor.
+wl_protocol_tests = [
+ ['test-module', 'test_wl_test.c'],
+]
+
+foreach t: wl_protocol_tests
+ exe = executable('test_wl_' + t[0].underscorify(),
+ [t[1], test_proto_src],
+ dependencies: [dependency('wayland-client')],
+ )
+ test('wl-' + t[0],
+ find_program('run-nested.sh'),
+ args: [exe],
+ env: wl_test_env,
+ timeout: 120,
+ )
+endforeach
diff --git a/src/tests/wayland/run-nested.sh b/src/tests/wayland/run-nested.sh
index 3ace564d6..285aa3bd6 100755
--- a/src/tests/wayland/run-nested.sh
+++ b/src/tests/wayland/run-nested.sh
@@ -84,16 +84,33 @@ cleanup() {
if [ "${E_TEST_KEEP:-0}" = "1" ]; then
echo "run-nested.sh: kept $RUNDIR (compositor log: $E_LOG)" >&2
else
- rm -rf "$RUNDIR"
+ # E forks helpers (enlightenment_fm and friends) that keep writing
+ # into HOME as they wind down, so a single rm -rf can lose a race with
+ # them and fail on a directory being repopulated while it is emptied.
+ # Retry, and never let the tidy-up decide the exit status: that turns
+ # a passing test into a failing one for no reason.
+ i=0
+ while [ $i -lt 10 ]; do
+ rm -rf "$RUNDIR" 2>/dev/null && break
+ i=$((i + 1))
+ sleep 0.1
+ done
fi
exit $status
}
trap cleanup EXIT INT TERM
+# The test back door is deliberately not in the wltest profile's module list:
+# pulling it in by env var keeps the profile a description of the compositor
+# under test rather than of the harness. E_MODULE_FORCE_LOAD also accepts an
+# absolute .so path, which e_module_new() has always handled.
+E_TEST_MODULE=${E_TEST_MODULE:-wl_test}
+
# The compositor's own environment. Deliberately not exported to this shell,
# so the client below is launched with WAYLAND_DISPLAY pointing at the nested
# compositor and nothing else inherited by accident.
XDG_RUNTIME_DIR="$RUNDIR" \
+E_MODULE_FORCE_LOAD="$E_TEST_MODULE" \
E_PREFIX="$E_PREFIX" \
E_BIN_DIR="$E_PREFIX/bin" \
E_LIB_DIR="$E_PREFIX/lib" \
diff --git a/src/tests/wayland/test_wl_test.c b/src/tests/wayland/test_wl_test.c
new file mode 100644
index 000000000..f22686b94
--- /dev/null
+++ b/src/tests/wayland/test_wl_test.c
@@ -0,0 +1,293 @@
+/* Exercises the test module itself.
+ *
+ * If this passes, three things are true that nothing else in the suite could
+ * establish:
+ *
+ * - a module can be pulled into the compositor from the build tree and
+ * serve a private protocol (E_MODULE_FORCE_LOAD);
+ * - a test can ask the compositor what it *believes* about a surface, and
+ * compare that against what it told the client;
+ * - a second zone can be conjured at runtime, so multi-output behaviour is
+ * testable on a headless backend that has exactly one.
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <wayland-client.h>
+#include "wl-test-client-protocol.h"
+#include "xdg-shell-client-protocol.h"
+
+#define W 200
+#define H 150
+
+#define FAIL(fmt, ...) \
+ do { fprintf(stderr, "test-wl-test: " fmt "\n", ##__VA_ARGS__); return 1; } while (0)
+
+static struct wl_compositor *compositor;
+static struct wl_shm *shm;
+static struct xdg_wm_base *wm_base;
+static struct wl_test *tester;
+static int output_count;
+
+static unsigned int client_serial;
+static int sync_done;
+
+static int info_valid;
+static int info_unknown;
+static int32_t info_x, info_y, info_w, info_h;
+static uint32_t info_visible, info_focused;
+
+static int configured;
+
+static void
+_client_serial(void *d, struct wl_test *t, uint32_t serial)
+{
+ (void)d; (void)t;
+ client_serial = serial;
+}
+
+static void
+_surface_info(void *d, struct wl_test *t, struct wl_surface *s,
+ int32_t x, int32_t y, int32_t w, int32_t h,
+ uint32_t visible, uint32_t focused)
+{
+ (void)d; (void)t; (void)s;
+ info_valid = 1; info_unknown = 0;
+ info_x = x; info_y = y; info_w = w; info_h = h;
+ info_visible = visible; info_focused = focused;
+}
+
+static void
+_surface_unknown(void *d, struct wl_test *t, struct wl_surface *s)
+{
+ (void)d; (void)t; (void)s;
+ info_valid = 0; info_unknown = 1;
+}
+
+static void
+_sync_done(void *d, struct wl_test *t)
+{
+ (void)d; (void)t;
+ sync_done = 1;
+}
+
+static const struct wl_test_listener _tester_listener =
+{
+ _client_serial, _surface_info, _surface_unknown, _sync_done
+};
+
+static void
+_wm_base_ping(void *d, struct xdg_wm_base *b, uint32_t serial)
+{
+ (void)d;
+ xdg_wm_base_pong(b, serial);
+}
+
+static const struct xdg_wm_base_listener _wm_base_listener = { _wm_base_ping };
+
+static void
+_xdg_surface_configure(void *d, struct xdg_surface *s, uint32_t serial)
+{
+ (void)d;
+ xdg_surface_ack_configure(s, serial);
+ configured = 1;
+}
+
+static const struct xdg_surface_listener _xdg_surface_listener =
+{
+ _xdg_surface_configure
+};
+
+static void
+_toplevel_configure(void *d, struct xdg_toplevel *t, int32_t w, int32_t h, struct wl_array *states)
+{
+ (void)d; (void)t; (void)w; (void)h; (void)states;
+}
+
+static void
+_toplevel_close(void *d, struct xdg_toplevel *t)
+{
+ (void)d; (void)t;
+}
+
+static const struct xdg_toplevel_listener _toplevel_listener =
+{
+ _toplevel_configure, _toplevel_close
+};
+
+static void
+_global_add(void *data, struct wl_registry *reg, uint32_t id, const char *iface, uint32_t version)
+{
+ (void)data; (void)version;
+
+ if (!strcmp(iface, "wl_compositor"))
+ compositor = wl_registry_bind(reg, id, &wl_compositor_interface, 4);
+ else if (!strcmp(iface, "wl_shm"))
+ shm = wl_registry_bind(reg, id, &wl_shm_interface, 1);
+ else if (!strcmp(iface, "xdg_wm_base"))
+ {
+ wm_base = wl_registry_bind(reg, id, &xdg_wm_base_interface, 1);
+ xdg_wm_base_add_listener(wm_base, &_wm_base_listener, NULL);
+ }
+ else if (!strcmp(iface, "wl_test"))
+ tester = wl_registry_bind(reg, id, &wl_test_interface, 1);
+ else if (!strcmp(iface, "wl_output"))
+ output_count++;
+}
+
+static void
+_global_remove(void *d, struct wl_registry *r, uint32_t id)
+{
+ (void)d; (void)r; (void)id;
+}
+
+static const struct wl_registry_listener _registry_listener =
+{
+ _global_add, _global_remove
+};
+
+/* Round-trip through the compositor's main loop, not just its request
+ * queue: E applies much of what it does from jobs and idlers, so asking
+ * "did that land?" straight after the request would race. */
+static int
+tester_sync(struct wl_display *disp)
+{
+ sync_done = 0;
+ wl_test_sync(tester);
+ while (!sync_done)
+ if (wl_display_dispatch(disp) < 0) return -1;
+ return 0;
+}
+
+static struct wl_buffer *
+make_buffer(void)
+{
+ int fd, stride = W * 4, size = stride * H;
+ void *map;
+ struct wl_shm_pool *pool;
+ struct wl_buffer *buf;
+ char name[] = "/e-test-shm-XXXXXX";
+
+ fd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600);
+ if (fd < 0) return NULL;
+ shm_unlink(name);
+ if (ftruncate(fd, size) < 0) { close(fd); return NULL; }
+
+ map = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+ if (map == MAP_FAILED) { close(fd); return NULL; }
+ memset(map, 0xff, size);
+
+ pool = wl_shm_create_pool(shm, fd, size);
+ buf = wl_shm_pool_create_buffer(pool, 0, W, H, stride, WL_SHM_FORMAT_ARGB8888);
+ wl_shm_pool_destroy(pool);
+ close(fd);
+ return buf;
+}
+
+int
+main(void)
+{
+ struct wl_display *disp;
+ struct wl_registry *reg;
+ struct wl_surface *surface;
+ struct xdg_surface *xdg_surface;
+ struct xdg_toplevel *toplevel;
+ struct wl_buffer *buffer;
+ int outputs_before;
+
+ disp = wl_display_connect(NULL);
+ if (!disp) FAIL("cannot connect to WAYLAND_DISPLAY=%s",
+ getenv("WAYLAND_DISPLAY") ?: "(unset)");
+
+ reg = wl_display_get_registry(disp);
+ wl_registry_add_listener(reg, &_registry_listener, NULL);
+ if (wl_display_roundtrip(disp) < 0) FAIL("registry roundtrip failed");
+
+ if (!tester)
+ FAIL("no wl_test global -- is the module built (-Dtests=true) and is "
+ "E_MODULE_FORCE_LOAD set?");
+ if (!compositor) FAIL("no wl_compositor");
+ if (!shm) FAIL("no wl_shm");
+ if (!wm_base) FAIL("no xdg_wm_base");
+ wl_test_add_listener(tester, &_tester_listener, NULL);
+
+ /* 1. Client serials are assigned server-side, in connection order. */
+ wl_test_get_client_serial(tester);
+ if (wl_display_roundtrip(disp) < 0) FAIL("client_serial roundtrip failed");
+ if (client_serial < 1)
+ FAIL("client serial is %u, expected >= 1", client_serial);
+
+ /* 2. E creates the E_Client in wl_compositor.create_surface
+ * (_e_comp_wl_compositor_cb_surface_create -> e_client_new), not at the
+ * first commit the way wlroots and Weston do. So a surface is known to
+ * the compositor immediately, before it has any content. Worth pinning
+ * down: it is the sort of difference a test written against another
+ * compositor's lifecycle gets wrong. */
+ surface = wl_compositor_create_surface(compositor);
+ wl_test_get_surface_info(tester, surface);
+ if (wl_display_roundtrip(disp) < 0) FAIL("surface_info roundtrip failed");
+ if (!info_valid)
+ FAIL("no E_Client for a freshly created surface -- E is expected to "
+ "make one in create_surface, not at first commit");
+
+ /* 3. Map it properly and the geometry should become real. */
+ xdg_surface = xdg_wm_base_get_xdg_surface(wm_base, surface);
+ xdg_surface_add_listener(xdg_surface, &_xdg_surface_listener, NULL);
+ toplevel = xdg_surface_get_toplevel(xdg_surface);
+ xdg_toplevel_add_listener(toplevel, &_toplevel_listener, NULL);
+ xdg_toplevel_set_title(toplevel, "e-test");
+ wl_surface_commit(surface);
+
+ while (!configured)
+ if (wl_display_dispatch(disp) < 0) FAIL("dispatch failed awaiting configure");
+
+ buffer = make_buffer();
+ if (!buffer) FAIL("could not create an shm buffer");
+ wl_surface_attach(surface, buffer, 0, 0);
+ wl_surface_damage(surface, 0, 0, W, H);
+ wl_surface_commit(surface);
+ if (tester_sync(disp) < 0) FAIL("sync failed after mapping");
+
+ info_valid = info_unknown = 0;
+ wl_test_get_surface_info(tester, surface);
+ if (wl_display_roundtrip(disp) < 0) FAIL("surface_info roundtrip failed");
+ if (!info_valid)
+ FAIL("compositor still reports no E_Client after a mapped commit");
+ if ((info_w != W) || (info_h != H))
+ FAIL("compositor believes the surface is %dx%d, client committed %dx%d",
+ info_w, info_h, W, H);
+
+ /* 4. move_surface, and the compositor's own view agrees afterwards. This
+ * is the whole point of the module: the wire never carries this. */
+ wl_test_move_surface(tester, surface, 120, 90);
+ if (tester_sync(disp) < 0) FAIL("sync failed after move");
+
+ info_valid = 0;
+ wl_test_get_surface_info(tester, surface);
+ if (wl_display_roundtrip(disp) < 0) FAIL("surface_info roundtrip failed");
+ if (!info_valid) FAIL("no surface_info after move");
+ if ((info_x != 120) || (info_y != 90))
+ FAIL("asked for (120,90), compositor believes (%d,%d)", info_x, info_y);
+
+ /* 5. A second zone, and with it a second wl_output. Without this every
+ * multi-monitor behaviour is untestable headless. */
+ outputs_before = output_count;
+ wl_test_zone_add(tester, 1024, 0, 800, 600);
+ if (tester_sync(disp) < 0) FAIL("sync failed after zone_add");
+ if (wl_display_roundtrip(disp) < 0) FAIL("roundtrip failed after zone_add");
+ if (output_count <= outputs_before)
+ FAIL("zone_add produced no new wl_output (had %d, still %d)",
+ outputs_before, output_count);
+
+ printf("test-wl-test: ok (serial=%u, geom=%dx%d at %d,%d, visible=%u, "
+ "focused=%u, outputs %d -> %d)\n",
+ client_serial, info_w, info_h, info_x, info_y,
+ info_visible, info_focused, outputs_before, output_count);
+
+ wl_test_destroy(tester);
+ wl_display_disconnect(disp);
+ return 0;
+}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.