fix: Windows MinGW CTest compatibility - DLL loading and module paths
- Add cmake -E chdir wrapper for CTest on Windows to resolve DLL loading - Auto-copy MinGW runtime DLLs to build directories during configure - Fix module paths in integration tests (.so -> .dll for Windows) - Update grove_add_test macro for cross-platform test registration Tests now pass: 55% (16/29) on Windows MinGW 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
0540fbf526
commit
edf4d76844
@ -199,6 +199,24 @@ option(GROVE_BUILD_TESTS "Build GroveEngine tests" ON)
|
||||
|
||||
if(GROVE_BUILD_TESTS)
|
||||
enable_testing()
|
||||
|
||||
# Windows/MinGW: Copy runtime DLLs to build directories for CTest
|
||||
if(WIN32 AND MINGW)
|
||||
get_filename_component(MINGW_BIN_DIR ${CMAKE_CXX_COMPILER} DIRECTORY)
|
||||
set(MINGW_RUNTIME_DLLS libgcc_s_seh-1.dll libstdc++-6.dll libwinpthread-1.dll)
|
||||
|
||||
foreach(DLL ${MINGW_RUNTIME_DLLS})
|
||||
set(DLL_PATH "${MINGW_BIN_DIR}/${DLL}")
|
||||
if(EXISTS "${DLL_PATH}")
|
||||
file(COPY "${DLL_PATH}" DESTINATION "${CMAKE_BINARY_DIR}")
|
||||
file(COPY "${DLL_PATH}" DESTINATION "${CMAKE_BINARY_DIR}/tests")
|
||||
file(COPY "${DLL_PATH}" DESTINATION "${CMAKE_BINARY_DIR}/external/StillHammer/topictree/tests")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
message(STATUS "MinGW runtime DLLs copied for CTest")
|
||||
endif()
|
||||
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
|
||||
|
||||
@ -9,6 +9,17 @@ FetchContent_Declare(
|
||||
)
|
||||
FetchContent_MakeAvailable(Catch2)
|
||||
|
||||
# Windows/MinGW: Copy runtime DLLs at configure time
|
||||
if(WIN32 AND MINGW)
|
||||
get_filename_component(MINGW_BIN_DIR ${CMAKE_CXX_COMPILER} DIRECTORY)
|
||||
set(MINGW_DLLS libgcc_s_seh-1.dll libstdc++-6.dll libwinpthread-1.dll)
|
||||
foreach(DLL ${MINGW_DLLS})
|
||||
if(EXISTS "${MINGW_BIN_DIR}/${DLL}")
|
||||
file(COPY "${MINGW_BIN_DIR}/${DLL}" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}")
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
# Helper macro to create test executables
|
||||
macro(add_topictree_test test_name)
|
||||
add_executable(${test_name} ${test_name}.cpp)
|
||||
@ -17,7 +28,14 @@ macro(add_topictree_test test_name)
|
||||
Catch2::Catch2WithMain
|
||||
)
|
||||
target_compile_features(${test_name} PRIVATE cxx_std_17)
|
||||
add_test(NAME ${test_name} COMMAND ${test_name})
|
||||
if(WIN32)
|
||||
# Use cmd.exe start to properly find DLLs in working directory
|
||||
add_test(NAME ${test_name}
|
||||
COMMAND ${CMAKE_COMMAND} -E chdir ${CMAKE_CURRENT_BINARY_DIR}
|
||||
$<TARGET_FILE:${test_name}>)
|
||||
else()
|
||||
add_test(NAME ${test_name} COMMAND ${test_name} WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
|
||||
endif()
|
||||
endmacro()
|
||||
|
||||
# Add all scenario tests
|
||||
|
||||
2399
tests/CMakeLists.txt
2399
tests/CMakeLists.txt
File diff suppressed because it is too large
Load Diff
@ -1,267 +1,270 @@
|
||||
#include "grove/ModuleLoader.h"
|
||||
#include "grove/SequentialModuleSystem.h"
|
||||
#include "grove/JsonDataNode.h"
|
||||
#include "../helpers/TestMetrics.h"
|
||||
#include "../helpers/TestAssertions.h"
|
||||
#include "../helpers/TestReporter.h"
|
||||
#include "../helpers/SystemUtils.h"
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <fstream>
|
||||
#include <regex>
|
||||
|
||||
using namespace grove;
|
||||
|
||||
int main() {
|
||||
TestReporter reporter("Production Hot-Reload");
|
||||
TestMetrics metrics;
|
||||
|
||||
std::cout << "================================================================================\n";
|
||||
std::cout << "TEST: Production Hot-Reload\n";
|
||||
std::cout << "================================================================================\n\n";
|
||||
|
||||
// === SETUP ===
|
||||
std::cout << "Setup: Loading TankModule...\n";
|
||||
|
||||
ModuleLoader loader;
|
||||
auto moduleSystem = std::make_unique<SequentialModuleSystem>();
|
||||
|
||||
// Charger module
|
||||
std::string modulePath = "./libTankModule.so";
|
||||
auto module = loader.load(modulePath, "TankModule", false);
|
||||
|
||||
// Config
|
||||
nlohmann::json configJson;
|
||||
configJson["tankCount"] = 50;
|
||||
configJson["version"] = "v1.0";
|
||||
auto config = std::make_unique<JsonDataNode>("config", configJson);
|
||||
|
||||
// Initialiser (setConfiguration)
|
||||
module->setConfiguration(*config, nullptr, nullptr);
|
||||
|
||||
// Enregistrer dans system
|
||||
moduleSystem->registerModule("TankModule", std::move(module));
|
||||
|
||||
std::cout << " ✓ Module loaded and initialized\n\n";
|
||||
|
||||
// === PHASE 1: Pre-Reload (15s = 900 frames) ===
|
||||
std::cout << "Phase 1: Running 15s before reload...\n";
|
||||
|
||||
// Créer input avec deltaTime
|
||||
nlohmann::json inputJson;
|
||||
inputJson["deltaTime"] = 1.0 / 60.0;
|
||||
auto inputNode = std::make_unique<JsonDataNode>("input", inputJson);
|
||||
|
||||
for (int frame = 0; frame < 900; frame++) {
|
||||
auto frameStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
moduleSystem->processModules(1.0f / 60.0f);
|
||||
|
||||
auto frameEnd = std::chrono::high_resolution_clock::now();
|
||||
float frameTime = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count();
|
||||
|
||||
metrics.recordFPS(1000.0f / frameTime);
|
||||
|
||||
if (frame % 60 == 0) {
|
||||
metrics.recordMemoryUsage(getCurrentMemoryUsage());
|
||||
}
|
||||
|
||||
if (frame % 300 == 0) {
|
||||
std::cout << " Frame " << frame << "/900\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Snapshot state AVANT reload
|
||||
auto tankModule = moduleSystem->extractModule();
|
||||
auto preReloadState = tankModule->getState();
|
||||
|
||||
// Cast to JsonDataNode to access JSON
|
||||
auto* jsonNodeBefore = dynamic_cast<JsonDataNode*>(preReloadState.get());
|
||||
if (!jsonNodeBefore) {
|
||||
std::cerr << "❌ Failed to cast state to JsonDataNode\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
const auto& stateJsonBefore = jsonNodeBefore->getJsonData();
|
||||
|
||||
int tankCountBefore = stateJsonBefore["tanks"].size();
|
||||
std::string versionBefore = stateJsonBefore.value("version", "unknown");
|
||||
int frameCountBefore = stateJsonBefore.value("frameCount", 0);
|
||||
|
||||
std::cout << "\nState snapshot BEFORE reload:\n";
|
||||
std::cout << " Version: " << versionBefore << "\n";
|
||||
std::cout << " Tank count: " << tankCountBefore << "\n";
|
||||
std::cout << " Frame: " << frameCountBefore << "\n\n";
|
||||
|
||||
ASSERT_EQ(tankCountBefore, 50, "Should have 50 tanks before reload");
|
||||
|
||||
// Ré-enregistrer le module temporairement
|
||||
moduleSystem->registerModule("TankModule", std::move(tankModule));
|
||||
|
||||
// === HOT-RELOAD ===
|
||||
std::cout << "Triggering hot-reload...\n";
|
||||
|
||||
// Modifier version dans source (HEADER)
|
||||
std::cout << " 1. Modifying source code (v1.0 -> v2.0 HOT-RELOADED)...\n";
|
||||
|
||||
// Test runs from build/tests/, so source files are at ../../tests/modules/
|
||||
std::ifstream input("../../tests/modules/TankModule.h");
|
||||
std::string content((std::istreambuf_iterator<char>(input)), std::istreambuf_iterator<char>());
|
||||
input.close();
|
||||
|
||||
size_t pos = content.find("std::string moduleVersion = \"v1.0\";");
|
||||
if (pos != std::string::npos) {
|
||||
content.replace(pos, 39, "std::string moduleVersion = \"v2.0 HOT-RELOADED\";");
|
||||
}
|
||||
|
||||
std::ofstream output("../../tests/modules/TankModule.h");
|
||||
output << content;
|
||||
output.close();
|
||||
|
||||
// Recompiler
|
||||
std::cout << " 2. Recompiling module...\n";
|
||||
// Note: This test runs from build/tests/, so we use make -C .. to build from build directory
|
||||
int buildResult = system("make -C .. TankModule 2>&1 > /dev/null");
|
||||
if (buildResult != 0) {
|
||||
std::cerr << "❌ Compilation failed!\n";
|
||||
return 1;
|
||||
}
|
||||
std::cout << " ✓ Compilation succeeded\n";
|
||||
|
||||
// Wait for file to be ready (simulate file stability check)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
|
||||
// Reload
|
||||
std::cout << " 3. Reloading module...\n";
|
||||
auto reloadStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
// Extract module from system
|
||||
tankModule = moduleSystem->extractModule();
|
||||
|
||||
// Use ModuleLoader::reload()
|
||||
auto newModule = loader.reload(std::move(tankModule));
|
||||
|
||||
// Re-register
|
||||
moduleSystem->registerModule("TankModule", std::move(newModule));
|
||||
|
||||
auto reloadEnd = std::chrono::high_resolution_clock::now();
|
||||
float reloadTime = std::chrono::duration<float, std::milli>(reloadEnd - reloadStart).count();
|
||||
|
||||
metrics.recordReloadTime(reloadTime);
|
||||
reporter.addMetric("reload_time_ms", reloadTime);
|
||||
|
||||
std::cout << " ✓ Reload completed in " << reloadTime << "ms\n\n";
|
||||
|
||||
// === VÉRIFICATIONS POST-RELOAD ===
|
||||
std::cout << "Verifying state preservation...\n";
|
||||
|
||||
tankModule = moduleSystem->extractModule();
|
||||
auto postReloadState = tankModule->getState();
|
||||
auto* jsonNodeAfter = dynamic_cast<JsonDataNode*>(postReloadState.get());
|
||||
|
||||
if (!jsonNodeAfter) {
|
||||
std::cerr << "❌ Failed to cast post-reload state to JsonDataNode\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
const auto& stateJsonAfter = jsonNodeAfter->getJsonData();
|
||||
|
||||
int tankCountAfter = stateJsonAfter["tanks"].size();
|
||||
std::string versionAfter = stateJsonAfter.value("version", "unknown");
|
||||
int frameCountAfter = stateJsonAfter.value("frameCount", 0);
|
||||
|
||||
std::cout << "\nState snapshot AFTER reload:\n";
|
||||
std::cout << " Version: " << versionAfter << "\n";
|
||||
std::cout << " Tank count: " << tankCountAfter << "\n";
|
||||
std::cout << " Frame: " << frameCountAfter << "\n\n";
|
||||
|
||||
// Vérification 1: Nombre de tanks
|
||||
ASSERT_EQ(tankCountAfter, 50, "Should still have 50 tanks after reload");
|
||||
reporter.addAssertion("tank_count_preserved", tankCountAfter == 50);
|
||||
|
||||
// Vérification 2: Version mise à jour
|
||||
bool versionUpdated = versionAfter.find("v2.0") != std::string::npos;
|
||||
ASSERT_TRUE(versionUpdated, "Version should be updated to v2.0");
|
||||
reporter.addAssertion("version_updated", versionUpdated);
|
||||
|
||||
// Vérification 3: Frame count préservé
|
||||
ASSERT_EQ(frameCountAfter, frameCountBefore, "Frame count should be preserved");
|
||||
reporter.addAssertion("framecount_preserved", frameCountAfter == frameCountBefore);
|
||||
|
||||
std::cout << " ✓ State preserved correctly\n";
|
||||
|
||||
// Ré-enregistrer module
|
||||
moduleSystem->registerModule("TankModule", std::move(tankModule));
|
||||
|
||||
// === PHASE 2: Post-Reload (15s = 900 frames) ===
|
||||
std::cout << "\nPhase 2: Running 15s after reload...\n";
|
||||
|
||||
for (int frame = 0; frame < 900; frame++) {
|
||||
auto frameStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
moduleSystem->processModules(1.0f / 60.0f);
|
||||
|
||||
auto frameEnd = std::chrono::high_resolution_clock::now();
|
||||
float frameTime = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count();
|
||||
|
||||
metrics.recordFPS(1000.0f / frameTime);
|
||||
|
||||
if (frame % 60 == 0) {
|
||||
metrics.recordMemoryUsage(getCurrentMemoryUsage());
|
||||
}
|
||||
|
||||
if (frame % 300 == 0) {
|
||||
std::cout << " Frame " << frame << "/900\n";
|
||||
}
|
||||
}
|
||||
|
||||
// === VÉRIFICATIONS FINALES ===
|
||||
std::cout << "\nFinal verifications...\n";
|
||||
|
||||
// Memory growth
|
||||
size_t memGrowth = metrics.getMemoryGrowth();
|
||||
float memGrowthMB = memGrowth / (1024.0f * 1024.0f);
|
||||
ASSERT_LT(memGrowthMB, 5.0f, "Memory growth should be < 5MB");
|
||||
reporter.addMetric("memory_growth_mb", memGrowthMB);
|
||||
|
||||
// FPS
|
||||
float minFPS = metrics.getFPSMin();
|
||||
ASSERT_GT(minFPS, 30.0f, "Min FPS should be > 30");
|
||||
reporter.addMetric("fps_min", minFPS);
|
||||
reporter.addMetric("fps_avg", metrics.getFPSAvg());
|
||||
reporter.addMetric("fps_max", metrics.getFPSMax());
|
||||
|
||||
// Reload time
|
||||
ASSERT_LT(reloadTime, 1000.0f, "Reload time should be < 1000ms");
|
||||
|
||||
// No crashes
|
||||
reporter.addAssertion("no_crashes", true);
|
||||
|
||||
// === CLEANUP ===
|
||||
std::cout << "\nCleaning up...\n";
|
||||
|
||||
// Restaurer version originale (HEADER)
|
||||
std::ifstream inputRestore("../../tests/modules/TankModule.h");
|
||||
std::string contentRestore((std::istreambuf_iterator<char>(inputRestore)), std::istreambuf_iterator<char>());
|
||||
inputRestore.close();
|
||||
|
||||
pos = contentRestore.find("std::string moduleVersion = \"v2.0 HOT-RELOADED\";");
|
||||
if (pos != std::string::npos) {
|
||||
contentRestore.replace(pos, 50, "std::string moduleVersion = \"v1.0\";");
|
||||
}
|
||||
|
||||
std::ofstream outputRestore("../../tests/modules/TankModule.h");
|
||||
outputRestore << contentRestore;
|
||||
outputRestore.close();
|
||||
|
||||
// Rebuild to restore original version (test runs from build/tests/)
|
||||
system("make -C .. TankModule 2>&1 > /dev/null");
|
||||
|
||||
// === RAPPORTS ===
|
||||
std::cout << "\n";
|
||||
metrics.printReport();
|
||||
reporter.printFinalReport();
|
||||
|
||||
return reporter.getExitCode();
|
||||
}
|
||||
#include "grove/ModuleLoader.h"
|
||||
#include "grove/SequentialModuleSystem.h"
|
||||
#include "grove/JsonDataNode.h"
|
||||
#include "../helpers/TestMetrics.h"
|
||||
#include "../helpers/TestAssertions.h"
|
||||
#include "../helpers/TestReporter.h"
|
||||
#include "../helpers/SystemUtils.h"
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <fstream>
|
||||
#include <regex>
|
||||
|
||||
using namespace grove;
|
||||
|
||||
int main() {
|
||||
TestReporter reporter("Production Hot-Reload");
|
||||
TestMetrics metrics;
|
||||
|
||||
std::cout << "================================================================================\n";
|
||||
std::cout << "TEST: Production Hot-Reload\n";
|
||||
std::cout << "================================================================================\n\n";
|
||||
|
||||
// === SETUP ===
|
||||
std::cout << "Setup: Loading TankModule...\n";
|
||||
|
||||
ModuleLoader loader;
|
||||
auto moduleSystem = std::make_unique<SequentialModuleSystem>();
|
||||
|
||||
// Charger module
|
||||
std::string modulePath = "./libTankModule.so";
|
||||
#ifdef _WIN32
|
||||
modulePath = "./libTankModule.dll";
|
||||
#endif
|
||||
auto module = loader.load(modulePath, "TankModule", false);
|
||||
|
||||
// Config
|
||||
nlohmann::json configJson;
|
||||
configJson["tankCount"] = 50;
|
||||
configJson["version"] = "v1.0";
|
||||
auto config = std::make_unique<JsonDataNode>("config", configJson);
|
||||
|
||||
// Initialiser (setConfiguration)
|
||||
module->setConfiguration(*config, nullptr, nullptr);
|
||||
|
||||
// Enregistrer dans system
|
||||
moduleSystem->registerModule("TankModule", std::move(module));
|
||||
|
||||
std::cout << " ✓ Module loaded and initialized\n\n";
|
||||
|
||||
// === PHASE 1: Pre-Reload (15s = 900 frames) ===
|
||||
std::cout << "Phase 1: Running 15s before reload...\n";
|
||||
|
||||
// Créer input avec deltaTime
|
||||
nlohmann::json inputJson;
|
||||
inputJson["deltaTime"] = 1.0 / 60.0;
|
||||
auto inputNode = std::make_unique<JsonDataNode>("input", inputJson);
|
||||
|
||||
for (int frame = 0; frame < 900; frame++) {
|
||||
auto frameStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
moduleSystem->processModules(1.0f / 60.0f);
|
||||
|
||||
auto frameEnd = std::chrono::high_resolution_clock::now();
|
||||
float frameTime = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count();
|
||||
|
||||
metrics.recordFPS(1000.0f / frameTime);
|
||||
|
||||
if (frame % 60 == 0) {
|
||||
metrics.recordMemoryUsage(getCurrentMemoryUsage());
|
||||
}
|
||||
|
||||
if (frame % 300 == 0) {
|
||||
std::cout << " Frame " << frame << "/900\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Snapshot state AVANT reload
|
||||
auto tankModule = moduleSystem->extractModule();
|
||||
auto preReloadState = tankModule->getState();
|
||||
|
||||
// Cast to JsonDataNode to access JSON
|
||||
auto* jsonNodeBefore = dynamic_cast<JsonDataNode*>(preReloadState.get());
|
||||
if (!jsonNodeBefore) {
|
||||
std::cerr << "❌ Failed to cast state to JsonDataNode\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
const auto& stateJsonBefore = jsonNodeBefore->getJsonData();
|
||||
|
||||
int tankCountBefore = stateJsonBefore["tanks"].size();
|
||||
std::string versionBefore = stateJsonBefore.value("version", "unknown");
|
||||
int frameCountBefore = stateJsonBefore.value("frameCount", 0);
|
||||
|
||||
std::cout << "\nState snapshot BEFORE reload:\n";
|
||||
std::cout << " Version: " << versionBefore << "\n";
|
||||
std::cout << " Tank count: " << tankCountBefore << "\n";
|
||||
std::cout << " Frame: " << frameCountBefore << "\n\n";
|
||||
|
||||
ASSERT_EQ(tankCountBefore, 50, "Should have 50 tanks before reload");
|
||||
|
||||
// Ré-enregistrer le module temporairement
|
||||
moduleSystem->registerModule("TankModule", std::move(tankModule));
|
||||
|
||||
// === HOT-RELOAD ===
|
||||
std::cout << "Triggering hot-reload...\n";
|
||||
|
||||
// Modifier version dans source (HEADER)
|
||||
std::cout << " 1. Modifying source code (v1.0 -> v2.0 HOT-RELOADED)...\n";
|
||||
|
||||
// Test runs from build/tests/, so source files are at ../../tests/modules/
|
||||
std::ifstream input("../../tests/modules/TankModule.h");
|
||||
std::string content((std::istreambuf_iterator<char>(input)), std::istreambuf_iterator<char>());
|
||||
input.close();
|
||||
|
||||
size_t pos = content.find("std::string moduleVersion = \"v1.0\";");
|
||||
if (pos != std::string::npos) {
|
||||
content.replace(pos, 39, "std::string moduleVersion = \"v2.0 HOT-RELOADED\";");
|
||||
}
|
||||
|
||||
std::ofstream output("../../tests/modules/TankModule.h");
|
||||
output << content;
|
||||
output.close();
|
||||
|
||||
// Recompiler
|
||||
std::cout << " 2. Recompiling module...\n";
|
||||
// Note: This test runs from build/tests/, so we use make -C .. to build from build directory
|
||||
int buildResult = system("make -C .. TankModule 2>&1 > /dev/null");
|
||||
if (buildResult != 0) {
|
||||
std::cerr << "❌ Compilation failed!\n";
|
||||
return 1;
|
||||
}
|
||||
std::cout << " ✓ Compilation succeeded\n";
|
||||
|
||||
// Wait for file to be ready (simulate file stability check)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
|
||||
// Reload
|
||||
std::cout << " 3. Reloading module...\n";
|
||||
auto reloadStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
// Extract module from system
|
||||
tankModule = moduleSystem->extractModule();
|
||||
|
||||
// Use ModuleLoader::reload()
|
||||
auto newModule = loader.reload(std::move(tankModule));
|
||||
|
||||
// Re-register
|
||||
moduleSystem->registerModule("TankModule", std::move(newModule));
|
||||
|
||||
auto reloadEnd = std::chrono::high_resolution_clock::now();
|
||||
float reloadTime = std::chrono::duration<float, std::milli>(reloadEnd - reloadStart).count();
|
||||
|
||||
metrics.recordReloadTime(reloadTime);
|
||||
reporter.addMetric("reload_time_ms", reloadTime);
|
||||
|
||||
std::cout << " ✓ Reload completed in " << reloadTime << "ms\n\n";
|
||||
|
||||
// === VÉRIFICATIONS POST-RELOAD ===
|
||||
std::cout << "Verifying state preservation...\n";
|
||||
|
||||
tankModule = moduleSystem->extractModule();
|
||||
auto postReloadState = tankModule->getState();
|
||||
auto* jsonNodeAfter = dynamic_cast<JsonDataNode*>(postReloadState.get());
|
||||
|
||||
if (!jsonNodeAfter) {
|
||||
std::cerr << "❌ Failed to cast post-reload state to JsonDataNode\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
const auto& stateJsonAfter = jsonNodeAfter->getJsonData();
|
||||
|
||||
int tankCountAfter = stateJsonAfter["tanks"].size();
|
||||
std::string versionAfter = stateJsonAfter.value("version", "unknown");
|
||||
int frameCountAfter = stateJsonAfter.value("frameCount", 0);
|
||||
|
||||
std::cout << "\nState snapshot AFTER reload:\n";
|
||||
std::cout << " Version: " << versionAfter << "\n";
|
||||
std::cout << " Tank count: " << tankCountAfter << "\n";
|
||||
std::cout << " Frame: " << frameCountAfter << "\n\n";
|
||||
|
||||
// Vérification 1: Nombre de tanks
|
||||
ASSERT_EQ(tankCountAfter, 50, "Should still have 50 tanks after reload");
|
||||
reporter.addAssertion("tank_count_preserved", tankCountAfter == 50);
|
||||
|
||||
// Vérification 2: Version mise à jour
|
||||
bool versionUpdated = versionAfter.find("v2.0") != std::string::npos;
|
||||
ASSERT_TRUE(versionUpdated, "Version should be updated to v2.0");
|
||||
reporter.addAssertion("version_updated", versionUpdated);
|
||||
|
||||
// Vérification 3: Frame count préservé
|
||||
ASSERT_EQ(frameCountAfter, frameCountBefore, "Frame count should be preserved");
|
||||
reporter.addAssertion("framecount_preserved", frameCountAfter == frameCountBefore);
|
||||
|
||||
std::cout << " ✓ State preserved correctly\n";
|
||||
|
||||
// Ré-enregistrer module
|
||||
moduleSystem->registerModule("TankModule", std::move(tankModule));
|
||||
|
||||
// === PHASE 2: Post-Reload (15s = 900 frames) ===
|
||||
std::cout << "\nPhase 2: Running 15s after reload...\n";
|
||||
|
||||
for (int frame = 0; frame < 900; frame++) {
|
||||
auto frameStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
moduleSystem->processModules(1.0f / 60.0f);
|
||||
|
||||
auto frameEnd = std::chrono::high_resolution_clock::now();
|
||||
float frameTime = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count();
|
||||
|
||||
metrics.recordFPS(1000.0f / frameTime);
|
||||
|
||||
if (frame % 60 == 0) {
|
||||
metrics.recordMemoryUsage(getCurrentMemoryUsage());
|
||||
}
|
||||
|
||||
if (frame % 300 == 0) {
|
||||
std::cout << " Frame " << frame << "/900\n";
|
||||
}
|
||||
}
|
||||
|
||||
// === VÉRIFICATIONS FINALES ===
|
||||
std::cout << "\nFinal verifications...\n";
|
||||
|
||||
// Memory growth
|
||||
size_t memGrowth = metrics.getMemoryGrowth();
|
||||
float memGrowthMB = memGrowth / (1024.0f * 1024.0f);
|
||||
ASSERT_LT(memGrowthMB, 5.0f, "Memory growth should be < 5MB");
|
||||
reporter.addMetric("memory_growth_mb", memGrowthMB);
|
||||
|
||||
// FPS
|
||||
float minFPS = metrics.getFPSMin();
|
||||
ASSERT_GT(minFPS, 30.0f, "Min FPS should be > 30");
|
||||
reporter.addMetric("fps_min", minFPS);
|
||||
reporter.addMetric("fps_avg", metrics.getFPSAvg());
|
||||
reporter.addMetric("fps_max", metrics.getFPSMax());
|
||||
|
||||
// Reload time
|
||||
ASSERT_LT(reloadTime, 1000.0f, "Reload time should be < 1000ms");
|
||||
|
||||
// No crashes
|
||||
reporter.addAssertion("no_crashes", true);
|
||||
|
||||
// === CLEANUP ===
|
||||
std::cout << "\nCleaning up...\n";
|
||||
|
||||
// Restaurer version originale (HEADER)
|
||||
std::ifstream inputRestore("../../tests/modules/TankModule.h");
|
||||
std::string contentRestore((std::istreambuf_iterator<char>(inputRestore)), std::istreambuf_iterator<char>());
|
||||
inputRestore.close();
|
||||
|
||||
pos = contentRestore.find("std::string moduleVersion = \"v2.0 HOT-RELOADED\";");
|
||||
if (pos != std::string::npos) {
|
||||
contentRestore.replace(pos, 50, "std::string moduleVersion = \"v1.0\";");
|
||||
}
|
||||
|
||||
std::ofstream outputRestore("../../tests/modules/TankModule.h");
|
||||
outputRestore << contentRestore;
|
||||
outputRestore.close();
|
||||
|
||||
// Rebuild to restore original version (test runs from build/tests/)
|
||||
system("make -C .. TankModule 2>&1 > /dev/null");
|
||||
|
||||
// === RAPPORTS ===
|
||||
std::cout << "\n";
|
||||
metrics.printReport();
|
||||
reporter.printFinalReport();
|
||||
|
||||
return reporter.getExitCode();
|
||||
}
|
||||
|
||||
@ -1,255 +1,258 @@
|
||||
// PREUVE : Décommenter cette ligne pour désactiver la recovery et voir le test ÉCHOUER
|
||||
// #define DISABLE_RECOVERY_FOR_TEST
|
||||
|
||||
#include "grove/ModuleLoader.h"
|
||||
#include "grove/SequentialModuleSystem.h"
|
||||
#include "grove/JsonDataNode.h"
|
||||
#include "../helpers/TestMetrics.h"
|
||||
#include "../helpers/TestAssertions.h"
|
||||
#include "../helpers/TestReporter.h"
|
||||
#include "../helpers/SystemUtils.h"
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <csignal>
|
||||
#include <atomic>
|
||||
|
||||
using namespace grove;
|
||||
|
||||
// Global for crash detection
|
||||
static std::atomic<bool> engineCrashed{false};
|
||||
|
||||
void signalHandler(int signal) {
|
||||
if (signal == SIGSEGV || signal == SIGABRT) {
|
||||
engineCrashed.store(true);
|
||||
std::cerr << "❌ FATAL: Signal " << signal << " received (SIGSEGV or SIGABRT)\n";
|
||||
std::cerr << "Engine has crashed unrecoverably.\n";
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
TestReporter reporter("Chaos Monkey");
|
||||
TestMetrics metrics;
|
||||
|
||||
std::cout << "================================================================================\n";
|
||||
std::cout << "TEST: Chaos Monkey\n";
|
||||
std::cout << "================================================================================\n\n";
|
||||
|
||||
// Setup signal handlers
|
||||
std::signal(SIGSEGV, signalHandler);
|
||||
std::signal(SIGABRT, signalHandler);
|
||||
|
||||
// === SETUP ===
|
||||
std::cout << "Setup: Loading ChaosModule...\n";
|
||||
|
||||
ModuleLoader loader;
|
||||
auto moduleSystem = std::make_unique<SequentialModuleSystem>();
|
||||
|
||||
// Load module
|
||||
std::string modulePath = "./libChaosModule.so";
|
||||
auto module = loader.load(modulePath, "ChaosModule", false);
|
||||
|
||||
// Configure module avec seed ALÉATOIRE basé sur le temps
|
||||
// Chaque run sera différent - VRAI chaos
|
||||
unsigned int randomSeed = static_cast<unsigned int>(std::chrono::system_clock::now().time_since_epoch().count());
|
||||
|
||||
nlohmann::json configJson;
|
||||
configJson["seed"] = randomSeed;
|
||||
configJson["hotReloadProbability"] = 0.30; // Non utilisé maintenant
|
||||
configJson["crashProbability"] = 0.05; // 5% par frame = crash fréquent
|
||||
configJson["corruptionProbability"] = 0.10; // Non utilisé
|
||||
configJson["invalidConfigProbability"] = 0.05; // Non utilisé
|
||||
auto config = std::make_unique<JsonDataNode>("config", configJson);
|
||||
|
||||
std::cout << " Random seed: " << randomSeed << " (time-based, unpredictable)\n";
|
||||
|
||||
module->setConfiguration(*config, nullptr, nullptr);
|
||||
|
||||
// Register in module system
|
||||
moduleSystem->registerModule("ChaosModule", std::move(module));
|
||||
|
||||
std::cout << " ✓ ChaosModule loaded and configured\n\n";
|
||||
|
||||
// === CHAOS LOOP (30 seconds = 1800 frames @ 60 FPS) ===
|
||||
// NOTE: Reduced from 5 minutes for faster testing
|
||||
std::cout << "Starting Chaos Monkey (30 seconds simulation)...\n";
|
||||
std::cout << "REAL CHAOS MODE:\n";
|
||||
std::cout << " - 5% crash probability PER FRAME (not per second)\n";
|
||||
std::cout << " - Expected crashes: ~90 crashes (5% of 1800 frames)\n";
|
||||
std::cout << " - Random seed (time-based): unpredictable pattern\n";
|
||||
std::cout << " - Multiple crash types: runtime_error, logic_error, out_of_range, domain_error, state corruption\n";
|
||||
std::cout << " - Corrupted state validation: module must reject corrupted state\n\n";
|
||||
|
||||
const int totalFrames = 1800; // 30 * 60
|
||||
int crashesDetected = 0;
|
||||
int reloadsTriggered = 0;
|
||||
int recoverySuccesses = 0;
|
||||
bool hadDeadlock = false;
|
||||
|
||||
auto testStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
for (int frame = 0; frame < totalFrames; frame++) {
|
||||
auto frameStart = std::chrono::high_resolution_clock::now();
|
||||
bool didRecoveryThisFrame = false;
|
||||
|
||||
try {
|
||||
// Process module (1/60th of a second)
|
||||
moduleSystem->processModules(1.0f / 60.0f);
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
// CRASH DETECTED - Attempt recovery
|
||||
crashesDetected++;
|
||||
std::cout << " [Frame " << frame << "] ⚠️ Crash detected: " << e.what() << "\n";
|
||||
|
||||
// PREUVE QUE LE TEST PEUT ÉCHOUER : désactiver la recovery
|
||||
#ifdef DISABLE_RECOVERY_FOR_TEST
|
||||
std::cout << " [Frame " << frame << "] ❌ RECOVERY DISABLED - Test will fail\n";
|
||||
reporter.addAssertion("recovery_disabled", false);
|
||||
break; // Le test DOIT échouer
|
||||
#endif
|
||||
|
||||
// Recovery attempt
|
||||
try {
|
||||
std::cout << " [Frame " << frame << "] 🔄 Attempting recovery...\n";
|
||||
|
||||
auto recoveryStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
// Extract module from system
|
||||
auto crashedModule = moduleSystem->extractModule();
|
||||
|
||||
// Reload module
|
||||
auto reloadedModule = loader.reload(std::move(crashedModule));
|
||||
|
||||
// Re-register
|
||||
moduleSystem->registerModule("ChaosModule", std::move(reloadedModule));
|
||||
|
||||
auto recoveryEnd = std::chrono::high_resolution_clock::now();
|
||||
float recoveryTime = std::chrono::duration<float, std::milli>(recoveryEnd - recoveryStart).count();
|
||||
|
||||
metrics.recordReloadTime(recoveryTime);
|
||||
recoverySuccesses++;
|
||||
didRecoveryThisFrame = true;
|
||||
|
||||
std::cout << " [Frame " << frame << "] ✅ Recovery successful (" << recoveryTime << "ms)\n";
|
||||
|
||||
} catch (const std::exception& recoveryError) {
|
||||
std::cout << " [Frame " << frame << "] ❌ Recovery FAILED: " << recoveryError.what() << "\n";
|
||||
reporter.addAssertion("recovery_failed", false);
|
||||
break; // Stop test - recovery failed
|
||||
}
|
||||
}
|
||||
|
||||
// Metrics
|
||||
auto frameEnd = std::chrono::high_resolution_clock::now();
|
||||
float frameTime = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count();
|
||||
|
||||
// Only record FPS for normal frames (not recovery frames)
|
||||
// Recovery frames are slow by design (100+ ms for hot-reload)
|
||||
if (!didRecoveryThisFrame) {
|
||||
metrics.recordFPS(1000.0f / frameTime);
|
||||
}
|
||||
|
||||
if (frame % 60 == 0) {
|
||||
metrics.recordMemoryUsage(getCurrentMemoryUsage());
|
||||
}
|
||||
|
||||
// Deadlock detection (frame > 100ms)
|
||||
// NOTE: Skip deadlock check if we just did a recovery (recovery takes >100ms by design)
|
||||
if (frameTime > 100.0f && !didRecoveryThisFrame) {
|
||||
std::cout << " [Frame " << frame << "] ⚠️ Potential deadlock (frame time: " << frameTime << "ms)\n";
|
||||
hadDeadlock = true;
|
||||
}
|
||||
|
||||
// Progress (every 600 frames = 10 seconds)
|
||||
if (frame % 600 == 0 && frame > 0) {
|
||||
float elapsedSec = frame / 60.0f;
|
||||
float progress = (frame * 100.0f) / totalFrames;
|
||||
std::cout << "Progress: " << elapsedSec << "/30.0 seconds (" << (int)progress << "%)\n";
|
||||
|
||||
// Show current metrics
|
||||
std::cout << " FPS: min=" << metrics.getFPSMin() << ", avg=" << metrics.getFPSAvg() << ", max=" << metrics.getFPSMax() << "\n";
|
||||
std::cout << " Memory: " << (getCurrentMemoryUsage() / (1024.0f * 1024.0f)) << " MB\n";
|
||||
}
|
||||
|
||||
// Check if engine crashed externally
|
||||
if (engineCrashed.load()) {
|
||||
std::cout << " [Frame " << frame << "] ❌ Engine crashed externally (signal received)\n";
|
||||
reporter.addAssertion("engine_crashed_externally", false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto testEnd = std::chrono::high_resolution_clock::now();
|
||||
float totalDuration = std::chrono::duration<float>(testEnd - testStart).count();
|
||||
|
||||
std::cout << "\nTest completed!\n\n";
|
||||
|
||||
// === FINAL VERIFICATIONS ===
|
||||
std::cout << "Final verifications...\n";
|
||||
|
||||
// Engine still alive
|
||||
bool engineAlive = !engineCrashed.load();
|
||||
ASSERT_TRUE(engineAlive, "Engine should still be alive");
|
||||
reporter.addAssertion("engine_alive", engineAlive);
|
||||
|
||||
// No deadlocks
|
||||
ASSERT_FALSE(hadDeadlock, "Should not have deadlocks");
|
||||
reporter.addAssertion("no_deadlocks", !hadDeadlock);
|
||||
|
||||
// Memory growth < 10MB
|
||||
size_t memGrowth = metrics.getMemoryGrowth();
|
||||
float memGrowthMB = memGrowth / (1024.0f * 1024.0f);
|
||||
ASSERT_LT(memGrowthMB, 10.0f, "Memory growth should be < 10MB");
|
||||
reporter.addMetric("memory_growth_mb", memGrowthMB);
|
||||
|
||||
// Test runs as fast as possible (not real-time)
|
||||
// Just check it completed within reasonable bounds (< 60 seconds wall time)
|
||||
ASSERT_LT(totalDuration, 60.0f, "Total duration should be < 60 seconds");
|
||||
reporter.addMetric("total_duration_sec", totalDuration);
|
||||
|
||||
// FPS metrics
|
||||
float minFPS = metrics.getFPSMin();
|
||||
float avgFPS = metrics.getFPSAvg();
|
||||
float maxFPS = metrics.getFPSMax();
|
||||
|
||||
// Min FPS should be reasonable (> 10 even with crashes)
|
||||
ASSERT_GT(minFPS, 10.0f, "Min FPS should be > 10");
|
||||
reporter.addMetric("fps_min", minFPS);
|
||||
reporter.addMetric("fps_avg", avgFPS);
|
||||
reporter.addMetric("fps_max", maxFPS);
|
||||
|
||||
// Recovery rate > 95%
|
||||
float recoveryRate = (crashesDetected > 0) ? (recoverySuccesses * 100.0f / crashesDetected) : 100.0f;
|
||||
ASSERT_GT(recoveryRate, 95.0f, "Recovery rate should be > 95%");
|
||||
reporter.addMetric("recovery_rate_percent", recoveryRate);
|
||||
|
||||
// === STATISTICS ===
|
||||
std::cout << "\n";
|
||||
std::cout << "================================================================================\n";
|
||||
std::cout << "CHAOS MONKEY STATISTICS\n";
|
||||
std::cout << "================================================================================\n";
|
||||
std::cout << " Total frames: " << totalFrames << "\n";
|
||||
std::cout << " Duration: " << totalDuration << "s (wall time, not simulation time)\n";
|
||||
std::cout << " Crashes detected: " << crashesDetected << "\n";
|
||||
std::cout << " Recovery successes: " << recoverySuccesses << "\n";
|
||||
std::cout << " Recovery rate: " << recoveryRate << "%\n";
|
||||
std::cout << " Memory growth: " << memGrowthMB << " MB (max: 10MB)\n";
|
||||
std::cout << " Had deadlocks: " << (hadDeadlock ? "YES ❌" : "NO ✅") << "\n";
|
||||
std::cout << " FPS min/avg/max: " << minFPS << " / " << avgFPS << " / " << maxFPS << "\n";
|
||||
std::cout << "================================================================================\n\n";
|
||||
|
||||
std::cout << "Note: ChaosModule generates random crashes internally.\n";
|
||||
std::cout << "The test should recover from ALL crashes automatically via hot-reload.\n\n";
|
||||
|
||||
// === CLEANUP ===
|
||||
std::cout << "Cleaning up...\n";
|
||||
moduleSystem.reset();
|
||||
std::cout << " ✓ Module system shutdown complete\n\n";
|
||||
|
||||
// === REPORTS ===
|
||||
metrics.printReport();
|
||||
reporter.printFinalReport();
|
||||
|
||||
return reporter.getExitCode();
|
||||
}
|
||||
// PREUVE : Décommenter cette ligne pour désactiver la recovery et voir le test ÉCHOUER
|
||||
// #define DISABLE_RECOVERY_FOR_TEST
|
||||
|
||||
#include "grove/ModuleLoader.h"
|
||||
#include "grove/SequentialModuleSystem.h"
|
||||
#include "grove/JsonDataNode.h"
|
||||
#include "../helpers/TestMetrics.h"
|
||||
#include "../helpers/TestAssertions.h"
|
||||
#include "../helpers/TestReporter.h"
|
||||
#include "../helpers/SystemUtils.h"
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <csignal>
|
||||
#include <atomic>
|
||||
|
||||
using namespace grove;
|
||||
|
||||
// Global for crash detection
|
||||
static std::atomic<bool> engineCrashed{false};
|
||||
|
||||
void signalHandler(int signal) {
|
||||
if (signal == SIGSEGV || signal == SIGABRT) {
|
||||
engineCrashed.store(true);
|
||||
std::cerr << "❌ FATAL: Signal " << signal << " received (SIGSEGV or SIGABRT)\n";
|
||||
std::cerr << "Engine has crashed unrecoverably.\n";
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
TestReporter reporter("Chaos Monkey");
|
||||
TestMetrics metrics;
|
||||
|
||||
std::cout << "================================================================================\n";
|
||||
std::cout << "TEST: Chaos Monkey\n";
|
||||
std::cout << "================================================================================\n\n";
|
||||
|
||||
// Setup signal handlers
|
||||
std::signal(SIGSEGV, signalHandler);
|
||||
std::signal(SIGABRT, signalHandler);
|
||||
|
||||
// === SETUP ===
|
||||
std::cout << "Setup: Loading ChaosModule...\n";
|
||||
|
||||
ModuleLoader loader;
|
||||
auto moduleSystem = std::make_unique<SequentialModuleSystem>();
|
||||
|
||||
// Load module
|
||||
std::string modulePath = "./libChaosModule.so";
|
||||
#ifdef _WIN32
|
||||
modulePath = "./libChaosModule.dll";
|
||||
#endif
|
||||
auto module = loader.load(modulePath, "ChaosModule", false);
|
||||
|
||||
// Configure module avec seed ALÉATOIRE basé sur le temps
|
||||
// Chaque run sera différent - VRAI chaos
|
||||
unsigned int randomSeed = static_cast<unsigned int>(std::chrono::system_clock::now().time_since_epoch().count());
|
||||
|
||||
nlohmann::json configJson;
|
||||
configJson["seed"] = randomSeed;
|
||||
configJson["hotReloadProbability"] = 0.30; // Non utilisé maintenant
|
||||
configJson["crashProbability"] = 0.05; // 5% par frame = crash fréquent
|
||||
configJson["corruptionProbability"] = 0.10; // Non utilisé
|
||||
configJson["invalidConfigProbability"] = 0.05; // Non utilisé
|
||||
auto config = std::make_unique<JsonDataNode>("config", configJson);
|
||||
|
||||
std::cout << " Random seed: " << randomSeed << " (time-based, unpredictable)\n";
|
||||
|
||||
module->setConfiguration(*config, nullptr, nullptr);
|
||||
|
||||
// Register in module system
|
||||
moduleSystem->registerModule("ChaosModule", std::move(module));
|
||||
|
||||
std::cout << " ✓ ChaosModule loaded and configured\n\n";
|
||||
|
||||
// === CHAOS LOOP (30 seconds = 1800 frames @ 60 FPS) ===
|
||||
// NOTE: Reduced from 5 minutes for faster testing
|
||||
std::cout << "Starting Chaos Monkey (30 seconds simulation)...\n";
|
||||
std::cout << "REAL CHAOS MODE:\n";
|
||||
std::cout << " - 5% crash probability PER FRAME (not per second)\n";
|
||||
std::cout << " - Expected crashes: ~90 crashes (5% of 1800 frames)\n";
|
||||
std::cout << " - Random seed (time-based): unpredictable pattern\n";
|
||||
std::cout << " - Multiple crash types: runtime_error, logic_error, out_of_range, domain_error, state corruption\n";
|
||||
std::cout << " - Corrupted state validation: module must reject corrupted state\n\n";
|
||||
|
||||
const int totalFrames = 1800; // 30 * 60
|
||||
int crashesDetected = 0;
|
||||
int reloadsTriggered = 0;
|
||||
int recoverySuccesses = 0;
|
||||
bool hadDeadlock = false;
|
||||
|
||||
auto testStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
for (int frame = 0; frame < totalFrames; frame++) {
|
||||
auto frameStart = std::chrono::high_resolution_clock::now();
|
||||
bool didRecoveryThisFrame = false;
|
||||
|
||||
try {
|
||||
// Process module (1/60th of a second)
|
||||
moduleSystem->processModules(1.0f / 60.0f);
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
// CRASH DETECTED - Attempt recovery
|
||||
crashesDetected++;
|
||||
std::cout << " [Frame " << frame << "] ⚠️ Crash detected: " << e.what() << "\n";
|
||||
|
||||
// PREUVE QUE LE TEST PEUT ÉCHOUER : désactiver la recovery
|
||||
#ifdef DISABLE_RECOVERY_FOR_TEST
|
||||
std::cout << " [Frame " << frame << "] ❌ RECOVERY DISABLED - Test will fail\n";
|
||||
reporter.addAssertion("recovery_disabled", false);
|
||||
break; // Le test DOIT échouer
|
||||
#endif
|
||||
|
||||
// Recovery attempt
|
||||
try {
|
||||
std::cout << " [Frame " << frame << "] 🔄 Attempting recovery...\n";
|
||||
|
||||
auto recoveryStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
// Extract module from system
|
||||
auto crashedModule = moduleSystem->extractModule();
|
||||
|
||||
// Reload module
|
||||
auto reloadedModule = loader.reload(std::move(crashedModule));
|
||||
|
||||
// Re-register
|
||||
moduleSystem->registerModule("ChaosModule", std::move(reloadedModule));
|
||||
|
||||
auto recoveryEnd = std::chrono::high_resolution_clock::now();
|
||||
float recoveryTime = std::chrono::duration<float, std::milli>(recoveryEnd - recoveryStart).count();
|
||||
|
||||
metrics.recordReloadTime(recoveryTime);
|
||||
recoverySuccesses++;
|
||||
didRecoveryThisFrame = true;
|
||||
|
||||
std::cout << " [Frame " << frame << "] ✅ Recovery successful (" << recoveryTime << "ms)\n";
|
||||
|
||||
} catch (const std::exception& recoveryError) {
|
||||
std::cout << " [Frame " << frame << "] ❌ Recovery FAILED: " << recoveryError.what() << "\n";
|
||||
reporter.addAssertion("recovery_failed", false);
|
||||
break; // Stop test - recovery failed
|
||||
}
|
||||
}
|
||||
|
||||
// Metrics
|
||||
auto frameEnd = std::chrono::high_resolution_clock::now();
|
||||
float frameTime = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count();
|
||||
|
||||
// Only record FPS for normal frames (not recovery frames)
|
||||
// Recovery frames are slow by design (100+ ms for hot-reload)
|
||||
if (!didRecoveryThisFrame) {
|
||||
metrics.recordFPS(1000.0f / frameTime);
|
||||
}
|
||||
|
||||
if (frame % 60 == 0) {
|
||||
metrics.recordMemoryUsage(getCurrentMemoryUsage());
|
||||
}
|
||||
|
||||
// Deadlock detection (frame > 100ms)
|
||||
// NOTE: Skip deadlock check if we just did a recovery (recovery takes >100ms by design)
|
||||
if (frameTime > 100.0f && !didRecoveryThisFrame) {
|
||||
std::cout << " [Frame " << frame << "] ⚠️ Potential deadlock (frame time: " << frameTime << "ms)\n";
|
||||
hadDeadlock = true;
|
||||
}
|
||||
|
||||
// Progress (every 600 frames = 10 seconds)
|
||||
if (frame % 600 == 0 && frame > 0) {
|
||||
float elapsedSec = frame / 60.0f;
|
||||
float progress = (frame * 100.0f) / totalFrames;
|
||||
std::cout << "Progress: " << elapsedSec << "/30.0 seconds (" << (int)progress << "%)\n";
|
||||
|
||||
// Show current metrics
|
||||
std::cout << " FPS: min=" << metrics.getFPSMin() << ", avg=" << metrics.getFPSAvg() << ", max=" << metrics.getFPSMax() << "\n";
|
||||
std::cout << " Memory: " << (getCurrentMemoryUsage() / (1024.0f * 1024.0f)) << " MB\n";
|
||||
}
|
||||
|
||||
// Check if engine crashed externally
|
||||
if (engineCrashed.load()) {
|
||||
std::cout << " [Frame " << frame << "] ❌ Engine crashed externally (signal received)\n";
|
||||
reporter.addAssertion("engine_crashed_externally", false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto testEnd = std::chrono::high_resolution_clock::now();
|
||||
float totalDuration = std::chrono::duration<float>(testEnd - testStart).count();
|
||||
|
||||
std::cout << "\nTest completed!\n\n";
|
||||
|
||||
// === FINAL VERIFICATIONS ===
|
||||
std::cout << "Final verifications...\n";
|
||||
|
||||
// Engine still alive
|
||||
bool engineAlive = !engineCrashed.load();
|
||||
ASSERT_TRUE(engineAlive, "Engine should still be alive");
|
||||
reporter.addAssertion("engine_alive", engineAlive);
|
||||
|
||||
// No deadlocks
|
||||
ASSERT_FALSE(hadDeadlock, "Should not have deadlocks");
|
||||
reporter.addAssertion("no_deadlocks", !hadDeadlock);
|
||||
|
||||
// Memory growth < 10MB
|
||||
size_t memGrowth = metrics.getMemoryGrowth();
|
||||
float memGrowthMB = memGrowth / (1024.0f * 1024.0f);
|
||||
ASSERT_LT(memGrowthMB, 10.0f, "Memory growth should be < 10MB");
|
||||
reporter.addMetric("memory_growth_mb", memGrowthMB);
|
||||
|
||||
// Test runs as fast as possible (not real-time)
|
||||
// Just check it completed within reasonable bounds (< 60 seconds wall time)
|
||||
ASSERT_LT(totalDuration, 60.0f, "Total duration should be < 60 seconds");
|
||||
reporter.addMetric("total_duration_sec", totalDuration);
|
||||
|
||||
// FPS metrics
|
||||
float minFPS = metrics.getFPSMin();
|
||||
float avgFPS = metrics.getFPSAvg();
|
||||
float maxFPS = metrics.getFPSMax();
|
||||
|
||||
// Min FPS should be reasonable (> 10 even with crashes)
|
||||
ASSERT_GT(minFPS, 10.0f, "Min FPS should be > 10");
|
||||
reporter.addMetric("fps_min", minFPS);
|
||||
reporter.addMetric("fps_avg", avgFPS);
|
||||
reporter.addMetric("fps_max", maxFPS);
|
||||
|
||||
// Recovery rate > 95%
|
||||
float recoveryRate = (crashesDetected > 0) ? (recoverySuccesses * 100.0f / crashesDetected) : 100.0f;
|
||||
ASSERT_GT(recoveryRate, 95.0f, "Recovery rate should be > 95%");
|
||||
reporter.addMetric("recovery_rate_percent", recoveryRate);
|
||||
|
||||
// === STATISTICS ===
|
||||
std::cout << "\n";
|
||||
std::cout << "================================================================================\n";
|
||||
std::cout << "CHAOS MONKEY STATISTICS\n";
|
||||
std::cout << "================================================================================\n";
|
||||
std::cout << " Total frames: " << totalFrames << "\n";
|
||||
std::cout << " Duration: " << totalDuration << "s (wall time, not simulation time)\n";
|
||||
std::cout << " Crashes detected: " << crashesDetected << "\n";
|
||||
std::cout << " Recovery successes: " << recoverySuccesses << "\n";
|
||||
std::cout << " Recovery rate: " << recoveryRate << "%\n";
|
||||
std::cout << " Memory growth: " << memGrowthMB << " MB (max: 10MB)\n";
|
||||
std::cout << " Had deadlocks: " << (hadDeadlock ? "YES ❌" : "NO ✅") << "\n";
|
||||
std::cout << " FPS min/avg/max: " << minFPS << " / " << avgFPS << " / " << maxFPS << "\n";
|
||||
std::cout << "================================================================================\n\n";
|
||||
|
||||
std::cout << "Note: ChaosModule generates random crashes internally.\n";
|
||||
std::cout << "The test should recover from ALL crashes automatically via hot-reload.\n\n";
|
||||
|
||||
// === CLEANUP ===
|
||||
std::cout << "Cleaning up...\n";
|
||||
moduleSystem.reset();
|
||||
std::cout << " ✓ Module system shutdown complete\n\n";
|
||||
|
||||
// === REPORTS ===
|
||||
metrics.printReport();
|
||||
reporter.printFinalReport();
|
||||
|
||||
return reporter.getExitCode();
|
||||
}
|
||||
|
||||
@ -1,247 +1,253 @@
|
||||
/**
|
||||
* @file test_03_stress_test.cpp
|
||||
* @brief Scenario 3: Stress Test - Long-duration stability validation
|
||||
*
|
||||
* OBJECTIVE:
|
||||
* Validate hot-reload system stability over extended duration with repeated reloads.
|
||||
*
|
||||
* TEST PARAMETERS:
|
||||
* - Duration: 10 minutes (36000 frames @ 60 FPS)
|
||||
* - Reload frequency: Every 5 seconds (300 frames)
|
||||
* - Total reloads: 120
|
||||
* - No random crashes - focus on hot-reload stability
|
||||
*
|
||||
* SUCCESS CRITERIA:
|
||||
* ✅ All 120 reloads succeed
|
||||
* ✅ Memory growth < 50MB over 10 minutes
|
||||
* ✅ Average reload time < 500ms
|
||||
* ✅ FPS remains stable (no degradation)
|
||||
* ✅ No file descriptor leaks
|
||||
* ✅ State preserved across all reloads
|
||||
*
|
||||
* WHAT THIS VALIDATES:
|
||||
* - No memory leaks in hot-reload system
|
||||
* - No file descriptor leaks (dlopen/dlclose)
|
||||
* - Reload performance doesn't degrade over time
|
||||
* - State preservation is reliable at scale
|
||||
* - System remains stable under repeated reload stress
|
||||
*/
|
||||
|
||||
#include "grove/ModuleLoader.h"
|
||||
#include "grove/SequentialModuleSystem.h"
|
||||
#include "grove/JsonDataNode.h"
|
||||
#include "../helpers/TestMetrics.h"
|
||||
#include "../helpers/TestAssertions.h"
|
||||
#include "../helpers/TestReporter.h"
|
||||
#include "../helpers/SystemUtils.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
using namespace grove;
|
||||
|
||||
// Test configuration
|
||||
constexpr int TARGET_FPS = 60;
|
||||
constexpr float FRAME_TIME = 1.0f / TARGET_FPS;
|
||||
constexpr int RELOAD_INTERVAL = 300; // Reload every 5 seconds (300 frames)
|
||||
constexpr int EXPECTED_RELOADS = 120; // 120 reloads
|
||||
constexpr int TOTAL_FRAMES = EXPECTED_RELOADS * RELOAD_INTERVAL; // 36000 frames = 10 minutes @ 60 FPS
|
||||
|
||||
// Memory threshold
|
||||
constexpr size_t MAX_MEMORY_GROWTH_MB = 50;
|
||||
|
||||
// Paths
|
||||
const std::string MODULE_PATH = "./libStressModule.so";
|
||||
|
||||
int main() {
|
||||
TestReporter reporter("Stress Test - 10 Minute Stability");
|
||||
TestMetrics metrics;
|
||||
|
||||
std::cout << "═══════════════════════════════════════════════════════════════\n";
|
||||
std::cout << " SCENARIO 3: STRESS TEST - LONG DURATION STABILITY\n";
|
||||
std::cout << "═══════════════════════════════════════════════════════════════\n";
|
||||
std::cout << "Duration: 10 minutes (" << TOTAL_FRAMES << " frames @ " << TARGET_FPS << " FPS)\n";
|
||||
std::cout << "Reload interval: Every " << RELOAD_INTERVAL << " frames (5 seconds)\n";
|
||||
std::cout << "Expected reloads: " << EXPECTED_RELOADS << "\n";
|
||||
std::cout << "Memory threshold: < " << MAX_MEMORY_GROWTH_MB << " MB growth\n";
|
||||
std::cout << "═══════════════════════════════════════════════════════════════\n\n";
|
||||
|
||||
size_t initialMemory = grove::getCurrentMemoryUsage() / (1024 * 1024);
|
||||
size_t peakMemory = initialMemory;
|
||||
|
||||
int successfulReloads = 0;
|
||||
int failedReloads = 0;
|
||||
|
||||
try {
|
||||
// === SETUP ===
|
||||
std::cout << "Setup: Loading StressModule...\n";
|
||||
|
||||
ModuleLoader loader;
|
||||
auto moduleSystem = std::make_unique<SequentialModuleSystem>();
|
||||
|
||||
// Load module
|
||||
auto module = loader.load(MODULE_PATH, "StressModule", false);
|
||||
|
||||
// Configure module with empty config
|
||||
nlohmann::json configJson;
|
||||
auto config = std::make_unique<JsonDataNode>("config", configJson);
|
||||
|
||||
module->setConfiguration(*config, nullptr, nullptr);
|
||||
|
||||
// Register in module system
|
||||
moduleSystem->registerModule("StressModule", std::move(module));
|
||||
|
||||
std::cout << " ✓ StressModule loaded and configured\n\n";
|
||||
|
||||
std::cout << "🚀 Starting 10-minute stress test...\n\n";
|
||||
|
||||
auto startTime = std::chrono::high_resolution_clock::now();
|
||||
|
||||
// Main simulation loop
|
||||
for (int frame = 1; frame <= TOTAL_FRAMES; ++frame) {
|
||||
auto frameStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
// Process modules
|
||||
try {
|
||||
moduleSystem->processModules(FRAME_TIME);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << " [Frame " << frame << "] ❌ Unexpected error during process: " << e.what() << "\n";
|
||||
reporter.addAssertion("process_error", false);
|
||||
break;
|
||||
}
|
||||
|
||||
auto frameEnd = std::chrono::high_resolution_clock::now();
|
||||
auto frameDuration = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count();
|
||||
float fps = frameDuration > 0.0f ? 1000.0f / frameDuration : 0.0f;
|
||||
metrics.recordFPS(fps);
|
||||
|
||||
// Hot-reload every RELOAD_INTERVAL frames
|
||||
if (frame % RELOAD_INTERVAL == 0) {
|
||||
int reloadNumber = frame / RELOAD_INTERVAL;
|
||||
std::cout << " [Frame " << frame << "/" << TOTAL_FRAMES << "] 🔄 Triggering hot-reload #" << reloadNumber << "...\n";
|
||||
|
||||
auto reloadStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
try {
|
||||
// Extract module from system
|
||||
auto extractedModule = moduleSystem->extractModule();
|
||||
if (!extractedModule) {
|
||||
std::cerr << " ❌ Failed to extract StressModule\n";
|
||||
failedReloads++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Perform hot-reload
|
||||
auto reloadedModule = loader.reload(std::move(extractedModule));
|
||||
|
||||
// Re-register reloaded module
|
||||
moduleSystem->registerModule("StressModule", std::move(reloadedModule));
|
||||
|
||||
auto reloadEnd = std::chrono::high_resolution_clock::now();
|
||||
auto reloadDuration = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
reloadEnd - reloadStart).count();
|
||||
|
||||
metrics.recordReloadTime(static_cast<float>(reloadDuration));
|
||||
successfulReloads++;
|
||||
|
||||
std::cout << " ✅ Hot-reload #" << reloadNumber << " succeeded in " << reloadDuration << "ms\n";
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << " ❌ Exception during hot-reload: " << e.what() << "\n";
|
||||
failedReloads++;
|
||||
}
|
||||
}
|
||||
|
||||
// Memory monitoring every 60 seconds (3600 frames)
|
||||
if (frame % 3600 == 0 && frame > 0) {
|
||||
size_t currentMemory = grove::getCurrentMemoryUsage() / (1024 * 1024);
|
||||
size_t memoryGrowth = currentMemory - initialMemory;
|
||||
peakMemory = std::max(peakMemory, currentMemory);
|
||||
|
||||
int minutesElapsed = frame / 3600;
|
||||
std::cout << "\n📊 Checkpoint at " << minutesElapsed << " minute(s):\n";
|
||||
std::cout << " Current memory: " << currentMemory << " MB\n";
|
||||
std::cout << " Growth: " << memoryGrowth << " MB\n";
|
||||
std::cout << " Peak: " << peakMemory << " MB\n";
|
||||
std::cout << " Avg FPS: " << metrics.getFPSAvg() << "\n";
|
||||
std::cout << " Reloads: " << successfulReloads << "/" << EXPECTED_RELOADS << "\n";
|
||||
std::cout << " Avg reload time: " << metrics.getReloadTimeAvg() << "ms\n\n";
|
||||
}
|
||||
|
||||
// Progress reporting every minute (without memory details)
|
||||
if (frame % 3600 == 0 && frame > 0) {
|
||||
int minutesElapsed = frame / 3600;
|
||||
int minutesRemaining = (TOTAL_FRAMES - frame) / 3600;
|
||||
std::cout << "⏱️ Progress: " << minutesElapsed << " minutes elapsed, " << minutesRemaining << " minutes remaining\n";
|
||||
}
|
||||
}
|
||||
|
||||
auto endTime = std::chrono::high_resolution_clock::now();
|
||||
auto totalDuration = std::chrono::duration_cast<std::chrono::seconds>(
|
||||
endTime - startTime).count();
|
||||
|
||||
// Final metrics
|
||||
size_t finalMemory = grove::getCurrentMemoryUsage() / (1024 * 1024);
|
||||
size_t totalMemoryGrowth = finalMemory - initialMemory;
|
||||
|
||||
std::cout << "\n═══════════════════════════════════════════════════════════════\n";
|
||||
std::cout << " STRESS TEST COMPLETED\n";
|
||||
std::cout << "═══════════════════════════════════════════════════════════════\n";
|
||||
std::cout << "Total frames: " << TOTAL_FRAMES << "\n";
|
||||
std::cout << "Real time: " << totalDuration << "s\n";
|
||||
std::cout << "Simulated time: " << (TOTAL_FRAMES / TARGET_FPS) << "s (10 minutes)\n";
|
||||
std::cout << "Successful reloads: " << successfulReloads << "/" << EXPECTED_RELOADS << "\n";
|
||||
std::cout << "Failed reloads: " << failedReloads << "\n";
|
||||
std::cout << "\n📊 PERFORMANCE METRICS:\n";
|
||||
std::cout << "Average FPS: " << metrics.getFPSAvg() << "\n";
|
||||
std::cout << "Min FPS: " << metrics.getFPSMin() << "\n";
|
||||
std::cout << "Max FPS: " << metrics.getFPSMax() << "\n";
|
||||
std::cout << "\n🔥 HOT-RELOAD METRICS:\n";
|
||||
std::cout << "Avg reload time: " << metrics.getReloadTimeAvg() << "ms\n";
|
||||
std::cout << "Min reload time: " << metrics.getReloadTimeMin() << "ms\n";
|
||||
std::cout << "Max reload time: " << metrics.getReloadTimeMax() << "ms\n";
|
||||
std::cout << "\n💾 MEMORY METRICS:\n";
|
||||
std::cout << "Initial memory: " << initialMemory << " MB\n";
|
||||
std::cout << "Final memory: " << finalMemory << " MB\n";
|
||||
std::cout << "Peak memory: " << peakMemory << " MB\n";
|
||||
std::cout << "Total growth: " << totalMemoryGrowth << " MB\n";
|
||||
std::cout << "═══════════════════════════════════════════════════════════════\n\n";
|
||||
|
||||
// Validate results
|
||||
bool allReloadsSucceeded = (successfulReloads == EXPECTED_RELOADS && failedReloads == 0);
|
||||
bool memoryWithinThreshold = (totalMemoryGrowth < MAX_MEMORY_GROWTH_MB);
|
||||
bool avgReloadTimeAcceptable = (metrics.getReloadTimeAvg() < 500.0f);
|
||||
bool fpsStable = (metrics.getFPSMin() > 30.0f); // Ensure FPS doesn't drop too much
|
||||
|
||||
reporter.addAssertion("all_reloads_succeeded", allReloadsSucceeded);
|
||||
reporter.addAssertion("memory_within_threshold", memoryWithinThreshold);
|
||||
reporter.addAssertion("avg_reload_time_acceptable", avgReloadTimeAcceptable);
|
||||
reporter.addAssertion("fps_stable", fpsStable);
|
||||
|
||||
if (allReloadsSucceeded && memoryWithinThreshold &&
|
||||
avgReloadTimeAcceptable && fpsStable) {
|
||||
std::cout << "✅ STRESS TEST PASSED - System is stable over 10 minutes\n";
|
||||
} else {
|
||||
if (!allReloadsSucceeded) {
|
||||
std::cerr << "❌ Reload success rate: " << successfulReloads << "/" << EXPECTED_RELOADS << "\n";
|
||||
}
|
||||
if (!memoryWithinThreshold) {
|
||||
std::cerr << "❌ Memory growth: " << totalMemoryGrowth << " MB (threshold: " << MAX_MEMORY_GROWTH_MB << " MB)\n";
|
||||
}
|
||||
if (!avgReloadTimeAcceptable) {
|
||||
std::cerr << "❌ Avg reload time: " << metrics.getReloadTimeAvg() << "ms (threshold: 500ms)\n";
|
||||
}
|
||||
if (!fpsStable) {
|
||||
std::cerr << "❌ Min FPS: " << metrics.getFPSMin() << " (threshold: 30.0)\n";
|
||||
}
|
||||
}
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Test failed with exception: " << e.what() << "\n";
|
||||
reporter.addAssertion("exception", false);
|
||||
}
|
||||
|
||||
reporter.printFinalReport();
|
||||
return reporter.getExitCode();
|
||||
}
|
||||
/**
|
||||
* @file test_03_stress_test.cpp
|
||||
* @brief Scenario 3: Stress Test - Long-duration stability validation
|
||||
*
|
||||
* OBJECTIVE:
|
||||
* Validate hot-reload system stability over extended duration with repeated reloads.
|
||||
*
|
||||
* TEST PARAMETERS:
|
||||
* - Duration: 10 minutes (36000 frames @ 60 FPS)
|
||||
* - Reload frequency: Every 5 seconds (300 frames)
|
||||
* - Total reloads: 120
|
||||
* - No random crashes - focus on hot-reload stability
|
||||
*
|
||||
* SUCCESS CRITERIA:
|
||||
* ✅ All 120 reloads succeed
|
||||
* ✅ Memory growth < 50MB over 10 minutes
|
||||
* ✅ Average reload time < 500ms
|
||||
* ✅ FPS remains stable (no degradation)
|
||||
* ✅ No file descriptor leaks
|
||||
* ✅ State preserved across all reloads
|
||||
*
|
||||
* WHAT THIS VALIDATES:
|
||||
* - No memory leaks in hot-reload system
|
||||
* - No file descriptor leaks (dlopen/dlclose)
|
||||
* - Reload performance doesn't degrade over time
|
||||
* - State preservation is reliable at scale
|
||||
* - System remains stable under repeated reload stress
|
||||
*/
|
||||
|
||||
#include "grove/ModuleLoader.h"
|
||||
#include "grove/SequentialModuleSystem.h"
|
||||
#include "grove/JsonDataNode.h"
|
||||
#include "../helpers/TestMetrics.h"
|
||||
#include "../helpers/TestAssertions.h"
|
||||
#include "../helpers/TestReporter.h"
|
||||
#include "../helpers/SystemUtils.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
using namespace grove;
|
||||
|
||||
// Test configuration
|
||||
constexpr int TARGET_FPS = 60;
|
||||
constexpr float FRAME_TIME = 1.0f / TARGET_FPS;
|
||||
constexpr int RELOAD_INTERVAL = 300; // Reload every 5 seconds (300 frames)
|
||||
constexpr int EXPECTED_RELOADS = 120; // 120 reloads
|
||||
constexpr int TOTAL_FRAMES = EXPECTED_RELOADS * RELOAD_INTERVAL; // 36000 frames = 10 minutes @ 60 FPS
|
||||
|
||||
// Memory threshold
|
||||
constexpr size_t MAX_MEMORY_GROWTH_MB = 50;
|
||||
|
||||
// Paths
|
||||
#ifdef _WIN32
|
||||
const std::string MODULE_PATH = "./libStressModule.dll";
|
||||
#else
|
||||
const std::string MODULE_PATH = "./libStressModule.so";
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
#endif
|
||||
|
||||
int main() {
|
||||
TestReporter reporter("Stress Test - 10 Minute Stability");
|
||||
TestMetrics metrics;
|
||||
|
||||
std::cout << "═══════════════════════════════════════════════════════════════\n";
|
||||
std::cout << " SCENARIO 3: STRESS TEST - LONG DURATION STABILITY\n";
|
||||
std::cout << "═══════════════════════════════════════════════════════════════\n";
|
||||
std::cout << "Duration: 10 minutes (" << TOTAL_FRAMES << " frames @ " << TARGET_FPS << " FPS)\n";
|
||||
std::cout << "Reload interval: Every " << RELOAD_INTERVAL << " frames (5 seconds)\n";
|
||||
std::cout << "Expected reloads: " << EXPECTED_RELOADS << "\n";
|
||||
std::cout << "Memory threshold: < " << MAX_MEMORY_GROWTH_MB << " MB growth\n";
|
||||
std::cout << "═══════════════════════════════════════════════════════════════\n\n";
|
||||
|
||||
size_t initialMemory = grove::getCurrentMemoryUsage() / (1024 * 1024);
|
||||
size_t peakMemory = initialMemory;
|
||||
|
||||
int successfulReloads = 0;
|
||||
int failedReloads = 0;
|
||||
|
||||
try {
|
||||
// === SETUP ===
|
||||
std::cout << "Setup: Loading StressModule...\n";
|
||||
|
||||
ModuleLoader loader;
|
||||
auto moduleSystem = std::make_unique<SequentialModuleSystem>();
|
||||
|
||||
// Load module
|
||||
auto module = loader.load(MODULE_PATH, "StressModule", false);
|
||||
|
||||
// Configure module with empty config
|
||||
nlohmann::json configJson;
|
||||
auto config = std::make_unique<JsonDataNode>("config", configJson);
|
||||
|
||||
module->setConfiguration(*config, nullptr, nullptr);
|
||||
|
||||
// Register in module system
|
||||
moduleSystem->registerModule("StressModule", std::move(module));
|
||||
|
||||
std::cout << " ✓ StressModule loaded and configured\n\n";
|
||||
|
||||
std::cout << "🚀 Starting 10-minute stress test...\n\n";
|
||||
|
||||
auto startTime = std::chrono::high_resolution_clock::now();
|
||||
|
||||
// Main simulation loop
|
||||
for (int frame = 1; frame <= TOTAL_FRAMES; ++frame) {
|
||||
auto frameStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
// Process modules
|
||||
try {
|
||||
moduleSystem->processModules(FRAME_TIME);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << " [Frame " << frame << "] ❌ Unexpected error during process: " << e.what() << "\n";
|
||||
reporter.addAssertion("process_error", false);
|
||||
break;
|
||||
}
|
||||
|
||||
auto frameEnd = std::chrono::high_resolution_clock::now();
|
||||
auto frameDuration = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count();
|
||||
float fps = frameDuration > 0.0f ? 1000.0f / frameDuration : 0.0f;
|
||||
metrics.recordFPS(fps);
|
||||
|
||||
// Hot-reload every RELOAD_INTERVAL frames
|
||||
if (frame % RELOAD_INTERVAL == 0) {
|
||||
int reloadNumber = frame / RELOAD_INTERVAL;
|
||||
std::cout << " [Frame " << frame << "/" << TOTAL_FRAMES << "] 🔄 Triggering hot-reload #" << reloadNumber << "...\n";
|
||||
|
||||
auto reloadStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
try {
|
||||
// Extract module from system
|
||||
auto extractedModule = moduleSystem->extractModule();
|
||||
if (!extractedModule) {
|
||||
std::cerr << " ❌ Failed to extract StressModule\n";
|
||||
failedReloads++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Perform hot-reload
|
||||
auto reloadedModule = loader.reload(std::move(extractedModule));
|
||||
|
||||
// Re-register reloaded module
|
||||
moduleSystem->registerModule("StressModule", std::move(reloadedModule));
|
||||
|
||||
auto reloadEnd = std::chrono::high_resolution_clock::now();
|
||||
auto reloadDuration = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
reloadEnd - reloadStart).count();
|
||||
|
||||
metrics.recordReloadTime(static_cast<float>(reloadDuration));
|
||||
successfulReloads++;
|
||||
|
||||
std::cout << " ✅ Hot-reload #" << reloadNumber << " succeeded in " << reloadDuration << "ms\n";
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << " ❌ Exception during hot-reload: " << e.what() << "\n";
|
||||
failedReloads++;
|
||||
}
|
||||
}
|
||||
|
||||
// Memory monitoring every 60 seconds (3600 frames)
|
||||
if (frame % 3600 == 0 && frame > 0) {
|
||||
size_t currentMemory = grove::getCurrentMemoryUsage() / (1024 * 1024);
|
||||
size_t memoryGrowth = currentMemory - initialMemory;
|
||||
peakMemory = std::max(peakMemory, currentMemory);
|
||||
|
||||
int minutesElapsed = frame / 3600;
|
||||
std::cout << "\n📊 Checkpoint at " << minutesElapsed << " minute(s):\n";
|
||||
std::cout << " Current memory: " << currentMemory << " MB\n";
|
||||
std::cout << " Growth: " << memoryGrowth << " MB\n";
|
||||
std::cout << " Peak: " << peakMemory << " MB\n";
|
||||
std::cout << " Avg FPS: " << metrics.getFPSAvg() << "\n";
|
||||
std::cout << " Reloads: " << successfulReloads << "/" << EXPECTED_RELOADS << "\n";
|
||||
std::cout << " Avg reload time: " << metrics.getReloadTimeAvg() << "ms\n\n";
|
||||
}
|
||||
|
||||
// Progress reporting every minute (without memory details)
|
||||
if (frame % 3600 == 0 && frame > 0) {
|
||||
int minutesElapsed = frame / 3600;
|
||||
int minutesRemaining = (TOTAL_FRAMES - frame) / 3600;
|
||||
std::cout << "⏱️ Progress: " << minutesElapsed << " minutes elapsed, " << minutesRemaining << " minutes remaining\n";
|
||||
}
|
||||
}
|
||||
|
||||
auto endTime = std::chrono::high_resolution_clock::now();
|
||||
auto totalDuration = std::chrono::duration_cast<std::chrono::seconds>(
|
||||
endTime - startTime).count();
|
||||
|
||||
// Final metrics
|
||||
size_t finalMemory = grove::getCurrentMemoryUsage() / (1024 * 1024);
|
||||
size_t totalMemoryGrowth = finalMemory - initialMemory;
|
||||
|
||||
std::cout << "\n═══════════════════════════════════════════════════════════════\n";
|
||||
std::cout << " STRESS TEST COMPLETED\n";
|
||||
std::cout << "═══════════════════════════════════════════════════════════════\n";
|
||||
std::cout << "Total frames: " << TOTAL_FRAMES << "\n";
|
||||
std::cout << "Real time: " << totalDuration << "s\n";
|
||||
std::cout << "Simulated time: " << (TOTAL_FRAMES / TARGET_FPS) << "s (10 minutes)\n";
|
||||
std::cout << "Successful reloads: " << successfulReloads << "/" << EXPECTED_RELOADS << "\n";
|
||||
std::cout << "Failed reloads: " << failedReloads << "\n";
|
||||
std::cout << "\n📊 PERFORMANCE METRICS:\n";
|
||||
std::cout << "Average FPS: " << metrics.getFPSAvg() << "\n";
|
||||
std::cout << "Min FPS: " << metrics.getFPSMin() << "\n";
|
||||
std::cout << "Max FPS: " << metrics.getFPSMax() << "\n";
|
||||
std::cout << "\n🔥 HOT-RELOAD METRICS:\n";
|
||||
std::cout << "Avg reload time: " << metrics.getReloadTimeAvg() << "ms\n";
|
||||
std::cout << "Min reload time: " << metrics.getReloadTimeMin() << "ms\n";
|
||||
std::cout << "Max reload time: " << metrics.getReloadTimeMax() << "ms\n";
|
||||
std::cout << "\n💾 MEMORY METRICS:\n";
|
||||
std::cout << "Initial memory: " << initialMemory << " MB\n";
|
||||
std::cout << "Final memory: " << finalMemory << " MB\n";
|
||||
std::cout << "Peak memory: " << peakMemory << " MB\n";
|
||||
std::cout << "Total growth: " << totalMemoryGrowth << " MB\n";
|
||||
std::cout << "═══════════════════════════════════════════════════════════════\n\n";
|
||||
|
||||
// Validate results
|
||||
bool allReloadsSucceeded = (successfulReloads == EXPECTED_RELOADS && failedReloads == 0);
|
||||
bool memoryWithinThreshold = (totalMemoryGrowth < MAX_MEMORY_GROWTH_MB);
|
||||
bool avgReloadTimeAcceptable = (metrics.getReloadTimeAvg() < 500.0f);
|
||||
bool fpsStable = (metrics.getFPSMin() > 30.0f); // Ensure FPS doesn't drop too much
|
||||
|
||||
reporter.addAssertion("all_reloads_succeeded", allReloadsSucceeded);
|
||||
reporter.addAssertion("memory_within_threshold", memoryWithinThreshold);
|
||||
reporter.addAssertion("avg_reload_time_acceptable", avgReloadTimeAcceptable);
|
||||
reporter.addAssertion("fps_stable", fpsStable);
|
||||
|
||||
if (allReloadsSucceeded && memoryWithinThreshold &&
|
||||
avgReloadTimeAcceptable && fpsStable) {
|
||||
std::cout << "✅ STRESS TEST PASSED - System is stable over 10 minutes\n";
|
||||
} else {
|
||||
if (!allReloadsSucceeded) {
|
||||
std::cerr << "❌ Reload success rate: " << successfulReloads << "/" << EXPECTED_RELOADS << "\n";
|
||||
}
|
||||
if (!memoryWithinThreshold) {
|
||||
std::cerr << "❌ Memory growth: " << totalMemoryGrowth << " MB (threshold: " << MAX_MEMORY_GROWTH_MB << " MB)\n";
|
||||
}
|
||||
if (!avgReloadTimeAcceptable) {
|
||||
std::cerr << "❌ Avg reload time: " << metrics.getReloadTimeAvg() << "ms (threshold: 500ms)\n";
|
||||
}
|
||||
if (!fpsStable) {
|
||||
std::cerr << "❌ Min FPS: " << metrics.getFPSMin() << " (threshold: 30.0)\n";
|
||||
}
|
||||
}
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Test failed with exception: " << e.what() << "\n";
|
||||
reporter.addAssertion("exception", false);
|
||||
}
|
||||
|
||||
reporter.printFinalReport();
|
||||
return reporter.getExitCode();
|
||||
}
|
||||
|
||||
@ -1,390 +1,393 @@
|
||||
#include "grove/ModuleLoader.h"
|
||||
#include "grove/SequentialModuleSystem.h"
|
||||
#include "grove/JsonDataNode.h"
|
||||
#include "../helpers/TestMetrics.h"
|
||||
#include "../helpers/TestAssertions.h"
|
||||
#include "../helpers/TestReporter.h"
|
||||
#include "../helpers/SystemUtils.h"
|
||||
#include "../helpers/AutoCompiler.h"
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <filesystem>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
|
||||
using namespace grove;
|
||||
using namespace TestHelpers;
|
||||
|
||||
int main() {
|
||||
TestReporter reporter("Race Condition Hunter");
|
||||
|
||||
std::cout << "================================================================================\n";
|
||||
std::cout << "TEST: Race Condition Hunter - Concurrent Compilation & Reload\n";
|
||||
std::cout << "================================================================================\n\n";
|
||||
|
||||
// === CONFIGURATION ===
|
||||
const int TOTAL_COMPILATIONS = 10; // Reduced for WSL2 compatibility
|
||||
const int COMPILE_INTERVAL_MS = 2000; // 2 seconds between compilations (allows for slower filesystems)
|
||||
const int FILE_CHECK_INTERVAL_MS = 50; // Check file changes every 50ms
|
||||
const float TARGET_FPS = 60.0f;
|
||||
const float FRAME_TIME = 1.0f / TARGET_FPS;
|
||||
|
||||
std::string modulePath = "./libTestModule.so";
|
||||
// Test runs from build/tests/, so source files are at ../../tests/modules/
|
||||
std::string sourcePath = "../../tests/modules/TestModule.cpp";
|
||||
std::string buildDir = "build";
|
||||
|
||||
// === ATOMIC COUNTERS (Thread-safe) ===
|
||||
std::atomic<int> reloadAttempts{0};
|
||||
std::atomic<int> reloadSuccesses{0};
|
||||
std::atomic<int> reloadFailures{0};
|
||||
std::atomic<int> corruptedLoads{0};
|
||||
std::atomic<int> crashes{0};
|
||||
std::atomic<bool> engineRunning{true};
|
||||
std::atomic<bool> watcherRunning{true};
|
||||
|
||||
// CRITICAL: Mutex to protect moduleSystem access between threads
|
||||
std::mutex moduleSystemMutex;
|
||||
|
||||
// Reload timing
|
||||
std::mutex reloadTimesMutex;
|
||||
std::vector<float> reloadTimes;
|
||||
|
||||
// Metrics
|
||||
TestMetrics metrics;
|
||||
|
||||
// === SETUP ===
|
||||
std::cout << "Setup:\n";
|
||||
std::cout << " Module path: " << modulePath << "\n";
|
||||
std::cout << " Source path: " << sourcePath << "\n";
|
||||
std::cout << " Compilations: " << TOTAL_COMPILATIONS << "\n";
|
||||
std::cout << " Interval: " << COMPILE_INTERVAL_MS << "ms\n";
|
||||
std::cout << " Expected time: ~" << (TOTAL_COMPILATIONS * COMPILE_INTERVAL_MS / 1000) << "s\n\n";
|
||||
|
||||
// Load module initially
|
||||
ModuleLoader loader;
|
||||
auto moduleSystem = std::make_unique<SequentialModuleSystem>();
|
||||
|
||||
try {
|
||||
auto module = loader.load(modulePath, "TestModule", false);
|
||||
nlohmann::json configJson = nlohmann::json::object();
|
||||
auto config = std::make_unique<JsonDataNode>("config", configJson);
|
||||
module->setConfiguration(*config, nullptr, nullptr);
|
||||
moduleSystem->registerModule("TestModule", std::move(module));
|
||||
std::cout << " ✓ Initial module loaded\n\n";
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "❌ Failed to load initial module: " << e.what() << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
// === THREAD 1: AUTO-COMPILER ===
|
||||
std::cout << "Starting AutoCompiler thread...\n";
|
||||
AutoCompiler compiler("TestModule", buildDir, sourcePath);
|
||||
compiler.start(TOTAL_COMPILATIONS, COMPILE_INTERVAL_MS);
|
||||
|
||||
// === THREAD 2: FILE WATCHER ===
|
||||
std::cout << "Starting FileWatcher thread...\n";
|
||||
std::thread watcherThread([&]() {
|
||||
try {
|
||||
auto lastWriteTime = std::filesystem::last_write_time(modulePath);
|
||||
|
||||
while (watcherRunning.load() && engineRunning.load()) {
|
||||
try {
|
||||
auto currentTime = std::filesystem::last_write_time(modulePath);
|
||||
|
||||
if (currentTime != lastWriteTime) {
|
||||
reloadAttempts++;
|
||||
|
||||
// Measure reload time
|
||||
auto reloadStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
try {
|
||||
// CRITICAL: Lock moduleSystem during entire reload
|
||||
std::lock_guard<std::mutex> lock(moduleSystemMutex);
|
||||
|
||||
// Extract module and save state
|
||||
auto module = moduleSystem->extractModule();
|
||||
auto state = module->getState();
|
||||
|
||||
// CRITICAL: Destroy old module BEFORE reloading
|
||||
// The loader.load() will unload the old .so
|
||||
module.reset();
|
||||
|
||||
// Reload
|
||||
auto newModule = loader.load(modulePath, "TestModule", true);
|
||||
|
||||
// Check if module loaded correctly
|
||||
if (!newModule) {
|
||||
corruptedLoads++;
|
||||
reloadFailures++;
|
||||
// Can't recover - old module already destroyed
|
||||
} else {
|
||||
// VALIDATE MODULE INTEGRITY
|
||||
bool isCorrupted = false;
|
||||
try {
|
||||
// Test 1: Can we get health status?
|
||||
auto health = newModule->getHealthStatus();
|
||||
std::string version = health->getString("version", "");
|
||||
|
||||
// Test 2: Is version valid?
|
||||
if (version.empty() || version == "unknown") {
|
||||
isCorrupted = true;
|
||||
}
|
||||
|
||||
// Test 3: Can we set configuration?
|
||||
nlohmann::json configJson;
|
||||
configJson["test"] = "validation";
|
||||
auto testConfig = std::make_unique<JsonDataNode>("config", configJson);
|
||||
newModule->setConfiguration(*testConfig, nullptr, nullptr);
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
// Module crashes on basic operations = corrupted
|
||||
isCorrupted = true;
|
||||
}
|
||||
|
||||
if (isCorrupted) {
|
||||
corruptedLoads++;
|
||||
reloadFailures++;
|
||||
// Can't recover - old module already destroyed
|
||||
} else {
|
||||
// Module is valid, restore state and register
|
||||
newModule->setState(*state);
|
||||
moduleSystem->registerModule("TestModule", std::move(newModule));
|
||||
reloadSuccesses++;
|
||||
|
||||
// Record reload time
|
||||
auto reloadEnd = std::chrono::high_resolution_clock::now();
|
||||
float reloadTimeMs = std::chrono::duration<float, std::milli>(reloadEnd - reloadStart).count();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> timeLock(reloadTimesMutex);
|
||||
reloadTimes.push_back(reloadTimeMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
reloadFailures++;
|
||||
// Module might already be registered, continue
|
||||
}
|
||||
|
||||
lastWriteTime = currentTime;
|
||||
}
|
||||
} catch (const std::filesystem::filesystem_error&) {
|
||||
// File might be being written, ignore
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(FILE_CHECK_INTERVAL_MS));
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[FileWatcher] Exception: " << e.what() << "\n";
|
||||
}
|
||||
});
|
||||
|
||||
// === THREAD 3: ENGINE LOOP ===
|
||||
std::cout << "Starting Engine thread (60 FPS)...\n\n";
|
||||
std::thread engineThread([&]() {
|
||||
try {
|
||||
auto lastMemoryCheck = std::chrono::steady_clock::now();
|
||||
|
||||
while (engineRunning.load()) {
|
||||
auto frameStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
try {
|
||||
// TRY to lock moduleSystem (non-blocking)
|
||||
// If reload is happening, skip this frame
|
||||
if (moduleSystemMutex.try_lock()) {
|
||||
try {
|
||||
moduleSystem->processModules(FRAME_TIME);
|
||||
moduleSystemMutex.unlock();
|
||||
} catch (const std::exception& e) {
|
||||
moduleSystemMutex.unlock();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
// else: reload in progress, skip frame
|
||||
} catch (const std::exception& e) {
|
||||
crashes++;
|
||||
std::cerr << "[Engine] Crash detected: " << e.what() << "\n";
|
||||
}
|
||||
|
||||
auto frameEnd = std::chrono::high_resolution_clock::now();
|
||||
float frameTime = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count();
|
||||
metrics.recordFPS(1000.0f / std::max(frameTime, 0.1f));
|
||||
|
||||
// Check memory every second
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
if (std::chrono::duration_cast<std::chrono::seconds>(now - lastMemoryCheck).count() >= 1) {
|
||||
metrics.recordMemoryUsage(getCurrentMemoryUsage());
|
||||
lastMemoryCheck = now;
|
||||
}
|
||||
|
||||
// Sleep to maintain target FPS (if frame finished early)
|
||||
auto targetFrameTime = std::chrono::milliseconds(static_cast<int>(FRAME_TIME * 1000));
|
||||
auto elapsed = frameEnd - frameStart;
|
||||
if (elapsed < targetFrameTime) {
|
||||
std::this_thread::sleep_for(targetFrameTime - elapsed);
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[Engine] Thread exception: " << e.what() << "\n";
|
||||
}
|
||||
});
|
||||
|
||||
// === MONITORING LOOP ===
|
||||
std::cout << "Test running...\n";
|
||||
auto startTime = std::chrono::steady_clock::now();
|
||||
int lastPrintedPercent = 0;
|
||||
const int MAX_TEST_TIME_SECONDS = 90; // Maximum 1.5 minutes (allows all 20 compilations)
|
||||
|
||||
while (compiler.isRunning() || compiler.getCurrentIteration() < TOTAL_COMPILATIONS) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(2));
|
||||
|
||||
int currentIteration = compiler.getCurrentIteration();
|
||||
int percent = (currentIteration * 100) / TOTAL_COMPILATIONS;
|
||||
|
||||
// Check for timeout
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - startTime).count();
|
||||
|
||||
if (elapsed > MAX_TEST_TIME_SECONDS) {
|
||||
std::cout << "\n⚠️ Test timeout after " << elapsed << "s - stopping...\n";
|
||||
break;
|
||||
}
|
||||
|
||||
// Print progress every 10%
|
||||
if (percent >= lastPrintedPercent + 10 && percent <= 100) {
|
||||
std::cout << "\nProgress: " << percent << "% (" << currentIteration << "/" << TOTAL_COMPILATIONS << " compilations)\n";
|
||||
std::cout << " Elapsed: " << elapsed << "s\n";
|
||||
std::cout << " Compilations: " << compiler.getSuccessCount() << " OK, " << compiler.getFailureCount() << " FAIL\n";
|
||||
std::cout << " Reloads: " << reloadSuccesses.load() << " OK, " << reloadFailures.load() << " FAIL\n";
|
||||
std::cout << " Corrupted: " << corruptedLoads.load() << "\n";
|
||||
std::cout << " Crashes: " << crashes.load() << "\n";
|
||||
|
||||
lastPrintedPercent = percent;
|
||||
}
|
||||
}
|
||||
|
||||
// === CLEANUP ===
|
||||
std::cout << "\n\nStopping threads...\n";
|
||||
|
||||
compiler.stop();
|
||||
watcherRunning = false;
|
||||
engineRunning = false;
|
||||
|
||||
if (watcherThread.joinable()) {
|
||||
watcherThread.join();
|
||||
}
|
||||
if (engineThread.joinable()) {
|
||||
engineThread.join();
|
||||
}
|
||||
|
||||
std::cout << " ✓ All threads stopped\n\n";
|
||||
|
||||
// === CALCULATE STATISTICS ===
|
||||
float compileSuccessRate = (compiler.getSuccessCount() * 100.0f) / std::max(1, TOTAL_COMPILATIONS);
|
||||
float reloadSuccessRate = (reloadSuccesses.load() * 100.0f) / std::max(1, reloadAttempts.load());
|
||||
|
||||
float avgReloadTime = 0.0f;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(reloadTimesMutex);
|
||||
if (!reloadTimes.empty()) {
|
||||
float sum = 0.0f;
|
||||
for (float t : reloadTimes) {
|
||||
sum += t;
|
||||
}
|
||||
avgReloadTime = sum / reloadTimes.size();
|
||||
}
|
||||
}
|
||||
|
||||
auto endTime = std::chrono::steady_clock::now();
|
||||
auto totalTimeSeconds = std::chrono::duration_cast<std::chrono::seconds>(endTime - startTime).count();
|
||||
|
||||
// === PRINT SUMMARY ===
|
||||
std::cout << "================================================================================\n";
|
||||
std::cout << "RACE CONDITION HUNTER SUMMARY\n";
|
||||
std::cout << "================================================================================\n\n";
|
||||
|
||||
std::cout << "Duration: " << totalTimeSeconds << "s\n\n";
|
||||
|
||||
std::cout << "Compilations:\n";
|
||||
std::cout << " Total: " << TOTAL_COMPILATIONS << "\n";
|
||||
std::cout << " Successes: " << compiler.getSuccessCount() << " (" << std::fixed << std::setprecision(1) << compileSuccessRate << "%)\n";
|
||||
std::cout << " Failures: " << compiler.getFailureCount() << "\n\n";
|
||||
|
||||
std::cout << "Reloads:\n";
|
||||
std::cout << " Attempts: " << reloadAttempts.load() << "\n";
|
||||
std::cout << " Successes: " << reloadSuccesses.load() << " (" << std::fixed << std::setprecision(1) << reloadSuccessRate << "%)\n";
|
||||
std::cout << " Failures: " << reloadFailures.load() << "\n";
|
||||
std::cout << " Corrupted: " << corruptedLoads.load() << "\n";
|
||||
std::cout << " Avg time: " << std::fixed << std::setprecision(0) << avgReloadTime << "ms\n\n";
|
||||
|
||||
std::cout << "Stability:\n";
|
||||
std::cout << " Crashes: " << crashes.load() << "\n";
|
||||
std::cout << " Avg FPS: " << std::fixed << std::setprecision(1) << metrics.getFPSAvg() << "\n";
|
||||
std::cout << " Memory: " << std::fixed << std::setprecision(2) << metrics.getMemoryGrowth() << " MB\n\n";
|
||||
|
||||
// === ASSERTIONS ===
|
||||
bool passed = true;
|
||||
|
||||
std::cout << "Validating results...\n";
|
||||
|
||||
// MUST PASS criteria
|
||||
// Note: Lowered from 95% to 70% for WSL2/slower filesystem compatibility
|
||||
// The important thing is that compilations don't fail, they just might timeout
|
||||
if (compileSuccessRate < 70.0f) {
|
||||
std::cout << " ❌ Compile success rate too low: " << compileSuccessRate << "% (need > 70%)\n";
|
||||
passed = false;
|
||||
} else {
|
||||
std::cout << " ✓ Compile success rate: " << compileSuccessRate << "%\n";
|
||||
}
|
||||
|
||||
if (corruptedLoads.load() > 0) {
|
||||
std::cout << " ❌ Corrupted loads detected: " << corruptedLoads.load() << " (need 0)\n";
|
||||
passed = false;
|
||||
} else {
|
||||
std::cout << " ✓ No corrupted loads\n";
|
||||
}
|
||||
|
||||
if (crashes.load() > 0) {
|
||||
std::cout << " ❌ Crashes detected: " << crashes.load() << " (need 0)\n";
|
||||
passed = false;
|
||||
} else {
|
||||
std::cout << " ✓ No crashes\n";
|
||||
}
|
||||
|
||||
if (reloadAttempts.load() > 0 && reloadSuccessRate < 99.0f) {
|
||||
std::cout << " ❌ Reload success rate too low: " << reloadSuccessRate << "% (need > 99%)\n";
|
||||
passed = false;
|
||||
} else if (reloadAttempts.load() > 0) {
|
||||
std::cout << " ✓ Reload success rate: " << reloadSuccessRate << "%\n";
|
||||
}
|
||||
|
||||
// File stability validation: reload time should be >= 100ms
|
||||
// This proves that ModuleLoader is waiting for file stability
|
||||
if (reloadAttempts.load() > 0) {
|
||||
if (avgReloadTime < 100.0f) {
|
||||
std::cout << " ❌ Reload time too fast: " << avgReloadTime << "ms (need >= 100ms)\n";
|
||||
std::cout << " File stability check is NOT working properly!\n";
|
||||
passed = false;
|
||||
} else if (avgReloadTime > 600.0f) {
|
||||
std::cout << " ⚠️ Reload time very slow: " << avgReloadTime << "ms (> 600ms)\n";
|
||||
std::cout << " File stability might be waiting too long\n";
|
||||
} else {
|
||||
std::cout << " ✓ Reload time: " << avgReloadTime << "ms (file stability working)\n";
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "\n";
|
||||
|
||||
// === FINAL RESULT ===
|
||||
std::cout << "================================================================================\n";
|
||||
if (passed) {
|
||||
std::cout << "Result: ✅ PASSED\n";
|
||||
} else {
|
||||
std::cout << "Result: ❌ FAILED\n";
|
||||
}
|
||||
std::cout << "================================================================================\n";
|
||||
|
||||
return passed ? 0 : 1;
|
||||
}
|
||||
#include "grove/ModuleLoader.h"
|
||||
#include "grove/SequentialModuleSystem.h"
|
||||
#include "grove/JsonDataNode.h"
|
||||
#include "../helpers/TestMetrics.h"
|
||||
#include "../helpers/TestAssertions.h"
|
||||
#include "../helpers/TestReporter.h"
|
||||
#include "../helpers/SystemUtils.h"
|
||||
#include "../helpers/AutoCompiler.h"
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <filesystem>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
|
||||
using namespace grove;
|
||||
using namespace TestHelpers;
|
||||
|
||||
int main() {
|
||||
TestReporter reporter("Race Condition Hunter");
|
||||
|
||||
std::cout << "================================================================================\n";
|
||||
std::cout << "TEST: Race Condition Hunter - Concurrent Compilation & Reload\n";
|
||||
std::cout << "================================================================================\n\n";
|
||||
|
||||
// === CONFIGURATION ===
|
||||
const int TOTAL_COMPILATIONS = 10; // Reduced for WSL2 compatibility
|
||||
const int COMPILE_INTERVAL_MS = 2000; // 2 seconds between compilations (allows for slower filesystems)
|
||||
const int FILE_CHECK_INTERVAL_MS = 50; // Check file changes every 50ms
|
||||
const float TARGET_FPS = 60.0f;
|
||||
const float FRAME_TIME = 1.0f / TARGET_FPS;
|
||||
|
||||
std::string modulePath = "./libTestModule.so";
|
||||
#ifdef _WIN32
|
||||
modulePath = "./libTestModule.dll";
|
||||
#endif
|
||||
// Test runs from build/tests/, so source files are at ../../tests/modules/
|
||||
std::string sourcePath = "../../tests/modules/TestModule.cpp";
|
||||
std::string buildDir = "build";
|
||||
|
||||
// === ATOMIC COUNTERS (Thread-safe) ===
|
||||
std::atomic<int> reloadAttempts{0};
|
||||
std::atomic<int> reloadSuccesses{0};
|
||||
std::atomic<int> reloadFailures{0};
|
||||
std::atomic<int> corruptedLoads{0};
|
||||
std::atomic<int> crashes{0};
|
||||
std::atomic<bool> engineRunning{true};
|
||||
std::atomic<bool> watcherRunning{true};
|
||||
|
||||
// CRITICAL: Mutex to protect moduleSystem access between threads
|
||||
std::mutex moduleSystemMutex;
|
||||
|
||||
// Reload timing
|
||||
std::mutex reloadTimesMutex;
|
||||
std::vector<float> reloadTimes;
|
||||
|
||||
// Metrics
|
||||
TestMetrics metrics;
|
||||
|
||||
// === SETUP ===
|
||||
std::cout << "Setup:\n";
|
||||
std::cout << " Module path: " << modulePath << "\n";
|
||||
std::cout << " Source path: " << sourcePath << "\n";
|
||||
std::cout << " Compilations: " << TOTAL_COMPILATIONS << "\n";
|
||||
std::cout << " Interval: " << COMPILE_INTERVAL_MS << "ms\n";
|
||||
std::cout << " Expected time: ~" << (TOTAL_COMPILATIONS * COMPILE_INTERVAL_MS / 1000) << "s\n\n";
|
||||
|
||||
// Load module initially
|
||||
ModuleLoader loader;
|
||||
auto moduleSystem = std::make_unique<SequentialModuleSystem>();
|
||||
|
||||
try {
|
||||
auto module = loader.load(modulePath, "TestModule", false);
|
||||
nlohmann::json configJson = nlohmann::json::object();
|
||||
auto config = std::make_unique<JsonDataNode>("config", configJson);
|
||||
module->setConfiguration(*config, nullptr, nullptr);
|
||||
moduleSystem->registerModule("TestModule", std::move(module));
|
||||
std::cout << " ✓ Initial module loaded\n\n";
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "❌ Failed to load initial module: " << e.what() << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
// === THREAD 1: AUTO-COMPILER ===
|
||||
std::cout << "Starting AutoCompiler thread...\n";
|
||||
AutoCompiler compiler("TestModule", buildDir, sourcePath);
|
||||
compiler.start(TOTAL_COMPILATIONS, COMPILE_INTERVAL_MS);
|
||||
|
||||
// === THREAD 2: FILE WATCHER ===
|
||||
std::cout << "Starting FileWatcher thread...\n";
|
||||
std::thread watcherThread([&]() {
|
||||
try {
|
||||
auto lastWriteTime = std::filesystem::last_write_time(modulePath);
|
||||
|
||||
while (watcherRunning.load() && engineRunning.load()) {
|
||||
try {
|
||||
auto currentTime = std::filesystem::last_write_time(modulePath);
|
||||
|
||||
if (currentTime != lastWriteTime) {
|
||||
reloadAttempts++;
|
||||
|
||||
// Measure reload time
|
||||
auto reloadStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
try {
|
||||
// CRITICAL: Lock moduleSystem during entire reload
|
||||
std::lock_guard<std::mutex> lock(moduleSystemMutex);
|
||||
|
||||
// Extract module and save state
|
||||
auto module = moduleSystem->extractModule();
|
||||
auto state = module->getState();
|
||||
|
||||
// CRITICAL: Destroy old module BEFORE reloading
|
||||
// The loader.load() will unload the old .so
|
||||
module.reset();
|
||||
|
||||
// Reload
|
||||
auto newModule = loader.load(modulePath, "TestModule", true);
|
||||
|
||||
// Check if module loaded correctly
|
||||
if (!newModule) {
|
||||
corruptedLoads++;
|
||||
reloadFailures++;
|
||||
// Can't recover - old module already destroyed
|
||||
} else {
|
||||
// VALIDATE MODULE INTEGRITY
|
||||
bool isCorrupted = false;
|
||||
try {
|
||||
// Test 1: Can we get health status?
|
||||
auto health = newModule->getHealthStatus();
|
||||
std::string version = health->getString("version", "");
|
||||
|
||||
// Test 2: Is version valid?
|
||||
if (version.empty() || version == "unknown") {
|
||||
isCorrupted = true;
|
||||
}
|
||||
|
||||
// Test 3: Can we set configuration?
|
||||
nlohmann::json configJson;
|
||||
configJson["test"] = "validation";
|
||||
auto testConfig = std::make_unique<JsonDataNode>("config", configJson);
|
||||
newModule->setConfiguration(*testConfig, nullptr, nullptr);
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
// Module crashes on basic operations = corrupted
|
||||
isCorrupted = true;
|
||||
}
|
||||
|
||||
if (isCorrupted) {
|
||||
corruptedLoads++;
|
||||
reloadFailures++;
|
||||
// Can't recover - old module already destroyed
|
||||
} else {
|
||||
// Module is valid, restore state and register
|
||||
newModule->setState(*state);
|
||||
moduleSystem->registerModule("TestModule", std::move(newModule));
|
||||
reloadSuccesses++;
|
||||
|
||||
// Record reload time
|
||||
auto reloadEnd = std::chrono::high_resolution_clock::now();
|
||||
float reloadTimeMs = std::chrono::duration<float, std::milli>(reloadEnd - reloadStart).count();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> timeLock(reloadTimesMutex);
|
||||
reloadTimes.push_back(reloadTimeMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
reloadFailures++;
|
||||
// Module might already be registered, continue
|
||||
}
|
||||
|
||||
lastWriteTime = currentTime;
|
||||
}
|
||||
} catch (const std::filesystem::filesystem_error&) {
|
||||
// File might be being written, ignore
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(FILE_CHECK_INTERVAL_MS));
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[FileWatcher] Exception: " << e.what() << "\n";
|
||||
}
|
||||
});
|
||||
|
||||
// === THREAD 3: ENGINE LOOP ===
|
||||
std::cout << "Starting Engine thread (60 FPS)...\n\n";
|
||||
std::thread engineThread([&]() {
|
||||
try {
|
||||
auto lastMemoryCheck = std::chrono::steady_clock::now();
|
||||
|
||||
while (engineRunning.load()) {
|
||||
auto frameStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
try {
|
||||
// TRY to lock moduleSystem (non-blocking)
|
||||
// If reload is happening, skip this frame
|
||||
if (moduleSystemMutex.try_lock()) {
|
||||
try {
|
||||
moduleSystem->processModules(FRAME_TIME);
|
||||
moduleSystemMutex.unlock();
|
||||
} catch (const std::exception& e) {
|
||||
moduleSystemMutex.unlock();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
// else: reload in progress, skip frame
|
||||
} catch (const std::exception& e) {
|
||||
crashes++;
|
||||
std::cerr << "[Engine] Crash detected: " << e.what() << "\n";
|
||||
}
|
||||
|
||||
auto frameEnd = std::chrono::high_resolution_clock::now();
|
||||
float frameTime = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count();
|
||||
metrics.recordFPS(1000.0f / std::max(frameTime, 0.1f));
|
||||
|
||||
// Check memory every second
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
if (std::chrono::duration_cast<std::chrono::seconds>(now - lastMemoryCheck).count() >= 1) {
|
||||
metrics.recordMemoryUsage(getCurrentMemoryUsage());
|
||||
lastMemoryCheck = now;
|
||||
}
|
||||
|
||||
// Sleep to maintain target FPS (if frame finished early)
|
||||
auto targetFrameTime = std::chrono::milliseconds(static_cast<int>(FRAME_TIME * 1000));
|
||||
auto elapsed = frameEnd - frameStart;
|
||||
if (elapsed < targetFrameTime) {
|
||||
std::this_thread::sleep_for(targetFrameTime - elapsed);
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[Engine] Thread exception: " << e.what() << "\n";
|
||||
}
|
||||
});
|
||||
|
||||
// === MONITORING LOOP ===
|
||||
std::cout << "Test running...\n";
|
||||
auto startTime = std::chrono::steady_clock::now();
|
||||
int lastPrintedPercent = 0;
|
||||
const int MAX_TEST_TIME_SECONDS = 90; // Maximum 1.5 minutes (allows all 20 compilations)
|
||||
|
||||
while (compiler.isRunning() || compiler.getCurrentIteration() < TOTAL_COMPILATIONS) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(2));
|
||||
|
||||
int currentIteration = compiler.getCurrentIteration();
|
||||
int percent = (currentIteration * 100) / TOTAL_COMPILATIONS;
|
||||
|
||||
// Check for timeout
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - startTime).count();
|
||||
|
||||
if (elapsed > MAX_TEST_TIME_SECONDS) {
|
||||
std::cout << "\n⚠️ Test timeout after " << elapsed << "s - stopping...\n";
|
||||
break;
|
||||
}
|
||||
|
||||
// Print progress every 10%
|
||||
if (percent >= lastPrintedPercent + 10 && percent <= 100) {
|
||||
std::cout << "\nProgress: " << percent << "% (" << currentIteration << "/" << TOTAL_COMPILATIONS << " compilations)\n";
|
||||
std::cout << " Elapsed: " << elapsed << "s\n";
|
||||
std::cout << " Compilations: " << compiler.getSuccessCount() << " OK, " << compiler.getFailureCount() << " FAIL\n";
|
||||
std::cout << " Reloads: " << reloadSuccesses.load() << " OK, " << reloadFailures.load() << " FAIL\n";
|
||||
std::cout << " Corrupted: " << corruptedLoads.load() << "\n";
|
||||
std::cout << " Crashes: " << crashes.load() << "\n";
|
||||
|
||||
lastPrintedPercent = percent;
|
||||
}
|
||||
}
|
||||
|
||||
// === CLEANUP ===
|
||||
std::cout << "\n\nStopping threads...\n";
|
||||
|
||||
compiler.stop();
|
||||
watcherRunning = false;
|
||||
engineRunning = false;
|
||||
|
||||
if (watcherThread.joinable()) {
|
||||
watcherThread.join();
|
||||
}
|
||||
if (engineThread.joinable()) {
|
||||
engineThread.join();
|
||||
}
|
||||
|
||||
std::cout << " ✓ All threads stopped\n\n";
|
||||
|
||||
// === CALCULATE STATISTICS ===
|
||||
float compileSuccessRate = (compiler.getSuccessCount() * 100.0f) / std::max(1, TOTAL_COMPILATIONS);
|
||||
float reloadSuccessRate = (reloadSuccesses.load() * 100.0f) / std::max(1, reloadAttempts.load());
|
||||
|
||||
float avgReloadTime = 0.0f;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(reloadTimesMutex);
|
||||
if (!reloadTimes.empty()) {
|
||||
float sum = 0.0f;
|
||||
for (float t : reloadTimes) {
|
||||
sum += t;
|
||||
}
|
||||
avgReloadTime = sum / reloadTimes.size();
|
||||
}
|
||||
}
|
||||
|
||||
auto endTime = std::chrono::steady_clock::now();
|
||||
auto totalTimeSeconds = std::chrono::duration_cast<std::chrono::seconds>(endTime - startTime).count();
|
||||
|
||||
// === PRINT SUMMARY ===
|
||||
std::cout << "================================================================================\n";
|
||||
std::cout << "RACE CONDITION HUNTER SUMMARY\n";
|
||||
std::cout << "================================================================================\n\n";
|
||||
|
||||
std::cout << "Duration: " << totalTimeSeconds << "s\n\n";
|
||||
|
||||
std::cout << "Compilations:\n";
|
||||
std::cout << " Total: " << TOTAL_COMPILATIONS << "\n";
|
||||
std::cout << " Successes: " << compiler.getSuccessCount() << " (" << std::fixed << std::setprecision(1) << compileSuccessRate << "%)\n";
|
||||
std::cout << " Failures: " << compiler.getFailureCount() << "\n\n";
|
||||
|
||||
std::cout << "Reloads:\n";
|
||||
std::cout << " Attempts: " << reloadAttempts.load() << "\n";
|
||||
std::cout << " Successes: " << reloadSuccesses.load() << " (" << std::fixed << std::setprecision(1) << reloadSuccessRate << "%)\n";
|
||||
std::cout << " Failures: " << reloadFailures.load() << "\n";
|
||||
std::cout << " Corrupted: " << corruptedLoads.load() << "\n";
|
||||
std::cout << " Avg time: " << std::fixed << std::setprecision(0) << avgReloadTime << "ms\n\n";
|
||||
|
||||
std::cout << "Stability:\n";
|
||||
std::cout << " Crashes: " << crashes.load() << "\n";
|
||||
std::cout << " Avg FPS: " << std::fixed << std::setprecision(1) << metrics.getFPSAvg() << "\n";
|
||||
std::cout << " Memory: " << std::fixed << std::setprecision(2) << metrics.getMemoryGrowth() << " MB\n\n";
|
||||
|
||||
// === ASSERTIONS ===
|
||||
bool passed = true;
|
||||
|
||||
std::cout << "Validating results...\n";
|
||||
|
||||
// MUST PASS criteria
|
||||
// Note: Lowered from 95% to 70% for WSL2/slower filesystem compatibility
|
||||
// The important thing is that compilations don't fail, they just might timeout
|
||||
if (compileSuccessRate < 70.0f) {
|
||||
std::cout << " ❌ Compile success rate too low: " << compileSuccessRate << "% (need > 70%)\n";
|
||||
passed = false;
|
||||
} else {
|
||||
std::cout << " ✓ Compile success rate: " << compileSuccessRate << "%\n";
|
||||
}
|
||||
|
||||
if (corruptedLoads.load() > 0) {
|
||||
std::cout << " ❌ Corrupted loads detected: " << corruptedLoads.load() << " (need 0)\n";
|
||||
passed = false;
|
||||
} else {
|
||||
std::cout << " ✓ No corrupted loads\n";
|
||||
}
|
||||
|
||||
if (crashes.load() > 0) {
|
||||
std::cout << " ❌ Crashes detected: " << crashes.load() << " (need 0)\n";
|
||||
passed = false;
|
||||
} else {
|
||||
std::cout << " ✓ No crashes\n";
|
||||
}
|
||||
|
||||
if (reloadAttempts.load() > 0 && reloadSuccessRate < 99.0f) {
|
||||
std::cout << " ❌ Reload success rate too low: " << reloadSuccessRate << "% (need > 99%)\n";
|
||||
passed = false;
|
||||
} else if (reloadAttempts.load() > 0) {
|
||||
std::cout << " ✓ Reload success rate: " << reloadSuccessRate << "%\n";
|
||||
}
|
||||
|
||||
// File stability validation: reload time should be >= 100ms
|
||||
// This proves that ModuleLoader is waiting for file stability
|
||||
if (reloadAttempts.load() > 0) {
|
||||
if (avgReloadTime < 100.0f) {
|
||||
std::cout << " ❌ Reload time too fast: " << avgReloadTime << "ms (need >= 100ms)\n";
|
||||
std::cout << " File stability check is NOT working properly!\n";
|
||||
passed = false;
|
||||
} else if (avgReloadTime > 600.0f) {
|
||||
std::cout << " ⚠️ Reload time very slow: " << avgReloadTime << "ms (> 600ms)\n";
|
||||
std::cout << " File stability might be waiting too long\n";
|
||||
} else {
|
||||
std::cout << " ✓ Reload time: " << avgReloadTime << "ms (file stability working)\n";
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "\n";
|
||||
|
||||
// === FINAL RESULT ===
|
||||
std::cout << "================================================================================\n";
|
||||
if (passed) {
|
||||
std::cout << "Result: ✅ PASSED\n";
|
||||
} else {
|
||||
std::cout << "Result: ❌ FAILED\n";
|
||||
}
|
||||
std::cout << "================================================================================\n";
|
||||
|
||||
return passed ? 0 : 1;
|
||||
}
|
||||
|
||||
@ -1,428 +1,431 @@
|
||||
// ============================================================================
|
||||
// test_05_memory_leak.cpp - Memory Leak Hunter
|
||||
// ============================================================================
|
||||
// Tests that repeated hot-reload cycles do not leak memory
|
||||
//
|
||||
// Strategy:
|
||||
// - Load the same .so file 200 times (no recompilation)
|
||||
// - Measure memory usage every 5 seconds
|
||||
// - Verify temp file cleanup
|
||||
// - Check for library handle leaks
|
||||
//
|
||||
// Success criteria:
|
||||
// - < 10 MB total memory growth
|
||||
// - < 50 KB average memory per reload
|
||||
// - Temp files cleaned up (≤ 2 at any time)
|
||||
// - No increase in mapped .so count
|
||||
// - 100% reload success rate
|
||||
// ============================================================================
|
||||
|
||||
#include "grove/ModuleLoader.h"
|
||||
#include "grove/SequentialModuleSystem.h"
|
||||
#include "grove/JsonDataNode.h"
|
||||
#include "../helpers/SystemUtils.h"
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <iostream>
|
||||
#include <iomanip>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <vector>
|
||||
#include <filesystem>
|
||||
#include <cmath>
|
||||
|
||||
using namespace grove;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// ============================================================================
|
||||
// Configuration
|
||||
// ============================================================================
|
||||
|
||||
const int TOTAL_RELOADS = 200;
|
||||
const int RELOAD_INTERVAL_MS = 500;
|
||||
const int MEMORY_CHECK_INTERVAL_MS = 5000;
|
||||
const float TARGET_FPS = 60.0f;
|
||||
const int MAX_TEST_TIME_SECONDS = 180;
|
||||
|
||||
// ============================================================================
|
||||
// State tracking
|
||||
// ============================================================================
|
||||
|
||||
struct MemorySnapshot {
|
||||
int reloadCount;
|
||||
size_t memoryBytes;
|
||||
int tempFiles;
|
||||
int mappedLibs;
|
||||
std::chrono::steady_clock::time_point timestamp;
|
||||
};
|
||||
|
||||
std::atomic<bool> g_running{true};
|
||||
std::atomic<int> g_reloadCount{0};
|
||||
std::atomic<int> g_reloadSuccesses{0};
|
||||
std::atomic<int> g_reloadFailures{0};
|
||||
std::atomic<int> g_crashes{0};
|
||||
std::vector<MemorySnapshot> g_snapshots;
|
||||
std::mutex g_snapshotMutex;
|
||||
|
||||
// ============================================================================
|
||||
// Reload Scheduler Thread
|
||||
// ============================================================================
|
||||
|
||||
void reloadSchedulerThread(ModuleLoader& loader, SequentialModuleSystem* moduleSystem,
|
||||
const fs::path& modulePath, std::mutex& moduleSystemMutex) {
|
||||
std::cout << " Starting ReloadScheduler thread...\n";
|
||||
|
||||
while (g_running && g_reloadCount < TOTAL_RELOADS) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(RELOAD_INTERVAL_MS));
|
||||
|
||||
if (!g_running) break;
|
||||
|
||||
try {
|
||||
std::lock_guard<std::mutex> lock(moduleSystemMutex);
|
||||
|
||||
// Extract current module and save state
|
||||
auto module = moduleSystem->extractModule();
|
||||
auto state = module->getState();
|
||||
auto config = std::make_unique<JsonDataNode>("config",
|
||||
dynamic_cast<const JsonDataNode&>(module->getConfiguration()).getJsonData());
|
||||
|
||||
// CRITICAL: Destroy old module BEFORE reloading to avoid use-after-free
|
||||
// The loader.load() will unload the old .so, so we must destroy the module first
|
||||
module.reset();
|
||||
|
||||
// Reload the same .so file
|
||||
auto newModule = loader.load(modulePath.string(), "LeakTestModule", true);
|
||||
|
||||
g_reloadCount++;
|
||||
|
||||
if (newModule) {
|
||||
// Restore state
|
||||
newModule->setConfiguration(*config, nullptr, nullptr);
|
||||
newModule->setState(*state);
|
||||
|
||||
// Register new module
|
||||
moduleSystem->registerModule("LeakTestModule", std::move(newModule));
|
||||
g_reloadSuccesses++;
|
||||
} else {
|
||||
// Reload failed - we can't recover (old module destroyed)
|
||||
g_reloadFailures++;
|
||||
}
|
||||
} catch (...) {
|
||||
g_crashes++;
|
||||
g_reloadFailures++;
|
||||
g_reloadCount++;
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << " ReloadScheduler thread finished.\n";
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Memory Monitor Thread
|
||||
// ============================================================================
|
||||
|
||||
void memoryMonitorThread() {
|
||||
std::cout << " Starting MemoryMonitor thread...\n";
|
||||
|
||||
auto startTime = std::chrono::steady_clock::now();
|
||||
|
||||
while (g_running) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(MEMORY_CHECK_INTERVAL_MS));
|
||||
|
||||
if (!g_running) break;
|
||||
|
||||
MemorySnapshot snapshot;
|
||||
snapshot.reloadCount = g_reloadCount;
|
||||
snapshot.memoryBytes = getCurrentMemoryUsage();
|
||||
snapshot.tempFiles = countTempFiles("/tmp/grove_module_*");
|
||||
snapshot.mappedLibs = getMappedLibraryCount();
|
||||
snapshot.timestamp = std::chrono::steady_clock::now();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_snapshotMutex);
|
||||
g_snapshots.push_back(snapshot);
|
||||
}
|
||||
|
||||
// Print progress
|
||||
float memoryMB = snapshot.memoryBytes / (1024.0f * 1024.0f);
|
||||
int progress = (snapshot.reloadCount * 100) / TOTAL_RELOADS;
|
||||
|
||||
std::cout << "\n Progress: " << snapshot.reloadCount << " reloads (" << progress << "%)\n";
|
||||
std::cout << " Memory: " << std::fixed << std::setprecision(1) << memoryMB << " MB\n";
|
||||
std::cout << " Temp files: " << snapshot.tempFiles << "\n";
|
||||
std::cout << " Mapped .so: " << snapshot.mappedLibs << "\n";
|
||||
}
|
||||
|
||||
std::cout << " MemoryMonitor thread finished.\n";
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Engine Thread
|
||||
// ============================================================================
|
||||
|
||||
void engineThread(SequentialModuleSystem* moduleSystem, std::mutex& moduleSystemMutex) {
|
||||
std::cout << " Starting Engine thread (60 FPS)...\n";
|
||||
|
||||
const float frameTime = 1.0f / TARGET_FPS;
|
||||
auto lastFrame = std::chrono::steady_clock::now();
|
||||
int frameCount = 0;
|
||||
|
||||
while (g_running && g_reloadCount < TOTAL_RELOADS) {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
float deltaTime = std::chrono::duration<float>(now - lastFrame).count();
|
||||
|
||||
if (deltaTime >= frameTime) {
|
||||
try {
|
||||
std::lock_guard<std::mutex> lock(moduleSystemMutex);
|
||||
moduleSystem->processModules(deltaTime);
|
||||
frameCount++;
|
||||
} catch (...) {
|
||||
g_crashes++;
|
||||
}
|
||||
|
||||
lastFrame = now;
|
||||
} else {
|
||||
// Sleep for remaining time
|
||||
int sleepMs = static_cast<int>((frameTime - deltaTime) * 1000);
|
||||
if (sleepMs > 0) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(sleepMs));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << " Engine thread finished (" << frameCount << " frames processed).\n";
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Main Test
|
||||
// ============================================================================
|
||||
|
||||
int main() {
|
||||
std::cout << "================================================================================\n";
|
||||
std::cout << "TEST: Memory Leak Hunter - " << TOTAL_RELOADS << " Reload Cycles\n";
|
||||
std::cout << "================================================================================\n\n";
|
||||
|
||||
// Find module path
|
||||
fs::path modulePath = "./libLeakTestModule.so";
|
||||
if (!fs::exists(modulePath)) {
|
||||
std::cerr << "❌ Module not found: " << modulePath << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::cout << "Setup:\n";
|
||||
std::cout << " Module path: " << modulePath << "\n";
|
||||
std::cout << " Total reloads: " << TOTAL_RELOADS << "\n";
|
||||
std::cout << " Interval: " << RELOAD_INTERVAL_MS << "ms\n";
|
||||
std::cout << " Expected time: ~" << (TOTAL_RELOADS * RELOAD_INTERVAL_MS / 1000) << "s\n\n";
|
||||
|
||||
// Create module loader and system
|
||||
ModuleLoader loader;
|
||||
auto moduleSystem = std::make_unique<SequentialModuleSystem>();
|
||||
std::mutex moduleSystemMutex;
|
||||
|
||||
// Disable verbose logging for performance
|
||||
moduleSystem->setLogLevel(spdlog::level::err);
|
||||
|
||||
// Load initial module
|
||||
try {
|
||||
auto module = loader.load(modulePath.string(), "LeakTestModule", false);
|
||||
if (!module) {
|
||||
std::cerr << "❌ Failed to load LeakTestModule\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
nlohmann::json configJson = nlohmann::json::object();
|
||||
auto config = std::make_unique<JsonDataNode>("config", configJson);
|
||||
module->setConfiguration(*config, nullptr, nullptr);
|
||||
moduleSystem->registerModule("LeakTestModule", std::move(module));
|
||||
std::cout << "✓ Initial module loaded\n\n";
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "❌ Failed to load initial module: " << e.what() << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Baseline memory
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
size_t baselineMemory = getCurrentMemoryUsage();
|
||||
int baselineMappedLibs = getMappedLibraryCount();
|
||||
float baselineMB = baselineMemory / (1024.0f * 1024.0f);
|
||||
|
||||
std::cout << "Baseline memory: " << std::fixed << std::setprecision(1) << baselineMB << " MB\n";
|
||||
std::cout << "Baseline mapped .so: " << baselineMappedLibs << "\n\n";
|
||||
|
||||
// Start threads
|
||||
auto startTime = std::chrono::steady_clock::now();
|
||||
|
||||
std::thread reloadThread(reloadSchedulerThread, std::ref(loader), moduleSystem.get(),
|
||||
modulePath, std::ref(moduleSystemMutex));
|
||||
std::thread monitorThread(memoryMonitorThread);
|
||||
std::thread engThread(engineThread, moduleSystem.get(), std::ref(moduleSystemMutex));
|
||||
|
||||
// Wait for completion or timeout
|
||||
auto deadline = startTime + std::chrono::seconds(MAX_TEST_TIME_SECONDS);
|
||||
|
||||
while (g_running && g_reloadCount < TOTAL_RELOADS) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
|
||||
if (std::chrono::steady_clock::now() > deadline) {
|
||||
std::cout << "\n⚠️ Test timeout after " << MAX_TEST_TIME_SECONDS << " seconds\n";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Stop threads
|
||||
g_running = false;
|
||||
|
||||
reloadThread.join();
|
||||
monitorThread.join();
|
||||
engThread.join();
|
||||
|
||||
auto endTime = std::chrono::steady_clock::now();
|
||||
float durationSeconds = std::chrono::duration<float>(endTime - startTime).count();
|
||||
|
||||
// Final measurements
|
||||
size_t finalMemory = getCurrentMemoryUsage();
|
||||
int finalMappedLibs = getMappedLibraryCount();
|
||||
int finalTempFiles = countTempFiles("/tmp/grove_module_*");
|
||||
|
||||
float finalMB = finalMemory / (1024.0f * 1024.0f);
|
||||
float growthMB = (finalMemory - baselineMemory) / (1024.0f * 1024.0f);
|
||||
|
||||
// ========================================================================
|
||||
// Results Summary
|
||||
// ========================================================================
|
||||
|
||||
std::cout << "\n================================================================================\n";
|
||||
std::cout << "MEMORY LEAK HUNTER SUMMARY\n";
|
||||
std::cout << "================================================================================\n\n";
|
||||
|
||||
std::cout << "Duration: " << static_cast<int>(durationSeconds) << "s\n\n";
|
||||
|
||||
std::cout << "Reloads:\n";
|
||||
std::cout << " Total: " << g_reloadCount << "\n";
|
||||
std::cout << " Successes: " << g_reloadSuccesses;
|
||||
if (g_reloadCount > 0) {
|
||||
float successRate = (g_reloadSuccesses * 100.0f) / g_reloadCount;
|
||||
std::cout << " (" << std::fixed << std::setprecision(1) << successRate << "%)";
|
||||
}
|
||||
std::cout << "\n";
|
||||
std::cout << " Failures: " << g_reloadFailures << "\n\n";
|
||||
|
||||
std::cout << "Memory Analysis:\n";
|
||||
std::cout << " Baseline: " << std::fixed << std::setprecision(1) << baselineMB << " MB\n";
|
||||
std::cout << " Final: " << finalMB << " MB\n";
|
||||
std::cout << " Growth: " << growthMB << " MB";
|
||||
|
||||
if (growthMB < 10.0f) {
|
||||
std::cout << " ✅";
|
||||
} else {
|
||||
std::cout << " ❌";
|
||||
}
|
||||
std::cout << "\n";
|
||||
|
||||
float memoryPerReloadKB = 0.0f;
|
||||
if (g_reloadCount > 0) {
|
||||
memoryPerReloadKB = (growthMB * 1024.0f) / g_reloadCount;
|
||||
}
|
||||
std::cout << " Per reload: " << std::fixed << std::setprecision(1) << memoryPerReloadKB << " KB";
|
||||
if (memoryPerReloadKB < 50.0f) {
|
||||
std::cout << " ✅";
|
||||
} else {
|
||||
std::cout << " ❌";
|
||||
}
|
||||
std::cout << "\n\n";
|
||||
|
||||
std::cout << "Resource Cleanup:\n";
|
||||
std::cout << " Temp files: " << finalTempFiles;
|
||||
if (finalTempFiles <= 2) {
|
||||
std::cout << " ✅";
|
||||
} else {
|
||||
std::cout << " ❌";
|
||||
}
|
||||
std::cout << "\n";
|
||||
|
||||
std::cout << " Mapped .so: " << finalMappedLibs;
|
||||
if (finalMappedLibs <= baselineMappedLibs + 2) {
|
||||
std::cout << " (stable) ✅";
|
||||
} else {
|
||||
std::cout << " (leak: +" << (finalMappedLibs - baselineMappedLibs) << ") ❌";
|
||||
}
|
||||
std::cout << "\n\n";
|
||||
|
||||
std::cout << "Stability:\n";
|
||||
std::cout << " Crashes: " << g_crashes;
|
||||
if (g_crashes == 0) {
|
||||
std::cout << " ✅";
|
||||
} else {
|
||||
std::cout << " ❌";
|
||||
}
|
||||
std::cout << "\n\n";
|
||||
|
||||
// ========================================================================
|
||||
// Validation
|
||||
// ========================================================================
|
||||
|
||||
std::cout << "Validating results...\n";
|
||||
|
||||
bool passed = true;
|
||||
|
||||
// 1. Memory growth < 10 MB
|
||||
if (growthMB > 10.0f) {
|
||||
std::cout << " ❌ Memory growth too high: " << growthMB << " MB (need < 10 MB)\n";
|
||||
passed = false;
|
||||
} else {
|
||||
std::cout << " ✓ Memory growth: " << growthMB << " MB (< 10 MB)\n";
|
||||
}
|
||||
|
||||
// 2. Memory per reload < 50 KB
|
||||
if (memoryPerReloadKB > 50.0f) {
|
||||
std::cout << " ❌ Memory per reload too high: " << memoryPerReloadKB << " KB (need < 50 KB)\n";
|
||||
passed = false;
|
||||
} else {
|
||||
std::cout << " ✓ Memory per reload: " << memoryPerReloadKB << " KB (< 50 KB)\n";
|
||||
}
|
||||
|
||||
// 3. Temp files cleaned
|
||||
if (finalTempFiles > 2) {
|
||||
std::cout << " ❌ Temp files not cleaned up: " << finalTempFiles << " (need ≤ 2)\n";
|
||||
passed = false;
|
||||
} else {
|
||||
std::cout << " ✓ Temp files cleaned: " << finalTempFiles << " (≤ 2)\n";
|
||||
}
|
||||
|
||||
// 4. No .so handle leaks
|
||||
if (finalMappedLibs > baselineMappedLibs + 2) {
|
||||
std::cout << " ❌ Library handle leak: +" << (finalMappedLibs - baselineMappedLibs) << "\n";
|
||||
passed = false;
|
||||
} else {
|
||||
std::cout << " ✓ No .so handle leaks\n";
|
||||
}
|
||||
|
||||
// 5. Reload success rate
|
||||
float successRate = g_reloadCount > 0 ? (g_reloadSuccesses * 100.0f) / g_reloadCount : 0.0f;
|
||||
if (successRate < 100.0f) {
|
||||
std::cout << " ❌ Reload success rate: " << std::fixed << std::setprecision(1)
|
||||
<< successRate << "% (need 100%)\n";
|
||||
passed = false;
|
||||
} else {
|
||||
std::cout << " ✓ Reload success rate: 100%\n";
|
||||
}
|
||||
|
||||
// 6. No crashes
|
||||
if (g_crashes > 0) {
|
||||
std::cout << " ❌ Crashes detected: " << g_crashes << "\n";
|
||||
passed = false;
|
||||
} else {
|
||||
std::cout << " ✓ No crashes\n";
|
||||
}
|
||||
|
||||
std::cout << "\n================================================================================\n";
|
||||
if (passed) {
|
||||
std::cout << "Result: ✅ PASSED\n";
|
||||
} else {
|
||||
std::cout << "Result: ❌ FAILED\n";
|
||||
}
|
||||
std::cout << "================================================================================\n";
|
||||
|
||||
return passed ? 0 : 1;
|
||||
}
|
||||
// ============================================================================
|
||||
// test_05_memory_leak.cpp - Memory Leak Hunter
|
||||
// ============================================================================
|
||||
// Tests that repeated hot-reload cycles do not leak memory
|
||||
//
|
||||
// Strategy:
|
||||
// - Load the same .so file 200 times (no recompilation)
|
||||
// - Measure memory usage every 5 seconds
|
||||
// - Verify temp file cleanup
|
||||
// - Check for library handle leaks
|
||||
//
|
||||
// Success criteria:
|
||||
// - < 10 MB total memory growth
|
||||
// - < 50 KB average memory per reload
|
||||
// - Temp files cleaned up (≤ 2 at any time)
|
||||
// - No increase in mapped .so count
|
||||
// - 100% reload success rate
|
||||
// ============================================================================
|
||||
|
||||
#include "grove/ModuleLoader.h"
|
||||
#include "grove/SequentialModuleSystem.h"
|
||||
#include "grove/JsonDataNode.h"
|
||||
#include "../helpers/SystemUtils.h"
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <iostream>
|
||||
#include <iomanip>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <vector>
|
||||
#include <filesystem>
|
||||
#include <cmath>
|
||||
|
||||
using namespace grove;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// ============================================================================
|
||||
// Configuration
|
||||
// ============================================================================
|
||||
|
||||
const int TOTAL_RELOADS = 200;
|
||||
const int RELOAD_INTERVAL_MS = 500;
|
||||
const int MEMORY_CHECK_INTERVAL_MS = 5000;
|
||||
const float TARGET_FPS = 60.0f;
|
||||
const int MAX_TEST_TIME_SECONDS = 180;
|
||||
|
||||
// ============================================================================
|
||||
// State tracking
|
||||
// ============================================================================
|
||||
|
||||
struct MemorySnapshot {
|
||||
int reloadCount;
|
||||
size_t memoryBytes;
|
||||
int tempFiles;
|
||||
int mappedLibs;
|
||||
std::chrono::steady_clock::time_point timestamp;
|
||||
};
|
||||
|
||||
std::atomic<bool> g_running{true};
|
||||
std::atomic<int> g_reloadCount{0};
|
||||
std::atomic<int> g_reloadSuccesses{0};
|
||||
std::atomic<int> g_reloadFailures{0};
|
||||
std::atomic<int> g_crashes{0};
|
||||
std::vector<MemorySnapshot> g_snapshots;
|
||||
std::mutex g_snapshotMutex;
|
||||
|
||||
// ============================================================================
|
||||
// Reload Scheduler Thread
|
||||
// ============================================================================
|
||||
|
||||
void reloadSchedulerThread(ModuleLoader& loader, SequentialModuleSystem* moduleSystem,
|
||||
const fs::path& modulePath, std::mutex& moduleSystemMutex) {
|
||||
std::cout << " Starting ReloadScheduler thread...\n";
|
||||
|
||||
while (g_running && g_reloadCount < TOTAL_RELOADS) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(RELOAD_INTERVAL_MS));
|
||||
|
||||
if (!g_running) break;
|
||||
|
||||
try {
|
||||
std::lock_guard<std::mutex> lock(moduleSystemMutex);
|
||||
|
||||
// Extract current module and save state
|
||||
auto module = moduleSystem->extractModule();
|
||||
auto state = module->getState();
|
||||
auto config = std::make_unique<JsonDataNode>("config",
|
||||
dynamic_cast<const JsonDataNode&>(module->getConfiguration()).getJsonData());
|
||||
|
||||
// CRITICAL: Destroy old module BEFORE reloading to avoid use-after-free
|
||||
// The loader.load() will unload the old .so, so we must destroy the module first
|
||||
module.reset();
|
||||
|
||||
// Reload the same .so file
|
||||
auto newModule = loader.load(modulePath.string(), "LeakTestModule", true);
|
||||
|
||||
g_reloadCount++;
|
||||
|
||||
if (newModule) {
|
||||
// Restore state
|
||||
newModule->setConfiguration(*config, nullptr, nullptr);
|
||||
newModule->setState(*state);
|
||||
|
||||
// Register new module
|
||||
moduleSystem->registerModule("LeakTestModule", std::move(newModule));
|
||||
g_reloadSuccesses++;
|
||||
} else {
|
||||
// Reload failed - we can't recover (old module destroyed)
|
||||
g_reloadFailures++;
|
||||
}
|
||||
} catch (...) {
|
||||
g_crashes++;
|
||||
g_reloadFailures++;
|
||||
g_reloadCount++;
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << " ReloadScheduler thread finished.\n";
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Memory Monitor Thread
|
||||
// ============================================================================
|
||||
|
||||
void memoryMonitorThread() {
|
||||
std::cout << " Starting MemoryMonitor thread...\n";
|
||||
|
||||
auto startTime = std::chrono::steady_clock::now();
|
||||
|
||||
while (g_running) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(MEMORY_CHECK_INTERVAL_MS));
|
||||
|
||||
if (!g_running) break;
|
||||
|
||||
MemorySnapshot snapshot;
|
||||
snapshot.reloadCount = g_reloadCount;
|
||||
snapshot.memoryBytes = getCurrentMemoryUsage();
|
||||
snapshot.tempFiles = countTempFiles("/tmp/grove_module_*");
|
||||
snapshot.mappedLibs = getMappedLibraryCount();
|
||||
snapshot.timestamp = std::chrono::steady_clock::now();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_snapshotMutex);
|
||||
g_snapshots.push_back(snapshot);
|
||||
}
|
||||
|
||||
// Print progress
|
||||
float memoryMB = snapshot.memoryBytes / (1024.0f * 1024.0f);
|
||||
int progress = (snapshot.reloadCount * 100) / TOTAL_RELOADS;
|
||||
|
||||
std::cout << "\n Progress: " << snapshot.reloadCount << " reloads (" << progress << "%)\n";
|
||||
std::cout << " Memory: " << std::fixed << std::setprecision(1) << memoryMB << " MB\n";
|
||||
std::cout << " Temp files: " << snapshot.tempFiles << "\n";
|
||||
std::cout << " Mapped .so: " << snapshot.mappedLibs << "\n";
|
||||
}
|
||||
|
||||
std::cout << " MemoryMonitor thread finished.\n";
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Engine Thread
|
||||
// ============================================================================
|
||||
|
||||
void engineThread(SequentialModuleSystem* moduleSystem, std::mutex& moduleSystemMutex) {
|
||||
std::cout << " Starting Engine thread (60 FPS)...\n";
|
||||
|
||||
const float frameTime = 1.0f / TARGET_FPS;
|
||||
auto lastFrame = std::chrono::steady_clock::now();
|
||||
int frameCount = 0;
|
||||
|
||||
while (g_running && g_reloadCount < TOTAL_RELOADS) {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
float deltaTime = std::chrono::duration<float>(now - lastFrame).count();
|
||||
|
||||
if (deltaTime >= frameTime) {
|
||||
try {
|
||||
std::lock_guard<std::mutex> lock(moduleSystemMutex);
|
||||
moduleSystem->processModules(deltaTime);
|
||||
frameCount++;
|
||||
} catch (...) {
|
||||
g_crashes++;
|
||||
}
|
||||
|
||||
lastFrame = now;
|
||||
} else {
|
||||
// Sleep for remaining time
|
||||
int sleepMs = static_cast<int>((frameTime - deltaTime) * 1000);
|
||||
if (sleepMs > 0) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(sleepMs));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << " Engine thread finished (" << frameCount << " frames processed).\n";
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Main Test
|
||||
// ============================================================================
|
||||
|
||||
int main() {
|
||||
std::cout << "================================================================================\n";
|
||||
std::cout << "TEST: Memory Leak Hunter - " << TOTAL_RELOADS << " Reload Cycles\n";
|
||||
std::cout << "================================================================================\n\n";
|
||||
|
||||
// Find module path
|
||||
fs::path modulePath = "./libLeakTestModule.so";
|
||||
#ifdef _WIN32
|
||||
modulePath = "./libLeakTestModule.dll";
|
||||
#endif
|
||||
if (!fs::exists(modulePath)) {
|
||||
std::cerr << "❌ Module not found: " << modulePath << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::cout << "Setup:\n";
|
||||
std::cout << " Module path: " << modulePath << "\n";
|
||||
std::cout << " Total reloads: " << TOTAL_RELOADS << "\n";
|
||||
std::cout << " Interval: " << RELOAD_INTERVAL_MS << "ms\n";
|
||||
std::cout << " Expected time: ~" << (TOTAL_RELOADS * RELOAD_INTERVAL_MS / 1000) << "s\n\n";
|
||||
|
||||
// Create module loader and system
|
||||
ModuleLoader loader;
|
||||
auto moduleSystem = std::make_unique<SequentialModuleSystem>();
|
||||
std::mutex moduleSystemMutex;
|
||||
|
||||
// Disable verbose logging for performance
|
||||
moduleSystem->setLogLevel(spdlog::level::err);
|
||||
|
||||
// Load initial module
|
||||
try {
|
||||
auto module = loader.load(modulePath.string(), "LeakTestModule", false);
|
||||
if (!module) {
|
||||
std::cerr << "❌ Failed to load LeakTestModule\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
nlohmann::json configJson = nlohmann::json::object();
|
||||
auto config = std::make_unique<JsonDataNode>("config", configJson);
|
||||
module->setConfiguration(*config, nullptr, nullptr);
|
||||
moduleSystem->registerModule("LeakTestModule", std::move(module));
|
||||
std::cout << "✓ Initial module loaded\n\n";
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "❌ Failed to load initial module: " << e.what() << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Baseline memory
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
size_t baselineMemory = getCurrentMemoryUsage();
|
||||
int baselineMappedLibs = getMappedLibraryCount();
|
||||
float baselineMB = baselineMemory / (1024.0f * 1024.0f);
|
||||
|
||||
std::cout << "Baseline memory: " << std::fixed << std::setprecision(1) << baselineMB << " MB\n";
|
||||
std::cout << "Baseline mapped .so: " << baselineMappedLibs << "\n\n";
|
||||
|
||||
// Start threads
|
||||
auto startTime = std::chrono::steady_clock::now();
|
||||
|
||||
std::thread reloadThread(reloadSchedulerThread, std::ref(loader), moduleSystem.get(),
|
||||
modulePath, std::ref(moduleSystemMutex));
|
||||
std::thread monitorThread(memoryMonitorThread);
|
||||
std::thread engThread(engineThread, moduleSystem.get(), std::ref(moduleSystemMutex));
|
||||
|
||||
// Wait for completion or timeout
|
||||
auto deadline = startTime + std::chrono::seconds(MAX_TEST_TIME_SECONDS);
|
||||
|
||||
while (g_running && g_reloadCount < TOTAL_RELOADS) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
|
||||
if (std::chrono::steady_clock::now() > deadline) {
|
||||
std::cout << "\n⚠️ Test timeout after " << MAX_TEST_TIME_SECONDS << " seconds\n";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Stop threads
|
||||
g_running = false;
|
||||
|
||||
reloadThread.join();
|
||||
monitorThread.join();
|
||||
engThread.join();
|
||||
|
||||
auto endTime = std::chrono::steady_clock::now();
|
||||
float durationSeconds = std::chrono::duration<float>(endTime - startTime).count();
|
||||
|
||||
// Final measurements
|
||||
size_t finalMemory = getCurrentMemoryUsage();
|
||||
int finalMappedLibs = getMappedLibraryCount();
|
||||
int finalTempFiles = countTempFiles("/tmp/grove_module_*");
|
||||
|
||||
float finalMB = finalMemory / (1024.0f * 1024.0f);
|
||||
float growthMB = (finalMemory - baselineMemory) / (1024.0f * 1024.0f);
|
||||
|
||||
// ========================================================================
|
||||
// Results Summary
|
||||
// ========================================================================
|
||||
|
||||
std::cout << "\n================================================================================\n";
|
||||
std::cout << "MEMORY LEAK HUNTER SUMMARY\n";
|
||||
std::cout << "================================================================================\n\n";
|
||||
|
||||
std::cout << "Duration: " << static_cast<int>(durationSeconds) << "s\n\n";
|
||||
|
||||
std::cout << "Reloads:\n";
|
||||
std::cout << " Total: " << g_reloadCount << "\n";
|
||||
std::cout << " Successes: " << g_reloadSuccesses;
|
||||
if (g_reloadCount > 0) {
|
||||
float successRate = (g_reloadSuccesses * 100.0f) / g_reloadCount;
|
||||
std::cout << " (" << std::fixed << std::setprecision(1) << successRate << "%)";
|
||||
}
|
||||
std::cout << "\n";
|
||||
std::cout << " Failures: " << g_reloadFailures << "\n\n";
|
||||
|
||||
std::cout << "Memory Analysis:\n";
|
||||
std::cout << " Baseline: " << std::fixed << std::setprecision(1) << baselineMB << " MB\n";
|
||||
std::cout << " Final: " << finalMB << " MB\n";
|
||||
std::cout << " Growth: " << growthMB << " MB";
|
||||
|
||||
if (growthMB < 10.0f) {
|
||||
std::cout << " ✅";
|
||||
} else {
|
||||
std::cout << " ❌";
|
||||
}
|
||||
std::cout << "\n";
|
||||
|
||||
float memoryPerReloadKB = 0.0f;
|
||||
if (g_reloadCount > 0) {
|
||||
memoryPerReloadKB = (growthMB * 1024.0f) / g_reloadCount;
|
||||
}
|
||||
std::cout << " Per reload: " << std::fixed << std::setprecision(1) << memoryPerReloadKB << " KB";
|
||||
if (memoryPerReloadKB < 50.0f) {
|
||||
std::cout << " ✅";
|
||||
} else {
|
||||
std::cout << " ❌";
|
||||
}
|
||||
std::cout << "\n\n";
|
||||
|
||||
std::cout << "Resource Cleanup:\n";
|
||||
std::cout << " Temp files: " << finalTempFiles;
|
||||
if (finalTempFiles <= 2) {
|
||||
std::cout << " ✅";
|
||||
} else {
|
||||
std::cout << " ❌";
|
||||
}
|
||||
std::cout << "\n";
|
||||
|
||||
std::cout << " Mapped .so: " << finalMappedLibs;
|
||||
if (finalMappedLibs <= baselineMappedLibs + 2) {
|
||||
std::cout << " (stable) ✅";
|
||||
} else {
|
||||
std::cout << " (leak: +" << (finalMappedLibs - baselineMappedLibs) << ") ❌";
|
||||
}
|
||||
std::cout << "\n\n";
|
||||
|
||||
std::cout << "Stability:\n";
|
||||
std::cout << " Crashes: " << g_crashes;
|
||||
if (g_crashes == 0) {
|
||||
std::cout << " ✅";
|
||||
} else {
|
||||
std::cout << " ❌";
|
||||
}
|
||||
std::cout << "\n\n";
|
||||
|
||||
// ========================================================================
|
||||
// Validation
|
||||
// ========================================================================
|
||||
|
||||
std::cout << "Validating results...\n";
|
||||
|
||||
bool passed = true;
|
||||
|
||||
// 1. Memory growth < 10 MB
|
||||
if (growthMB > 10.0f) {
|
||||
std::cout << " ❌ Memory growth too high: " << growthMB << " MB (need < 10 MB)\n";
|
||||
passed = false;
|
||||
} else {
|
||||
std::cout << " ✓ Memory growth: " << growthMB << " MB (< 10 MB)\n";
|
||||
}
|
||||
|
||||
// 2. Memory per reload < 50 KB
|
||||
if (memoryPerReloadKB > 50.0f) {
|
||||
std::cout << " ❌ Memory per reload too high: " << memoryPerReloadKB << " KB (need < 50 KB)\n";
|
||||
passed = false;
|
||||
} else {
|
||||
std::cout << " ✓ Memory per reload: " << memoryPerReloadKB << " KB (< 50 KB)\n";
|
||||
}
|
||||
|
||||
// 3. Temp files cleaned
|
||||
if (finalTempFiles > 2) {
|
||||
std::cout << " ❌ Temp files not cleaned up: " << finalTempFiles << " (need ≤ 2)\n";
|
||||
passed = false;
|
||||
} else {
|
||||
std::cout << " ✓ Temp files cleaned: " << finalTempFiles << " (≤ 2)\n";
|
||||
}
|
||||
|
||||
// 4. No .so handle leaks
|
||||
if (finalMappedLibs > baselineMappedLibs + 2) {
|
||||
std::cout << " ❌ Library handle leak: +" << (finalMappedLibs - baselineMappedLibs) << "\n";
|
||||
passed = false;
|
||||
} else {
|
||||
std::cout << " ✓ No .so handle leaks\n";
|
||||
}
|
||||
|
||||
// 5. Reload success rate
|
||||
float successRate = g_reloadCount > 0 ? (g_reloadSuccesses * 100.0f) / g_reloadCount : 0.0f;
|
||||
if (successRate < 100.0f) {
|
||||
std::cout << " ❌ Reload success rate: " << std::fixed << std::setprecision(1)
|
||||
<< successRate << "% (need 100%)\n";
|
||||
passed = false;
|
||||
} else {
|
||||
std::cout << " ✓ Reload success rate: 100%\n";
|
||||
}
|
||||
|
||||
// 6. No crashes
|
||||
if (g_crashes > 0) {
|
||||
std::cout << " ❌ Crashes detected: " << g_crashes << "\n";
|
||||
passed = false;
|
||||
} else {
|
||||
std::cout << " ✓ No crashes\n";
|
||||
}
|
||||
|
||||
std::cout << "\n================================================================================\n";
|
||||
if (passed) {
|
||||
std::cout << "Result: ✅ PASSED\n";
|
||||
} else {
|
||||
std::cout << "Result: ❌ FAILED\n";
|
||||
}
|
||||
std::cout << "================================================================================\n";
|
||||
|
||||
return passed ? 0 : 1;
|
||||
}
|
||||
|
||||
@ -1,272 +1,275 @@
|
||||
#include "grove/ModuleLoader.h"
|
||||
#include "grove/SequentialModuleSystem.h"
|
||||
#include "grove/JsonDataNode.h"
|
||||
#include "../helpers/TestMetrics.h"
|
||||
#include "../helpers/TestAssertions.h"
|
||||
#include "../helpers/TestReporter.h"
|
||||
#include "../helpers/SystemUtils.h"
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <stdexcept>
|
||||
|
||||
using namespace grove;
|
||||
|
||||
/**
|
||||
* Test 06: Error Recovery
|
||||
*
|
||||
* Objectif: Valider que le système peut détecter et récupérer automatiquement
|
||||
* d'un crash de module via hot-reload.
|
||||
*
|
||||
* Scénario:
|
||||
* 1. Charger ErrorRecoveryModule avec crash planifié à frame 60
|
||||
* 2. Lancer execution jusqu'au crash
|
||||
* 3. Détecter le crash (exception)
|
||||
* 4. Trigger hot-reload automatique
|
||||
* 5. Vérifier que le module récupère (auto-recovery)
|
||||
* 6. Continuer execution normalement
|
||||
*
|
||||
* Métriques:
|
||||
* - Crash detection time
|
||||
* - Recovery success rate
|
||||
* - State preservation après recovery
|
||||
* - Stabilité du moteur
|
||||
*/
|
||||
|
||||
int main() {
|
||||
TestReporter reporter("Error Recovery");
|
||||
TestMetrics metrics;
|
||||
|
||||
std::cout << "================================================================================\n";
|
||||
std::cout << "TEST: Error Recovery - Crash Detection & Auto-Recovery\n";
|
||||
std::cout << "================================================================================\n\n";
|
||||
|
||||
// === SETUP ===
|
||||
std::cout << "Setup: Loading ErrorRecoveryModule with crash trigger...\n";
|
||||
|
||||
ModuleLoader loader;
|
||||
auto moduleSystem = std::make_unique<SequentialModuleSystem>();
|
||||
|
||||
// Charger module
|
||||
std::string modulePath = "./libErrorRecoveryModule.so";
|
||||
auto module = loader.load(modulePath, "ErrorRecoveryModule", false);
|
||||
|
||||
// Config: crash à frame 60, type runtime_error
|
||||
nlohmann::json configJson;
|
||||
configJson["crashAtFrame"] = 60;
|
||||
configJson["crashType"] = 0; // runtime_error
|
||||
configJson["enableAutoRecovery"] = true;
|
||||
configJson["versionTag"] = "v1.0";
|
||||
auto config = std::make_unique<JsonDataNode>("config", configJson);
|
||||
|
||||
module->setConfiguration(*config, nullptr, nullptr);
|
||||
moduleSystem->registerModule("ErrorRecoveryModule", std::move(module));
|
||||
|
||||
std::cout << " ✓ Module loaded with crash trigger at frame 60\n\n";
|
||||
|
||||
// === PHASE 1: Run until crash ===
|
||||
std::cout << "Phase 1: Running until crash (target frame: 60)...\n";
|
||||
|
||||
bool crashDetected = false;
|
||||
int crashFrame = -1;
|
||||
auto crashDetectionStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
for (int frame = 1; frame <= 100; frame++) {
|
||||
try {
|
||||
auto frameStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
moduleSystem->processModules(1.0f / 60.0f);
|
||||
|
||||
auto frameEnd = std::chrono::high_resolution_clock::now();
|
||||
float frameTime = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count();
|
||||
metrics.recordFPS(1000.0f / frameTime);
|
||||
|
||||
if (frame % 20 == 0) {
|
||||
std::cout << " Frame " << frame << "/100 - OK\n";
|
||||
}
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
// CRASH DÉTECTÉ !
|
||||
auto crashDetectionEnd = std::chrono::high_resolution_clock::now();
|
||||
float detectionTime = std::chrono::duration<float, std::milli>(
|
||||
crashDetectionEnd - crashDetectionStart).count();
|
||||
|
||||
crashDetected = true;
|
||||
crashFrame = frame;
|
||||
|
||||
std::cout << "\n💥 CRASH DETECTED at frame " << frame << "\n";
|
||||
std::cout << " Exception: " << e.what() << "\n";
|
||||
std::cout << " Detection time: " << detectionTime << "ms\n\n";
|
||||
|
||||
metrics.recordCrash("runtime_error at frame " + std::to_string(frame));
|
||||
reporter.addMetric("crash_detection_time_ms", detectionTime);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ASSERT_TRUE(crashDetected, "Crash should have been detected");
|
||||
ASSERT_EQ(crashFrame, 60, "Crash should occur at frame 60");
|
||||
reporter.addAssertion("crash_detected", crashDetected);
|
||||
reporter.addAssertion("crash_at_expected_frame", crashFrame == 60);
|
||||
|
||||
// === PHASE 2: Extract state before recovery ===
|
||||
std::cout << "Phase 2: Extracting state before recovery...\n";
|
||||
|
||||
auto crashedModule = moduleSystem->extractModule();
|
||||
auto preRecoveryState = crashedModule->getState();
|
||||
|
||||
auto* jsonNodeBefore = dynamic_cast<JsonDataNode*>(preRecoveryState.get());
|
||||
if (!jsonNodeBefore) {
|
||||
std::cerr << "❌ Failed to extract state before recovery\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
const auto& stateBefore = jsonNodeBefore->getJsonData();
|
||||
int frameCountBefore = stateBefore.value("frameCount", 0);
|
||||
int crashCountBefore = stateBefore.value("crashCount", 0);
|
||||
bool hasCrashedBefore = stateBefore.value("hasCrashed", false);
|
||||
|
||||
std::cout << " State before recovery:\n";
|
||||
std::cout << " Frame count: " << frameCountBefore << "\n";
|
||||
std::cout << " Crash count: " << crashCountBefore << "\n";
|
||||
std::cout << " Has crashed: " << (hasCrashedBefore ? "YES" : "NO") << "\n\n";
|
||||
|
||||
ASSERT_TRUE(hasCrashedBefore, "Module should be in crashed state");
|
||||
|
||||
// === PHASE 3: Trigger hot-reload (recovery) ===
|
||||
std::cout << "Phase 3: Triggering hot-reload for recovery...\n";
|
||||
|
||||
auto recoveryStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
// Hot-reload via ModuleLoader
|
||||
auto recoveredModule = loader.reload(std::move(crashedModule));
|
||||
|
||||
auto recoveryEnd = std::chrono::high_resolution_clock::now();
|
||||
float recoveryTime = std::chrono::duration<float, std::milli>(recoveryEnd - recoveryStart).count();
|
||||
|
||||
std::cout << " ✓ Hot-reload completed in " << recoveryTime << "ms\n";
|
||||
|
||||
metrics.recordReloadTime(recoveryTime);
|
||||
reporter.addMetric("recovery_time_ms", recoveryTime);
|
||||
|
||||
// Ré-enregistrer module récupéré
|
||||
moduleSystem->registerModule("ErrorRecoveryModule", std::move(recoveredModule));
|
||||
|
||||
// === PHASE 4: Verify recovery ===
|
||||
std::cout << "\nPhase 4: Verifying recovery...\n";
|
||||
|
||||
auto recoveredModuleRef = moduleSystem->extractModule();
|
||||
auto postRecoveryState = recoveredModuleRef->getState();
|
||||
|
||||
auto* jsonNodeAfter = dynamic_cast<JsonDataNode*>(postRecoveryState.get());
|
||||
if (!jsonNodeAfter) {
|
||||
std::cerr << "❌ Failed to extract state after recovery\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
const auto& stateAfter = jsonNodeAfter->getJsonData();
|
||||
int frameCountAfter = stateAfter.value("frameCount", 0);
|
||||
int crashCountAfter = stateAfter.value("crashCount", 0);
|
||||
int recoveryCountAfter = stateAfter.value("recoveryCount", 0);
|
||||
bool hasCrashedAfter = stateAfter.value("hasCrashed", false);
|
||||
int crashAtFrameAfter = stateAfter.value("crashAtFrame", -1);
|
||||
|
||||
std::cout << " State after recovery:\n";
|
||||
std::cout << " Frame count: " << frameCountAfter << "\n";
|
||||
std::cout << " Crash count: " << crashCountAfter << "\n";
|
||||
std::cout << " Recovery count: " << recoveryCountAfter << "\n";
|
||||
std::cout << " Has crashed: " << (hasCrashedAfter ? "YES" : "NO") << "\n";
|
||||
std::cout << " Crash trigger: " << crashAtFrameAfter << "\n\n";
|
||||
|
||||
// Vérifications de recovery
|
||||
ASSERT_EQ(frameCountAfter, frameCountBefore, "Frame count should be preserved");
|
||||
ASSERT_FALSE(hasCrashedAfter, "Module should no longer be in crashed state");
|
||||
ASSERT_EQ(recoveryCountAfter, 1, "Recovery count should be 1");
|
||||
ASSERT_EQ(crashAtFrameAfter, -1, "Crash trigger should be disabled");
|
||||
|
||||
reporter.addAssertion("frame_count_preserved", frameCountAfter == frameCountBefore);
|
||||
reporter.addAssertion("crash_state_cleared", !hasCrashedAfter);
|
||||
reporter.addAssertion("recovery_count_incremented", recoveryCountAfter == 1);
|
||||
reporter.addAssertion("crash_trigger_disabled", crashAtFrameAfter == -1);
|
||||
|
||||
std::cout << " ✅ RECOVERY SUCCESSFUL - Module is healthy again\n\n";
|
||||
|
||||
// Ré-enregistrer pour phase 5
|
||||
moduleSystem->registerModule("ErrorRecoveryModule", std::move(recoveredModuleRef));
|
||||
|
||||
// === PHASE 5: Continue execution (stability check) ===
|
||||
std::cout << "Phase 5: Stability check - Running 120 more frames...\n";
|
||||
|
||||
bool stableExecution = true;
|
||||
int framesAfterRecovery = 0;
|
||||
|
||||
for (int frame = 1; frame <= 120; frame++) {
|
||||
try {
|
||||
auto frameStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
moduleSystem->processModules(1.0f / 60.0f);
|
||||
|
||||
auto frameEnd = std::chrono::high_resolution_clock::now();
|
||||
float frameTime = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count();
|
||||
metrics.recordFPS(1000.0f / frameTime);
|
||||
|
||||
framesAfterRecovery++;
|
||||
|
||||
if (frame % 30 == 0) {
|
||||
std::cout << " Frame " << frame << "/120 - Stable\n";
|
||||
}
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
std::cout << "\n❌ UNEXPECTED CRASH after recovery at frame " << frame << "\n";
|
||||
std::cout << " Exception: " << e.what() << "\n";
|
||||
stableExecution = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ASSERT_TRUE(stableExecution, "Module should execute stably after recovery");
|
||||
ASSERT_EQ(framesAfterRecovery, 120, "Should complete all 120 frames");
|
||||
|
||||
reporter.addAssertion("stable_after_recovery", stableExecution);
|
||||
reporter.addMetric("frames_after_recovery", static_cast<float>(framesAfterRecovery));
|
||||
|
||||
std::cout << " ✅ Stability verified - " << framesAfterRecovery << " frames executed without issues\n\n";
|
||||
|
||||
// === VÉRIFICATIONS FINALES ===
|
||||
std::cout << "Final verifications...\n";
|
||||
|
||||
// Memory growth
|
||||
size_t memGrowth = metrics.getMemoryGrowth();
|
||||
float memGrowthMB = memGrowth / (1024.0f * 1024.0f);
|
||||
ASSERT_LT(memGrowthMB, 10.0f, "Memory growth should be < 10MB");
|
||||
reporter.addMetric("memory_growth_mb", memGrowthMB);
|
||||
|
||||
// FPS (moins strict pour test de recovery - focus sur stability)
|
||||
float minFPS = metrics.getFPSMin();
|
||||
ASSERT_GT(minFPS, 5.0f, "Min FPS should be > 5 (recovery test allows slower frames)");
|
||||
reporter.addMetric("fps_min", minFPS);
|
||||
reporter.addMetric("fps_avg", metrics.getFPSAvg());
|
||||
|
||||
// Recovery time threshold
|
||||
ASSERT_LT(recoveryTime, 500.0f, "Recovery time should be < 500ms");
|
||||
|
||||
// Crash count
|
||||
int totalCrashes = metrics.getCrashCount();
|
||||
ASSERT_EQ(totalCrashes, 1, "Should have exactly 1 controlled crash");
|
||||
reporter.addMetric("total_crashes", static_cast<float>(totalCrashes));
|
||||
|
||||
// === RAPPORTS ===
|
||||
std::cout << "\n";
|
||||
std::cout << "Summary:\n";
|
||||
std::cout << " 🎯 Crash detected at frame " << crashFrame << " (expected: 60)\n";
|
||||
std::cout << " 🔄 Recovery time: " << recoveryTime << "ms\n";
|
||||
std::cout << " ✅ Stable execution: " << framesAfterRecovery << " frames after recovery\n";
|
||||
std::cout << " 💾 Memory growth: " << memGrowthMB << " MB\n";
|
||||
std::cout << " 📊 FPS: min=" << minFPS << ", avg=" << metrics.getFPSAvg() << "\n\n";
|
||||
|
||||
metrics.printReport();
|
||||
reporter.printFinalReport();
|
||||
|
||||
return reporter.getExitCode();
|
||||
}
|
||||
#include "grove/ModuleLoader.h"
|
||||
#include "grove/SequentialModuleSystem.h"
|
||||
#include "grove/JsonDataNode.h"
|
||||
#include "../helpers/TestMetrics.h"
|
||||
#include "../helpers/TestAssertions.h"
|
||||
#include "../helpers/TestReporter.h"
|
||||
#include "../helpers/SystemUtils.h"
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <stdexcept>
|
||||
|
||||
using namespace grove;
|
||||
|
||||
/**
|
||||
* Test 06: Error Recovery
|
||||
*
|
||||
* Objectif: Valider que le système peut détecter et récupérer automatiquement
|
||||
* d'un crash de module via hot-reload.
|
||||
*
|
||||
* Scénario:
|
||||
* 1. Charger ErrorRecoveryModule avec crash planifié à frame 60
|
||||
* 2. Lancer execution jusqu'au crash
|
||||
* 3. Détecter le crash (exception)
|
||||
* 4. Trigger hot-reload automatique
|
||||
* 5. Vérifier que le module récupère (auto-recovery)
|
||||
* 6. Continuer execution normalement
|
||||
*
|
||||
* Métriques:
|
||||
* - Crash detection time
|
||||
* - Recovery success rate
|
||||
* - State preservation après recovery
|
||||
* - Stabilité du moteur
|
||||
*/
|
||||
|
||||
int main() {
|
||||
TestReporter reporter("Error Recovery");
|
||||
TestMetrics metrics;
|
||||
|
||||
std::cout << "================================================================================\n";
|
||||
std::cout << "TEST: Error Recovery - Crash Detection & Auto-Recovery\n";
|
||||
std::cout << "================================================================================\n\n";
|
||||
|
||||
// === SETUP ===
|
||||
std::cout << "Setup: Loading ErrorRecoveryModule with crash trigger...\n";
|
||||
|
||||
ModuleLoader loader;
|
||||
auto moduleSystem = std::make_unique<SequentialModuleSystem>();
|
||||
|
||||
// Charger module
|
||||
std::string modulePath = "./libErrorRecoveryModule.so";
|
||||
#ifdef _WIN32
|
||||
modulePath = "./libErrorRecoveryModule.dll";
|
||||
#endif
|
||||
auto module = loader.load(modulePath, "ErrorRecoveryModule", false);
|
||||
|
||||
// Config: crash à frame 60, type runtime_error
|
||||
nlohmann::json configJson;
|
||||
configJson["crashAtFrame"] = 60;
|
||||
configJson["crashType"] = 0; // runtime_error
|
||||
configJson["enableAutoRecovery"] = true;
|
||||
configJson["versionTag"] = "v1.0";
|
||||
auto config = std::make_unique<JsonDataNode>("config", configJson);
|
||||
|
||||
module->setConfiguration(*config, nullptr, nullptr);
|
||||
moduleSystem->registerModule("ErrorRecoveryModule", std::move(module));
|
||||
|
||||
std::cout << " ✓ Module loaded with crash trigger at frame 60\n\n";
|
||||
|
||||
// === PHASE 1: Run until crash ===
|
||||
std::cout << "Phase 1: Running until crash (target frame: 60)...\n";
|
||||
|
||||
bool crashDetected = false;
|
||||
int crashFrame = -1;
|
||||
auto crashDetectionStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
for (int frame = 1; frame <= 100; frame++) {
|
||||
try {
|
||||
auto frameStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
moduleSystem->processModules(1.0f / 60.0f);
|
||||
|
||||
auto frameEnd = std::chrono::high_resolution_clock::now();
|
||||
float frameTime = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count();
|
||||
metrics.recordFPS(1000.0f / frameTime);
|
||||
|
||||
if (frame % 20 == 0) {
|
||||
std::cout << " Frame " << frame << "/100 - OK\n";
|
||||
}
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
// CRASH DÉTECTÉ !
|
||||
auto crashDetectionEnd = std::chrono::high_resolution_clock::now();
|
||||
float detectionTime = std::chrono::duration<float, std::milli>(
|
||||
crashDetectionEnd - crashDetectionStart).count();
|
||||
|
||||
crashDetected = true;
|
||||
crashFrame = frame;
|
||||
|
||||
std::cout << "\n💥 CRASH DETECTED at frame " << frame << "\n";
|
||||
std::cout << " Exception: " << e.what() << "\n";
|
||||
std::cout << " Detection time: " << detectionTime << "ms\n\n";
|
||||
|
||||
metrics.recordCrash("runtime_error at frame " + std::to_string(frame));
|
||||
reporter.addMetric("crash_detection_time_ms", detectionTime);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ASSERT_TRUE(crashDetected, "Crash should have been detected");
|
||||
ASSERT_EQ(crashFrame, 60, "Crash should occur at frame 60");
|
||||
reporter.addAssertion("crash_detected", crashDetected);
|
||||
reporter.addAssertion("crash_at_expected_frame", crashFrame == 60);
|
||||
|
||||
// === PHASE 2: Extract state before recovery ===
|
||||
std::cout << "Phase 2: Extracting state before recovery...\n";
|
||||
|
||||
auto crashedModule = moduleSystem->extractModule();
|
||||
auto preRecoveryState = crashedModule->getState();
|
||||
|
||||
auto* jsonNodeBefore = dynamic_cast<JsonDataNode*>(preRecoveryState.get());
|
||||
if (!jsonNodeBefore) {
|
||||
std::cerr << "❌ Failed to extract state before recovery\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
const auto& stateBefore = jsonNodeBefore->getJsonData();
|
||||
int frameCountBefore = stateBefore.value("frameCount", 0);
|
||||
int crashCountBefore = stateBefore.value("crashCount", 0);
|
||||
bool hasCrashedBefore = stateBefore.value("hasCrashed", false);
|
||||
|
||||
std::cout << " State before recovery:\n";
|
||||
std::cout << " Frame count: " << frameCountBefore << "\n";
|
||||
std::cout << " Crash count: " << crashCountBefore << "\n";
|
||||
std::cout << " Has crashed: " << (hasCrashedBefore ? "YES" : "NO") << "\n\n";
|
||||
|
||||
ASSERT_TRUE(hasCrashedBefore, "Module should be in crashed state");
|
||||
|
||||
// === PHASE 3: Trigger hot-reload (recovery) ===
|
||||
std::cout << "Phase 3: Triggering hot-reload for recovery...\n";
|
||||
|
||||
auto recoveryStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
// Hot-reload via ModuleLoader
|
||||
auto recoveredModule = loader.reload(std::move(crashedModule));
|
||||
|
||||
auto recoveryEnd = std::chrono::high_resolution_clock::now();
|
||||
float recoveryTime = std::chrono::duration<float, std::milli>(recoveryEnd - recoveryStart).count();
|
||||
|
||||
std::cout << " ✓ Hot-reload completed in " << recoveryTime << "ms\n";
|
||||
|
||||
metrics.recordReloadTime(recoveryTime);
|
||||
reporter.addMetric("recovery_time_ms", recoveryTime);
|
||||
|
||||
// Ré-enregistrer module récupéré
|
||||
moduleSystem->registerModule("ErrorRecoveryModule", std::move(recoveredModule));
|
||||
|
||||
// === PHASE 4: Verify recovery ===
|
||||
std::cout << "\nPhase 4: Verifying recovery...\n";
|
||||
|
||||
auto recoveredModuleRef = moduleSystem->extractModule();
|
||||
auto postRecoveryState = recoveredModuleRef->getState();
|
||||
|
||||
auto* jsonNodeAfter = dynamic_cast<JsonDataNode*>(postRecoveryState.get());
|
||||
if (!jsonNodeAfter) {
|
||||
std::cerr << "❌ Failed to extract state after recovery\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
const auto& stateAfter = jsonNodeAfter->getJsonData();
|
||||
int frameCountAfter = stateAfter.value("frameCount", 0);
|
||||
int crashCountAfter = stateAfter.value("crashCount", 0);
|
||||
int recoveryCountAfter = stateAfter.value("recoveryCount", 0);
|
||||
bool hasCrashedAfter = stateAfter.value("hasCrashed", false);
|
||||
int crashAtFrameAfter = stateAfter.value("crashAtFrame", -1);
|
||||
|
||||
std::cout << " State after recovery:\n";
|
||||
std::cout << " Frame count: " << frameCountAfter << "\n";
|
||||
std::cout << " Crash count: " << crashCountAfter << "\n";
|
||||
std::cout << " Recovery count: " << recoveryCountAfter << "\n";
|
||||
std::cout << " Has crashed: " << (hasCrashedAfter ? "YES" : "NO") << "\n";
|
||||
std::cout << " Crash trigger: " << crashAtFrameAfter << "\n\n";
|
||||
|
||||
// Vérifications de recovery
|
||||
ASSERT_EQ(frameCountAfter, frameCountBefore, "Frame count should be preserved");
|
||||
ASSERT_FALSE(hasCrashedAfter, "Module should no longer be in crashed state");
|
||||
ASSERT_EQ(recoveryCountAfter, 1, "Recovery count should be 1");
|
||||
ASSERT_EQ(crashAtFrameAfter, -1, "Crash trigger should be disabled");
|
||||
|
||||
reporter.addAssertion("frame_count_preserved", frameCountAfter == frameCountBefore);
|
||||
reporter.addAssertion("crash_state_cleared", !hasCrashedAfter);
|
||||
reporter.addAssertion("recovery_count_incremented", recoveryCountAfter == 1);
|
||||
reporter.addAssertion("crash_trigger_disabled", crashAtFrameAfter == -1);
|
||||
|
||||
std::cout << " ✅ RECOVERY SUCCESSFUL - Module is healthy again\n\n";
|
||||
|
||||
// Ré-enregistrer pour phase 5
|
||||
moduleSystem->registerModule("ErrorRecoveryModule", std::move(recoveredModuleRef));
|
||||
|
||||
// === PHASE 5: Continue execution (stability check) ===
|
||||
std::cout << "Phase 5: Stability check - Running 120 more frames...\n";
|
||||
|
||||
bool stableExecution = true;
|
||||
int framesAfterRecovery = 0;
|
||||
|
||||
for (int frame = 1; frame <= 120; frame++) {
|
||||
try {
|
||||
auto frameStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
moduleSystem->processModules(1.0f / 60.0f);
|
||||
|
||||
auto frameEnd = std::chrono::high_resolution_clock::now();
|
||||
float frameTime = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count();
|
||||
metrics.recordFPS(1000.0f / frameTime);
|
||||
|
||||
framesAfterRecovery++;
|
||||
|
||||
if (frame % 30 == 0) {
|
||||
std::cout << " Frame " << frame << "/120 - Stable\n";
|
||||
}
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
std::cout << "\n❌ UNEXPECTED CRASH after recovery at frame " << frame << "\n";
|
||||
std::cout << " Exception: " << e.what() << "\n";
|
||||
stableExecution = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ASSERT_TRUE(stableExecution, "Module should execute stably after recovery");
|
||||
ASSERT_EQ(framesAfterRecovery, 120, "Should complete all 120 frames");
|
||||
|
||||
reporter.addAssertion("stable_after_recovery", stableExecution);
|
||||
reporter.addMetric("frames_after_recovery", static_cast<float>(framesAfterRecovery));
|
||||
|
||||
std::cout << " ✅ Stability verified - " << framesAfterRecovery << " frames executed without issues\n\n";
|
||||
|
||||
// === VÉRIFICATIONS FINALES ===
|
||||
std::cout << "Final verifications...\n";
|
||||
|
||||
// Memory growth
|
||||
size_t memGrowth = metrics.getMemoryGrowth();
|
||||
float memGrowthMB = memGrowth / (1024.0f * 1024.0f);
|
||||
ASSERT_LT(memGrowthMB, 10.0f, "Memory growth should be < 10MB");
|
||||
reporter.addMetric("memory_growth_mb", memGrowthMB);
|
||||
|
||||
// FPS (moins strict pour test de recovery - focus sur stability)
|
||||
float minFPS = metrics.getFPSMin();
|
||||
ASSERT_GT(minFPS, 5.0f, "Min FPS should be > 5 (recovery test allows slower frames)");
|
||||
reporter.addMetric("fps_min", minFPS);
|
||||
reporter.addMetric("fps_avg", metrics.getFPSAvg());
|
||||
|
||||
// Recovery time threshold
|
||||
ASSERT_LT(recoveryTime, 500.0f, "Recovery time should be < 500ms");
|
||||
|
||||
// Crash count
|
||||
int totalCrashes = metrics.getCrashCount();
|
||||
ASSERT_EQ(totalCrashes, 1, "Should have exactly 1 controlled crash");
|
||||
reporter.addMetric("total_crashes", static_cast<float>(totalCrashes));
|
||||
|
||||
// === RAPPORTS ===
|
||||
std::cout << "\n";
|
||||
std::cout << "Summary:\n";
|
||||
std::cout << " 🎯 Crash detected at frame " << crashFrame << " (expected: 60)\n";
|
||||
std::cout << " 🔄 Recovery time: " << recoveryTime << "ms\n";
|
||||
std::cout << " ✅ Stable execution: " << framesAfterRecovery << " frames after recovery\n";
|
||||
std::cout << " 💾 Memory growth: " << memGrowthMB << " MB\n";
|
||||
std::cout << " 📊 FPS: min=" << minFPS << ", avg=" << metrics.getFPSAvg() << "\n\n";
|
||||
|
||||
metrics.printReport();
|
||||
reporter.printFinalReport();
|
||||
|
||||
return reporter.getExitCode();
|
||||
}
|
||||
|
||||
@ -1,418 +1,421 @@
|
||||
#include "grove/ModuleLoader.h"
|
||||
#include "grove/SequentialModuleSystem.h"
|
||||
#include "grove/JsonDataNode.h"
|
||||
#include "../helpers/TestMetrics.h"
|
||||
#include "../helpers/TestAssertions.h"
|
||||
#include "../helpers/TestReporter.h"
|
||||
#include "../helpers/SystemUtils.h"
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <stdexcept>
|
||||
#include <numeric>
|
||||
#include <fstream>
|
||||
|
||||
using namespace grove;
|
||||
|
||||
/**
|
||||
* Test 07: Limite Tests
|
||||
*
|
||||
* Objectif: Valider la robustesse du système face aux conditions extrêmes:
|
||||
* - Large state (100MB+)
|
||||
* - Long initialization
|
||||
* - Timeouts
|
||||
* - Memory pressure
|
||||
* - State corruption detection
|
||||
*/
|
||||
|
||||
int main() {
|
||||
TestReporter reporter("Limite Tests");
|
||||
TestMetrics metrics;
|
||||
|
||||
std::cout << "================================================================================\n";
|
||||
std::cout << "TEST: Limite Tests - Extreme Conditions & Edge Cases\n";
|
||||
std::cout << "================================================================================\n\n";
|
||||
|
||||
// ========================================================================
|
||||
// TEST 1: Large State Serialization
|
||||
// ========================================================================
|
||||
std::cout << "=== TEST 1: Large State Serialization ===\n\n";
|
||||
|
||||
ModuleLoader loader;
|
||||
auto moduleSystem = std::make_unique<SequentialModuleSystem>();
|
||||
|
||||
std::string modulePath = "./libHeavyStateModule.so";
|
||||
auto module = loader.load(modulePath, "HeavyStateModule", false);
|
||||
|
||||
// Config: particules réduites pour test rapide, mais assez pour être significatif
|
||||
nlohmann::json configJson;
|
||||
configJson["version"] = "v1.0";
|
||||
configJson["particleCount"] = 100000; // 100k au lieu de 1M pour test rapide
|
||||
configJson["terrainSize"] = 1000; // 1000x1000 au lieu de 10000x10000
|
||||
configJson["initDuration"] = 2.0f; // 2s au lieu de 8s
|
||||
configJson["initTimeout"] = 5.0f;
|
||||
|
||||
auto config = std::make_unique<JsonDataNode>("config", configJson);
|
||||
|
||||
// Mesurer temps d'initialisation
|
||||
std::cout << "Initializing module...\n";
|
||||
auto initStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
module->setConfiguration(*config, nullptr, nullptr);
|
||||
|
||||
auto initEnd = std::chrono::high_resolution_clock::now();
|
||||
float initTime = std::chrono::duration<float>(initEnd - initStart).count();
|
||||
|
||||
std::cout << " Init time: " << initTime << "s\n";
|
||||
ASSERT_GT(initTime, 1.5f, "Init should take at least 1.5s (simulated heavy init)");
|
||||
ASSERT_LT(initTime, 3.0f, "Init should not exceed 3s");
|
||||
reporter.addMetric("init_time_s", initTime);
|
||||
|
||||
moduleSystem->registerModule("HeavyStateModule", std::move(module));
|
||||
|
||||
// Exécuter quelques frames
|
||||
std::cout << "Running 300 frames...\n";
|
||||
for (int i = 0; i < 300; i++) {
|
||||
moduleSystem->processModules(1.0f / 60.0f);
|
||||
}
|
||||
|
||||
// Mesurer temps de getState()
|
||||
std::cout << "Extracting state (getState)...\n";
|
||||
auto getStateStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
auto heavyModule = moduleSystem->extractModule();
|
||||
auto state = heavyModule->getState();
|
||||
|
||||
auto getStateEnd = std::chrono::high_resolution_clock::now();
|
||||
float getStateTime = std::chrono::duration<float, std::milli>(getStateEnd - getStateStart).count();
|
||||
|
||||
std::cout << " getState time: " << getStateTime << "ms\n";
|
||||
ASSERT_LT(getStateTime, 2000.0f, "getState() should be < 2000ms");
|
||||
reporter.addMetric("getstate_time_ms", getStateTime);
|
||||
|
||||
// Estimer taille de l'état
|
||||
auto* jsonNode = dynamic_cast<JsonDataNode*>(state.get());
|
||||
if (jsonNode) {
|
||||
std::string stateStr = jsonNode->getJsonData().dump();
|
||||
size_t stateSize = stateStr.size();
|
||||
float stateSizeMB = stateSize / 1024.0f / 1024.0f;
|
||||
std::cout << " State size: " << stateSizeMB << " MB\n";
|
||||
reporter.addMetric("state_size_mb", stateSizeMB);
|
||||
}
|
||||
|
||||
// Recharger le module (simuler hot-reload)
|
||||
std::cout << "Reloading module...\n";
|
||||
auto reloadStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
// setState() est appelé automatiquement par reload()
|
||||
auto moduleReloaded = loader.reload(std::move(heavyModule));
|
||||
|
||||
auto reloadEnd = std::chrono::high_resolution_clock::now();
|
||||
float reloadTime = std::chrono::duration<float, std::milli>(reloadEnd - reloadStart).count();
|
||||
std::cout << " Total reload time: " << reloadTime << "ms\n";
|
||||
reporter.addMetric("reload_time_ms", reloadTime);
|
||||
|
||||
// Ré-enregistrer
|
||||
moduleSystem->registerModule("HeavyStateModule", std::move(moduleReloaded));
|
||||
|
||||
// Vérifier intégrité après reload
|
||||
auto heavyModuleAfter = moduleSystem->extractModule();
|
||||
auto stateAfter = heavyModuleAfter->getState();
|
||||
auto* jsonNodeAfter = dynamic_cast<JsonDataNode*>(stateAfter.get());
|
||||
if (jsonNodeAfter) {
|
||||
const auto& dataAfter = jsonNodeAfter->getJsonData();
|
||||
int particleCount = dataAfter["config"]["particleCount"];
|
||||
ASSERT_EQ(particleCount, 100000, "Should have 100k particles after reload");
|
||||
reporter.addAssertion("particles_preserved", particleCount == 100000);
|
||||
std::cout << " ✓ Particles preserved: " << particleCount << "\n";
|
||||
}
|
||||
|
||||
// Ré-enregistrer pour continuer
|
||||
moduleSystem->registerModule("HeavyStateModule", std::move(heavyModuleAfter));
|
||||
|
||||
// Continuer exécution
|
||||
std::cout << "Running 300 more frames post-reload...\n";
|
||||
for (int i = 0; i < 300; i++) {
|
||||
moduleSystem->processModules(1.0f / 60.0f);
|
||||
}
|
||||
|
||||
std::cout << "\n✅ TEST 1 PASSED\n\n";
|
||||
|
||||
// ========================================================================
|
||||
// TEST 2: Long Initialization Timeout
|
||||
// ========================================================================
|
||||
std::cout << "=== TEST 2: Long Initialization Timeout ===\n\n";
|
||||
|
||||
auto moduleSystem2 = std::make_unique<SequentialModuleSystem>();
|
||||
auto moduleTimeout = loader.load(modulePath, "HeavyStateModule", false);
|
||||
|
||||
// Config avec init long + timeout court (va échouer)
|
||||
nlohmann::json configTimeout;
|
||||
configTimeout["version"] = "v1.0";
|
||||
configTimeout["particleCount"] = 100000;
|
||||
configTimeout["terrainSize"] = 1000;
|
||||
configTimeout["initDuration"] = 4.0f; // Init va prendre 4s
|
||||
configTimeout["initTimeout"] = 3.0f; // Timeout à 3s (trop court)
|
||||
|
||||
auto configTimeoutNode = std::make_unique<JsonDataNode>("config", configTimeout);
|
||||
|
||||
bool timedOut = false;
|
||||
std::cout << "Attempting init with timeout=3s (duration=4s)...\n";
|
||||
try {
|
||||
moduleTimeout->setConfiguration(*configTimeoutNode, nullptr, nullptr);
|
||||
} catch (const std::exception& e) {
|
||||
std::string msg = e.what();
|
||||
if (msg.find("timeout") != std::string::npos ||
|
||||
msg.find("Timeout") != std::string::npos ||
|
||||
msg.find("exceeded") != std::string::npos) {
|
||||
timedOut = true;
|
||||
std::cout << " ✓ Timeout detected: " << msg << "\n";
|
||||
} else {
|
||||
std::cout << " ✗ Unexpected error: " << msg << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
ASSERT_TRUE(timedOut, "Should timeout when init > timeout threshold");
|
||||
reporter.addAssertion("timeout_detection", timedOut);
|
||||
|
||||
// Réessayer avec timeout suffisant
|
||||
std::cout << "\nRetrying with adequate timeout=6s...\n";
|
||||
auto moduleTimeout2 = loader.load(modulePath, "HeavyStateModule", false);
|
||||
|
||||
nlohmann::json configOk;
|
||||
configOk["version"] = "v1.0";
|
||||
configOk["particleCount"] = 50000;
|
||||
configOk["terrainSize"] = 500;
|
||||
configOk["initDuration"] = 2.0f;
|
||||
configOk["initTimeout"] = 5.0f;
|
||||
|
||||
auto configOkNode = std::make_unique<JsonDataNode>("config", configOk);
|
||||
|
||||
bool success = true;
|
||||
try {
|
||||
moduleTimeout2->setConfiguration(*configOkNode, nullptr, nullptr);
|
||||
moduleSystem2->registerModule("HeavyStateModule", std::move(moduleTimeout2));
|
||||
moduleSystem2->processModules(1.0f / 60.0f);
|
||||
std::cout << " ✓ Init succeeded with adequate timeout\n";
|
||||
} catch (const std::exception& e) {
|
||||
success = false;
|
||||
std::cout << " ✗ Failed: " << e.what() << "\n";
|
||||
}
|
||||
|
||||
ASSERT_TRUE(success, "Should succeed with adequate timeout");
|
||||
reporter.addAssertion("timeout_recovery", success);
|
||||
|
||||
std::cout << "\n✅ TEST 2 PASSED\n\n";
|
||||
|
||||
// ========================================================================
|
||||
// TEST 3: Memory Pressure During Reload
|
||||
// ========================================================================
|
||||
std::cout << "=== TEST 3: Memory Pressure During Reload ===\n\n";
|
||||
|
||||
// Créer un nouveau system pour ce test
|
||||
auto moduleSystem3Pressure = std::make_unique<SequentialModuleSystem>();
|
||||
auto modulePressureInit = loader.load(modulePath, "HeavyStateModule", false);
|
||||
|
||||
nlohmann::json configPressure;
|
||||
configPressure["version"] = "v1.0";
|
||||
configPressure["particleCount"] = 10000;
|
||||
configPressure["terrainSize"] = 100;
|
||||
configPressure["initDuration"] = 0.5f;
|
||||
auto configPressureNode = std::make_unique<JsonDataNode>("config", configPressure);
|
||||
modulePressureInit->setConfiguration(*configPressureNode, nullptr, nullptr);
|
||||
moduleSystem3Pressure->registerModule("HeavyStateModule", std::move(modulePressureInit));
|
||||
|
||||
size_t memBefore = getCurrentMemoryUsage();
|
||||
std::cout << "Memory before: " << (memBefore / 1024.0f / 1024.0f) << " MB\n";
|
||||
|
||||
// Exécuter quelques frames
|
||||
std::cout << "Running 300 frames...\n";
|
||||
for (int i = 0; i < 300; i++) {
|
||||
moduleSystem3Pressure->processModules(1.0f / 60.0f);
|
||||
}
|
||||
|
||||
// Allouer temporairement beaucoup de mémoire
|
||||
std::cout << "Allocating temporary 50MB during reload...\n";
|
||||
std::vector<uint8_t> tempAlloc;
|
||||
|
||||
auto reloadPressureStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
// Allouer 50MB
|
||||
tempAlloc.resize(50 * 1024 * 1024);
|
||||
std::fill(tempAlloc.begin(), tempAlloc.end(), 0x42);
|
||||
|
||||
size_t memDuringAlloc = getCurrentMemoryUsage();
|
||||
std::cout << " Memory with allocation: " << (memDuringAlloc / 1024.0f / 1024.0f) << " MB\n";
|
||||
|
||||
// Reload pendant la pression mémoire
|
||||
auto modulePressure = moduleSystem3Pressure->extractModule();
|
||||
auto modulePressureReloaded = loader.reload(std::move(modulePressure));
|
||||
moduleSystem3Pressure->registerModule("HeavyStateModule", std::move(modulePressureReloaded));
|
||||
|
||||
auto reloadPressureEnd = std::chrono::high_resolution_clock::now();
|
||||
float reloadPressureTime = std::chrono::duration<float, std::milli>(
|
||||
reloadPressureEnd - reloadPressureStart).count();
|
||||
|
||||
std::cout << " Reload under pressure: " << reloadPressureTime << "ms\n";
|
||||
reporter.addMetric("reload_under_pressure_ms", reloadPressureTime);
|
||||
|
||||
// Libérer allocation temporaire
|
||||
tempAlloc.clear();
|
||||
tempAlloc.shrink_to_fit();
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
|
||||
size_t memAfter = getCurrentMemoryUsage();
|
||||
std::cout << " Memory after cleanup: " << (memAfter / 1024.0f / 1024.0f) << " MB\n";
|
||||
|
||||
long memGrowth = static_cast<long>(memAfter) - static_cast<long>(memBefore);
|
||||
float memGrowthMB = memGrowth / 1024.0f / 1024.0f;
|
||||
std::cout << " Net memory growth: " << memGrowthMB << " MB\n";
|
||||
|
||||
// Tolérance: max 10MB de croissance
|
||||
ASSERT_LT(std::abs(memGrowth), 10 * 1024 * 1024, "Memory growth should be < 10MB");
|
||||
reporter.addMetric("memory_growth_mb", memGrowthMB);
|
||||
|
||||
std::cout << "\n✅ TEST 3 PASSED\n\n";
|
||||
|
||||
// ========================================================================
|
||||
// TEST 4: Incremental Reloads
|
||||
// ========================================================================
|
||||
std::cout << "=== TEST 4: Incremental Reloads ===\n\n";
|
||||
|
||||
auto moduleSystem3 = std::make_unique<SequentialModuleSystem>();
|
||||
|
||||
nlohmann::json configIncremental;
|
||||
configIncremental["version"] = "v1.0";
|
||||
configIncremental["particleCount"] = 10000; // Petit pour test rapide
|
||||
configIncremental["terrainSize"] = 100;
|
||||
configIncremental["initDuration"] = 0.5f;
|
||||
configIncremental["incrementalState"] = true;
|
||||
|
||||
auto configIncrNode = std::make_unique<JsonDataNode>("config", configIncremental);
|
||||
auto moduleIncr = loader.load(modulePath, "HeavyStateModule", false);
|
||||
moduleIncr->setConfiguration(*configIncrNode, nullptr, nullptr);
|
||||
moduleSystem3->registerModule("HeavyStateModule", std::move(moduleIncr));
|
||||
|
||||
std::vector<float> incrementalTimes;
|
||||
|
||||
std::cout << "Performing 5 incremental reloads...\n";
|
||||
for (int reload = 0; reload < 5; reload++) {
|
||||
// Exécuter 60 frames
|
||||
for (int i = 0; i < 60; i++) {
|
||||
moduleSystem3->processModules(1.0f / 60.0f);
|
||||
}
|
||||
|
||||
// Reload
|
||||
auto incStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
auto moduleInc = moduleSystem3->extractModule();
|
||||
auto moduleIncReloaded = loader.reload(std::move(moduleInc));
|
||||
moduleSystem3->registerModule("HeavyStateModule", std::move(moduleIncReloaded));
|
||||
|
||||
auto incEnd = std::chrono::high_resolution_clock::now();
|
||||
float incTime = std::chrono::duration<float, std::milli>(incEnd - incStart).count();
|
||||
|
||||
incrementalTimes.push_back(incTime);
|
||||
std::cout << " Reload #" << reload << ": " << incTime << "ms\n";
|
||||
}
|
||||
|
||||
float avgIncremental = std::accumulate(incrementalTimes.begin(), incrementalTimes.end(), 0.0f)
|
||||
/ incrementalTimes.size();
|
||||
std::cout << "\nAverage incremental reload: " << avgIncremental << "ms\n";
|
||||
|
||||
ASSERT_LT(avgIncremental, 2000.0f, "Incremental reloads should be reasonably fast");
|
||||
reporter.addMetric("avg_incremental_reload_ms", avgIncremental);
|
||||
|
||||
std::cout << "\n✅ TEST 4 PASSED\n\n";
|
||||
|
||||
// ========================================================================
|
||||
// TEST 5: State Corruption Detection
|
||||
// ========================================================================
|
||||
std::cout << "=== TEST 5: State Corruption Detection ===\n\n";
|
||||
|
||||
auto moduleSystem4 = std::make_unique<SequentialModuleSystem>();
|
||||
|
||||
nlohmann::json configNormal;
|
||||
configNormal["version"] = "v1.0";
|
||||
configNormal["particleCount"] = 1000;
|
||||
configNormal["terrainSize"] = 50;
|
||||
configNormal["initDuration"] = 0.2f;
|
||||
|
||||
auto configNormalNode = std::make_unique<JsonDataNode>("config", configNormal);
|
||||
auto moduleNormal = loader.load(modulePath, "HeavyStateModule", false);
|
||||
moduleNormal->setConfiguration(*configNormalNode, nullptr, nullptr);
|
||||
moduleSystem4->registerModule("HeavyStateModule", std::move(moduleNormal));
|
||||
|
||||
// Exécuter un peu
|
||||
for (int i = 0; i < 60; i++) {
|
||||
moduleSystem4->processModules(1.0f / 60.0f);
|
||||
}
|
||||
|
||||
// Créer un état corrompu
|
||||
std::cout << "Creating corrupted state...\n";
|
||||
nlohmann::json corruptedState;
|
||||
corruptedState["version"] = "v1.0";
|
||||
corruptedState["frameCount"] = "INVALID_STRING"; // Type incorrect
|
||||
corruptedState["config"]["particleCount"] = -500; // Valeur invalide
|
||||
corruptedState["config"]["terrainWidth"] = 100;
|
||||
corruptedState["config"]["terrainHeight"] = 100;
|
||||
corruptedState["particles"]["count"] = 1000;
|
||||
corruptedState["particles"]["data"] = "CORRUPTED";
|
||||
corruptedState["terrain"]["width"] = 50;
|
||||
corruptedState["terrain"]["height"] = 50;
|
||||
corruptedState["terrain"]["compressed"] = true;
|
||||
corruptedState["terrain"]["data"] = "CORRUPTED";
|
||||
corruptedState["history"] = nlohmann::json::array();
|
||||
|
||||
auto corruptedNode = std::make_unique<JsonDataNode>("corrupted", corruptedState);
|
||||
|
||||
bool detectedCorruption = false;
|
||||
std::cout << "Attempting to apply corrupted state...\n";
|
||||
try {
|
||||
auto moduleCorrupt = loader.load(modulePath, "HeavyStateModule", false);
|
||||
moduleCorrupt->setState(*corruptedNode);
|
||||
} catch (const std::exception& e) {
|
||||
std::string msg = e.what();
|
||||
std::cout << " ✓ Corruption detected: " << msg << "\n";
|
||||
detectedCorruption = true;
|
||||
}
|
||||
|
||||
ASSERT_TRUE(detectedCorruption, "Should detect corrupted state");
|
||||
reporter.addAssertion("corruption_detection", detectedCorruption);
|
||||
|
||||
// Vérifier que le module d'origine reste fonctionnel
|
||||
std::cout << "Verifying original module still functional...\n";
|
||||
bool stillFunctional = true;
|
||||
try {
|
||||
for (int i = 0; i < 60; i++) {
|
||||
moduleSystem4->processModules(1.0f / 60.0f);
|
||||
}
|
||||
std::cout << " ✓ Module remains functional\n";
|
||||
} catch (const std::exception& e) {
|
||||
stillFunctional = false;
|
||||
std::cout << " ✗ Module broken: " << e.what() << "\n";
|
||||
}
|
||||
|
||||
ASSERT_TRUE(stillFunctional, "Module should remain functional after rejected corrupted state");
|
||||
reporter.addAssertion("functional_after_corruption", stillFunctional);
|
||||
|
||||
std::cout << "\n✅ TEST 5 PASSED\n\n";
|
||||
|
||||
// ========================================================================
|
||||
// RAPPORT FINAL
|
||||
// ========================================================================
|
||||
|
||||
std::cout << "================================================================================\n";
|
||||
std::cout << "SUMMARY\n";
|
||||
std::cout << "================================================================================\n\n";
|
||||
|
||||
metrics.printReport();
|
||||
reporter.printFinalReport();
|
||||
|
||||
std::cout << "\n================================================================================\n";
|
||||
std::cout << "Result: " << (reporter.getExitCode() == 0 ? "✅ ALL TESTS PASSED" : "❌ SOME TESTS FAILED") << "\n";
|
||||
std::cout << "================================================================================\n";
|
||||
|
||||
return reporter.getExitCode();
|
||||
}
|
||||
#include "grove/ModuleLoader.h"
|
||||
#include "grove/SequentialModuleSystem.h"
|
||||
#include "grove/JsonDataNode.h"
|
||||
#include "../helpers/TestMetrics.h"
|
||||
#include "../helpers/TestAssertions.h"
|
||||
#include "../helpers/TestReporter.h"
|
||||
#include "../helpers/SystemUtils.h"
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <stdexcept>
|
||||
#include <numeric>
|
||||
#include <fstream>
|
||||
|
||||
using namespace grove;
|
||||
|
||||
/**
|
||||
* Test 07: Limite Tests
|
||||
*
|
||||
* Objectif: Valider la robustesse du système face aux conditions extrêmes:
|
||||
* - Large state (100MB+)
|
||||
* - Long initialization
|
||||
* - Timeouts
|
||||
* - Memory pressure
|
||||
* - State corruption detection
|
||||
*/
|
||||
|
||||
int main() {
|
||||
TestReporter reporter("Limite Tests");
|
||||
TestMetrics metrics;
|
||||
|
||||
std::cout << "================================================================================\n";
|
||||
std::cout << "TEST: Limite Tests - Extreme Conditions & Edge Cases\n";
|
||||
std::cout << "================================================================================\n\n";
|
||||
|
||||
// ========================================================================
|
||||
// TEST 1: Large State Serialization
|
||||
// ========================================================================
|
||||
std::cout << "=== TEST 1: Large State Serialization ===\n\n";
|
||||
|
||||
ModuleLoader loader;
|
||||
auto moduleSystem = std::make_unique<SequentialModuleSystem>();
|
||||
|
||||
std::string modulePath = "./libHeavyStateModule.so";
|
||||
#ifdef _WIN32
|
||||
modulePath = "./libHeavyStateModule.dll";
|
||||
#endif
|
||||
auto module = loader.load(modulePath, "HeavyStateModule", false);
|
||||
|
||||
// Config: particules réduites pour test rapide, mais assez pour être significatif
|
||||
nlohmann::json configJson;
|
||||
configJson["version"] = "v1.0";
|
||||
configJson["particleCount"] = 100000; // 100k au lieu de 1M pour test rapide
|
||||
configJson["terrainSize"] = 1000; // 1000x1000 au lieu de 10000x10000
|
||||
configJson["initDuration"] = 2.0f; // 2s au lieu de 8s
|
||||
configJson["initTimeout"] = 5.0f;
|
||||
|
||||
auto config = std::make_unique<JsonDataNode>("config", configJson);
|
||||
|
||||
// Mesurer temps d'initialisation
|
||||
std::cout << "Initializing module...\n";
|
||||
auto initStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
module->setConfiguration(*config, nullptr, nullptr);
|
||||
|
||||
auto initEnd = std::chrono::high_resolution_clock::now();
|
||||
float initTime = std::chrono::duration<float>(initEnd - initStart).count();
|
||||
|
||||
std::cout << " Init time: " << initTime << "s\n";
|
||||
ASSERT_GT(initTime, 1.5f, "Init should take at least 1.5s (simulated heavy init)");
|
||||
ASSERT_LT(initTime, 3.0f, "Init should not exceed 3s");
|
||||
reporter.addMetric("init_time_s", initTime);
|
||||
|
||||
moduleSystem->registerModule("HeavyStateModule", std::move(module));
|
||||
|
||||
// Exécuter quelques frames
|
||||
std::cout << "Running 300 frames...\n";
|
||||
for (int i = 0; i < 300; i++) {
|
||||
moduleSystem->processModules(1.0f / 60.0f);
|
||||
}
|
||||
|
||||
// Mesurer temps de getState()
|
||||
std::cout << "Extracting state (getState)...\n";
|
||||
auto getStateStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
auto heavyModule = moduleSystem->extractModule();
|
||||
auto state = heavyModule->getState();
|
||||
|
||||
auto getStateEnd = std::chrono::high_resolution_clock::now();
|
||||
float getStateTime = std::chrono::duration<float, std::milli>(getStateEnd - getStateStart).count();
|
||||
|
||||
std::cout << " getState time: " << getStateTime << "ms\n";
|
||||
ASSERT_LT(getStateTime, 2000.0f, "getState() should be < 2000ms");
|
||||
reporter.addMetric("getstate_time_ms", getStateTime);
|
||||
|
||||
// Estimer taille de l'état
|
||||
auto* jsonNode = dynamic_cast<JsonDataNode*>(state.get());
|
||||
if (jsonNode) {
|
||||
std::string stateStr = jsonNode->getJsonData().dump();
|
||||
size_t stateSize = stateStr.size();
|
||||
float stateSizeMB = stateSize / 1024.0f / 1024.0f;
|
||||
std::cout << " State size: " << stateSizeMB << " MB\n";
|
||||
reporter.addMetric("state_size_mb", stateSizeMB);
|
||||
}
|
||||
|
||||
// Recharger le module (simuler hot-reload)
|
||||
std::cout << "Reloading module...\n";
|
||||
auto reloadStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
// setState() est appelé automatiquement par reload()
|
||||
auto moduleReloaded = loader.reload(std::move(heavyModule));
|
||||
|
||||
auto reloadEnd = std::chrono::high_resolution_clock::now();
|
||||
float reloadTime = std::chrono::duration<float, std::milli>(reloadEnd - reloadStart).count();
|
||||
std::cout << " Total reload time: " << reloadTime << "ms\n";
|
||||
reporter.addMetric("reload_time_ms", reloadTime);
|
||||
|
||||
// Ré-enregistrer
|
||||
moduleSystem->registerModule("HeavyStateModule", std::move(moduleReloaded));
|
||||
|
||||
// Vérifier intégrité après reload
|
||||
auto heavyModuleAfter = moduleSystem->extractModule();
|
||||
auto stateAfter = heavyModuleAfter->getState();
|
||||
auto* jsonNodeAfter = dynamic_cast<JsonDataNode*>(stateAfter.get());
|
||||
if (jsonNodeAfter) {
|
||||
const auto& dataAfter = jsonNodeAfter->getJsonData();
|
||||
int particleCount = dataAfter["config"]["particleCount"];
|
||||
ASSERT_EQ(particleCount, 100000, "Should have 100k particles after reload");
|
||||
reporter.addAssertion("particles_preserved", particleCount == 100000);
|
||||
std::cout << " ✓ Particles preserved: " << particleCount << "\n";
|
||||
}
|
||||
|
||||
// Ré-enregistrer pour continuer
|
||||
moduleSystem->registerModule("HeavyStateModule", std::move(heavyModuleAfter));
|
||||
|
||||
// Continuer exécution
|
||||
std::cout << "Running 300 more frames post-reload...\n";
|
||||
for (int i = 0; i < 300; i++) {
|
||||
moduleSystem->processModules(1.0f / 60.0f);
|
||||
}
|
||||
|
||||
std::cout << "\n✅ TEST 1 PASSED\n\n";
|
||||
|
||||
// ========================================================================
|
||||
// TEST 2: Long Initialization Timeout
|
||||
// ========================================================================
|
||||
std::cout << "=== TEST 2: Long Initialization Timeout ===\n\n";
|
||||
|
||||
auto moduleSystem2 = std::make_unique<SequentialModuleSystem>();
|
||||
auto moduleTimeout = loader.load(modulePath, "HeavyStateModule", false);
|
||||
|
||||
// Config avec init long + timeout court (va échouer)
|
||||
nlohmann::json configTimeout;
|
||||
configTimeout["version"] = "v1.0";
|
||||
configTimeout["particleCount"] = 100000;
|
||||
configTimeout["terrainSize"] = 1000;
|
||||
configTimeout["initDuration"] = 4.0f; // Init va prendre 4s
|
||||
configTimeout["initTimeout"] = 3.0f; // Timeout à 3s (trop court)
|
||||
|
||||
auto configTimeoutNode = std::make_unique<JsonDataNode>("config", configTimeout);
|
||||
|
||||
bool timedOut = false;
|
||||
std::cout << "Attempting init with timeout=3s (duration=4s)...\n";
|
||||
try {
|
||||
moduleTimeout->setConfiguration(*configTimeoutNode, nullptr, nullptr);
|
||||
} catch (const std::exception& e) {
|
||||
std::string msg = e.what();
|
||||
if (msg.find("timeout") != std::string::npos ||
|
||||
msg.find("Timeout") != std::string::npos ||
|
||||
msg.find("exceeded") != std::string::npos) {
|
||||
timedOut = true;
|
||||
std::cout << " ✓ Timeout detected: " << msg << "\n";
|
||||
} else {
|
||||
std::cout << " ✗ Unexpected error: " << msg << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
ASSERT_TRUE(timedOut, "Should timeout when init > timeout threshold");
|
||||
reporter.addAssertion("timeout_detection", timedOut);
|
||||
|
||||
// Réessayer avec timeout suffisant
|
||||
std::cout << "\nRetrying with adequate timeout=6s...\n";
|
||||
auto moduleTimeout2 = loader.load(modulePath, "HeavyStateModule", false);
|
||||
|
||||
nlohmann::json configOk;
|
||||
configOk["version"] = "v1.0";
|
||||
configOk["particleCount"] = 50000;
|
||||
configOk["terrainSize"] = 500;
|
||||
configOk["initDuration"] = 2.0f;
|
||||
configOk["initTimeout"] = 5.0f;
|
||||
|
||||
auto configOkNode = std::make_unique<JsonDataNode>("config", configOk);
|
||||
|
||||
bool success = true;
|
||||
try {
|
||||
moduleTimeout2->setConfiguration(*configOkNode, nullptr, nullptr);
|
||||
moduleSystem2->registerModule("HeavyStateModule", std::move(moduleTimeout2));
|
||||
moduleSystem2->processModules(1.0f / 60.0f);
|
||||
std::cout << " ✓ Init succeeded with adequate timeout\n";
|
||||
} catch (const std::exception& e) {
|
||||
success = false;
|
||||
std::cout << " ✗ Failed: " << e.what() << "\n";
|
||||
}
|
||||
|
||||
ASSERT_TRUE(success, "Should succeed with adequate timeout");
|
||||
reporter.addAssertion("timeout_recovery", success);
|
||||
|
||||
std::cout << "\n✅ TEST 2 PASSED\n\n";
|
||||
|
||||
// ========================================================================
|
||||
// TEST 3: Memory Pressure During Reload
|
||||
// ========================================================================
|
||||
std::cout << "=== TEST 3: Memory Pressure During Reload ===\n\n";
|
||||
|
||||
// Créer un nouveau system pour ce test
|
||||
auto moduleSystem3Pressure = std::make_unique<SequentialModuleSystem>();
|
||||
auto modulePressureInit = loader.load(modulePath, "HeavyStateModule", false);
|
||||
|
||||
nlohmann::json configPressure;
|
||||
configPressure["version"] = "v1.0";
|
||||
configPressure["particleCount"] = 10000;
|
||||
configPressure["terrainSize"] = 100;
|
||||
configPressure["initDuration"] = 0.5f;
|
||||
auto configPressureNode = std::make_unique<JsonDataNode>("config", configPressure);
|
||||
modulePressureInit->setConfiguration(*configPressureNode, nullptr, nullptr);
|
||||
moduleSystem3Pressure->registerModule("HeavyStateModule", std::move(modulePressureInit));
|
||||
|
||||
size_t memBefore = getCurrentMemoryUsage();
|
||||
std::cout << "Memory before: " << (memBefore / 1024.0f / 1024.0f) << " MB\n";
|
||||
|
||||
// Exécuter quelques frames
|
||||
std::cout << "Running 300 frames...\n";
|
||||
for (int i = 0; i < 300; i++) {
|
||||
moduleSystem3Pressure->processModules(1.0f / 60.0f);
|
||||
}
|
||||
|
||||
// Allouer temporairement beaucoup de mémoire
|
||||
std::cout << "Allocating temporary 50MB during reload...\n";
|
||||
std::vector<uint8_t> tempAlloc;
|
||||
|
||||
auto reloadPressureStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
// Allouer 50MB
|
||||
tempAlloc.resize(50 * 1024 * 1024);
|
||||
std::fill(tempAlloc.begin(), tempAlloc.end(), 0x42);
|
||||
|
||||
size_t memDuringAlloc = getCurrentMemoryUsage();
|
||||
std::cout << " Memory with allocation: " << (memDuringAlloc / 1024.0f / 1024.0f) << " MB\n";
|
||||
|
||||
// Reload pendant la pression mémoire
|
||||
auto modulePressure = moduleSystem3Pressure->extractModule();
|
||||
auto modulePressureReloaded = loader.reload(std::move(modulePressure));
|
||||
moduleSystem3Pressure->registerModule("HeavyStateModule", std::move(modulePressureReloaded));
|
||||
|
||||
auto reloadPressureEnd = std::chrono::high_resolution_clock::now();
|
||||
float reloadPressureTime = std::chrono::duration<float, std::milli>(
|
||||
reloadPressureEnd - reloadPressureStart).count();
|
||||
|
||||
std::cout << " Reload under pressure: " << reloadPressureTime << "ms\n";
|
||||
reporter.addMetric("reload_under_pressure_ms", reloadPressureTime);
|
||||
|
||||
// Libérer allocation temporaire
|
||||
tempAlloc.clear();
|
||||
tempAlloc.shrink_to_fit();
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
|
||||
size_t memAfter = getCurrentMemoryUsage();
|
||||
std::cout << " Memory after cleanup: " << (memAfter / 1024.0f / 1024.0f) << " MB\n";
|
||||
|
||||
long memGrowth = static_cast<long>(memAfter) - static_cast<long>(memBefore);
|
||||
float memGrowthMB = memGrowth / 1024.0f / 1024.0f;
|
||||
std::cout << " Net memory growth: " << memGrowthMB << " MB\n";
|
||||
|
||||
// Tolérance: max 10MB de croissance
|
||||
ASSERT_LT(std::abs(memGrowth), 10 * 1024 * 1024, "Memory growth should be < 10MB");
|
||||
reporter.addMetric("memory_growth_mb", memGrowthMB);
|
||||
|
||||
std::cout << "\n✅ TEST 3 PASSED\n\n";
|
||||
|
||||
// ========================================================================
|
||||
// TEST 4: Incremental Reloads
|
||||
// ========================================================================
|
||||
std::cout << "=== TEST 4: Incremental Reloads ===\n\n";
|
||||
|
||||
auto moduleSystem3 = std::make_unique<SequentialModuleSystem>();
|
||||
|
||||
nlohmann::json configIncremental;
|
||||
configIncremental["version"] = "v1.0";
|
||||
configIncremental["particleCount"] = 10000; // Petit pour test rapide
|
||||
configIncremental["terrainSize"] = 100;
|
||||
configIncremental["initDuration"] = 0.5f;
|
||||
configIncremental["incrementalState"] = true;
|
||||
|
||||
auto configIncrNode = std::make_unique<JsonDataNode>("config", configIncremental);
|
||||
auto moduleIncr = loader.load(modulePath, "HeavyStateModule", false);
|
||||
moduleIncr->setConfiguration(*configIncrNode, nullptr, nullptr);
|
||||
moduleSystem3->registerModule("HeavyStateModule", std::move(moduleIncr));
|
||||
|
||||
std::vector<float> incrementalTimes;
|
||||
|
||||
std::cout << "Performing 5 incremental reloads...\n";
|
||||
for (int reload = 0; reload < 5; reload++) {
|
||||
// Exécuter 60 frames
|
||||
for (int i = 0; i < 60; i++) {
|
||||
moduleSystem3->processModules(1.0f / 60.0f);
|
||||
}
|
||||
|
||||
// Reload
|
||||
auto incStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
auto moduleInc = moduleSystem3->extractModule();
|
||||
auto moduleIncReloaded = loader.reload(std::move(moduleInc));
|
||||
moduleSystem3->registerModule("HeavyStateModule", std::move(moduleIncReloaded));
|
||||
|
||||
auto incEnd = std::chrono::high_resolution_clock::now();
|
||||
float incTime = std::chrono::duration<float, std::milli>(incEnd - incStart).count();
|
||||
|
||||
incrementalTimes.push_back(incTime);
|
||||
std::cout << " Reload #" << reload << ": " << incTime << "ms\n";
|
||||
}
|
||||
|
||||
float avgIncremental = std::accumulate(incrementalTimes.begin(), incrementalTimes.end(), 0.0f)
|
||||
/ incrementalTimes.size();
|
||||
std::cout << "\nAverage incremental reload: " << avgIncremental << "ms\n";
|
||||
|
||||
ASSERT_LT(avgIncremental, 2000.0f, "Incremental reloads should be reasonably fast");
|
||||
reporter.addMetric("avg_incremental_reload_ms", avgIncremental);
|
||||
|
||||
std::cout << "\n✅ TEST 4 PASSED\n\n";
|
||||
|
||||
// ========================================================================
|
||||
// TEST 5: State Corruption Detection
|
||||
// ========================================================================
|
||||
std::cout << "=== TEST 5: State Corruption Detection ===\n\n";
|
||||
|
||||
auto moduleSystem4 = std::make_unique<SequentialModuleSystem>();
|
||||
|
||||
nlohmann::json configNormal;
|
||||
configNormal["version"] = "v1.0";
|
||||
configNormal["particleCount"] = 1000;
|
||||
configNormal["terrainSize"] = 50;
|
||||
configNormal["initDuration"] = 0.2f;
|
||||
|
||||
auto configNormalNode = std::make_unique<JsonDataNode>("config", configNormal);
|
||||
auto moduleNormal = loader.load(modulePath, "HeavyStateModule", false);
|
||||
moduleNormal->setConfiguration(*configNormalNode, nullptr, nullptr);
|
||||
moduleSystem4->registerModule("HeavyStateModule", std::move(moduleNormal));
|
||||
|
||||
// Exécuter un peu
|
||||
for (int i = 0; i < 60; i++) {
|
||||
moduleSystem4->processModules(1.0f / 60.0f);
|
||||
}
|
||||
|
||||
// Créer un état corrompu
|
||||
std::cout << "Creating corrupted state...\n";
|
||||
nlohmann::json corruptedState;
|
||||
corruptedState["version"] = "v1.0";
|
||||
corruptedState["frameCount"] = "INVALID_STRING"; // Type incorrect
|
||||
corruptedState["config"]["particleCount"] = -500; // Valeur invalide
|
||||
corruptedState["config"]["terrainWidth"] = 100;
|
||||
corruptedState["config"]["terrainHeight"] = 100;
|
||||
corruptedState["particles"]["count"] = 1000;
|
||||
corruptedState["particles"]["data"] = "CORRUPTED";
|
||||
corruptedState["terrain"]["width"] = 50;
|
||||
corruptedState["terrain"]["height"] = 50;
|
||||
corruptedState["terrain"]["compressed"] = true;
|
||||
corruptedState["terrain"]["data"] = "CORRUPTED";
|
||||
corruptedState["history"] = nlohmann::json::array();
|
||||
|
||||
auto corruptedNode = std::make_unique<JsonDataNode>("corrupted", corruptedState);
|
||||
|
||||
bool detectedCorruption = false;
|
||||
std::cout << "Attempting to apply corrupted state...\n";
|
||||
try {
|
||||
auto moduleCorrupt = loader.load(modulePath, "HeavyStateModule", false);
|
||||
moduleCorrupt->setState(*corruptedNode);
|
||||
} catch (const std::exception& e) {
|
||||
std::string msg = e.what();
|
||||
std::cout << " ✓ Corruption detected: " << msg << "\n";
|
||||
detectedCorruption = true;
|
||||
}
|
||||
|
||||
ASSERT_TRUE(detectedCorruption, "Should detect corrupted state");
|
||||
reporter.addAssertion("corruption_detection", detectedCorruption);
|
||||
|
||||
// Vérifier que le module d'origine reste fonctionnel
|
||||
std::cout << "Verifying original module still functional...\n";
|
||||
bool stillFunctional = true;
|
||||
try {
|
||||
for (int i = 0; i < 60; i++) {
|
||||
moduleSystem4->processModules(1.0f / 60.0f);
|
||||
}
|
||||
std::cout << " ✓ Module remains functional\n";
|
||||
} catch (const std::exception& e) {
|
||||
stillFunctional = false;
|
||||
std::cout << " ✗ Module broken: " << e.what() << "\n";
|
||||
}
|
||||
|
||||
ASSERT_TRUE(stillFunctional, "Module should remain functional after rejected corrupted state");
|
||||
reporter.addAssertion("functional_after_corruption", stillFunctional);
|
||||
|
||||
std::cout << "\n✅ TEST 5 PASSED\n\n";
|
||||
|
||||
// ========================================================================
|
||||
// RAPPORT FINAL
|
||||
// ========================================================================
|
||||
|
||||
std::cout << "================================================================================\n";
|
||||
std::cout << "SUMMARY\n";
|
||||
std::cout << "================================================================================\n\n";
|
||||
|
||||
metrics.printReport();
|
||||
reporter.printFinalReport();
|
||||
|
||||
std::cout << "\n================================================================================\n";
|
||||
std::cout << "Result: " << (reporter.getExitCode() == 0 ? "✅ ALL TESTS PASSED" : "❌ SOME TESTS FAILED") << "\n";
|
||||
std::cout << "================================================================================\n";
|
||||
|
||||
return reporter.getExitCode();
|
||||
}
|
||||
|
||||
@ -1,349 +1,352 @@
|
||||
#include "grove/ModuleLoader.h"
|
||||
#include "grove/SequentialModuleSystem.h"
|
||||
#include "grove/JsonDataNode.h"
|
||||
#include "../helpers/TestMetrics.h"
|
||||
#include "../helpers/TestAssertions.h"
|
||||
#include "../helpers/TestReporter.h"
|
||||
#include "../helpers/SystemUtils.h"
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <set>
|
||||
|
||||
using namespace grove;
|
||||
|
||||
/**
|
||||
* Test 08: Config Hot-Reload
|
||||
*
|
||||
* Objectif: Valider que le système peut modifier la configuration d'un module
|
||||
* à la volée sans redémarrage, avec validation et rollback.
|
||||
*
|
||||
* Scénario:
|
||||
* Phase 0: Baseline avec config initiale (10s)
|
||||
* Phase 1: Doubler spawn rate et speed (10s)
|
||||
* Phase 2: Changements complexes (couleurs, physique, limites) (10s)
|
||||
* Phase 3: Config invalide + rollback (5s)
|
||||
* Phase 4: Partial config update (5s)
|
||||
*
|
||||
* Métriques:
|
||||
* - Config update time
|
||||
* - Config validation
|
||||
* - Rollback functionality
|
||||
* - Partial merge accuracy
|
||||
*/
|
||||
|
||||
int main() {
|
||||
TestReporter reporter("Config Hot-Reload");
|
||||
TestMetrics metrics;
|
||||
|
||||
std::cout << "================================================================================\n";
|
||||
std::cout << "TEST: Config Hot-Reload\n";
|
||||
std::cout << "================================================================================\n\n";
|
||||
|
||||
// === SETUP ===
|
||||
std::cout << "Setup: Loading ConfigurableModule with initial config...\n";
|
||||
|
||||
ModuleLoader loader;
|
||||
auto moduleSystem = std::make_unique<SequentialModuleSystem>();
|
||||
|
||||
// Charger module
|
||||
std::string modulePath = "./libConfigurableModule.so";
|
||||
auto module = loader.load(modulePath, "ConfigurableModule", false);
|
||||
|
||||
// Config initiale
|
||||
nlohmann::json configJson;
|
||||
configJson["spawnRate"] = 10;
|
||||
configJson["maxEntities"] = 150; // Higher limit for Phase 0
|
||||
configJson["entitySpeed"] = 5.0;
|
||||
configJson["colors"] = nlohmann::json::array({"red", "blue"});
|
||||
configJson["physics"]["gravity"] = 9.8;
|
||||
configJson["physics"]["friction"] = 0.5;
|
||||
auto config = std::make_unique<JsonDataNode>("config", configJson);
|
||||
|
||||
module->setConfiguration(*config, nullptr, nullptr);
|
||||
moduleSystem->registerModule("ConfigurableModule", std::move(module));
|
||||
|
||||
std::cout << " Initial config:\n";
|
||||
std::cout << " Spawn rate: 10/s\n";
|
||||
std::cout << " Max entities: 150\n";
|
||||
std::cout << " Entity speed: 5.0\n";
|
||||
std::cout << " Colors: [red, blue]\n\n";
|
||||
|
||||
// === PHASE 0: Baseline (10s) ===
|
||||
std::cout << "=== Phase 0: Initial config (10s) ===\n";
|
||||
|
||||
for (int i = 0; i < 600; i++) { // 10s * 60 FPS
|
||||
auto frameStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
moduleSystem->processModules(1.0f / 60.0f);
|
||||
|
||||
auto frameEnd = std::chrono::high_resolution_clock::now();
|
||||
float frameTime = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count();
|
||||
metrics.recordFPS(1000.0f / frameTime);
|
||||
metrics.recordMemoryUsage(grove::getCurrentMemoryUsage());
|
||||
}
|
||||
|
||||
auto state0 = moduleSystem->extractModule()->getState();
|
||||
auto* json0 = dynamic_cast<JsonDataNode*>(state0.get());
|
||||
ASSERT_TRUE(json0 != nullptr, "State should be JsonDataNode");
|
||||
|
||||
const auto& state0Data = json0->getJsonData();
|
||||
int entityCount0 = state0Data["entities"].size();
|
||||
|
||||
std::cout << "✓ Baseline: " << entityCount0 << " entities spawned (~100 expected)\n";
|
||||
ASSERT_WITHIN(entityCount0, 100, 20, "Should have ~100 entities after 10s");
|
||||
reporter.addAssertion("initial_spawn_rate", true);
|
||||
|
||||
// Vérifier vitesse des entités initiales
|
||||
for (const auto& entity : state0Data["entities"]) {
|
||||
float speed = entity["speed"];
|
||||
ASSERT_EQ_FLOAT(speed, 5.0f, 0.01f, "Initial entity speed should be 5.0");
|
||||
}
|
||||
|
||||
// Re-register module
|
||||
auto module0 = loader.load(modulePath, "ConfigurableModule", false);
|
||||
module0->setState(*state0);
|
||||
moduleSystem->registerModule("ConfigurableModule", std::move(module0));
|
||||
|
||||
// === PHASE 1: Simple Config Change (10s) ===
|
||||
std::cout << "\n=== Phase 1: Doubling spawn rate and speed (10s) ===\n";
|
||||
|
||||
nlohmann::json newConfig1;
|
||||
newConfig1["spawnRate"] = 20; // Double spawn rate
|
||||
newConfig1["maxEntities"] = 150; // Keep same limit
|
||||
newConfig1["entitySpeed"] = 10.0; // Double speed
|
||||
newConfig1["colors"] = nlohmann::json::array({"red", "blue"});
|
||||
newConfig1["physics"]["gravity"] = 9.8;
|
||||
newConfig1["physics"]["friction"] = 0.5;
|
||||
auto newConfigNode1 = std::make_unique<JsonDataNode>("config", newConfig1);
|
||||
|
||||
auto updateStart1 = std::chrono::high_resolution_clock::now();
|
||||
|
||||
// Extract, update config, re-register
|
||||
auto modulePhase1 = moduleSystem->extractModule();
|
||||
bool updateResult1 = modulePhase1->updateConfig(*newConfigNode1);
|
||||
|
||||
auto updateEnd1 = std::chrono::high_resolution_clock::now();
|
||||
float updateTime1 = std::chrono::duration<float, std::milli>(updateEnd1 - updateStart1).count();
|
||||
|
||||
ASSERT_TRUE(updateResult1, "Config update should succeed");
|
||||
reporter.addAssertion("config_update_simple", updateResult1);
|
||||
reporter.addMetric("config_update_time_ms", updateTime1);
|
||||
|
||||
std::cout << " Config updated in " << updateTime1 << "ms\n";
|
||||
|
||||
moduleSystem->registerModule("ConfigurableModule", std::move(modulePhase1));
|
||||
|
||||
// Run 10s
|
||||
for (int i = 0; i < 600; i++) {
|
||||
moduleSystem->processModules(1.0f / 60.0f);
|
||||
metrics.recordMemoryUsage(grove::getCurrentMemoryUsage());
|
||||
}
|
||||
|
||||
auto state1 = moduleSystem->extractModule()->getState();
|
||||
auto* json1 = dynamic_cast<JsonDataNode*>(state1.get());
|
||||
const auto& state1Data = json1->getJsonData();
|
||||
int entityCount1 = state1Data["entities"].size();
|
||||
|
||||
// Should have reached limit (150)
|
||||
std::cout << "✓ Phase 1: " << entityCount1 << " entities (max: 150)\n";
|
||||
ASSERT_LE(entityCount1, 150, "Should respect maxEntities limit");
|
||||
reporter.addAssertion("max_entities_respected", entityCount1 <= 150);
|
||||
|
||||
// Vérifier que nouvelles entités ont speed = 10.0
|
||||
int newEntityCount = 0;
|
||||
for (const auto& entity : state1Data["entities"]) {
|
||||
if (entity["id"] >= entityCount0) { // Nouvelle entité
|
||||
float speed = entity["speed"];
|
||||
ASSERT_EQ_FLOAT(speed, 10.0f, 0.01f, "New entities should have speed 10.0");
|
||||
newEntityCount++;
|
||||
}
|
||||
}
|
||||
std::cout << " " << newEntityCount << " new entities with speed 10.0\n";
|
||||
|
||||
// Re-register
|
||||
auto module1 = loader.load(modulePath, "ConfigurableModule", false);
|
||||
module1->setState(*state1);
|
||||
moduleSystem->registerModule("ConfigurableModule", std::move(module1));
|
||||
|
||||
// === PHASE 2: Complex Config Change (10s) ===
|
||||
std::cout << "\n=== Phase 2: Complex config changes (10s) ===\n";
|
||||
|
||||
nlohmann::json newConfig2;
|
||||
newConfig2["spawnRate"] = 15;
|
||||
newConfig2["maxEntities"] = 200; // Augmenter limite (was 150)
|
||||
newConfig2["entitySpeed"] = 7.5;
|
||||
newConfig2["colors"] = nlohmann::json::array({"green", "yellow", "purple"}); // Nouvelles couleurs
|
||||
newConfig2["physics"]["gravity"] = 1.6; // Gravité lunaire
|
||||
newConfig2["physics"]["friction"] = 0.2;
|
||||
auto newConfigNode2 = std::make_unique<JsonDataNode>("config", newConfig2);
|
||||
|
||||
auto modulePhase2 = moduleSystem->extractModule();
|
||||
bool updateResult2 = modulePhase2->updateConfig(*newConfigNode2);
|
||||
ASSERT_TRUE(updateResult2, "Config update 2 should succeed");
|
||||
|
||||
int entitiesBeforePhase2 = entityCount1;
|
||||
std::cout << " Config updated: new colors [green, yellow, purple]\n";
|
||||
std::cout << " Max entities increased to 200\n";
|
||||
|
||||
moduleSystem->registerModule("ConfigurableModule", std::move(modulePhase2));
|
||||
|
||||
// Run 10s
|
||||
for (int i = 0; i < 600; i++) {
|
||||
moduleSystem->processModules(1.0f / 60.0f);
|
||||
}
|
||||
|
||||
auto state2 = moduleSystem->extractModule()->getState();
|
||||
auto* json2 = dynamic_cast<JsonDataNode*>(state2.get());
|
||||
const auto& state2Data = json2->getJsonData();
|
||||
int entityCount2 = state2Data["entities"].size();
|
||||
|
||||
std::cout << "✓ Phase 2: " << entityCount2 << " total entities\n";
|
||||
ASSERT_GT(entityCount2, entitiesBeforePhase2, "Entity count should have increased");
|
||||
ASSERT_LE(entityCount2, 200, "Should respect new maxEntities = 200");
|
||||
|
||||
// Vérifier couleurs des nouvelles entités
|
||||
std::set<std::string> newColors;
|
||||
for (const auto& entity : state2Data["entities"]) {
|
||||
if (entity["id"] >= entitiesBeforePhase2) {
|
||||
newColors.insert(entity["color"]);
|
||||
}
|
||||
}
|
||||
|
||||
bool hasNewColors = newColors.count("green") || newColors.count("yellow") || newColors.count("purple");
|
||||
ASSERT_TRUE(hasNewColors, "New entities should use new color palette");
|
||||
reporter.addAssertion("new_colors_applied", hasNewColors);
|
||||
|
||||
std::cout << " New colors found: ";
|
||||
for (const auto& color : newColors) std::cout << color << " ";
|
||||
std::cout << "\n";
|
||||
|
||||
// Re-register
|
||||
auto module2 = loader.load(modulePath, "ConfigurableModule", false);
|
||||
module2->setState(*state2);
|
||||
moduleSystem->registerModule("ConfigurableModule", std::move(module2));
|
||||
|
||||
// === PHASE 3: Invalid Config + Rollback (5s) ===
|
||||
std::cout << "\n=== Phase 3: Invalid config rejection (5s) ===\n";
|
||||
|
||||
nlohmann::json invalidConfig;
|
||||
invalidConfig["spawnRate"] = -5; // INVALIDE: négatif
|
||||
invalidConfig["maxEntities"] = 1000000; // INVALIDE: trop grand
|
||||
invalidConfig["entitySpeed"] = 5.0;
|
||||
invalidConfig["colors"] = nlohmann::json::array({"red"});
|
||||
invalidConfig["physics"]["gravity"] = 9.8;
|
||||
invalidConfig["physics"]["friction"] = 0.5;
|
||||
auto invalidConfigNode = std::make_unique<JsonDataNode>("config", invalidConfig);
|
||||
|
||||
auto modulePhase3 = moduleSystem->extractModule();
|
||||
bool updateResult3 = modulePhase3->updateConfig(*invalidConfigNode);
|
||||
|
||||
std::cout << " Invalid config rejected: " << (!updateResult3 ? "YES" : "NO") << "\n";
|
||||
ASSERT_FALSE(updateResult3, "Invalid config should be rejected");
|
||||
reporter.addAssertion("invalid_config_rejected", !updateResult3);
|
||||
|
||||
moduleSystem->registerModule("ConfigurableModule", std::move(modulePhase3));
|
||||
|
||||
// Continuer - devrait utiliser la config précédente (valide)
|
||||
for (int i = 0; i < 300; i++) { // 5s
|
||||
moduleSystem->processModules(1.0f / 60.0f);
|
||||
}
|
||||
|
||||
auto state3 = moduleSystem->extractModule()->getState();
|
||||
auto* json3 = dynamic_cast<JsonDataNode*>(state3.get());
|
||||
const auto& state3Data = json3->getJsonData();
|
||||
int entityCount3 = state3Data["entities"].size();
|
||||
|
||||
std::cout << "✓ Rollback successful: " << (entityCount3 - entityCount2) << " entities spawned with previous config\n";
|
||||
// Note: We might already be at maxEntities (200), so we just verify no crash and config stayed valid
|
||||
ASSERT_GE(entityCount3, entityCount2, "Entity count should not decrease");
|
||||
reporter.addAssertion("config_rollback_works", entityCount3 >= entityCount2);
|
||||
|
||||
// Re-register
|
||||
auto module3 = loader.load(modulePath, "ConfigurableModule", false);
|
||||
module3->setState(*state3);
|
||||
moduleSystem->registerModule("ConfigurableModule", std::move(module3));
|
||||
|
||||
// === PHASE 4: Partial Config Update (5s) ===
|
||||
std::cout << "\n=== Phase 4: Partial config update (5s) ===\n";
|
||||
|
||||
nlohmann::json partialConfig;
|
||||
partialConfig["entitySpeed"] = 2.0; // Modifier seulement la vitesse
|
||||
auto partialConfigNode = std::make_unique<JsonDataNode>("config", partialConfig);
|
||||
|
||||
auto modulePhase4 = moduleSystem->extractModule();
|
||||
bool updateResult4 = modulePhase4->updateConfigPartial(*partialConfigNode);
|
||||
|
||||
std::cout << " Partial update (entitySpeed only): " << (updateResult4 ? "SUCCESS" : "FAILED") << "\n";
|
||||
ASSERT_TRUE(updateResult4, "Partial config update should succeed");
|
||||
|
||||
moduleSystem->registerModule("ConfigurableModule", std::move(modulePhase4));
|
||||
|
||||
// Run 5s
|
||||
for (int i = 0; i < 300; i++) {
|
||||
moduleSystem->processModules(1.0f / 60.0f);
|
||||
}
|
||||
|
||||
auto state4 = moduleSystem->extractModule()->getState();
|
||||
auto* json4 = dynamic_cast<JsonDataNode*>(state4.get());
|
||||
const auto& state4Data = json4->getJsonData();
|
||||
|
||||
// Vérifier que nouvelles entités ont speed = 2.0
|
||||
// Et que colors sont toujours ceux de Phase 2
|
||||
// Note: We might be at maxEntities, so check if any new entities were spawned
|
||||
bool foundNewSpeed = false;
|
||||
bool foundOldColors = false;
|
||||
int newEntitiesPhase4 = 0;
|
||||
|
||||
for (const auto& entity : state4Data["entities"]) {
|
||||
if (entity["id"] >= entityCount3) {
|
||||
newEntitiesPhase4++;
|
||||
float speed = entity["speed"];
|
||||
if (std::abs(speed - 2.0f) < 0.01f) foundNewSpeed = true;
|
||||
|
||||
std::string color = entity["color"];
|
||||
if (color == "green" || color == "yellow" || color == "purple") {
|
||||
foundOldColors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "✓ Partial update: speed changed to 2.0, other params preserved\n";
|
||||
std::cout << " New entities in Phase 4: " << newEntitiesPhase4 << " (may be 0 if at maxEntities)\n";
|
||||
|
||||
// If we spawned new entities, verify they have the new speed
|
||||
// Otherwise, just verify the partial update succeeded (which it did above)
|
||||
if (newEntitiesPhase4 > 0) {
|
||||
ASSERT_TRUE(foundNewSpeed, "New entities should have updated speed");
|
||||
ASSERT_TRUE(foundOldColors, "Colors should be preserved from Phase 2");
|
||||
reporter.addAssertion("partial_update_works", foundNewSpeed && foundOldColors);
|
||||
} else {
|
||||
// At maxEntities, just verify no crash and config updated
|
||||
std::cout << " (At maxEntities, cannot verify new entity speed)\n";
|
||||
reporter.addAssertion("partial_update_works", true);
|
||||
}
|
||||
|
||||
// === VÉRIFICATIONS FINALES ===
|
||||
std::cout << "\n================================================================================\n";
|
||||
std::cout << "FINAL VERIFICATION\n";
|
||||
std::cout << "================================================================================\n";
|
||||
|
||||
// Memory stability
|
||||
size_t memGrowth = metrics.getMemoryGrowth();
|
||||
float memGrowthMB = memGrowth / (1024.0f * 1024.0f);
|
||||
|
||||
std::cout << "Memory growth: " << memGrowthMB << " MB (threshold: < 10 MB)\n";
|
||||
ASSERT_LT(memGrowth, 10 * 1024 * 1024, "Memory growth should be < 10MB");
|
||||
reporter.addMetric("memory_growth_mb", memGrowthMB);
|
||||
|
||||
// No crashes
|
||||
reporter.addAssertion("no_crashes", true);
|
||||
|
||||
std::cout << "\n";
|
||||
|
||||
// === RAPPORT FINAL ===
|
||||
metrics.printReport();
|
||||
reporter.printFinalReport();
|
||||
|
||||
return reporter.getExitCode();
|
||||
}
|
||||
#include "grove/ModuleLoader.h"
|
||||
#include "grove/SequentialModuleSystem.h"
|
||||
#include "grove/JsonDataNode.h"
|
||||
#include "../helpers/TestMetrics.h"
|
||||
#include "../helpers/TestAssertions.h"
|
||||
#include "../helpers/TestReporter.h"
|
||||
#include "../helpers/SystemUtils.h"
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <set>
|
||||
|
||||
using namespace grove;
|
||||
|
||||
/**
|
||||
* Test 08: Config Hot-Reload
|
||||
*
|
||||
* Objectif: Valider que le système peut modifier la configuration d'un module
|
||||
* à la volée sans redémarrage, avec validation et rollback.
|
||||
*
|
||||
* Scénario:
|
||||
* Phase 0: Baseline avec config initiale (10s)
|
||||
* Phase 1: Doubler spawn rate et speed (10s)
|
||||
* Phase 2: Changements complexes (couleurs, physique, limites) (10s)
|
||||
* Phase 3: Config invalide + rollback (5s)
|
||||
* Phase 4: Partial config update (5s)
|
||||
*
|
||||
* Métriques:
|
||||
* - Config update time
|
||||
* - Config validation
|
||||
* - Rollback functionality
|
||||
* - Partial merge accuracy
|
||||
*/
|
||||
|
||||
int main() {
|
||||
TestReporter reporter("Config Hot-Reload");
|
||||
TestMetrics metrics;
|
||||
|
||||
std::cout << "================================================================================\n";
|
||||
std::cout << "TEST: Config Hot-Reload\n";
|
||||
std::cout << "================================================================================\n\n";
|
||||
|
||||
// === SETUP ===
|
||||
std::cout << "Setup: Loading ConfigurableModule with initial config...\n";
|
||||
|
||||
ModuleLoader loader;
|
||||
auto moduleSystem = std::make_unique<SequentialModuleSystem>();
|
||||
|
||||
// Charger module
|
||||
std::string modulePath = "./libConfigurableModule.so";
|
||||
#ifdef _WIN32
|
||||
modulePath = "./libConfigurableModule.dll";
|
||||
#endif
|
||||
auto module = loader.load(modulePath, "ConfigurableModule", false);
|
||||
|
||||
// Config initiale
|
||||
nlohmann::json configJson;
|
||||
configJson["spawnRate"] = 10;
|
||||
configJson["maxEntities"] = 150; // Higher limit for Phase 0
|
||||
configJson["entitySpeed"] = 5.0;
|
||||
configJson["colors"] = nlohmann::json::array({"red", "blue"});
|
||||
configJson["physics"]["gravity"] = 9.8;
|
||||
configJson["physics"]["friction"] = 0.5;
|
||||
auto config = std::make_unique<JsonDataNode>("config", configJson);
|
||||
|
||||
module->setConfiguration(*config, nullptr, nullptr);
|
||||
moduleSystem->registerModule("ConfigurableModule", std::move(module));
|
||||
|
||||
std::cout << " Initial config:\n";
|
||||
std::cout << " Spawn rate: 10/s\n";
|
||||
std::cout << " Max entities: 150\n";
|
||||
std::cout << " Entity speed: 5.0\n";
|
||||
std::cout << " Colors: [red, blue]\n\n";
|
||||
|
||||
// === PHASE 0: Baseline (10s) ===
|
||||
std::cout << "=== Phase 0: Initial config (10s) ===\n";
|
||||
|
||||
for (int i = 0; i < 600; i++) { // 10s * 60 FPS
|
||||
auto frameStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
moduleSystem->processModules(1.0f / 60.0f);
|
||||
|
||||
auto frameEnd = std::chrono::high_resolution_clock::now();
|
||||
float frameTime = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count();
|
||||
metrics.recordFPS(1000.0f / frameTime);
|
||||
metrics.recordMemoryUsage(grove::getCurrentMemoryUsage());
|
||||
}
|
||||
|
||||
auto state0 = moduleSystem->extractModule()->getState();
|
||||
auto* json0 = dynamic_cast<JsonDataNode*>(state0.get());
|
||||
ASSERT_TRUE(json0 != nullptr, "State should be JsonDataNode");
|
||||
|
||||
const auto& state0Data = json0->getJsonData();
|
||||
int entityCount0 = state0Data["entities"].size();
|
||||
|
||||
std::cout << "✓ Baseline: " << entityCount0 << " entities spawned (~100 expected)\n";
|
||||
ASSERT_WITHIN(entityCount0, 100, 20, "Should have ~100 entities after 10s");
|
||||
reporter.addAssertion("initial_spawn_rate", true);
|
||||
|
||||
// Vérifier vitesse des entités initiales
|
||||
for (const auto& entity : state0Data["entities"]) {
|
||||
float speed = entity["speed"];
|
||||
ASSERT_EQ_FLOAT(speed, 5.0f, 0.01f, "Initial entity speed should be 5.0");
|
||||
}
|
||||
|
||||
// Re-register module
|
||||
auto module0 = loader.load(modulePath, "ConfigurableModule", false);
|
||||
module0->setState(*state0);
|
||||
moduleSystem->registerModule("ConfigurableModule", std::move(module0));
|
||||
|
||||
// === PHASE 1: Simple Config Change (10s) ===
|
||||
std::cout << "\n=== Phase 1: Doubling spawn rate and speed (10s) ===\n";
|
||||
|
||||
nlohmann::json newConfig1;
|
||||
newConfig1["spawnRate"] = 20; // Double spawn rate
|
||||
newConfig1["maxEntities"] = 150; // Keep same limit
|
||||
newConfig1["entitySpeed"] = 10.0; // Double speed
|
||||
newConfig1["colors"] = nlohmann::json::array({"red", "blue"});
|
||||
newConfig1["physics"]["gravity"] = 9.8;
|
||||
newConfig1["physics"]["friction"] = 0.5;
|
||||
auto newConfigNode1 = std::make_unique<JsonDataNode>("config", newConfig1);
|
||||
|
||||
auto updateStart1 = std::chrono::high_resolution_clock::now();
|
||||
|
||||
// Extract, update config, re-register
|
||||
auto modulePhase1 = moduleSystem->extractModule();
|
||||
bool updateResult1 = modulePhase1->updateConfig(*newConfigNode1);
|
||||
|
||||
auto updateEnd1 = std::chrono::high_resolution_clock::now();
|
||||
float updateTime1 = std::chrono::duration<float, std::milli>(updateEnd1 - updateStart1).count();
|
||||
|
||||
ASSERT_TRUE(updateResult1, "Config update should succeed");
|
||||
reporter.addAssertion("config_update_simple", updateResult1);
|
||||
reporter.addMetric("config_update_time_ms", updateTime1);
|
||||
|
||||
std::cout << " Config updated in " << updateTime1 << "ms\n";
|
||||
|
||||
moduleSystem->registerModule("ConfigurableModule", std::move(modulePhase1));
|
||||
|
||||
// Run 10s
|
||||
for (int i = 0; i < 600; i++) {
|
||||
moduleSystem->processModules(1.0f / 60.0f);
|
||||
metrics.recordMemoryUsage(grove::getCurrentMemoryUsage());
|
||||
}
|
||||
|
||||
auto state1 = moduleSystem->extractModule()->getState();
|
||||
auto* json1 = dynamic_cast<JsonDataNode*>(state1.get());
|
||||
const auto& state1Data = json1->getJsonData();
|
||||
int entityCount1 = state1Data["entities"].size();
|
||||
|
||||
// Should have reached limit (150)
|
||||
std::cout << "✓ Phase 1: " << entityCount1 << " entities (max: 150)\n";
|
||||
ASSERT_LE(entityCount1, 150, "Should respect maxEntities limit");
|
||||
reporter.addAssertion("max_entities_respected", entityCount1 <= 150);
|
||||
|
||||
// Vérifier que nouvelles entités ont speed = 10.0
|
||||
int newEntityCount = 0;
|
||||
for (const auto& entity : state1Data["entities"]) {
|
||||
if (entity["id"] >= entityCount0) { // Nouvelle entité
|
||||
float speed = entity["speed"];
|
||||
ASSERT_EQ_FLOAT(speed, 10.0f, 0.01f, "New entities should have speed 10.0");
|
||||
newEntityCount++;
|
||||
}
|
||||
}
|
||||
std::cout << " " << newEntityCount << " new entities with speed 10.0\n";
|
||||
|
||||
// Re-register
|
||||
auto module1 = loader.load(modulePath, "ConfigurableModule", false);
|
||||
module1->setState(*state1);
|
||||
moduleSystem->registerModule("ConfigurableModule", std::move(module1));
|
||||
|
||||
// === PHASE 2: Complex Config Change (10s) ===
|
||||
std::cout << "\n=== Phase 2: Complex config changes (10s) ===\n";
|
||||
|
||||
nlohmann::json newConfig2;
|
||||
newConfig2["spawnRate"] = 15;
|
||||
newConfig2["maxEntities"] = 200; // Augmenter limite (was 150)
|
||||
newConfig2["entitySpeed"] = 7.5;
|
||||
newConfig2["colors"] = nlohmann::json::array({"green", "yellow", "purple"}); // Nouvelles couleurs
|
||||
newConfig2["physics"]["gravity"] = 1.6; // Gravité lunaire
|
||||
newConfig2["physics"]["friction"] = 0.2;
|
||||
auto newConfigNode2 = std::make_unique<JsonDataNode>("config", newConfig2);
|
||||
|
||||
auto modulePhase2 = moduleSystem->extractModule();
|
||||
bool updateResult2 = modulePhase2->updateConfig(*newConfigNode2);
|
||||
ASSERT_TRUE(updateResult2, "Config update 2 should succeed");
|
||||
|
||||
int entitiesBeforePhase2 = entityCount1;
|
||||
std::cout << " Config updated: new colors [green, yellow, purple]\n";
|
||||
std::cout << " Max entities increased to 200\n";
|
||||
|
||||
moduleSystem->registerModule("ConfigurableModule", std::move(modulePhase2));
|
||||
|
||||
// Run 10s
|
||||
for (int i = 0; i < 600; i++) {
|
||||
moduleSystem->processModules(1.0f / 60.0f);
|
||||
}
|
||||
|
||||
auto state2 = moduleSystem->extractModule()->getState();
|
||||
auto* json2 = dynamic_cast<JsonDataNode*>(state2.get());
|
||||
const auto& state2Data = json2->getJsonData();
|
||||
int entityCount2 = state2Data["entities"].size();
|
||||
|
||||
std::cout << "✓ Phase 2: " << entityCount2 << " total entities\n";
|
||||
ASSERT_GT(entityCount2, entitiesBeforePhase2, "Entity count should have increased");
|
||||
ASSERT_LE(entityCount2, 200, "Should respect new maxEntities = 200");
|
||||
|
||||
// Vérifier couleurs des nouvelles entités
|
||||
std::set<std::string> newColors;
|
||||
for (const auto& entity : state2Data["entities"]) {
|
||||
if (entity["id"] >= entitiesBeforePhase2) {
|
||||
newColors.insert(entity["color"]);
|
||||
}
|
||||
}
|
||||
|
||||
bool hasNewColors = newColors.count("green") || newColors.count("yellow") || newColors.count("purple");
|
||||
ASSERT_TRUE(hasNewColors, "New entities should use new color palette");
|
||||
reporter.addAssertion("new_colors_applied", hasNewColors);
|
||||
|
||||
std::cout << " New colors found: ";
|
||||
for (const auto& color : newColors) std::cout << color << " ";
|
||||
std::cout << "\n";
|
||||
|
||||
// Re-register
|
||||
auto module2 = loader.load(modulePath, "ConfigurableModule", false);
|
||||
module2->setState(*state2);
|
||||
moduleSystem->registerModule("ConfigurableModule", std::move(module2));
|
||||
|
||||
// === PHASE 3: Invalid Config + Rollback (5s) ===
|
||||
std::cout << "\n=== Phase 3: Invalid config rejection (5s) ===\n";
|
||||
|
||||
nlohmann::json invalidConfig;
|
||||
invalidConfig["spawnRate"] = -5; // INVALIDE: négatif
|
||||
invalidConfig["maxEntities"] = 1000000; // INVALIDE: trop grand
|
||||
invalidConfig["entitySpeed"] = 5.0;
|
||||
invalidConfig["colors"] = nlohmann::json::array({"red"});
|
||||
invalidConfig["physics"]["gravity"] = 9.8;
|
||||
invalidConfig["physics"]["friction"] = 0.5;
|
||||
auto invalidConfigNode = std::make_unique<JsonDataNode>("config", invalidConfig);
|
||||
|
||||
auto modulePhase3 = moduleSystem->extractModule();
|
||||
bool updateResult3 = modulePhase3->updateConfig(*invalidConfigNode);
|
||||
|
||||
std::cout << " Invalid config rejected: " << (!updateResult3 ? "YES" : "NO") << "\n";
|
||||
ASSERT_FALSE(updateResult3, "Invalid config should be rejected");
|
||||
reporter.addAssertion("invalid_config_rejected", !updateResult3);
|
||||
|
||||
moduleSystem->registerModule("ConfigurableModule", std::move(modulePhase3));
|
||||
|
||||
// Continuer - devrait utiliser la config précédente (valide)
|
||||
for (int i = 0; i < 300; i++) { // 5s
|
||||
moduleSystem->processModules(1.0f / 60.0f);
|
||||
}
|
||||
|
||||
auto state3 = moduleSystem->extractModule()->getState();
|
||||
auto* json3 = dynamic_cast<JsonDataNode*>(state3.get());
|
||||
const auto& state3Data = json3->getJsonData();
|
||||
int entityCount3 = state3Data["entities"].size();
|
||||
|
||||
std::cout << "✓ Rollback successful: " << (entityCount3 - entityCount2) << " entities spawned with previous config\n";
|
||||
// Note: We might already be at maxEntities (200), so we just verify no crash and config stayed valid
|
||||
ASSERT_GE(entityCount3, entityCount2, "Entity count should not decrease");
|
||||
reporter.addAssertion("config_rollback_works", entityCount3 >= entityCount2);
|
||||
|
||||
// Re-register
|
||||
auto module3 = loader.load(modulePath, "ConfigurableModule", false);
|
||||
module3->setState(*state3);
|
||||
moduleSystem->registerModule("ConfigurableModule", std::move(module3));
|
||||
|
||||
// === PHASE 4: Partial Config Update (5s) ===
|
||||
std::cout << "\n=== Phase 4: Partial config update (5s) ===\n";
|
||||
|
||||
nlohmann::json partialConfig;
|
||||
partialConfig["entitySpeed"] = 2.0; // Modifier seulement la vitesse
|
||||
auto partialConfigNode = std::make_unique<JsonDataNode>("config", partialConfig);
|
||||
|
||||
auto modulePhase4 = moduleSystem->extractModule();
|
||||
bool updateResult4 = modulePhase4->updateConfigPartial(*partialConfigNode);
|
||||
|
||||
std::cout << " Partial update (entitySpeed only): " << (updateResult4 ? "SUCCESS" : "FAILED") << "\n";
|
||||
ASSERT_TRUE(updateResult4, "Partial config update should succeed");
|
||||
|
||||
moduleSystem->registerModule("ConfigurableModule", std::move(modulePhase4));
|
||||
|
||||
// Run 5s
|
||||
for (int i = 0; i < 300; i++) {
|
||||
moduleSystem->processModules(1.0f / 60.0f);
|
||||
}
|
||||
|
||||
auto state4 = moduleSystem->extractModule()->getState();
|
||||
auto* json4 = dynamic_cast<JsonDataNode*>(state4.get());
|
||||
const auto& state4Data = json4->getJsonData();
|
||||
|
||||
// Vérifier que nouvelles entités ont speed = 2.0
|
||||
// Et que colors sont toujours ceux de Phase 2
|
||||
// Note: We might be at maxEntities, so check if any new entities were spawned
|
||||
bool foundNewSpeed = false;
|
||||
bool foundOldColors = false;
|
||||
int newEntitiesPhase4 = 0;
|
||||
|
||||
for (const auto& entity : state4Data["entities"]) {
|
||||
if (entity["id"] >= entityCount3) {
|
||||
newEntitiesPhase4++;
|
||||
float speed = entity["speed"];
|
||||
if (std::abs(speed - 2.0f) < 0.01f) foundNewSpeed = true;
|
||||
|
||||
std::string color = entity["color"];
|
||||
if (color == "green" || color == "yellow" || color == "purple") {
|
||||
foundOldColors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "✓ Partial update: speed changed to 2.0, other params preserved\n";
|
||||
std::cout << " New entities in Phase 4: " << newEntitiesPhase4 << " (may be 0 if at maxEntities)\n";
|
||||
|
||||
// If we spawned new entities, verify they have the new speed
|
||||
// Otherwise, just verify the partial update succeeded (which it did above)
|
||||
if (newEntitiesPhase4 > 0) {
|
||||
ASSERT_TRUE(foundNewSpeed, "New entities should have updated speed");
|
||||
ASSERT_TRUE(foundOldColors, "Colors should be preserved from Phase 2");
|
||||
reporter.addAssertion("partial_update_works", foundNewSpeed && foundOldColors);
|
||||
} else {
|
||||
// At maxEntities, just verify no crash and config updated
|
||||
std::cout << " (At maxEntities, cannot verify new entity speed)\n";
|
||||
reporter.addAssertion("partial_update_works", true);
|
||||
}
|
||||
|
||||
// === VÉRIFICATIONS FINALES ===
|
||||
std::cout << "\n================================================================================\n";
|
||||
std::cout << "FINAL VERIFICATION\n";
|
||||
std::cout << "================================================================================\n";
|
||||
|
||||
// Memory stability
|
||||
size_t memGrowth = metrics.getMemoryGrowth();
|
||||
float memGrowthMB = memGrowth / (1024.0f * 1024.0f);
|
||||
|
||||
std::cout << "Memory growth: " << memGrowthMB << " MB (threshold: < 10 MB)\n";
|
||||
ASSERT_LT(memGrowth, 10 * 1024 * 1024, "Memory growth should be < 10MB");
|
||||
reporter.addMetric("memory_growth_mb", memGrowthMB);
|
||||
|
||||
// No crashes
|
||||
reporter.addAssertion("no_crashes", true);
|
||||
|
||||
std::cout << "\n";
|
||||
|
||||
// === RAPPORT FINAL ===
|
||||
metrics.printReport();
|
||||
reporter.printFinalReport();
|
||||
|
||||
return reporter.getExitCode();
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,491 +1,491 @@
|
||||
/**
|
||||
* Scenario 11: IO System Stress Test
|
||||
*
|
||||
* Tests IntraIO pub/sub system with:
|
||||
* - Basic publish/subscribe
|
||||
* - Pattern matching with wildcards
|
||||
* - Multi-module routing (1-to-many)
|
||||
* - Message batching (low-frequency subscriptions)
|
||||
* - Backpressure and queue overflow
|
||||
* - Thread safety
|
||||
* - Health monitoring
|
||||
*
|
||||
* Known bug to validate: IntraIOManager may route only to first subscriber (std::move limitation)
|
||||
*/
|
||||
|
||||
#include "grove/IModule.h"
|
||||
#include "grove/IOFactory.h"
|
||||
#include "grove/IntraIOManager.h"
|
||||
#include "grove/JsonDataNode.h"
|
||||
#include "../helpers/TestMetrics.h"
|
||||
#include "../helpers/TestAssertions.h"
|
||||
#include "../helpers/TestReporter.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <dlfcn.h>
|
||||
#endif
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
|
||||
// Cross-platform dlopen wrappers
|
||||
#ifdef _WIN32
|
||||
inline void* grove_dlopen(const char* path, int flags) {
|
||||
(void)flags;
|
||||
return LoadLibraryA(path);
|
||||
}
|
||||
inline void* grove_dlsym(void* handle, const char* symbol) {
|
||||
return (void*)GetProcAddress((HMODULE)handle, symbol);
|
||||
}
|
||||
inline int grove_dlclose(void* handle) {
|
||||
return FreeLibrary((HMODULE)handle) ? 0 : -1;
|
||||
}
|
||||
inline const char* grove_dlerror() {
|
||||
static thread_local char buf[256];
|
||||
DWORD err = GetLastError();
|
||||
snprintf(buf, sizeof(buf), "Windows error code: %lu", err);
|
||||
return buf;
|
||||
}
|
||||
#define RTLD_NOW 0
|
||||
#define RTLD_LOCAL 0
|
||||
#else
|
||||
#define grove_dlopen dlopen
|
||||
#define grove_dlsym dlsym
|
||||
#define grove_dlclose dlclose
|
||||
#define grove_dlerror dlerror
|
||||
#endif
|
||||
|
||||
using namespace grove;
|
||||
|
||||
// Module handle for testing
|
||||
struct ModuleHandle {
|
||||
void* dlHandle = nullptr;
|
||||
grove::IModule* instance = nullptr;
|
||||
std::unique_ptr<IIO> io;
|
||||
std::string modulePath;
|
||||
};
|
||||
|
||||
// Simple module loader for IO testing
|
||||
class IOTestEngine {
|
||||
public:
|
||||
IOTestEngine() {}
|
||||
|
||||
~IOTestEngine() {
|
||||
for (auto& [name, handle] : modules_) {
|
||||
unloadModule(name);
|
||||
}
|
||||
}
|
||||
|
||||
bool loadModule(const std::string& name, const std::string& path) {
|
||||
if (modules_.count(name) > 0) {
|
||||
std::cerr << "Module " << name << " already loaded\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
void* dlHandle = grove_dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
|
||||
if (!dlHandle) {
|
||||
std::cerr << "Failed to load module " << name << ": " << grove_dlerror() << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
auto createFunc = (grove::IModule* (*)())grove_dlsym(dlHandle, "createModule");
|
||||
if (!createFunc) {
|
||||
std::cerr << "Failed to find createModule in " << name << ": " << grove_dlerror() << "\n";
|
||||
grove_dlclose(dlHandle);
|
||||
return false;
|
||||
}
|
||||
|
||||
grove::IModule* instance = createFunc();
|
||||
if (!instance) {
|
||||
std::cerr << "createModule returned nullptr for " << name << "\n";
|
||||
grove_dlclose(dlHandle);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create IntraIO instance for this module
|
||||
auto io = IOFactory::create("intra", name);
|
||||
|
||||
ModuleHandle handle;
|
||||
handle.dlHandle = dlHandle;
|
||||
handle.instance = instance;
|
||||
handle.io = std::move(io);
|
||||
handle.modulePath = path;
|
||||
|
||||
modules_[name] = std::move(handle);
|
||||
|
||||
// Initialize module with IO
|
||||
auto config = std::make_unique<JsonDataNode>("config", nlohmann::json::object());
|
||||
instance->setConfiguration(*config, modules_[name].io.get(), nullptr);
|
||||
|
||||
std::cout << " ✓ Loaded " << name << "\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
void unloadModule(const std::string& name) {
|
||||
auto it = modules_.find(name);
|
||||
if (it == modules_.end()) return;
|
||||
|
||||
auto& handle = it->second;
|
||||
|
||||
if (handle.instance) {
|
||||
handle.instance->shutdown();
|
||||
delete handle.instance;
|
||||
handle.instance = nullptr;
|
||||
}
|
||||
|
||||
if (handle.dlHandle) {
|
||||
grove_dlclose(handle.dlHandle);
|
||||
handle.dlHandle = nullptr;
|
||||
}
|
||||
|
||||
modules_.erase(it);
|
||||
}
|
||||
|
||||
grove::IModule* getModule(const std::string& name) {
|
||||
auto it = modules_.find(name);
|
||||
return (it != modules_.end()) ? it->second.instance : nullptr;
|
||||
}
|
||||
|
||||
IIO* getIO(const std::string& name) {
|
||||
auto it = modules_.find(name);
|
||||
return (it != modules_.end()) ? it->second.io.get() : nullptr;
|
||||
}
|
||||
|
||||
void processAll(const IDataNode& input) {
|
||||
for (auto& [name, handle] : modules_) {
|
||||
if (handle.instance) {
|
||||
handle.instance->process(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::map<std::string, ModuleHandle> modules_;
|
||||
};
|
||||
|
||||
int main() {
|
||||
TestReporter reporter("IO System Stress Test");
|
||||
TestMetrics metrics;
|
||||
|
||||
std::cout << "================================================================================\n";
|
||||
std::cout << "TEST: IO System Stress Test (Scenario 11)\n";
|
||||
std::cout << "================================================================================\n\n";
|
||||
|
||||
// === SETUP ===
|
||||
std::cout << "Setup: Loading IO modules...\n";
|
||||
|
||||
IOTestEngine engine;
|
||||
|
||||
// Load all IO test modules
|
||||
bool loadSuccess = true;
|
||||
loadSuccess &= engine.loadModule("ProducerModule", "./libProducerModule.so");
|
||||
loadSuccess &= engine.loadModule("ConsumerModule", "./libConsumerModule.so");
|
||||
loadSuccess &= engine.loadModule("BroadcastModule", "./libBroadcastModule.so");
|
||||
loadSuccess &= engine.loadModule("BatchModule", "./libBatchModule.so");
|
||||
loadSuccess &= engine.loadModule("IOStressModule", "./libIOStressModule.so");
|
||||
|
||||
if (!loadSuccess) {
|
||||
std::cerr << "❌ Failed to load required modules\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::cout << "\n";
|
||||
|
||||
auto producerIO = engine.getIO("ProducerModule");
|
||||
auto consumerIO = engine.getIO("ConsumerModule");
|
||||
auto broadcastIO = engine.getIO("BroadcastModule");
|
||||
auto batchIO = engine.getIO("BatchModule");
|
||||
auto stressIO = engine.getIO("IOStressModule");
|
||||
|
||||
if (!producerIO || !consumerIO || !broadcastIO || !batchIO || !stressIO) {
|
||||
std::cerr << "❌ Failed to get IO instances\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto emptyInput = std::make_unique<JsonDataNode>("input", nlohmann::json::object());
|
||||
|
||||
// ========================================================================
|
||||
// TEST 1: Basic Publish-Subscribe
|
||||
// ========================================================================
|
||||
std::cout << "=== TEST 1: Basic Publish-Subscribe ===\n";
|
||||
|
||||
// Consumer subscribes to "test:basic"
|
||||
consumerIO->subscribe("test:basic");
|
||||
|
||||
// Publish 100 messages
|
||||
for (int i = 0; i < 100; i++) {
|
||||
auto data = std::make_unique<JsonDataNode>("data", nlohmann::json{
|
||||
{"id", i},
|
||||
{"payload", "test_message_" + std::to_string(i)}
|
||||
});
|
||||
producerIO->publish("test:basic", std::move(data));
|
||||
}
|
||||
|
||||
// Process to allow routing
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
|
||||
// Count received messages
|
||||
int receivedCount = 0;
|
||||
while (consumerIO->hasMessages() > 0) {
|
||||
auto msg = consumerIO->pullMessage();
|
||||
receivedCount++;
|
||||
}
|
||||
|
||||
ASSERT_EQ(receivedCount, 100, "Should receive all 100 messages");
|
||||
reporter.addAssertion("basic_pubsub", receivedCount == 100);
|
||||
reporter.addMetric("basic_pubsub_count", receivedCount);
|
||||
std::cout << " ✓ Received " << receivedCount << "/100 messages\n";
|
||||
std::cout << "✓ TEST 1 PASSED\n\n";
|
||||
|
||||
// ========================================================================
|
||||
// TEST 2: Pattern Matching with Wildcards
|
||||
// ========================================================================
|
||||
std::cout << "=== TEST 2: Pattern Matching ===\n";
|
||||
|
||||
// Subscribe to patterns
|
||||
consumerIO->subscribe("player:.*");
|
||||
|
||||
// Publish test messages
|
||||
std::vector<std::string> testTopics = {
|
||||
"player:001:position",
|
||||
"player:001:health",
|
||||
"player:002:position",
|
||||
"enemy:001:position"
|
||||
};
|
||||
|
||||
for (const auto& topic : testTopics) {
|
||||
auto data = std::make_unique<JsonDataNode>("data", nlohmann::json{{"topic", topic}});
|
||||
producerIO->publish(topic, std::move(data));
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
|
||||
// Count player messages (should match 3 of 4)
|
||||
int playerMsgCount = 0;
|
||||
while (consumerIO->hasMessages() > 0) {
|
||||
auto msg = consumerIO->pullMessage();
|
||||
if (msg.topic.find("player:") == 0) {
|
||||
playerMsgCount++;
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << " Pattern 'player:.*' matched " << playerMsgCount << " messages\n";
|
||||
ASSERT_GE(playerMsgCount, 3, "Should match at least 3 player messages");
|
||||
reporter.addAssertion("pattern_matching", playerMsgCount >= 3);
|
||||
reporter.addMetric("pattern_match_count", playerMsgCount);
|
||||
std::cout << "✓ TEST 2 PASSED\n\n";
|
||||
|
||||
// ========================================================================
|
||||
// TEST 3: Multi-Module Routing (1-to-many) - Bug Detection
|
||||
// ========================================================================
|
||||
std::cout << "=== TEST 3: Multi-Module Routing (1-to-many) ===\n";
|
||||
std::cout << " Testing for known bug: std::move limitation in routing\n";
|
||||
|
||||
// All modules subscribe to "broadcast:.*"
|
||||
consumerIO->subscribe("broadcast:.*");
|
||||
broadcastIO->subscribe("broadcast:.*");
|
||||
batchIO->subscribe("broadcast:.*");
|
||||
stressIO->subscribe("broadcast:.*");
|
||||
|
||||
// Publish 10 broadcast messages
|
||||
for (int i = 0; i < 10; i++) {
|
||||
auto data = std::make_unique<JsonDataNode>("data", nlohmann::json{{"broadcast_id", i}});
|
||||
producerIO->publish("broadcast:data", std::move(data));
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
|
||||
// Check which modules received messages
|
||||
int consumerReceived = consumerIO->hasMessages();
|
||||
int broadcastReceived = broadcastIO->hasMessages();
|
||||
int batchReceived = batchIO->hasMessages();
|
||||
int stressReceived = stressIO->hasMessages();
|
||||
|
||||
std::cout << " Broadcast distribution:\n";
|
||||
std::cout << " ConsumerModule: " << consumerReceived << " messages\n";
|
||||
std::cout << " BroadcastModule: " << broadcastReceived << " messages\n";
|
||||
std::cout << " BatchModule: " << batchReceived << " messages\n";
|
||||
std::cout << " IOStressModule: " << stressReceived << " messages\n";
|
||||
|
||||
int totalReceived = consumerReceived + broadcastReceived + batchReceived + stressReceived;
|
||||
|
||||
if (totalReceived == 10) {
|
||||
std::cout << " ⚠️ BUG CONFIRMED: Only one module received all messages\n";
|
||||
std::cout << " This confirms the clone() limitation in routing\n";
|
||||
reporter.addMetric("broadcast_bug_present", 1.0f);
|
||||
} else if (totalReceived >= 40) {
|
||||
std::cout << " ✓ FIXED: All modules received copies (clone() implemented!)\n";
|
||||
reporter.addMetric("broadcast_bug_present", 0.0f);
|
||||
} else {
|
||||
std::cout << " ⚠️ Unexpected: " << totalReceived << " messages received (expected 10 or 40)\n";
|
||||
reporter.addMetric("broadcast_bug_present", 0.5f);
|
||||
}
|
||||
|
||||
reporter.addAssertion("multi_module_routing_tested", true);
|
||||
std::cout << "✓ TEST 3 COMPLETED (bug documented)\n\n";
|
||||
|
||||
// Clean up for next test
|
||||
while (consumerIO->hasMessages() > 0) consumerIO->pullMessage();
|
||||
while (broadcastIO->hasMessages() > 0) broadcastIO->pullMessage();
|
||||
while (batchIO->hasMessages() > 0) batchIO->pullMessage();
|
||||
while (stressIO->hasMessages() > 0) stressIO->pullMessage();
|
||||
|
||||
// ========================================================================
|
||||
// TEST 4: Low-Frequency Subscriptions (Batching)
|
||||
// ========================================================================
|
||||
std::cout << "=== TEST 4: Low-Frequency Subscriptions ===\n";
|
||||
|
||||
SubscriptionConfig batchConfig;
|
||||
batchConfig.replaceable = true;
|
||||
batchConfig.batchInterval = 1000; // 1 second
|
||||
batchIO->subscribeLowFreq("batch:.*", batchConfig);
|
||||
|
||||
std::cout << " Publishing 100 messages over 2 seconds...\n";
|
||||
int batchPublished = 0;
|
||||
auto batchStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
for (int i = 0; i < 100; i++) {
|
||||
auto data = std::make_unique<JsonDataNode>("data", nlohmann::json{
|
||||
{"timestamp", i},
|
||||
{"value", i * 0.1f}
|
||||
});
|
||||
producerIO->publish("batch:metric", std::move(data));
|
||||
batchPublished++;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20)); // 50 Hz
|
||||
}
|
||||
|
||||
auto batchEnd = std::chrono::high_resolution_clock::now();
|
||||
float batchDuration = std::chrono::duration<float>(batchEnd - batchStart).count();
|
||||
|
||||
// Check batched messages
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
int batchesReceived = 0;
|
||||
while (batchIO->hasMessages() > 0) {
|
||||
auto msg = batchIO->pullMessage();
|
||||
batchesReceived++;
|
||||
}
|
||||
|
||||
std::cout << " Published: " << batchPublished << " messages over " << batchDuration << "s\n";
|
||||
std::cout << " Received: " << batchesReceived << " batches\n";
|
||||
std::cout << " Expected: ~" << static_cast<int>(batchDuration) << " batches (1/second)\n";
|
||||
|
||||
// With 1s batching, expect fewer messages than published
|
||||
ASSERT_LT(batchesReceived, batchPublished, "Batching should reduce message count");
|
||||
reporter.addMetric("batch_count", batchesReceived);
|
||||
reporter.addMetric("batch_published", batchPublished);
|
||||
reporter.addAssertion("batching_reduces_messages", batchesReceived < batchPublished);
|
||||
std::cout << "✓ TEST 4 PASSED\n\n";
|
||||
|
||||
// ========================================================================
|
||||
// TEST 5: Backpressure & Queue Overflow
|
||||
// ========================================================================
|
||||
std::cout << "=== TEST 5: Backpressure & Queue Overflow ===\n";
|
||||
|
||||
consumerIO->subscribe("stress:flood");
|
||||
|
||||
std::cout << " Publishing 10000 messages without pulling...\n";
|
||||
for (int i = 0; i < 10000; i++) {
|
||||
auto data = std::make_unique<JsonDataNode>("data", nlohmann::json{{"flood_id", i}});
|
||||
producerIO->publish("stress:flood", std::move(data));
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
|
||||
// Check health
|
||||
auto health = consumerIO->getHealth();
|
||||
std::cout << " Health status:\n";
|
||||
std::cout << " Queue size: " << health.queueSize << " / " << health.maxQueueSize << "\n";
|
||||
std::cout << " Dropping: " << (health.dropping ? "YES" : "NO") << "\n";
|
||||
std::cout << " Dropped count: " << health.droppedMessageCount << "\n";
|
||||
|
||||
ASSERT_GT(health.queueSize, 0, "Queue should have messages");
|
||||
reporter.addMetric("queue_size", health.queueSize);
|
||||
reporter.addMetric("dropped_messages", health.droppedMessageCount);
|
||||
reporter.addAssertion("backpressure_monitoring", true);
|
||||
std::cout << "✓ TEST 5 PASSED\n\n";
|
||||
|
||||
// Clean up queue
|
||||
while (consumerIO->hasMessages() > 0) consumerIO->pullMessage();
|
||||
|
||||
// ========================================================================
|
||||
// TEST 6: Thread Safety (Concurrent Pub/Pull)
|
||||
// ========================================================================
|
||||
std::cout << "=== TEST 6: Thread Safety ===\n";
|
||||
|
||||
consumerIO->subscribe("thread:.*");
|
||||
|
||||
std::atomic<int> publishedTotal{0};
|
||||
std::atomic<int> receivedTotal{0};
|
||||
std::atomic<bool> running{true};
|
||||
|
||||
std::cout << " Launching 5 publisher threads...\n";
|
||||
std::vector<std::thread> publishers;
|
||||
for (int t = 0; t < 5; t++) {
|
||||
publishers.emplace_back([&, t]() {
|
||||
for (int i = 0; i < 100; i++) {
|
||||
auto data = std::make_unique<JsonDataNode>("data", nlohmann::json{
|
||||
{"thread", t},
|
||||
{"id", i}
|
||||
});
|
||||
producerIO->publish("thread:test", std::move(data));
|
||||
publishedTotal++;
|
||||
std::this_thread::sleep_for(std::chrono::microseconds(100));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
std::cout << " Launching 3 consumer threads...\n";
|
||||
std::vector<std::thread> consumers;
|
||||
for (int t = 0; t < 3; t++) {
|
||||
consumers.emplace_back([&]() {
|
||||
while (running || consumerIO->hasMessages() > 0) {
|
||||
if (consumerIO->hasMessages() > 0) {
|
||||
try {
|
||||
auto msg = consumerIO->pullMessage();
|
||||
receivedTotal++;
|
||||
} catch (...) {
|
||||
// Expected: may have race conditions
|
||||
}
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::microseconds(500));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Wait for publishers
|
||||
for (auto& t : publishers) {
|
||||
t.join();
|
||||
}
|
||||
|
||||
std::cout << " All publishers done: " << publishedTotal << " messages\n";
|
||||
|
||||
// Let consumers finish
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
running = false;
|
||||
|
||||
for (auto& t : consumers) {
|
||||
t.join();
|
||||
}
|
||||
|
||||
std::cout << " All consumers done: " << receivedTotal << " messages\n";
|
||||
|
||||
ASSERT_GT(receivedTotal, 0, "Should receive at least some messages");
|
||||
reporter.addMetric("concurrent_published", publishedTotal);
|
||||
reporter.addMetric("concurrent_received", receivedTotal);
|
||||
reporter.addAssertion("thread_safety", true); // No crash = success
|
||||
std::cout << "✓ TEST 6 PASSED (no crashes)\n\n";
|
||||
|
||||
// ========================================================================
|
||||
// FINAL REPORT
|
||||
// ========================================================================
|
||||
|
||||
metrics.printReport();
|
||||
reporter.printFinalReport();
|
||||
|
||||
return reporter.getExitCode();
|
||||
}
|
||||
/**
|
||||
* Scenario 11: IO System Stress Test
|
||||
*
|
||||
* Tests IntraIO pub/sub system with:
|
||||
* - Basic publish/subscribe
|
||||
* - Pattern matching with wildcards
|
||||
* - Multi-module routing (1-to-many)
|
||||
* - Message batching (low-frequency subscriptions)
|
||||
* - Backpressure and queue overflow
|
||||
* - Thread safety
|
||||
* - Health monitoring
|
||||
*
|
||||
* Known bug to validate: IntraIOManager may route only to first subscriber (std::move limitation)
|
||||
*/
|
||||
|
||||
#include "grove/IModule.h"
|
||||
#include "grove/IOFactory.h"
|
||||
#include "grove/IntraIOManager.h"
|
||||
#include "grove/JsonDataNode.h"
|
||||
#include "../helpers/TestMetrics.h"
|
||||
#include "../helpers/TestAssertions.h"
|
||||
#include "../helpers/TestReporter.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <dlfcn.h>
|
||||
#endif
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
|
||||
// Cross-platform dlopen wrappers
|
||||
#ifdef _WIN32
|
||||
inline void* grove_dlopen(const char* path, int flags) {
|
||||
(void)flags;
|
||||
return LoadLibraryA(path);
|
||||
}
|
||||
inline void* grove_dlsym(void* handle, const char* symbol) {
|
||||
return (void*)GetProcAddress((HMODULE)handle, symbol);
|
||||
}
|
||||
inline int grove_dlclose(void* handle) {
|
||||
return FreeLibrary((HMODULE)handle) ? 0 : -1;
|
||||
}
|
||||
inline const char* grove_dlerror() {
|
||||
static thread_local char buf[256];
|
||||
DWORD err = GetLastError();
|
||||
snprintf(buf, sizeof(buf), "Windows error code: %lu", err);
|
||||
return buf;
|
||||
}
|
||||
#define RTLD_NOW 0
|
||||
#define RTLD_LOCAL 0
|
||||
#else
|
||||
#define grove_dlopen dlopen
|
||||
#define grove_dlsym dlsym
|
||||
#define grove_dlclose dlclose
|
||||
#define grove_dlerror dlerror
|
||||
#endif
|
||||
|
||||
using namespace grove;
|
||||
|
||||
// Module handle for testing
|
||||
struct ModuleHandle {
|
||||
void* dlHandle = nullptr;
|
||||
grove::IModule* instance = nullptr;
|
||||
std::unique_ptr<IIO> io;
|
||||
std::string modulePath;
|
||||
};
|
||||
|
||||
// Simple module loader for IO testing
|
||||
class IOTestEngine {
|
||||
public:
|
||||
IOTestEngine() {}
|
||||
|
||||
~IOTestEngine() {
|
||||
for (auto& [name, handle] : modules_) {
|
||||
unloadModule(name);
|
||||
}
|
||||
}
|
||||
|
||||
bool loadModule(const std::string& name, const std::string& path) {
|
||||
if (modules_.count(name) > 0) {
|
||||
std::cerr << "Module " << name << " already loaded\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
void* dlHandle = grove_dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
|
||||
if (!dlHandle) {
|
||||
std::cerr << "Failed to load module " << name << ": " << grove_dlerror() << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
auto createFunc = (grove::IModule* (*)())grove_dlsym(dlHandle, "createModule");
|
||||
if (!createFunc) {
|
||||
std::cerr << "Failed to find createModule in " << name << ": " << grove_dlerror() << "\n";
|
||||
grove_dlclose(dlHandle);
|
||||
return false;
|
||||
}
|
||||
|
||||
grove::IModule* instance = createFunc();
|
||||
if (!instance) {
|
||||
std::cerr << "createModule returned nullptr for " << name << "\n";
|
||||
grove_dlclose(dlHandle);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create IntraIO instance for this module
|
||||
auto io = IOFactory::create("intra", name);
|
||||
|
||||
ModuleHandle handle;
|
||||
handle.dlHandle = dlHandle;
|
||||
handle.instance = instance;
|
||||
handle.io = std::move(io);
|
||||
handle.modulePath = path;
|
||||
|
||||
modules_[name] = std::move(handle);
|
||||
|
||||
// Initialize module with IO
|
||||
auto config = std::make_unique<JsonDataNode>("config", nlohmann::json::object());
|
||||
instance->setConfiguration(*config, modules_[name].io.get(), nullptr);
|
||||
|
||||
std::cout << " ✓ Loaded " << name << "\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
void unloadModule(const std::string& name) {
|
||||
auto it = modules_.find(name);
|
||||
if (it == modules_.end()) return;
|
||||
|
||||
auto& handle = it->second;
|
||||
|
||||
if (handle.instance) {
|
||||
handle.instance->shutdown();
|
||||
delete handle.instance;
|
||||
handle.instance = nullptr;
|
||||
}
|
||||
|
||||
if (handle.dlHandle) {
|
||||
grove_dlclose(handle.dlHandle);
|
||||
handle.dlHandle = nullptr;
|
||||
}
|
||||
|
||||
modules_.erase(it);
|
||||
}
|
||||
|
||||
grove::IModule* getModule(const std::string& name) {
|
||||
auto it = modules_.find(name);
|
||||
return (it != modules_.end()) ? it->second.instance : nullptr;
|
||||
}
|
||||
|
||||
IIO* getIO(const std::string& name) {
|
||||
auto it = modules_.find(name);
|
||||
return (it != modules_.end()) ? it->second.io.get() : nullptr;
|
||||
}
|
||||
|
||||
void processAll(const IDataNode& input) {
|
||||
for (auto& [name, handle] : modules_) {
|
||||
if (handle.instance) {
|
||||
handle.instance->process(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::map<std::string, ModuleHandle> modules_;
|
||||
};
|
||||
|
||||
int main() {
|
||||
TestReporter reporter("IO System Stress Test");
|
||||
TestMetrics metrics;
|
||||
|
||||
std::cout << "================================================================================\n";
|
||||
std::cout << "TEST: IO System Stress Test (Scenario 11)\n";
|
||||
std::cout << "================================================================================\n\n";
|
||||
|
||||
// === SETUP ===
|
||||
std::cout << "Setup: Loading IO modules...\n";
|
||||
|
||||
IOTestEngine engine;
|
||||
|
||||
// Load all IO test modules
|
||||
bool loadSuccess = true;
|
||||
loadSuccess &= engine.loadModule("ProducerModule", "./libProducerModule.dll");
|
||||
loadSuccess &= engine.loadModule("ConsumerModule", "./libConsumerModule.dll");
|
||||
loadSuccess &= engine.loadModule("BroadcastModule", "./libBroadcastModule.dll");
|
||||
loadSuccess &= engine.loadModule("BatchModule", "./libBatchModule.dll");
|
||||
loadSuccess &= engine.loadModule("IOStressModule", "./libIOStressModule.dll");
|
||||
|
||||
if (!loadSuccess) {
|
||||
std::cerr << "❌ Failed to load required modules\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::cout << "\n";
|
||||
|
||||
auto producerIO = engine.getIO("ProducerModule");
|
||||
auto consumerIO = engine.getIO("ConsumerModule");
|
||||
auto broadcastIO = engine.getIO("BroadcastModule");
|
||||
auto batchIO = engine.getIO("BatchModule");
|
||||
auto stressIO = engine.getIO("IOStressModule");
|
||||
|
||||
if (!producerIO || !consumerIO || !broadcastIO || !batchIO || !stressIO) {
|
||||
std::cerr << "❌ Failed to get IO instances\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto emptyInput = std::make_unique<JsonDataNode>("input", nlohmann::json::object());
|
||||
|
||||
// ========================================================================
|
||||
// TEST 1: Basic Publish-Subscribe
|
||||
// ========================================================================
|
||||
std::cout << "=== TEST 1: Basic Publish-Subscribe ===\n";
|
||||
|
||||
// Consumer subscribes to "test:basic"
|
||||
consumerIO->subscribe("test:basic");
|
||||
|
||||
// Publish 100 messages
|
||||
for (int i = 0; i < 100; i++) {
|
||||
auto data = std::make_unique<JsonDataNode>("data", nlohmann::json{
|
||||
{"id", i},
|
||||
{"payload", "test_message_" + std::to_string(i)}
|
||||
});
|
||||
producerIO->publish("test:basic", std::move(data));
|
||||
}
|
||||
|
||||
// Process to allow routing
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
|
||||
// Count received messages
|
||||
int receivedCount = 0;
|
||||
while (consumerIO->hasMessages() > 0) {
|
||||
auto msg = consumerIO->pullMessage();
|
||||
receivedCount++;
|
||||
}
|
||||
|
||||
ASSERT_EQ(receivedCount, 100, "Should receive all 100 messages");
|
||||
reporter.addAssertion("basic_pubsub", receivedCount == 100);
|
||||
reporter.addMetric("basic_pubsub_count", receivedCount);
|
||||
std::cout << " ✓ Received " << receivedCount << "/100 messages\n";
|
||||
std::cout << "✓ TEST 1 PASSED\n\n";
|
||||
|
||||
// ========================================================================
|
||||
// TEST 2: Pattern Matching with Wildcards
|
||||
// ========================================================================
|
||||
std::cout << "=== TEST 2: Pattern Matching ===\n";
|
||||
|
||||
// Subscribe to patterns
|
||||
consumerIO->subscribe("player:.*");
|
||||
|
||||
// Publish test messages
|
||||
std::vector<std::string> testTopics = {
|
||||
"player:001:position",
|
||||
"player:001:health",
|
||||
"player:002:position",
|
||||
"enemy:001:position"
|
||||
};
|
||||
|
||||
for (const auto& topic : testTopics) {
|
||||
auto data = std::make_unique<JsonDataNode>("data", nlohmann::json{{"topic", topic}});
|
||||
producerIO->publish(topic, std::move(data));
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
|
||||
// Count player messages (should match 3 of 4)
|
||||
int playerMsgCount = 0;
|
||||
while (consumerIO->hasMessages() > 0) {
|
||||
auto msg = consumerIO->pullMessage();
|
||||
if (msg.topic.find("player:") == 0) {
|
||||
playerMsgCount++;
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << " Pattern 'player:.*' matched " << playerMsgCount << " messages\n";
|
||||
ASSERT_GE(playerMsgCount, 3, "Should match at least 3 player messages");
|
||||
reporter.addAssertion("pattern_matching", playerMsgCount >= 3);
|
||||
reporter.addMetric("pattern_match_count", playerMsgCount);
|
||||
std::cout << "✓ TEST 2 PASSED\n\n";
|
||||
|
||||
// ========================================================================
|
||||
// TEST 3: Multi-Module Routing (1-to-many) - Bug Detection
|
||||
// ========================================================================
|
||||
std::cout << "=== TEST 3: Multi-Module Routing (1-to-many) ===\n";
|
||||
std::cout << " Testing for known bug: std::move limitation in routing\n";
|
||||
|
||||
// All modules subscribe to "broadcast:.*"
|
||||
consumerIO->subscribe("broadcast:.*");
|
||||
broadcastIO->subscribe("broadcast:.*");
|
||||
batchIO->subscribe("broadcast:.*");
|
||||
stressIO->subscribe("broadcast:.*");
|
||||
|
||||
// Publish 10 broadcast messages
|
||||
for (int i = 0; i < 10; i++) {
|
||||
auto data = std::make_unique<JsonDataNode>("data", nlohmann::json{{"broadcast_id", i}});
|
||||
producerIO->publish("broadcast:data", std::move(data));
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
|
||||
// Check which modules received messages
|
||||
int consumerReceived = consumerIO->hasMessages();
|
||||
int broadcastReceived = broadcastIO->hasMessages();
|
||||
int batchReceived = batchIO->hasMessages();
|
||||
int stressReceived = stressIO->hasMessages();
|
||||
|
||||
std::cout << " Broadcast distribution:\n";
|
||||
std::cout << " ConsumerModule: " << consumerReceived << " messages\n";
|
||||
std::cout << " BroadcastModule: " << broadcastReceived << " messages\n";
|
||||
std::cout << " BatchModule: " << batchReceived << " messages\n";
|
||||
std::cout << " IOStressModule: " << stressReceived << " messages\n";
|
||||
|
||||
int totalReceived = consumerReceived + broadcastReceived + batchReceived + stressReceived;
|
||||
|
||||
if (totalReceived == 10) {
|
||||
std::cout << " ⚠️ BUG CONFIRMED: Only one module received all messages\n";
|
||||
std::cout << " This confirms the clone() limitation in routing\n";
|
||||
reporter.addMetric("broadcast_bug_present", 1.0f);
|
||||
} else if (totalReceived >= 40) {
|
||||
std::cout << " ✓ FIXED: All modules received copies (clone() implemented!)\n";
|
||||
reporter.addMetric("broadcast_bug_present", 0.0f);
|
||||
} else {
|
||||
std::cout << " ⚠️ Unexpected: " << totalReceived << " messages received (expected 10 or 40)\n";
|
||||
reporter.addMetric("broadcast_bug_present", 0.5f);
|
||||
}
|
||||
|
||||
reporter.addAssertion("multi_module_routing_tested", true);
|
||||
std::cout << "✓ TEST 3 COMPLETED (bug documented)\n\n";
|
||||
|
||||
// Clean up for next test
|
||||
while (consumerIO->hasMessages() > 0) consumerIO->pullMessage();
|
||||
while (broadcastIO->hasMessages() > 0) broadcastIO->pullMessage();
|
||||
while (batchIO->hasMessages() > 0) batchIO->pullMessage();
|
||||
while (stressIO->hasMessages() > 0) stressIO->pullMessage();
|
||||
|
||||
// ========================================================================
|
||||
// TEST 4: Low-Frequency Subscriptions (Batching)
|
||||
// ========================================================================
|
||||
std::cout << "=== TEST 4: Low-Frequency Subscriptions ===\n";
|
||||
|
||||
SubscriptionConfig batchConfig;
|
||||
batchConfig.replaceable = true;
|
||||
batchConfig.batchInterval = 1000; // 1 second
|
||||
batchIO->subscribeLowFreq("batch:.*", batchConfig);
|
||||
|
||||
std::cout << " Publishing 100 messages over 2 seconds...\n";
|
||||
int batchPublished = 0;
|
||||
auto batchStart = std::chrono::high_resolution_clock::now();
|
||||
|
||||
for (int i = 0; i < 100; i++) {
|
||||
auto data = std::make_unique<JsonDataNode>("data", nlohmann::json{
|
||||
{"timestamp", i},
|
||||
{"value", i * 0.1f}
|
||||
});
|
||||
producerIO->publish("batch:metric", std::move(data));
|
||||
batchPublished++;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20)); // 50 Hz
|
||||
}
|
||||
|
||||
auto batchEnd = std::chrono::high_resolution_clock::now();
|
||||
float batchDuration = std::chrono::duration<float>(batchEnd - batchStart).count();
|
||||
|
||||
// Check batched messages
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
int batchesReceived = 0;
|
||||
while (batchIO->hasMessages() > 0) {
|
||||
auto msg = batchIO->pullMessage();
|
||||
batchesReceived++;
|
||||
}
|
||||
|
||||
std::cout << " Published: " << batchPublished << " messages over " << batchDuration << "s\n";
|
||||
std::cout << " Received: " << batchesReceived << " batches\n";
|
||||
std::cout << " Expected: ~" << static_cast<int>(batchDuration) << " batches (1/second)\n";
|
||||
|
||||
// With 1s batching, expect fewer messages than published
|
||||
ASSERT_LT(batchesReceived, batchPublished, "Batching should reduce message count");
|
||||
reporter.addMetric("batch_count", batchesReceived);
|
||||
reporter.addMetric("batch_published", batchPublished);
|
||||
reporter.addAssertion("batching_reduces_messages", batchesReceived < batchPublished);
|
||||
std::cout << "✓ TEST 4 PASSED\n\n";
|
||||
|
||||
// ========================================================================
|
||||
// TEST 5: Backpressure & Queue Overflow
|
||||
// ========================================================================
|
||||
std::cout << "=== TEST 5: Backpressure & Queue Overflow ===\n";
|
||||
|
||||
consumerIO->subscribe("stress:flood");
|
||||
|
||||
std::cout << " Publishing 10000 messages without pulling...\n";
|
||||
for (int i = 0; i < 10000; i++) {
|
||||
auto data = std::make_unique<JsonDataNode>("data", nlohmann::json{{"flood_id", i}});
|
||||
producerIO->publish("stress:flood", std::move(data));
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
|
||||
// Check health
|
||||
auto health = consumerIO->getHealth();
|
||||
std::cout << " Health status:\n";
|
||||
std::cout << " Queue size: " << health.queueSize << " / " << health.maxQueueSize << "\n";
|
||||
std::cout << " Dropping: " << (health.dropping ? "YES" : "NO") << "\n";
|
||||
std::cout << " Dropped count: " << health.droppedMessageCount << "\n";
|
||||
|
||||
ASSERT_GT(health.queueSize, 0, "Queue should have messages");
|
||||
reporter.addMetric("queue_size", health.queueSize);
|
||||
reporter.addMetric("dropped_messages", health.droppedMessageCount);
|
||||
reporter.addAssertion("backpressure_monitoring", true);
|
||||
std::cout << "✓ TEST 5 PASSED\n\n";
|
||||
|
||||
// Clean up queue
|
||||
while (consumerIO->hasMessages() > 0) consumerIO->pullMessage();
|
||||
|
||||
// ========================================================================
|
||||
// TEST 6: Thread Safety (Concurrent Pub/Pull)
|
||||
// ========================================================================
|
||||
std::cout << "=== TEST 6: Thread Safety ===\n";
|
||||
|
||||
consumerIO->subscribe("thread:.*");
|
||||
|
||||
std::atomic<int> publishedTotal{0};
|
||||
std::atomic<int> receivedTotal{0};
|
||||
std::atomic<bool> running{true};
|
||||
|
||||
std::cout << " Launching 5 publisher threads...\n";
|
||||
std::vector<std::thread> publishers;
|
||||
for (int t = 0; t < 5; t++) {
|
||||
publishers.emplace_back([&, t]() {
|
||||
for (int i = 0; i < 100; i++) {
|
||||
auto data = std::make_unique<JsonDataNode>("data", nlohmann::json{
|
||||
{"thread", t},
|
||||
{"id", i}
|
||||
});
|
||||
producerIO->publish("thread:test", std::move(data));
|
||||
publishedTotal++;
|
||||
std::this_thread::sleep_for(std::chrono::microseconds(100));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
std::cout << " Launching 3 consumer threads...\n";
|
||||
std::vector<std::thread> consumers;
|
||||
for (int t = 0; t < 3; t++) {
|
||||
consumers.emplace_back([&]() {
|
||||
while (running || consumerIO->hasMessages() > 0) {
|
||||
if (consumerIO->hasMessages() > 0) {
|
||||
try {
|
||||
auto msg = consumerIO->pullMessage();
|
||||
receivedTotal++;
|
||||
} catch (...) {
|
||||
// Expected: may have race conditions
|
||||
}
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::microseconds(500));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Wait for publishers
|
||||
for (auto& t : publishers) {
|
||||
t.join();
|
||||
}
|
||||
|
||||
std::cout << " All publishers done: " << publishedTotal << " messages\n";
|
||||
|
||||
// Let consumers finish
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
running = false;
|
||||
|
||||
for (auto& t : consumers) {
|
||||
t.join();
|
||||
}
|
||||
|
||||
std::cout << " All consumers done: " << receivedTotal << " messages\n";
|
||||
|
||||
ASSERT_GT(receivedTotal, 0, "Should receive at least some messages");
|
||||
reporter.addMetric("concurrent_published", publishedTotal);
|
||||
reporter.addMetric("concurrent_received", receivedTotal);
|
||||
reporter.addAssertion("thread_safety", true); // No crash = success
|
||||
std::cout << "✓ TEST 6 PASSED (no crashes)\n\n";
|
||||
|
||||
// ========================================================================
|
||||
// FINAL REPORT
|
||||
// ========================================================================
|
||||
|
||||
metrics.printReport();
|
||||
reporter.printFinalReport();
|
||||
|
||||
return reporter.getExitCode();
|
||||
}
|
||||
|
||||
@ -32,7 +32,7 @@ public:
|
||||
private:
|
||||
std::vector<Tank> tanks;
|
||||
int frameCount = 0;
|
||||
std::string moduleVersion = "v1.0";std::shared_ptr<spdlog::logger> logger;
|
||||
std::string moduleVersion = "v2.0 HOT-RELOADED";:shared_ptr<spdlog::logger> logger;
|
||||
std::unique_ptr<IDataNode> config;
|
||||
|
||||
void updateTank(Tank& tank, float dt);
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
#include <memory>
|
||||
|
||||
// This line will be modified by AutoCompiler during race condition tests
|
||||
std::string moduleVersion = "v1";
|
||||
std::string moduleVersion = "v10";
|
||||
|
||||
namespace grove {
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user