This is an automated email from the git hooks/post-receive script.

git pushed a commit to branch wl/browser-all
in repository enlightenment.

View the commit online.

commit ab2b3919cf08bd23ad2866807d49fe139afd0ce6
Author: Cedric BAIL <[email protected]>
AuthorDate: Mon Aug 17 21:58:59 2026 -0600

    tests - ask the maximize/restore question of the X11 compositor too
    
    e_client.c is shared between E's X11 compositor and its Wayland one, and the
    saving and restoring of a window's pre-maximize size happens there. The Wayland
    test added alongside this reproduces a bug in that shared code; fixing it with
    evidence from one backend only is how the fix becomes a regression in the other
    with nothing to say so.
    
    So: run-x11.sh, which is to X11 what run-nested.sh is to Wayland - Xvfb, E as a
    real window manager on it, a client, and a private HOME so none of it touches
    the developer's. Deliberately a separate script rather than a mode flag on the
    existing one; almost nothing is shared. There is no back door to stage, because
    an X11 client can ask the server about its own window, and readiness is
    _NET_SUPPORTING_WM_CHECK appearing rather than a socket. It never inherits the
    caller's DISPLAY: E claims the WM selection and reparents everything on the
    screen it is given, so pointing this at a live session would fight the
    developer's own compositor for it.
    
    The test is the X11 spelling of the Wayland one. _GTK_FRAME_EXTENTS is how a
    client says "16 pixels of me on each side are shadow", so a 432x332 window is a
    400x300 frame - the same surface-versus-window ambiguity that
    set_window_geometry creates on Wayland, and E honours it (e_comp_x.c).
    
    It passes today, and that is the point: on X11 the client window really is the
    whole thing including its shadow, so ec->client.w is the honest size of it and
    saving that is correct. This is the guard rail for the Wayland fix, not a
    reproducer. Measured: baseline 432x332, maximized to 1056x800 - E sizes the
    window so its *visible* 1024x768 fills the zone, which is the shadow arithmetic
    being done right - and restored to 432x332 exactly.
    
    Its own suite, and a skip rather than a failure when Xvfb is missing: a test
    tree cannot require a build machine to have an X server.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
    Claude-Session: https://claude.ai/code/session_01FtoiXoSKUmZb6Aix6U3GZS
---
 src/tests/wayland/meson.build                 |  24 +++
 src/tests/wayland/run-x11.sh                  | 170 +++++++++++++++++++
 src/tests/wayland/test_x11_maximize_restore.c | 233 ++++++++++++++++++++++++++
 3 files changed, 427 insertions(+)

diff --git a/src/tests/wayland/meson.build b/src/tests/wayland/meson.build
index 91fadd779..862616ef0 100644
--- a/src/tests/wayland/meson.build
+++ b/src/tests/wayland/meson.build
@@ -172,3 +172,27 @@ foreach b: ['firefox', 'chromium', 'brave']
     timeout: 300,
   )
 endforeach
