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)
enable_testing()
# Windows/MinGW: Copy runtime DLLs to build directories for CTest
if(WIN32 AND MINGW)
get_filename_component(MINGW_BIN_DIR ${CMAKE_CXX_COMPILER} DIRECTORY)
set(MINGW_RUNTIME_DLLS libgcc_s_seh-1.dll libstdc++-6.dll libwinpthread-1.dll)
foreach(DLL ${MINGW_RUNTIME_DLLS})
set(DLL_PATH "${MINGW_BIN_DIR}/${DLL}")
if(EXISTS "${DLL_PATH}")
file(COPY "${DLL_PATH}" DESTINATION "${CMAKE_BINARY_DIR}")
file(COPY "${DLL_PATH}" DESTINATION "${CMAKE_BINARY_DIR}/tests")
file(COPY "${DLL_PATH}" DESTINATION "${CMAKE_BINARY_DIR}/external/StillHammer/topictree/tests")
endif()
endforeach()
message(STATUS "MinGW runtime DLLs copied for CTest")
endif()
add_subdirectory(tests)
endif()

View File

@ -9,6 +9,17 @@ FetchContent_Declare(
)
FetchContent_MakeAvailable(Catch2)
# Windows/MinGW: Copy runtime DLLs at configure time
if(WIN32 AND MINGW)
get_filename_component(MINGW_BIN_DIR ${CMAKE_CXX_COMPILER} DIRECTORY)
set(MINGW_DLLS libgcc_s_seh-1.dll libstdc++-6.dll libwinpthread-1.dll)
foreach(DLL ${MINGW_DLLS})
if(EXISTS "${MINGW_BIN_DIR}/${DLL}")
file(COPY "${MINGW_BIN_DIR}/${DLL}" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}")
endif()
endforeach()
endif()
# Helper macro to create test executables
macro(add_topictree_test test_name)
add_executable(${test_name} ${test_name}.cpp)
@ -17,7 +28,14 @@ macro(add_topictree_test test_name)
Catch2::Catch2WithMain
)
target_compile_features(${test_name} PRIVATE cxx_std_17)
add_test(NAME ${test_name} COMMAND ${test_name})
if(WIN32)
# Use cmd.exe start to properly find DLLs in working directory
add_test(NAME ${test_name}
COMMAND ${CMAKE_COMMAND} -E chdir ${CMAKE_CURRENT_BINARY_DIR}
$<TARGET_FILE:${test_name}>)
else()
add_test(NAME ${test_name} COMMAND ${test_name} WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
endif()
endmacro()
# Add all scenario tests

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -32,7 +32,7 @@ public:
private:
std::vector<Tank> tanks;
int frameCount = 0;
std::string moduleVersion = "v1.0";std::shared_ptr<spdlog::logger> logger;
std::string moduleVersion = "v2.0 HOT-RELOADED";:shared_ptr<spdlog::logger> logger;
std::unique_ptr<IDataNode> config;
void updateTank(Tank& tank, float dt);

View File

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