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:
StillHammer 2025-12-30 20:04:44 +07:00
parent 0540fbf526
commit edf4d76844
16 changed files with 5497 additions and 5407 deletions

View File

@ -199,6 +199,24 @@ option(GROVE_BUILD_TESTS "Build GroveEngine tests" ON)
if(GROVE_BUILD_TESTS) if(GROVE_BUILD_TESTS)
enable_testing() 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) add_subdirectory(tests)
endif() endif()

View File

@ -9,6 +9,17 @@ FetchContent_Declare(
) )
FetchContent_MakeAvailable(Catch2) 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 # Helper macro to create test executables
macro(add_topictree_test test_name) macro(add_topictree_test test_name)
add_executable(${test_name} ${test_name}.cpp) add_executable(${test_name} ${test_name}.cpp)
@ -17,7 +28,14 @@ macro(add_topictree_test test_name)
Catch2::Catch2WithMain Catch2::Catch2WithMain
) )
target_compile_features(${test_name} PRIVATE cxx_std_17) 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() endmacro()
# Add all scenario tests # Add all scenario tests

File diff suppressed because it is too large Load Diff

View File

@ -1,267 +1,270 @@
#include "grove/ModuleLoader.h" #include "grove/ModuleLoader.h"
#include "grove/SequentialModuleSystem.h" #include "grove/SequentialModuleSystem.h"
#include "grove/JsonDataNode.h" #include "grove/JsonDataNode.h"
#include "../helpers/TestMetrics.h" #include "../helpers/TestMetrics.h"
#include "../helpers/TestAssertions.h" #include "../helpers/TestAssertions.h"
#include "../helpers/TestReporter.h" #include "../helpers/TestReporter.h"
#include "../helpers/SystemUtils.h" #include "../helpers/SystemUtils.h"
#include <iostream> #include <iostream>
#include <chrono> #include <chrono>
#include <thread> #include <thread>
#include <fstream> #include <fstream>
#include <regex> #include <regex>
using namespace grove; using namespace grove;
int main() { int main() {
TestReporter reporter("Production Hot-Reload"); TestReporter reporter("Production Hot-Reload");
TestMetrics metrics; TestMetrics metrics;
std::cout << "================================================================================\n"; std::cout << "================================================================================\n";
std::cout << "TEST: Production Hot-Reload\n"; std::cout << "TEST: Production Hot-Reload\n";
std::cout << "================================================================================\n\n"; std::cout << "================================================================================\n\n";
// === SETUP === // === SETUP ===
std::cout << "Setup: Loading TankModule...\n"; std::cout << "Setup: Loading TankModule...\n";
ModuleLoader loader; ModuleLoader loader;
auto moduleSystem = std::make_unique<SequentialModuleSystem>(); auto moduleSystem = std::make_unique<SequentialModuleSystem>();
// Charger module // Charger module
std::string modulePath = "./libTankModule.so"; std::string modulePath = "./libTankModule.so";
auto module = loader.load(modulePath, "TankModule", false); #ifdef _WIN32
modulePath = "./libTankModule.dll";
// Config #endif
nlohmann::json configJson; auto module = loader.load(modulePath, "TankModule", false);
configJson["tankCount"] = 50;
configJson["version"] = "v1.0"; // Config
auto config = std::make_unique<JsonDataNode>("config", configJson); nlohmann::json configJson;
configJson["tankCount"] = 50;
// Initialiser (setConfiguration) configJson["version"] = "v1.0";
module->setConfiguration(*config, nullptr, nullptr); auto config = std::make_unique<JsonDataNode>("config", configJson);
// Enregistrer dans system // Initialiser (setConfiguration)
moduleSystem->registerModule("TankModule", std::move(module)); module->setConfiguration(*config, nullptr, nullptr);
std::cout << " ✓ Module loaded and initialized\n\n"; // Enregistrer dans system
moduleSystem->registerModule("TankModule", std::move(module));
// === PHASE 1: Pre-Reload (15s = 900 frames) ===
std::cout << "Phase 1: Running 15s before reload...\n"; std::cout << " ✓ Module loaded and initialized\n\n";
// Créer input avec deltaTime // === PHASE 1: Pre-Reload (15s = 900 frames) ===
nlohmann::json inputJson; std::cout << "Phase 1: Running 15s before reload...\n";
inputJson["deltaTime"] = 1.0 / 60.0;
auto inputNode = std::make_unique<JsonDataNode>("input", inputJson); // Créer input avec deltaTime
nlohmann::json inputJson;
for (int frame = 0; frame < 900; frame++) { inputJson["deltaTime"] = 1.0 / 60.0;
auto frameStart = std::chrono::high_resolution_clock::now(); auto inputNode = std::make_unique<JsonDataNode>("input", inputJson);
moduleSystem->processModules(1.0f / 60.0f); for (int frame = 0; frame < 900; frame++) {
auto frameStart = std::chrono::high_resolution_clock::now();
auto frameEnd = std::chrono::high_resolution_clock::now();
float frameTime = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count(); moduleSystem->processModules(1.0f / 60.0f);
metrics.recordFPS(1000.0f / frameTime); auto frameEnd = std::chrono::high_resolution_clock::now();
float frameTime = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count();
if (frame % 60 == 0) {
metrics.recordMemoryUsage(getCurrentMemoryUsage()); metrics.recordFPS(1000.0f / frameTime);
}
if (frame % 60 == 0) {
if (frame % 300 == 0) { metrics.recordMemoryUsage(getCurrentMemoryUsage());
std::cout << " Frame " << frame << "/900\n"; }
}
} if (frame % 300 == 0) {
std::cout << " Frame " << frame << "/900\n";
// Snapshot state AVANT reload }
auto tankModule = moduleSystem->extractModule(); }
auto preReloadState = tankModule->getState();
// Snapshot state AVANT reload
// Cast to JsonDataNode to access JSON auto tankModule = moduleSystem->extractModule();
auto* jsonNodeBefore = dynamic_cast<JsonDataNode*>(preReloadState.get()); auto preReloadState = tankModule->getState();
if (!jsonNodeBefore) {
std::cerr << "❌ Failed to cast state to JsonDataNode\n"; // Cast to JsonDataNode to access JSON
return 1; auto* jsonNodeBefore = dynamic_cast<JsonDataNode*>(preReloadState.get());
} if (!jsonNodeBefore) {
std::cerr << "❌ Failed to cast state to JsonDataNode\n";
const auto& stateJsonBefore = jsonNodeBefore->getJsonData(); return 1;
}
int tankCountBefore = stateJsonBefore["tanks"].size();
std::string versionBefore = stateJsonBefore.value("version", "unknown"); const auto& stateJsonBefore = jsonNodeBefore->getJsonData();
int frameCountBefore = stateJsonBefore.value("frameCount", 0);
int tankCountBefore = stateJsonBefore["tanks"].size();
std::cout << "\nState snapshot BEFORE reload:\n"; std::string versionBefore = stateJsonBefore.value("version", "unknown");
std::cout << " Version: " << versionBefore << "\n"; int frameCountBefore = stateJsonBefore.value("frameCount", 0);
std::cout << " Tank count: " << tankCountBefore << "\n";
std::cout << " Frame: " << frameCountBefore << "\n\n"; std::cout << "\nState snapshot BEFORE reload:\n";
std::cout << " Version: " << versionBefore << "\n";
ASSERT_EQ(tankCountBefore, 50, "Should have 50 tanks before reload"); std::cout << " Tank count: " << tankCountBefore << "\n";
std::cout << " Frame: " << frameCountBefore << "\n\n";
// Ré-enregistrer le module temporairement
moduleSystem->registerModule("TankModule", std::move(tankModule)); ASSERT_EQ(tankCountBefore, 50, "Should have 50 tanks before reload");
// === HOT-RELOAD === // Ré-enregistrer le module temporairement
std::cout << "Triggering hot-reload...\n"; moduleSystem->registerModule("TankModule", std::move(tankModule));
// Modifier version dans source (HEADER) // === HOT-RELOAD ===
std::cout << " 1. Modifying source code (v1.0 -> v2.0 HOT-RELOADED)...\n"; std::cout << "Triggering hot-reload...\n";
// Test runs from build/tests/, so source files are at ../../tests/modules/ // Modifier version dans source (HEADER)
std::ifstream input("../../tests/modules/TankModule.h"); std::cout << " 1. Modifying source code (v1.0 -> v2.0 HOT-RELOADED)...\n";
std::string content((std::istreambuf_iterator<char>(input)), std::istreambuf_iterator<char>());
input.close(); // Test runs from build/tests/, so source files are at ../../tests/modules/
std::ifstream input("../../tests/modules/TankModule.h");
size_t pos = content.find("std::string moduleVersion = \"v1.0\";"); std::string content((std::istreambuf_iterator<char>(input)), std::istreambuf_iterator<char>());
if (pos != std::string::npos) { input.close();
content.replace(pos, 39, "std::string moduleVersion = \"v2.0 HOT-RELOADED\";");
} size_t pos = content.find("std::string moduleVersion = \"v1.0\";");
if (pos != std::string::npos) {
std::ofstream output("../../tests/modules/TankModule.h"); content.replace(pos, 39, "std::string moduleVersion = \"v2.0 HOT-RELOADED\";");
output << content; }
output.close();
std::ofstream output("../../tests/modules/TankModule.h");
// Recompiler output << content;
std::cout << " 2. Recompiling module...\n"; output.close();
// 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"); // Recompiler
if (buildResult != 0) { std::cout << " 2. Recompiling module...\n";
std::cerr << "❌ Compilation failed!\n"; // Note: This test runs from build/tests/, so we use make -C .. to build from build directory
return 1; int buildResult = system("make -C .. TankModule 2>&1 > /dev/null");
} if (buildResult != 0) {
std::cout << " ✓ Compilation succeeded\n"; std::cerr << "❌ Compilation failed!\n";
return 1;
// Wait for file to be ready (simulate file stability check) }
std::this_thread::sleep_for(std::chrono::milliseconds(500)); std::cout << " ✓ Compilation succeeded\n";
// Reload // Wait for file to be ready (simulate file stability check)
std::cout << " 3. Reloading module...\n"; std::this_thread::sleep_for(std::chrono::milliseconds(500));
auto reloadStart = std::chrono::high_resolution_clock::now();
// Reload
// Extract module from system std::cout << " 3. Reloading module...\n";
tankModule = moduleSystem->extractModule(); auto reloadStart = std::chrono::high_resolution_clock::now();
// Use ModuleLoader::reload() // Extract module from system
auto newModule = loader.reload(std::move(tankModule)); tankModule = moduleSystem->extractModule();
// Re-register // Use ModuleLoader::reload()
moduleSystem->registerModule("TankModule", std::move(newModule)); auto newModule = loader.reload(std::move(tankModule));
auto reloadEnd = std::chrono::high_resolution_clock::now(); // Re-register
float reloadTime = std::chrono::duration<float, std::milli>(reloadEnd - reloadStart).count(); moduleSystem->registerModule("TankModule", std::move(newModule));
metrics.recordReloadTime(reloadTime); auto reloadEnd = std::chrono::high_resolution_clock::now();
reporter.addMetric("reload_time_ms", reloadTime); float reloadTime = std::chrono::duration<float, std::milli>(reloadEnd - reloadStart).count();
std::cout << " ✓ Reload completed in " << reloadTime << "ms\n\n"; metrics.recordReloadTime(reloadTime);
reporter.addMetric("reload_time_ms", reloadTime);
// === VÉRIFICATIONS POST-RELOAD ===
std::cout << "Verifying state preservation...\n"; std::cout << " ✓ Reload completed in " << reloadTime << "ms\n\n";
tankModule = moduleSystem->extractModule(); // === VÉRIFICATIONS POST-RELOAD ===
auto postReloadState = tankModule->getState(); std::cout << "Verifying state preservation...\n";
auto* jsonNodeAfter = dynamic_cast<JsonDataNode*>(postReloadState.get());
tankModule = moduleSystem->extractModule();
if (!jsonNodeAfter) { auto postReloadState = tankModule->getState();
std::cerr << "❌ Failed to cast post-reload state to JsonDataNode\n"; auto* jsonNodeAfter = dynamic_cast<JsonDataNode*>(postReloadState.get());
return 1;
} if (!jsonNodeAfter) {
std::cerr << "❌ Failed to cast post-reload state to JsonDataNode\n";
const auto& stateJsonAfter = jsonNodeAfter->getJsonData(); return 1;
}
int tankCountAfter = stateJsonAfter["tanks"].size();
std::string versionAfter = stateJsonAfter.value("version", "unknown"); const auto& stateJsonAfter = jsonNodeAfter->getJsonData();
int frameCountAfter = stateJsonAfter.value("frameCount", 0);
int tankCountAfter = stateJsonAfter["tanks"].size();
std::cout << "\nState snapshot AFTER reload:\n"; std::string versionAfter = stateJsonAfter.value("version", "unknown");
std::cout << " Version: " << versionAfter << "\n"; int frameCountAfter = stateJsonAfter.value("frameCount", 0);
std::cout << " Tank count: " << tankCountAfter << "\n";
std::cout << " Frame: " << frameCountAfter << "\n\n"; std::cout << "\nState snapshot AFTER reload:\n";
std::cout << " Version: " << versionAfter << "\n";
// Vérification 1: Nombre de tanks std::cout << " Tank count: " << tankCountAfter << "\n";
ASSERT_EQ(tankCountAfter, 50, "Should still have 50 tanks after reload"); std::cout << " Frame: " << frameCountAfter << "\n\n";
reporter.addAssertion("tank_count_preserved", tankCountAfter == 50);
// Vérification 1: Nombre de tanks
// Vérification 2: Version mise à jour ASSERT_EQ(tankCountAfter, 50, "Should still have 50 tanks after reload");
bool versionUpdated = versionAfter.find("v2.0") != std::string::npos; reporter.addAssertion("tank_count_preserved", tankCountAfter == 50);
ASSERT_TRUE(versionUpdated, "Version should be updated to v2.0");
reporter.addAssertion("version_updated", versionUpdated); // Vérification 2: Version mise à jour
bool versionUpdated = versionAfter.find("v2.0") != std::string::npos;
// Vérification 3: Frame count préservé ASSERT_TRUE(versionUpdated, "Version should be updated to v2.0");
ASSERT_EQ(frameCountAfter, frameCountBefore, "Frame count should be preserved"); reporter.addAssertion("version_updated", versionUpdated);
reporter.addAssertion("framecount_preserved", frameCountAfter == frameCountBefore);
// Vérification 3: Frame count préservé
std::cout << " ✓ State preserved correctly\n"; ASSERT_EQ(frameCountAfter, frameCountBefore, "Frame count should be preserved");
reporter.addAssertion("framecount_preserved", frameCountAfter == frameCountBefore);
// Ré-enregistrer module
moduleSystem->registerModule("TankModule", std::move(tankModule)); std::cout << " ✓ State preserved correctly\n";
// === PHASE 2: Post-Reload (15s = 900 frames) === // Ré-enregistrer module
std::cout << "\nPhase 2: Running 15s after reload...\n"; moduleSystem->registerModule("TankModule", std::move(tankModule));
for (int frame = 0; frame < 900; frame++) { // === PHASE 2: Post-Reload (15s = 900 frames) ===
auto frameStart = std::chrono::high_resolution_clock::now(); std::cout << "\nPhase 2: Running 15s after reload...\n";
moduleSystem->processModules(1.0f / 60.0f); for (int frame = 0; frame < 900; frame++) {
auto frameStart = std::chrono::high_resolution_clock::now();
auto frameEnd = std::chrono::high_resolution_clock::now();
float frameTime = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count(); moduleSystem->processModules(1.0f / 60.0f);
metrics.recordFPS(1000.0f / frameTime); auto frameEnd = std::chrono::high_resolution_clock::now();
float frameTime = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count();
if (frame % 60 == 0) {
metrics.recordMemoryUsage(getCurrentMemoryUsage()); metrics.recordFPS(1000.0f / frameTime);
}
if (frame % 60 == 0) {
if (frame % 300 == 0) { metrics.recordMemoryUsage(getCurrentMemoryUsage());
std::cout << " Frame " << frame << "/900\n"; }
}
} if (frame % 300 == 0) {
std::cout << " Frame " << frame << "/900\n";
// === VÉRIFICATIONS FINALES === }
std::cout << "\nFinal verifications...\n"; }
// Memory growth // === VÉRIFICATIONS FINALES ===
size_t memGrowth = metrics.getMemoryGrowth(); std::cout << "\nFinal verifications...\n";
float memGrowthMB = memGrowth / (1024.0f * 1024.0f);
ASSERT_LT(memGrowthMB, 5.0f, "Memory growth should be < 5MB"); // Memory growth
reporter.addMetric("memory_growth_mb", memGrowthMB); size_t memGrowth = metrics.getMemoryGrowth();
float memGrowthMB = memGrowth / (1024.0f * 1024.0f);
// FPS ASSERT_LT(memGrowthMB, 5.0f, "Memory growth should be < 5MB");
float minFPS = metrics.getFPSMin(); reporter.addMetric("memory_growth_mb", memGrowthMB);
ASSERT_GT(minFPS, 30.0f, "Min FPS should be > 30");
reporter.addMetric("fps_min", minFPS); // FPS
reporter.addMetric("fps_avg", metrics.getFPSAvg()); float minFPS = metrics.getFPSMin();
reporter.addMetric("fps_max", metrics.getFPSMax()); ASSERT_GT(minFPS, 30.0f, "Min FPS should be > 30");
reporter.addMetric("fps_min", minFPS);
// Reload time reporter.addMetric("fps_avg", metrics.getFPSAvg());
ASSERT_LT(reloadTime, 1000.0f, "Reload time should be < 1000ms"); reporter.addMetric("fps_max", metrics.getFPSMax());
// No crashes // Reload time
reporter.addAssertion("no_crashes", true); ASSERT_LT(reloadTime, 1000.0f, "Reload time should be < 1000ms");
// === CLEANUP === // No crashes
std::cout << "\nCleaning up...\n"; reporter.addAssertion("no_crashes", true);
// Restaurer version originale (HEADER) // === CLEANUP ===
std::ifstream inputRestore("../../tests/modules/TankModule.h"); std::cout << "\nCleaning up...\n";
std::string contentRestore((std::istreambuf_iterator<char>(inputRestore)), std::istreambuf_iterator<char>());
inputRestore.close(); // Restaurer version originale (HEADER)
std::ifstream inputRestore("../../tests/modules/TankModule.h");
pos = contentRestore.find("std::string moduleVersion = \"v2.0 HOT-RELOADED\";"); std::string contentRestore((std::istreambuf_iterator<char>(inputRestore)), std::istreambuf_iterator<char>());
if (pos != std::string::npos) { inputRestore.close();
contentRestore.replace(pos, 50, "std::string moduleVersion = \"v1.0\";");
} pos = contentRestore.find("std::string moduleVersion = \"v2.0 HOT-RELOADED\";");
if (pos != std::string::npos) {
std::ofstream outputRestore("../../tests/modules/TankModule.h"); contentRestore.replace(pos, 50, "std::string moduleVersion = \"v1.0\";");
outputRestore << contentRestore; }
outputRestore.close();
std::ofstream outputRestore("../../tests/modules/TankModule.h");
// Rebuild to restore original version (test runs from build/tests/) outputRestore << contentRestore;
system("make -C .. TankModule 2>&1 > /dev/null"); outputRestore.close();
// === RAPPORTS === // Rebuild to restore original version (test runs from build/tests/)
std::cout << "\n"; system("make -C .. TankModule 2>&1 > /dev/null");
metrics.printReport();
reporter.printFinalReport(); // === RAPPORTS ===
std::cout << "\n";
return reporter.getExitCode(); metrics.printReport();
} reporter.printFinalReport();
return reporter.getExitCode();
}

View File

@ -1,255 +1,258 @@
// PREUVE : Décommenter cette ligne pour désactiver la recovery et voir le test ÉCHOUER // PREUVE : Décommenter cette ligne pour désactiver la recovery et voir le test ÉCHOUER
// #define DISABLE_RECOVERY_FOR_TEST // #define DISABLE_RECOVERY_FOR_TEST
#include "grove/ModuleLoader.h" #include "grove/ModuleLoader.h"
#include "grove/SequentialModuleSystem.h" #include "grove/SequentialModuleSystem.h"
#include "grove/JsonDataNode.h" #include "grove/JsonDataNode.h"
#include "../helpers/TestMetrics.h" #include "../helpers/TestMetrics.h"
#include "../helpers/TestAssertions.h" #include "../helpers/TestAssertions.h"
#include "../helpers/TestReporter.h" #include "../helpers/TestReporter.h"
#include "../helpers/SystemUtils.h" #include "../helpers/SystemUtils.h"
#include <iostream> #include <iostream>
#include <chrono> #include <chrono>
#include <thread> #include <thread>
#include <csignal> #include <csignal>
#include <atomic> #include <atomic>
using namespace grove; using namespace grove;
// Global for crash detection // Global for crash detection
static std::atomic<bool> engineCrashed{false}; static std::atomic<bool> engineCrashed{false};
void signalHandler(int signal) { void signalHandler(int signal) {
if (signal == SIGSEGV || signal == SIGABRT) { if (signal == SIGSEGV || signal == SIGABRT) {
engineCrashed.store(true); engineCrashed.store(true);
std::cerr << "❌ FATAL: Signal " << signal << " received (SIGSEGV or SIGABRT)\n"; std::cerr << "❌ FATAL: Signal " << signal << " received (SIGSEGV or SIGABRT)\n";
std::cerr << "Engine has crashed unrecoverably.\n"; std::cerr << "Engine has crashed unrecoverably.\n";
std::exit(1); std::exit(1);
} }
} }
int main() { int main() {
TestReporter reporter("Chaos Monkey"); TestReporter reporter("Chaos Monkey");
TestMetrics metrics; TestMetrics metrics;
std::cout << "================================================================================\n"; std::cout << "================================================================================\n";
std::cout << "TEST: Chaos Monkey\n"; std::cout << "TEST: Chaos Monkey\n";
std::cout << "================================================================================\n\n"; std::cout << "================================================================================\n\n";
// Setup signal handlers // Setup signal handlers
std::signal(SIGSEGV, signalHandler); std::signal(SIGSEGV, signalHandler);
std::signal(SIGABRT, signalHandler); std::signal(SIGABRT, signalHandler);
// === SETUP === // === SETUP ===
std::cout << "Setup: Loading ChaosModule...\n"; std::cout << "Setup: Loading ChaosModule...\n";
ModuleLoader loader; ModuleLoader loader;
auto moduleSystem = std::make_unique<SequentialModuleSystem>(); auto moduleSystem = std::make_unique<SequentialModuleSystem>();
// Load module // Load module
std::string modulePath = "./libChaosModule.so"; std::string modulePath = "./libChaosModule.so";
auto module = loader.load(modulePath, "ChaosModule", false); #ifdef _WIN32
modulePath = "./libChaosModule.dll";
// Configure module avec seed ALÉATOIRE basé sur le temps #endif
// Chaque run sera différent - VRAI chaos auto module = loader.load(modulePath, "ChaosModule", false);
unsigned int randomSeed = static_cast<unsigned int>(std::chrono::system_clock::now().time_since_epoch().count());
// Configure module avec seed ALÉATOIRE basé sur le temps
nlohmann::json configJson; // Chaque run sera différent - VRAI chaos
configJson["seed"] = randomSeed; unsigned int randomSeed = static_cast<unsigned int>(std::chrono::system_clock::now().time_since_epoch().count());
configJson["hotReloadProbability"] = 0.30; // Non utilisé maintenant
configJson["crashProbability"] = 0.05; // 5% par frame = crash fréquent nlohmann::json configJson;
configJson["corruptionProbability"] = 0.10; // Non utilisé configJson["seed"] = randomSeed;
configJson["invalidConfigProbability"] = 0.05; // Non utilisé configJson["hotReloadProbability"] = 0.30; // Non utilisé maintenant
auto config = std::make_unique<JsonDataNode>("config", configJson); configJson["crashProbability"] = 0.05; // 5% par frame = crash fréquent
configJson["corruptionProbability"] = 0.10; // Non utilisé
std::cout << " Random seed: " << randomSeed << " (time-based, unpredictable)\n"; configJson["invalidConfigProbability"] = 0.05; // Non utilisé
auto config = std::make_unique<JsonDataNode>("config", configJson);
module->setConfiguration(*config, nullptr, nullptr);
std::cout << " Random seed: " << randomSeed << " (time-based, unpredictable)\n";
// Register in module system
moduleSystem->registerModule("ChaosModule", std::move(module)); module->setConfiguration(*config, nullptr, nullptr);
std::cout << " ✓ ChaosModule loaded and configured\n\n"; // Register in module system
moduleSystem->registerModule("ChaosModule", std::move(module));
// === CHAOS LOOP (30 seconds = 1800 frames @ 60 FPS) ===
// NOTE: Reduced from 5 minutes for faster testing std::cout << " ✓ ChaosModule loaded and configured\n\n";
std::cout << "Starting Chaos Monkey (30 seconds simulation)...\n";
std::cout << "REAL CHAOS MODE:\n"; // === CHAOS LOOP (30 seconds = 1800 frames @ 60 FPS) ===
std::cout << " - 5% crash probability PER FRAME (not per second)\n"; // NOTE: Reduced from 5 minutes for faster testing
std::cout << " - Expected crashes: ~90 crashes (5% of 1800 frames)\n"; std::cout << "Starting Chaos Monkey (30 seconds simulation)...\n";
std::cout << " - Random seed (time-based): unpredictable pattern\n"; std::cout << "REAL CHAOS MODE:\n";
std::cout << " - Multiple crash types: runtime_error, logic_error, out_of_range, domain_error, state corruption\n"; std::cout << " - 5% crash probability PER FRAME (not per second)\n";
std::cout << " - Corrupted state validation: module must reject corrupted state\n\n"; std::cout << " - Expected crashes: ~90 crashes (5% of 1800 frames)\n";
std::cout << " - Random seed (time-based): unpredictable pattern\n";
const int totalFrames = 1800; // 30 * 60 std::cout << " - Multiple crash types: runtime_error, logic_error, out_of_range, domain_error, state corruption\n";
int crashesDetected = 0; std::cout << " - Corrupted state validation: module must reject corrupted state\n\n";
int reloadsTriggered = 0;
int recoverySuccesses = 0; const int totalFrames = 1800; // 30 * 60
bool hadDeadlock = false; int crashesDetected = 0;
int reloadsTriggered = 0;
auto testStart = std::chrono::high_resolution_clock::now(); int recoverySuccesses = 0;
bool hadDeadlock = false;
for (int frame = 0; frame < totalFrames; frame++) {
auto frameStart = std::chrono::high_resolution_clock::now(); auto testStart = std::chrono::high_resolution_clock::now();
bool didRecoveryThisFrame = false;
for (int frame = 0; frame < totalFrames; frame++) {
try { auto frameStart = std::chrono::high_resolution_clock::now();
// Process module (1/60th of a second) bool didRecoveryThisFrame = false;
moduleSystem->processModules(1.0f / 60.0f);
try {
} catch (const std::exception& e) { // Process module (1/60th of a second)
// CRASH DETECTED - Attempt recovery moduleSystem->processModules(1.0f / 60.0f);
crashesDetected++;
std::cout << " [Frame " << frame << "] ⚠️ Crash detected: " << e.what() << "\n"; } catch (const std::exception& e) {
// CRASH DETECTED - Attempt recovery
// PREUVE QUE LE TEST PEUT ÉCHOUER : désactiver la recovery crashesDetected++;
#ifdef DISABLE_RECOVERY_FOR_TEST std::cout << " [Frame " << frame << "] ⚠️ Crash detected: " << e.what() << "\n";
std::cout << " [Frame " << frame << "] ❌ RECOVERY DISABLED - Test will fail\n";
reporter.addAssertion("recovery_disabled", false); // PREUVE QUE LE TEST PEUT ÉCHOUER : désactiver la recovery
break; // Le test DOIT échouer #ifdef DISABLE_RECOVERY_FOR_TEST
#endif std::cout << " [Frame " << frame << "] ❌ RECOVERY DISABLED - Test will fail\n";
reporter.addAssertion("recovery_disabled", false);
// Recovery attempt break; // Le test DOIT échouer
try { #endif
std::cout << " [Frame " << frame << "] 🔄 Attempting recovery...\n";
// Recovery attempt
auto recoveryStart = std::chrono::high_resolution_clock::now(); try {
std::cout << " [Frame " << frame << "] 🔄 Attempting recovery...\n";
// Extract module from system
auto crashedModule = moduleSystem->extractModule(); auto recoveryStart = std::chrono::high_resolution_clock::now();
// Reload module // Extract module from system
auto reloadedModule = loader.reload(std::move(crashedModule)); auto crashedModule = moduleSystem->extractModule();
// Re-register // Reload module
moduleSystem->registerModule("ChaosModule", std::move(reloadedModule)); auto reloadedModule = loader.reload(std::move(crashedModule));
auto recoveryEnd = std::chrono::high_resolution_clock::now(); // Re-register
float recoveryTime = std::chrono::duration<float, std::milli>(recoveryEnd - recoveryStart).count(); moduleSystem->registerModule("ChaosModule", std::move(reloadedModule));
metrics.recordReloadTime(recoveryTime); auto recoveryEnd = std::chrono::high_resolution_clock::now();
recoverySuccesses++; float recoveryTime = std::chrono::duration<float, std::milli>(recoveryEnd - recoveryStart).count();
didRecoveryThisFrame = true;
metrics.recordReloadTime(recoveryTime);
std::cout << " [Frame " << frame << "] ✅ Recovery successful (" << recoveryTime << "ms)\n"; recoverySuccesses++;
didRecoveryThisFrame = true;
} catch (const std::exception& recoveryError) {
std::cout << " [Frame " << frame << "] ❌ Recovery FAILED: " << recoveryError.what() << "\n"; std::cout << " [Frame " << frame << "] ✅ Recovery successful (" << recoveryTime << "ms)\n";
reporter.addAssertion("recovery_failed", false);
break; // Stop test - recovery failed } 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();
// Metrics
// Only record FPS for normal frames (not recovery frames) auto frameEnd = std::chrono::high_resolution_clock::now();
// Recovery frames are slow by design (100+ ms for hot-reload) float frameTime = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count();
if (!didRecoveryThisFrame) {
metrics.recordFPS(1000.0f / frameTime); // Only record FPS for normal frames (not recovery frames)
} // Recovery frames are slow by design (100+ ms for hot-reload)
if (!didRecoveryThisFrame) {
if (frame % 60 == 0) { metrics.recordFPS(1000.0f / frameTime);
metrics.recordMemoryUsage(getCurrentMemoryUsage()); }
}
if (frame % 60 == 0) {
// Deadlock detection (frame > 100ms) metrics.recordMemoryUsage(getCurrentMemoryUsage());
// 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"; // Deadlock detection (frame > 100ms)
hadDeadlock = true; // 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";
// Progress (every 600 frames = 10 seconds) hadDeadlock = true;
if (frame % 600 == 0 && frame > 0) { }
float elapsedSec = frame / 60.0f;
float progress = (frame * 100.0f) / totalFrames; // Progress (every 600 frames = 10 seconds)
std::cout << "Progress: " << elapsedSec << "/30.0 seconds (" << (int)progress << "%)\n"; if (frame % 600 == 0 && frame > 0) {
float elapsedSec = frame / 60.0f;
// Show current metrics float progress = (frame * 100.0f) / totalFrames;
std::cout << " FPS: min=" << metrics.getFPSMin() << ", avg=" << metrics.getFPSAvg() << ", max=" << metrics.getFPSMax() << "\n"; std::cout << "Progress: " << elapsedSec << "/30.0 seconds (" << (int)progress << "%)\n";
std::cout << " Memory: " << (getCurrentMemoryUsage() / (1024.0f * 1024.0f)) << " MB\n";
} // Show current metrics
std::cout << " FPS: min=" << metrics.getFPSMin() << ", avg=" << metrics.getFPSAvg() << ", max=" << metrics.getFPSMax() << "\n";
// Check if engine crashed externally std::cout << " Memory: " << (getCurrentMemoryUsage() / (1024.0f * 1024.0f)) << " MB\n";
if (engineCrashed.load()) { }
std::cout << " [Frame " << frame << "] ❌ Engine crashed externally (signal received)\n";
reporter.addAssertion("engine_crashed_externally", false); // Check if engine crashed externally
break; 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"; auto testEnd = std::chrono::high_resolution_clock::now();
float totalDuration = std::chrono::duration<float>(testEnd - testStart).count();
// === FINAL VERIFICATIONS ===
std::cout << "Final verifications...\n"; std::cout << "\nTest completed!\n\n";
// Engine still alive // === FINAL VERIFICATIONS ===
bool engineAlive = !engineCrashed.load(); std::cout << "Final verifications...\n";
ASSERT_TRUE(engineAlive, "Engine should still be alive");
reporter.addAssertion("engine_alive", engineAlive); // Engine still alive
bool engineAlive = !engineCrashed.load();
// No deadlocks ASSERT_TRUE(engineAlive, "Engine should still be alive");
ASSERT_FALSE(hadDeadlock, "Should not have deadlocks"); reporter.addAssertion("engine_alive", engineAlive);
reporter.addAssertion("no_deadlocks", !hadDeadlock);
// No deadlocks
// Memory growth < 10MB ASSERT_FALSE(hadDeadlock, "Should not have deadlocks");
size_t memGrowth = metrics.getMemoryGrowth(); reporter.addAssertion("no_deadlocks", !hadDeadlock);
float memGrowthMB = memGrowth / (1024.0f * 1024.0f);
ASSERT_LT(memGrowthMB, 10.0f, "Memory growth should be < 10MB"); // Memory growth < 10MB
reporter.addMetric("memory_growth_mb", memGrowthMB); size_t memGrowth = metrics.getMemoryGrowth();
float memGrowthMB = memGrowth / (1024.0f * 1024.0f);
// Test runs as fast as possible (not real-time) ASSERT_LT(memGrowthMB, 10.0f, "Memory growth should be < 10MB");
// Just check it completed within reasonable bounds (< 60 seconds wall time) reporter.addMetric("memory_growth_mb", memGrowthMB);
ASSERT_LT(totalDuration, 60.0f, "Total duration should be < 60 seconds");
reporter.addMetric("total_duration_sec", totalDuration); // Test runs as fast as possible (not real-time)
// Just check it completed within reasonable bounds (< 60 seconds wall time)
// FPS metrics ASSERT_LT(totalDuration, 60.0f, "Total duration should be < 60 seconds");
float minFPS = metrics.getFPSMin(); reporter.addMetric("total_duration_sec", totalDuration);
float avgFPS = metrics.getFPSAvg();
float maxFPS = metrics.getFPSMax(); // FPS metrics
float minFPS = metrics.getFPSMin();
// Min FPS should be reasonable (> 10 even with crashes) float avgFPS = metrics.getFPSAvg();
ASSERT_GT(minFPS, 10.0f, "Min FPS should be > 10"); float maxFPS = metrics.getFPSMax();
reporter.addMetric("fps_min", minFPS);
reporter.addMetric("fps_avg", avgFPS); // Min FPS should be reasonable (> 10 even with crashes)
reporter.addMetric("fps_max", maxFPS); ASSERT_GT(minFPS, 10.0f, "Min FPS should be > 10");
reporter.addMetric("fps_min", minFPS);
// Recovery rate > 95% reporter.addMetric("fps_avg", avgFPS);
float recoveryRate = (crashesDetected > 0) ? (recoverySuccesses * 100.0f / crashesDetected) : 100.0f; reporter.addMetric("fps_max", maxFPS);
ASSERT_GT(recoveryRate, 95.0f, "Recovery rate should be > 95%");
reporter.addMetric("recovery_rate_percent", recoveryRate); // Recovery rate > 95%
float recoveryRate = (crashesDetected > 0) ? (recoverySuccesses * 100.0f / crashesDetected) : 100.0f;
// === STATISTICS === ASSERT_GT(recoveryRate, 95.0f, "Recovery rate should be > 95%");
std::cout << "\n"; reporter.addMetric("recovery_rate_percent", recoveryRate);
std::cout << "================================================================================\n";
std::cout << "CHAOS MONKEY STATISTICS\n"; // === STATISTICS ===
std::cout << "================================================================================\n"; std::cout << "\n";
std::cout << " Total frames: " << totalFrames << "\n"; std::cout << "================================================================================\n";
std::cout << " Duration: " << totalDuration << "s (wall time, not simulation time)\n"; std::cout << "CHAOS MONKEY STATISTICS\n";
std::cout << " Crashes detected: " << crashesDetected << "\n"; std::cout << "================================================================================\n";
std::cout << " Recovery successes: " << recoverySuccesses << "\n"; std::cout << " Total frames: " << totalFrames << "\n";
std::cout << " Recovery rate: " << recoveryRate << "%\n"; std::cout << " Duration: " << totalDuration << "s (wall time, not simulation time)\n";
std::cout << " Memory growth: " << memGrowthMB << " MB (max: 10MB)\n"; std::cout << " Crashes detected: " << crashesDetected << "\n";
std::cout << " Had deadlocks: " << (hadDeadlock ? "YES ❌" : "NO ✅") << "\n"; std::cout << " Recovery successes: " << recoverySuccesses << "\n";
std::cout << " FPS min/avg/max: " << minFPS << " / " << avgFPS << " / " << maxFPS << "\n"; std::cout << " Recovery rate: " << recoveryRate << "%\n";
std::cout << "================================================================================\n\n"; std::cout << " Memory growth: " << memGrowthMB << " MB (max: 10MB)\n";
std::cout << " Had deadlocks: " << (hadDeadlock ? "YES ❌" : "NO ✅") << "\n";
std::cout << "Note: ChaosModule generates random crashes internally.\n"; std::cout << " FPS min/avg/max: " << minFPS << " / " << avgFPS << " / " << maxFPS << "\n";
std::cout << "The test should recover from ALL crashes automatically via hot-reload.\n\n"; std::cout << "================================================================================\n\n";
// === CLEANUP === std::cout << "Note: ChaosModule generates random crashes internally.\n";
std::cout << "Cleaning up...\n"; std::cout << "The test should recover from ALL crashes automatically via hot-reload.\n\n";
moduleSystem.reset();
std::cout << " ✓ Module system shutdown complete\n\n"; // === CLEANUP ===
std::cout << "Cleaning up...\n";
// === REPORTS === moduleSystem.reset();
metrics.printReport(); std::cout << " ✓ Module system shutdown complete\n\n";
reporter.printFinalReport();
// === REPORTS ===
return reporter.getExitCode(); metrics.printReport();
} reporter.printFinalReport();
return reporter.getExitCode();
}

View File

@ -1,247 +1,253 @@
/** /**
* @file test_03_stress_test.cpp * @file test_03_stress_test.cpp
* @brief Scenario 3: Stress Test - Long-duration stability validation * @brief Scenario 3: Stress Test - Long-duration stability validation
* *
* OBJECTIVE: * OBJECTIVE:
* Validate hot-reload system stability over extended duration with repeated reloads. * Validate hot-reload system stability over extended duration with repeated reloads.
* *
* TEST PARAMETERS: * TEST PARAMETERS:
* - Duration: 10 minutes (36000 frames @ 60 FPS) * - Duration: 10 minutes (36000 frames @ 60 FPS)
* - Reload frequency: Every 5 seconds (300 frames) * - Reload frequency: Every 5 seconds (300 frames)
* - Total reloads: 120 * - Total reloads: 120
* - No random crashes - focus on hot-reload stability * - No random crashes - focus on hot-reload stability
* *
* SUCCESS CRITERIA: * SUCCESS CRITERIA:
* All 120 reloads succeed * All 120 reloads succeed
* Memory growth < 50MB over 10 minutes * Memory growth < 50MB over 10 minutes
* Average reload time < 500ms * Average reload time < 500ms
* FPS remains stable (no degradation) * FPS remains stable (no degradation)
* No file descriptor leaks * No file descriptor leaks
* State preserved across all reloads * State preserved across all reloads
* *
* WHAT THIS VALIDATES: * WHAT THIS VALIDATES:
* - No memory leaks in hot-reload system * - No memory leaks in hot-reload system
* - No file descriptor leaks (dlopen/dlclose) * - No file descriptor leaks (dlopen/dlclose)
* - Reload performance doesn't degrade over time * - Reload performance doesn't degrade over time
* - State preservation is reliable at scale * - State preservation is reliable at scale
* - System remains stable under repeated reload stress * - System remains stable under repeated reload stress
*/ */
#include "grove/ModuleLoader.h" #include "grove/ModuleLoader.h"
#include "grove/SequentialModuleSystem.h" #include "grove/SequentialModuleSystem.h"
#include "grove/JsonDataNode.h" #include "grove/JsonDataNode.h"
#include "../helpers/TestMetrics.h" #include "../helpers/TestMetrics.h"
#include "../helpers/TestAssertions.h" #include "../helpers/TestAssertions.h"
#include "../helpers/TestReporter.h" #include "../helpers/TestReporter.h"
#include "../helpers/SystemUtils.h" #include "../helpers/SystemUtils.h"
#include <iostream> #include <iostream>
#include <chrono> #include <chrono>
#include <thread> #include <thread>
using namespace grove; using namespace grove;
// Test configuration // Test configuration
constexpr int TARGET_FPS = 60; constexpr int TARGET_FPS = 60;
constexpr float FRAME_TIME = 1.0f / TARGET_FPS; constexpr float FRAME_TIME = 1.0f / TARGET_FPS;
constexpr int RELOAD_INTERVAL = 300; // Reload every 5 seconds (300 frames) constexpr int RELOAD_INTERVAL = 300; // Reload every 5 seconds (300 frames)
constexpr int EXPECTED_RELOADS = 120; // 120 reloads constexpr int EXPECTED_RELOADS = 120; // 120 reloads
constexpr int TOTAL_FRAMES = EXPECTED_RELOADS * RELOAD_INTERVAL; // 36000 frames = 10 minutes @ 60 FPS constexpr int TOTAL_FRAMES = EXPECTED_RELOADS * RELOAD_INTERVAL; // 36000 frames = 10 minutes @ 60 FPS
// Memory threshold // Memory threshold
constexpr size_t MAX_MEMORY_GROWTH_MB = 50; constexpr size_t MAX_MEMORY_GROWTH_MB = 50;
// Paths // Paths
const std::string MODULE_PATH = "./libStressModule.so"; #ifdef _WIN32
const std::string MODULE_PATH = "./libStressModule.dll";
int main() { #else
TestReporter reporter("Stress Test - 10 Minute Stability"); const std::string MODULE_PATH = "./libStressModule.so";
TestMetrics metrics; #endif
#ifdef _WIN32
std::cout << "═══════════════════════════════════════════════════════════════\n"; #endif
std::cout << " SCENARIO 3: STRESS TEST - LONG DURATION STABILITY\n";
std::cout << "═══════════════════════════════════════════════════════════════\n"; int main() {
std::cout << "Duration: 10 minutes (" << TOTAL_FRAMES << " frames @ " << TARGET_FPS << " FPS)\n"; TestReporter reporter("Stress Test - 10 Minute Stability");
std::cout << "Reload interval: Every " << RELOAD_INTERVAL << " frames (5 seconds)\n"; TestMetrics metrics;
std::cout << "Expected reloads: " << EXPECTED_RELOADS << "\n";
std::cout << "Memory threshold: < " << MAX_MEMORY_GROWTH_MB << " MB growth\n"; std::cout << "═══════════════════════════════════════════════════════════════\n";
std::cout << "═══════════════════════════════════════════════════════════════\n\n"; std::cout << " SCENARIO 3: STRESS TEST - LONG DURATION STABILITY\n";
std::cout << "═══════════════════════════════════════════════════════════════\n";
size_t initialMemory = grove::getCurrentMemoryUsage() / (1024 * 1024); std::cout << "Duration: 10 minutes (" << TOTAL_FRAMES << " frames @ " << TARGET_FPS << " FPS)\n";
size_t peakMemory = initialMemory; std::cout << "Reload interval: Every " << RELOAD_INTERVAL << " frames (5 seconds)\n";
std::cout << "Expected reloads: " << EXPECTED_RELOADS << "\n";
int successfulReloads = 0; std::cout << "Memory threshold: < " << MAX_MEMORY_GROWTH_MB << " MB growth\n";
int failedReloads = 0; std::cout << "═══════════════════════════════════════════════════════════════\n\n";
try { size_t initialMemory = grove::getCurrentMemoryUsage() / (1024 * 1024);
// === SETUP === size_t peakMemory = initialMemory;
std::cout << "Setup: Loading StressModule...\n";
int successfulReloads = 0;
ModuleLoader loader; int failedReloads = 0;
auto moduleSystem = std::make_unique<SequentialModuleSystem>();
try {
// Load module // === SETUP ===
auto module = loader.load(MODULE_PATH, "StressModule", false); std::cout << "Setup: Loading StressModule...\n";
// Configure module with empty config ModuleLoader loader;
nlohmann::json configJson; auto moduleSystem = std::make_unique<SequentialModuleSystem>();
auto config = std::make_unique<JsonDataNode>("config", configJson);
// Load module
module->setConfiguration(*config, nullptr, nullptr); auto module = loader.load(MODULE_PATH, "StressModule", false);
// Register in module system // Configure module with empty config
moduleSystem->registerModule("StressModule", std::move(module)); nlohmann::json configJson;
auto config = std::make_unique<JsonDataNode>("config", configJson);
std::cout << " ✓ StressModule loaded and configured\n\n";
module->setConfiguration(*config, nullptr, nullptr);
std::cout << "🚀 Starting 10-minute stress test...\n\n";
// Register in module system
auto startTime = std::chrono::high_resolution_clock::now(); moduleSystem->registerModule("StressModule", std::move(module));
// Main simulation loop std::cout << " ✓ StressModule loaded and configured\n\n";
for (int frame = 1; frame <= TOTAL_FRAMES; ++frame) {
auto frameStart = std::chrono::high_resolution_clock::now(); std::cout << "🚀 Starting 10-minute stress test...\n\n";
// Process modules auto startTime = std::chrono::high_resolution_clock::now();
try {
moduleSystem->processModules(FRAME_TIME); // Main simulation loop
} catch (const std::exception& e) { for (int frame = 1; frame <= TOTAL_FRAMES; ++frame) {
std::cerr << " [Frame " << frame << "] ❌ Unexpected error during process: " << e.what() << "\n"; auto frameStart = std::chrono::high_resolution_clock::now();
reporter.addAssertion("process_error", false);
break; // Process modules
} try {
moduleSystem->processModules(FRAME_TIME);
auto frameEnd = std::chrono::high_resolution_clock::now(); } catch (const std::exception& e) {
auto frameDuration = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count(); std::cerr << " [Frame " << frame << "] ❌ Unexpected error during process: " << e.what() << "\n";
float fps = frameDuration > 0.0f ? 1000.0f / frameDuration : 0.0f; reporter.addAssertion("process_error", false);
metrics.recordFPS(fps); break;
}
// Hot-reload every RELOAD_INTERVAL frames
if (frame % RELOAD_INTERVAL == 0) { auto frameEnd = std::chrono::high_resolution_clock::now();
int reloadNumber = frame / RELOAD_INTERVAL; auto frameDuration = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count();
std::cout << " [Frame " << frame << "/" << TOTAL_FRAMES << "] 🔄 Triggering hot-reload #" << reloadNumber << "...\n"; float fps = frameDuration > 0.0f ? 1000.0f / frameDuration : 0.0f;
metrics.recordFPS(fps);
auto reloadStart = std::chrono::high_resolution_clock::now();
// Hot-reload every RELOAD_INTERVAL frames
try { if (frame % RELOAD_INTERVAL == 0) {
// Extract module from system int reloadNumber = frame / RELOAD_INTERVAL;
auto extractedModule = moduleSystem->extractModule(); std::cout << " [Frame " << frame << "/" << TOTAL_FRAMES << "] 🔄 Triggering hot-reload #" << reloadNumber << "...\n";
if (!extractedModule) {
std::cerr << " ❌ Failed to extract StressModule\n"; auto reloadStart = std::chrono::high_resolution_clock::now();
failedReloads++;
continue; try {
} // Extract module from system
auto extractedModule = moduleSystem->extractModule();
// Perform hot-reload if (!extractedModule) {
auto reloadedModule = loader.reload(std::move(extractedModule)); std::cerr << " ❌ Failed to extract StressModule\n";
failedReloads++;
// Re-register reloaded module continue;
moduleSystem->registerModule("StressModule", std::move(reloadedModule)); }
auto reloadEnd = std::chrono::high_resolution_clock::now(); // Perform hot-reload
auto reloadDuration = std::chrono::duration_cast<std::chrono::milliseconds>( auto reloadedModule = loader.reload(std::move(extractedModule));
reloadEnd - reloadStart).count();
// Re-register reloaded module
metrics.recordReloadTime(static_cast<float>(reloadDuration)); moduleSystem->registerModule("StressModule", std::move(reloadedModule));
successfulReloads++;
auto reloadEnd = std::chrono::high_resolution_clock::now();
std::cout << " ✅ Hot-reload #" << reloadNumber << " succeeded in " << reloadDuration << "ms\n"; auto reloadDuration = std::chrono::duration_cast<std::chrono::milliseconds>(
reloadEnd - reloadStart).count();
} catch (const std::exception& e) {
std::cerr << " ❌ Exception during hot-reload: " << e.what() << "\n"; metrics.recordReloadTime(static_cast<float>(reloadDuration));
failedReloads++; successfulReloads++;
}
} std::cout << " ✅ Hot-reload #" << reloadNumber << " succeeded in " << reloadDuration << "ms\n";
// Memory monitoring every 60 seconds (3600 frames) } catch (const std::exception& e) {
if (frame % 3600 == 0 && frame > 0) { std::cerr << " ❌ Exception during hot-reload: " << e.what() << "\n";
size_t currentMemory = grove::getCurrentMemoryUsage() / (1024 * 1024); failedReloads++;
size_t memoryGrowth = currentMemory - initialMemory; }
peakMemory = std::max(peakMemory, currentMemory); }
int minutesElapsed = frame / 3600; // Memory monitoring every 60 seconds (3600 frames)
std::cout << "\n📊 Checkpoint at " << minutesElapsed << " minute(s):\n"; if (frame % 3600 == 0 && frame > 0) {
std::cout << " Current memory: " << currentMemory << " MB\n"; size_t currentMemory = grove::getCurrentMemoryUsage() / (1024 * 1024);
std::cout << " Growth: " << memoryGrowth << " MB\n"; size_t memoryGrowth = currentMemory - initialMemory;
std::cout << " Peak: " << peakMemory << " MB\n"; peakMemory = std::max(peakMemory, currentMemory);
std::cout << " Avg FPS: " << metrics.getFPSAvg() << "\n";
std::cout << " Reloads: " << successfulReloads << "/" << EXPECTED_RELOADS << "\n"; int minutesElapsed = frame / 3600;
std::cout << " Avg reload time: " << metrics.getReloadTimeAvg() << "ms\n\n"; std::cout << "\n📊 Checkpoint at " << minutesElapsed << " minute(s):\n";
} std::cout << " Current memory: " << currentMemory << " MB\n";
std::cout << " Growth: " << memoryGrowth << " MB\n";
// Progress reporting every minute (without memory details) std::cout << " Peak: " << peakMemory << " MB\n";
if (frame % 3600 == 0 && frame > 0) { std::cout << " Avg FPS: " << metrics.getFPSAvg() << "\n";
int minutesElapsed = frame / 3600; std::cout << " Reloads: " << successfulReloads << "/" << EXPECTED_RELOADS << "\n";
int minutesRemaining = (TOTAL_FRAMES - frame) / 3600; std::cout << " Avg reload time: " << metrics.getReloadTimeAvg() << "ms\n\n";
std::cout << "⏱️ Progress: " << minutesElapsed << " minutes elapsed, " << minutesRemaining << " minutes remaining\n"; }
}
} // Progress reporting every minute (without memory details)
if (frame % 3600 == 0 && frame > 0) {
auto endTime = std::chrono::high_resolution_clock::now(); int minutesElapsed = frame / 3600;
auto totalDuration = std::chrono::duration_cast<std::chrono::seconds>( int minutesRemaining = (TOTAL_FRAMES - frame) / 3600;
endTime - startTime).count(); std::cout << "⏱️ Progress: " << minutesElapsed << " minutes elapsed, " << minutesRemaining << " minutes remaining\n";
}
// Final metrics }
size_t finalMemory = grove::getCurrentMemoryUsage() / (1024 * 1024);
size_t totalMemoryGrowth = finalMemory - initialMemory; auto endTime = std::chrono::high_resolution_clock::now();
auto totalDuration = std::chrono::duration_cast<std::chrono::seconds>(
std::cout << "\n═══════════════════════════════════════════════════════════════\n"; endTime - startTime).count();
std::cout << " STRESS TEST COMPLETED\n";
std::cout << "═══════════════════════════════════════════════════════════════\n"; // Final metrics
std::cout << "Total frames: " << TOTAL_FRAMES << "\n"; size_t finalMemory = grove::getCurrentMemoryUsage() / (1024 * 1024);
std::cout << "Real time: " << totalDuration << "s\n"; size_t totalMemoryGrowth = finalMemory - initialMemory;
std::cout << "Simulated time: " << (TOTAL_FRAMES / TARGET_FPS) << "s (10 minutes)\n";
std::cout << "Successful reloads: " << successfulReloads << "/" << EXPECTED_RELOADS << "\n"; std::cout << "\n═══════════════════════════════════════════════════════════════\n";
std::cout << "Failed reloads: " << failedReloads << "\n"; std::cout << " STRESS TEST COMPLETED\n";
std::cout << "\n📊 PERFORMANCE METRICS:\n"; std::cout << "═══════════════════════════════════════════════════════════════\n";
std::cout << "Average FPS: " << metrics.getFPSAvg() << "\n"; std::cout << "Total frames: " << TOTAL_FRAMES << "\n";
std::cout << "Min FPS: " << metrics.getFPSMin() << "\n"; std::cout << "Real time: " << totalDuration << "s\n";
std::cout << "Max FPS: " << metrics.getFPSMax() << "\n"; std::cout << "Simulated time: " << (TOTAL_FRAMES / TARGET_FPS) << "s (10 minutes)\n";
std::cout << "\n🔥 HOT-RELOAD METRICS:\n"; std::cout << "Successful reloads: " << successfulReloads << "/" << EXPECTED_RELOADS << "\n";
std::cout << "Avg reload time: " << metrics.getReloadTimeAvg() << "ms\n"; std::cout << "Failed reloads: " << failedReloads << "\n";
std::cout << "Min reload time: " << metrics.getReloadTimeMin() << "ms\n"; std::cout << "\n📊 PERFORMANCE METRICS:\n";
std::cout << "Max reload time: " << metrics.getReloadTimeMax() << "ms\n"; std::cout << "Average FPS: " << metrics.getFPSAvg() << "\n";
std::cout << "\n💾 MEMORY METRICS:\n"; std::cout << "Min FPS: " << metrics.getFPSMin() << "\n";
std::cout << "Initial memory: " << initialMemory << " MB\n"; std::cout << "Max FPS: " << metrics.getFPSMax() << "\n";
std::cout << "Final memory: " << finalMemory << " MB\n"; std::cout << "\n🔥 HOT-RELOAD METRICS:\n";
std::cout << "Peak memory: " << peakMemory << " MB\n"; std::cout << "Avg reload time: " << metrics.getReloadTimeAvg() << "ms\n";
std::cout << "Total growth: " << totalMemoryGrowth << " MB\n"; std::cout << "Min reload time: " << metrics.getReloadTimeMin() << "ms\n";
std::cout << "═══════════════════════════════════════════════════════════════\n\n"; std::cout << "Max reload time: " << metrics.getReloadTimeMax() << "ms\n";
std::cout << "\n💾 MEMORY METRICS:\n";
// Validate results std::cout << "Initial memory: " << initialMemory << " MB\n";
bool allReloadsSucceeded = (successfulReloads == EXPECTED_RELOADS && failedReloads == 0); std::cout << "Final memory: " << finalMemory << " MB\n";
bool memoryWithinThreshold = (totalMemoryGrowth < MAX_MEMORY_GROWTH_MB); std::cout << "Peak memory: " << peakMemory << " MB\n";
bool avgReloadTimeAcceptable = (metrics.getReloadTimeAvg() < 500.0f); std::cout << "Total growth: " << totalMemoryGrowth << " MB\n";
bool fpsStable = (metrics.getFPSMin() > 30.0f); // Ensure FPS doesn't drop too much std::cout << "═══════════════════════════════════════════════════════════════\n\n";
reporter.addAssertion("all_reloads_succeeded", allReloadsSucceeded); // Validate results
reporter.addAssertion("memory_within_threshold", memoryWithinThreshold); bool allReloadsSucceeded = (successfulReloads == EXPECTED_RELOADS && failedReloads == 0);
reporter.addAssertion("avg_reload_time_acceptable", avgReloadTimeAcceptable); bool memoryWithinThreshold = (totalMemoryGrowth < MAX_MEMORY_GROWTH_MB);
reporter.addAssertion("fps_stable", fpsStable); bool avgReloadTimeAcceptable = (metrics.getReloadTimeAvg() < 500.0f);
bool fpsStable = (metrics.getFPSMin() > 30.0f); // Ensure FPS doesn't drop too much
if (allReloadsSucceeded && memoryWithinThreshold &&
avgReloadTimeAcceptable && fpsStable) { reporter.addAssertion("all_reloads_succeeded", allReloadsSucceeded);
std::cout << "✅ STRESS TEST PASSED - System is stable over 10 minutes\n"; reporter.addAssertion("memory_within_threshold", memoryWithinThreshold);
} else { reporter.addAssertion("avg_reload_time_acceptable", avgReloadTimeAcceptable);
if (!allReloadsSucceeded) { reporter.addAssertion("fps_stable", fpsStable);
std::cerr << "❌ Reload success rate: " << successfulReloads << "/" << EXPECTED_RELOADS << "\n";
} if (allReloadsSucceeded && memoryWithinThreshold &&
if (!memoryWithinThreshold) { avgReloadTimeAcceptable && fpsStable) {
std::cerr << "❌ Memory growth: " << totalMemoryGrowth << " MB (threshold: " << MAX_MEMORY_GROWTH_MB << " MB)\n"; std::cout << "✅ STRESS TEST PASSED - System is stable over 10 minutes\n";
} } else {
if (!avgReloadTimeAcceptable) { if (!allReloadsSucceeded) {
std::cerr << "❌ Avg reload time: " << metrics.getReloadTimeAvg() << "ms (threshold: 500ms)\n"; std::cerr << "❌ Reload success rate: " << successfulReloads << "/" << EXPECTED_RELOADS << "\n";
} }
if (!fpsStable) { if (!memoryWithinThreshold) {
std::cerr << "❌ Min FPS: " << metrics.getFPSMin() << " (threshold: 30.0)\n"; 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";
} catch (const std::exception& e) { }
std::cerr << "Test failed with exception: " << e.what() << "\n"; if (!fpsStable) {
reporter.addAssertion("exception", false); std::cerr << "❌ Min FPS: " << metrics.getFPSMin() << " (threshold: 30.0)\n";
} }
}
reporter.printFinalReport();
return reporter.getExitCode(); } catch (const std::exception& e) {
} std::cerr << "Test failed with exception: " << e.what() << "\n";
reporter.addAssertion("exception", false);
}
reporter.printFinalReport();
return reporter.getExitCode();
}

View File

@ -1,390 +1,393 @@
#include "grove/ModuleLoader.h" #include "grove/ModuleLoader.h"
#include "grove/SequentialModuleSystem.h" #include "grove/SequentialModuleSystem.h"
#include "grove/JsonDataNode.h" #include "grove/JsonDataNode.h"
#include "../helpers/TestMetrics.h" #include "../helpers/TestMetrics.h"
#include "../helpers/TestAssertions.h" #include "../helpers/TestAssertions.h"
#include "../helpers/TestReporter.h" #include "../helpers/TestReporter.h"
#include "../helpers/SystemUtils.h" #include "../helpers/SystemUtils.h"
#include "../helpers/AutoCompiler.h" #include "../helpers/AutoCompiler.h"
#include <iostream> #include <iostream>
#include <chrono> #include <chrono>
#include <thread> #include <thread>
#include <filesystem> #include <filesystem>
#include <atomic> #include <atomic>
#include <mutex> #include <mutex>
using namespace grove; using namespace grove;
using namespace TestHelpers; using namespace TestHelpers;
int main() { int main() {
TestReporter reporter("Race Condition Hunter"); TestReporter reporter("Race Condition Hunter");
std::cout << "================================================================================\n"; std::cout << "================================================================================\n";
std::cout << "TEST: Race Condition Hunter - Concurrent Compilation & Reload\n"; std::cout << "TEST: Race Condition Hunter - Concurrent Compilation & Reload\n";
std::cout << "================================================================================\n\n"; std::cout << "================================================================================\n\n";
// === CONFIGURATION === // === CONFIGURATION ===
const int TOTAL_COMPILATIONS = 10; // Reduced for WSL2 compatibility 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 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 int FILE_CHECK_INTERVAL_MS = 50; // Check file changes every 50ms
const float TARGET_FPS = 60.0f; const float TARGET_FPS = 60.0f;
const float FRAME_TIME = 1.0f / TARGET_FPS; const float FRAME_TIME = 1.0f / TARGET_FPS;
std::string modulePath = "./libTestModule.so"; std::string modulePath = "./libTestModule.so";
// Test runs from build/tests/, so source files are at ../../tests/modules/ #ifdef _WIN32
std::string sourcePath = "../../tests/modules/TestModule.cpp"; modulePath = "./libTestModule.dll";
std::string buildDir = "build"; #endif
// Test runs from build/tests/, so source files are at ../../tests/modules/
// === ATOMIC COUNTERS (Thread-safe) === std::string sourcePath = "../../tests/modules/TestModule.cpp";
std::atomic<int> reloadAttempts{0}; std::string buildDir = "build";
std::atomic<int> reloadSuccesses{0};
std::atomic<int> reloadFailures{0}; // === ATOMIC COUNTERS (Thread-safe) ===
std::atomic<int> corruptedLoads{0}; std::atomic<int> reloadAttempts{0};
std::atomic<int> crashes{0}; std::atomic<int> reloadSuccesses{0};
std::atomic<bool> engineRunning{true}; std::atomic<int> reloadFailures{0};
std::atomic<bool> watcherRunning{true}; std::atomic<int> corruptedLoads{0};
std::atomic<int> crashes{0};
// CRITICAL: Mutex to protect moduleSystem access between threads std::atomic<bool> engineRunning{true};
std::mutex moduleSystemMutex; std::atomic<bool> watcherRunning{true};
// Reload timing // CRITICAL: Mutex to protect moduleSystem access between threads
std::mutex reloadTimesMutex; std::mutex moduleSystemMutex;
std::vector<float> reloadTimes;
// Reload timing
// Metrics std::mutex reloadTimesMutex;
TestMetrics metrics; std::vector<float> reloadTimes;
// === SETUP === // Metrics
std::cout << "Setup:\n"; TestMetrics metrics;
std::cout << " Module path: " << modulePath << "\n";
std::cout << " Source path: " << sourcePath << "\n"; // === SETUP ===
std::cout << " Compilations: " << TOTAL_COMPILATIONS << "\n"; std::cout << "Setup:\n";
std::cout << " Interval: " << COMPILE_INTERVAL_MS << "ms\n"; std::cout << " Module path: " << modulePath << "\n";
std::cout << " Expected time: ~" << (TOTAL_COMPILATIONS * COMPILE_INTERVAL_MS / 1000) << "s\n\n"; std::cout << " Source path: " << sourcePath << "\n";
std::cout << " Compilations: " << TOTAL_COMPILATIONS << "\n";
// Load module initially std::cout << " Interval: " << COMPILE_INTERVAL_MS << "ms\n";
ModuleLoader loader; std::cout << " Expected time: ~" << (TOTAL_COMPILATIONS * COMPILE_INTERVAL_MS / 1000) << "s\n\n";
auto moduleSystem = std::make_unique<SequentialModuleSystem>();
// Load module initially
try { ModuleLoader loader;
auto module = loader.load(modulePath, "TestModule", false); auto moduleSystem = std::make_unique<SequentialModuleSystem>();
nlohmann::json configJson = nlohmann::json::object();
auto config = std::make_unique<JsonDataNode>("config", configJson); try {
module->setConfiguration(*config, nullptr, nullptr); auto module = loader.load(modulePath, "TestModule", false);
moduleSystem->registerModule("TestModule", std::move(module)); nlohmann::json configJson = nlohmann::json::object();
std::cout << " ✓ Initial module loaded\n\n"; auto config = std::make_unique<JsonDataNode>("config", configJson);
} catch (const std::exception& e) { module->setConfiguration(*config, nullptr, nullptr);
std::cerr << "❌ Failed to load initial module: " << e.what() << "\n"; moduleSystem->registerModule("TestModule", std::move(module));
return 1; std::cout << " ✓ Initial module loaded\n\n";
} } catch (const std::exception& e) {
std::cerr << "❌ Failed to load initial module: " << e.what() << "\n";
// === THREAD 1: AUTO-COMPILER === return 1;
std::cout << "Starting AutoCompiler thread...\n"; }
AutoCompiler compiler("TestModule", buildDir, sourcePath);
compiler.start(TOTAL_COMPILATIONS, COMPILE_INTERVAL_MS); // === THREAD 1: AUTO-COMPILER ===
std::cout << "Starting AutoCompiler thread...\n";
// === THREAD 2: FILE WATCHER === AutoCompiler compiler("TestModule", buildDir, sourcePath);
std::cout << "Starting FileWatcher thread...\n"; compiler.start(TOTAL_COMPILATIONS, COMPILE_INTERVAL_MS);
std::thread watcherThread([&]() {
try { // === THREAD 2: FILE WATCHER ===
auto lastWriteTime = std::filesystem::last_write_time(modulePath); std::cout << "Starting FileWatcher thread...\n";
std::thread watcherThread([&]() {
while (watcherRunning.load() && engineRunning.load()) { try {
try { auto lastWriteTime = std::filesystem::last_write_time(modulePath);
auto currentTime = std::filesystem::last_write_time(modulePath);
while (watcherRunning.load() && engineRunning.load()) {
if (currentTime != lastWriteTime) { try {
reloadAttempts++; auto currentTime = std::filesystem::last_write_time(modulePath);
// Measure reload time if (currentTime != lastWriteTime) {
auto reloadStart = std::chrono::high_resolution_clock::now(); reloadAttempts++;
try { // Measure reload time
// CRITICAL: Lock moduleSystem during entire reload auto reloadStart = std::chrono::high_resolution_clock::now();
std::lock_guard<std::mutex> lock(moduleSystemMutex);
try {
// Extract module and save state // CRITICAL: Lock moduleSystem during entire reload
auto module = moduleSystem->extractModule(); std::lock_guard<std::mutex> lock(moduleSystemMutex);
auto state = module->getState();
// Extract module and save state
// CRITICAL: Destroy old module BEFORE reloading auto module = moduleSystem->extractModule();
// The loader.load() will unload the old .so auto state = module->getState();
module.reset();
// CRITICAL: Destroy old module BEFORE reloading
// Reload // The loader.load() will unload the old .so
auto newModule = loader.load(modulePath, "TestModule", true); module.reset();
// Check if module loaded correctly // Reload
if (!newModule) { auto newModule = loader.load(modulePath, "TestModule", true);
corruptedLoads++;
reloadFailures++; // Check if module loaded correctly
// Can't recover - old module already destroyed if (!newModule) {
} else { corruptedLoads++;
// VALIDATE MODULE INTEGRITY reloadFailures++;
bool isCorrupted = false; // Can't recover - old module already destroyed
try { } else {
// Test 1: Can we get health status? // VALIDATE MODULE INTEGRITY
auto health = newModule->getHealthStatus(); bool isCorrupted = false;
std::string version = health->getString("version", ""); try {
// Test 1: Can we get health status?
// Test 2: Is version valid? auto health = newModule->getHealthStatus();
if (version.empty() || version == "unknown") { std::string version = health->getString("version", "");
isCorrupted = true;
} // Test 2: Is version valid?
if (version.empty() || version == "unknown") {
// Test 3: Can we set configuration? isCorrupted = true;
nlohmann::json configJson; }
configJson["test"] = "validation";
auto testConfig = std::make_unique<JsonDataNode>("config", configJson); // Test 3: Can we set configuration?
newModule->setConfiguration(*testConfig, nullptr, nullptr); nlohmann::json configJson;
configJson["test"] = "validation";
} catch (const std::exception& e) { auto testConfig = std::make_unique<JsonDataNode>("config", configJson);
// Module crashes on basic operations = corrupted newModule->setConfiguration(*testConfig, nullptr, nullptr);
isCorrupted = true;
} } catch (const std::exception& e) {
// Module crashes on basic operations = corrupted
if (isCorrupted) { isCorrupted = true;
corruptedLoads++; }
reloadFailures++;
// Can't recover - old module already destroyed if (isCorrupted) {
} else { corruptedLoads++;
// Module is valid, restore state and register reloadFailures++;
newModule->setState(*state); // Can't recover - old module already destroyed
moduleSystem->registerModule("TestModule", std::move(newModule)); } else {
reloadSuccesses++; // Module is valid, restore state and register
newModule->setState(*state);
// Record reload time moduleSystem->registerModule("TestModule", std::move(newModule));
auto reloadEnd = std::chrono::high_resolution_clock::now(); reloadSuccesses++;
float reloadTimeMs = std::chrono::duration<float, std::milli>(reloadEnd - reloadStart).count();
// Record reload time
{ auto reloadEnd = std::chrono::high_resolution_clock::now();
std::lock_guard<std::mutex> timeLock(reloadTimesMutex); float reloadTimeMs = std::chrono::duration<float, std::milli>(reloadEnd - reloadStart).count();
reloadTimes.push_back(reloadTimeMs);
} {
} std::lock_guard<std::mutex> timeLock(reloadTimesMutex);
} reloadTimes.push_back(reloadTimeMs);
} catch (const std::exception& e) { }
reloadFailures++; }
// Module might already be registered, continue }
} } catch (const std::exception& e) {
reloadFailures++;
lastWriteTime = currentTime; // Module might already be registered, continue
} }
} catch (const std::filesystem::filesystem_error&) {
// File might be being written, ignore lastWriteTime = currentTime;
} }
} catch (const std::filesystem::filesystem_error&) {
std::this_thread::sleep_for(std::chrono::milliseconds(FILE_CHECK_INTERVAL_MS)); // File might be being written, ignore
} }
} catch (const std::exception& e) {
std::cerr << "[FileWatcher] Exception: " << e.what() << "\n"; 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 { // === THREAD 3: ENGINE LOOP ===
auto lastMemoryCheck = std::chrono::steady_clock::now(); std::cout << "Starting Engine thread (60 FPS)...\n\n";
std::thread engineThread([&]() {
while (engineRunning.load()) { try {
auto frameStart = std::chrono::high_resolution_clock::now(); auto lastMemoryCheck = std::chrono::steady_clock::now();
try { while (engineRunning.load()) {
// TRY to lock moduleSystem (non-blocking) auto frameStart = std::chrono::high_resolution_clock::now();
// If reload is happening, skip this frame
if (moduleSystemMutex.try_lock()) { try {
try { // TRY to lock moduleSystem (non-blocking)
moduleSystem->processModules(FRAME_TIME); // If reload is happening, skip this frame
moduleSystemMutex.unlock(); if (moduleSystemMutex.try_lock()) {
} catch (const std::exception& e) { try {
moduleSystemMutex.unlock(); moduleSystem->processModules(FRAME_TIME);
throw; moduleSystemMutex.unlock();
} } catch (const std::exception& e) {
} moduleSystemMutex.unlock();
// else: reload in progress, skip frame throw;
} catch (const std::exception& e) { }
crashes++; }
std::cerr << "[Engine] Crash detected: " << e.what() << "\n"; // else: reload in progress, skip frame
} } catch (const std::exception& e) {
crashes++;
auto frameEnd = std::chrono::high_resolution_clock::now(); std::cerr << "[Engine] Crash detected: " << e.what() << "\n";
float frameTime = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count(); }
metrics.recordFPS(1000.0f / std::max(frameTime, 0.1f));
auto frameEnd = std::chrono::high_resolution_clock::now();
// Check memory every second float frameTime = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count();
auto now = std::chrono::steady_clock::now(); metrics.recordFPS(1000.0f / std::max(frameTime, 0.1f));
if (std::chrono::duration_cast<std::chrono::seconds>(now - lastMemoryCheck).count() >= 1) {
metrics.recordMemoryUsage(getCurrentMemoryUsage()); // Check memory every second
lastMemoryCheck = now; auto now = std::chrono::steady_clock::now();
} if (std::chrono::duration_cast<std::chrono::seconds>(now - lastMemoryCheck).count() >= 1) {
metrics.recordMemoryUsage(getCurrentMemoryUsage());
// Sleep to maintain target FPS (if frame finished early) lastMemoryCheck = now;
auto targetFrameTime = std::chrono::milliseconds(static_cast<int>(FRAME_TIME * 1000)); }
auto elapsed = frameEnd - frameStart;
if (elapsed < targetFrameTime) { // Sleep to maintain target FPS (if frame finished early)
std::this_thread::sleep_for(targetFrameTime - elapsed); auto targetFrameTime = std::chrono::milliseconds(static_cast<int>(FRAME_TIME * 1000));
} auto elapsed = frameEnd - frameStart;
} if (elapsed < targetFrameTime) {
} catch (const std::exception& e) { std::this_thread::sleep_for(targetFrameTime - elapsed);
std::cerr << "[Engine] Thread exception: " << e.what() << "\n"; }
} }
}); } 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; // === MONITORING LOOP ===
const int MAX_TEST_TIME_SECONDS = 90; // Maximum 1.5 minutes (allows all 20 compilations) std::cout << "Test running...\n";
auto startTime = std::chrono::steady_clock::now();
while (compiler.isRunning() || compiler.getCurrentIteration() < TOTAL_COMPILATIONS) { int lastPrintedPercent = 0;
std::this_thread::sleep_for(std::chrono::seconds(2)); const int MAX_TEST_TIME_SECONDS = 90; // Maximum 1.5 minutes (allows all 20 compilations)
int currentIteration = compiler.getCurrentIteration(); while (compiler.isRunning() || compiler.getCurrentIteration() < TOTAL_COMPILATIONS) {
int percent = (currentIteration * 100) / TOTAL_COMPILATIONS; std::this_thread::sleep_for(std::chrono::seconds(2));
// Check for timeout int currentIteration = compiler.getCurrentIteration();
auto now = std::chrono::steady_clock::now(); int percent = (currentIteration * 100) / TOTAL_COMPILATIONS;
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - startTime).count();
// Check for timeout
if (elapsed > MAX_TEST_TIME_SECONDS) { auto now = std::chrono::steady_clock::now();
std::cout << "\n⚠️ Test timeout after " << elapsed << "s - stopping...\n"; auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - startTime).count();
break;
} if (elapsed > MAX_TEST_TIME_SECONDS) {
std::cout << "\n⚠️ Test timeout after " << elapsed << "s - stopping...\n";
// Print progress every 10% break;
if (percent >= lastPrintedPercent + 10 && percent <= 100) { }
std::cout << "\nProgress: " << percent << "% (" << currentIteration << "/" << TOTAL_COMPILATIONS << " compilations)\n";
std::cout << " Elapsed: " << elapsed << "s\n"; // Print progress every 10%
std::cout << " Compilations: " << compiler.getSuccessCount() << " OK, " << compiler.getFailureCount() << " FAIL\n"; if (percent >= lastPrintedPercent + 10 && percent <= 100) {
std::cout << " Reloads: " << reloadSuccesses.load() << " OK, " << reloadFailures.load() << " FAIL\n"; std::cout << "\nProgress: " << percent << "% (" << currentIteration << "/" << TOTAL_COMPILATIONS << " compilations)\n";
std::cout << " Corrupted: " << corruptedLoads.load() << "\n"; std::cout << " Elapsed: " << elapsed << "s\n";
std::cout << " Crashes: " << crashes.load() << "\n"; std::cout << " Compilations: " << compiler.getSuccessCount() << " OK, " << compiler.getFailureCount() << " FAIL\n";
std::cout << " Reloads: " << reloadSuccesses.load() << " OK, " << reloadFailures.load() << " FAIL\n";
lastPrintedPercent = percent; std::cout << " Corrupted: " << corruptedLoads.load() << "\n";
} std::cout << " Crashes: " << crashes.load() << "\n";
}
lastPrintedPercent = percent;
// === CLEANUP === }
std::cout << "\n\nStopping threads...\n"; }
compiler.stop(); // === CLEANUP ===
watcherRunning = false; std::cout << "\n\nStopping threads...\n";
engineRunning = false;
compiler.stop();
if (watcherThread.joinable()) { watcherRunning = false;
watcherThread.join(); engineRunning = false;
}
if (engineThread.joinable()) { if (watcherThread.joinable()) {
engineThread.join(); watcherThread.join();
} }
if (engineThread.joinable()) {
std::cout << " ✓ All threads stopped\n\n"; engineThread.join();
}
// === CALCULATE STATISTICS ===
float compileSuccessRate = (compiler.getSuccessCount() * 100.0f) / std::max(1, TOTAL_COMPILATIONS); std::cout << " ✓ All threads stopped\n\n";
float reloadSuccessRate = (reloadSuccesses.load() * 100.0f) / std::max(1, reloadAttempts.load());
// === CALCULATE STATISTICS ===
float avgReloadTime = 0.0f; float compileSuccessRate = (compiler.getSuccessCount() * 100.0f) / std::max(1, TOTAL_COMPILATIONS);
{ float reloadSuccessRate = (reloadSuccesses.load() * 100.0f) / std::max(1, reloadAttempts.load());
std::lock_guard<std::mutex> lock(reloadTimesMutex);
if (!reloadTimes.empty()) { float avgReloadTime = 0.0f;
float sum = 0.0f; {
for (float t : reloadTimes) { std::lock_guard<std::mutex> lock(reloadTimesMutex);
sum += t; if (!reloadTimes.empty()) {
} float sum = 0.0f;
avgReloadTime = sum / reloadTimes.size(); 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 === auto endTime = std::chrono::steady_clock::now();
std::cout << "================================================================================\n"; auto totalTimeSeconds = std::chrono::duration_cast<std::chrono::seconds>(endTime - startTime).count();
std::cout << "RACE CONDITION HUNTER SUMMARY\n";
std::cout << "================================================================================\n\n"; // === PRINT SUMMARY ===
std::cout << "================================================================================\n";
std::cout << "Duration: " << totalTimeSeconds << "s\n\n"; std::cout << "RACE CONDITION HUNTER SUMMARY\n";
std::cout << "================================================================================\n\n";
std::cout << "Compilations:\n";
std::cout << " Total: " << TOTAL_COMPILATIONS << "\n"; std::cout << "Duration: " << totalTimeSeconds << "s\n\n";
std::cout << " Successes: " << compiler.getSuccessCount() << " (" << std::fixed << std::setprecision(1) << compileSuccessRate << "%)\n";
std::cout << " Failures: " << compiler.getFailureCount() << "\n\n"; std::cout << "Compilations:\n";
std::cout << " Total: " << TOTAL_COMPILATIONS << "\n";
std::cout << "Reloads:\n"; std::cout << " Successes: " << compiler.getSuccessCount() << " (" << std::fixed << std::setprecision(1) << compileSuccessRate << "%)\n";
std::cout << " Attempts: " << reloadAttempts.load() << "\n"; std::cout << " Failures: " << compiler.getFailureCount() << "\n\n";
std::cout << " Successes: " << reloadSuccesses.load() << " (" << std::fixed << std::setprecision(1) << reloadSuccessRate << "%)\n";
std::cout << " Failures: " << reloadFailures.load() << "\n"; std::cout << "Reloads:\n";
std::cout << " Corrupted: " << corruptedLoads.load() << "\n"; std::cout << " Attempts: " << reloadAttempts.load() << "\n";
std::cout << " Avg time: " << std::fixed << std::setprecision(0) << avgReloadTime << "ms\n\n"; std::cout << " Successes: " << reloadSuccesses.load() << " (" << std::fixed << std::setprecision(1) << reloadSuccessRate << "%)\n";
std::cout << " Failures: " << reloadFailures.load() << "\n";
std::cout << "Stability:\n"; std::cout << " Corrupted: " << corruptedLoads.load() << "\n";
std::cout << " Crashes: " << crashes.load() << "\n"; std::cout << " Avg time: " << std::fixed << std::setprecision(0) << avgReloadTime << "ms\n\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"; std::cout << "Stability:\n";
std::cout << " Crashes: " << crashes.load() << "\n";
// === ASSERTIONS === std::cout << " Avg FPS: " << std::fixed << std::setprecision(1) << metrics.getFPSAvg() << "\n";
bool passed = true; std::cout << " Memory: " << std::fixed << std::setprecision(2) << metrics.getMemoryGrowth() << " MB\n\n";
std::cout << "Validating results...\n"; // === ASSERTIONS ===
bool passed = true;
// MUST PASS criteria
// Note: Lowered from 95% to 70% for WSL2/slower filesystem compatibility std::cout << "Validating results...\n";
// The important thing is that compilations don't fail, they just might timeout
if (compileSuccessRate < 70.0f) { // MUST PASS criteria
std::cout << " ❌ Compile success rate too low: " << compileSuccessRate << "% (need > 70%)\n"; // Note: Lowered from 95% to 70% for WSL2/slower filesystem compatibility
passed = false; // The important thing is that compilations don't fail, they just might timeout
} else { if (compileSuccessRate < 70.0f) {
std::cout << " ✓ Compile success rate: " << compileSuccessRate << "%\n"; std::cout << " ❌ Compile success rate too low: " << compileSuccessRate << "% (need > 70%)\n";
} passed = false;
} else {
if (corruptedLoads.load() > 0) { std::cout << " ✓ Compile success rate: " << compileSuccessRate << "%\n";
std::cout << " ❌ Corrupted loads detected: " << corruptedLoads.load() << " (need 0)\n"; }
passed = false;
} else { if (corruptedLoads.load() > 0) {
std::cout << " ✓ No corrupted loads\n"; std::cout << " ❌ Corrupted loads detected: " << corruptedLoads.load() << " (need 0)\n";
} passed = false;
} else {
if (crashes.load() > 0) { std::cout << " ✓ No corrupted loads\n";
std::cout << " ❌ Crashes detected: " << crashes.load() << " (need 0)\n"; }
passed = false;
} else { if (crashes.load() > 0) {
std::cout << " ✓ No crashes\n"; std::cout << " ❌ Crashes detected: " << crashes.load() << " (need 0)\n";
} passed = false;
} else {
if (reloadAttempts.load() > 0 && reloadSuccessRate < 99.0f) { std::cout << " ✓ No crashes\n";
std::cout << " ❌ Reload success rate too low: " << reloadSuccessRate << "% (need > 99%)\n"; }
passed = false;
} else if (reloadAttempts.load() > 0) { if (reloadAttempts.load() > 0 && reloadSuccessRate < 99.0f) {
std::cout << " ✓ Reload success rate: " << reloadSuccessRate << "%\n"; std::cout << " ❌ Reload success rate too low: " << reloadSuccessRate << "% (need > 99%)\n";
} passed = false;
} else if (reloadAttempts.load() > 0) {
// File stability validation: reload time should be >= 100ms std::cout << " ✓ Reload success rate: " << reloadSuccessRate << "%\n";
// This proves that ModuleLoader is waiting for file stability }
if (reloadAttempts.load() > 0) {
if (avgReloadTime < 100.0f) { // File stability validation: reload time should be >= 100ms
std::cout << " ❌ Reload time too fast: " << avgReloadTime << "ms (need >= 100ms)\n"; // This proves that ModuleLoader is waiting for file stability
std::cout << " File stability check is NOT working properly!\n"; if (reloadAttempts.load() > 0) {
passed = false; if (avgReloadTime < 100.0f) {
} else if (avgReloadTime > 600.0f) { std::cout << " ❌ Reload time too fast: " << avgReloadTime << "ms (need >= 100ms)\n";
std::cout << " ⚠️ Reload time very slow: " << avgReloadTime << "ms (> 600ms)\n"; std::cout << " File stability check is NOT working properly!\n";
std::cout << " File stability might be waiting too long\n"; passed = false;
} else { } else if (avgReloadTime > 600.0f) {
std::cout << " ✓ Reload time: " << avgReloadTime << "ms (file stability working)\n"; 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"; std::cout << "\n";
if (passed) {
std::cout << "Result: ✅ PASSED\n"; // === FINAL RESULT ===
} else { std::cout << "================================================================================\n";
std::cout << "Result: ❌ FAILED\n"; if (passed) {
} std::cout << "Result: ✅ PASSED\n";
std::cout << "================================================================================\n"; } else {
std::cout << "Result: ❌ FAILED\n";
return passed ? 0 : 1; }
} std::cout << "================================================================================\n";
return passed ? 0 : 1;
}

View File

@ -1,428 +1,431 @@
// ============================================================================ // ============================================================================
// test_05_memory_leak.cpp - Memory Leak Hunter // test_05_memory_leak.cpp - Memory Leak Hunter
// ============================================================================ // ============================================================================
// Tests that repeated hot-reload cycles do not leak memory // Tests that repeated hot-reload cycles do not leak memory
// //
// Strategy: // Strategy:
// - Load the same .so file 200 times (no recompilation) // - Load the same .so file 200 times (no recompilation)
// - Measure memory usage every 5 seconds // - Measure memory usage every 5 seconds
// - Verify temp file cleanup // - Verify temp file cleanup
// - Check for library handle leaks // - Check for library handle leaks
// //
// Success criteria: // Success criteria:
// - < 10 MB total memory growth // - < 10 MB total memory growth
// - < 50 KB average memory per reload // - < 50 KB average memory per reload
// - Temp files cleaned up (≤ 2 at any time) // - Temp files cleaned up (≤ 2 at any time)
// - No increase in mapped .so count // - No increase in mapped .so count
// - 100% reload success rate // - 100% reload success rate
// ============================================================================ // ============================================================================
#include "grove/ModuleLoader.h" #include "grove/ModuleLoader.h"
#include "grove/SequentialModuleSystem.h" #include "grove/SequentialModuleSystem.h"
#include "grove/JsonDataNode.h" #include "grove/JsonDataNode.h"
#include "../helpers/SystemUtils.h" #include "../helpers/SystemUtils.h"
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include <iostream> #include <iostream>
#include <iomanip> #include <iomanip>
#include <thread> #include <thread>
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <vector> #include <vector>
#include <filesystem> #include <filesystem>
#include <cmath> #include <cmath>
using namespace grove; using namespace grove;
namespace fs = std::filesystem; namespace fs = std::filesystem;
// ============================================================================ // ============================================================================
// Configuration // Configuration
// ============================================================================ // ============================================================================
const int TOTAL_RELOADS = 200; const int TOTAL_RELOADS = 200;
const int RELOAD_INTERVAL_MS = 500; const int RELOAD_INTERVAL_MS = 500;
const int MEMORY_CHECK_INTERVAL_MS = 5000; const int MEMORY_CHECK_INTERVAL_MS = 5000;
const float TARGET_FPS = 60.0f; const float TARGET_FPS = 60.0f;
const int MAX_TEST_TIME_SECONDS = 180; const int MAX_TEST_TIME_SECONDS = 180;
// ============================================================================ // ============================================================================
// State tracking // State tracking
// ============================================================================ // ============================================================================
struct MemorySnapshot { struct MemorySnapshot {
int reloadCount; int reloadCount;
size_t memoryBytes; size_t memoryBytes;
int tempFiles; int tempFiles;
int mappedLibs; int mappedLibs;
std::chrono::steady_clock::time_point timestamp; std::chrono::steady_clock::time_point timestamp;
}; };
std::atomic<bool> g_running{true}; std::atomic<bool> g_running{true};
std::atomic<int> g_reloadCount{0}; std::atomic<int> g_reloadCount{0};
std::atomic<int> g_reloadSuccesses{0}; std::atomic<int> g_reloadSuccesses{0};
std::atomic<int> g_reloadFailures{0}; std::atomic<int> g_reloadFailures{0};
std::atomic<int> g_crashes{0}; std::atomic<int> g_crashes{0};
std::vector<MemorySnapshot> g_snapshots; std::vector<MemorySnapshot> g_snapshots;
std::mutex g_snapshotMutex; std::mutex g_snapshotMutex;
// ============================================================================ // ============================================================================
// Reload Scheduler Thread // Reload Scheduler Thread
// ============================================================================ // ============================================================================
void reloadSchedulerThread(ModuleLoader& loader, SequentialModuleSystem* moduleSystem, void reloadSchedulerThread(ModuleLoader& loader, SequentialModuleSystem* moduleSystem,
const fs::path& modulePath, std::mutex& moduleSystemMutex) { const fs::path& modulePath, std::mutex& moduleSystemMutex) {
std::cout << " Starting ReloadScheduler thread...\n"; std::cout << " Starting ReloadScheduler thread...\n";
while (g_running && g_reloadCount < TOTAL_RELOADS) { while (g_running && g_reloadCount < TOTAL_RELOADS) {
std::this_thread::sleep_for(std::chrono::milliseconds(RELOAD_INTERVAL_MS)); std::this_thread::sleep_for(std::chrono::milliseconds(RELOAD_INTERVAL_MS));
if (!g_running) break; if (!g_running) break;
try { try {
std::lock_guard<std::mutex> lock(moduleSystemMutex); std::lock_guard<std::mutex> lock(moduleSystemMutex);
// Extract current module and save state // Extract current module and save state
auto module = moduleSystem->extractModule(); auto module = moduleSystem->extractModule();
auto state = module->getState(); auto state = module->getState();
auto config = std::make_unique<JsonDataNode>("config", auto config = std::make_unique<JsonDataNode>("config",
dynamic_cast<const JsonDataNode&>(module->getConfiguration()).getJsonData()); dynamic_cast<const JsonDataNode&>(module->getConfiguration()).getJsonData());
// CRITICAL: Destroy old module BEFORE reloading to avoid use-after-free // 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 // The loader.load() will unload the old .so, so we must destroy the module first
module.reset(); module.reset();
// Reload the same .so file // Reload the same .so file
auto newModule = loader.load(modulePath.string(), "LeakTestModule", true); auto newModule = loader.load(modulePath.string(), "LeakTestModule", true);
g_reloadCount++; g_reloadCount++;
if (newModule) { if (newModule) {
// Restore state // Restore state
newModule->setConfiguration(*config, nullptr, nullptr); newModule->setConfiguration(*config, nullptr, nullptr);
newModule->setState(*state); newModule->setState(*state);
// Register new module // Register new module
moduleSystem->registerModule("LeakTestModule", std::move(newModule)); moduleSystem->registerModule("LeakTestModule", std::move(newModule));
g_reloadSuccesses++; g_reloadSuccesses++;
} else { } else {
// Reload failed - we can't recover (old module destroyed) // Reload failed - we can't recover (old module destroyed)
g_reloadFailures++; g_reloadFailures++;
} }
} catch (...) { } catch (...) {
g_crashes++; g_crashes++;
g_reloadFailures++; g_reloadFailures++;
g_reloadCount++; g_reloadCount++;
} }
} }
std::cout << " ReloadScheduler thread finished.\n"; std::cout << " ReloadScheduler thread finished.\n";
} }
// ============================================================================ // ============================================================================
// Memory Monitor Thread // Memory Monitor Thread
// ============================================================================ // ============================================================================
void memoryMonitorThread() { void memoryMonitorThread() {
std::cout << " Starting MemoryMonitor thread...\n"; std::cout << " Starting MemoryMonitor thread...\n";
auto startTime = std::chrono::steady_clock::now(); auto startTime = std::chrono::steady_clock::now();
while (g_running) { while (g_running) {
std::this_thread::sleep_for(std::chrono::milliseconds(MEMORY_CHECK_INTERVAL_MS)); std::this_thread::sleep_for(std::chrono::milliseconds(MEMORY_CHECK_INTERVAL_MS));
if (!g_running) break; if (!g_running) break;
MemorySnapshot snapshot; MemorySnapshot snapshot;
snapshot.reloadCount = g_reloadCount; snapshot.reloadCount = g_reloadCount;
snapshot.memoryBytes = getCurrentMemoryUsage(); snapshot.memoryBytes = getCurrentMemoryUsage();
snapshot.tempFiles = countTempFiles("/tmp/grove_module_*"); snapshot.tempFiles = countTempFiles("/tmp/grove_module_*");
snapshot.mappedLibs = getMappedLibraryCount(); snapshot.mappedLibs = getMappedLibraryCount();
snapshot.timestamp = std::chrono::steady_clock::now(); snapshot.timestamp = std::chrono::steady_clock::now();
{ {
std::lock_guard<std::mutex> lock(g_snapshotMutex); std::lock_guard<std::mutex> lock(g_snapshotMutex);
g_snapshots.push_back(snapshot); g_snapshots.push_back(snapshot);
} }
// Print progress // Print progress
float memoryMB = snapshot.memoryBytes / (1024.0f * 1024.0f); float memoryMB = snapshot.memoryBytes / (1024.0f * 1024.0f);
int progress = (snapshot.reloadCount * 100) / TOTAL_RELOADS; int progress = (snapshot.reloadCount * 100) / TOTAL_RELOADS;
std::cout << "\n Progress: " << snapshot.reloadCount << " reloads (" << progress << "%)\n"; std::cout << "\n Progress: " << snapshot.reloadCount << " reloads (" << progress << "%)\n";
std::cout << " Memory: " << std::fixed << std::setprecision(1) << memoryMB << " MB\n"; std::cout << " Memory: " << std::fixed << std::setprecision(1) << memoryMB << " MB\n";
std::cout << " Temp files: " << snapshot.tempFiles << "\n"; std::cout << " Temp files: " << snapshot.tempFiles << "\n";
std::cout << " Mapped .so: " << snapshot.mappedLibs << "\n"; std::cout << " Mapped .so: " << snapshot.mappedLibs << "\n";
} }
std::cout << " MemoryMonitor thread finished.\n"; std::cout << " MemoryMonitor thread finished.\n";
} }
// ============================================================================ // ============================================================================
// Engine Thread // Engine Thread
// ============================================================================ // ============================================================================
void engineThread(SequentialModuleSystem* moduleSystem, std::mutex& moduleSystemMutex) { void engineThread(SequentialModuleSystem* moduleSystem, std::mutex& moduleSystemMutex) {
std::cout << " Starting Engine thread (60 FPS)...\n"; std::cout << " Starting Engine thread (60 FPS)...\n";
const float frameTime = 1.0f / TARGET_FPS; const float frameTime = 1.0f / TARGET_FPS;
auto lastFrame = std::chrono::steady_clock::now(); auto lastFrame = std::chrono::steady_clock::now();
int frameCount = 0; int frameCount = 0;
while (g_running && g_reloadCount < TOTAL_RELOADS) { while (g_running && g_reloadCount < TOTAL_RELOADS) {
auto now = std::chrono::steady_clock::now(); auto now = std::chrono::steady_clock::now();
float deltaTime = std::chrono::duration<float>(now - lastFrame).count(); float deltaTime = std::chrono::duration<float>(now - lastFrame).count();
if (deltaTime >= frameTime) { if (deltaTime >= frameTime) {
try { try {
std::lock_guard<std::mutex> lock(moduleSystemMutex); std::lock_guard<std::mutex> lock(moduleSystemMutex);
moduleSystem->processModules(deltaTime); moduleSystem->processModules(deltaTime);
frameCount++; frameCount++;
} catch (...) { } catch (...) {
g_crashes++; g_crashes++;
} }
lastFrame = now; lastFrame = now;
} else { } else {
// Sleep for remaining time // Sleep for remaining time
int sleepMs = static_cast<int>((frameTime - deltaTime) * 1000); int sleepMs = static_cast<int>((frameTime - deltaTime) * 1000);
if (sleepMs > 0) { if (sleepMs > 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(sleepMs)); std::this_thread::sleep_for(std::chrono::milliseconds(sleepMs));
} }
} }
} }
std::cout << " Engine thread finished (" << frameCount << " frames processed).\n"; std::cout << " Engine thread finished (" << frameCount << " frames processed).\n";
} }
// ============================================================================ // ============================================================================
// Main Test // Main Test
// ============================================================================ // ============================================================================
int main() { int main() {
std::cout << "================================================================================\n"; std::cout << "================================================================================\n";
std::cout << "TEST: Memory Leak Hunter - " << TOTAL_RELOADS << " Reload Cycles\n"; std::cout << "TEST: Memory Leak Hunter - " << TOTAL_RELOADS << " Reload Cycles\n";
std::cout << "================================================================================\n\n"; std::cout << "================================================================================\n\n";
// Find module path // Find module path
fs::path modulePath = "./libLeakTestModule.so"; fs::path modulePath = "./libLeakTestModule.so";
if (!fs::exists(modulePath)) { #ifdef _WIN32
std::cerr << "❌ Module not found: " << modulePath << "\n"; modulePath = "./libLeakTestModule.dll";
return 1; #endif
} if (!fs::exists(modulePath)) {
std::cerr << "❌ Module not found: " << modulePath << "\n";
std::cout << "Setup:\n"; return 1;
std::cout << " Module path: " << modulePath << "\n"; }
std::cout << " Total reloads: " << TOTAL_RELOADS << "\n";
std::cout << " Interval: " << RELOAD_INTERVAL_MS << "ms\n"; std::cout << "Setup:\n";
std::cout << " Expected time: ~" << (TOTAL_RELOADS * RELOAD_INTERVAL_MS / 1000) << "s\n\n"; std::cout << " Module path: " << modulePath << "\n";
std::cout << " Total reloads: " << TOTAL_RELOADS << "\n";
// Create module loader and system std::cout << " Interval: " << RELOAD_INTERVAL_MS << "ms\n";
ModuleLoader loader; std::cout << " Expected time: ~" << (TOTAL_RELOADS * RELOAD_INTERVAL_MS / 1000) << "s\n\n";
auto moduleSystem = std::make_unique<SequentialModuleSystem>();
std::mutex moduleSystemMutex; // Create module loader and system
ModuleLoader loader;
// Disable verbose logging for performance auto moduleSystem = std::make_unique<SequentialModuleSystem>();
moduleSystem->setLogLevel(spdlog::level::err); std::mutex moduleSystemMutex;
// Load initial module // Disable verbose logging for performance
try { moduleSystem->setLogLevel(spdlog::level::err);
auto module = loader.load(modulePath.string(), "LeakTestModule", false);
if (!module) { // Load initial module
std::cerr << "❌ Failed to load LeakTestModule\n"; try {
return 1; auto module = loader.load(modulePath.string(), "LeakTestModule", false);
} if (!module) {
std::cerr << "❌ Failed to load LeakTestModule\n";
nlohmann::json configJson = nlohmann::json::object(); return 1;
auto config = std::make_unique<JsonDataNode>("config", configJson); }
module->setConfiguration(*config, nullptr, nullptr);
moduleSystem->registerModule("LeakTestModule", std::move(module)); nlohmann::json configJson = nlohmann::json::object();
std::cout << "✓ Initial module loaded\n\n"; auto config = std::make_unique<JsonDataNode>("config", configJson);
} catch (const std::exception& e) { module->setConfiguration(*config, nullptr, nullptr);
std::cerr << "❌ Failed to load initial module: " << e.what() << "\n"; moduleSystem->registerModule("LeakTestModule", std::move(module));
return 1; std::cout << "✓ Initial module loaded\n\n";
} } catch (const std::exception& e) {
std::cerr << "❌ Failed to load initial module: " << e.what() << "\n";
// Baseline memory return 1;
std::this_thread::sleep_for(std::chrono::milliseconds(100)); }
size_t baselineMemory = getCurrentMemoryUsage();
int baselineMappedLibs = getMappedLibraryCount(); // Baseline memory
float baselineMB = baselineMemory / (1024.0f * 1024.0f); std::this_thread::sleep_for(std::chrono::milliseconds(100));
size_t baselineMemory = getCurrentMemoryUsage();
std::cout << "Baseline memory: " << std::fixed << std::setprecision(1) << baselineMB << " MB\n"; int baselineMappedLibs = getMappedLibraryCount();
std::cout << "Baseline mapped .so: " << baselineMappedLibs << "\n\n"; float baselineMB = baselineMemory / (1024.0f * 1024.0f);
// Start threads std::cout << "Baseline memory: " << std::fixed << std::setprecision(1) << baselineMB << " MB\n";
auto startTime = std::chrono::steady_clock::now(); std::cout << "Baseline mapped .so: " << baselineMappedLibs << "\n\n";
std::thread reloadThread(reloadSchedulerThread, std::ref(loader), moduleSystem.get(), // Start threads
modulePath, std::ref(moduleSystemMutex)); auto startTime = std::chrono::steady_clock::now();
std::thread monitorThread(memoryMonitorThread);
std::thread engThread(engineThread, moduleSystem.get(), std::ref(moduleSystemMutex)); std::thread reloadThread(reloadSchedulerThread, std::ref(loader), moduleSystem.get(),
modulePath, std::ref(moduleSystemMutex));
// Wait for completion or timeout std::thread monitorThread(memoryMonitorThread);
auto deadline = startTime + std::chrono::seconds(MAX_TEST_TIME_SECONDS); std::thread engThread(engineThread, moduleSystem.get(), std::ref(moduleSystemMutex));
while (g_running && g_reloadCount < TOTAL_RELOADS) { // Wait for completion or timeout
std::this_thread::sleep_for(std::chrono::milliseconds(100)); auto deadline = startTime + std::chrono::seconds(MAX_TEST_TIME_SECONDS);
if (std::chrono::steady_clock::now() > deadline) { while (g_running && g_reloadCount < TOTAL_RELOADS) {
std::cout << "\n⚠️ Test timeout after " << MAX_TEST_TIME_SECONDS << " seconds\n"; std::this_thread::sleep_for(std::chrono::milliseconds(100));
break;
} 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(); // Stop threads
monitorThread.join(); g_running = false;
engThread.join();
reloadThread.join();
auto endTime = std::chrono::steady_clock::now(); monitorThread.join();
float durationSeconds = std::chrono::duration<float>(endTime - startTime).count(); engThread.join();
// Final measurements auto endTime = std::chrono::steady_clock::now();
size_t finalMemory = getCurrentMemoryUsage(); float durationSeconds = std::chrono::duration<float>(endTime - startTime).count();
int finalMappedLibs = getMappedLibraryCount();
int finalTempFiles = countTempFiles("/tmp/grove_module_*"); // Final measurements
size_t finalMemory = getCurrentMemoryUsage();
float finalMB = finalMemory / (1024.0f * 1024.0f); int finalMappedLibs = getMappedLibraryCount();
float growthMB = (finalMemory - baselineMemory) / (1024.0f * 1024.0f); int finalTempFiles = countTempFiles("/tmp/grove_module_*");
// ======================================================================== float finalMB = finalMemory / (1024.0f * 1024.0f);
// Results Summary float growthMB = (finalMemory - baselineMemory) / (1024.0f * 1024.0f);
// ========================================================================
// ========================================================================
std::cout << "\n================================================================================\n"; // Results Summary
std::cout << "MEMORY LEAK HUNTER SUMMARY\n"; // ========================================================================
std::cout << "================================================================================\n\n";
std::cout << "\n================================================================================\n";
std::cout << "Duration: " << static_cast<int>(durationSeconds) << "s\n\n"; std::cout << "MEMORY LEAK HUNTER SUMMARY\n";
std::cout << "================================================================================\n\n";
std::cout << "Reloads:\n";
std::cout << " Total: " << g_reloadCount << "\n"; std::cout << "Duration: " << static_cast<int>(durationSeconds) << "s\n\n";
std::cout << " Successes: " << g_reloadSuccesses;
if (g_reloadCount > 0) { std::cout << "Reloads:\n";
float successRate = (g_reloadSuccesses * 100.0f) / g_reloadCount; std::cout << " Total: " << g_reloadCount << "\n";
std::cout << " (" << std::fixed << std::setprecision(1) << successRate << "%)"; std::cout << " Successes: " << g_reloadSuccesses;
} if (g_reloadCount > 0) {
std::cout << "\n"; float successRate = (g_reloadSuccesses * 100.0f) / g_reloadCount;
std::cout << " Failures: " << g_reloadFailures << "\n\n"; std::cout << " (" << std::fixed << std::setprecision(1) << successRate << "%)";
}
std::cout << "Memory Analysis:\n"; std::cout << "\n";
std::cout << " Baseline: " << std::fixed << std::setprecision(1) << baselineMB << " MB\n"; std::cout << " Failures: " << g_reloadFailures << "\n\n";
std::cout << " Final: " << finalMB << " MB\n";
std::cout << " Growth: " << growthMB << " MB"; std::cout << "Memory Analysis:\n";
std::cout << " Baseline: " << std::fixed << std::setprecision(1) << baselineMB << " MB\n";
if (growthMB < 10.0f) { std::cout << " Final: " << finalMB << " MB\n";
std::cout << ""; std::cout << " Growth: " << growthMB << " MB";
} else {
std::cout << ""; if (growthMB < 10.0f) {
} std::cout << "";
std::cout << "\n"; } else {
std::cout << "";
float memoryPerReloadKB = 0.0f; }
if (g_reloadCount > 0) { std::cout << "\n";
memoryPerReloadKB = (growthMB * 1024.0f) / g_reloadCount;
} float memoryPerReloadKB = 0.0f;
std::cout << " Per reload: " << std::fixed << std::setprecision(1) << memoryPerReloadKB << " KB"; if (g_reloadCount > 0) {
if (memoryPerReloadKB < 50.0f) { memoryPerReloadKB = (growthMB * 1024.0f) / g_reloadCount;
std::cout << ""; }
} else { std::cout << " Per reload: " << std::fixed << std::setprecision(1) << memoryPerReloadKB << " KB";
std::cout << ""; if (memoryPerReloadKB < 50.0f) {
} std::cout << "";
std::cout << "\n\n"; } else {
std::cout << "";
std::cout << "Resource Cleanup:\n"; }
std::cout << " Temp files: " << finalTempFiles; std::cout << "\n\n";
if (finalTempFiles <= 2) {
std::cout << ""; std::cout << "Resource Cleanup:\n";
} else { std::cout << " Temp files: " << finalTempFiles;
std::cout << ""; if (finalTempFiles <= 2) {
} std::cout << "";
std::cout << "\n"; } else {
std::cout << "";
std::cout << " Mapped .so: " << finalMappedLibs; }
if (finalMappedLibs <= baselineMappedLibs + 2) { std::cout << "\n";
std::cout << " (stable) ✅";
} else { std::cout << " Mapped .so: " << finalMappedLibs;
std::cout << " (leak: +" << (finalMappedLibs - baselineMappedLibs) << ") ❌"; if (finalMappedLibs <= baselineMappedLibs + 2) {
} std::cout << " (stable) ✅";
std::cout << "\n\n"; } else {
std::cout << " (leak: +" << (finalMappedLibs - baselineMappedLibs) << ") ❌";
std::cout << "Stability:\n"; }
std::cout << " Crashes: " << g_crashes; std::cout << "\n\n";
if (g_crashes == 0) {
std::cout << ""; std::cout << "Stability:\n";
} else { std::cout << " Crashes: " << g_crashes;
std::cout << ""; if (g_crashes == 0) {
} std::cout << "";
std::cout << "\n\n"; } else {
std::cout << "";
// ======================================================================== }
// Validation std::cout << "\n\n";
// ========================================================================
// ========================================================================
std::cout << "Validating results...\n"; // Validation
// ========================================================================
bool passed = true;
std::cout << "Validating results...\n";
// 1. Memory growth < 10 MB
if (growthMB > 10.0f) { bool passed = true;
std::cout << " ❌ Memory growth too high: " << growthMB << " MB (need < 10 MB)\n";
passed = false; // 1. Memory growth < 10 MB
} else { if (growthMB > 10.0f) {
std::cout << " ✓ Memory growth: " << growthMB << " MB (< 10 MB)\n"; std::cout << " ❌ Memory growth too high: " << growthMB << " MB (need < 10 MB)\n";
} passed = false;
} else {
// 2. Memory per reload < 50 KB std::cout << " ✓ Memory growth: " << growthMB << " MB (< 10 MB)\n";
if (memoryPerReloadKB > 50.0f) { }
std::cout << " ❌ Memory per reload too high: " << memoryPerReloadKB << " KB (need < 50 KB)\n";
passed = false; // 2. Memory per reload < 50 KB
} else { if (memoryPerReloadKB > 50.0f) {
std::cout << " ✓ Memory per reload: " << memoryPerReloadKB << " KB (< 50 KB)\n"; std::cout << " ❌ Memory per reload too high: " << memoryPerReloadKB << " KB (need < 50 KB)\n";
} passed = false;
} else {
// 3. Temp files cleaned std::cout << " ✓ Memory per reload: " << memoryPerReloadKB << " KB (< 50 KB)\n";
if (finalTempFiles > 2) { }
std::cout << " ❌ Temp files not cleaned up: " << finalTempFiles << " (need ≤ 2)\n";
passed = false; // 3. Temp files cleaned
} else { if (finalTempFiles > 2) {
std::cout << " ✓ Temp files cleaned: " << finalTempFiles << " (≤ 2)\n"; std::cout << " ❌ Temp files not cleaned up: " << finalTempFiles << " (need ≤ 2)\n";
} passed = false;
} else {
// 4. No .so handle leaks std::cout << " ✓ Temp files cleaned: " << finalTempFiles << " (≤ 2)\n";
if (finalMappedLibs > baselineMappedLibs + 2) { }
std::cout << " ❌ Library handle leak: +" << (finalMappedLibs - baselineMappedLibs) << "\n";
passed = false; // 4. No .so handle leaks
} else { if (finalMappedLibs > baselineMappedLibs + 2) {
std::cout << " ✓ No .so handle leaks\n"; std::cout << " ❌ Library handle leak: +" << (finalMappedLibs - baselineMappedLibs) << "\n";
} passed = false;
} else {
// 5. Reload success rate std::cout << " ✓ No .so handle leaks\n";
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) // 5. Reload success rate
<< successRate << "% (need 100%)\n"; float successRate = g_reloadCount > 0 ? (g_reloadSuccesses * 100.0f) / g_reloadCount : 0.0f;
passed = false; if (successRate < 100.0f) {
} else { std::cout << " ❌ Reload success rate: " << std::fixed << std::setprecision(1)
std::cout << " ✓ Reload success rate: 100%\n"; << successRate << "% (need 100%)\n";
} passed = false;
} else {
// 6. No crashes std::cout << " ✓ Reload success rate: 100%\n";
if (g_crashes > 0) { }
std::cout << " ❌ Crashes detected: " << g_crashes << "\n";
passed = false; // 6. No crashes
} else { if (g_crashes > 0) {
std::cout << " ✓ No crashes\n"; std::cout << " ❌ Crashes detected: " << g_crashes << "\n";
} passed = false;
} else {
std::cout << "\n================================================================================\n"; std::cout << " ✓ No crashes\n";
if (passed) { }
std::cout << "Result: ✅ PASSED\n";
} else { std::cout << "\n================================================================================\n";
std::cout << "Result: ❌ FAILED\n"; if (passed) {
} std::cout << "Result: ✅ PASSED\n";
std::cout << "================================================================================\n"; } else {
std::cout << "Result: ❌ FAILED\n";
return passed ? 0 : 1; }
} std::cout << "================================================================================\n";
return passed ? 0 : 1;
}

View File

@ -1,272 +1,275 @@
#include "grove/ModuleLoader.h" #include "grove/ModuleLoader.h"
#include "grove/SequentialModuleSystem.h" #include "grove/SequentialModuleSystem.h"
#include "grove/JsonDataNode.h" #include "grove/JsonDataNode.h"
#include "../helpers/TestMetrics.h" #include "../helpers/TestMetrics.h"
#include "../helpers/TestAssertions.h" #include "../helpers/TestAssertions.h"
#include "../helpers/TestReporter.h" #include "../helpers/TestReporter.h"
#include "../helpers/SystemUtils.h" #include "../helpers/SystemUtils.h"
#include <iostream> #include <iostream>
#include <chrono> #include <chrono>
#include <thread> #include <thread>
#include <stdexcept> #include <stdexcept>
using namespace grove; using namespace grove;
/** /**
* Test 06: Error Recovery * Test 06: Error Recovery
* *
* Objectif: Valider que le système peut détecter et récupérer automatiquement * Objectif: Valider que le système peut détecter et récupérer automatiquement
* d'un crash de module via hot-reload. * d'un crash de module via hot-reload.
* *
* Scénario: * Scénario:
* 1. Charger ErrorRecoveryModule avec crash planifié à frame 60 * 1. Charger ErrorRecoveryModule avec crash planifié à frame 60
* 2. Lancer execution jusqu'au crash * 2. Lancer execution jusqu'au crash
* 3. Détecter le crash (exception) * 3. Détecter le crash (exception)
* 4. Trigger hot-reload automatique * 4. Trigger hot-reload automatique
* 5. Vérifier que le module récupère (auto-recovery) * 5. Vérifier que le module récupère (auto-recovery)
* 6. Continuer execution normalement * 6. Continuer execution normalement
* *
* Métriques: * Métriques:
* - Crash detection time * - Crash detection time
* - Recovery success rate * - Recovery success rate
* - State preservation après recovery * - State preservation après recovery
* - Stabilité du moteur * - Stabilité du moteur
*/ */
int main() { int main() {
TestReporter reporter("Error Recovery"); TestReporter reporter("Error Recovery");
TestMetrics metrics; TestMetrics metrics;
std::cout << "================================================================================\n"; std::cout << "================================================================================\n";
std::cout << "TEST: Error Recovery - Crash Detection & Auto-Recovery\n"; std::cout << "TEST: Error Recovery - Crash Detection & Auto-Recovery\n";
std::cout << "================================================================================\n\n"; std::cout << "================================================================================\n\n";
// === SETUP === // === SETUP ===
std::cout << "Setup: Loading ErrorRecoveryModule with crash trigger...\n"; std::cout << "Setup: Loading ErrorRecoveryModule with crash trigger...\n";
ModuleLoader loader; ModuleLoader loader;
auto moduleSystem = std::make_unique<SequentialModuleSystem>(); auto moduleSystem = std::make_unique<SequentialModuleSystem>();
// Charger module // Charger module
std::string modulePath = "./libErrorRecoveryModule.so"; std::string modulePath = "./libErrorRecoveryModule.so";
auto module = loader.load(modulePath, "ErrorRecoveryModule", false); #ifdef _WIN32
modulePath = "./libErrorRecoveryModule.dll";
// Config: crash à frame 60, type runtime_error #endif
nlohmann::json configJson; auto module = loader.load(modulePath, "ErrorRecoveryModule", false);
configJson["crashAtFrame"] = 60;
configJson["crashType"] = 0; // runtime_error // Config: crash à frame 60, type runtime_error
configJson["enableAutoRecovery"] = true; nlohmann::json configJson;
configJson["versionTag"] = "v1.0"; configJson["crashAtFrame"] = 60;
auto config = std::make_unique<JsonDataNode>("config", configJson); configJson["crashType"] = 0; // runtime_error
configJson["enableAutoRecovery"] = true;
module->setConfiguration(*config, nullptr, nullptr); configJson["versionTag"] = "v1.0";
moduleSystem->registerModule("ErrorRecoveryModule", std::move(module)); auto config = std::make_unique<JsonDataNode>("config", configJson);
std::cout << " ✓ Module loaded with crash trigger at frame 60\n\n"; module->setConfiguration(*config, nullptr, nullptr);
moduleSystem->registerModule("ErrorRecoveryModule", std::move(module));
// === PHASE 1: Run until crash ===
std::cout << "Phase 1: Running until crash (target frame: 60)...\n"; std::cout << " ✓ Module loaded with crash trigger at frame 60\n\n";
bool crashDetected = false; // === PHASE 1: Run until crash ===
int crashFrame = -1; std::cout << "Phase 1: Running until crash (target frame: 60)...\n";
auto crashDetectionStart = std::chrono::high_resolution_clock::now();
bool crashDetected = false;
for (int frame = 1; frame <= 100; frame++) { int crashFrame = -1;
try { auto crashDetectionStart = std::chrono::high_resolution_clock::now();
auto frameStart = std::chrono::high_resolution_clock::now();
for (int frame = 1; frame <= 100; frame++) {
moduleSystem->processModules(1.0f / 60.0f); try {
auto frameStart = std::chrono::high_resolution_clock::now();
auto frameEnd = std::chrono::high_resolution_clock::now();
float frameTime = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count(); moduleSystem->processModules(1.0f / 60.0f);
metrics.recordFPS(1000.0f / frameTime);
auto frameEnd = std::chrono::high_resolution_clock::now();
if (frame % 20 == 0) { float frameTime = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count();
std::cout << " Frame " << frame << "/100 - OK\n"; metrics.recordFPS(1000.0f / frameTime);
}
if (frame % 20 == 0) {
} catch (const std::exception& e) { std::cout << " Frame " << frame << "/100 - OK\n";
// CRASH DÉTECTÉ ! }
auto crashDetectionEnd = std::chrono::high_resolution_clock::now();
float detectionTime = std::chrono::duration<float, std::milli>( } catch (const std::exception& e) {
crashDetectionEnd - crashDetectionStart).count(); // CRASH DÉTECTÉ !
auto crashDetectionEnd = std::chrono::high_resolution_clock::now();
crashDetected = true; float detectionTime = std::chrono::duration<float, std::milli>(
crashFrame = frame; crashDetectionEnd - crashDetectionStart).count();
std::cout << "\n💥 CRASH DETECTED at frame " << frame << "\n"; crashDetected = true;
std::cout << " Exception: " << e.what() << "\n"; crashFrame = frame;
std::cout << " Detection time: " << detectionTime << "ms\n\n";
std::cout << "\n💥 CRASH DETECTED at frame " << frame << "\n";
metrics.recordCrash("runtime_error at frame " + std::to_string(frame)); std::cout << " Exception: " << e.what() << "\n";
reporter.addMetric("crash_detection_time_ms", detectionTime); std::cout << " Detection time: " << detectionTime << "ms\n\n";
break; 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); ASSERT_TRUE(crashDetected, "Crash should have been detected");
ASSERT_EQ(crashFrame, 60, "Crash should occur at frame 60");
// === PHASE 2: Extract state before recovery === reporter.addAssertion("crash_detected", crashDetected);
std::cout << "Phase 2: Extracting state before recovery...\n"; reporter.addAssertion("crash_at_expected_frame", crashFrame == 60);
auto crashedModule = moduleSystem->extractModule(); // === PHASE 2: Extract state before recovery ===
auto preRecoveryState = crashedModule->getState(); std::cout << "Phase 2: Extracting state before recovery...\n";
auto* jsonNodeBefore = dynamic_cast<JsonDataNode*>(preRecoveryState.get()); auto crashedModule = moduleSystem->extractModule();
if (!jsonNodeBefore) { auto preRecoveryState = crashedModule->getState();
std::cerr << "❌ Failed to extract state before recovery\n";
return 1; auto* jsonNodeBefore = dynamic_cast<JsonDataNode*>(preRecoveryState.get());
} if (!jsonNodeBefore) {
std::cerr << "❌ Failed to extract state before recovery\n";
const auto& stateBefore = jsonNodeBefore->getJsonData(); return 1;
int frameCountBefore = stateBefore.value("frameCount", 0); }
int crashCountBefore = stateBefore.value("crashCount", 0);
bool hasCrashedBefore = stateBefore.value("hasCrashed", false); const auto& stateBefore = jsonNodeBefore->getJsonData();
int frameCountBefore = stateBefore.value("frameCount", 0);
std::cout << " State before recovery:\n"; int crashCountBefore = stateBefore.value("crashCount", 0);
std::cout << " Frame count: " << frameCountBefore << "\n"; bool hasCrashedBefore = stateBefore.value("hasCrashed", false);
std::cout << " Crash count: " << crashCountBefore << "\n";
std::cout << " Has crashed: " << (hasCrashedBefore ? "YES" : "NO") << "\n\n"; std::cout << " State before recovery:\n";
std::cout << " Frame count: " << frameCountBefore << "\n";
ASSERT_TRUE(hasCrashedBefore, "Module should be in crashed state"); std::cout << " Crash count: " << crashCountBefore << "\n";
std::cout << " Has crashed: " << (hasCrashedBefore ? "YES" : "NO") << "\n\n";
// === PHASE 3: Trigger hot-reload (recovery) ===
std::cout << "Phase 3: Triggering hot-reload for recovery...\n"; ASSERT_TRUE(hasCrashedBefore, "Module should be in crashed state");
auto recoveryStart = std::chrono::high_resolution_clock::now(); // === PHASE 3: Trigger hot-reload (recovery) ===
std::cout << "Phase 3: Triggering hot-reload for recovery...\n";
// Hot-reload via ModuleLoader
auto recoveredModule = loader.reload(std::move(crashedModule)); auto recoveryStart = std::chrono::high_resolution_clock::now();
auto recoveryEnd = std::chrono::high_resolution_clock::now(); // Hot-reload via ModuleLoader
float recoveryTime = std::chrono::duration<float, std::milli>(recoveryEnd - recoveryStart).count(); auto recoveredModule = loader.reload(std::move(crashedModule));
std::cout << " ✓ Hot-reload completed in " << recoveryTime << "ms\n"; auto recoveryEnd = std::chrono::high_resolution_clock::now();
float recoveryTime = std::chrono::duration<float, std::milli>(recoveryEnd - recoveryStart).count();
metrics.recordReloadTime(recoveryTime);
reporter.addMetric("recovery_time_ms", recoveryTime); std::cout << " ✓ Hot-reload completed in " << recoveryTime << "ms\n";
// Ré-enregistrer module récupéré metrics.recordReloadTime(recoveryTime);
moduleSystem->registerModule("ErrorRecoveryModule", std::move(recoveredModule)); reporter.addMetric("recovery_time_ms", recoveryTime);
// === PHASE 4: Verify recovery === // Ré-enregistrer module récupéré
std::cout << "\nPhase 4: Verifying recovery...\n"; moduleSystem->registerModule("ErrorRecoveryModule", std::move(recoveredModule));
auto recoveredModuleRef = moduleSystem->extractModule(); // === PHASE 4: Verify recovery ===
auto postRecoveryState = recoveredModuleRef->getState(); std::cout << "\nPhase 4: Verifying recovery...\n";
auto* jsonNodeAfter = dynamic_cast<JsonDataNode*>(postRecoveryState.get()); auto recoveredModuleRef = moduleSystem->extractModule();
if (!jsonNodeAfter) { auto postRecoveryState = recoveredModuleRef->getState();
std::cerr << "❌ Failed to extract state after recovery\n";
return 1; auto* jsonNodeAfter = dynamic_cast<JsonDataNode*>(postRecoveryState.get());
} if (!jsonNodeAfter) {
std::cerr << "❌ Failed to extract state after recovery\n";
const auto& stateAfter = jsonNodeAfter->getJsonData(); return 1;
int frameCountAfter = stateAfter.value("frameCount", 0); }
int crashCountAfter = stateAfter.value("crashCount", 0);
int recoveryCountAfter = stateAfter.value("recoveryCount", 0); const auto& stateAfter = jsonNodeAfter->getJsonData();
bool hasCrashedAfter = stateAfter.value("hasCrashed", false); int frameCountAfter = stateAfter.value("frameCount", 0);
int crashAtFrameAfter = stateAfter.value("crashAtFrame", -1); int crashCountAfter = stateAfter.value("crashCount", 0);
int recoveryCountAfter = stateAfter.value("recoveryCount", 0);
std::cout << " State after recovery:\n"; bool hasCrashedAfter = stateAfter.value("hasCrashed", false);
std::cout << " Frame count: " << frameCountAfter << "\n"; int crashAtFrameAfter = stateAfter.value("crashAtFrame", -1);
std::cout << " Crash count: " << crashCountAfter << "\n";
std::cout << " Recovery count: " << recoveryCountAfter << "\n"; std::cout << " State after recovery:\n";
std::cout << " Has crashed: " << (hasCrashedAfter ? "YES" : "NO") << "\n"; std::cout << " Frame count: " << frameCountAfter << "\n";
std::cout << " Crash trigger: " << crashAtFrameAfter << "\n\n"; std::cout << " Crash count: " << crashCountAfter << "\n";
std::cout << " Recovery count: " << recoveryCountAfter << "\n";
// Vérifications de recovery std::cout << " Has crashed: " << (hasCrashedAfter ? "YES" : "NO") << "\n";
ASSERT_EQ(frameCountAfter, frameCountBefore, "Frame count should be preserved"); std::cout << " Crash trigger: " << crashAtFrameAfter << "\n\n";
ASSERT_FALSE(hasCrashedAfter, "Module should no longer be in crashed state");
ASSERT_EQ(recoveryCountAfter, 1, "Recovery count should be 1"); // Vérifications de recovery
ASSERT_EQ(crashAtFrameAfter, -1, "Crash trigger should be disabled"); ASSERT_EQ(frameCountAfter, frameCountBefore, "Frame count should be preserved");
ASSERT_FALSE(hasCrashedAfter, "Module should no longer be in crashed state");
reporter.addAssertion("frame_count_preserved", frameCountAfter == frameCountBefore); ASSERT_EQ(recoveryCountAfter, 1, "Recovery count should be 1");
reporter.addAssertion("crash_state_cleared", !hasCrashedAfter); ASSERT_EQ(crashAtFrameAfter, -1, "Crash trigger should be disabled");
reporter.addAssertion("recovery_count_incremented", recoveryCountAfter == 1);
reporter.addAssertion("crash_trigger_disabled", crashAtFrameAfter == -1); reporter.addAssertion("frame_count_preserved", frameCountAfter == frameCountBefore);
reporter.addAssertion("crash_state_cleared", !hasCrashedAfter);
std::cout << " ✅ RECOVERY SUCCESSFUL - Module is healthy again\n\n"; reporter.addAssertion("recovery_count_incremented", recoveryCountAfter == 1);
reporter.addAssertion("crash_trigger_disabled", crashAtFrameAfter == -1);
// Ré-enregistrer pour phase 5
moduleSystem->registerModule("ErrorRecoveryModule", std::move(recoveredModuleRef)); std::cout << " ✅ RECOVERY SUCCESSFUL - Module is healthy again\n\n";
// === PHASE 5: Continue execution (stability check) === // Ré-enregistrer pour phase 5
std::cout << "Phase 5: Stability check - Running 120 more frames...\n"; moduleSystem->registerModule("ErrorRecoveryModule", std::move(recoveredModuleRef));
bool stableExecution = true; // === PHASE 5: Continue execution (stability check) ===
int framesAfterRecovery = 0; std::cout << "Phase 5: Stability check - Running 120 more frames...\n";
for (int frame = 1; frame <= 120; frame++) { bool stableExecution = true;
try { int framesAfterRecovery = 0;
auto frameStart = std::chrono::high_resolution_clock::now();
for (int frame = 1; frame <= 120; frame++) {
moduleSystem->processModules(1.0f / 60.0f); try {
auto frameStart = std::chrono::high_resolution_clock::now();
auto frameEnd = std::chrono::high_resolution_clock::now();
float frameTime = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count(); moduleSystem->processModules(1.0f / 60.0f);
metrics.recordFPS(1000.0f / frameTime);
auto frameEnd = std::chrono::high_resolution_clock::now();
framesAfterRecovery++; float frameTime = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count();
metrics.recordFPS(1000.0f / frameTime);
if (frame % 30 == 0) {
std::cout << " Frame " << frame << "/120 - Stable\n"; framesAfterRecovery++;
}
if (frame % 30 == 0) {
} catch (const std::exception& e) { std::cout << " Frame " << frame << "/120 - Stable\n";
std::cout << "\n❌ UNEXPECTED CRASH after recovery at frame " << frame << "\n"; }
std::cout << " Exception: " << e.what() << "\n";
stableExecution = false; } catch (const std::exception& e) {
break; 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); ASSERT_TRUE(stableExecution, "Module should execute stably after recovery");
reporter.addMetric("frames_after_recovery", static_cast<float>(framesAfterRecovery)); ASSERT_EQ(framesAfterRecovery, 120, "Should complete all 120 frames");
std::cout << " ✅ Stability verified - " << framesAfterRecovery << " frames executed without issues\n\n"; reporter.addAssertion("stable_after_recovery", stableExecution);
reporter.addMetric("frames_after_recovery", static_cast<float>(framesAfterRecovery));
// === VÉRIFICATIONS FINALES ===
std::cout << "Final verifications...\n"; std::cout << " ✅ Stability verified - " << framesAfterRecovery << " frames executed without issues\n\n";
// Memory growth // === VÉRIFICATIONS FINALES ===
size_t memGrowth = metrics.getMemoryGrowth(); std::cout << "Final verifications...\n";
float memGrowthMB = memGrowth / (1024.0f * 1024.0f);
ASSERT_LT(memGrowthMB, 10.0f, "Memory growth should be < 10MB"); // Memory growth
reporter.addMetric("memory_growth_mb", memGrowthMB); size_t memGrowth = metrics.getMemoryGrowth();
float memGrowthMB = memGrowth / (1024.0f * 1024.0f);
// FPS (moins strict pour test de recovery - focus sur stability) ASSERT_LT(memGrowthMB, 10.0f, "Memory growth should be < 10MB");
float minFPS = metrics.getFPSMin(); reporter.addMetric("memory_growth_mb", memGrowthMB);
ASSERT_GT(minFPS, 5.0f, "Min FPS should be > 5 (recovery test allows slower frames)");
reporter.addMetric("fps_min", minFPS); // FPS (moins strict pour test de recovery - focus sur stability)
reporter.addMetric("fps_avg", metrics.getFPSAvg()); float minFPS = metrics.getFPSMin();
ASSERT_GT(minFPS, 5.0f, "Min FPS should be > 5 (recovery test allows slower frames)");
// Recovery time threshold reporter.addMetric("fps_min", minFPS);
ASSERT_LT(recoveryTime, 500.0f, "Recovery time should be < 500ms"); reporter.addMetric("fps_avg", metrics.getFPSAvg());
// Crash count // Recovery time threshold
int totalCrashes = metrics.getCrashCount(); ASSERT_LT(recoveryTime, 500.0f, "Recovery time should be < 500ms");
ASSERT_EQ(totalCrashes, 1, "Should have exactly 1 controlled crash");
reporter.addMetric("total_crashes", static_cast<float>(totalCrashes)); // Crash count
int totalCrashes = metrics.getCrashCount();
// === RAPPORTS === ASSERT_EQ(totalCrashes, 1, "Should have exactly 1 controlled crash");
std::cout << "\n"; reporter.addMetric("total_crashes", static_cast<float>(totalCrashes));
std::cout << "Summary:\n";
std::cout << " 🎯 Crash detected at frame " << crashFrame << " (expected: 60)\n"; // === RAPPORTS ===
std::cout << " 🔄 Recovery time: " << recoveryTime << "ms\n"; std::cout << "\n";
std::cout << " ✅ Stable execution: " << framesAfterRecovery << " frames after recovery\n"; std::cout << "Summary:\n";
std::cout << " 💾 Memory growth: " << memGrowthMB << " MB\n"; std::cout << " 🎯 Crash detected at frame " << crashFrame << " (expected: 60)\n";
std::cout << " 📊 FPS: min=" << minFPS << ", avg=" << metrics.getFPSAvg() << "\n\n"; std::cout << " 🔄 Recovery time: " << recoveryTime << "ms\n";
std::cout << " ✅ Stable execution: " << framesAfterRecovery << " frames after recovery\n";
metrics.printReport(); std::cout << " 💾 Memory growth: " << memGrowthMB << " MB\n";
reporter.printFinalReport(); std::cout << " 📊 FPS: min=" << minFPS << ", avg=" << metrics.getFPSAvg() << "\n\n";
return reporter.getExitCode(); metrics.printReport();
} reporter.printFinalReport();
return reporter.getExitCode();
}

View File

@ -1,418 +1,421 @@
#include "grove/ModuleLoader.h" #include "grove/ModuleLoader.h"
#include "grove/SequentialModuleSystem.h" #include "grove/SequentialModuleSystem.h"
#include "grove/JsonDataNode.h" #include "grove/JsonDataNode.h"
#include "../helpers/TestMetrics.h" #include "../helpers/TestMetrics.h"
#include "../helpers/TestAssertions.h" #include "../helpers/TestAssertions.h"
#include "../helpers/TestReporter.h" #include "../helpers/TestReporter.h"
#include "../helpers/SystemUtils.h" #include "../helpers/SystemUtils.h"
#include <iostream> #include <iostream>
#include <chrono> #include <chrono>
#include <thread> #include <thread>
#include <stdexcept> #include <stdexcept>
#include <numeric> #include <numeric>
#include <fstream> #include <fstream>
using namespace grove; using namespace grove;
/** /**
* Test 07: Limite Tests * Test 07: Limite Tests
* *
* Objectif: Valider la robustesse du système face aux conditions extrêmes: * Objectif: Valider la robustesse du système face aux conditions extrêmes:
* - Large state (100MB+) * - Large state (100MB+)
* - Long initialization * - Long initialization
* - Timeouts * - Timeouts
* - Memory pressure * - Memory pressure
* - State corruption detection * - State corruption detection
*/ */
int main() { int main() {
TestReporter reporter("Limite Tests"); TestReporter reporter("Limite Tests");
TestMetrics metrics; TestMetrics metrics;
std::cout << "================================================================================\n"; std::cout << "================================================================================\n";
std::cout << "TEST: Limite Tests - Extreme Conditions & Edge Cases\n"; std::cout << "TEST: Limite Tests - Extreme Conditions & Edge Cases\n";
std::cout << "================================================================================\n\n"; std::cout << "================================================================================\n\n";
// ======================================================================== // ========================================================================
// TEST 1: Large State Serialization // TEST 1: Large State Serialization
// ======================================================================== // ========================================================================
std::cout << "=== TEST 1: Large State Serialization ===\n\n"; std::cout << "=== TEST 1: Large State Serialization ===\n\n";
ModuleLoader loader; ModuleLoader loader;
auto moduleSystem = std::make_unique<SequentialModuleSystem>(); auto moduleSystem = std::make_unique<SequentialModuleSystem>();
std::string modulePath = "./libHeavyStateModule.so"; std::string modulePath = "./libHeavyStateModule.so";
auto module = loader.load(modulePath, "HeavyStateModule", false); #ifdef _WIN32
modulePath = "./libHeavyStateModule.dll";
// Config: particules réduites pour test rapide, mais assez pour être significatif #endif
nlohmann::json configJson; auto module = loader.load(modulePath, "HeavyStateModule", false);
configJson["version"] = "v1.0";
configJson["particleCount"] = 100000; // 100k au lieu de 1M pour test rapide // Config: particules réduites pour test rapide, mais assez pour être significatif
configJson["terrainSize"] = 1000; // 1000x1000 au lieu de 10000x10000 nlohmann::json configJson;
configJson["initDuration"] = 2.0f; // 2s au lieu de 8s configJson["version"] = "v1.0";
configJson["initTimeout"] = 5.0f; configJson["particleCount"] = 100000; // 100k au lieu de 1M pour test rapide
configJson["terrainSize"] = 1000; // 1000x1000 au lieu de 10000x10000
auto config = std::make_unique<JsonDataNode>("config", configJson); configJson["initDuration"] = 2.0f; // 2s au lieu de 8s
configJson["initTimeout"] = 5.0f;
// Mesurer temps d'initialisation
std::cout << "Initializing module...\n"; auto config = std::make_unique<JsonDataNode>("config", configJson);
auto initStart = std::chrono::high_resolution_clock::now();
// Mesurer temps d'initialisation
module->setConfiguration(*config, nullptr, nullptr); std::cout << "Initializing module...\n";
auto initStart = std::chrono::high_resolution_clock::now();
auto initEnd = std::chrono::high_resolution_clock::now();
float initTime = std::chrono::duration<float>(initEnd - initStart).count(); module->setConfiguration(*config, nullptr, nullptr);
std::cout << " Init time: " << initTime << "s\n"; auto initEnd = std::chrono::high_resolution_clock::now();
ASSERT_GT(initTime, 1.5f, "Init should take at least 1.5s (simulated heavy init)"); float initTime = std::chrono::duration<float>(initEnd - initStart).count();
ASSERT_LT(initTime, 3.0f, "Init should not exceed 3s");
reporter.addMetric("init_time_s", initTime); std::cout << " Init time: " << initTime << "s\n";
ASSERT_GT(initTime, 1.5f, "Init should take at least 1.5s (simulated heavy init)");
moduleSystem->registerModule("HeavyStateModule", std::move(module)); ASSERT_LT(initTime, 3.0f, "Init should not exceed 3s");
reporter.addMetric("init_time_s", initTime);
// Exécuter quelques frames
std::cout << "Running 300 frames...\n"; moduleSystem->registerModule("HeavyStateModule", std::move(module));
for (int i = 0; i < 300; i++) {
moduleSystem->processModules(1.0f / 60.0f); // Exécuter quelques frames
} std::cout << "Running 300 frames...\n";
for (int i = 0; i < 300; i++) {
// Mesurer temps de getState() moduleSystem->processModules(1.0f / 60.0f);
std::cout << "Extracting state (getState)...\n"; }
auto getStateStart = std::chrono::high_resolution_clock::now();
// Mesurer temps de getState()
auto heavyModule = moduleSystem->extractModule(); std::cout << "Extracting state (getState)...\n";
auto state = heavyModule->getState(); auto getStateStart = std::chrono::high_resolution_clock::now();
auto getStateEnd = std::chrono::high_resolution_clock::now(); auto heavyModule = moduleSystem->extractModule();
float getStateTime = std::chrono::duration<float, std::milli>(getStateEnd - getStateStart).count(); auto state = heavyModule->getState();
std::cout << " getState time: " << getStateTime << "ms\n"; auto getStateEnd = std::chrono::high_resolution_clock::now();
ASSERT_LT(getStateTime, 2000.0f, "getState() should be < 2000ms"); float getStateTime = std::chrono::duration<float, std::milli>(getStateEnd - getStateStart).count();
reporter.addMetric("getstate_time_ms", getStateTime);
std::cout << " getState time: " << getStateTime << "ms\n";
// Estimer taille de l'état ASSERT_LT(getStateTime, 2000.0f, "getState() should be < 2000ms");
auto* jsonNode = dynamic_cast<JsonDataNode*>(state.get()); reporter.addMetric("getstate_time_ms", getStateTime);
if (jsonNode) {
std::string stateStr = jsonNode->getJsonData().dump(); // Estimer taille de l'état
size_t stateSize = stateStr.size(); auto* jsonNode = dynamic_cast<JsonDataNode*>(state.get());
float stateSizeMB = stateSize / 1024.0f / 1024.0f; if (jsonNode) {
std::cout << " State size: " << stateSizeMB << " MB\n"; std::string stateStr = jsonNode->getJsonData().dump();
reporter.addMetric("state_size_mb", stateSizeMB); size_t stateSize = stateStr.size();
} float stateSizeMB = stateSize / 1024.0f / 1024.0f;
std::cout << " State size: " << stateSizeMB << " MB\n";
// Recharger le module (simuler hot-reload) reporter.addMetric("state_size_mb", stateSizeMB);
std::cout << "Reloading module...\n"; }
auto reloadStart = std::chrono::high_resolution_clock::now();
// Recharger le module (simuler hot-reload)
// setState() est appelé automatiquement par reload() std::cout << "Reloading module...\n";
auto moduleReloaded = loader.reload(std::move(heavyModule)); auto reloadStart = std::chrono::high_resolution_clock::now();
auto reloadEnd = std::chrono::high_resolution_clock::now(); // setState() est appelé automatiquement par reload()
float reloadTime = std::chrono::duration<float, std::milli>(reloadEnd - reloadStart).count(); auto moduleReloaded = loader.reload(std::move(heavyModule));
std::cout << " Total reload time: " << reloadTime << "ms\n";
reporter.addMetric("reload_time_ms", reloadTime); auto reloadEnd = std::chrono::high_resolution_clock::now();
float reloadTime = std::chrono::duration<float, std::milli>(reloadEnd - reloadStart).count();
// Ré-enregistrer std::cout << " Total reload time: " << reloadTime << "ms\n";
moduleSystem->registerModule("HeavyStateModule", std::move(moduleReloaded)); reporter.addMetric("reload_time_ms", reloadTime);
// Vérifier intégrité après reload // Ré-enregistrer
auto heavyModuleAfter = moduleSystem->extractModule(); moduleSystem->registerModule("HeavyStateModule", std::move(moduleReloaded));
auto stateAfter = heavyModuleAfter->getState();
auto* jsonNodeAfter = dynamic_cast<JsonDataNode*>(stateAfter.get()); // Vérifier intégrité après reload
if (jsonNodeAfter) { auto heavyModuleAfter = moduleSystem->extractModule();
const auto& dataAfter = jsonNodeAfter->getJsonData(); auto stateAfter = heavyModuleAfter->getState();
int particleCount = dataAfter["config"]["particleCount"]; auto* jsonNodeAfter = dynamic_cast<JsonDataNode*>(stateAfter.get());
ASSERT_EQ(particleCount, 100000, "Should have 100k particles after reload"); if (jsonNodeAfter) {
reporter.addAssertion("particles_preserved", particleCount == 100000); const auto& dataAfter = jsonNodeAfter->getJsonData();
std::cout << " ✓ Particles preserved: " << particleCount << "\n"; int particleCount = dataAfter["config"]["particleCount"];
} ASSERT_EQ(particleCount, 100000, "Should have 100k particles after reload");
reporter.addAssertion("particles_preserved", particleCount == 100000);
// Ré-enregistrer pour continuer std::cout << " ✓ Particles preserved: " << particleCount << "\n";
moduleSystem->registerModule("HeavyStateModule", std::move(heavyModuleAfter)); }
// Continuer exécution // Ré-enregistrer pour continuer
std::cout << "Running 300 more frames post-reload...\n"; moduleSystem->registerModule("HeavyStateModule", std::move(heavyModuleAfter));
for (int i = 0; i < 300; i++) {
moduleSystem->processModules(1.0f / 60.0f); // Continuer exécution
} std::cout << "Running 300 more frames post-reload...\n";
for (int i = 0; i < 300; i++) {
std::cout << "\n✅ TEST 1 PASSED\n\n"; moduleSystem->processModules(1.0f / 60.0f);
}
// ========================================================================
// TEST 2: Long Initialization Timeout std::cout << "\n✅ TEST 1 PASSED\n\n";
// ========================================================================
std::cout << "=== TEST 2: Long Initialization Timeout ===\n\n"; // ========================================================================
// TEST 2: Long Initialization Timeout
auto moduleSystem2 = std::make_unique<SequentialModuleSystem>(); // ========================================================================
auto moduleTimeout = loader.load(modulePath, "HeavyStateModule", false); std::cout << "=== TEST 2: Long Initialization Timeout ===\n\n";
// Config avec init long + timeout court (va échouer) auto moduleSystem2 = std::make_unique<SequentialModuleSystem>();
nlohmann::json configTimeout; auto moduleTimeout = loader.load(modulePath, "HeavyStateModule", false);
configTimeout["version"] = "v1.0";
configTimeout["particleCount"] = 100000; // Config avec init long + timeout court (va échouer)
configTimeout["terrainSize"] = 1000; nlohmann::json configTimeout;
configTimeout["initDuration"] = 4.0f; // Init va prendre 4s configTimeout["version"] = "v1.0";
configTimeout["initTimeout"] = 3.0f; // Timeout à 3s (trop court) configTimeout["particleCount"] = 100000;
configTimeout["terrainSize"] = 1000;
auto configTimeoutNode = std::make_unique<JsonDataNode>("config", configTimeout); configTimeout["initDuration"] = 4.0f; // Init va prendre 4s
configTimeout["initTimeout"] = 3.0f; // Timeout à 3s (trop court)
bool timedOut = false;
std::cout << "Attempting init with timeout=3s (duration=4s)...\n"; auto configTimeoutNode = std::make_unique<JsonDataNode>("config", configTimeout);
try {
moduleTimeout->setConfiguration(*configTimeoutNode, nullptr, nullptr); bool timedOut = false;
} catch (const std::exception& e) { std::cout << "Attempting init with timeout=3s (duration=4s)...\n";
std::string msg = e.what(); try {
if (msg.find("timeout") != std::string::npos || moduleTimeout->setConfiguration(*configTimeoutNode, nullptr, nullptr);
msg.find("Timeout") != std::string::npos || } catch (const std::exception& e) {
msg.find("exceeded") != std::string::npos) { std::string msg = e.what();
timedOut = true; if (msg.find("timeout") != std::string::npos ||
std::cout << " ✓ Timeout detected: " << msg << "\n"; msg.find("Timeout") != std::string::npos ||
} else { msg.find("exceeded") != std::string::npos) {
std::cout << " ✗ Unexpected error: " << msg << "\n"; 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 ASSERT_TRUE(timedOut, "Should timeout when init > timeout threshold");
std::cout << "\nRetrying with adequate timeout=6s...\n"; reporter.addAssertion("timeout_detection", timedOut);
auto moduleTimeout2 = loader.load(modulePath, "HeavyStateModule", false);
// Réessayer avec timeout suffisant
nlohmann::json configOk; std::cout << "\nRetrying with adequate timeout=6s...\n";
configOk["version"] = "v1.0"; auto moduleTimeout2 = loader.load(modulePath, "HeavyStateModule", false);
configOk["particleCount"] = 50000;
configOk["terrainSize"] = 500; nlohmann::json configOk;
configOk["initDuration"] = 2.0f; configOk["version"] = "v1.0";
configOk["initTimeout"] = 5.0f; configOk["particleCount"] = 50000;
configOk["terrainSize"] = 500;
auto configOkNode = std::make_unique<JsonDataNode>("config", configOk); configOk["initDuration"] = 2.0f;
configOk["initTimeout"] = 5.0f;
bool success = true;
try { auto configOkNode = std::make_unique<JsonDataNode>("config", configOk);
moduleTimeout2->setConfiguration(*configOkNode, nullptr, nullptr);
moduleSystem2->registerModule("HeavyStateModule", std::move(moduleTimeout2)); bool success = true;
moduleSystem2->processModules(1.0f / 60.0f); try {
std::cout << " ✓ Init succeeded with adequate timeout\n"; moduleTimeout2->setConfiguration(*configOkNode, nullptr, nullptr);
} catch (const std::exception& e) { moduleSystem2->registerModule("HeavyStateModule", std::move(moduleTimeout2));
success = false; moduleSystem2->processModules(1.0f / 60.0f);
std::cout << " ✗ Failed: " << e.what() << "\n"; std::cout << " ✓ Init succeeded with adequate timeout\n";
} } catch (const std::exception& e) {
success = false;
ASSERT_TRUE(success, "Should succeed with adequate timeout"); std::cout << " ✗ Failed: " << e.what() << "\n";
reporter.addAssertion("timeout_recovery", success); }
std::cout << "\n✅ TEST 2 PASSED\n\n"; ASSERT_TRUE(success, "Should succeed with adequate timeout");
reporter.addAssertion("timeout_recovery", success);
// ========================================================================
// TEST 3: Memory Pressure During Reload std::cout << "\n✅ TEST 2 PASSED\n\n";
// ========================================================================
std::cout << "=== TEST 3: Memory Pressure During Reload ===\n\n"; // ========================================================================
// TEST 3: Memory Pressure During Reload
// Créer un nouveau system pour ce test // ========================================================================
auto moduleSystem3Pressure = std::make_unique<SequentialModuleSystem>(); std::cout << "=== TEST 3: Memory Pressure During Reload ===\n\n";
auto modulePressureInit = loader.load(modulePath, "HeavyStateModule", false);
// Créer un nouveau system pour ce test
nlohmann::json configPressure; auto moduleSystem3Pressure = std::make_unique<SequentialModuleSystem>();
configPressure["version"] = "v1.0"; auto modulePressureInit = loader.load(modulePath, "HeavyStateModule", false);
configPressure["particleCount"] = 10000;
configPressure["terrainSize"] = 100; nlohmann::json configPressure;
configPressure["initDuration"] = 0.5f; configPressure["version"] = "v1.0";
auto configPressureNode = std::make_unique<JsonDataNode>("config", configPressure); configPressure["particleCount"] = 10000;
modulePressureInit->setConfiguration(*configPressureNode, nullptr, nullptr); configPressure["terrainSize"] = 100;
moduleSystem3Pressure->registerModule("HeavyStateModule", std::move(modulePressureInit)); configPressure["initDuration"] = 0.5f;
auto configPressureNode = std::make_unique<JsonDataNode>("config", configPressure);
size_t memBefore = getCurrentMemoryUsage(); modulePressureInit->setConfiguration(*configPressureNode, nullptr, nullptr);
std::cout << "Memory before: " << (memBefore / 1024.0f / 1024.0f) << " MB\n"; moduleSystem3Pressure->registerModule("HeavyStateModule", std::move(modulePressureInit));
// Exécuter quelques frames size_t memBefore = getCurrentMemoryUsage();
std::cout << "Running 300 frames...\n"; std::cout << "Memory before: " << (memBefore / 1024.0f / 1024.0f) << " MB\n";
for (int i = 0; i < 300; i++) {
moduleSystem3Pressure->processModules(1.0f / 60.0f); // Exécuter quelques frames
} std::cout << "Running 300 frames...\n";
for (int i = 0; i < 300; i++) {
// Allouer temporairement beaucoup de mémoire moduleSystem3Pressure->processModules(1.0f / 60.0f);
std::cout << "Allocating temporary 50MB during reload...\n"; }
std::vector<uint8_t> tempAlloc;
// Allouer temporairement beaucoup de mémoire
auto reloadPressureStart = std::chrono::high_resolution_clock::now(); std::cout << "Allocating temporary 50MB during reload...\n";
std::vector<uint8_t> tempAlloc;
// Allouer 50MB
tempAlloc.resize(50 * 1024 * 1024); auto reloadPressureStart = std::chrono::high_resolution_clock::now();
std::fill(tempAlloc.begin(), tempAlloc.end(), 0x42);
// Allouer 50MB
size_t memDuringAlloc = getCurrentMemoryUsage(); tempAlloc.resize(50 * 1024 * 1024);
std::cout << " Memory with allocation: " << (memDuringAlloc / 1024.0f / 1024.0f) << " MB\n"; std::fill(tempAlloc.begin(), tempAlloc.end(), 0x42);
// Reload pendant la pression mémoire size_t memDuringAlloc = getCurrentMemoryUsage();
auto modulePressure = moduleSystem3Pressure->extractModule(); std::cout << " Memory with allocation: " << (memDuringAlloc / 1024.0f / 1024.0f) << " MB\n";
auto modulePressureReloaded = loader.reload(std::move(modulePressure));
moduleSystem3Pressure->registerModule("HeavyStateModule", std::move(modulePressureReloaded)); // Reload pendant la pression mémoire
auto modulePressure = moduleSystem3Pressure->extractModule();
auto reloadPressureEnd = std::chrono::high_resolution_clock::now(); auto modulePressureReloaded = loader.reload(std::move(modulePressure));
float reloadPressureTime = std::chrono::duration<float, std::milli>( moduleSystem3Pressure->registerModule("HeavyStateModule", std::move(modulePressureReloaded));
reloadPressureEnd - reloadPressureStart).count();
auto reloadPressureEnd = std::chrono::high_resolution_clock::now();
std::cout << " Reload under pressure: " << reloadPressureTime << "ms\n"; float reloadPressureTime = std::chrono::duration<float, std::milli>(
reporter.addMetric("reload_under_pressure_ms", reloadPressureTime); reloadPressureEnd - reloadPressureStart).count();
// Libérer allocation temporaire std::cout << " Reload under pressure: " << reloadPressureTime << "ms\n";
tempAlloc.clear(); reporter.addMetric("reload_under_pressure_ms", reloadPressureTime);
tempAlloc.shrink_to_fit();
// Libérer allocation temporaire
std::this_thread::sleep_for(std::chrono::milliseconds(200)); tempAlloc.clear();
tempAlloc.shrink_to_fit();
size_t memAfter = getCurrentMemoryUsage();
std::cout << " Memory after cleanup: " << (memAfter / 1024.0f / 1024.0f) << " MB\n"; std::this_thread::sleep_for(std::chrono::milliseconds(200));
long memGrowth = static_cast<long>(memAfter) - static_cast<long>(memBefore); size_t memAfter = getCurrentMemoryUsage();
float memGrowthMB = memGrowth / 1024.0f / 1024.0f; std::cout << " Memory after cleanup: " << (memAfter / 1024.0f / 1024.0f) << " MB\n";
std::cout << " Net memory growth: " << memGrowthMB << " MB\n";
long memGrowth = static_cast<long>(memAfter) - static_cast<long>(memBefore);
// Tolérance: max 10MB de croissance float memGrowthMB = memGrowth / 1024.0f / 1024.0f;
ASSERT_LT(std::abs(memGrowth), 10 * 1024 * 1024, "Memory growth should be < 10MB"); std::cout << " Net memory growth: " << memGrowthMB << " MB\n";
reporter.addMetric("memory_growth_mb", memGrowthMB);
// Tolérance: max 10MB de croissance
std::cout << "\n✅ TEST 3 PASSED\n\n"; ASSERT_LT(std::abs(memGrowth), 10 * 1024 * 1024, "Memory growth should be < 10MB");
reporter.addMetric("memory_growth_mb", memGrowthMB);
// ========================================================================
// TEST 4: Incremental Reloads std::cout << "\n✅ TEST 3 PASSED\n\n";
// ========================================================================
std::cout << "=== TEST 4: Incremental Reloads ===\n\n"; // ========================================================================
// TEST 4: Incremental Reloads
auto moduleSystem3 = std::make_unique<SequentialModuleSystem>(); // ========================================================================
std::cout << "=== TEST 4: Incremental Reloads ===\n\n";
nlohmann::json configIncremental;
configIncremental["version"] = "v1.0"; auto moduleSystem3 = std::make_unique<SequentialModuleSystem>();
configIncremental["particleCount"] = 10000; // Petit pour test rapide
configIncremental["terrainSize"] = 100; nlohmann::json configIncremental;
configIncremental["initDuration"] = 0.5f; configIncremental["version"] = "v1.0";
configIncremental["incrementalState"] = true; configIncremental["particleCount"] = 10000; // Petit pour test rapide
configIncremental["terrainSize"] = 100;
auto configIncrNode = std::make_unique<JsonDataNode>("config", configIncremental); configIncremental["initDuration"] = 0.5f;
auto moduleIncr = loader.load(modulePath, "HeavyStateModule", false); configIncremental["incrementalState"] = true;
moduleIncr->setConfiguration(*configIncrNode, nullptr, nullptr);
moduleSystem3->registerModule("HeavyStateModule", std::move(moduleIncr)); auto configIncrNode = std::make_unique<JsonDataNode>("config", configIncremental);
auto moduleIncr = loader.load(modulePath, "HeavyStateModule", false);
std::vector<float> incrementalTimes; moduleIncr->setConfiguration(*configIncrNode, nullptr, nullptr);
moduleSystem3->registerModule("HeavyStateModule", std::move(moduleIncr));
std::cout << "Performing 5 incremental reloads...\n";
for (int reload = 0; reload < 5; reload++) { std::vector<float> incrementalTimes;
// Exécuter 60 frames
for (int i = 0; i < 60; i++) { std::cout << "Performing 5 incremental reloads...\n";
moduleSystem3->processModules(1.0f / 60.0f); for (int reload = 0; reload < 5; reload++) {
} // Exécuter 60 frames
for (int i = 0; i < 60; i++) {
// Reload moduleSystem3->processModules(1.0f / 60.0f);
auto incStart = std::chrono::high_resolution_clock::now(); }
auto moduleInc = moduleSystem3->extractModule(); // Reload
auto moduleIncReloaded = loader.reload(std::move(moduleInc)); auto incStart = std::chrono::high_resolution_clock::now();
moduleSystem3->registerModule("HeavyStateModule", std::move(moduleIncReloaded));
auto moduleInc = moduleSystem3->extractModule();
auto incEnd = std::chrono::high_resolution_clock::now(); auto moduleIncReloaded = loader.reload(std::move(moduleInc));
float incTime = std::chrono::duration<float, std::milli>(incEnd - incStart).count(); moduleSystem3->registerModule("HeavyStateModule", std::move(moduleIncReloaded));
incrementalTimes.push_back(incTime); auto incEnd = std::chrono::high_resolution_clock::now();
std::cout << " Reload #" << reload << ": " << incTime << "ms\n"; float incTime = std::chrono::duration<float, std::milli>(incEnd - incStart).count();
}
incrementalTimes.push_back(incTime);
float avgIncremental = std::accumulate(incrementalTimes.begin(), incrementalTimes.end(), 0.0f) std::cout << " Reload #" << reload << ": " << incTime << "ms\n";
/ incrementalTimes.size(); }
std::cout << "\nAverage incremental reload: " << avgIncremental << "ms\n";
float avgIncremental = std::accumulate(incrementalTimes.begin(), incrementalTimes.end(), 0.0f)
ASSERT_LT(avgIncremental, 2000.0f, "Incremental reloads should be reasonably fast"); / incrementalTimes.size();
reporter.addMetric("avg_incremental_reload_ms", avgIncremental); std::cout << "\nAverage incremental reload: " << avgIncremental << "ms\n";
std::cout << "\n✅ TEST 4 PASSED\n\n"; ASSERT_LT(avgIncremental, 2000.0f, "Incremental reloads should be reasonably fast");
reporter.addMetric("avg_incremental_reload_ms", avgIncremental);
// ========================================================================
// TEST 5: State Corruption Detection std::cout << "\n✅ TEST 4 PASSED\n\n";
// ========================================================================
std::cout << "=== TEST 5: State Corruption Detection ===\n\n"; // ========================================================================
// TEST 5: State Corruption Detection
auto moduleSystem4 = std::make_unique<SequentialModuleSystem>(); // ========================================================================
std::cout << "=== TEST 5: State Corruption Detection ===\n\n";
nlohmann::json configNormal;
configNormal["version"] = "v1.0"; auto moduleSystem4 = std::make_unique<SequentialModuleSystem>();
configNormal["particleCount"] = 1000;
configNormal["terrainSize"] = 50; nlohmann::json configNormal;
configNormal["initDuration"] = 0.2f; configNormal["version"] = "v1.0";
configNormal["particleCount"] = 1000;
auto configNormalNode = std::make_unique<JsonDataNode>("config", configNormal); configNormal["terrainSize"] = 50;
auto moduleNormal = loader.load(modulePath, "HeavyStateModule", false); configNormal["initDuration"] = 0.2f;
moduleNormal->setConfiguration(*configNormalNode, nullptr, nullptr);
moduleSystem4->registerModule("HeavyStateModule", std::move(moduleNormal)); auto configNormalNode = std::make_unique<JsonDataNode>("config", configNormal);
auto moduleNormal = loader.load(modulePath, "HeavyStateModule", false);
// Exécuter un peu moduleNormal->setConfiguration(*configNormalNode, nullptr, nullptr);
for (int i = 0; i < 60; i++) { moduleSystem4->registerModule("HeavyStateModule", std::move(moduleNormal));
moduleSystem4->processModules(1.0f / 60.0f);
} // Exécuter un peu
for (int i = 0; i < 60; i++) {
// Créer un état corrompu moduleSystem4->processModules(1.0f / 60.0f);
std::cout << "Creating corrupted state...\n"; }
nlohmann::json corruptedState;
corruptedState["version"] = "v1.0"; // Créer un état corrompu
corruptedState["frameCount"] = "INVALID_STRING"; // Type incorrect std::cout << "Creating corrupted state...\n";
corruptedState["config"]["particleCount"] = -500; // Valeur invalide nlohmann::json corruptedState;
corruptedState["config"]["terrainWidth"] = 100; corruptedState["version"] = "v1.0";
corruptedState["config"]["terrainHeight"] = 100; corruptedState["frameCount"] = "INVALID_STRING"; // Type incorrect
corruptedState["particles"]["count"] = 1000; corruptedState["config"]["particleCount"] = -500; // Valeur invalide
corruptedState["particles"]["data"] = "CORRUPTED"; corruptedState["config"]["terrainWidth"] = 100;
corruptedState["terrain"]["width"] = 50; corruptedState["config"]["terrainHeight"] = 100;
corruptedState["terrain"]["height"] = 50; corruptedState["particles"]["count"] = 1000;
corruptedState["terrain"]["compressed"] = true; corruptedState["particles"]["data"] = "CORRUPTED";
corruptedState["terrain"]["data"] = "CORRUPTED"; corruptedState["terrain"]["width"] = 50;
corruptedState["history"] = nlohmann::json::array(); corruptedState["terrain"]["height"] = 50;
corruptedState["terrain"]["compressed"] = true;
auto corruptedNode = std::make_unique<JsonDataNode>("corrupted", corruptedState); corruptedState["terrain"]["data"] = "CORRUPTED";
corruptedState["history"] = nlohmann::json::array();
bool detectedCorruption = false;
std::cout << "Attempting to apply corrupted state...\n"; auto corruptedNode = std::make_unique<JsonDataNode>("corrupted", corruptedState);
try {
auto moduleCorrupt = loader.load(modulePath, "HeavyStateModule", false); bool detectedCorruption = false;
moduleCorrupt->setState(*corruptedNode); std::cout << "Attempting to apply corrupted state...\n";
} catch (const std::exception& e) { try {
std::string msg = e.what(); auto moduleCorrupt = loader.load(modulePath, "HeavyStateModule", false);
std::cout << " ✓ Corruption detected: " << msg << "\n"; moduleCorrupt->setState(*corruptedNode);
detectedCorruption = true; } catch (const std::exception& e) {
} std::string msg = e.what();
std::cout << " ✓ Corruption detected: " << msg << "\n";
ASSERT_TRUE(detectedCorruption, "Should detect corrupted state"); detectedCorruption = true;
reporter.addAssertion("corruption_detection", detectedCorruption); }
// Vérifier que le module d'origine reste fonctionnel ASSERT_TRUE(detectedCorruption, "Should detect corrupted state");
std::cout << "Verifying original module still functional...\n"; reporter.addAssertion("corruption_detection", detectedCorruption);
bool stillFunctional = true;
try { // Vérifier que le module d'origine reste fonctionnel
for (int i = 0; i < 60; i++) { std::cout << "Verifying original module still functional...\n";
moduleSystem4->processModules(1.0f / 60.0f); bool stillFunctional = true;
} try {
std::cout << " ✓ Module remains functional\n"; for (int i = 0; i < 60; i++) {
} catch (const std::exception& e) { moduleSystem4->processModules(1.0f / 60.0f);
stillFunctional = false; }
std::cout << " ✗ Module broken: " << e.what() << "\n"; std::cout << " ✓ Module remains functional\n";
} } catch (const std::exception& e) {
stillFunctional = false;
ASSERT_TRUE(stillFunctional, "Module should remain functional after rejected corrupted state"); std::cout << " ✗ Module broken: " << e.what() << "\n";
reporter.addAssertion("functional_after_corruption", stillFunctional); }
std::cout << "\n✅ TEST 5 PASSED\n\n"; ASSERT_TRUE(stillFunctional, "Module should remain functional after rejected corrupted state");
reporter.addAssertion("functional_after_corruption", stillFunctional);
// ========================================================================
// RAPPORT FINAL std::cout << "\n✅ TEST 5 PASSED\n\n";
// ========================================================================
// ========================================================================
std::cout << "================================================================================\n"; // RAPPORT FINAL
std::cout << "SUMMARY\n"; // ========================================================================
std::cout << "================================================================================\n\n";
std::cout << "================================================================================\n";
metrics.printReport(); std::cout << "SUMMARY\n";
reporter.printFinalReport(); std::cout << "================================================================================\n\n";
std::cout << "\n================================================================================\n"; metrics.printReport();
std::cout << "Result: " << (reporter.getExitCode() == 0 ? "✅ ALL TESTS PASSED" : "❌ SOME TESTS FAILED") << "\n"; reporter.printFinalReport();
std::cout << "================================================================================\n";
std::cout << "\n================================================================================\n";
return reporter.getExitCode(); std::cout << "Result: " << (reporter.getExitCode() == 0 ? "✅ ALL TESTS PASSED" : "❌ SOME TESTS FAILED") << "\n";
} std::cout << "================================================================================\n";
return reporter.getExitCode();
}

View File

@ -1,349 +1,352 @@
#include "grove/ModuleLoader.h" #include "grove/ModuleLoader.h"
#include "grove/SequentialModuleSystem.h" #include "grove/SequentialModuleSystem.h"
#include "grove/JsonDataNode.h" #include "grove/JsonDataNode.h"
#include "../helpers/TestMetrics.h" #include "../helpers/TestMetrics.h"
#include "../helpers/TestAssertions.h" #include "../helpers/TestAssertions.h"
#include "../helpers/TestReporter.h" #include "../helpers/TestReporter.h"
#include "../helpers/SystemUtils.h" #include "../helpers/SystemUtils.h"
#include <iostream> #include <iostream>
#include <chrono> #include <chrono>
#include <thread> #include <thread>
#include <set> #include <set>
using namespace grove; using namespace grove;
/** /**
* Test 08: Config Hot-Reload * Test 08: Config Hot-Reload
* *
* Objectif: Valider que le système peut modifier la configuration d'un module * Objectif: Valider que le système peut modifier la configuration d'un module
* à la volée sans redémarrage, avec validation et rollback. * à la volée sans redémarrage, avec validation et rollback.
* *
* Scénario: * Scénario:
* Phase 0: Baseline avec config initiale (10s) * Phase 0: Baseline avec config initiale (10s)
* Phase 1: Doubler spawn rate et speed (10s) * Phase 1: Doubler spawn rate et speed (10s)
* Phase 2: Changements complexes (couleurs, physique, limites) (10s) * Phase 2: Changements complexes (couleurs, physique, limites) (10s)
* Phase 3: Config invalide + rollback (5s) * Phase 3: Config invalide + rollback (5s)
* Phase 4: Partial config update (5s) * Phase 4: Partial config update (5s)
* *
* Métriques: * Métriques:
* - Config update time * - Config update time
* - Config validation * - Config validation
* - Rollback functionality * - Rollback functionality
* - Partial merge accuracy * - Partial merge accuracy
*/ */
int main() { int main() {
TestReporter reporter("Config Hot-Reload"); TestReporter reporter("Config Hot-Reload");
TestMetrics metrics; TestMetrics metrics;
std::cout << "================================================================================\n"; std::cout << "================================================================================\n";
std::cout << "TEST: Config Hot-Reload\n"; std::cout << "TEST: Config Hot-Reload\n";
std::cout << "================================================================================\n\n"; std::cout << "================================================================================\n\n";
// === SETUP === // === SETUP ===
std::cout << "Setup: Loading ConfigurableModule with initial config...\n"; std::cout << "Setup: Loading ConfigurableModule with initial config...\n";
ModuleLoader loader; ModuleLoader loader;
auto moduleSystem = std::make_unique<SequentialModuleSystem>(); auto moduleSystem = std::make_unique<SequentialModuleSystem>();
// Charger module // Charger module
std::string modulePath = "./libConfigurableModule.so"; std::string modulePath = "./libConfigurableModule.so";
auto module = loader.load(modulePath, "ConfigurableModule", false); #ifdef _WIN32
modulePath = "./libConfigurableModule.dll";
// Config initiale #endif
nlohmann::json configJson; auto module = loader.load(modulePath, "ConfigurableModule", false);
configJson["spawnRate"] = 10;
configJson["maxEntities"] = 150; // Higher limit for Phase 0 // Config initiale
configJson["entitySpeed"] = 5.0; nlohmann::json configJson;
configJson["colors"] = nlohmann::json::array({"red", "blue"}); configJson["spawnRate"] = 10;
configJson["physics"]["gravity"] = 9.8; configJson["maxEntities"] = 150; // Higher limit for Phase 0
configJson["physics"]["friction"] = 0.5; configJson["entitySpeed"] = 5.0;
auto config = std::make_unique<JsonDataNode>("config", configJson); configJson["colors"] = nlohmann::json::array({"red", "blue"});
configJson["physics"]["gravity"] = 9.8;
module->setConfiguration(*config, nullptr, nullptr); configJson["physics"]["friction"] = 0.5;
moduleSystem->registerModule("ConfigurableModule", std::move(module)); auto config = std::make_unique<JsonDataNode>("config", configJson);
std::cout << " Initial config:\n"; module->setConfiguration(*config, nullptr, nullptr);
std::cout << " Spawn rate: 10/s\n"; moduleSystem->registerModule("ConfigurableModule", std::move(module));
std::cout << " Max entities: 150\n";
std::cout << " Entity speed: 5.0\n"; std::cout << " Initial config:\n";
std::cout << " Colors: [red, blue]\n\n"; std::cout << " Spawn rate: 10/s\n";
std::cout << " Max entities: 150\n";
// === PHASE 0: Baseline (10s) === std::cout << " Entity speed: 5.0\n";
std::cout << "=== Phase 0: Initial config (10s) ===\n"; std::cout << " Colors: [red, blue]\n\n";
for (int i = 0; i < 600; i++) { // 10s * 60 FPS // === PHASE 0: Baseline (10s) ===
auto frameStart = std::chrono::high_resolution_clock::now(); std::cout << "=== Phase 0: Initial config (10s) ===\n";
moduleSystem->processModules(1.0f / 60.0f); for (int i = 0; i < 600; i++) { // 10s * 60 FPS
auto frameStart = std::chrono::high_resolution_clock::now();
auto frameEnd = std::chrono::high_resolution_clock::now();
float frameTime = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count(); moduleSystem->processModules(1.0f / 60.0f);
metrics.recordFPS(1000.0f / frameTime);
metrics.recordMemoryUsage(grove::getCurrentMemoryUsage()); auto frameEnd = std::chrono::high_resolution_clock::now();
} float frameTime = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count();
metrics.recordFPS(1000.0f / frameTime);
auto state0 = moduleSystem->extractModule()->getState(); metrics.recordMemoryUsage(grove::getCurrentMemoryUsage());
auto* json0 = dynamic_cast<JsonDataNode*>(state0.get()); }
ASSERT_TRUE(json0 != nullptr, "State should be JsonDataNode");
auto state0 = moduleSystem->extractModule()->getState();
const auto& state0Data = json0->getJsonData(); auto* json0 = dynamic_cast<JsonDataNode*>(state0.get());
int entityCount0 = state0Data["entities"].size(); ASSERT_TRUE(json0 != nullptr, "State should be JsonDataNode");
std::cout << "✓ Baseline: " << entityCount0 << " entities spawned (~100 expected)\n"; const auto& state0Data = json0->getJsonData();
ASSERT_WITHIN(entityCount0, 100, 20, "Should have ~100 entities after 10s"); int entityCount0 = state0Data["entities"].size();
reporter.addAssertion("initial_spawn_rate", true);
std::cout << "✓ Baseline: " << entityCount0 << " entities spawned (~100 expected)\n";
// Vérifier vitesse des entités initiales ASSERT_WITHIN(entityCount0, 100, 20, "Should have ~100 entities after 10s");
for (const auto& entity : state0Data["entities"]) { reporter.addAssertion("initial_spawn_rate", true);
float speed = entity["speed"];
ASSERT_EQ_FLOAT(speed, 5.0f, 0.01f, "Initial entity speed should be 5.0"); // Vérifier vitesse des entités initiales
} for (const auto& entity : state0Data["entities"]) {
float speed = entity["speed"];
// Re-register module ASSERT_EQ_FLOAT(speed, 5.0f, 0.01f, "Initial entity speed should be 5.0");
auto module0 = loader.load(modulePath, "ConfigurableModule", false); }
module0->setState(*state0);
moduleSystem->registerModule("ConfigurableModule", std::move(module0)); // Re-register module
auto module0 = loader.load(modulePath, "ConfigurableModule", false);
// === PHASE 1: Simple Config Change (10s) === module0->setState(*state0);
std::cout << "\n=== Phase 1: Doubling spawn rate and speed (10s) ===\n"; moduleSystem->registerModule("ConfigurableModule", std::move(module0));
nlohmann::json newConfig1; // === PHASE 1: Simple Config Change (10s) ===
newConfig1["spawnRate"] = 20; // Double spawn rate std::cout << "\n=== Phase 1: Doubling spawn rate and speed (10s) ===\n";
newConfig1["maxEntities"] = 150; // Keep same limit
newConfig1["entitySpeed"] = 10.0; // Double speed nlohmann::json newConfig1;
newConfig1["colors"] = nlohmann::json::array({"red", "blue"}); newConfig1["spawnRate"] = 20; // Double spawn rate
newConfig1["physics"]["gravity"] = 9.8; newConfig1["maxEntities"] = 150; // Keep same limit
newConfig1["physics"]["friction"] = 0.5; newConfig1["entitySpeed"] = 10.0; // Double speed
auto newConfigNode1 = std::make_unique<JsonDataNode>("config", newConfig1); newConfig1["colors"] = nlohmann::json::array({"red", "blue"});
newConfig1["physics"]["gravity"] = 9.8;
auto updateStart1 = std::chrono::high_resolution_clock::now(); newConfig1["physics"]["friction"] = 0.5;
auto newConfigNode1 = std::make_unique<JsonDataNode>("config", newConfig1);
// Extract, update config, re-register
auto modulePhase1 = moduleSystem->extractModule(); auto updateStart1 = std::chrono::high_resolution_clock::now();
bool updateResult1 = modulePhase1->updateConfig(*newConfigNode1);
// Extract, update config, re-register
auto updateEnd1 = std::chrono::high_resolution_clock::now(); auto modulePhase1 = moduleSystem->extractModule();
float updateTime1 = std::chrono::duration<float, std::milli>(updateEnd1 - updateStart1).count(); bool updateResult1 = modulePhase1->updateConfig(*newConfigNode1);
ASSERT_TRUE(updateResult1, "Config update should succeed"); auto updateEnd1 = std::chrono::high_resolution_clock::now();
reporter.addAssertion("config_update_simple", updateResult1); float updateTime1 = std::chrono::duration<float, std::milli>(updateEnd1 - updateStart1).count();
reporter.addMetric("config_update_time_ms", updateTime1);
ASSERT_TRUE(updateResult1, "Config update should succeed");
std::cout << " Config updated in " << updateTime1 << "ms\n"; reporter.addAssertion("config_update_simple", updateResult1);
reporter.addMetric("config_update_time_ms", updateTime1);
moduleSystem->registerModule("ConfigurableModule", std::move(modulePhase1));
std::cout << " Config updated in " << updateTime1 << "ms\n";
// Run 10s
for (int i = 0; i < 600; i++) { moduleSystem->registerModule("ConfigurableModule", std::move(modulePhase1));
moduleSystem->processModules(1.0f / 60.0f);
metrics.recordMemoryUsage(grove::getCurrentMemoryUsage()); // Run 10s
} for (int i = 0; i < 600; i++) {
moduleSystem->processModules(1.0f / 60.0f);
auto state1 = moduleSystem->extractModule()->getState(); metrics.recordMemoryUsage(grove::getCurrentMemoryUsage());
auto* json1 = dynamic_cast<JsonDataNode*>(state1.get()); }
const auto& state1Data = json1->getJsonData();
int entityCount1 = state1Data["entities"].size(); auto state1 = moduleSystem->extractModule()->getState();
auto* json1 = dynamic_cast<JsonDataNode*>(state1.get());
// Should have reached limit (150) const auto& state1Data = json1->getJsonData();
std::cout << "✓ Phase 1: " << entityCount1 << " entities (max: 150)\n"; int entityCount1 = state1Data["entities"].size();
ASSERT_LE(entityCount1, 150, "Should respect maxEntities limit");
reporter.addAssertion("max_entities_respected", entityCount1 <= 150); // Should have reached limit (150)
std::cout << "✓ Phase 1: " << entityCount1 << " entities (max: 150)\n";
// Vérifier que nouvelles entités ont speed = 10.0 ASSERT_LE(entityCount1, 150, "Should respect maxEntities limit");
int newEntityCount = 0; reporter.addAssertion("max_entities_respected", entityCount1 <= 150);
for (const auto& entity : state1Data["entities"]) {
if (entity["id"] >= entityCount0) { // Nouvelle entité // Vérifier que nouvelles entités ont speed = 10.0
float speed = entity["speed"]; int newEntityCount = 0;
ASSERT_EQ_FLOAT(speed, 10.0f, 0.01f, "New entities should have speed 10.0"); for (const auto& entity : state1Data["entities"]) {
newEntityCount++; 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");
std::cout << " " << newEntityCount << " new entities with speed 10.0\n"; newEntityCount++;
}
// Re-register }
auto module1 = loader.load(modulePath, "ConfigurableModule", false); std::cout << " " << newEntityCount << " new entities with speed 10.0\n";
module1->setState(*state1);
moduleSystem->registerModule("ConfigurableModule", std::move(module1)); // Re-register
auto module1 = loader.load(modulePath, "ConfigurableModule", false);
// === PHASE 2: Complex Config Change (10s) === module1->setState(*state1);
std::cout << "\n=== Phase 2: Complex config changes (10s) ===\n"; moduleSystem->registerModule("ConfigurableModule", std::move(module1));
nlohmann::json newConfig2; // === PHASE 2: Complex Config Change (10s) ===
newConfig2["spawnRate"] = 15; std::cout << "\n=== Phase 2: Complex config changes (10s) ===\n";
newConfig2["maxEntities"] = 200; // Augmenter limite (was 150)
newConfig2["entitySpeed"] = 7.5; nlohmann::json newConfig2;
newConfig2["colors"] = nlohmann::json::array({"green", "yellow", "purple"}); // Nouvelles couleurs newConfig2["spawnRate"] = 15;
newConfig2["physics"]["gravity"] = 1.6; // Gravité lunaire newConfig2["maxEntities"] = 200; // Augmenter limite (was 150)
newConfig2["physics"]["friction"] = 0.2; newConfig2["entitySpeed"] = 7.5;
auto newConfigNode2 = std::make_unique<JsonDataNode>("config", newConfig2); newConfig2["colors"] = nlohmann::json::array({"green", "yellow", "purple"}); // Nouvelles couleurs
newConfig2["physics"]["gravity"] = 1.6; // Gravité lunaire
auto modulePhase2 = moduleSystem->extractModule(); newConfig2["physics"]["friction"] = 0.2;
bool updateResult2 = modulePhase2->updateConfig(*newConfigNode2); auto newConfigNode2 = std::make_unique<JsonDataNode>("config", newConfig2);
ASSERT_TRUE(updateResult2, "Config update 2 should succeed");
auto modulePhase2 = moduleSystem->extractModule();
int entitiesBeforePhase2 = entityCount1; bool updateResult2 = modulePhase2->updateConfig(*newConfigNode2);
std::cout << " Config updated: new colors [green, yellow, purple]\n"; ASSERT_TRUE(updateResult2, "Config update 2 should succeed");
std::cout << " Max entities increased to 200\n";
int entitiesBeforePhase2 = entityCount1;
moduleSystem->registerModule("ConfigurableModule", std::move(modulePhase2)); std::cout << " Config updated: new colors [green, yellow, purple]\n";
std::cout << " Max entities increased to 200\n";
// Run 10s
for (int i = 0; i < 600; i++) { moduleSystem->registerModule("ConfigurableModule", std::move(modulePhase2));
moduleSystem->processModules(1.0f / 60.0f);
} // Run 10s
for (int i = 0; i < 600; i++) {
auto state2 = moduleSystem->extractModule()->getState(); moduleSystem->processModules(1.0f / 60.0f);
auto* json2 = dynamic_cast<JsonDataNode*>(state2.get()); }
const auto& state2Data = json2->getJsonData();
int entityCount2 = state2Data["entities"].size(); auto state2 = moduleSystem->extractModule()->getState();
auto* json2 = dynamic_cast<JsonDataNode*>(state2.get());
std::cout << "✓ Phase 2: " << entityCount2 << " total entities\n"; const auto& state2Data = json2->getJsonData();
ASSERT_GT(entityCount2, entitiesBeforePhase2, "Entity count should have increased"); int entityCount2 = state2Data["entities"].size();
ASSERT_LE(entityCount2, 200, "Should respect new maxEntities = 200");
std::cout << "✓ Phase 2: " << entityCount2 << " total entities\n";
// Vérifier couleurs des nouvelles entités ASSERT_GT(entityCount2, entitiesBeforePhase2, "Entity count should have increased");
std::set<std::string> newColors; ASSERT_LE(entityCount2, 200, "Should respect new maxEntities = 200");
for (const auto& entity : state2Data["entities"]) {
if (entity["id"] >= entitiesBeforePhase2) { // Vérifier couleurs des nouvelles entités
newColors.insert(entity["color"]); 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);
bool hasNewColors = newColors.count("green") || newColors.count("yellow") || newColors.count("purple");
std::cout << " New colors found: "; ASSERT_TRUE(hasNewColors, "New entities should use new color palette");
for (const auto& color : newColors) std::cout << color << " "; reporter.addAssertion("new_colors_applied", hasNewColors);
std::cout << "\n";
std::cout << " New colors found: ";
// Re-register for (const auto& color : newColors) std::cout << color << " ";
auto module2 = loader.load(modulePath, "ConfigurableModule", false); std::cout << "\n";
module2->setState(*state2);
moduleSystem->registerModule("ConfigurableModule", std::move(module2)); // Re-register
auto module2 = loader.load(modulePath, "ConfigurableModule", false);
// === PHASE 3: Invalid Config + Rollback (5s) === module2->setState(*state2);
std::cout << "\n=== Phase 3: Invalid config rejection (5s) ===\n"; moduleSystem->registerModule("ConfigurableModule", std::move(module2));
nlohmann::json invalidConfig; // === PHASE 3: Invalid Config + Rollback (5s) ===
invalidConfig["spawnRate"] = -5; // INVALIDE: négatif std::cout << "\n=== Phase 3: Invalid config rejection (5s) ===\n";
invalidConfig["maxEntities"] = 1000000; // INVALIDE: trop grand
invalidConfig["entitySpeed"] = 5.0; nlohmann::json invalidConfig;
invalidConfig["colors"] = nlohmann::json::array({"red"}); invalidConfig["spawnRate"] = -5; // INVALIDE: négatif
invalidConfig["physics"]["gravity"] = 9.8; invalidConfig["maxEntities"] = 1000000; // INVALIDE: trop grand
invalidConfig["physics"]["friction"] = 0.5; invalidConfig["entitySpeed"] = 5.0;
auto invalidConfigNode = std::make_unique<JsonDataNode>("config", invalidConfig); invalidConfig["colors"] = nlohmann::json::array({"red"});
invalidConfig["physics"]["gravity"] = 9.8;
auto modulePhase3 = moduleSystem->extractModule(); invalidConfig["physics"]["friction"] = 0.5;
bool updateResult3 = modulePhase3->updateConfig(*invalidConfigNode); auto invalidConfigNode = std::make_unique<JsonDataNode>("config", invalidConfig);
std::cout << " Invalid config rejected: " << (!updateResult3 ? "YES" : "NO") << "\n"; auto modulePhase3 = moduleSystem->extractModule();
ASSERT_FALSE(updateResult3, "Invalid config should be rejected"); bool updateResult3 = modulePhase3->updateConfig(*invalidConfigNode);
reporter.addAssertion("invalid_config_rejected", !updateResult3);
std::cout << " Invalid config rejected: " << (!updateResult3 ? "YES" : "NO") << "\n";
moduleSystem->registerModule("ConfigurableModule", std::move(modulePhase3)); ASSERT_FALSE(updateResult3, "Invalid config should be rejected");
reporter.addAssertion("invalid_config_rejected", !updateResult3);
// Continuer - devrait utiliser la config précédente (valide)
for (int i = 0; i < 300; i++) { // 5s moduleSystem->registerModule("ConfigurableModule", std::move(modulePhase3));
moduleSystem->processModules(1.0f / 60.0f);
} // Continuer - devrait utiliser la config précédente (valide)
for (int i = 0; i < 300; i++) { // 5s
auto state3 = moduleSystem->extractModule()->getState(); moduleSystem->processModules(1.0f / 60.0f);
auto* json3 = dynamic_cast<JsonDataNode*>(state3.get()); }
const auto& state3Data = json3->getJsonData();
int entityCount3 = state3Data["entities"].size(); auto state3 = moduleSystem->extractModule()->getState();
auto* json3 = dynamic_cast<JsonDataNode*>(state3.get());
std::cout << "✓ Rollback successful: " << (entityCount3 - entityCount2) << " entities spawned with previous config\n"; const auto& state3Data = json3->getJsonData();
// Note: We might already be at maxEntities (200), so we just verify no crash and config stayed valid int entityCount3 = state3Data["entities"].size();
ASSERT_GE(entityCount3, entityCount2, "Entity count should not decrease");
reporter.addAssertion("config_rollback_works", entityCount3 >= entityCount2); 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
// Re-register ASSERT_GE(entityCount3, entityCount2, "Entity count should not decrease");
auto module3 = loader.load(modulePath, "ConfigurableModule", false); reporter.addAssertion("config_rollback_works", entityCount3 >= entityCount2);
module3->setState(*state3);
moduleSystem->registerModule("ConfigurableModule", std::move(module3)); // Re-register
auto module3 = loader.load(modulePath, "ConfigurableModule", false);
// === PHASE 4: Partial Config Update (5s) === module3->setState(*state3);
std::cout << "\n=== Phase 4: Partial config update (5s) ===\n"; moduleSystem->registerModule("ConfigurableModule", std::move(module3));
nlohmann::json partialConfig; // === PHASE 4: Partial Config Update (5s) ===
partialConfig["entitySpeed"] = 2.0; // Modifier seulement la vitesse std::cout << "\n=== Phase 4: Partial config update (5s) ===\n";
auto partialConfigNode = std::make_unique<JsonDataNode>("config", partialConfig);
nlohmann::json partialConfig;
auto modulePhase4 = moduleSystem->extractModule(); partialConfig["entitySpeed"] = 2.0; // Modifier seulement la vitesse
bool updateResult4 = modulePhase4->updateConfigPartial(*partialConfigNode); auto partialConfigNode = std::make_unique<JsonDataNode>("config", partialConfig);
std::cout << " Partial update (entitySpeed only): " << (updateResult4 ? "SUCCESS" : "FAILED") << "\n"; auto modulePhase4 = moduleSystem->extractModule();
ASSERT_TRUE(updateResult4, "Partial config update should succeed"); bool updateResult4 = modulePhase4->updateConfigPartial(*partialConfigNode);
moduleSystem->registerModule("ConfigurableModule", std::move(modulePhase4)); std::cout << " Partial update (entitySpeed only): " << (updateResult4 ? "SUCCESS" : "FAILED") << "\n";
ASSERT_TRUE(updateResult4, "Partial config update should succeed");
// Run 5s
for (int i = 0; i < 300; i++) { moduleSystem->registerModule("ConfigurableModule", std::move(modulePhase4));
moduleSystem->processModules(1.0f / 60.0f);
} // Run 5s
for (int i = 0; i < 300; i++) {
auto state4 = moduleSystem->extractModule()->getState(); moduleSystem->processModules(1.0f / 60.0f);
auto* json4 = dynamic_cast<JsonDataNode*>(state4.get()); }
const auto& state4Data = json4->getJsonData();
auto state4 = moduleSystem->extractModule()->getState();
// Vérifier que nouvelles entités ont speed = 2.0 auto* json4 = dynamic_cast<JsonDataNode*>(state4.get());
// Et que colors sont toujours ceux de Phase 2 const auto& state4Data = json4->getJsonData();
// Note: We might be at maxEntities, so check if any new entities were spawned
bool foundNewSpeed = false; // Vérifier que nouvelles entités ont speed = 2.0
bool foundOldColors = false; // Et que colors sont toujours ceux de Phase 2
int newEntitiesPhase4 = 0; // Note: We might be at maxEntities, so check if any new entities were spawned
bool foundNewSpeed = false;
for (const auto& entity : state4Data["entities"]) { bool foundOldColors = false;
if (entity["id"] >= entityCount3) { int newEntitiesPhase4 = 0;
newEntitiesPhase4++;
float speed = entity["speed"]; for (const auto& entity : state4Data["entities"]) {
if (std::abs(speed - 2.0f) < 0.01f) foundNewSpeed = true; if (entity["id"] >= entityCount3) {
newEntitiesPhase4++;
std::string color = entity["color"]; float speed = entity["speed"];
if (color == "green" || color == "yellow" || color == "purple") { if (std::abs(speed - 2.0f) < 0.01f) foundNewSpeed = true;
foundOldColors = 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 std::cout << "✓ Partial update: speed changed to 2.0, other params preserved\n";
// Otherwise, just verify the partial update succeeded (which it did above) std::cout << " New entities in Phase 4: " << newEntitiesPhase4 << " (may be 0 if at maxEntities)\n";
if (newEntitiesPhase4 > 0) {
ASSERT_TRUE(foundNewSpeed, "New entities should have updated speed"); // If we spawned new entities, verify they have the new speed
ASSERT_TRUE(foundOldColors, "Colors should be preserved from Phase 2"); // Otherwise, just verify the partial update succeeded (which it did above)
reporter.addAssertion("partial_update_works", foundNewSpeed && foundOldColors); if (newEntitiesPhase4 > 0) {
} else { ASSERT_TRUE(foundNewSpeed, "New entities should have updated speed");
// At maxEntities, just verify no crash and config updated ASSERT_TRUE(foundOldColors, "Colors should be preserved from Phase 2");
std::cout << " (At maxEntities, cannot verify new entity speed)\n"; reporter.addAssertion("partial_update_works", foundNewSpeed && foundOldColors);
reporter.addAssertion("partial_update_works", true); } else {
} // At maxEntities, just verify no crash and config updated
std::cout << " (At maxEntities, cannot verify new entity speed)\n";
// === VÉRIFICATIONS FINALES === reporter.addAssertion("partial_update_works", true);
std::cout << "\n================================================================================\n"; }
std::cout << "FINAL VERIFICATION\n";
std::cout << "================================================================================\n"; // === VÉRIFICATIONS FINALES ===
std::cout << "\n================================================================================\n";
// Memory stability std::cout << "FINAL VERIFICATION\n";
size_t memGrowth = metrics.getMemoryGrowth(); std::cout << "================================================================================\n";
float memGrowthMB = memGrowth / (1024.0f * 1024.0f);
// Memory stability
std::cout << "Memory growth: " << memGrowthMB << " MB (threshold: < 10 MB)\n"; size_t memGrowth = metrics.getMemoryGrowth();
ASSERT_LT(memGrowth, 10 * 1024 * 1024, "Memory growth should be < 10MB"); float memGrowthMB = memGrowth / (1024.0f * 1024.0f);
reporter.addMetric("memory_growth_mb", memGrowthMB);
std::cout << "Memory growth: " << memGrowthMB << " MB (threshold: < 10 MB)\n";
// No crashes ASSERT_LT(memGrowth, 10 * 1024 * 1024, "Memory growth should be < 10MB");
reporter.addAssertion("no_crashes", true); reporter.addMetric("memory_growth_mb", memGrowthMB);
std::cout << "\n"; // No crashes
reporter.addAssertion("no_crashes", true);
// === RAPPORT FINAL ===
metrics.printReport(); std::cout << "\n";
reporter.printFinalReport();
// === RAPPORT FINAL ===
return reporter.getExitCode(); 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

View File

@ -1,491 +1,491 @@
/** /**
* Scenario 11: IO System Stress Test * Scenario 11: IO System Stress Test
* *
* Tests IntraIO pub/sub system with: * Tests IntraIO pub/sub system with:
* - Basic publish/subscribe * - Basic publish/subscribe
* - Pattern matching with wildcards * - Pattern matching with wildcards
* - Multi-module routing (1-to-many) * - Multi-module routing (1-to-many)
* - Message batching (low-frequency subscriptions) * - Message batching (low-frequency subscriptions)
* - Backpressure and queue overflow * - Backpressure and queue overflow
* - Thread safety * - Thread safety
* - Health monitoring * - Health monitoring
* *
* Known bug to validate: IntraIOManager may route only to first subscriber (std::move limitation) * Known bug to validate: IntraIOManager may route only to first subscriber (std::move limitation)
*/ */
#include "grove/IModule.h" #include "grove/IModule.h"
#include "grove/IOFactory.h" #include "grove/IOFactory.h"
#include "grove/IntraIOManager.h" #include "grove/IntraIOManager.h"
#include "grove/JsonDataNode.h" #include "grove/JsonDataNode.h"
#include "../helpers/TestMetrics.h" #include "../helpers/TestMetrics.h"
#include "../helpers/TestAssertions.h" #include "../helpers/TestAssertions.h"
#include "../helpers/TestReporter.h" #include "../helpers/TestReporter.h"
#ifdef _WIN32 #ifdef _WIN32
#include <windows.h> #include <windows.h>
#else #else
#include <dlfcn.h> #include <dlfcn.h>
#endif #endif
#include <iostream> #include <iostream>
#include <chrono> #include <chrono>
#include <thread> #include <thread>
#include <atomic> #include <atomic>
#include <vector> #include <vector>
#include <map> #include <map>
// Cross-platform dlopen wrappers // Cross-platform dlopen wrappers
#ifdef _WIN32 #ifdef _WIN32
inline void* grove_dlopen(const char* path, int flags) { inline void* grove_dlopen(const char* path, int flags) {
(void)flags; (void)flags;
return LoadLibraryA(path); return LoadLibraryA(path);
} }
inline void* grove_dlsym(void* handle, const char* symbol) { inline void* grove_dlsym(void* handle, const char* symbol) {
return (void*)GetProcAddress((HMODULE)handle, symbol); return (void*)GetProcAddress((HMODULE)handle, symbol);
} }
inline int grove_dlclose(void* handle) { inline int grove_dlclose(void* handle) {
return FreeLibrary((HMODULE)handle) ? 0 : -1; return FreeLibrary((HMODULE)handle) ? 0 : -1;
} }
inline const char* grove_dlerror() { inline const char* grove_dlerror() {
static thread_local char buf[256]; static thread_local char buf[256];
DWORD err = GetLastError(); DWORD err = GetLastError();
snprintf(buf, sizeof(buf), "Windows error code: %lu", err); snprintf(buf, sizeof(buf), "Windows error code: %lu", err);
return buf; return buf;
} }
#define RTLD_NOW 0 #define RTLD_NOW 0
#define RTLD_LOCAL 0 #define RTLD_LOCAL 0
#else #else
#define grove_dlopen dlopen #define grove_dlopen dlopen
#define grove_dlsym dlsym #define grove_dlsym dlsym
#define grove_dlclose dlclose #define grove_dlclose dlclose
#define grove_dlerror dlerror #define grove_dlerror dlerror
#endif #endif
using namespace grove; using namespace grove;
// Module handle for testing // Module handle for testing
struct ModuleHandle { struct ModuleHandle {
void* dlHandle = nullptr; void* dlHandle = nullptr;
grove::IModule* instance = nullptr; grove::IModule* instance = nullptr;
std::unique_ptr<IIO> io; std::unique_ptr<IIO> io;
std::string modulePath; std::string modulePath;
}; };
// Simple module loader for IO testing // Simple module loader for IO testing
class IOTestEngine { class IOTestEngine {
public: public:
IOTestEngine() {} IOTestEngine() {}
~IOTestEngine() { ~IOTestEngine() {
for (auto& [name, handle] : modules_) { for (auto& [name, handle] : modules_) {
unloadModule(name); unloadModule(name);
} }
} }
bool loadModule(const std::string& name, const std::string& path) { bool loadModule(const std::string& name, const std::string& path) {
if (modules_.count(name) > 0) { if (modules_.count(name) > 0) {
std::cerr << "Module " << name << " already loaded\n"; std::cerr << "Module " << name << " already loaded\n";
return false; return false;
} }
void* dlHandle = grove_dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); void* dlHandle = grove_dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
if (!dlHandle) { if (!dlHandle) {
std::cerr << "Failed to load module " << name << ": " << grove_dlerror() << "\n"; std::cerr << "Failed to load module " << name << ": " << grove_dlerror() << "\n";
return false; return false;
} }
auto createFunc = (grove::IModule* (*)())grove_dlsym(dlHandle, "createModule"); auto createFunc = (grove::IModule* (*)())grove_dlsym(dlHandle, "createModule");
if (!createFunc) { if (!createFunc) {
std::cerr << "Failed to find createModule in " << name << ": " << grove_dlerror() << "\n"; std::cerr << "Failed to find createModule in " << name << ": " << grove_dlerror() << "\n";
grove_dlclose(dlHandle); grove_dlclose(dlHandle);
return false; return false;
} }
grove::IModule* instance = createFunc(); grove::IModule* instance = createFunc();
if (!instance) { if (!instance) {
std::cerr << "createModule returned nullptr for " << name << "\n"; std::cerr << "createModule returned nullptr for " << name << "\n";
grove_dlclose(dlHandle); grove_dlclose(dlHandle);
return false; return false;
} }
// Create IntraIO instance for this module // Create IntraIO instance for this module
auto io = IOFactory::create("intra", name); auto io = IOFactory::create("intra", name);
ModuleHandle handle; ModuleHandle handle;
handle.dlHandle = dlHandle; handle.dlHandle = dlHandle;
handle.instance = instance; handle.instance = instance;
handle.io = std::move(io); handle.io = std::move(io);
handle.modulePath = path; handle.modulePath = path;
modules_[name] = std::move(handle); modules_[name] = std::move(handle);
// Initialize module with IO // Initialize module with IO
auto config = std::make_unique<JsonDataNode>("config", nlohmann::json::object()); auto config = std::make_unique<JsonDataNode>("config", nlohmann::json::object());
instance->setConfiguration(*config, modules_[name].io.get(), nullptr); instance->setConfiguration(*config, modules_[name].io.get(), nullptr);
std::cout << " ✓ Loaded " << name << "\n"; std::cout << " ✓ Loaded " << name << "\n";
return true; return true;
} }
void unloadModule(const std::string& name) { void unloadModule(const std::string& name) {
auto it = modules_.find(name); auto it = modules_.find(name);
if (it == modules_.end()) return; if (it == modules_.end()) return;
auto& handle = it->second; auto& handle = it->second;
if (handle.instance) { if (handle.instance) {
handle.instance->shutdown(); handle.instance->shutdown();
delete handle.instance; delete handle.instance;
handle.instance = nullptr; handle.instance = nullptr;
} }
if (handle.dlHandle) { if (handle.dlHandle) {
grove_dlclose(handle.dlHandle); grove_dlclose(handle.dlHandle);
handle.dlHandle = nullptr; handle.dlHandle = nullptr;
} }
modules_.erase(it); modules_.erase(it);
} }
grove::IModule* getModule(const std::string& name) { grove::IModule* getModule(const std::string& name) {
auto it = modules_.find(name); auto it = modules_.find(name);
return (it != modules_.end()) ? it->second.instance : nullptr; return (it != modules_.end()) ? it->second.instance : nullptr;
} }
IIO* getIO(const std::string& name) { IIO* getIO(const std::string& name) {
auto it = modules_.find(name); auto it = modules_.find(name);
return (it != modules_.end()) ? it->second.io.get() : nullptr; return (it != modules_.end()) ? it->second.io.get() : nullptr;
} }
void processAll(const IDataNode& input) { void processAll(const IDataNode& input) {
for (auto& [name, handle] : modules_) { for (auto& [name, handle] : modules_) {
if (handle.instance) { if (handle.instance) {
handle.instance->process(input); handle.instance->process(input);
} }
} }
} }
private: private:
std::map<std::string, ModuleHandle> modules_; std::map<std::string, ModuleHandle> modules_;
}; };
int main() { int main() {
TestReporter reporter("IO System Stress Test"); TestReporter reporter("IO System Stress Test");
TestMetrics metrics; TestMetrics metrics;
std::cout << "================================================================================\n"; std::cout << "================================================================================\n";
std::cout << "TEST: IO System Stress Test (Scenario 11)\n"; std::cout << "TEST: IO System Stress Test (Scenario 11)\n";
std::cout << "================================================================================\n\n"; std::cout << "================================================================================\n\n";
// === SETUP === // === SETUP ===
std::cout << "Setup: Loading IO modules...\n"; std::cout << "Setup: Loading IO modules...\n";
IOTestEngine engine; IOTestEngine engine;
// Load all IO test modules // Load all IO test modules
bool loadSuccess = true; bool loadSuccess = true;
loadSuccess &= engine.loadModule("ProducerModule", "./libProducerModule.so"); loadSuccess &= engine.loadModule("ProducerModule", "./libProducerModule.dll");
loadSuccess &= engine.loadModule("ConsumerModule", "./libConsumerModule.so"); loadSuccess &= engine.loadModule("ConsumerModule", "./libConsumerModule.dll");
loadSuccess &= engine.loadModule("BroadcastModule", "./libBroadcastModule.so"); loadSuccess &= engine.loadModule("BroadcastModule", "./libBroadcastModule.dll");
loadSuccess &= engine.loadModule("BatchModule", "./libBatchModule.so"); loadSuccess &= engine.loadModule("BatchModule", "./libBatchModule.dll");
loadSuccess &= engine.loadModule("IOStressModule", "./libIOStressModule.so"); loadSuccess &= engine.loadModule("IOStressModule", "./libIOStressModule.dll");
if (!loadSuccess) { if (!loadSuccess) {
std::cerr << "❌ Failed to load required modules\n"; std::cerr << "❌ Failed to load required modules\n";
return 1; return 1;
} }
std::cout << "\n"; std::cout << "\n";
auto producerIO = engine.getIO("ProducerModule"); auto producerIO = engine.getIO("ProducerModule");
auto consumerIO = engine.getIO("ConsumerModule"); auto consumerIO = engine.getIO("ConsumerModule");
auto broadcastIO = engine.getIO("BroadcastModule"); auto broadcastIO = engine.getIO("BroadcastModule");
auto batchIO = engine.getIO("BatchModule"); auto batchIO = engine.getIO("BatchModule");
auto stressIO = engine.getIO("IOStressModule"); auto stressIO = engine.getIO("IOStressModule");
if (!producerIO || !consumerIO || !broadcastIO || !batchIO || !stressIO) { if (!producerIO || !consumerIO || !broadcastIO || !batchIO || !stressIO) {
std::cerr << "❌ Failed to get IO instances\n"; std::cerr << "❌ Failed to get IO instances\n";
return 1; return 1;
} }
auto emptyInput = std::make_unique<JsonDataNode>("input", nlohmann::json::object()); auto emptyInput = std::make_unique<JsonDataNode>("input", nlohmann::json::object());
// ======================================================================== // ========================================================================
// TEST 1: Basic Publish-Subscribe // TEST 1: Basic Publish-Subscribe
// ======================================================================== // ========================================================================
std::cout << "=== TEST 1: Basic Publish-Subscribe ===\n"; std::cout << "=== TEST 1: Basic Publish-Subscribe ===\n";
// Consumer subscribes to "test:basic" // Consumer subscribes to "test:basic"
consumerIO->subscribe("test:basic"); consumerIO->subscribe("test:basic");
// Publish 100 messages // Publish 100 messages
for (int i = 0; i < 100; i++) { for (int i = 0; i < 100; i++) {
auto data = std::make_unique<JsonDataNode>("data", nlohmann::json{ auto data = std::make_unique<JsonDataNode>("data", nlohmann::json{
{"id", i}, {"id", i},
{"payload", "test_message_" + std::to_string(i)} {"payload", "test_message_" + std::to_string(i)}
}); });
producerIO->publish("test:basic", std::move(data)); producerIO->publish("test:basic", std::move(data));
} }
// Process to allow routing // Process to allow routing
std::this_thread::sleep_for(std::chrono::milliseconds(10)); std::this_thread::sleep_for(std::chrono::milliseconds(10));
// Count received messages // Count received messages
int receivedCount = 0; int receivedCount = 0;
while (consumerIO->hasMessages() > 0) { while (consumerIO->hasMessages() > 0) {
auto msg = consumerIO->pullMessage(); auto msg = consumerIO->pullMessage();
receivedCount++; receivedCount++;
} }
ASSERT_EQ(receivedCount, 100, "Should receive all 100 messages"); ASSERT_EQ(receivedCount, 100, "Should receive all 100 messages");
reporter.addAssertion("basic_pubsub", receivedCount == 100); reporter.addAssertion("basic_pubsub", receivedCount == 100);
reporter.addMetric("basic_pubsub_count", receivedCount); reporter.addMetric("basic_pubsub_count", receivedCount);
std::cout << " ✓ Received " << receivedCount << "/100 messages\n"; std::cout << " ✓ Received " << receivedCount << "/100 messages\n";
std::cout << "✓ TEST 1 PASSED\n\n"; std::cout << "✓ TEST 1 PASSED\n\n";
// ======================================================================== // ========================================================================
// TEST 2: Pattern Matching with Wildcards // TEST 2: Pattern Matching with Wildcards
// ======================================================================== // ========================================================================
std::cout << "=== TEST 2: Pattern Matching ===\n"; std::cout << "=== TEST 2: Pattern Matching ===\n";
// Subscribe to patterns // Subscribe to patterns
consumerIO->subscribe("player:.*"); consumerIO->subscribe("player:.*");
// Publish test messages // Publish test messages
std::vector<std::string> testTopics = { std::vector<std::string> testTopics = {
"player:001:position", "player:001:position",
"player:001:health", "player:001:health",
"player:002:position", "player:002:position",
"enemy:001:position" "enemy:001:position"
}; };
for (const auto& topic : testTopics) { for (const auto& topic : testTopics) {
auto data = std::make_unique<JsonDataNode>("data", nlohmann::json{{"topic", topic}}); auto data = std::make_unique<JsonDataNode>("data", nlohmann::json{{"topic", topic}});
producerIO->publish(topic, std::move(data)); producerIO->publish(topic, std::move(data));
} }
std::this_thread::sleep_for(std::chrono::milliseconds(10)); std::this_thread::sleep_for(std::chrono::milliseconds(10));
// Count player messages (should match 3 of 4) // Count player messages (should match 3 of 4)
int playerMsgCount = 0; int playerMsgCount = 0;
while (consumerIO->hasMessages() > 0) { while (consumerIO->hasMessages() > 0) {
auto msg = consumerIO->pullMessage(); auto msg = consumerIO->pullMessage();
if (msg.topic.find("player:") == 0) { if (msg.topic.find("player:") == 0) {
playerMsgCount++; playerMsgCount++;
} }
} }
std::cout << " Pattern 'player:.*' matched " << playerMsgCount << " messages\n"; std::cout << " Pattern 'player:.*' matched " << playerMsgCount << " messages\n";
ASSERT_GE(playerMsgCount, 3, "Should match at least 3 player messages"); ASSERT_GE(playerMsgCount, 3, "Should match at least 3 player messages");
reporter.addAssertion("pattern_matching", playerMsgCount >= 3); reporter.addAssertion("pattern_matching", playerMsgCount >= 3);
reporter.addMetric("pattern_match_count", playerMsgCount); reporter.addMetric("pattern_match_count", playerMsgCount);
std::cout << "✓ TEST 2 PASSED\n\n"; std::cout << "✓ TEST 2 PASSED\n\n";
// ======================================================================== // ========================================================================
// TEST 3: Multi-Module Routing (1-to-many) - Bug Detection // TEST 3: Multi-Module Routing (1-to-many) - Bug Detection
// ======================================================================== // ========================================================================
std::cout << "=== TEST 3: Multi-Module Routing (1-to-many) ===\n"; std::cout << "=== TEST 3: Multi-Module Routing (1-to-many) ===\n";
std::cout << " Testing for known bug: std::move limitation in routing\n"; std::cout << " Testing for known bug: std::move limitation in routing\n";
// All modules subscribe to "broadcast:.*" // All modules subscribe to "broadcast:.*"
consumerIO->subscribe("broadcast:.*"); consumerIO->subscribe("broadcast:.*");
broadcastIO->subscribe("broadcast:.*"); broadcastIO->subscribe("broadcast:.*");
batchIO->subscribe("broadcast:.*"); batchIO->subscribe("broadcast:.*");
stressIO->subscribe("broadcast:.*"); stressIO->subscribe("broadcast:.*");
// Publish 10 broadcast messages // Publish 10 broadcast messages
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
auto data = std::make_unique<JsonDataNode>("data", nlohmann::json{{"broadcast_id", i}}); auto data = std::make_unique<JsonDataNode>("data", nlohmann::json{{"broadcast_id", i}});
producerIO->publish("broadcast:data", std::move(data)); producerIO->publish("broadcast:data", std::move(data));
} }
std::this_thread::sleep_for(std::chrono::milliseconds(10)); std::this_thread::sleep_for(std::chrono::milliseconds(10));
// Check which modules received messages // Check which modules received messages
int consumerReceived = consumerIO->hasMessages(); int consumerReceived = consumerIO->hasMessages();
int broadcastReceived = broadcastIO->hasMessages(); int broadcastReceived = broadcastIO->hasMessages();
int batchReceived = batchIO->hasMessages(); int batchReceived = batchIO->hasMessages();
int stressReceived = stressIO->hasMessages(); int stressReceived = stressIO->hasMessages();
std::cout << " Broadcast distribution:\n"; std::cout << " Broadcast distribution:\n";
std::cout << " ConsumerModule: " << consumerReceived << " messages\n"; std::cout << " ConsumerModule: " << consumerReceived << " messages\n";
std::cout << " BroadcastModule: " << broadcastReceived << " messages\n"; std::cout << " BroadcastModule: " << broadcastReceived << " messages\n";
std::cout << " BatchModule: " << batchReceived << " messages\n"; std::cout << " BatchModule: " << batchReceived << " messages\n";
std::cout << " IOStressModule: " << stressReceived << " messages\n"; std::cout << " IOStressModule: " << stressReceived << " messages\n";
int totalReceived = consumerReceived + broadcastReceived + batchReceived + stressReceived; int totalReceived = consumerReceived + broadcastReceived + batchReceived + stressReceived;
if (totalReceived == 10) { if (totalReceived == 10) {
std::cout << " ⚠️ BUG CONFIRMED: Only one module received all messages\n"; std::cout << " ⚠️ BUG CONFIRMED: Only one module received all messages\n";
std::cout << " This confirms the clone() limitation in routing\n"; std::cout << " This confirms the clone() limitation in routing\n";
reporter.addMetric("broadcast_bug_present", 1.0f); reporter.addMetric("broadcast_bug_present", 1.0f);
} else if (totalReceived >= 40) { } else if (totalReceived >= 40) {
std::cout << " ✓ FIXED: All modules received copies (clone() implemented!)\n"; std::cout << " ✓ FIXED: All modules received copies (clone() implemented!)\n";
reporter.addMetric("broadcast_bug_present", 0.0f); reporter.addMetric("broadcast_bug_present", 0.0f);
} else { } else {
std::cout << " ⚠️ Unexpected: " << totalReceived << " messages received (expected 10 or 40)\n"; std::cout << " ⚠️ Unexpected: " << totalReceived << " messages received (expected 10 or 40)\n";
reporter.addMetric("broadcast_bug_present", 0.5f); reporter.addMetric("broadcast_bug_present", 0.5f);
} }
reporter.addAssertion("multi_module_routing_tested", true); reporter.addAssertion("multi_module_routing_tested", true);
std::cout << "✓ TEST 3 COMPLETED (bug documented)\n\n"; std::cout << "✓ TEST 3 COMPLETED (bug documented)\n\n";
// Clean up for next test // Clean up for next test
while (consumerIO->hasMessages() > 0) consumerIO->pullMessage(); while (consumerIO->hasMessages() > 0) consumerIO->pullMessage();
while (broadcastIO->hasMessages() > 0) broadcastIO->pullMessage(); while (broadcastIO->hasMessages() > 0) broadcastIO->pullMessage();
while (batchIO->hasMessages() > 0) batchIO->pullMessage(); while (batchIO->hasMessages() > 0) batchIO->pullMessage();
while (stressIO->hasMessages() > 0) stressIO->pullMessage(); while (stressIO->hasMessages() > 0) stressIO->pullMessage();
// ======================================================================== // ========================================================================
// TEST 4: Low-Frequency Subscriptions (Batching) // TEST 4: Low-Frequency Subscriptions (Batching)
// ======================================================================== // ========================================================================
std::cout << "=== TEST 4: Low-Frequency Subscriptions ===\n"; std::cout << "=== TEST 4: Low-Frequency Subscriptions ===\n";
SubscriptionConfig batchConfig; SubscriptionConfig batchConfig;
batchConfig.replaceable = true; batchConfig.replaceable = true;
batchConfig.batchInterval = 1000; // 1 second batchConfig.batchInterval = 1000; // 1 second
batchIO->subscribeLowFreq("batch:.*", batchConfig); batchIO->subscribeLowFreq("batch:.*", batchConfig);
std::cout << " Publishing 100 messages over 2 seconds...\n"; std::cout << " Publishing 100 messages over 2 seconds...\n";
int batchPublished = 0; int batchPublished = 0;
auto batchStart = std::chrono::high_resolution_clock::now(); auto batchStart = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 100; i++) { for (int i = 0; i < 100; i++) {
auto data = std::make_unique<JsonDataNode>("data", nlohmann::json{ auto data = std::make_unique<JsonDataNode>("data", nlohmann::json{
{"timestamp", i}, {"timestamp", i},
{"value", i * 0.1f} {"value", i * 0.1f}
}); });
producerIO->publish("batch:metric", std::move(data)); producerIO->publish("batch:metric", std::move(data));
batchPublished++; batchPublished++;
std::this_thread::sleep_for(std::chrono::milliseconds(20)); // 50 Hz std::this_thread::sleep_for(std::chrono::milliseconds(20)); // 50 Hz
} }
auto batchEnd = std::chrono::high_resolution_clock::now(); auto batchEnd = std::chrono::high_resolution_clock::now();
float batchDuration = std::chrono::duration<float>(batchEnd - batchStart).count(); float batchDuration = std::chrono::duration<float>(batchEnd - batchStart).count();
// Check batched messages // Check batched messages
std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::this_thread::sleep_for(std::chrono::milliseconds(100));
int batchesReceived = 0; int batchesReceived = 0;
while (batchIO->hasMessages() > 0) { while (batchIO->hasMessages() > 0) {
auto msg = batchIO->pullMessage(); auto msg = batchIO->pullMessage();
batchesReceived++; batchesReceived++;
} }
std::cout << " Published: " << batchPublished << " messages over " << batchDuration << "s\n"; std::cout << " Published: " << batchPublished << " messages over " << batchDuration << "s\n";
std::cout << " Received: " << batchesReceived << " batches\n"; std::cout << " Received: " << batchesReceived << " batches\n";
std::cout << " Expected: ~" << static_cast<int>(batchDuration) << " batches (1/second)\n"; std::cout << " Expected: ~" << static_cast<int>(batchDuration) << " batches (1/second)\n";
// With 1s batching, expect fewer messages than published // With 1s batching, expect fewer messages than published
ASSERT_LT(batchesReceived, batchPublished, "Batching should reduce message count"); ASSERT_LT(batchesReceived, batchPublished, "Batching should reduce message count");
reporter.addMetric("batch_count", batchesReceived); reporter.addMetric("batch_count", batchesReceived);
reporter.addMetric("batch_published", batchPublished); reporter.addMetric("batch_published", batchPublished);
reporter.addAssertion("batching_reduces_messages", batchesReceived < batchPublished); reporter.addAssertion("batching_reduces_messages", batchesReceived < batchPublished);
std::cout << "✓ TEST 4 PASSED\n\n"; std::cout << "✓ TEST 4 PASSED\n\n";
// ======================================================================== // ========================================================================
// TEST 5: Backpressure & Queue Overflow // TEST 5: Backpressure & Queue Overflow
// ======================================================================== // ========================================================================
std::cout << "=== TEST 5: Backpressure & Queue Overflow ===\n"; std::cout << "=== TEST 5: Backpressure & Queue Overflow ===\n";
consumerIO->subscribe("stress:flood"); consumerIO->subscribe("stress:flood");
std::cout << " Publishing 10000 messages without pulling...\n"; std::cout << " Publishing 10000 messages without pulling...\n";
for (int i = 0; i < 10000; i++) { for (int i = 0; i < 10000; i++) {
auto data = std::make_unique<JsonDataNode>("data", nlohmann::json{{"flood_id", i}}); auto data = std::make_unique<JsonDataNode>("data", nlohmann::json{{"flood_id", i}});
producerIO->publish("stress:flood", std::move(data)); producerIO->publish("stress:flood", std::move(data));
} }
std::this_thread::sleep_for(std::chrono::milliseconds(50)); std::this_thread::sleep_for(std::chrono::milliseconds(50));
// Check health // Check health
auto health = consumerIO->getHealth(); auto health = consumerIO->getHealth();
std::cout << " Health status:\n"; std::cout << " Health status:\n";
std::cout << " Queue size: " << health.queueSize << " / " << health.maxQueueSize << "\n"; std::cout << " Queue size: " << health.queueSize << " / " << health.maxQueueSize << "\n";
std::cout << " Dropping: " << (health.dropping ? "YES" : "NO") << "\n"; std::cout << " Dropping: " << (health.dropping ? "YES" : "NO") << "\n";
std::cout << " Dropped count: " << health.droppedMessageCount << "\n"; std::cout << " Dropped count: " << health.droppedMessageCount << "\n";
ASSERT_GT(health.queueSize, 0, "Queue should have messages"); ASSERT_GT(health.queueSize, 0, "Queue should have messages");
reporter.addMetric("queue_size", health.queueSize); reporter.addMetric("queue_size", health.queueSize);
reporter.addMetric("dropped_messages", health.droppedMessageCount); reporter.addMetric("dropped_messages", health.droppedMessageCount);
reporter.addAssertion("backpressure_monitoring", true); reporter.addAssertion("backpressure_monitoring", true);
std::cout << "✓ TEST 5 PASSED\n\n"; std::cout << "✓ TEST 5 PASSED\n\n";
// Clean up queue // Clean up queue
while (consumerIO->hasMessages() > 0) consumerIO->pullMessage(); while (consumerIO->hasMessages() > 0) consumerIO->pullMessage();
// ======================================================================== // ========================================================================
// TEST 6: Thread Safety (Concurrent Pub/Pull) // TEST 6: Thread Safety (Concurrent Pub/Pull)
// ======================================================================== // ========================================================================
std::cout << "=== TEST 6: Thread Safety ===\n"; std::cout << "=== TEST 6: Thread Safety ===\n";
consumerIO->subscribe("thread:.*"); consumerIO->subscribe("thread:.*");
std::atomic<int> publishedTotal{0}; std::atomic<int> publishedTotal{0};
std::atomic<int> receivedTotal{0}; std::atomic<int> receivedTotal{0};
std::atomic<bool> running{true}; std::atomic<bool> running{true};
std::cout << " Launching 5 publisher threads...\n"; std::cout << " Launching 5 publisher threads...\n";
std::vector<std::thread> publishers; std::vector<std::thread> publishers;
for (int t = 0; t < 5; t++) { for (int t = 0; t < 5; t++) {
publishers.emplace_back([&, t]() { publishers.emplace_back([&, t]() {
for (int i = 0; i < 100; i++) { for (int i = 0; i < 100; i++) {
auto data = std::make_unique<JsonDataNode>("data", nlohmann::json{ auto data = std::make_unique<JsonDataNode>("data", nlohmann::json{
{"thread", t}, {"thread", t},
{"id", i} {"id", i}
}); });
producerIO->publish("thread:test", std::move(data)); producerIO->publish("thread:test", std::move(data));
publishedTotal++; publishedTotal++;
std::this_thread::sleep_for(std::chrono::microseconds(100)); std::this_thread::sleep_for(std::chrono::microseconds(100));
} }
}); });
} }
std::cout << " Launching 3 consumer threads...\n"; std::cout << " Launching 3 consumer threads...\n";
std::vector<std::thread> consumers; std::vector<std::thread> consumers;
for (int t = 0; t < 3; t++) { for (int t = 0; t < 3; t++) {
consumers.emplace_back([&]() { consumers.emplace_back([&]() {
while (running || consumerIO->hasMessages() > 0) { while (running || consumerIO->hasMessages() > 0) {
if (consumerIO->hasMessages() > 0) { if (consumerIO->hasMessages() > 0) {
try { try {
auto msg = consumerIO->pullMessage(); auto msg = consumerIO->pullMessage();
receivedTotal++; receivedTotal++;
} catch (...) { } catch (...) {
// Expected: may have race conditions // Expected: may have race conditions
} }
} }
std::this_thread::sleep_for(std::chrono::microseconds(500)); std::this_thread::sleep_for(std::chrono::microseconds(500));
} }
}); });
} }
// Wait for publishers // Wait for publishers
for (auto& t : publishers) { for (auto& t : publishers) {
t.join(); t.join();
} }
std::cout << " All publishers done: " << publishedTotal << " messages\n"; std::cout << " All publishers done: " << publishedTotal << " messages\n";
// Let consumers finish // Let consumers finish
std::this_thread::sleep_for(std::chrono::milliseconds(200)); std::this_thread::sleep_for(std::chrono::milliseconds(200));
running = false; running = false;
for (auto& t : consumers) { for (auto& t : consumers) {
t.join(); t.join();
} }
std::cout << " All consumers done: " << receivedTotal << " messages\n"; std::cout << " All consumers done: " << receivedTotal << " messages\n";
ASSERT_GT(receivedTotal, 0, "Should receive at least some messages"); ASSERT_GT(receivedTotal, 0, "Should receive at least some messages");
reporter.addMetric("concurrent_published", publishedTotal); reporter.addMetric("concurrent_published", publishedTotal);
reporter.addMetric("concurrent_received", receivedTotal); reporter.addMetric("concurrent_received", receivedTotal);
reporter.addAssertion("thread_safety", true); // No crash = success reporter.addAssertion("thread_safety", true); // No crash = success
std::cout << "✓ TEST 6 PASSED (no crashes)\n\n"; std::cout << "✓ TEST 6 PASSED (no crashes)\n\n";
// ======================================================================== // ========================================================================
// FINAL REPORT // FINAL REPORT
// ======================================================================== // ========================================================================
metrics.printReport(); metrics.printReport();
reporter.printFinalReport(); reporter.printFinalReport();
return reporter.getExitCode(); return reporter.getExitCode();
} }

View File

@ -32,7 +32,7 @@ public:
private: private:
std::vector<Tank> tanks; std::vector<Tank> tanks;
int frameCount = 0; 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; std::unique_ptr<IDataNode> config;
void updateTank(Tank& tank, float dt); void updateTank(Tank& tank, float dt);

View File

@ -5,7 +5,7 @@
#include <memory> #include <memory>
// This line will be modified by AutoCompiler during race condition tests // This line will be modified by AutoCompiler during race condition tests
std::string moduleVersion = "v1"; std::string moduleVersion = "v10";
namespace grove { namespace grove {