GroveEngine/tests/integration/test_01_production_hotreload.cpp
StillHammer ddbed30ed7 feat: Add Scenario 11 IO System test & fix IntraIO routing architecture
Implémentation complète du scénario 11 (IO System Stress Test) avec correction majeure de l'architecture de routing IntraIO.

## Nouveaux Modules de Test (Scenario 11)
- ProducerModule: Publie messages pour tests IO
- ConsumerModule: Consomme et valide messages reçus
- BroadcastModule: Test multi-subscriber broadcasting
- BatchModule: Test low-frequency batching
- IOStressModule: Tests de charge concurrents

## Test d'Intégration
- test_11_io_system.cpp: 6 tests validant:
  * Basic Publish-Subscribe
  * Pattern Matching avec wildcards
  * Multi-Module Routing (1-to-many)
  * Low-Frequency Subscriptions (batching)
  * Backpressure & Queue Overflow
  * Thread Safety (concurrent pub/pull)

## Fix Architecture Critique: IntraIO Routing
**Problème**: IntraIO::publish() et subscribe() n'utilisaient PAS IntraIOManager pour router entre modules.

**Solution**: Utilisation de JSON comme format de transport intermédiaire
- IntraIO::publish() → extrait JSON → IntraIOManager::routeMessage()
- IntraIO::subscribe() → enregistre au IntraIOManager::registerSubscription()
- IntraIOManager::routeMessage() → copie JSON pour chaque subscriber → deliverMessage()

**Bénéfices**:
-  Routing centralisé fonctionnel
-  Support 1-to-many (copie JSON au lieu de move unique_ptr)
-  Pas besoin d'implémenter IDataNode::clone()
-  Compatible futur NetworkIO (JSON sérialisable)

## Modules Scenario 13 (Cross-System)
- ConfigWatcherModule, PlayerModule, EconomyModule, MetricsModule
- test_13_cross_system.cpp (stub)

## Documentation
- CLAUDE_NEXT_SESSION.md: Instructions détaillées pour build/test

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 11:43:08 +08:00

268 lines
9.6 KiB
C++

#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();
}