summaryrefslogtreecommitdiffhomepage
path: root/packages/ext-layer-shell/tests/test_client.cpp
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-12 22:44:16 +0900
committerAdam Malczewski <[email protected]>2026-06-12 22:44:16 +0900
commitc102a1b67a70149b6f9c9b2cfd8b31ceb52c09b7 (patch)
treef6dea2875b939c0f661292d8bfa0d79a96fe67d7 /packages/ext-layer-shell/tests/test_client.cpp
parent6949c3582ed1e480e70aabfcfa3a11b78007cc12 (diff)
downloadunbox-c102a1b67a70149b6f9c9b2cfd8b31ceb52c09b7.tar.gz
unbox-c102a1b67a70149b6f9c9b2cfd8b31ceb52c09b7.zip
Slice 4: extension host + typed bus; xdg-shell/layer-shell extracted to core extensions
The kernel now names NO concrete feature. It owns: the extension host (install/topological activate, missing-dep/cycle = startup error), the typed Event/Filter bus (error-isolated: a throwing extension is disabled, never the session; RAII Subscriptions), the Host API (per-extension facade: borrows, scene layers, event catalogue, typed services), the public RAII Listener, and a typed surface→scene-tree registry (Host::host_surface/scene_tree_for) that replaced the untyped wlr_surface.data convention both extensions flagged. - ext-xdg-shell (core): toplevel/popup lifecycle, focus-on-map, click/tap-to-focus, pointer/touch routing incl. button+axis (the kernel only moves the cursor and emits — a contract-doc lie caught by user hands-on), interactive move/resize via pure GrabMachine (fixes the request-arrives-after-release race: grab requires request ∧ button-down, release always ends it), Alt+F1 cycle, Ctrl+Alt+Backspace terminate (labwc's default A-Escape=Exit killed the dev session once — never again; see nested-run skill). - ext-layer-shell (core): wlr-layer-shell v1 (proto v5) for external clients; pure doctest-hard arrangement core; fuzzel verified visually nested (fix: seed outputs from output_layout at activate — events-only tracking missed pre-activation outputs; plus a scene-node double-free). - First protocol codegen: vendored wlr-layer-shell XML + wayland-scanner server-header propagated through kernel_dep; wlr.hpp grew a namespace→_namespace keyword fix for the generated header. - Glossary: 'scene layer' (user-approved). New rules earned: parallel-wave-builds, contract-docs. - User hands-on verified: typing, click-to-focus, drag-select, scroll, titlebar drag-move (slow + flick), Alt+F1, fuzzel + arrows, touch tap, Ctrl+Alt+Backspace. 68 doctest cases green, ASan/UBSan clean (our code), idle RSS ≈73 MiB.
Diffstat (limited to 'packages/ext-layer-shell/tests/test_client.cpp')
-rw-r--r--packages/ext-layer-shell/tests/test_client.cpp174
1 files changed, 174 insertions, 0 deletions
diff --git a/packages/ext-layer-shell/tests/test_client.cpp b/packages/ext-layer-shell/tests/test_client.cpp
new file mode 100644
index 0000000..5c1d232
--- /dev/null
+++ b/packages/ext-layer-shell/tests/test_client.cpp
@@ -0,0 +1,174 @@
+#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
+#include <doctest/doctest.h>
+
+#include <unbox/ext-layer-shell/ext_layer_shell.hpp>
+#include <unbox/kernel/server.hpp>
+
+#include <wayland-client.h>
+
+// The generated client header (and code) name a request argument `namespace` —
+// a C identifier but a C++ keyword. Same scoped-macro fix wlr.hpp documents for
+// the server side: rename it across exactly these two generated includes. The
+// only call that touches it (get_layer_surface's last arg) is a string literal
+// we pass positionally, so the rename never leaks into our own code.
+#define namespace namespace_
+#include "wlr-layer-shell-unstable-v1-client-protocol.h"
+// xdg-shell marshalling code: supplies xdg_popup_interface, referenced by the
+// layer-shell tables (get_popup). Code-form only; we use no xdg-shell requests.
+#include "xdg-shell-client-protocol-code.h"
+// The protocol marshalling tables/stubs (wayland-scanner private-code), emitted
+// as a header so it can be compiled by the C++ TU exactly once (see meson.build).
+#include "wlr-layer-shell-unstable-v1-client-protocol-code.h"
+#undef namespace
+
+#include <cstdlib>
+#include <cstring>
+#include <memory>
+#include <poll.h>
+
+// REGRESSION: a real wayland CLIENT (the fuzzel reproduction, in-process) binds
+// zwlr_layer_shell_v1, creates a layer surface with a NIL output, and MUST
+// receive a configure. Before the fix, ext-layer-shell's output tracking was
+// events-only: the headless output created during Server::create() (BEFORE
+// activation) was never tracked, so an output-less surface got no output, no
+// wlr_scene_layer_surface_v1_configure, no configure event — the exact symptom
+// in /tmp/opencode/fuzzel-trace.log (get_layer_surface(... nil ...) then a bare
+// closed() with no configure). This test would have caught it.
+//
+// Server and client share this single thread; we pump both loops cooperatively
+// (the standard libwayland prepare_read / read_events dance, non-blocking).
+
+namespace {
+
+struct Client {
+ wl_display* display = nullptr;
+ wl_registry* registry = nullptr;
+ wl_compositor* compositor = nullptr;
+ wl_output* output = nullptr;
+ zwlr_layer_shell_v1* layer_shell = nullptr;
+
+ wl_surface* surface = nullptr;
+ zwlr_layer_surface_v1* layer_surface = nullptr;
+
+ bool configured = false;
+ bool closed = false;
+ std::uint32_t configure_serial = 0;
+};
+
+void registry_global(void* data, wl_registry* reg, std::uint32_t name,
+ const char* iface, std::uint32_t version) {
+ auto* c = static_cast<Client*>(data);
+ if (std::strcmp(iface, wl_compositor_interface.name) == 0) {
+ c->compositor = static_cast<wl_compositor*>(
+ wl_registry_bind(reg, name, &wl_compositor_interface, 4));
+ } else if (std::strcmp(iface, wl_output_interface.name) == 0 &&
+ c->output == nullptr) {
+ c->output = static_cast<wl_output*>(
+ wl_registry_bind(reg, name, &wl_output_interface, version));
+ } else if (std::strcmp(iface, zwlr_layer_shell_v1_interface.name) == 0) {
+ c->layer_shell = static_cast<zwlr_layer_shell_v1*>(wl_registry_bind(
+ reg, name, &zwlr_layer_shell_v1_interface,
+ version < 4 ? version : 4));
+ }
+}
+void registry_global_remove(void*, wl_registry*, std::uint32_t) {}
+
+const wl_registry_listener kRegistryListener{registry_global,
+ registry_global_remove};
+
+void ls_configure(void* data, zwlr_layer_surface_v1* ls, std::uint32_t serial,
+ std::uint32_t, std::uint32_t) {
+ auto* c = static_cast<Client*>(data);
+ c->configured = true;
+ c->configure_serial = serial;
+ zwlr_layer_surface_v1_ack_configure(ls, serial);
+}
+void ls_closed(void* data, zwlr_layer_surface_v1*) {
+ static_cast<Client*>(data)->closed = true;
+}
+const zwlr_layer_surface_v1_listener kLayerSurfaceListener{ls_configure,
+ ls_closed};
+
+// Pump server and client once, without blocking the client read.
+void pump(unbox::kernel::Server& server, wl_display* client) {
+ wl_display_flush(client);
+ server.dispatch(5);
+
+ // Drain anything already queued, then do a guarded non-blocking read.
+ while (wl_display_prepare_read(client) != 0) {
+ wl_display_dispatch_pending(client);
+ }
+ wl_display_flush(client);
+
+ pollfd pfd{wl_display_get_fd(client), POLLIN, 0};
+ if (poll(&pfd, 1, 5) > 0 && (pfd.revents & POLLIN)) {
+ wl_display_read_events(client);
+ } else {
+ wl_display_cancel_read(client);
+ }
+ wl_display_dispatch_pending(client);
+}
+
+auto make_headless_server() -> std::unique_ptr<unbox::kernel::Server> {
+ setenv("WLR_BACKENDS", "headless", 1);
+ setenv("WLR_RENDERER", "pixman", 1);
+ setenv("WLR_HEADLESS_OUTPUTS", "1", 1); // output exists BEFORE activation
+ return unbox::kernel::Server::create({});
+}
+
+} // namespace
+
+TEST_CASE("a real client's nil-output layer surface receives a configure") {
+ auto server = make_headless_server();
+ server->install(unbox::ext_layer_shell::create());
+ server->activate_extensions();
+ REQUIRE(!server->socket_name().empty());
+
+ Client c;
+ c.display = wl_display_connect(server->socket_name().c_str());
+ REQUIRE(c.display != nullptr);
+ c.registry = wl_display_get_registry(c.display);
+ wl_registry_add_listener(c.registry, &kRegistryListener, &c);
+
+ // Round 1: let the registry advertise globals and our binds reach the
+ // server (compositor, output, layer_shell).
+ for (int i = 0; i < 50 && (c.compositor == nullptr || c.layer_shell == nullptr);
+ ++i) {
+ pump(*server, c.display);
+ }
+ REQUIRE(c.compositor != nullptr);
+ REQUIRE(c.layer_shell != nullptr);
+
+ // Create a layer surface with a NIL output (exactly fuzzel's get_layer_surface
+ // call: output=nil, layer=overlay, namespace="launcher").
+ c.surface = wl_compositor_create_surface(c.compositor);
+ c.layer_surface = zwlr_layer_shell_v1_get_layer_surface(
+ c.layer_shell, c.surface, /*output=*/nullptr,
+ ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY, "launcher");
+ REQUIRE(c.layer_surface != nullptr);
+ zwlr_layer_surface_v1_add_listener(c.layer_surface, &kLayerSurfaceListener, &c);
+ zwlr_layer_surface_v1_set_size(c.layer_surface, 382, 386);
+ zwlr_layer_surface_v1_set_anchor(c.layer_surface, 0);
+ // The mandatory initial empty commit (no buffer): the compositor MUST reply
+ // with a configure.
+ wl_surface_commit(c.surface);
+
+ // Round 2: pump until the configure arrives (or we give up -> the bug).
+ for (int i = 0; i < 200 && !c.configured && !c.closed; ++i) {
+ pump(*server, c.display);
+ }
+
+ CHECK(c.configured); // the fix: a configure was sent
+ CHECK_FALSE(c.closed); // the bug: surface closed with no configure
+ CHECK(c.configure_serial != 0);
+
+ if (c.layer_surface != nullptr) {
+ zwlr_layer_surface_v1_destroy(c.layer_surface);
+ }
+ if (c.surface != nullptr) {
+ wl_surface_destroy(c.surface);
+ }
+ wl_display_flush(c.display);
+ pump(*server, c.display);
+ wl_display_disconnect(c.display);
+}