summaryrefslogtreecommitdiffhomepage
path: root/packages/ext-xdg-shell/tests/test_minimize.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'packages/ext-xdg-shell/tests/test_minimize.cpp')
-rw-r--r--packages/ext-xdg-shell/tests/test_minimize.cpp325
1 files changed, 325 insertions, 0 deletions
diff --git a/packages/ext-xdg-shell/tests/test_minimize.cpp b/packages/ext-xdg-shell/tests/test_minimize.cpp
new file mode 100644
index 0000000..2e60799
--- /dev/null
+++ b/packages/ext-xdg-shell/tests/test_minimize.cpp
@@ -0,0 +1,325 @@
+#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
+#include <doctest/doctest.h>
+
+#include <unbox/ext-xdg-shell/ext_xdg_shell.hpp>
+#include <unbox/kernel/host.hpp>
+#include <unbox/kernel/server.hpp>
+#include <unbox/kernel/wlr.hpp>
+
+#include <wayland-client.h>
+
+// xdg-shell CLIENT bindings, generated from the canonical xdg-shell.xml the
+// same way ext-layer-shell generates its protocol code (a single C++ TU
+// #includes the header + the private-code-as-header exactly once). This lets an
+// in-process real client map an xdg_toplevel so we can drive the slice-10 b1
+// minimize mechanism (hide/show/geometry/scene_tree) end-to-end on the wlr
+// headless backend.
+#include "xdg-shell-client-protocol.h"
+#include "xdg-shell-client-protocol-code.h"
+
+#include <cstdint>
+#include <cstdlib>
+#include <cstring>
+#include <memory>
+#include <poll.h>
+#include <unistd.h>
+
+// slice 10 / b1 — Toplevel minimize mechanism, integration-tested with a real
+// client. We install ext-xdg-shell plus a tiny OBSERVER extension that depends
+// on it, fetches its Service, and captures the live Toplevel* borrow + Host on
+// on_toplevel_mapped (and notes any on_toplevel_unmapped). A real client maps a
+// toplevel; we then exercise hide()/show()/geometry()/scene_tree() against the
+// borrow and assert the scene node enable-bit flips, no unmap fired, geometry
+// is non-empty, and scene_tree() resolves to the same tree as
+// Host::scene_tree_for(the focused surface). Lenient ext-tier glue per AGENTS.md
+// (the strict correctness lives in the pure-core test_policy.cpp).
+
+namespace {
+
+using unbox::ext_xdg_shell::Service;
+using unbox::ext_xdg_shell::Toplevel;
+using unbox::ext_xdg_shell::ToplevelEvent;
+
+// ---- the observer extension (test-only, depends on xdg-shell) --------------
+//
+// Holds onto the kernel Host so the test can resolve scene_tree_for() the way a
+// real consumer (ext-stage-dock) would. Captures the mapped Toplevel borrow;
+// the borrow is valid until on_toplevel_unmapped, which is exactly when we stop
+// touching it. No ownership of anything kernel-owned.
+class Observer final : public unbox::kernel::Extension {
+public:
+ [[nodiscard]] auto manifest() const -> const unbox::kernel::Manifest& override {
+ return manifest_;
+ }
+
+ void activate(unbox::kernel::Host& host) override {
+ host_ = &host;
+ // Typed cross-extension coupling: a missing xdg-shell is a nullptr here
+ // (and a link error on the Service type), never a string lookup.
+ Service* svc = host.service<Service>();
+ REQUIRE(svc != nullptr);
+ sub_mapped_ = host.subscribe(svc->on_toplevel_mapped(),
+ [this](const ToplevelEvent& e) {
+ mapped_ = e.toplevel;
+ ++mapped_count_;
+ });
+ sub_unmapped_ = host.subscribe(svc->on_toplevel_unmapped(),
+ [this](const ToplevelEvent& e) {
+ if (e.toplevel == mapped_) {
+ mapped_ = nullptr;
+ }
+ ++unmapped_count_;
+ });
+ }
+
+ [[nodiscard]] auto host() -> unbox::kernel::Host* { return host_; }
+ [[nodiscard]] auto mapped() -> Toplevel* { return mapped_; }
+ [[nodiscard]] auto mapped_count() const -> int { return mapped_count_; }
+ [[nodiscard]] auto unmapped_count() const -> int { return unmapped_count_; }
+
+private:
+ unbox::kernel::Manifest manifest_{"test-observer", unbox::kernel::Tier::standard,
+ {"xdg-shell"}};
+ unbox::kernel::Host* host_ = nullptr;
+ Toplevel* mapped_ = nullptr;
+ int mapped_count_ = 0;
+ int unmapped_count_ = 0;
+ unbox::kernel::Subscription sub_mapped_;
+ unbox::kernel::Subscription sub_unmapped_;
+};
+
+// ---- minimal xdg-shell client ----------------------------------------------
+
+struct Client {
+ wl_display* display = nullptr;
+ wl_registry* registry = nullptr;
+ wl_compositor* compositor = nullptr;
+ wl_shm* shm = nullptr;
+ xdg_wm_base* wm_base = nullptr;
+
+ wl_surface* surface = nullptr;
+ xdg_surface* xsurface = nullptr;
+ xdg_toplevel* toplevel = nullptr;
+
+ bool configured = false;
+ std::uint32_t last_configure_serial = 0;
+};
+
+void wm_base_ping(void*, xdg_wm_base* wm, std::uint32_t serial) {
+ xdg_wm_base_pong(wm, serial);
+}
+const xdg_wm_base_listener kWmBaseListener{wm_base_ping};
+
+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_shm_interface.name) == 0) {
+ c->shm = static_cast<wl_shm*>(wl_registry_bind(reg, name, &wl_shm_interface, 1));
+ } else if (std::strcmp(iface, xdg_wm_base_interface.name) == 0) {
+ c->wm_base = static_cast<xdg_wm_base*>(wl_registry_bind(
+ reg, name, &xdg_wm_base_interface, version < 3 ? version : 3));
+ xdg_wm_base_add_listener(c->wm_base, &kWmBaseListener, c);
+ }
+}
+void registry_global_remove(void*, wl_registry*, std::uint32_t) {}
+const wl_registry_listener kRegistryListener{registry_global, registry_global_remove};
+
+void xsurface_configure(void* data, xdg_surface* xs, std::uint32_t serial) {
+ auto* c = static_cast<Client*>(data);
+ c->last_configure_serial = serial;
+ xdg_surface_ack_configure(xs, serial);
+ c->configured = true;
+}
+const xdg_surface_listener kXSurfaceListener{xsurface_configure};
+
+void toplevel_configure(void*, xdg_toplevel*, std::int32_t, std::int32_t, wl_array*) {}
+void toplevel_close(void*, xdg_toplevel*) {}
+// Trailing members (configure_bounds, wm_capabilities — xdg-shell v4/v5) are
+// value-initialized to nullptr; we bind at most v3 so they are never invoked.
+const xdg_toplevel_listener kToplevelListener{toplevel_configure, toplevel_close, {}, {}};
+
+// Cooperative single-thread pump (the standard libwayland guarded-read dance).
+void pump(unbox::kernel::Server& server, wl_display* client) {
+ wl_display_flush(client);
+ server.dispatch(5);
+ 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);
+ return unbox::kernel::Server::create({});
+}
+
+// A 256x256 ARGB shm buffer so the surface actually maps (a toplevel maps on
+// its first buffer-bearing commit after the initial configure).
+auto make_buffer(Client& c, int w, int h) -> wl_buffer* {
+ const int stride = w * 4;
+ const int size = stride * h;
+ char name[] = "/tmp/unbox-xdg-test-XXXXXX";
+ int fd = mkstemp(name);
+ REQUIRE(fd >= 0);
+ unlink(name);
+ REQUIRE(ftruncate(fd, size) == 0);
+ REQUIRE(c.shm != nullptr);
+ wl_shm_pool* pool = wl_shm_create_pool(c.shm, fd, size);
+ wl_buffer* buffer =
+ wl_shm_pool_create_buffer(pool, 0, w, h, stride, WL_SHM_FORMAT_ARGB8888);
+ wl_shm_pool_destroy(pool);
+ close(fd);
+ return buffer;
+}
+
+} // namespace
+
+TEST_CASE("slice 10/b1: hide/show/geometry/scene_tree on a real mapped toplevel") {
+ auto server = make_headless_server();
+ server->install(unbox::ext_xdg_shell::create());
+ auto observer_owned = std::make_unique<Observer>();
+ Observer* observer = observer_owned.get();
+ server->install(std::move(observer_owned));
+ 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);
+
+ for (int i = 0; i < 50 &&
+ (c.compositor == nullptr || c.wm_base == nullptr || c.shm == nullptr);
+ ++i) {
+ pump(*server, c.display);
+ }
+ REQUIRE(c.compositor != nullptr);
+ REQUIRE(c.wm_base != nullptr);
+ REQUIRE(c.shm != nullptr);
+
+ // Map a toplevel: create surface -> xdg_surface -> xdg_toplevel, the
+ // mandatory empty initial commit, wait for the configure, then attach a
+ // buffer and commit to actually map it.
+ c.surface = wl_compositor_create_surface(c.compositor);
+ c.xsurface = xdg_wm_base_get_xdg_surface(c.wm_base, c.surface);
+ xdg_surface_add_listener(c.xsurface, &kXSurfaceListener, &c);
+ c.toplevel = xdg_surface_get_toplevel(c.xsurface);
+ xdg_toplevel_add_listener(c.toplevel, &kToplevelListener, &c);
+ xdg_toplevel_set_title(c.toplevel, "unbox-test");
+ wl_surface_commit(c.surface); // initial empty commit -> expect configure
+
+ for (int i = 0; i < 200 && !c.configured; ++i) {
+ pump(*server, c.display);
+ }
+ REQUIRE(c.configured);
+
+ wl_buffer* buffer = make_buffer(c, 256, 256);
+ REQUIRE(buffer != nullptr);
+ wl_surface_attach(c.surface, buffer, 0, 0);
+ wl_surface_damage(c.surface, 0, 0, 256, 256);
+ wl_surface_commit(c.surface);
+
+ // Pump until the server-side map fires and the observer captures the borrow.
+ for (int i = 0; i < 200 && observer->mapped() == nullptr; ++i) {
+ pump(*server, c.display);
+ }
+
+ Toplevel* tl = observer->mapped();
+ REQUIRE(tl != nullptr);
+ REQUIRE(observer->mapped_count() == 1);
+ REQUIRE(observer->unmapped_count() == 0);
+
+ // scene_tree(): non-null, a borrow, and equal to what the kernel registry
+ // resolves the toplevel's surface to (the typed surface->tree contract the
+ // dock relies on). We recover the toplevel's wl_surface by walking the
+ // buffer nodes under the returned tree (the public Toplevel contract does
+ // not expose the surface; the kernel registry keys on it), then confirm the
+ // round-trip scene_tree_for(surface) == scene_tree().
+ wlr_scene_tree* tree = tl->scene_tree();
+ REQUIRE(tree != nullptr);
+ struct SurfaceCatch {
+ wlr_surface* surface = nullptr;
+ } caught;
+ wlr_scene_node_for_each_buffer(
+ &tree->node,
+ [](wlr_scene_buffer* buf, int, int, void* data) {
+ auto* sc = static_cast<SurfaceCatch*>(data);
+ if (sc->surface == nullptr) {
+ if (wlr_scene_surface* ss = wlr_scene_surface_try_from_buffer(buf)) {
+ sc->surface = ss->surface;
+ }
+ }
+ },
+ &caught);
+ REQUIRE(caught.surface != nullptr);
+ CHECK(observer->host()->scene_tree_for(caught.surface) == tree);
+
+ // geometry(): a non-empty box for a mapped toplevel.
+ wlr_box box = tl->geometry();
+ CHECK(box.width > 0);
+ CHECK(box.height > 0);
+
+ // hide(): disables the scene node (no compositing, frame callbacks cease),
+ // WITHOUT unmapping — no on_toplevel_unmapped, client stays mapped.
+ CHECK(tree->node.enabled == true);
+ tl->hide();
+ CHECK(tree->node.enabled == false);
+ tl->hide(); // idempotent
+ CHECK(tree->node.enabled == false);
+ // Pump a few turns: the client must NOT be unmapped by the hide.
+ for (int i = 0; i < 10; ++i) {
+ pump(*server, c.display);
+ }
+ CHECK(observer->unmapped_count() == 0);
+ CHECK(observer->mapped() == tl); // still the live borrow
+
+ // show(): re-enables the node. Idempotent.
+ tl->show();
+ CHECK(tree->node.enabled == true);
+ tl->show();
+ CHECK(tree->node.enabled == true);
+
+ // A hidden toplevel must still unmap normally when the client closes it:
+ // hide it again, then destroy client-side; on_toplevel_unmapped MUST fire.
+ tl->hide();
+ xdg_toplevel_destroy(c.toplevel);
+ xdg_surface_destroy(c.xsurface);
+ wl_surface_destroy(c.surface);
+ c.toplevel = nullptr;
+ c.xsurface = nullptr;
+ c.surface = nullptr;
+ for (int i = 0; i < 200 && observer->unmapped_count() == 0; ++i) {
+ pump(*server, c.display);
+ }
+ CHECK(observer->unmapped_count() == 1);
+
+ wl_buffer_destroy(buffer);
+ if (c.wm_base != nullptr) {
+ xdg_wm_base_destroy(c.wm_base);
+ }
+ if (c.shm != nullptr) {
+ wl_shm_destroy(c.shm);
+ }
+ if (c.compositor != nullptr) {
+ wl_compositor_destroy(c.compositor);
+ }
+ if (c.registry != nullptr) {
+ wl_registry_destroy(c.registry);
+ }
+ wl_display_flush(c.display);
+ pump(*server, c.display);
+ wl_display_disconnect(c.display);
+}