Skip to content

Commit 49e91ca

Browse files
committed
test(hot-reload): add ReloadRequestKind classification and dispatch tests
New coordinator tests for ClassifyReloadKind: - DLL change classified as SlotTargeted - Script change classified as ProviderLocal - Manifest change classified as FullRuntime - MergeReloadKinds picks most conservative strategy New integration tests: - FullRuntime kind triggers full ReloadModules path - Targeted reload broadcasts lifecycle for single module only - Provider-local reload suppresses notify callback
1 parent 1deedc4 commit 49e91ca

2 files changed

Lines changed: 282 additions & 7 deletions

File tree

tests/HotReloadCoordinatorTests.cpp

Lines changed: 159 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
#include <filesystem>
77
#include <fstream>
88
#include <mutex>
9+
#include <optional>
10+
#include <string_view>
911
#include <thread>
1012
#include <vector>
1113

@@ -49,6 +51,42 @@ void CreateMinimalDll(const std::filesystem::path& path) {
4951
}
5052
}
5153

54+
BML_Bool StubProviderCanHandle(const char *entry_path) {
55+
if (!entry_path) {
56+
return BML_FALSE;
57+
}
58+
return std::string_view(entry_path).ends_with(".as") ? BML_TRUE : BML_FALSE;
59+
}
60+
61+
BML_Result StubProviderAttachModule(BML_Mod, const BML_Services *, const char *, const char *) {
62+
return BML_RESULT_OK;
63+
}
64+
65+
BML_Result StubProviderPrepareDetach(BML_Mod) {
66+
return BML_RESULT_OK;
67+
}
68+
69+
BML_Result StubProviderDetachModule(BML_Mod) {
70+
return BML_RESULT_OK;
71+
}
72+
73+
BML_Result StubProviderReloadModule(BML_Mod mod) {
74+
auto *called = reinterpret_cast<bool *>(mod);
75+
if (called) {
76+
*called = true;
77+
}
78+
return BML_RESULT_OK;
79+
}
80+
81+
const BML_ModuleRuntimeProvider kStubRuntimeProvider = {
82+
sizeof(BML_ModuleRuntimeProvider),
83+
StubProviderCanHandle,
84+
StubProviderAttachModule,
85+
StubProviderPrepareDetach,
86+
StubProviderDetachModule,
87+
StubProviderReloadModule,
88+
};
89+
5290
} // namespace
5391

