This is an automated email from the git hooks/post-receive script.
git pushed a commit to branch wl/real-browser
in repository enlightenment.
View the commit online.
commit 006e5fbd3af7f407c3bb1e077578970d4027027e
Author: Cedric BAIL <[email protected]>
AuthorDate: Sun Aug 16 22:08:06 2026 -0600
tests - a test kit, and drive E's own actions from a test
Two things that only make sense together.
client_action runs one of E's registered actions on a window named by id. It is
the escape hatch, and deliberately the first way in rather than a request per
behaviour: E already has an action for everything a binding can do, so one
request buys window_maximized_toggle, window_iconic_toggle, window_move,
window_resize and the rest, and buys them through the same entry point a real
binding uses rather than a private path that could quietly diverge. It also
sidesteps the fact that the wltest profile has no key bindings and one mouse
binding, so anything input-driven would have to invent a binding scheme first
and would then be testing the profile.
Naming a window that is gone, or an action that does not exist, kills the
connection. The alternative is a silent no-op, which reads as the compositor
declining to act rather than as a typo in the test, and passes.
e_wl_testkit is the plumbing seven tests were each carrying a copy of. It
matters more than the line count: a test client has to *behave* like a client,
and the kit is where that lives. A compositor cannot resize a Wayland client by
fiat - E answers a maximise by sending a configure and then explicitly
declining to maximise anything (_e_comp_wl_evas_cb_maximize_pre sets the
requested maximise back to zero) - so a client that ignores configures never
reaches any of the states a test wants to assert, and every assertion fails for
a reason that has nothing to do with the compositor. The kit's toplevels ack
every configure, repaint at the size they were given, and release their buffers
when the compositor is done with them.
tk_settle syncs twice, and for a reason rather than for luck: the first round
trip carries E's configure out and our commit back, the second lets E apply
that commit. Still nothing that sleeps anywhere in the suite.
test_client_action asserts the maximise round trip and is marked xfail, because
it found a real defect rather than passing. E maximises the state and takes the
size back. The configures, in order:
0x0 0x0 320x240 1024x768 320x240
1024x768 is the zone and is right. Then E puts it back. The restore in
maximize_pre is deliberate - the client's own commit is supposed to drive the
resize - but the other half is missing: when the 1024x768 commit arrives, E
compares it against ec->w/h, still 320x240, and corrects the client back down.
Nothing remembers the size E asked for. A real client obeys, which is very
likely why Firefox came up at 500x120 in the browser probe.
meson's should_fail carries that: the suite stays green while it fails, and
reports a failure the day it passes, which is the signal that the compositor
fix landed and the marker can go. Same bargain as wlcs/expected-failures.txt.
Suite: wl-globals, eight protocol tests and e_wlcs_driver pass; client-action
fails as declared.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
src/modules/wl_test/e_mod_main.c | 48 ++++
src/modules/wl_test/wl-test.xml | 37 +++
src/tests/wayland/e_wl_testkit.c | 462 +++++++++++++++++++++++++++++++++
src/tests/wayland/e_wl_testkit.h | 100 +++++++
src/tests/wayland/meson.build | 17 +-
src/tests/wayland/test_client_action.c | 108 ++++++++
src/tests/wayland/test_client_list.c | 337 ++----------------------
7 files changed, 791 insertions(+), 318 deletions(-)
diff --git a/src/modules/wl_test/e_mod_main.c b/src/modules/wl_test/e_mod_main.c
index 821adec3d..9facf77e2 100644
--- a/src/modules/wl_test/e_mod_main.c
+++ b/src/modules/wl_test/e_mod_main.c
@@ -144,6 +144,53 @@ _wl_test_cb_get_clients(struct wl_client *client EINA_UNUSED, struct wl_resource
wl_test_send_clients_done(resource);
}
+/* Resolve an id, or kill the connection. A test naming a window that is gone
+ * has a bug in the test, and the alternative to killing it is a silent no-op -
+ * which reads as the compositor declining to act rather than as a mistake, and
+ * passes. */
+static E_Client *
+_wl_test_client_by_id(struct wl_resource *resource, uint32_t id)
+{
+ E_Client *ec;
+
+ if (id)
+ {
+ E_CLIENT_FOREACH(ec)
+ {
+ if (e_object_is_del(E_OBJECT(ec))) continue;
+ if (_wl_test_client_id(ec) == id) return ec;
+ }
+ }
+
+ wl_resource_post_error(resource, WL_TEST_ERROR_UNKNOWN_CLIENT,
+ "no window with id %u", id);
+ return NULL;
+}
+
+static void
+_wl_test_cb_client_action(struct wl_client *client EINA_UNUSED, struct wl_resource *resource, uint32_t id, const char *name, const char *params)
+{
+ E_Client *ec;
+ E_Action *act;
+
+ ec = _wl_test_client_by_id(resource, id);
+ if (!ec) return;
+
+ act = e_action_find(name);
+ if ((!act) || (!act->func.go))
+ {
+ wl_resource_post_error(resource, WL_TEST_ERROR_UNKNOWN_ACTION,
+ "no action named '%s'", name ?: "");
+ return;
+ }
+
+ /* Empty means none. E's actions distinguish a null params from an empty
+ * one - _e_actions_maximize_parse answers e_config->maximize_policy for
+ * null and parses anything else - so passing "" straight through would
+ * mean something other than "the configured maximize". */
+ act->func.go(E_OBJECT(ec), (params && params[0]) ? params : NULL);
+}
+
static void
_wl_test_cb_destroy(struct wl_client *client EINA_UNUSED, struct wl_resource *resource)
{
@@ -460,6 +507,7 @@ static const struct wl_test_interface _wl_test_implementation =
_wl_test_cb_output_scale_set,
/* version 4 */
_wl_test_cb_get_clients,
+ _wl_test_cb_client_action,
};
static void
diff --git a/src/modules/wl_test/wl-test.xml b/src/modules/wl_test/wl-test.xml
index 1b5c6244c..aeaca3b00 100644
--- a/src/modules/wl_test/wl-test.xml
+++ b/src/modules/wl_test/wl-test.xml
@@ -349,5 +349,42 @@
afterwards on the same connection.
</description>
</event>
+
+ <enum name="error" since="4">
+ <description summary="fatal misuse of the id-based requests">
+ A test that names a window that is gone, or an action that does not
+ exist, has a bug in the test. Killing the connection is the point:
+ the alternative is a silent no-op, which reads as the compositor
+ declining to do something rather than as a typo, and passes.
+ </description>
+ <entry name="unknown_client" value="0" summary="no window has that id"/>
+ <entry name="unknown_action" value="1" summary="no such action is registered"/>
+ </enum>
+
+ <request name="client_action" since="4">
+ <description summary="run one of E's own actions on a window">
+ The escape hatch, and deliberately the first way in rather than a
+ request per behaviour. E already has an action for everything a
+ binding can do - window_maximized_toggle, window_iconic_toggle,
+ window_fullscreen_toggle, window_move, window_resize, window_close -
+ so one request buys all of them, and buys them through the same entry
+ point a key or mouse binding uses rather than a private path that
+ could quietly diverge from it.
+
+ It also sidesteps a real problem with driving these from input: the
+ wltest profile has no key bindings at all and exactly one mouse
+ binding, so anything else would have to invent a binding scheme first,
+ and would then be testing the profile.
+
+ An empty params string means no parameters, which is not the same as
+ the empty parameter - E's actions distinguish them, and passing "" to
+ window_maximized_toggle would mean something other than "toggle".
+
+ Posts unknown_action if no action by that name is registered.
+ </description>
+ <arg name="id" type="uint"/>
+ <arg name="name" type="string"/>
+ <arg name="params" type="string"/>
+ </request>
</interface>
</protocol>
diff --git a/src/tests/wayland/e_wl_testkit.c b/src/tests/wayland/e_wl_testkit.c
new file mode 100644
index 000000000..1b44dfd4b
--- /dev/null
+++ b/src/tests/wayland/e_wl_testkit.c
@@ -0,0 +1,462 @@
+/* See e_wl_testkit.h. */
+/* memfd_create is behind __USE_GNU in <sys/mman.h>, and the test clients do
+ * not include config.h where the rest of the tree gets _GNU_SOURCE. */
+#define _GNU_SOURCE 1
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/mman.h>
+
+#include "e_wl_testkit.h"
+
+struct _Tk_Toplevel
+{
+ Tk *tk;
+ struct wl_surface *surface;
+ struct xdg_surface *xdg_surface;
+ struct xdg_toplevel *toplevel;
+ int w, h; /* the size we last painted */
+ int pending_w, pending_h; /* what the last configure asked for */
+ int configures;
+};
+
+struct _Tk
+{
+ const char *prog;
+
+ struct wl_display *disp;
+ struct wl_registry *registry;
+ struct wl_compositor *compositor;
+ struct wl_shm *shm;
+ struct xdg_wm_base *wm_base;
+ struct wl_test *tester;
+ uint32_t tester_version;
+
+ Tk_Client clients[TK_MAX_CLIENTS];
+ int client_count;
+ int done_count;
+
+ int configured;
+};
+
+/* --------------------------------------------------------------- wl_test */
+
+static void
+_client_serial(void *data, struct wl_test *t, uint32_t serial)
+{
+ (void)data; (void)t; (void)serial;
+}
+
+static void
+_surface_info(void *data, struct wl_test *t, struct wl_surface *s,
+ int32_t x, int32_t y, int32_t w, int32_t h,
+ uint32_t vis, uint32_t foc)
+{
+ (void)data; (void)t; (void)s; (void)x; (void)y; (void)w; (void)h;
+ (void)vis; (void)foc;
+}
+
+static void
+_surface_unknown(void *data, struct wl_test *t, struct wl_surface *s)
+{
+ (void)data; (void)t; (void)s;
+}
+
+static void
+_sync_done(void *data, struct wl_test *t)
+{
+ (void)data; (void)t;
+}
+
+static void
+_surface_buffer_info(void *data, struct wl_test *t, struct wl_surface *s,
+ int32_t scale, int32_t transform, int32_t oscale)
+{
+ (void)data; (void)t; (void)s; (void)scale; (void)transform; (void)oscale;
+}
+
+static void
+_client_info(void *data, struct wl_test *t, uint32_t id, int32_t pid,
+ const char *title, const char *app_id, uint32_t parent,
+ int32_t x, int32_t y, int32_t w, int32_t h,
+ uint32_t states, uint32_t output)
+{
+ Tk *tk = data;
+ Tk_Client *c;
+
+ (void)t;
+ if (tk->client_count >= TK_MAX_CLIENTS) return;
+
+ c = &tk->clients[tk->client_count++];
+ c->id = id;
+ c->pid = pid;
+ snprintf(c->title, sizeof(c->title), "%s", title ?: "");
+ snprintf(c->app_id, sizeof(c->app_id), "%s", app_id ?: "");
+ c->parent = parent;
+ c->x = x; c->y = y; c->w = w; c->h = h;
+ c->states = states;
+ c->output = output;
+}
+
+static void
+_clients_done(void *data, struct wl_test *t)
+{
+ Tk *tk = data;
+
+ (void)t;
+ tk->done_count++;
+}
+
+static const struct wl_test_listener _test_listener =
+{
+ _client_serial,
+ _surface_info,
+ _surface_unknown,
+ _sync_done,
+ _surface_buffer_info,
+ _client_info,
+ _clients_done,
+};
+
+/* ------------------------------------------------------------- xdg-shell */
+
+static void
+_wm_base_ping(void *data, struct xdg_wm_base *b, uint32_t serial)
+{
+ (void)data;
+ xdg_wm_base_pong(b, serial);
+}
+
+static const struct xdg_wm_base_listener _wm_base_listener = { _wm_base_ping };
+
+static struct wl_buffer *_buffer_make(Tk *tk, int w, int h);
+
+/* Answer a configure the way a real client does: ack it, then paint at the
+ * size we were given and commit. Doing this from the xdg_surface handler
+ * rather than the toplevel one is what the protocol asks for - the toplevel
+ * event carries the size, the surface event is the commit point. */
+static void
+_xdg_surface_configure(void *data, struct xdg_surface *s, uint32_t serial)
+{
+ Tk_Toplevel *top = data;
+ struct wl_buffer *buffer;
+
+ xdg_surface_ack_configure(s, serial);
+
+ /* 0 means "you choose", which is what the initial configure says. */
+ if (top->pending_w > 0) top->w = top->pending_w;
+ if (top->pending_h > 0) top->h = top->pending_h;
+
+ buffer = _buffer_make(top->tk, top->w, top->h);
+ if (!buffer) tk_fail(top->tk, "cannot make a %dx%d buffer", top->w, top->h);
+
+ wl_surface_attach(top->surface, buffer, 0, 0);
+ wl_surface_damage(top->surface, 0, 0, top->w, top->h);
+ wl_surface_commit(top->surface);
+
+ top->configures++;
+}
+
+static const struct xdg_surface_listener _xdg_surface_listener =
+{
+ _xdg_surface_configure
+};
+
+static void
+_toplevel_configure(void *data, struct xdg_toplevel *t, int32_t w, int32_t h,
+ struct wl_array *states)
+{
+ Tk_Toplevel *top = data;
+
+ (void)t; (void)states;
+ top->pending_w = w;
+ top->pending_h = h;
+}
+
+static void
+_toplevel_close(void *data, struct xdg_toplevel *t)
+{
+ (void)data; (void)t;
+}
+
+static const struct xdg_toplevel_listener _toplevel_listener =
+{
+ _toplevel_configure,
+ _toplevel_close
+};
+
+/* -------------------------------------------------------------- registry */
+
+static void
+_global_add(void *data, struct wl_registry *reg, uint32_t id,
+ const char *iface, uint32_t version)
+{
+ Tk *tk = data;
+
+ if (!strcmp(iface, "wl_compositor"))
+ tk->compositor = wl_registry_bind(reg, id, &wl_compositor_interface,
+ version < 4 ? version : 4);
+ else if (!strcmp(iface, "wl_shm"))
+ tk->shm = wl_registry_bind(reg, id, &wl_shm_interface, 1);
+ else if (!strcmp(iface, "xdg_wm_base"))
+ tk->wm_base = wl_registry_bind(reg, id, &xdg_wm_base_interface, 1);
+ else if (!strcmp(iface, "wl_test"))
+ {
+ tk->tester_version = version;
+ /* The version the kit speaks, not whatever is on offer. Binding lower
+ * would turn a missing request into a silent no-op instead of a
+ * failure to start. */
+ tk->tester = wl_registry_bind(reg, id, &wl_test_interface, 4);
+ }
+}
+
+static void
+_global_remove(void *data, struct wl_registry *reg, uint32_t id)
+{
+ (void)data; (void)reg; (void)id;
+}
+
+static const struct wl_registry_listener _registry_listener =
+{
+ _global_add, _global_remove
+};
+
+/* ----------------------------------------------------------------- shm */
+
+static void
+_buffer_release(void *data, struct wl_buffer *buffer)
+{
+ (void)data;
+ /* A configure per state change means a buffer per state change. Destroying
+ * on release rather than never is what keeps a test from running the
+ * compositor out of file descriptors on a long scenario. */
+ wl_buffer_destroy(buffer);
+}
+
+static const struct wl_buffer_listener _buffer_listener = { _buffer_release };
+
+static struct wl_buffer *
+_buffer_make(Tk *tk, int w, int h)
+{
+ struct wl_shm_pool *pool;
+ struct wl_buffer *buf;
+ int stride = w * 4, size = stride * h, fd;
+ void *map;
+
+ fd = memfd_create("e-wl-testkit", MFD_CLOEXEC);
+ if (fd < 0) return NULL;
+ 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);
+ munmap(map, size);
+
+ pool = wl_shm_create_pool(tk->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);
+ wl_buffer_add_listener(buf, &_buffer_listener, NULL);
+ return buf;
+}
+
+/* ------------------------------------------------------------------ api */
+
+Tk *
+tk_connect(const char *progname)
+{
+ Tk *tk;
+
+ tk = calloc(1, sizeof(*tk));
+ if (!tk)
+ {
+ fprintf(stderr, "%s: out of memory\n", progname);
+ exit(1);
+ }
+ tk->prog = progname;
+
+ tk->disp = wl_display_connect(NULL);
+ if (!tk->disp)
+ {
+ fprintf(stderr, "%s: cannot connect to WAYLAND_DISPLAY=%s\n",
+ progname, getenv("WAYLAND_DISPLAY") ?: "(unset)");
+ exit(1);
+ }
+
+ tk->registry = wl_display_get_registry(tk->disp);
+ wl_registry_add_listener(tk->registry, &_registry_listener, tk);
+ wl_display_roundtrip(tk->disp);
+
+ if (!tk->compositor) tk_fail(tk, "no wl_compositor");
+ if (!tk->shm) tk_fail(tk, "no wl_shm");
+ if (!tk->wm_base) tk_fail(tk, "no xdg_wm_base");
+ if (!tk->tester)
+ tk_fail(tk, "no wl_test at version 4 (advertised %u); is the wl_test "
+ "module loaded?", tk->tester_version);
+
+ wl_test_add_listener(tk->tester, &_test_listener, tk);
+ xdg_wm_base_add_listener(tk->wm_base, &_wm_base_listener, tk);
+
+ return tk;
+}
+
+void
+tk_disconnect(Tk *tk)
+{
+ if (!tk) return;
+ if (tk->disp) wl_display_disconnect(tk->disp);
+ free(tk);
+}
+
+void
+tk_fail(Tk *tk, const char *fmt, ...)
+{
+ va_list args;
+ int i;
+
+ fprintf(stderr, "%s: ", tk->prog);
+ va_start(args, fmt);
+ vfprintf(stderr, fmt, args);
+ va_end(args);
+ fprintf(stderr, "\n");
+
+ /* The window list, unconditionally. It is the first thing anyone reading
+ * a failure wants and it is already one round trip away. */
+ if (tk->tester)
+ {
+ int n = tk_clients(tk, NULL, 0);
+
+ fprintf(stderr, "%s: compositor knows %d window(s):\n", tk->prog, n);
+ for (i = 0; i < tk->client_count; i++)
+ fprintf(stderr,
+ " id=%u pid=%d app_id='%s' title='%s' %dx%d+%d+%d "
+ "states=0x%x parent=%u output=%u\n",
+ tk->clients[i].id, tk->clients[i].pid,
+ tk->clients[i].app_id, tk->clients[i].title,
+ tk->clients[i].w, tk->clients[i].h,
+ tk->clients[i].x, tk->clients[i].y,
+ tk->clients[i].states, tk->clients[i].parent,
+ tk->clients[i].output);
+ }
+
+ exit(1);
+}
+
+Tk_Toplevel *
+tk_toplevel_new(Tk *tk, const char *app_id, const char *title, int w, int h)
+{
+ Tk_Toplevel *top;
+
+ top = calloc(1, sizeof(*top));
+ if (!top) tk_fail(tk, "out of memory");
+ top->tk = tk;
+ top->w = w;
+ top->h = h;
+
+ top->surface = wl_compositor_create_surface(tk->compositor);
+ top->xdg_surface = xdg_wm_base_get_xdg_surface(tk->wm_base, top->surface);
+ xdg_surface_add_listener(top->xdg_surface, &_xdg_surface_listener, top);
+ top->toplevel = xdg_surface_get_toplevel(top->xdg_surface);
+ xdg_toplevel_add_listener(top->toplevel, &_toplevel_listener, top);
+ if (title) xdg_toplevel_set_title(top->toplevel, title);
+ if (app_id) xdg_toplevel_set_app_id(top->toplevel, app_id);
+
+ /* The empty commit xdg-shell requires before any buffer. The configure it
+ * provokes is what paints us, from _xdg_surface_configure. */
+ wl_surface_commit(top->surface);
+ while (top->configures < 1)
+ if (wl_display_dispatch(tk->disp) < 0)
+ tk_fail(tk, "disconnected waiting for the initial configure");
+
+ tk_settle(tk);
+ return top;
+}
+
+void
+tk_sync(Tk *tk)
+{
+ wl_test_sync(tk->tester);
+ if (wl_display_roundtrip(tk->disp) < 0)
+ {
+ fprintf(stderr, "%s: disconnected during sync\n", tk->prog);
+ exit(1);
+ }
+}
+
+void
+tk_toplevel_configured(Tk_Toplevel *top, int *w, int *h, int *count)
+{
+ if (w) *w = top->pending_w;
+ if (h) *h = top->pending_h;
+ if (count) *count = top->configures;
+}
+
+void
+tk_settle(Tk *tk)
+{
+ /* Twice, and for a reason rather than for luck. The first round trip
+ * carries the compositor's configure out to us and our answering commit
+ * back; the second lets the compositor apply that commit. One is enough
+ * only for changes that need nothing from the client, and those are the
+ * uninteresting ones. Still no sleeping: every wait here is a round trip
+ * through E's own main loop. */
+ tk_sync(tk);
+ tk_sync(tk);
+}
+
+int
+tk_clients(Tk *tk, Tk_Client *out, int max)
+{
+ int i;
+
+ tk->client_count = 0;
+ tk->done_count = 0;
+ wl_test_get_clients(tk->tester);
+ if (wl_display_roundtrip(tk->disp) < 0)
+ {
+ fprintf(stderr, "%s: disconnected during get_clients\n", tk->prog);
+ exit(1);
+ }
+ if (tk->done_count != 1)
+ {
+ fprintf(stderr, "%s: expected one clients_done, got %d\n",
+ tk->prog, tk->done_count);
+ exit(1);
+ }
+
+ for (i = 0; (i < tk->client_count) && (i < max) && out; i++)
+ out[i] = tk->clients[i];
+
+ return tk->client_count;
+}
+
+Tk_Client *
+tk_find(Tk *tk, const char *app_id)
+{
+ int i;
+
+ tk_clients(tk, NULL, 0);
+ for (i = 0; i < tk->client_count; i++)
+ if (!strcmp(tk->clients[i].app_id, app_id)) return &tk->clients[i];
+
+ return NULL;
+}
+
+Tk_Client *
+tk_expect(Tk *tk, const char *app_id)
+{
+ Tk_Client *c = tk_find(tk, app_id);
+
+ if (!c) tk_fail(tk, "no window with app_id '%s'", app_id);
+ return c;
+}
+
+void
+tk_action(Tk *tk, unsigned int id, const char *name, const char *params)
+{
+ wl_test_client_action(tk->tester, id, name, params ?: "");
+ tk_settle(tk);
+}
diff --git a/src/tests/wayland/e_wl_testkit.h b/src/tests/wayland/e_wl_testkit.h
new file mode 100644
index 000000000..4f2354104
--- /dev/null
+++ b/src/tests/wayland/e_wl_testkit.h
@@ -0,0 +1,100 @@
+/* Shared plumbing for the Wayland test clients.
+ *
+ * Every test in this directory was re-implementing the same forty lines of
+ * registry binding, roundtripping and toplevel construction. That is tolerable
+ * for a handful of tests and not for the browser matrix, where each scenario is
+ * otherwise mostly boilerplate.
+ *
+ * The kit deliberately has no waiting-with-a-timeout helper yet. Nothing here
+ * needs one: wl_test.sync runs a compositor main loop iteration before it
+ * answers, so a test can ask a question and get a settled answer without ever
+ * sleeping. When a browser test needs to wait for something a browser does on
+ * its own schedule, that is the point to add one - with a deadline and a
+ * description, never a sleep.
+ */
+#ifndef E_WL_TESTKIT_H
+#define E_WL_TESTKIT_H
+
+#include <wayland-client.h>
+#include "wl-test-client-protocol.h"
+#include "xdg-shell-client-protocol.h"
+
+#define TK_MAX_CLIENTS 64
+
+/* One window, as the compositor describes it. Mirrors wl_test.client_info. */
+typedef struct
+{
+ unsigned int id;
+ int pid;
+ char title[256];
+ char app_id[256];
+ unsigned int parent;
+ int x, y, w, h;
+ unsigned int states;
+ unsigned int output;
+} Tk_Client;
+
+typedef struct _Tk Tk;
+
+/* One toplevel this test owns. Opaque; tk_toplevel_new hands one back and the
+ * kit keeps it answering configures for as long as the connection lives. */
+typedef struct _Tk_Toplevel Tk_Toplevel;
+
+/* Connect, bind what every test needs, and bind wl_test at the version the
+ * kit speaks. Never returns NULL: anything missing is a failure to start, and
+ * a test that cannot start should say so and stop rather than press on and
+ * report something less true. progname prefixes every message. */
+Tk *tk_connect(const char *progname);
+void tk_disconnect(Tk *tk);
+
+/* Print, dump every window the compositor knows about, and exit non-zero.
+ * The dump is the point: a bare "expected 2, got 1" costs another run to make
+ * sense of, and the information was already on hand. */
+void tk_fail(Tk *tk, const char *fmt, ...) __attribute__((noreturn, format(printf, 2, 3)));
+
+/* Map a toplevel the way xdg-shell requires: role, empty commit, wait for the
+ * configure, attach, commit. Returns once the compositor has a window for it.
+ *
+ * The result keeps answering configures for the life of the connection, and
+ * that is not a convenience - it is the difference between a test that can
+ * assert anything about window state and one that cannot. A compositor cannot
+ * resize a Wayland client on its own: E answers a maximise by sending a
+ * configure and then explicitly declining to maximise anything
+ * (_e_comp_wl_evas_cb_maximize_pre in e_comp_wl.c sets the requested maximise
+ * back to zero), and the window becomes maximised only once the client acks
+ * and commits a buffer at the size it was given. A test client that ignores
+ * configures never gets there, and every state assertion fails for a reason
+ * that has nothing to do with the compositor. */
+Tk_Toplevel *tk_toplevel_new(Tk *tk, const char *app_id, const char *title,
+ int w, int h);
+
+/* The size the compositor last asked this toplevel to be, and the number of
+ * configures it has answered. What E asked for and what E then reports as the
+ * frame are different questions, and when they disagree only one of them is
+ * the bug - so a test that asserts geometry should be able to say which. */
+void tk_toplevel_configured(Tk_Toplevel *top, int *w, int *h, int *count);
+
+/* Round-trip through the compositor's own main loop, so effects E applies from
+ * a job or an idler have landed. Not wl_display.sync, which only proves the
+ * requests were read. */
+void tk_sync(Tk *tk);
+
+/* Sync until a change that needs the client's cooperation has completed: one
+ * round trip to carry the compositor's configure out and our commit back,
+ * one more for the compositor to apply it. Still no sleeping anywhere. */
+void tk_settle(Tk *tk);
+
+/* Enumerate. Returns how many windows were reported, filling up to max. */
+int tk_clients(Tk *tk, Tk_Client *out, int max);
+
+/* Enumerate and return the one window with this app_id, or NULL. The result
+ * points into the kit and is invalidated by the next call. */
+Tk_Client *tk_find(Tk *tk, const char *app_id);
+
+/* Like tk_find, but a miss is a failure rather than a NULL to check. */
+Tk_Client *tk_expect(Tk *tk, const char *app_id);
+
+/* Run one of E's own actions on a window. Empty or NULL params means none. */
+void tk_action(Tk *tk, unsigned int id, const char *name, const char *params);
+
+#endif
diff --git a/src/tests/wayland/meson.build b/src/tests/wayland/meson.build
index d4c7d5172..583a115fc 100644
--- a/src/tests/wayland/meson.build
+++ b/src/tests/wayland/meson.build
@@ -52,6 +52,13 @@ test('wl-globals',
# Protocol tests. Each is a plain wayland-client program that exits non-zero
# with an explanatory message; run-nested.sh supplies the compositor.
+#
+# A third element marks a test as expected to fail: the behaviour it asserts is
+# the correct one and E does not have it yet. meson then reports the suite
+# green while it fails, and reports a *failure* the day it starts passing -
+# which is the signal that the compositor fix landed and the marker can go.
+# Same bargain as wlcs/expected-failures.txt, and for the same reason: a
+# workaround with no expiry outlives its cause and nobody finds out.
wl_protocol_tests = [
['test-module', 'test_wl_test.c'],
['pointer-enter', 'test_pointer_enter.c'],
@@ -61,11 +68,18 @@ wl_protocol_tests = [
['activation', 'test_activation.c'],
['buffer-scale', 'test_buffer_scale.c'],
['client-list', 'test_client_list.c'],
+ ['client-action', 'test_client_action.c', 'xfail'],
]
+# Shared plumbing: registry binding, toplevel construction, enumeration and a
+# failure path that dumps the compositor's window list. Compiled into each test
+# rather than built as a library - there are no other consumers, and a static
+# library here would be ceremony around three files.
+tk_src = files('e_wl_testkit.c')
+
foreach t: wl_protocol_tests
exe = executable('test_wl_' + t[0].underscorify(),
- [t[1], test_proto_src],
+ [t[1], tk_src, test_proto_src],
dependencies: [dependency('wayland-client')],
)
test('wl-' + t[0],
@@ -73,5 +87,6 @@ foreach t: wl_protocol_tests
args: [exe],
env: wl_test_env,
timeout: 120,
+ should_fail: t.length() > 2 and t[2] == 'xfail',
)
endforeach
diff --git a/src/tests/wayland/test_client_action.c b/src/tests/wayland/test_client_action.c
new file mode 100644
index 000000000..975f8eef4
--- /dev/null
+++ b/src/tests/wayland/test_client_action.c
@@ -0,0 +1,108 @@
+/* wl_test.client_action - driving E through its own actions.
+ *
+ * A browser is a black box: it will not maximise itself on request, and the
+ * wltest profile has no key bindings and one mouse binding, so there is
+ * nothing to press either. What there is, is the action every binding would
+ * have run. Naming it directly drives the compositor through the same entry
+ * point a real binding uses, rather than a private path that could quietly
+ * diverge from it.
+ *
+ * Maximise is the case worth asserting first, because the round trip proves
+ * more than the outward leg does. Anything can make a window bigger; putting
+ * it back exactly where it was means E kept the pre-maximise geometry and
+ * restored it, which is the part a client notices when it is wrong.
+ *
+ * EXPECTED TO FAIL - marked xfail in meson.build. The behaviour asserted here
+ * is the correct one; E does not have it yet.
+ *
+ * E maximises the *state* and then takes the *size* back. The configures this
+ * test's toplevel receives, in order:
+ *
+ * 0x0 the initial "you choose"
+ * 0x0
+ * 320x240 our own size, acked
+ * 1024x768 the maximise - the whole zone, which is right
+ * 320x240 and immediately back to where it started
+ *
+ * so the window ends up reporting maximized_h | maximized_v at its original
+ * 320x240. _e_comp_wl_evas_cb_maximize_pre (e_comp_wl.c) sends the maximised
+ * size and then restores ec->w/h, because a compositor may not resize a
+ * Wayland client by fiat and the client's own commit is supposed to drive the
+ * resize. What is missing is the other half: when that commit arrives at
+ * 1024x768, E compares it against ec->w/h - still 320x240 - and corrects the
+ * client back down. Nothing remembers that E asked for the larger size.
+ *
+ * A real client obeys, which is very likely why Firefox came up at 500x120 in
+ * the browser probe rather than at anything sensible.
+ *
+ * Fixing it means tracking the size of the configure E has sent and not yet
+ * seen acked, which is ordinary xdg-shell bookkeeping and a real piece of
+ * work. When it lands, this test goes green and meson reports the unexpected
+ * pass - which is the signal to delete the xfail marker.
+ */
+#include <stdio.h>
+
+#include "e_wl_testkit.h"
+
+#define APP_ID "e.test.client_action"
+#define TITLE "actions under test"
+#define W 320
+#define H 240
+
+#define MAXIMIZED (WL_TEST_CLIENT_STATE_MAXIMIZED_H | \
+ WL_TEST_CLIENT_STATE_MAXIMIZED_V)
+
+int
+main(void)
+{
+ Tk *tk;
+ Tk_Toplevel *top;
+ Tk_Client *c;
+ unsigned int id;
+ int x0, y0, w0, h0, cw, ch, nconf;
+
+ tk = tk_connect("test-client-action");
+ top = tk_toplevel_new(tk, APP_ID, TITLE, W, H);
+
+ c = tk_expect(tk, APP_ID);
+ id = c->id;
+ x0 = c->x; y0 = c->y; w0 = c->w; h0 = c->h;
+
+ if (c->states & MAXIMIZED)
+ tk_fail(tk, "a freshly mapped window is already maximized (states=0x%x)",
+ c->states);
+
+ tk_action(tk, id, "window_maximized_toggle", NULL);
+
+ c = tk_expect(tk, APP_ID);
+ if (!(c->states & WL_TEST_CLIENT_STATE_MAXIMIZED_H))
+ tk_fail(tk, "not maximized horizontally after the toggle (states=0x%x)",
+ c->states);
+ if (!(c->states & WL_TEST_CLIENT_STATE_MAXIMIZED_V))
+ tk_fail(tk, "not maximized vertically after the toggle (states=0x%x)",
+ c->states);
+ tk_toplevel_configured(top, &cw, &ch, &nconf);
+ if ((cw <= w0) || (ch <= h0))
+ tk_fail(tk, "maximized, but the configure asked for %dx%d, no bigger "
+ "than the original %dx%d (%d configures)",
+ cw, ch, w0, h0, nconf);
+ if ((c->w == w0) && (c->h == h0))
+ tk_fail(tk, "maximized and configured to %dx%d, but the frame is still "
+ "%dx%d (%d configures)", cw, ch, c->w, c->h, nconf);
+
+ tk_action(tk, id, "window_maximized_toggle", NULL);
+
+ c = tk_expect(tk, APP_ID);
+ if (c->states & MAXIMIZED)
+ tk_fail(tk, "still maximized after the second toggle (states=0x%x)",
+ c->states);
+ if ((c->x != x0) || (c->y != y0) || (c->w != w0) || (c->h != h0))
+ tk_fail(tk, "restored to %dx%d+%d+%d, was %dx%d+%d+%d before maximizing",
+ c->w, c->h, c->x, c->y, w0, h0, x0, y0);
+
+ printf("test-client-action: ok (id=%u maximized and restored to "
+ "%dx%d+%d+%d)\n", id, c->w, c->h, c->x, c->y);
+
+ tk_disconnect(tk);
+ return 0;
+}
diff --git a/src/tests/wayland/test_client_list.c b/src/tests/wayland/test_client_list.c
index ce7ee6b17..998b36ef0 100644
--- a/src/tests/wayland/test_client_list.c
+++ b/src/tests/wayland/test_client_list.c
@@ -7,352 +7,55 @@
* starts the program, it does not own its wl_display, and there is no way to
* name one of its windows.
*
- * So the compositor hands out its own ids. This asserts the two properties
- * that make them worth having:
- *
- * - the enumeration finds a window this test mapped, and reports the title,
- * app_id and frame geometry the compositor actually holds for it;
- * - clients_done arrives, so a caller can tell "that is all of them" from
- * "none yet", which is the difference between a test that waits and a
- * test that hangs.
+ * So the compositor hands out its own ids. This asserts the properties that
+ * make them worth having: the enumeration finds a window this test mapped,
+ * and reports the title, app_id, pid and frame geometry the compositor
+ * actually holds for it.
*
* The window is our own only because an in-tree test has nothing else to map.
* Nothing here goes through the wl_surface object: the test finds its own
* window by app_id, exactly as a browser test will.
*/
-/* memfd_create is behind __USE_GNU in <sys/mman.h>. */
-#define _GNU_SOURCE 1
#include <stdio.h>
-#include <stdlib.h>
#include <string.h>
#include <unistd.h>
-#include <sys/mman.h>
-#include <wayland-client.h>
-#include "wl-test-client-protocol.h"
-#include "xdg-shell-client-protocol.h"
-#define FAIL(fmt, ...) \
- do { fprintf(stderr, "test-client-list: " fmt "\n", ##__VA_ARGS__); return 1; } while (0)
+#include "e_wl_testkit.h"
#define APP_ID "e.test.client_list"
#define TITLE "client list under test"
#define W 320
#define H 240
-typedef struct
-{
- unsigned int id;
- int pid;
- char title[256];
- char app_id[256];
- unsigned int parent;
- int x, y, w, h;
- unsigned int states;
- unsigned int output;
-} Info;
-
-typedef struct
-{
- struct wl_compositor *compositor;
- struct wl_shm *shm;
- struct xdg_wm_base *wm_base;
- struct wl_test *tester;
- uint32_t tester_version;
-
- Info infos[64];
- int info_count;
- int done_count;
-
- int configured;
-} Ctx;
-
-static Ctx ctx;
-
-/* ------------------------------------------------------------- wl_test */
-
-static void
-_client_serial(void *data, struct wl_test *t, uint32_t serial)
-{
- (void)data; (void)t; (void)serial;
-}
-
-static void
-_surface_info(void *data, struct wl_test *t, struct wl_surface *s,
- int32_t x, int32_t y, int32_t w, int32_t h,
- uint32_t vis, uint32_t foc)
-{
- (void)data; (void)t; (void)s; (void)x; (void)y; (void)w; (void)h;
- (void)vis; (void)foc;
-}
-
-static void
-_surface_unknown(void *data, struct wl_test *t, struct wl_surface *s)
-{
- (void)data; (void)t; (void)s;
-}
-
-static void
-_sync_done(void *data, struct wl_test *t)
-{
- (void)data; (void)t;
-}
-
-static void
-_surface_buffer_info(void *data, struct wl_test *t, struct wl_surface *s,
- int32_t scale, int32_t transform, int32_t oscale)
-{
- (void)data; (void)t; (void)s; (void)scale; (void)transform; (void)oscale;
-}
-
-static void
-_client_info(void *data, struct wl_test *t, uint32_t id, int32_t pid,
- const char *title, const char *app_id, uint32_t parent,
- int32_t x, int32_t y, int32_t w, int32_t h,
- uint32_t states, uint32_t output)
-{
- Ctx *c = data;
- Info *i;
-
- (void)t;
- if (c->info_count >= (int)(sizeof(c->infos) / sizeof(c->infos[0]))) return;
-
- i = &c->infos[c->info_count++];
- i->id = id;
- i->pid = pid;
- snprintf(i->title, sizeof(i->title), "%s", title ?: "");
- snprintf(i->app_id, sizeof(i->app_id), "%s", app_id ?: "");
- i->parent = parent;
- i->x = x; i->y = y; i->w = w; i->h = h;
- i->states = states;
- i->output = output;
-}
-
-static void
-_clients_done(void *data, struct wl_test *t)
-{
- Ctx *c = data;
-
- (void)t;
- c->done_count++;
-}
-
-static const struct wl_test_listener _test_listener =
-{
- _client_serial,
- _surface_info,
- _surface_unknown,
- _sync_done,
- _surface_buffer_info,
- _client_info,
- _clients_done,
-};
-
-/* ------------------------------------------------------------- xdg-shell */
-
-static void
-_wm_base_ping(void *data, struct xdg_wm_base *b, uint32_t serial)
-{
- (void)data;
- 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 *data, struct xdg_surface *s, uint32_t serial)
-{
- Ctx *c = data;
-
- xdg_surface_ack_configure(s, serial);
- c->configured++;
-}
-
-static const struct xdg_surface_listener _xdg_surface_listener =
-{
- _xdg_surface_configure
-};
-
-static void
-_toplevel_configure(void *data, struct xdg_toplevel *t, int32_t w, int32_t h,
- struct wl_array *states)
-{
- (void)data; (void)t; (void)w; (void)h; (void)states;
-}
-
-static void
-_toplevel_close(void *data, struct xdg_toplevel *t)
-{
- (void)data; (void)t;
-}
-
-static const struct xdg_toplevel_listener _toplevel_listener =
-{
- _toplevel_configure,
- _toplevel_close
-};
-
-/* ------------------------------------------------------------- registry */
-
-static void
-_global_add(void *data, struct wl_registry *reg, uint32_t id,
- const char *iface, uint32_t version)
-{
- Ctx *c = data;
-
- if (!strcmp(iface, "wl_compositor"))
- c->compositor = wl_registry_bind(reg, id, &wl_compositor_interface,
- version < 4 ? version : 4);
- else if (!strcmp(iface, "wl_shm"))
- c->shm = wl_registry_bind(reg, id, &wl_shm_interface, 1);
- else if (!strcmp(iface, "xdg_wm_base"))
- c->wm_base = wl_registry_bind(reg, id, &xdg_wm_base_interface, 1);
- else if (!strcmp(iface, "wl_test"))
- {
- c->tester_version = version;
- /* Deliberately the version this test needs, not whatever is on
- * offer: binding lower would make a missing request look like a
- * silent no-op instead of a failure to start. */
- c->tester = wl_registry_bind(reg, id, &wl_test_interface, 4);
- }
-}
-
-static void
-_global_remove(void *data, struct wl_registry *reg, uint32_t id)
-{
- (void)data; (void)reg; (void)id;
-}
-
-static const struct wl_registry_listener _registry_listener =
-{
- _global_add, _global_remove
-};
-
-/* ------------------------------------------------------------- buffer */
-
-static struct wl_buffer *
-_buffer_make(Ctx *c, int w, int h)
-{
- struct wl_shm_pool *pool;
- struct wl_buffer *buf;
- int stride = w * 4, size = stride * h, fd;
- void *map;
-
- fd = memfd_create("e-test-client-list", MFD_CLOEXEC);
- if (fd < 0) return NULL;
- 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);
- munmap(map, size);
-
- pool = wl_shm_create_pool(c->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;
-}
-
-/* ------------------------------------------------------------- the test */
-
int
main(void)
{
- struct wl_display *disp;
- struct wl_registry *registry;
- struct wl_surface *surface;
- struct xdg_surface *xdg_surface;
- struct xdg_toplevel *toplevel;
- struct wl_buffer *buffer;
- Info *mine = NULL;
- int i;
+ Tk *tk;
+ Tk_Client *mine;
- disp = wl_display_connect(NULL);
- if (!disp) FAIL("cannot connect to WAYLAND_DISPLAY");
+ tk = tk_connect("test-client-list");
- registry = wl_display_get_registry(disp);
- wl_registry_add_listener(registry, &_registry_listener, &ctx);
- wl_display_roundtrip(disp);
+ tk_toplevel_new(tk, APP_ID, TITLE, W, H);
- if (!ctx.compositor) FAIL("no wl_compositor");
- if (!ctx.shm) FAIL("no wl_shm");
- if (!ctx.wm_base) FAIL("no xdg_wm_base");
- if (!ctx.tester)
- FAIL("no wl_test at version 4 (advertised %u)", ctx.tester_version);
-
- wl_test_add_listener(ctx.tester, &_test_listener, &ctx);
- xdg_wm_base_add_listener(ctx.wm_base, &_wm_base_listener, &ctx);
-
- /* Map a toplevel the protocol-correct way: role, empty commit, wait for
- * the configure, then attach. */
- surface = wl_compositor_create_surface(ctx.compositor);
- xdg_surface = xdg_wm_base_get_xdg_surface(ctx.wm_base, surface);
- xdg_surface_add_listener(xdg_surface, &_xdg_surface_listener, &ctx);
- toplevel = xdg_surface_get_toplevel(xdg_surface);
- xdg_toplevel_add_listener(toplevel, &_toplevel_listener, &ctx);
- xdg_toplevel_set_title(toplevel, TITLE);
- xdg_toplevel_set_app_id(toplevel, APP_ID);
- wl_surface_commit(surface);
-
- while (!ctx.configured)
- if (wl_display_dispatch(disp) < 0) FAIL("disconnected waiting for configure");
-
- buffer = _buffer_make(&ctx, W, H);
- if (!buffer) FAIL("cannot make a buffer");
- wl_surface_attach(surface, buffer, 0, 0);
- wl_surface_damage(surface, 0, 0, W, H);
- wl_surface_commit(surface);
-
- /* wl_test.sync, not wl_display.sync: the map lands from a job. */
- wl_test_sync(ctx.tester);
- wl_display_roundtrip(disp);
-
- wl_test_get_clients(ctx.tester);
- wl_display_roundtrip(disp);
-
- if (ctx.done_count != 1)
- FAIL("expected exactly one clients_done, got %d", ctx.done_count);
- if (ctx.info_count < 1)
- FAIL("get_clients reported no windows at all");
-
- for (i = 0; i < ctx.info_count; i++)
- if (!strcmp(ctx.infos[i].app_id, APP_ID)) { mine = &ctx.infos[i]; break; }
-
- if (!mine)
- {
- fprintf(stderr, "test-client-list: %d window(s), none with app_id '%s':\n",
- ctx.info_count, APP_ID);
- for (i = 0; i < ctx.info_count; i++)
- fprintf(stderr, " id=%u pid=%d app_id='%s' title='%s' %dx%d+%d+%d\n",
- ctx.infos[i].id, ctx.infos[i].pid, ctx.infos[i].app_id,
- ctx.infos[i].title, ctx.infos[i].w, ctx.infos[i].h,
- ctx.infos[i].x, ctx.infos[i].y);
- return 1;
- }
+ mine = tk_expect(tk, APP_ID);
if (mine->id == 0)
- FAIL("id 0 is reserved for 'no window', so no window may have it");
+ tk_fail(tk, "id 0 is reserved for 'no window', so no window may have it");
if (strcmp(mine->title, TITLE))
- FAIL("title is '%s', expected '%s'", mine->title, TITLE);
+ tk_fail(tk, "title is '%s', expected '%s'", mine->title, TITLE);
if (mine->pid != (int)getpid())
- FAIL("pid is %d, expected %d", mine->pid, (int)getpid());
+ tk_fail(tk, "pid is %d, expected %d", mine->pid, (int)getpid());
if ((mine->w != W) || (mine->h != H))
- FAIL("frame is %dx%d, expected %dx%d", mine->w, mine->h, W, H);
+ tk_fail(tk, "frame is %dx%d, expected %dx%d", mine->w, mine->h, W, H);
if (!(mine->states & WL_TEST_CLIENT_STATE_VISIBLE))
- FAIL("a mapped window with a buffer is not reported visible (states=0x%x)",
- mine->states);
+ tk_fail(tk, "a mapped window with a buffer is not reported visible "
+ "(states=0x%x)", mine->states);
if (mine->parent != 0)
- FAIL("a toplevel with no parent reports parent=%u", mine->parent);
+ tk_fail(tk, "a toplevel with no parent reports parent=%u", mine->parent);
- printf("test-client-list: ok (%d window(s), mine id=%u %dx%d+%d+%d states=0x%x)\n",
- ctx.info_count, mine->id, mine->w, mine->h, mine->x, mine->y,
- mine->states);
+ printf("test-client-list: ok (id=%u %dx%d+%d+%d states=0x%x)\n",
+ mine->id, mine->w, mine->h, mine->x, mine->y, mine->states);
- xdg_toplevel_destroy(toplevel);
- xdg_surface_destroy(xdg_surface);
- wl_surface_destroy(surface);
- wl_buffer_destroy(buffer);
- wl_display_disconnect(disp);
+ tk_disconnect(tk);
return 0;
}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.