summaryrefslogtreecommitdiffhomepage
path: root/packages/kernel/tests/test_kernel.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'packages/kernel/tests/test_kernel.cpp')
-rw-r--r--packages/kernel/tests/test_kernel.cpp383
1 files changed, 383 insertions, 0 deletions
diff --git a/packages/kernel/tests/test_kernel.cpp b/packages/kernel/tests/test_kernel.cpp
index 6f8ce9b..e5b4a64 100644
--- a/packages/kernel/tests/test_kernel.cpp
+++ b/packages/kernel/tests/test_kernel.cpp
@@ -1,10 +1,18 @@
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include <doctest/doctest.h>
+#include <unbox/kernel/extension.hpp>
+#include <unbox/kernel/hooks.hpp>
+#include <unbox/kernel/host.hpp>
#include <unbox/kernel/kernel.hpp>
#include <unbox/kernel/server.hpp>
+#include <unbox/kernel/surface_registry.hpp>
#include <cstdlib>
+#include <memory>
+#include <stdexcept>
+#include <string>
+#include <vector>
TEST_CASE("kernel compiles against and links wlroots + libwayland-server") {
CHECK(unbox::kernel::link_probe());
@@ -99,3 +107,378 @@ TEST_CASE("ui spike submits an upright (non-flipped) buffer") {
unsetenv("UNBOX_UI_SPIKE_FORCE_SHM");
}
+
+// ============================================================================
+// The typed bus — PURE CORE (strict; zero mocks of unbox modules, no wlroots
+// running). A test DisableSink stands in for the kernel's isolation registry.
+// ============================================================================
+
+namespace {
+
+using unbox::kernel::detail::DisableSink;
+using unbox::kernel::detail::HookBase;
+using unbox::kernel::Event;
+using unbox::kernel::ExtensionId;
+using unbox::kernel::Filter;
+using unbox::kernel::Subscription;
+
+// Mirrors Server::Impl's isolation behavior at pure-core scale: on disable(),
+// purge the offending extension from every registered hook. Records who got
+// disabled so tests can assert isolation hit the RIGHT extension.
+struct TestRegistry final : DisableSink {
+ std::vector<HookBase*> hooks;
+ std::vector<ExtensionId> disabled;
+
+ void track(HookBase& h) {
+ h.set_sink(this);
+ hooks.push_back(&h);
+ }
+ void disable(ExtensionId who) noexcept override {
+ disabled.push_back(who);
+ for (HookBase* h : hooks) {
+ h->purge(who);
+ }
+ }
+};
+
+constexpr ExtensionId ext_a{1};
+constexpr ExtensionId ext_b{2};
+constexpr ExtensionId ext_c{3};
+
+} // namespace
+
+TEST_CASE("Event fans out to all listeners in subscription order") {
+ Event<int> ev;
+ std::vector<int> log;
+ auto s1 = ev.subscribe(ext_a, [&](int v) { log.push_back(v + 10); });
+ auto s2 = ev.subscribe(ext_b, [&](int v) { log.push_back(v + 20); });
+ auto s3 = ev.subscribe(ext_c, [&](int v) { log.push_back(v + 30); });
+
+ ev.emit(1);
+ CHECK(log == std::vector<int>{11, 21, 31});
+}
+
+TEST_CASE("Subscription RAII unsubscribes on destruction") {
+ Event<int> ev;
+ int hits = 0;
+ auto outer = ev.subscribe(ext_a, [&](int) { ++hits; });
+ {
+ auto inner = ev.subscribe(ext_b, [&](int) { ++hits; });
+ ev.emit(0);
+ CHECK(hits == 2);
+ }
+ // inner dropped: only outer remains.
+ ev.emit(0);
+ CHECK(hits == 3);
+
+ // Explicit reset() also unsubscribes.
+ outer.reset();
+ CHECK(!outer.active());
+ ev.emit(0);
+ CHECK(hits == 3);
+}
+
+TEST_CASE("Subscription is move-only and the moved-from handle is inert") {
+ Event<int> ev;
+ int hits = 0;
+ Subscription s = ev.subscribe(ext_a, [&](int) { ++hits; });
+ Subscription moved = std::move(s);
+ CHECK(moved.active());
+ CHECK(!s.active());
+ ev.emit(0);
+ CHECK(hits == 1);
+ s.reset(); // no-op on moved-from
+ ev.emit(0);
+ CHECK(hits == 2);
+}
+
+TEST_CASE("a listener may unsubscribe ITSELF during dispatch (deferred removal)") {
+ Event<int> ev;
+ int a = 0;
+ int c = 0;
+ std::unique_ptr<Subscription> self;
+ auto sa = ev.subscribe(ext_a, [&](int) { ++a; });
+ auto sb = ev.subscribe(ext_b, [&](int) { self->reset(); }); // drop self mid-dispatch
+ auto sc = ev.subscribe(ext_c, [&](int) { ++c; });
+ self = std::make_unique<Subscription>(std::move(sb));
+
+ ev.emit(0);
+ // a and c still fired this round despite b removing itself.
+ CHECK(a == 1);
+ CHECK(c == 1);
+
+ ev.emit(0); // b gone now
+ CHECK(a == 2);
+ CHECK(c == 2);
+}
+
+TEST_CASE("re-entrant emit is safe") {
+ Event<int> ev;
+ int inner = 0;
+ bool reentered = false;
+ auto s = ev.subscribe(ext_a, [&](int v) {
+ if (!reentered && v == 1) {
+ reentered = true;
+ ev.emit(2); // re-enter
+ }
+ ++inner;
+ });
+ ev.emit(1);
+ CHECK(inner == 2); // outer (v=1) and inner (v=2)
+}
+
+TEST_CASE("Filter threads the value through links in order") {
+ Filter<int> flt;
+ auto s1 = flt.subscribe(ext_a, [](int v) { return v + 1; });
+ auto s2 = flt.subscribe(ext_b, [](int v) { return v * 10; });
+ // (((5)+1)*10) = 60
+ CHECK(flt.apply(5) == 60);
+}
+
+TEST_CASE("Filter with no links returns the value unchanged") {
+ Filter<int> flt;
+ CHECK(flt.apply(42) == 42);
+}
+
+TEST_CASE("error isolation: a throwing listener disables only its extension") {
+ TestRegistry reg;
+ Event<int> ev{&reg};
+ reg.track(ev);
+
+ std::vector<std::string> log;
+ auto sa = ev.subscribe(ext_a, [&](int) { log.emplace_back("a"); });
+ auto sb = ev.subscribe(ext_b, [&](int) {
+ log.emplace_back("b-throw");
+ throw std::runtime_error("boom");
+ });
+ auto sc = ev.subscribe(ext_c, [&](int) { log.emplace_back("c"); });
+
+ ev.emit(0);
+ // All three ran THIS emit (isolation doesn't abort the in-flight fan-out);
+ // b was disabled.
+ CHECK(log == std::vector<std::string>{"a", "b-throw", "c"});
+ CHECK(reg.disabled == std::vector<ExtensionId>{ext_b});
+
+ log.clear();
+ ev.emit(0);
+ // b's subscription was purged; a and c remain.
+ CHECK(log == std::vector<std::string>{"a", "c"});
+}
+
+TEST_CASE("error isolation: a throwing filter link is skipped and chain continues") {
+ TestRegistry reg;
+ Filter<int> flt{&reg};
+ reg.track(flt);
+
+ auto s1 = flt.subscribe(ext_a, [](int v) { return v + 1; });
+ auto s2 = flt.subscribe(ext_b, [](int) -> int { throw std::runtime_error("boom"); });
+ auto s3 = flt.subscribe(ext_c, [](int v) { return v * 10; });
+
+ // a: 0->1, b throws (skipped, value stays 1), c: 1->10.
+ CHECK(flt.apply(0) == 10);
+ CHECK(reg.disabled == std::vector<ExtensionId>{ext_b});
+
+ // b purged: a then c.
+ CHECK(flt.apply(0) == 10);
+}
+
+TEST_CASE("disabling an extension purges it across MULTIPLE hooks") {
+ TestRegistry reg;
+ Event<int> ev1{&reg};
+ Event<int> ev2{&reg};
+ reg.track(ev1);
+ reg.track(ev2);
+
+ int ev2_hits = 0;
+ // ext_b subscribes to BOTH hooks; throwing on ev1 must drop its ev2 sub too.
+ auto a1 = ev1.subscribe(ext_a, [](int) {});
+ auto b1 = ev1.subscribe(ext_b, [](int) { throw std::runtime_error("boom"); });
+ auto b2 = ev2.subscribe(ext_b, [&](int) { ++ev2_hits; });
+
+ ev1.emit(0); // disables ext_b everywhere
+ ev2.emit(0); // ext_b's ev2 listener must NOT fire
+ CHECK(ev2_hits == 0);
+ CHECK(reg.disabled == std::vector<ExtensionId>{ext_b});
+}
+
+// ============================================================================
+// Extension host: install + topological activation (no wlroots input needed).
+// ============================================================================
+
+namespace {
+
+// Records activation order into a shared log so tests can assert topo order.
+class RecordingExtension : public unbox::kernel::Extension {
+public:
+ RecordingExtension(unbox::kernel::Manifest m, std::vector<std::string>* log)
+ : manifest_(std::move(m)), log_(log) {}
+ auto manifest() const -> const unbox::kernel::Manifest& override { return manifest_; }
+ void activate(unbox::kernel::Host&) override { log_->push_back(manifest_.id); }
+
+private:
+ unbox::kernel::Manifest manifest_;
+ std::vector<std::string>* log_;
+};
+
+auto make_headless_server() -> std::unique_ptr<unbox::kernel::Server> {
+ setenv("WLR_BACKENDS", "headless", 1);
+ setenv("WLR_RENDERER", "pixman", 1);
+ return unbox::kernel::Server::create({});
+}
+
+using unbox::kernel::Manifest;
+using unbox::kernel::Tier;
+
+} // namespace
+
+TEST_CASE("activation respects depends_on topological order") {
+ auto server = make_headless_server();
+ std::vector<std::string> log;
+
+ // Install in an order that does NOT match the dependency order.
+ server->install(std::make_unique<RecordingExtension>(
+ Manifest{"taskbar", Tier::standard, {"xdg-shell"}}, &log));
+ server->install(std::make_unique<RecordingExtension>(
+ Manifest{"xdg-shell", Tier::core, {}}, &log));
+ server->install(std::make_unique<RecordingExtension>(
+ Manifest{"tiling", Tier::standard, {"xdg-shell", "taskbar"}}, &log));
+
+ server->activate_extensions();
+
+ // xdg-shell first (no deps, core tier), then taskbar, then tiling.
+ CHECK(log == std::vector<std::string>{"xdg-shell", "taskbar", "tiling"});
+}
+
+TEST_CASE("activate_extensions is idempotent") {
+ auto server = make_headless_server();
+ std::vector<std::string> log;
+ server->install(
+ std::make_unique<RecordingExtension>(Manifest{"a", Tier::core, {}}, &log));
+ server->activate_extensions();
+ server->activate_extensions();
+ CHECK(log == std::vector<std::string>{"a"});
+}
+
+TEST_CASE("duplicate extension id is a startup error at install") {
+ auto server = make_headless_server();
+ std::vector<std::string> log;
+ server->install(
+ std::make_unique<RecordingExtension>(Manifest{"dup", Tier::core, {}}, &log));
+ CHECK_THROWS_AS(server->install(std::make_unique<RecordingExtension>(
+ Manifest{"dup", Tier::standard, {}}, &log)),
+ std::runtime_error);
+}
+
+TEST_CASE("missing dependency is a startup error at activation") {
+ auto server = make_headless_server();
+ std::vector<std::string> log;
+ server->install(std::make_unique<RecordingExtension>(
+ Manifest{"needs-missing", Tier::core, {"nope"}}, &log));
+ CHECK_THROWS_AS(server->activate_extensions(), std::runtime_error);
+}
+
+TEST_CASE("dependency cycle is a startup error at activation") {
+ auto server = make_headless_server();
+ std::vector<std::string> log;
+ server->install(
+ std::make_unique<RecordingExtension>(Manifest{"x", Tier::core, {"y"}}, &log));
+ server->install(
+ std::make_unique<RecordingExtension>(Manifest{"y", Tier::core, {"x"}}, &log));
+ CHECK_THROWS_AS(server->activate_extensions(), std::runtime_error);
+}
+
+TEST_CASE("featureless kernel: zero extensions boots, runs, shuts down clean") {
+ auto server = make_headless_server();
+ CHECK(!server->socket_name().empty());
+ server->activate_extensions(); // no-op with zero extensions
+ for (int i = 0; i < 3; ++i) {
+ CHECK(server->dispatch(10));
+ }
+}
+
+// ============================================================================
+// Typed surface->scene-tree association — PURE CORE (no wlroots). Keys/values
+// are pointer identities; dummy addresses stand in for wlr_surface*/scene_tree*.
+// ============================================================================
+
+namespace {
+
+using unbox::kernel::detail::PointerAssoc;
+using unbox::kernel::SurfaceRegistration;
+
+// Distinct, never-dereferenced sentinel addresses.
+int surf_a_obj = 0, surf_b_obj = 0, tree_1_obj = 0, tree_2_obj = 0;
+void* const surf_a = &surf_a_obj;
+void* const surf_b = &surf_b_obj;
+void* const tree_1 = &tree_1_obj;
+void* const tree_2 = &tree_2_obj;
+
+} // namespace
+
+TEST_CASE("surface assoc: register, lookup, unregister") {
+ PointerAssoc store;
+ CHECK(store.get(surf_a) == nullptr); // unregistered -> null
+
+ SurfaceRegistration reg(&store, surf_a, store.set(surf_a, tree_1));
+ CHECK(reg.active());
+ CHECK(store.get(surf_a) == tree_1);
+ CHECK(store.get(surf_b) == nullptr); // independent key still null
+
+ reg.reset();
+ CHECK(!reg.active());
+ CHECK(store.get(surf_a) == nullptr); // unregistered on reset
+ CHECK(store.size() == 0);
+}
+
+TEST_CASE("surface assoc: RAII handle unregisters on destruction") {
+ PointerAssoc store;
+ {
+ SurfaceRegistration reg(&store, surf_a, store.set(surf_a, tree_1));
+ CHECK(store.get(surf_a) == tree_1);
+ }
+ CHECK(store.get(surf_a) == nullptr);
+}
+
+TEST_CASE("surface assoc: move transfers ownership; moved-from is inert") {
+ PointerAssoc store;
+ SurfaceRegistration a(&store, surf_a, store.set(surf_a, tree_1));
+ SurfaceRegistration b = std::move(a);
+ CHECK(b.active());
+ CHECK(!a.active());
+ a.reset(); // no-op
+ CHECK(store.get(surf_a) == tree_1); // still registered (b owns it)
+ b.reset();
+ CHECK(store.get(surf_a) == nullptr);
+}
+
+TEST_CASE("surface assoc: double-register replaces value; stale handle is a no-op") {
+ PointerAssoc store;
+ // First registration of surf_a -> tree_1.
+ SurfaceRegistration first(&store, surf_a, store.set(surf_a, tree_1));
+ CHECK(store.get(surf_a) == tree_1);
+
+ // Re-host the SAME surface in tree_2: replaces the mapping, bumps token.
+ SurfaceRegistration second(&store, surf_a, store.set(surf_a, tree_2));
+ CHECK(store.get(surf_a) == tree_2);
+
+ // Destroying the SUPERSEDED first handle must NOT tear down the newer
+ // mapping (token defense).
+ first.reset();
+ CHECK(store.get(surf_a) == tree_2);
+
+ // The current owner still unregisters correctly.
+ second.reset();
+ CHECK(store.get(surf_a) == nullptr);
+}
+
+TEST_CASE("surface assoc: distinct keys are independent") {
+ PointerAssoc store;
+ SurfaceRegistration ra(&store, surf_a, store.set(surf_a, tree_1));
+ SurfaceRegistration rb(&store, surf_b, store.set(surf_b, tree_2));
+ CHECK(store.get(surf_a) == tree_1);
+ CHECK(store.get(surf_b) == tree_2);
+ CHECK(store.size() == 2);
+ ra.reset();
+ CHECK(store.get(surf_a) == nullptr);
+ CHECK(store.get(surf_b) == tree_2); // unaffected
+}