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.

Reply via email to