Files
EVEngine/test/capability.cpp
T
Cursor Agentand西风逍遥游 08679654e0 feat(common): add the capability registry, use it for GPU info and main-thread posts
Introduces eve::cap in common/Capability.h: a name-keyed registry where the
consumer owns the interface, the provider registers an implementation, and
neither includes the other. It generalises what common/RenderTrace.h already
does for the render tracer, and supports both a single provider (provide/query)
and many ordered ones (addListener/forEach), keyed by string so a provider
compiled into a plugin lands in the same slot as the host.

Two upward dependencies go away:

  system -> graphics   IGpuInfo. The Vulkan physical-device fields move into
                       graphics/vulkan/GpuInfoProvider.cpp, with a WebGPU
                       counterpart, so System.cpp loses both the graphics
                       include and its EVENGINE_WEBGPU branches.

  thread -> event      IMainThreadPost. postMain and submitPost now enqueue
                       through the capability. The interface makes explicit
                       what was previously only a comment in submitPost: the
                       submitting thread calls prepare() so the worker never
                       reaches the module registry.

Both now report an error when no provider is linked, rather than lazily
creating a module the build may not contain.

Step S3 of docs/dev/模块编排与裁剪架构.md; back-edge whitelist 16 -> 14.

Co-authored-by: 西风逍遥游 <sunxfancy@gmail.com>
2026-08-18 13:46:04 +00:00

208 lines
5.7 KiB
C++

#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include "common/Capability.h"
#include <string>
#include <vector>
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<std::string>& 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<std::string>& 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<IGreeter>() == nullptr);
CHECK_EQ(eve::cap::listenerCount<IStep>(), 0u);
CHECK(eve::cap::listenerAt<IStep>(0) == nullptr);
}
TEST_CASE("capability.provideAndQuery") {
Reset reset;
Hello hello;
eve::cap::provide<IGreeter>(&hello);
IGreeter* found = eve::cap::query<IGreeter>();
REQUIRE(found != nullptr);
CHECK_EQ(found->greet(), "hello");
}
TEST_CASE("capability.provideReplacesPrevious") {
Reset reset;
Hello hello;
Bonjour bonjour;
eve::cap::provide<IGreeter>(&hello);
eve::cap::provide<IGreeter>(&bonjour);
REQUIRE(eve::cap::query<IGreeter>() != nullptr);
CHECK_EQ(eve::cap::query<IGreeter>()->greet(), "bonjour");
}
TEST_CASE("capability.revoke") {
Reset reset;
Hello hello;
eve::cap::provide<IGreeter>(&hello);
eve::cap::revoke<IGreeter>(&hello);
CHECK(eve::cap::query<IGreeter>() == nullptr);
}
TEST_CASE("capability.revokeOfSupersededProviderIsIgnored") {
Reset reset;
Hello hello;
Bonjour bonjour;
eve::cap::provide<IGreeter>(&hello);
eve::cap::provide<IGreeter>(&bonjour);
// A module torn down after being replaced must not unregister its successor.
eve::cap::revoke<IGreeter>(&hello);
REQUIRE(eve::cap::query<IGreeter>() != nullptr);
CHECK_EQ(eve::cap::query<IGreeter>()->greet(), "bonjour");
}
TEST_CASE("capability.listenersRunInPriorityOrder") {
Reset reset;
NamedStep late("late"), early("early"), middle("middle");
eve::cap::addListener<IStep>(&late, 30);
eve::cap::addListener<IStep>(&early, 10);
eve::cap::addListener<IStep>(&middle, 20);
std::vector<std::string> log;
eve::cap::forEach<IStep>([&](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<IStep>(&a);
eve::cap::addListener<IStep>(&b);
eve::cap::addListener<IStep>(&c);
std::vector<std::string> log;
eve::cap::forEach<IStep>([&](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<IStep>(&a);
eve::cap::addListener<IStep>(&a);
CHECK_EQ(eve::cap::listenerCount<IStep>(), 1u);
}
TEST_CASE("capability.removeListener") {
Reset reset;
NamedStep a("a"), b("b");
eve::cap::addListener<IStep>(&a);
eve::cap::addListener<IStep>(&b);
eve::cap::removeListener<IStep>(&a);
std::vector<std::string> log;
eve::cap::forEach<IStep>([&](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<IStep>(&png);
eve::cap::addListener<IStep>(&json);
eve::cap::addListener<IStep>(&wav);
std::vector<std::string> tried;
const bool handled = eve::cap::forEachUntil<IStep>([&](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>([&](IStep* s) { return s->handles("gif"); }));
}
TEST_CASE("capability.listenersAndServiceAreIndependent") {
Reset reset;
Hello hello;
NamedStep a("a");
eve::cap::provide<IGreeter>(&hello);
eve::cap::addListener<IStep>(&a);
CHECK(eve::cap::query<IGreeter>() != nullptr);
CHECK_EQ(eve::cap::listenerCount<IStep>(), 1u);
// Distinct capability names never collide.
CHECK(eve::cap::query<IStep>() == nullptr);
CHECK_EQ(eve::cap::listenerCount<IGreeter>(), 0u);
}
TEST_CASE("capability.withdrawDuringDispatchIsSafe") {
Reset reset;
NamedStep a("a"), b("b");
eve::cap::addListener<IStep>(&a);
eve::cap::addListener<IStep>(&b);
std::vector<std::string> log;
eve::cap::forEach<IStep>([&](IStep* s) {
s->run(log);
eve::cap::removeListener<IStep>(&b);
});
// "a" runs, removes "b", and the index-based walk simply finds nothing at 1.
REQUIRE(log.size() == 1u);
CHECK_EQ(log[0], "a");
CHECK_EQ(eve::cap::listenerCount<IStep>(), 1u);
}