+
+# The X11 second opinion.
+#
+# e_client.c is shared between the X11 compositor and the Wayland one, and the
+# maximize/restore question above is a question about that shared file. Asking
+# it on one backend only is how a fix for one lands as a regression in the
+# other with nothing to say so.
+#
+# Its own suite because it needs Xvfb, which is not something this tree can
+# require of a build machine - run-x11.sh exits 77 when it is missing, and
+# meson reports that as a skip.
+dep_x11 = dependency('x11', required: false)
+if dep_x11.found()
+  test('x11-maximize-restore',
+    find_program('run-x11.sh'),
+    args: [executable('test_x11_maximize_restore',
+      'test_x11_maximize_restore.c',
+      dependencies: [dep_x11],
+    )],
+    env: ['E_TEST_BIN=' + e_test_bin],
+    suite  : 'x11',
+    timeout: 300,
+  )
+endif
diff --git a/src/tests/wayland/run-x11.sh b/src/tests/wayland/run-x11.sh
new file mode 100755
index 000000000..be27bea74
--- /dev/null
+++ b/src/tests/wayland/run-x11.sh
@@ -0,0 +1,170 @@
+#!/bin/sh
+# Run an X11 client against a throwaway Enlightenment running as an X11 window
+# manager.
+#
+#   run-x11.sh <client> [args...]
+#
+# Environment:
+#   E_TEST_BIN      path to the installed enlightenment binary (required)
+#   E_TEST_TIMEOUT  seconds to wait for E to claim the screen (default 60)
+#   E_TEST_KEEP     set to 1 to keep the temp dir and print E's log path
+#   E_TEST_SCREEN   Xvfb screen geometry (default 1024x768x24)
+#
+# The Wayland sibling of this script is run-nested.sh, and the two are separate
+# on purpose rather than one script with a mode flag. Almost nothing is shared:
+# there is no wl_test back door here (an X11 client can read its own geometry
+# straight from the server, so the whole staging dance is unnecessary), no
+# socket to discover, and the readiness condition is a root window property
+# rather than a unix socket appearing.
+#
+# What this exists for: e_client.c is shared between the X11 compositor and the
+# Wayland one, and some of the questions the Wayland tests ask are questions
+# about that shared code. Asking them on only one backend is how a fix for one
+# lands as a regression in the other. This is the second opinion.
+#
+# Always Xvfb, never the caller's DISPLAY. E takes over the screen it is given -
+# it claims the WM selection, reparents every window and grabs keys - so
+# pointing this at a live session would fight the developer's own compositor
+# for it. run-nested.sh can inherit a DISPLAY because it is a *client* of it;
+# this one cannot.
+
+set -eu
+
+if [ $# -lt 1 ]; then
+    echo "usage: $0 <client> [args...]" >&2
+    exit 2
+fi
+
+E_BIN=${E_TEST_BIN:-}
+if [ -z "$E_BIN" ] || [ ! -x "$E_BIN" ]; then
+    echo "run-x11.sh: E_TEST_BIN is unset or not executable: '${E_BIN}'" >&2
+    echo "run-x11.sh: the tests run against an *installed* tree." >&2
+    echo "run-x11.sh: run 'ninja -C build install' first." >&2
+    exit 1
+fi
+
+if ! command -v Xvfb >/dev/null 2>&1; then
+    echo "run-x11.sh: no Xvfb; install xorg-server-xvfb" >&2
+    exit 77
+fi
+
+TIMEOUT=${E_TEST_TIMEOUT:-60}
+E_PREFIX=$(cd "$(dirname "$E_BIN")/.." && pwd)
+if [ ! -d "$E_PREFIX/share/enlightenment" ]; then
+    echo "run-x11.sh: no share/enlightenment under '$E_PREFIX'" >&2
+    exit 1
+fi
+
+RUNDIR=$(mktemp -d "${TMPDIR:-/tmp}/e-x11-test.XXXXXX")
+chmod 0700 "$RUNDIR"
+E_LOG="$RUNDIR/enlightenment.log"
+
+X_PID=""
+E_PID=""
+
+cleanup() {
+    status=$?
+    if [ -n "$E_PID" ] && kill -0 "$E_PID" 2>/dev/null; then
+        kill -TERM "$E_PID" 2>/dev/null || :
+        i=0
+        while [ $i -lt 30 ] && kill -0 "$E_PID" 2>/dev/null; do
+            i=$((i + 1))
+            sleep 0.1
+        done
+        kill -KILL "$E_PID" 2>/dev/null || :
+        wait "$E_PID" 2>/dev/null || :
+    fi
+    [ -n "$X_PID" ] && { kill "$X_PID" 2>/dev/null || :; }
+    if [ "${E_TEST_KEEP:-0}" = "1" ]; then
+        echo "run-x11.sh: kept $RUNDIR (compositor log: $E_LOG)" >&2
+    else
+        # '|| :' for the reason spelt out at length in run-nested.sh: rm racing
+        # E's helpers is a top-level command under set -e, and losing that race
+        # would end the shell three statements before the exit that reports the
+        # client's status.
+        i=0
+        while [ $i -lt 20 ]; do
+            rm -rf "$RUNDIR" 2>/dev/null || :
+            [ -d "$RUNDIR" ] || break
+            i=$((i + 1))
+            sleep 0.1
+        done
+    fi
+    exit $status
+}
+trap cleanup EXIT INT TERM
+
+# A display number nobody else is on. :0-:9 are real sessions.
+X_DISPLAY=":$((70 + $$ % 20))"
+Xvfb "$X_DISPLAY" -screen 0 "${E_TEST_SCREEN:-1024x768x24}" \
+    >"$RUNDIR/xvfb.log" 2>&1 &
+X_PID=$!
+
+i=0
+while [ $i -lt 200 ]; do
+    [ -S "/tmp/.X11-unix/X${X_DISPLAY#:}" ] && break
+    i=$((i + 1))
+    sleep 0.05
+done
+if [ ! -S "/tmp/.X11-unix/X${X_DISPLAY#:}" ]; then
+    echo "run-x11.sh: Xvfb never came up on $X_DISPLAY" >&2
+    cat "$RUNDIR/xvfb.log" >&2 || :
+    exit 1
+fi
+
+# Same isolation as run-nested.sh, and for the same reasons: a private HOME so
+# nothing touches the developer's ~/.e, and the wltest profile so E comes up
+# with a known module list instead of the wizard.
+DISPLAY="$X_DISPLAY" \
+XDG_RUNTIME_DIR="$RUNDIR" \
+E_PREFIX="$E_PREFIX" \
+E_BIN_DIR="$E_PREFIX/bin" \
+E_LIB_DIR="$E_PREFIX/lib" \
+E_DATA_DIR="$E_PREFIX/share/enlightenment" \
+E_LOCALE_DIR="$E_PREFIX/share/locale" \
+E_CONF_PROFILE=wltest \
+E_CONF_PROFILE_NOSAVE=1 \
+E_TEST_NO_DIALOGS=1 \
+E_HOME="$RUNDIR/.e" \
+HOME="$RUNDIR" \
+    "$E_BIN" >"$E_LOG" 2>&1 &
+E_PID=$!
+
+# Readiness is E owning the screen, not E having started. _NET_SUPPORTING_WM_CHECK
+# is the EWMH handshake and it is the first moment a client can be managed - poll
+# for it rather than sleeping, exactly as the Wayland side polls for its socket.
+elapsed=0
+ready=0
+while [ "$elapsed" -lt "$TIMEOUT" ]; do
+    if ! kill -0 "$E_PID" 2>/dev/null; then
+        echo "run-x11.sh: E exited before claiming the screen" >&2
+        cat "$E_LOG" >&2 || :
+        exit 1
+    fi
+    if DISPLAY="$X_DISPLAY" xprop -root _NET_SUPPORTING_WM_CHECK 2>/dev/null \
+        | grep -q "window id"; then
+        ready=1
+        break
+    fi
+    elapsed=$((elapsed + 1))
+    sleep 1
+done
+
+if [ "$ready" != "1" ]; then
+    echo "run-x11.sh: E did not claim the screen within ${TIMEOUT}s" >&2
+    cat "$E_LOG" >&2 || :
+    exit 1
+fi
+
+set +e
+DISPLAY="$X_DISPLAY" XDG_RUNTIME_DIR="$RUNDIR" HOME="$RUNDIR" "$@"
+client_status=$?
+set -e
+
+if [ $client_status -ne 0 ]; then
+    echo "run-x11.sh: client '$1' exited $client_status" >&2
+    echo "--- compositor log ---" >&2
+    cat "$E_LOG" >&2 || :
+fi
+
+exit $client_status
diff --git a/src/tests/wayland/test_x11_maximize_restore.c b/src/tests/wayland/test_x11_maximize_restore.c
new file mode 100644
index 000000000..4990ec412
--- /dev/null
+++ b/src/tests/wayland/test_x11_maximize_restore.c
@@ -0,0 +1,233 @@
+/* The X11 half of "a window with a shadow must come back the size it was".
+ *
+ * test_maximize_restore.c asks this of a Wayland client whose surface is bigger
+ * than its window geometry. The saving and restoring both happen in
+ * e_client.c, which the X11 compositor shares, so the same question has to be
+ * asked here before anyone edits that file - a fix aimed at one backend is
+ * otherwise free to be a regression in the other, and nothing would say so.
+ *
+ * The X11 spelling of "my surface is bigger than my window" is
+ * _GTK_FRAME_EXTENTS: the window really is 432x332, and the property declares
+ * that 16 pixels on each side of it are invisible shadow, so the frame a user
+ * sees is 400x300. E honours it (e_comp_x.c, ATM_GTK_FRAME_EXTENTS) - that is
+ * what makes this the analogue rather than merely a similar test.
+ *
+ * Note what is *not* claimed here. On X11 the client window is the whole thing,
+ * shadow included, so ec->client.w is the honest size of it and saving that is
+ * correct. This test is therefore expected to pass today, and its job is to
+ * keep passing: it is the guard rail for the Wayland fix, not a reproducer.
+ *
+ * No test back door: an X11 client can ask the server about its own window, so
+ * everything asserted here is read straight from X.
+ */
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+#include <X11/Xlib.h>
+#include <X11/Xatom.h>
+
+#define PROG "test-x11-maximize-restore"
+
+#define SHADOW 16
+#define FRAME_W 400
+#define FRAME_H 300
+#define WIN_W (FRAME_W + (2 * SHADOW))
+#define WIN_H (FRAME_H + (2 * SHADOW))
+
+#define SETTLE_MS 15000
+
+static Display *dpy;
+static Window win;
+static Atom a_state, a_max_h, a_max_v, a_gtk_extents;
+
+static void fail(const char *fmt, ...) __attribute__((noreturn, format(printf, 1, 2)));
+
+static void
+fail(const char *fmt, ...)
+{
+   va_list ap;
+
+   fprintf(stderr, PROG ": ");
+   va_start(ap, fmt);
+   vfprintf(stderr, fmt, ap);
+   va_end(ap);
+   fprintf(stderr, "\n");
+   exit(1);
+}
+
+static double
+now_ms(void)
+{
+   struct timespec ts;
+
+   clock_gettime(CLOCK_MONOTONIC, &ts);
+   return (ts.tv_sec * 1000.0) + (ts.tv_nsec / 1000000.0);
+}
+
+static void
+geometry_get(int *w, int *h)
+{
+   Window root;
+   int x, y;
+   unsigned int uw, uh, bw, depth;
+
+   XSync(dpy, False);
+   if (!XGetGeometry(dpy, win, &root, &x, &y, &uw, &uh, &bw, &depth))
+     fail("XGetGeometry failed - the window is gone");
+   *w = (int)uw;
+   *h = (int)uh;
+}
+
+/* Is the window maximized, according to the property the window manager owns? */
+static int
+maximized_get(void)
+{
+   Atom type;
+   int fmt, i, found = 0;
+   unsigned long n, after;
+   unsigned char *data = ""
+   Atom *atoms;
+
+   if (XGetWindowProperty(dpy, win, a_state, 0, 32, False, XA_ATOM,
+                          &type, &fmt, &n, &after, &data) != Success)
+     return 0;
+   if (!data) return 0;
+
+   atoms = (Atom *)data;
+   for (i = 0; i < (int)n; i++)
+     if ((atoms[i] == a_max_h) || (atoms[i] == a_max_v)) found++;
+   XFree(data);
+
+   return found >= 2;
+}
+
+/* _NET_WM_STATE client message: 1 add, 0 remove. The EWMH way for a client to
+ * ask its window manager to maximize it - the X11 counterpart of
+ * xdg_toplevel.set_maximized. */
+static void
+maximize_request(int on)
+{
+   XEvent ev;
+
+   memset(&ev, 0, sizeof(ev));
+   ev.xclient.type = ClientMessage;
+   ev.xclient.window = win;
+   ev.xclient.message_type = a_state;
+   ev.xclient.format = 32;
+   ev.xclient.data.l[0] = on ? 1 : 0;
+   ev.xclient.data.l[1] = (long)a_max_h;
+   ev.xclient.data.l[2] = (long)a_max_v;
+   ev.xclient.data.l[3] = 1;    /* source: normal application */
+
+   XSendEvent(dpy, DefaultRootWindow(dpy), False,
+              SubstructureNotifyMask | SubstructureRedirectMask, &ev);
+   XFlush(dpy);
+}
+
+static void
+wait_maximized(int want, const char *what)
+{
+   double deadline = now_ms() + SETTLE_MS;
+
+   while (now_ms() < deadline)
+     {
+        if (maximized_get() == want) return;
+        /* Drain whatever the server has for us so the property read is not
+         * looking at a stale connection, then give E a moment of real time -
+         * unlike the Wayland side there is no round trip through E's main loop
+         * available to a plain X client. */
+        while (XPending(dpy))
+          {
+             XEvent ev;
+
+             XNextEvent(dpy, &ev);
+          }
+        usleep(50 * 1000);
+     }
+   fail("waited %dms for %s and it never happened", SETTLE_MS, what);
+}
+
+int
+main(void)
+{
+   int w0, h0, mw, mh, w1, h1;
+   long extents[4] = { SHADOW, SHADOW, SHADOW, SHADOW };
+   XSetWindowAttributes attr;
+
+   dpy = XOpenDisplay(NULL);
+   if (!dpy) fail("cannot open DISPLAY - run me under run-x11.sh");
+
+   a_state = XInternAtom(dpy, "_NET_WM_STATE", False);
+   a_max_h = XInternAtom(dpy, "_NET_WM_STATE_MAXIMIZED_HORZ", False);
+   a_max_v = XInternAtom(dpy, "_NET_WM_STATE_MAXIMIZED_VERT", False);
+   a_gtk_extents = XInternAtom(dpy, "_GTK_FRAME_EXTENTS", False);
+
+   attr.event_mask = StructureNotifyMask | PropertyChangeMask;
+   win = XCreateWindow(dpy, DefaultRootWindow(dpy), 100, 100, WIN_W, WIN_H, 0,
+                       CopyFromParent, InputOutput, CopyFromParent,
+                       CWEventMask, &attr);
+
+   /* Declare the shadow before mapping, the way a GTK client does: the manager
+    * has to know about it from the first time it looks. */
+   XChangeProperty(dpy, win, a_gtk_extents, XA_CARDINAL, 32, PropModeReplace,
+                   (unsigned char *)extents, 4);
+   XStoreName(dpy, win, "x11 maximize restore");
+
+   XMapWindow(dpy, win);
+   XFlush(dpy);
+
+   /* Managed, and settled. A window manager reparents and may resize, so the
+    * baseline is whatever it has decided on rather than what was asked for. */
+   {
+      double deadline = now_ms() + SETTLE_MS;
+      int mapped = 0;
+
+      while ((now_ms() < deadline) && (!mapped))
+        {
+           XEvent ev;
+
+           while (XPending(dpy))
+             {
+                XNextEvent(dpy, &ev);
+                if (ev.type == MapNotify) mapped = 1;
+             }
+           usleep(50 * 1000);
+        }
+      if (!mapped) fail("the window was never mapped in %dms", SETTLE_MS);
+   }
+   usleep(500 * 1000);
+
+   geometry_get(&w0, &h0);
+   printf(PROG ": mapped at %dx%d (declared %dpx of shadow a side, so a "
+          "%dx%d frame)\n", w0, h0, SHADOW, w0 - (2 * SHADOW), h0 - (2 * SHADOW));
+
+   maximize_request(1);
+   wait_maximized(1, "the window manager to maximize us");
+   usleep(500 * 1000);
+   geometry_get(&mw, &mh);
+   if ((mw <= w0) && (mh <= h0))
+     fail("maximized to %dx%d, no bigger than the %dx%d it started at",
+          mw, mh, w0, h0);
+   printf(PROG ": maximized to %dx%d\n", mw, mh);
+
+   maximize_request(0);
+   wait_maximized(0, "the window manager to restore us");
+   usleep(500 * 1000);
+   geometry_get(&w1, &h1);
+
+   if ((w1 != w0) || (h1 != h0))
+     fail("restored to %dx%d, but it was %dx%d before maximizing. The shadow "
+          "is %d a side - a difference of %d in each axis means the saved size "
+          "and the restored one disagree about whether it is included",
+          w1, h1, w0, h0, SHADOW, 2 * SHADOW);
+
+   printf(PROG ": restored to %dx%d\n", w1, h1);
+   printf(PROG ": ok\n");
+
+   XDestroyWindow(dpy, win);
+   XCloseDisplay(dpy);
+   return 0;
+}

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.

Reply via email to