#include "zeroerr/assert.h" #include "zeroerr/unittest.h" #include "common/Capability.h" #include #include namespace { class IGreeter { public: static constexpr const char* capabilityName = "test.IGreeter"; virtual ~IGreeter() = default; virtual std::string greet() const = 0; }; class Hello : public IGreeter { public: std::string greet() const override { return "hello"; } }; class Bonjour : public IGreeter { public: std::string greet() const override { return "bonjour"; } }; class IStep { public: static constexpr const char* capabilityName = "test.IStep"; virtual ~IStep() = default; virtual void run(std::vector& log) = 0; virtual bool handles(const std::string& kind) const = 0; }; class NamedStep : public IStep { public: explicit NamedStep(std::string name) : name_(std::move(name)) {} void run(std::vector& log) override { log.push_back(name_); } bool handles(const std::string& kind) const override { return kind == name_; } private: std::string name_; }; /** Each case starts from an empty registry; entries are process-global. */ struct Reset { Reset() { eve::cap::detail::clearAllRaw(); } ~Reset() { eve::cap::detail::clearAllRaw(); } }; } // namespace TEST_CASE("capability.absentQueryIsNull") { Reset reset; CHECK(eve::cap::query() == nullptr); CHECK_EQ(eve::cap::listenerCount(), 0u); CHECK(eve::cap::listenerAt(0) == nullptr); } TEST_CASE("capability.provideAndQuery") { Reset reset; Hello hello; eve::cap::provide(&hello); IGreeter* found = eve::cap::query(); REQUIRE(found != nullptr); CHECK_EQ(found->greet(), "hello"); } TEST_CASE("capability.provideReplacesPrevious") { Reset reset; Hello hello; Bonjour bonjour; eve::cap::provide(&hello); eve::cap::provide(&bonjour); REQUIRE(eve::cap::query() != nullptr); CHECK_EQ(eve::cap::query()->greet(), "bonjour"); } TEST_CASE("capability.revoke") { Reset reset; Hello hello; eve::cap::provide(&hello); eve::cap::revoke(&hello); CHECK(eve::cap::query() == nullptr); } TEST_CASE("capability.revokeOfSupersededProviderIsIgnored") { Reset reset; Hello hello; Bonjour bonjour; eve::cap::provide(&hello); eve::cap::provide(&bonjour); // A module torn down after being replaced must not unregister its successor. eve::cap::revoke(&hello); REQUIRE(eve::cap::query() != nullptr); CHECK_EQ(eve::cap::query()->greet(), "bonjour"); } TEST_CASE("capability.listenersRunInPriorityOrder") { Reset reset; NamedStep late("late"), early("early"), middle("middle"); eve::cap::addListener(&late, 30); eve::cap::addListener(&early, 10); eve::cap::addListener(&middle, 20); std::vector log; eve::cap::forEach([&](IStep* s) { s->run(log); }); REQUIRE(log.size() == 3u); CHECK_EQ(log[0], "early"); CHECK_EQ(log[1], "middle"); CHECK_EQ(log[2], "late"); } TEST_CASE("capability.equalPriorityKeepsRegistrationOrder") { Reset reset; NamedStep a("a"), b("b"), c("c"); eve::cap::addListener(&a); eve::cap::addListener(&b); eve::cap::addListener(&c); std::vector log; eve::cap::forEach([&](IStep* s) { s->run(log); }); REQUIRE(log.size() == 3u); CHECK_EQ(log[0], "a"); CHECK_EQ(log[1], "b"); CHECK_EQ(log[2], "c"); } TEST_CASE("capability.addListenerIsIdempotent") { Reset reset; NamedStep a("a"); eve::cap::addListener(&a); eve::cap::addListener(&a); CHECK_EQ(eve::cap::listenerCount(), 1u); } TEST_CASE("capability.removeListener") { Reset reset; NamedStep a("a"), b("b"); eve::cap::addListener(&a); eve::cap::addListener(&b); eve::cap::removeListener(&a); std::vector log; eve::cap::forEach([&](IStep* s) { s->run(log); }); REQUIRE(log.size() == 1u); CHECK_EQ(log[0], "b"); } TEST_CASE("capability.forEachUntilStopsAtFirstHandler") { Reset reset; NamedStep png("png"), json("json"), wav("wav"); eve::cap::addListener(&png); eve::cap::addListener(&json); eve::cap::addListener(&wav); std::vector tried; const bool handled = eve::cap::forEachUntil([&](IStep* s) { tried.push_back("probe"); return s->handles("json"); }); CHECK(handled); // png then json; wav is never probed. CHECK_EQ(tried.size(), 2u); tried.clear(); CHECK(!eve::cap::forEachUntil([&](IStep* s) { return s->handles("gif"); })); } TEST_CASE("capability.listenersAndServiceAreIndependent") { Reset reset; Hello hello; NamedStep a("a"); eve::cap::provide(&hello); eve::cap::addListener(&a); CHECK(eve::cap::query() != nullptr); CHECK_EQ(eve::cap::listenerCount(), 1u); // Distinct capability names never collide. CHECK(eve::cap::query() == nullptr); CHECK_EQ(eve::cap::listenerCount(), 0u); } TEST_CASE("capability.withdrawDuringDispatchAffectsNextDispatch") { Reset reset; NamedStep a("a"), b("b"); eve::cap::addListener(&a); eve::cap::addListener(&b); std::vector log; eve::cap::forEach([&](IStep* s) { s->run(log); eve::cap::removeListener(&b); }); // The active dispatch owns a stable pointer snapshot. Removing b changes // the registry for the next dispatch without changing this one. REQUIRE(log.size() == 2u); CHECK_EQ(log[0], "a"); CHECK_EQ(log[1], "b"); CHECK_EQ(eve::cap::listenerCount(), 1u); log.clear(); eve::cap::forEach([&](IStep* s) { s->run(log); }); REQUIRE(log.size() == 1u); CHECK_EQ(log[0], "a"); } TEST_CASE("capability.withdrawCurrentListenerDoesNotSkipSuccessor") { Reset reset; NamedStep a("a"), b("b"), c("c"); eve::cap::addListener(&a); eve::cap::addListener(&b); eve::cap::addListener(&c); std::vector log; eve::cap::forEach([&](IStep* s) { s->run(log); if (s == &a) eve::cap::removeListener(&a); }); REQUIRE(log.size() == 3u); CHECK_EQ(log[0], "a"); CHECK_EQ(log[1], "b"); CHECK_EQ(log[2], "c"); CHECK_EQ(eve::cap::listenerCount(), 2u); } TEST_CASE("capability.registrationDuringDispatchAffectsNextDispatch") { Reset reset; NamedStep a("a"), b("b"), early("early"); eve::cap::addListener(&a, 10); eve::cap::addListener(&b, 20); std::vector log; eve::cap::forEach([&](IStep* s) { s->run(log); if (s == &a) eve::cap::addListener(&early, 0); }); REQUIRE(log.size() == 2u); CHECK_EQ(log[0], "a"); CHECK_EQ(log[1], "b"); log.clear(); eve::cap::forEach([&](IStep* s) { s->run(log); }); REQUIRE(log.size() == 3u); CHECK_EQ(log[0], "early"); CHECK_EQ(log[1], "a"); CHECK_EQ(log[2], "b"); } TEST_CASE("capability.nestedDispatchUsesCurrentRegistry") { Reset reset; NamedStep a("a"), b("b"), early("early"); eve::cap::addListener(&a, 10); eve::cap::addListener(&b, 20); std::vector outer; std::vector nested; eve::cap::forEach([&](IStep* s) { s->run(outer); if (s == &a) { eve::cap::addListener(&early, 0); eve::cap::forEach([&](IStep* inner) { inner->run(nested); }); } }); REQUIRE(outer.size() == 2u); CHECK_EQ(outer[0], "a"); CHECK_EQ(outer[1], "b"); REQUIRE(nested.size() == 3u); CHECK_EQ(nested[0], "early"); CHECK_EQ(nested[1], "a"); CHECK_EQ(nested[2], "b"); }