Queue ImageData load through IAsyncWorkExecutor so newTextureFromFile returns a stub immediately; GPU upload still uses the existing immediate path on first use. Skip a second ImGui atlas rebuild on ui.setScale so CJK glyphs are not duplicated during galgame startup. Co-authored-by: Cursor <cursoragent@cursor.com>
375 lines
12 KiB
C++
375 lines
12 KiB
C++
#include "zeroerr/assert.h"
|
|
#include "zeroerr/unittest.h"
|
|
|
|
#include "common/Capability.h"
|
|
#include "common/Resource.h"
|
|
#include "common/AsyncWork.h"
|
|
|
|
#include <functional>
|
|
#include <map>
|
|
#include <mutex>
|
|
#include <set>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <utility>
|
|
|
|
namespace {
|
|
|
|
/** Payload = a per-path generation counter observed at load time. */
|
|
class TestResource : public eve::Resource {
|
|
public:
|
|
TestResource() : eve::Resource("") {}
|
|
|
|
void adopt(eve::Resource &replacement) override {
|
|
auto &other = static_cast<TestResource &>(replacement);
|
|
if (other.failOnAdopt) throw std::runtime_error("injected adopt failure");
|
|
std::swap(value, other.value);
|
|
}
|
|
|
|
int value = 0;
|
|
bool failOnAdopt = false;
|
|
};
|
|
|
|
/** Fake asset provider: claims "*.dat" and serves a fresh generation each load. */
|
|
class TestProvider : public eve::caps::IAssetReloader {
|
|
public:
|
|
const char *reloadKind() const override { return "test"; }
|
|
|
|
bool handlesPath(const std::string &normPath) const override {
|
|
const std::string path = eve::ResourceManager::pathOfKey(normPath);
|
|
return path.size() >= 4 && path.compare(path.size() - 4, 4, ".dat") == 0;
|
|
}
|
|
|
|
eve::Result<bool> reload(const std::string &) override {
|
|
// ResourceManager owns the test cache refresh; this provider only
|
|
// supplies detached candidates through load().
|
|
return eve::Result<bool>::success(false);
|
|
}
|
|
|
|
eve::Resource *load(const std::string &key) override {
|
|
if (!handlesPath(key)) return nullptr;
|
|
if (failures.count(key)) return nullptr;
|
|
auto *r = new TestResource();
|
|
r->value = ++generation[key];
|
|
r->failOnAdopt = adoptFailures.count(key) != 0;
|
|
return r;
|
|
}
|
|
|
|
static std::map<std::string, int> generation;
|
|
static std::set<std::string> failures;
|
|
static std::set<std::string> adoptFailures;
|
|
};
|
|
|
|
std::map<std::string, int> TestProvider::generation;
|
|
std::set<std::string> TestProvider::failures;
|
|
std::set<std::string> TestProvider::adoptFailures;
|
|
|
|
/** Every case starts from an empty cache + empty capability registry. */
|
|
struct Reset {
|
|
Reset() {
|
|
eve::ResourceManager::getInstance().clear();
|
|
eve::cap::detail::clearAllRaw();
|
|
}
|
|
~Reset() {
|
|
eve::ResourceManager::getInstance().clear();
|
|
eve::cap::detail::clearAllRaw();
|
|
}
|
|
Reset(const Reset &) = delete;
|
|
Reset &operator=(const Reset &) = delete;
|
|
};
|
|
|
|
TestProvider &provider() {
|
|
static TestProvider p;
|
|
eve::cap::addListener<eve::caps::IAssetReloader>(&p, eve::caps::IAssetReloader::kCache);
|
|
TestProvider::generation.clear();
|
|
TestProvider::failures.clear();
|
|
TestProvider::adoptFailures.clear();
|
|
return p;
|
|
}
|
|
|
|
eve::Resource *get(const std::string &key) {
|
|
return eve::ResourceManager::getInstance().get(key);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("resource.normalizePath") {
|
|
CHECK_EQ(eve::ResourceManager::normalizePath("./a/b.nut"), std::string("a/b.nut"));
|
|
CHECK_EQ(eve::ResourceManager::normalizePath("a\\b.json"), std::string("a/b.json"));
|
|
CHECK_EQ(eve::ResourceManager::normalizePath("x/"), std::string("x"));
|
|
}
|
|
|
|
TEST_CASE("resource.makeKeyAndPathOfKey") {
|
|
CHECK_EQ(eve::ResourceManager::makeKey("a/b.png"), std::string("a/b.png"));
|
|
CHECK_EQ(eve::ResourceManager::makeKey("a\\b.png", "size=16"), std::string("a/b.png?size=16"));
|
|
CHECK_EQ(eve::ResourceManager::pathOfKey("a/b.png?size=16"), std::string("a/b.png"));
|
|
CHECK_EQ(eve::ResourceManager::pathOfKey("./a/b.png?size=16"), std::string("a/b.png"));
|
|
}
|
|
|
|
TEST_CASE("resource.getUnknownKeyIsNull") {
|
|
Reset reset;
|
|
provider();
|
|
CHECK(get("no_such_kind.zzz") == nullptr);
|
|
CHECK_EQ(eve::ResourceManager::getInstance().count(), 0u);
|
|
}
|
|
|
|
TEST_CASE("resource.getCachesSameInstance") {
|
|
Reset reset;
|
|
provider();
|
|
|
|
eve::Resource *a = get("a.dat");
|
|
eve::Resource *b = get("a.dat");
|
|
REQUIRE(a != nullptr);
|
|
CHECK(a == b);
|
|
CHECK_EQ(static_cast<TestResource *>(a)->value, 1);
|
|
CHECK_EQ(TestProvider::generation.size(), 1u);
|
|
CHECK_EQ(eve::ResourceManager::getInstance().count(), 1u);
|
|
}
|
|
|
|
TEST_CASE("resource.paramsAreDistinctEntries") {
|
|
Reset reset;
|
|
provider();
|
|
|
|
eve::Resource *a = get("a.dat?size=16");
|
|
eve::Resource *b = get("a.dat?size=32");
|
|
REQUIRE(a != nullptr);
|
|
REQUIRE(b != nullptr);
|
|
CHECK(a != b);
|
|
CHECK_EQ(eve::ResourceManager::getInstance().count(), 2u);
|
|
}
|
|
|
|
TEST_CASE("resource.unloadDropsEntryButKeepsHoldersAlive") {
|
|
Reset reset;
|
|
provider();
|
|
|
|
eve::Resource *a = get("a.dat");
|
|
REQUIRE(a != nullptr);
|
|
eve::ref<eve::Resource> holder(a); // an external holder keeps it alive
|
|
eve::ResourceManager::getInstance().unload("a.dat");
|
|
CHECK_EQ(eve::ResourceManager::getInstance().count(), 0u);
|
|
// The holder's ref keeps the object alive and usable.
|
|
CHECK_EQ(static_cast<TestResource *>(a)->value, 1);
|
|
|
|
// A later get() loads a fresh instance.
|
|
eve::Resource *b = get("a.dat");
|
|
REQUIRE(b != nullptr);
|
|
CHECK(b != a);
|
|
CHECK_EQ(static_cast<TestResource *>(b)->value, 2);
|
|
}
|
|
|
|
TEST_CASE("resource.unloadPathMatchesAnyParams") {
|
|
Reset reset;
|
|
provider();
|
|
|
|
REQUIRE(get("a.dat?size=16") != nullptr);
|
|
REQUIRE(get("a.dat?size=32") != nullptr);
|
|
REQUIRE(get("b.dat") != nullptr);
|
|
eve::ResourceManager::getInstance().unloadPath("a.dat");
|
|
CHECK_EQ(eve::ResourceManager::getInstance().count(), 1u);
|
|
}
|
|
|
|
TEST_CASE("resource.reloadRefreshesInPlace") {
|
|
Reset reset;
|
|
provider();
|
|
|
|
eve::Resource *a = get("a.dat");
|
|
REQUIRE(a != nullptr);
|
|
TestResource *ptr = static_cast<TestResource *>(a);
|
|
CHECK_EQ(ptr->value, 1);
|
|
|
|
auto result = eve::ResourceManager::getInstance().reload("a.dat");
|
|
CHECK(result.ok());
|
|
CHECK(result.value());
|
|
CHECK(a == ptr); // identity stays stable
|
|
CHECK_EQ(ptr->value, 2); // fresh payload adopted
|
|
CHECK_EQ(eve::ResourceManager::getInstance().count(), 1u);
|
|
}
|
|
|
|
TEST_CASE("resource.reloadWithoutCachedEntryIsNoop") {
|
|
Reset reset;
|
|
provider();
|
|
|
|
CHECK(!eve::ResourceManager::getInstance().handlesPath("a.dat"));
|
|
auto result = eve::ResourceManager::getInstance().reload("a.dat");
|
|
CHECK(result.ok());
|
|
CHECK(!result.value());
|
|
}
|
|
|
|
TEST_CASE("resource.reloadTriggersDependents") {
|
|
Reset reset;
|
|
provider();
|
|
|
|
eve::Resource *a = get("a.dat");
|
|
eve::Resource *b = get("b.dat");
|
|
REQUIRE(a != nullptr);
|
|
REQUIRE(b != nullptr);
|
|
b->addDependency(eve::ref<eve::Resource>(a));
|
|
CHECK_EQ(static_cast<TestResource *>(a)->value, 1);
|
|
CHECK_EQ(static_cast<TestResource *>(b)->value, 1);
|
|
|
|
auto result = eve::ResourceManager::getInstance().reload("a.dat");
|
|
CHECK(result.ok());
|
|
CHECK(result.value());
|
|
CHECK_EQ(static_cast<TestResource *>(a)->value, 2);
|
|
// The dependent entry refreshed transitively, in place.
|
|
CHECK_EQ(static_cast<TestResource *>(b)->value, 2);
|
|
CHECK_EQ(eve::ResourceManager::getInstance().count(), 2u);
|
|
}
|
|
|
|
TEST_CASE("resource.reloadDependencyCycleIsSafe") {
|
|
Reset reset;
|
|
provider();
|
|
|
|
eve::Resource *a = get("a.dat");
|
|
eve::Resource *b = get("b.dat");
|
|
REQUIRE(a != nullptr);
|
|
REQUIRE(b != nullptr);
|
|
a->addDependency(eve::ref<eve::Resource>(b));
|
|
b->addDependency(eve::ref<eve::Resource>(a));
|
|
|
|
auto result = eve::ResourceManager::getInstance().reload("a.dat");
|
|
CHECK(result.ok());
|
|
CHECK(result.value());
|
|
CHECK_EQ(static_cast<TestResource *>(a)->value, 2);
|
|
CHECK_EQ(static_cast<TestResource *>(b)->value, 2);
|
|
}
|
|
|
|
TEST_CASE("resource.reloadAllVariantsIsTransactional") {
|
|
Reset reset;
|
|
provider();
|
|
|
|
auto *small = static_cast<TestResource *>(get("a.dat?size=16"));
|
|
auto *large = static_cast<TestResource *>(get("a.dat?size=32"));
|
|
REQUIRE(small != nullptr);
|
|
REQUIRE(large != nullptr);
|
|
TestProvider::failures.insert("a.dat?size=32");
|
|
|
|
auto result = eve::ResourceManager::getInstance().reload("a.dat");
|
|
CHECK(result.ok());
|
|
CHECK(!result.value());
|
|
CHECK_EQ(small->value, 1);
|
|
CHECK_EQ(large->value, 1);
|
|
}
|
|
|
|
TEST_CASE("resource.reloadDependentFailureRollsBackWholeGraph") {
|
|
Reset reset;
|
|
provider();
|
|
|
|
auto *source = static_cast<TestResource *>(get("source.dat"));
|
|
auto *derived = static_cast<TestResource *>(get("derived.dat"));
|
|
REQUIRE(source != nullptr);
|
|
REQUIRE(derived != nullptr);
|
|
derived->addDependency(eve::ref<eve::Resource>(source));
|
|
TestProvider::failures.insert("derived.dat");
|
|
|
|
auto result = eve::ResourceManager::getInstance().reload("source.dat");
|
|
CHECK(result.ok());
|
|
CHECK(!result.value());
|
|
CHECK_EQ(source->value, 1);
|
|
CHECK_EQ(derived->value, 1);
|
|
}
|
|
|
|
TEST_CASE("resource.reloadCommitFailureRollsBackEarlierEntries") {
|
|
Reset reset;
|
|
provider();
|
|
|
|
auto *first = static_cast<TestResource *>(get("a.dat?variant=1"));
|
|
auto *second = static_cast<TestResource *>(get("a.dat?variant=2"));
|
|
REQUIRE(first != nullptr);
|
|
REQUIRE(second != nullptr);
|
|
TestProvider::adoptFailures.insert("a.dat?variant=2");
|
|
|
|
auto result = eve::ResourceManager::getInstance().reload("a.dat");
|
|
CHECK(result.ok());
|
|
CHECK(!result.value());
|
|
CHECK_EQ(first->value, 1);
|
|
CHECK_EQ(second->value, 1);
|
|
}
|
|
|
|
TEST_CASE("resource.requestNoOpWhenCached") {
|
|
Reset reset;
|
|
provider();
|
|
REQUIRE(get("a.dat") != nullptr);
|
|
auto queued = eve::ResourceManager::getInstance().request("a.dat");
|
|
CHECK(queued.ok());
|
|
CHECK_EQ(queued.code(), eve::StatusCode::NoOp);
|
|
}
|
|
|
|
TEST_CASE("resource.requestUnknownIsNotFound") {
|
|
Reset reset;
|
|
provider();
|
|
auto queued = eve::ResourceManager::getInstance().request("no_such_kind.zzz");
|
|
CHECK(!queued.ok());
|
|
CHECK_EQ(queued.code(), eve::StatusCode::NotFound);
|
|
}
|
|
|
|
TEST_CASE("resource.peekDoesNotLoad") {
|
|
Reset reset;
|
|
provider();
|
|
CHECK(!eve::ResourceManager::getInstance().peek("a.dat").has_value());
|
|
CHECK_EQ(eve::ResourceManager::getInstance().count(), 0u);
|
|
}
|
|
|
|
TEST_CASE("resource.requestAsyncThenWaitFor") {
|
|
Reset reset;
|
|
provider();
|
|
|
|
std::mutex gate;
|
|
gate.lock();
|
|
class DelayedExecutor final : public eve::caps::IAsyncWorkExecutor {
|
|
public:
|
|
explicit DelayedExecutor(std::mutex *gate) : gate_(gate) {}
|
|
eve::Result<void> submit(std::function<void()> work) override {
|
|
std::thread([work = std::move(work), gate = gate_]() {
|
|
std::lock_guard<std::mutex> hold(*gate);
|
|
work();
|
|
}).detach();
|
|
return eve::Result<void>::success(eve::Status::success(eve::StatusCode::Applied));
|
|
}
|
|
std::mutex *gate_ = nullptr;
|
|
} executor{&gate};
|
|
eve::cap::provide<eve::caps::IAsyncWorkExecutor>(&executor);
|
|
|
|
auto queued = eve::ResourceManager::getInstance().request("async.dat");
|
|
CHECK(queued.ok());
|
|
CHECK_EQ(queued.code(), eve::StatusCode::Applied);
|
|
CHECK_EQ(eve::ResourceManager::getInstance().pendingCount(), 1u);
|
|
CHECK(!eve::ResourceManager::getInstance().peek("async.dat").has_value());
|
|
|
|
gate.unlock();
|
|
auto ready = eve::ResourceManager::getInstance().waitFor("async.dat");
|
|
REQUIRE(ready.ok());
|
|
CHECK_EQ(static_cast<TestResource &>(ready.value().get()).value, 1);
|
|
CHECK_EQ(eve::ResourceManager::getInstance().pendingCount(), 0u);
|
|
}
|
|
|
|
TEST_CASE("resource.getJoinsInFlightRequest") {
|
|
Reset reset;
|
|
provider();
|
|
|
|
std::mutex gate;
|
|
gate.lock();
|
|
class DelayedExecutor final : public eve::caps::IAsyncWorkExecutor {
|
|
public:
|
|
explicit DelayedExecutor(std::mutex *gate) : gate_(gate) {}
|
|
eve::Result<void> submit(std::function<void()> work) override {
|
|
std::thread([work = std::move(work), gate = gate_]() {
|
|
std::lock_guard<std::mutex> hold(*gate);
|
|
work();
|
|
}).detach();
|
|
return eve::Result<void>::success(eve::Status::success(eve::StatusCode::Applied));
|
|
}
|
|
std::mutex *gate_ = nullptr;
|
|
} executor{&gate};
|
|
eve::cap::provide<eve::caps::IAsyncWorkExecutor>(&executor);
|
|
|
|
auto queued = eve::ResourceManager::getInstance().request("join.dat");
|
|
CHECK(queued.ok());
|
|
CHECK_EQ(eve::ResourceManager::getInstance().pendingCount(), 1u);
|
|
gate.unlock();
|
|
eve::Resource *loaded = get("join.dat");
|
|
REQUIRE(loaded != nullptr);
|
|
CHECK_EQ(static_cast<TestResource *>(loaded)->value, 1);
|
|
}
|