#include "zeroerr/assert.h" #include "zeroerr/unittest.h" #include "ScriptTest.h" #include "filesystem/Filesystem.h" #include "filesystem/HotReload.h" #include "particles/Particles.h" #include "particles/ParticleEmitter.h" #include "particles/ParticleSystem.h" #include #include #include namespace { eve::filesystem::Filesystem *fs() { return eve::filesystem::Filesystem::create(); } void useIdentity(const char *id) { auto *f = fs(); REQUIRE(f->setIdentity(id, true)); REQUIRE(f->setupWriteDirectory()); } } // namespace TEST_CASE("hotreload.normalizePath") { CHECK_EQ(eve::filesystem::HotReload::normalizePath("./a/b.nut"), std::string("a/b.nut")); CHECK_EQ(eve::filesystem::HotReload::normalizePath("a\\b.json"), std::string("a/b.json")); CHECK_EQ(eve::filesystem::HotReload::normalizePath("x/"), std::string("x")); } TEST_CASE("hotreload.tryReloadParticleJson") { useIdentity("ev_ut_hot_particle"); auto *f = fs(); const char *name = "hot_fx.json"; const char *json1 = R"({"emissionRate":5,"particleLifetime":[1,1]})"; f->write(name, json1, std::strlen(json1)); auto *mod = eve::particles::Particles::create(); auto *e = mod->newEmitter(16); REQUIRE(e->loadConfig(name)); CHECK(std::abs(e->getEmissionRate() - 5.f) < 1e-4f); auto *hot = eve::filesystem::HotReload::create(); const char *json2 = R"({"emissionRate":42,"particleLifetime":[1,1]})"; f->write(name, json2, std::strlen(json2)); CHECK(hot->tryReload(name)); CHECK(std::abs(e->getEmissionRate() - 42.f) < 1e-4f); // Prefixed path still matches after normalize. e->resource()->modtime = 0; const char *json3 = R"({"emissionRate":9,"particleLifetime":[1,1]})"; f->write(name, json3, std::strlen(json3)); CHECK(hot->tryReload(std::string("./") + name)); CHECK(std::abs(e->getEmissionRate() - 9.f) < 1e-4f); f->remove(name); } TEST_CASE("hotreload.softScriptReloadPreservesState") { // Soft reload redefines callbacks without resetting guarded globals. static const char *kScript = R"SQ( reload_count <- 0 if (!("counter" in getroottable())) counter <- 0 counter = counter + 1 eve_update <- function(dt) { reload_count = reload_count + 1; } function soft_reload() { // Simulate load.nut soft reload of the same source. if (!("counter" in getroottable())) counter <- 0 // guarded: do not reset counter eve_update <- function(dt) { reload_count = reload_count + 10; } if ("eve_reload" in getroottable()) eve_reload(); } eve_reload <- function() { counter = counter + 100; } soft_reload(); )SQ"; class SoftReloadTest : public ScriptTest { public: SoftReloadTest() : ScriptTest(kScript) {} using ScriptTest::vm; }; SoftReloadTest t; // First load increments counter once; eve_reload from soft_reload adds 100. CHECK_EQ(t.vm.find("counter").toInt(), 101); // Updated eve_update from soft_reload uses +10 t.vm.callFunc(t.vm.findFunc("eve_update"), t.vm, 0.016f); CHECK_EQ(t.vm.find("reload_count").toInt(), 10); } TEST_CASE("hotreload.bindAndUnbind") { auto *hot = eve::filesystem::HotReload::create(); hot->bind("foo/bar.png", "texture"); hot->unbind("foo/bar.png"); // Unbound unknown image: tryReload returns false (no cached texture / emitters). CHECK(!hot->tryReload("foo/bar.png")); } TEST_CASE("hotreload.watchNewDirectoryRecurses") { useIdentity("ev_ut_hot_watch_directory"); auto *f = fs(); f->unwatchAll(); REQUIRE(f->createDirectory("live_root")); REQUIRE(f->createDirectory("live_root/nested")); auto *hot = eve::filesystem::HotReload::create(); CHECK(!hot->watchNewDirectory("not_a_directory")); REQUIRE(hot->watchNewDirectory("live_root")); CHECK_GE(f->getWatchCount(), 2); REQUIRE(f->createDirectory("live_root/nested/deeper")); REQUIRE(hot->watchNewDirectory("live_root/nested/deeper")); CHECK_GE(f->getWatchCount(), 3); f->unwatchAll(); f->remove("live_root/nested/deeper"); f->remove("live_root/nested"); f->remove("live_root"); }