|
6 | 6 | #include <filesystem> |
7 | 7 | #include <fstream> |
8 | 8 | #include <mutex> |
| 9 | +#include <optional> |
| 10 | +#include <string_view> |
9 | 11 | #include <thread> |
10 | 12 | #include <vector> |
11 | 13 |
|
@@ -49,6 +51,42 @@ void CreateMinimalDll(const std::filesystem::path& path) { |
49 | 51 | } |
50 | 52 | } |
51 | 53 |
|
| 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 | + |
52 | 90 | } // namespace |
53 | 91 |
|
54 | 92 | class HotReloadCoordinatorTest : public ::testing::Test { |
@@ -253,7 +291,8 @@ TEST_F(HotReloadCoordinatorTest, NotifyCallback) { |
253 | 291 | coordinator.SetNotifyCallback([&](const std::string& mod_id, |
254 | 292 | ReloadResult result, |
255 | 293 | unsigned int, |
256 | | - ReloadFailure) { |
| 294 | + ReloadFailure, |
| 295 | + ReloadRequestKind) { |
257 | 296 | notified_mod_id = mod_id; |
258 | 297 | notified_result = result; |
259 | 298 | callback_called = true; |
@@ -357,7 +396,8 @@ TEST_F(HotReloadCoordinatorTest, IgnoresUnrelatedRootFileChanges) { |
357 | 396 | ASSERT_TRUE(coordinator.RegisterModule(entry)); |
358 | 397 |
|
359 | 398 | 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) { |
361 | 401 | callback_count.fetch_add(1, std::memory_order_relaxed); |
362 | 402 | }); |
363 | 403 |
|
@@ -446,7 +486,8 @@ TEST_F(HotReloadCoordinatorTest, WatchesIncludedScriptFilesForRuntimeReload) { |
446 | 486 | ASSERT_TRUE(coordinator.RegisterModule(entry)); |
447 | 487 |
|
448 | 488 | 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) { |
450 | 491 | callback_count.fetch_add(1, std::memory_order_relaxed); |
451 | 492 | }); |
452 | 493 |
|
@@ -525,7 +566,8 @@ TEST_F(HotReloadCoordinatorTest, OnFileChanged_AddedEvent_TriggersReload) { |
525 | 566 | ASSERT_TRUE(coordinator.RegisterModule(entry)); |
526 | 567 |
|
527 | 568 | 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) { |
529 | 571 | callback_count.fetch_add(1, std::memory_order_relaxed); |
530 | 572 | }); |
531 | 573 |
|
@@ -590,14 +632,15 @@ TEST_F(HotReloadCoordinatorTest, OnFileChanged_DeletedEvent_DoesNotTriggerReload |
590 | 632 | ASSERT_TRUE(coordinator.RegisterModule(entry)); |
591 | 633 |
|
592 | 634 | 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) { |
594 | 637 | callback_count.fetch_add(1, std::memory_order_relaxed); |
595 | 638 | }); |
596 | 639 |
|
597 | 640 | coordinator.Start(); |
598 | 641 | std::this_thread::sleep_for(500ms); |
599 | 642 |
|
600 | | - // Delete a file — should NOT trigger reload |
| 643 | + // Delete a file - should NOT trigger reload |
601 | 644 | std::filesystem::remove(manifest_path); |
602 | 645 |
|
603 | 646 | auto deadline = std::chrono::steady_clock::now() + 1500ms; |
@@ -651,7 +694,8 @@ TEST_F(HotReloadCoordinatorTest, RecursiveWatch_ScriptModule_WatchesSubdirectori |
651 | 694 | ASSERT_TRUE(coordinator.RegisterModule(entry)); |
652 | 695 |
|
653 | 696 | 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) { |
655 | 699 | callback_count.fetch_add(1, std::memory_order_relaxed); |
656 | 700 | }); |
657 | 701 |
|
@@ -708,3 +752,111 @@ TEST_F(HotReloadCoordinatorTest, GetSlotModuleInfo_ReturnsSlotState) { |
708 | 752 | EXPECT_FALSE(coordinator.GetSlotModuleInfo("test.mod", &handle, &ep)); |
709 | 753 | EXPECT_FALSE(coordinator.GetSlotModuleInfo("nonexistent", &handle, &ep)); |
710 | 754 | } |
| 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 | +} |
0 commit comments