summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorBerke Güzel <wenekar1@gmail.com>2026-10-13 23:52:22 +0300
committerBerke Güzel <wenekar1@gmail.com>2026-10-13 23:52:22 +0300
commit563b23a33726289387193b85d90fb569631ee05f (patch)
treeddf438338034b5ab2690b531684906999e56df44 /tests
parent76073fe5fad8799e5850805f2f090af2cb62c741 (diff)
Reconnect to a restarted Wayland compositor
Windows now outlive the compositor. On connection loss YASL sends DISPLAY_LOST instead of closing every window, retries the connection on a loop timer, rebuilds each toplevel with its title, size, state and GL context, and sends DISPLAY_RESTORED. CPU windows keep their pixels; GPU renderers drop Vulkan surfaces and GL objects on loss and rebuild them on restore. - Free the dead connection's objects on the next wait, after the application has released GPU objects built on them. - Report a lost connection as an event rather than a failed wait. - Refuse window and Vulkan surface creation while offline with the new YASL_ERROR_DISCONNECTED. - Close popups, cancel drags and fail pending clipboard requests on loss. - Print only protocol errors, not ordinary disconnects. - Examples survive a restart; game.c rebinds its GL context. - Add an end-to-end test that kills Weston under CPU, GL and Vulkan windows and requires them to recover. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'tests')
-rw-r--r--tests/helpers/run_reconnect.py67
-rw-r--r--tests/meson.build5
-rw-r--r--tests/wayland/test_reconnect.c118
3 files changed, 190 insertions, 0 deletions
diff --git a/tests/helpers/run_reconnect.py b/tests/helpers/run_reconnect.py
new file mode 100644
index 0000000..e86e994
--- /dev/null
+++ b/tests/helpers/run_reconnect.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python3
+"""Kill Weston under a running client, start a new one on the same socket, and
+require the client to reconnect: run_reconnect.py <weston> <client>."""
+import os
+from pathlib import Path
+import signal
+import subprocess
+import sys
+import tempfile
+import time
+
+
+def start_weston(weston, root, env, log):
+ process = subprocess.Popen([weston, '--backend=headless', '--renderer=gl', '--socket=yasl-test',
+ '--idle-time=0', '--no-config', '--width=1024', '--height=768'],
+ env=env, stdout=log, stderr=log)
+ deadline = time.monotonic() + 12
+ while not (root/'yasl-test').is_socket():
+ if process.poll() is not None or time.monotonic() > deadline:
+ raise RuntimeError('Weston did not start')
+ time.sleep(0.02)
+ return process
+
+
+def expect(client, word):
+ line = client.stdout.readline().strip()
+ if line != word:
+ raise RuntimeError(f'client printed {line!r}, expected {word!r}')
+
+
+def main():
+ weston, binary = sys.argv[1:]
+ signal.alarm(50)
+ with tempfile.TemporaryDirectory(prefix='yasl-reconnect-') as directory:
+ root = Path(directory)
+ env = os.environ.copy()
+ env.pop('WAYLAND_SOCKET', None)
+ env.update(XDG_RUNTIME_DIR=directory, WAYLAND_DISPLAY='yasl-test', LIBGL_ALWAYS_SOFTWARE='1')
+ lavapipe = next(Path('/usr/share/vulkan/icd.d').glob('*lvp*.json'), None)
+ if lavapipe:
+ env['VK_DRIVER_FILES'] = str(lavapipe)
+ with (root/'weston.log').open('w+') as log:
+ servers = [start_weston(weston, root, env, log)]
+ client = subprocess.Popen([binary], env=env, stdout=subprocess.PIPE, text=True)
+ try:
+ expect(client, 'ready')
+ servers[0].send_signal(signal.SIGKILL)
+ servers[0].wait()
+ expect(client, 'lost')
+ time.sleep(0.5)
+ servers.append(start_weston(weston, root, env, log))
+ expect(client, 'restored')
+ if client.wait(timeout=15) != 0:
+ raise RuntimeError(f'client exited with {client.returncode}')
+ except BaseException:
+ client.kill()
+ log.flush(); log.seek(0); print(log.read(), file=sys.stderr)
+ raise
+ finally:
+ for server in servers:
+ if server.poll() is None:
+ server.terminate()
+ server.wait(timeout=5)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tests/meson.build b/tests/meson.build
index 4cbf082..d61d094 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -93,6 +93,11 @@ if get_option('integration_tests')
xwayland = find_program('Xwayland')
test('surface-backends', python, args: [files('helpers/run_surfaces.py'), weston, xwayland,
integration, hello_example, tablet_example, gl_integration, vulkan_integration, notepad_example], timeout: 120)
+ if has_wayland and has_egl
+ reconnect = executable('yasl-reconnect-test', 'wayland/test_reconnect.c', dependencies: yasl_dep,
+ c_args: ['-DTEST_VULKAN=' + (has_vulkan ? '1' : '0')])
+ test('reconnect-wayland', python, args: [files('helpers/run_reconnect.py'), weston, reconnect], timeout: 60)
+ endif
elif has_cocoa
test('surface-cocoa', integration, args: ['cocoa'], timeout: 30)
foreach unit : ['native', 'ownership', 'keyboard', 'tablet', 'cursor']
diff --git a/tests/wayland/test_reconnect.c b/tests/wayland/test_reconnect.c
new file mode 100644
index 0000000..0c42043
--- /dev/null
+++ b/tests/wayland/test_reconnect.c
@@ -0,0 +1,118 @@
+/* Compositor restart: run by helpers/run_reconnect.py, which kills the compositor
+ * after "ready" and starts a new one after "lost". Windows must survive, keep
+ * their pixels, and accept new GL and Vulkan objects after DISPLAY_RESTORED. */
+#include <yasl/yasl.h>
+#include <yasl/cpu.h>
+#include <yasl/gl.h>
+#if TEST_VULKAN
+#include <yasl/vulkan.h>
+#endif
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define CHECK(cond, what) do { if (!(cond)) { const char *why; yasl_get_error(&why); \
+ fprintf(stderr, "reconnect: %s failed at line %d (%s)\n", what, __LINE__, why); exit(1); } } while (0)
+
+typedef void (*clear_color_fn)(float, float, float, float);
+typedef void (*clear_fn)(unsigned);
+
+static yasl_surface *cpu, *gl;
+
+/* Waits up to timeout_ms for an event of the given type, failing on any CLOSE. */
+static yasl_event expect(enum yasl_event_type type, yasl_surface *surface, int timeout_ms)
+{
+ yasl_event e;
+ for (int waited = 0; waited < timeout_ms; waited += 50) {
+ CHECK(yasl_wait_event(&e, 50) >= 0, "wait");
+ CHECK(e.type != YASL_EVENT_CLOSE, "keeping windows open");
+ if (e.type == type && (!surface || e.surface == surface)) return e;
+ }
+ fprintf(stderr, "reconnect: timed out waiting for event %d\n", type);
+ exit(1);
+}
+
+static void draw_gl(float red)
+{
+ CHECK(yasl_gl_make_current(gl), "binding GL");
+ clear_color_fn clear_color = (clear_color_fn)yasl_gl_get_proc_address("glClearColor");
+ clear_fn clear = (clear_fn)yasl_gl_get_proc_address("glClear");
+ CHECK(clear_color && clear, "loading GL procedures");
+ clear_color(red, 0.2f, 0.3f, 1);
+ clear(0x00004000u);
+ CHECK(yasl_gl_swap_buffers(gl), "swapping GL buffers");
+}
+
+#if TEST_VULKAN
+static VkInstance instance;
+static yasl_surface *vk;
+
+static VkSurfaceKHR vulkan_surface(void)
+{
+ VkSurfaceKHR out = VK_NULL_HANDLE;
+ CHECK(yasl_vulkan_create_surface(vk, instance, NULL, &out) == VK_SUCCESS, "creating Vulkan surface");
+ return out;
+}
+#endif
+
+int main(void)
+{
+ CHECK(yasl_set_backend(YASL_BACKEND_WAYLAND), "selecting Wayland");
+ cpu = yasl_surface_create("reconnect CPU", 160, 120, YASL_SURFACE_CPU);
+ CHECK(cpu, "creating CPU window");
+ gl = yasl_gl_surface_create("reconnect GL", 160, 120, 0, NULL, NULL);
+ CHECK(gl, "creating GL window");
+
+ expect(YASL_EVENT_REFRESH, cpu, 5000);
+ int w, h;
+ uint32_t *pixels = yasl_cpu_pixels(cpu, &w, &h);
+ CHECK(pixels, "getting pixels");
+ for (size_t i = 0; i < (size_t)w * h; i++) pixels[i] = 0xff336699u;
+ CHECK(yasl_cpu_present(cpu), "presenting");
+ draw_gl(0.8f);
+
+#if TEST_VULKAN
+ CHECK(yasl_vulkan_init(vkGetInstanceProcAddr), "loading Vulkan");
+ vk = yasl_surface_create("reconnect Vulkan", 160, 120, 0);
+ CHECK(vk, "creating Vulkan window");
+ uint32_t count = 0;
+ const char *const *extensions = yasl_vulkan_instance_extensions(vk, &count);
+ VkInstanceCreateInfo info = {.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
+ .enabledExtensionCount = count, .ppEnabledExtensionNames = extensions};
+ CHECK(vkCreateInstance(&info, NULL, &instance) == VK_SUCCESS, "creating Vulkan instance");
+ VkSurfaceKHR surface = vulkan_surface();
+#endif
+
+ puts("ready");
+ fflush(stdout);
+
+ expect(YASL_EVENT_DISPLAY_LOST, NULL, 10000);
+#if TEST_VULKAN
+ vkDestroySurfaceKHR(instance, surface, NULL);
+#endif
+ CHECK(!yasl_surface_create("offline", 64, 64, YASL_SURFACE_CPU), "refusing windows while offline");
+ CHECK(yasl_get_error(NULL) == YASL_ERROR_DISCONNECTED, "reporting the disconnect");
+ puts("lost");
+ fflush(stdout);
+
+ expect(YASL_EVENT_DISPLAY_RESTORED, NULL, 15000);
+ expect(YASL_EVENT_REFRESH, cpu, 5000);
+ int rw, rh;
+ pixels = yasl_cpu_pixels(cpu, &rw, &rh);
+ CHECK(pixels && rw == w && rh == h, "keeping the window size");
+ CHECK(pixels[0] == 0xff336699u && pixels[(size_t)w * h - 1] == 0xff336699u, "keeping the pixels");
+ CHECK(yasl_cpu_present(cpu), "presenting after reconnect");
+ draw_gl(0.1f);
+#if TEST_VULKAN
+ surface = vulkan_surface();
+ vkDestroySurfaceKHR(instance, surface, NULL);
+ vkDestroyInstance(instance, NULL);
+#endif
+
+ /* A second window proves the new connection accepts creation. */
+ CHECK(yasl_surface_create("after reconnect", 64, 64, YASL_SURFACE_CPU), "creating a window after reconnect");
+ yasl_gl_make_current(NULL);
+ yasl_shutdown();
+ puts("restored");
+ return 0;
+}