5492
class HotReloadCoordinatorTest : public ::testing::Test {
@@ -253,7 +291,8 @@ TEST_F(HotReloadCoordinatorTest, NotifyCallback) {
253291
coordinator.SetNotifyCallback([&](const std::string& mod_id,
254292
ReloadResult result,
255293
unsigned int,
256-
ReloadFailure) {
294+
ReloadFailure,
295+
ReloadRequestKind) {
257296
notified_mod_id = mod_id;
258297
notified_result = result;
259298
callback_called = true;
@@ -357,7 +396,8 @@ TEST_F(HotReloadCoordinatorTest, IgnoresUnrelatedRootFileChanges) {
357396
ASSERT_TRUE(coordinator.RegisterModule(entry));
358397

359398
std::atomic<int> callback_count{0};
360-
coordinator.SetNotifyCallback([&](const std::string &, ReloadResult, unsigned int, ReloadFailure) {
399+
coordinator.SetNotifyCallback([&](const std::string &, ReloadResult, unsigned int, ReloadFailure,
400+
ReloadRequestKind) {
361401
callback_count.fetch_add(1, std::memory_order_relaxed);
362402
});
363403

@@ -446,7 +486,8 @@ TEST_F(HotReloadCoordinatorTest, WatchesIncludedScriptFilesForRuntimeReload) {
446486
ASSERT_TRUE(coordinator.RegisterModule(entry));
447487

448488
std::atomic<int> callback_count{0};
449-
coordinator.SetNotifyCallback([&](const std::string &, ReloadResult, unsigned int, ReloadFailure) {
489+
coordinator.SetNotifyCallback([&](const std::string &, ReloadResult, unsigned int, ReloadFailure,
490+
ReloadRequestKind) {
450491
callback_count.fetch_add(1, std::memory_order_relaxed);
451492
});
452493

@@ -525,7 +566,8 @@ TEST_F(HotReloadCoordinatorTest, OnFileChanged_AddedEvent_TriggersReload) {
525566
ASSERT_TRUE(coordinator.RegisterModule(entry));
526567

527568
std::atomic<int> callback_count{0};
528-
coordinator.SetNotifyCallback([&](const std::string &, ReloadResult, unsigned int, ReloadFailure) {
569+
coordinator.SetNotifyCallback([&](const std::string &, ReloadResult, unsigned int, ReloadFailure,
570+
ReloadRequestKind) {
529571
callback_count.fetch_add(1, std::memory_order_relaxed);
530572
});
531573

@@ -590,14 +632,15 @@ TEST_F(HotReloadCoordinatorTest, OnFileChanged_DeletedEvent_DoesNotTriggerReload
590632
ASSERT_TRUE(coordinator.RegisterModule(entry));
591633

592634
std::atomic<int> callback_count{0};
593-
coordinator.SetNotifyCallback([&](const std::string &, ReloadResult, unsigned int, ReloadFailure) {
635+
coordinator.SetNotifyCallback([&](const std::string &, ReloadResult, unsigned int, ReloadFailure,
636+
ReloadRequestKind) {
594637
callback_count.fetch_add(1, std::memory_order_relaxed);
595638
});
596639

597640
coordinator.Start();
598641
std::this_thread::sleep_for(500ms);
599642

600-
// Delete a file should NOT trigger reload
643+
// Delete a file - should NOT trigger reload
601644
std::filesystem::remove(manifest_path);
602645

603646
auto deadline = std::chrono::steady_clock::now() + 1500ms;
@@ -651,7 +694,8 @@ TEST_F(HotReloadCoordinatorTest, RecursiveWatch_ScriptModule_WatchesSubdirectori
651694
ASSERT_TRUE(coordinator.RegisterModule(entry));
652695

653696
std::atomic<int> callback_count{0};
654-
coordinator.SetNotifyCallback([&](const std::string &, ReloadResult, unsigned int, ReloadFailure) {
697+
coordinator.SetNotifyCallback([&](const std::string &, ReloadResult, unsigned int, ReloadFailure,
698+
ReloadRequestKind) {
655699
callback_count.fetch_add(1, std::memory_order_relaxed);
656700
});
657701

@@ -708,3 +752,111 @@ TEST_F(HotReloadCoordinatorTest, GetSlotModuleInfo_ReturnsSlotState) {
708752
EXPECT_FALSE(coordinator.GetSlotModuleInfo("test.mod", &handle, &ep));
709753
EXPECT_FALSE(coordinator.GetSlotModuleInfo("nonexistent", &handle, &ep));
710754
}
755+
756+
TEST_F(HotReloadCoordinatorTest, ManifestEditForNativeModuleSchedulesFullRuntimeReload) {
757+
auto dll_path = m_TempDir / "test.dll";
758+
auto manifest_path = m_TempDir / "mod.toml";
759+
CreateMinimalDll(dll_path);
760+
{
761+
std::ofstream manifest_file(manifest_path);
762+
manifest_file << "[package]\n";
763+
}
764+
765+
HotReloadCoordinator coordinator(*m_Context, *kernel_);
766+
coordinator.SetServices(&m_DummyServices);
767+
768+
HotReloadSettings settings;
769+
settings.enabled = true;
770+
settings.debounce = 0ms;
771+
settings.temp_directory = (m_TempDir / "temp").wstring();
772+
coordinator.Configure(settings);
773+
774+
HotReloadModuleEntry entry;
775+
entry.id = "test.mod";
776+
entry.dll_path = dll_path.wstring();
777+
entry.watch_path = m_TempDir.wstring();
778+
ModManifest manifest{};
779+
manifest.package.id = "test.mod";
780+
manifest.directory = m_TempDir.wstring();
781+
manifest.manifest_path = manifest_path.wstring();
782+
entry.manifest = manifest;
783+
ASSERT_TRUE(coordinator.RegisterModule(entry));
784+
785+
std::optional<ReloadRequestKind> notified_kind;
786+
coordinator.SetNotifyCallback([&](const std::string &, ReloadResult, unsigned int, ReloadFailure,
787+
ReloadRequestKind kind) {
788+
notified_kind = kind;
789+
});
790+
791+
coordinator.Start();
792+
std::this_thread::sleep_for(500ms);
793+
794+
{
795+
std::ofstream manifest_file(manifest_path, std::ios::trunc);
796+
manifest_file << "[package]\nname='changed'\n";
797+
manifest_file.flush();
798+
}
799+
800+
auto deadline = std::chrono::steady_clock::now() + 10s;
801+
while (std::chrono::steady_clock::now() < deadline) {
802+
coordinator.Update();
803+
if (notified_kind.has_value()) {
804+
break;
805+
}
806+
std::this_thread::sleep_for(100ms);
807+
}
808+
809+
coordinator.Stop();
810+
811+
ASSERT_TRUE(notified_kind.has_value());
812+
EXPECT_EQ(*notified_kind, ReloadRequestKind::FullRuntime);
813+
}
814+
815+
TEST_F(HotReloadCoordinatorTest, ProviderOwnerBinaryChangeSchedulesFullRuntimeReload) {
816+
auto dll_path = m_TempDir / "BML_Scripting.dll";
817+
CreateMinimalDll(dll_path);
818+
819+
ASSERT_EQ(m_Context->RegisterRuntimeProvider(&kStubRuntimeProvider, "com.bml.scripting"),
820+
BML_RESULT_OK);
821+
822+
HotReloadCoordinator coordinator(*m_Context, *kernel_);
823+
coordinator.SetServices(&m_DummyServices);
824+
825+
HotReloadSettings settings;
826+
settings.enabled = true;
827+
settings.debounce = 0ms;
828+
settings.temp_directory = (m_TempDir / "temp").wstring();
829+
coordinator.Configure(settings);
830+
831+
HotReloadModuleEntry entry;
832+
entry.id = "com.bml.scripting";
833+
entry.dll_path = dll_path.wstring();
834+
entry.watch_path = m_TempDir.wstring();
835+
ASSERT_TRUE(coordinator.RegisterModule(entry));
836+
837+
std::optional<ReloadRequestKind> notified_kind;
838+
coordinator.SetNotifyCallback([&](const std::string &, ReloadResult, unsigned int, ReloadFailure,
839+
ReloadRequestKind kind) {
840+
notified_kind = kind;
841+
});
842+
843+
coordinator.Start();
844+
std::this_thread::sleep_for(500ms);
845+
846+
std::ofstream(dll_path, std::ios::app) << "modified";
847+
848+
auto deadline = std::chrono::steady_clock::now() + 10s;
849+
while (std::chrono::steady_clock::now() < deadline) {
850+
coordinator.Update();
851+
if (notified_kind.has_value()) {
852+
break;
853+
}
854+
std::this_thread::sleep_for(100ms);
855+
}
856+
857+
coordinator.Stop();
858+
859+
ASSERT_TRUE(notified_kind.has_value());
860+
EXPECT_EQ(*notified_kind, ReloadRequestKind::FullRuntime);
861+
EXPECT_EQ(m_Context->UnregisterRuntimeProvider(&kStubRuntimeProvider), BML_RESULT_OK);
862+
}

tests/HotReloadIntegrationTests.cpp

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include <vector>
1313

1414
#include "Core/ModuleRuntime.h"
15+
1516
#include "TestKernel.h"
1617
#include "TestKernelBuilder.h"
1718

@@ -109,6 +110,58 @@ void WriteManifest(const std::filesystem::path &manifest_path,
109110
manifest << "description = \"" << description << "\"\n";
110111
}
111112

113+
std::string FindManifestDescription(BML::Core::Context &context, const std::string &mod_id) {
114+
for (const auto &manifest : context.GetManifestSnapshot()) {
115+
if (manifest.package.id == mod_id) {
116+
return manifest.package.description;
117+
}
118+
}
119+
return {};
120+
}
121+
122+
void AddLoadedModule(BML::Core::Context &context,
123+
const std::string &mod_id,
124+
const std::string &name,
125+
const std::wstring &directory,
126+
const std::wstring &entry_path) {
127+
auto manifest = std::make_unique<BML::Core::ModManifest>();
128+
manifest->package.id = mod_id;
129+
manifest->package.name = name;
130+
manifest->package.version = "1.0.0";
131+
manifest->package.parsed_version = {1, 0, 0};
132+
manifest->package.entry = std::filesystem::path(entry_path).filename().string();
133+
manifest->directory = directory;
134+
manifest->manifest_path = (std::filesystem::path(directory) / (mod_id + ".toml")).wstring();
135+
136+
auto *manifest_ptr = manifest.get();
137+
context.RegisterManifest(std::move(manifest));
138+
139+
auto mod_handle = context.CreateModHandle(*manifest_ptr);
140+
ASSERT_NE(mod_handle, nullptr);
141+
142+
BML::Core::LoadedModule loaded;
143+
loaded.id = mod_id;
144+
loaded.manifest = manifest_ptr;
145+
loaded.handle = reinterpret_cast<HMODULE>(mod_handle.get());
146+
loaded.entrypoint = reinterpret_cast<PFN_BML_ModEntrypoint>(mod_handle.get());
147+
loaded.path = entry_path;
148+
loaded.mod_handle = std::move(mod_handle);
149+
context.AddLoadedModule(std::move(loaded));
150+
}
151+
152+
void WriteManifestForId(const std::filesystem::path &manifest_path,
153+
const std::string &mod_id,
154+
const std::string &description,
155+
const std::string &entry_name) {
156+
std::ofstream manifest(manifest_path);
157+
manifest << "[package]\n";
158+
manifest << "id = \"" << mod_id << "\"\n";
159+
manifest << "name = \"" << mod_id << "\"\n";
160+
manifest << "version = \"1.0.0\"\n";
161+
manifest << "entry = \"" << entry_name << "\"\n";
162+
manifest << "description = \"" << description << "\"\n";
163+
}
164+
112165
} // namespace
113166

114167
TEST(HotReloadIntegrationTests, ReloadsSampleModWhenManifestChanges) {
@@ -161,6 +214,53 @@ TEST(HotReloadIntegrationTests, ReloadsSampleModWhenManifestChanges) {
161214
EXPECT_TRUE(reload_diag.load_error.message.empty()) << reload_diag.load_error.message;
162215
}
163216

217+
TEST(HotReloadIntegrationTests, FullRuntimeNotificationReloadsNativeModuleManifest) {
218+
const auto sample_mod = GetSampleModPath();
219+
ASSERT_TRUE(std::filesystem::exists(sample_mod)) << "Sample mod missing: " << sample_mod.string();
220+
221+
const auto mods_dir = CreateModsDirectory();
222+
const auto run_root = mods_dir.parent_path();
223+
TempDirGuard temp_guard{run_root};
224+
225+
const auto mod_dir = mods_dir / "Sample";
226+
std::filesystem::create_directories(mod_dir);
227+
228+
const auto dll_name = sample_mod.filename();
229+
const auto dll_destination = mod_dir / dll_name;
230+
std::error_code copy_ec;
231+
std::filesystem::copy_file(sample_mod,
232+
dll_destination,
233+
std::filesystem::copy_options::overwrite_existing,
234+
copy_ec);
235+
ASSERT_FALSE(copy_ec) << copy_ec.message();
236+
237+
const auto manifest_path = mod_dir / "mod.toml";
238+
WriteManifest(manifest_path, "initial", dll_name.generic_string());
239+
240+
TestKernel kernel = TestKernelBuilder()
241+
.WithAll()
242+
.Build();
243+
BML::Core::ModuleRuntime runtime;
244+
runtime.BindKernel(*kernel);
245+
RuntimeGuard runtime_guard{runtime};
246+
247+
BML::Core::ModuleBootstrapDiagnostics discover_diag;
248+
ASSERT_TRUE(runtime.DiscoverAndValidate(mods_dir.wstring(), discover_diag))
249+
<< "Discover failed: " << discover_diag.dependency_error.message;
250+
251+
BML::Core::ModuleBootstrapDiagnostics initial_diag;
252+
BML_Services services{};
253+
ASSERT_TRUE(runtime.LoadDiscovered(initial_diag, &services))
254+
<< "Initial load failed: " << initial_diag.load_error.message;
255+
256+
WriteManifest(manifest_path, "reloaded", dll_name.generic_string());
257+
258+
runtime.TestHandleHotReloadNotify("hot.reload.sample", BML::Core::ReloadResult::Success, 0,
259+
BML::Core::ReloadFailure::None,
260+
BML::Core::ReloadRequestKind::FullRuntime);
261+
EXPECT_EQ(FindManifestDescription(*kernel->context, "hot.reload.sample"), "reloaded");
262+
}
263+
164264
TEST(HotReloadIntegrationTests, HandleHotReloadNotify_UnloadBlocked_FallsBackToFull) {
165265
const auto sample_mod = GetSampleModPath();
166266
ASSERT_TRUE(std::filesystem::exists(sample_mod)) << "Sample mod missing: " << sample_mod.string();
@@ -202,3 +302,26 @@ TEST(HotReloadIntegrationTests, HandleHotReloadNotify_UnloadBlocked_FallsBackToF
202302
EXPECT_TRUE(runtime.ReloadModules(reload_diag))
203303
<< "Full reload failed: " << reload_diag.load_error.message;
204304
}
305+
306+
TEST(HotReloadIntegrationTests, TargetedReloadPublishesLifecycleOnlyForAffectedModule) {
307+
TestKernel kernel = TestKernelBuilder()
308+
.WithAll()
309+
.Build();
310+
BML::Core::ModuleRuntime runtime;
311+
runtime.BindKernel(*kernel);
312+
313+
const auto mods_dir = CreateModsDirectory();
314+
const auto run_root = mods_dir.parent_path();
315+
TempDirGuard temp_guard{run_root};
316+
const auto target_dir = mods_dir / "Targeted";
317+
std::filesystem::create_directories(target_dir);
318+
319+
AddLoadedModule(*kernel->context, "hot.reload.sample.a", "Sample A",
320+
target_dir.wstring(), (target_dir / "SampleA.dll").wstring());
321+
AddLoadedModule(*kernel->context, "hot.reload.sample.b", "Sample B",
322+
target_dir.wstring(), (target_dir / "SampleB.dll").wstring());
323+
324+
const auto targets = runtime.TestGetLifecycleBroadcastTargets("hot.reload.sample.a");
325+
ASSERT_EQ(targets.size(), 1u);
326+
EXPECT_EQ(targets[0], "hot.reload.sample.a");
327+
}

0 commit comments

Comments
 (0)