Added three new integration test scenario documents: - Scenario 11: IO System Stress Test - Tests IntraIO pub/sub with pattern matching, batching, backpressure, and thread safety - Scenario 12: DataNode Integration Test - Tests IDataTree with hot-reload, persistence, hashing, and performance on 1000+ nodes - Scenario 13: Cross-System Integration - Tests IO + DataNode working together with config hot-reload chains and concurrent access Also includes comprehensive DataNode system architecture analysis documentation. These scenarios complement the existing test suite by covering the IO communication layer and data management systems that were previously untested. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
38 KiB
38 KiB
Scénario 13: Cross-System Integration (IO + DataNode)
Priorité: ⭐⭐ SHOULD HAVE Phase: 2 (SHOULD HAVE) Durée estimée: ~6 minutes Effort implémentation: ~6-8 heures
🎯 Objectif
Valider que les systèmes IO (IntraIO) et DataNode (IDataTree) fonctionnent correctement ensemble dans des cas d'usage réels:
- Config hot-reload → republish via IO
- State persistence via DataNode + message routing via IO
- Multi-module coordination (Module A publie state → Module B lit via DataNode)
- Concurrent access (IO threads + DataNode threads)
- Integration avec hot-reload de modules
- Performance du système complet
Note: Ce test valide l'intégration complète du moteur, pas les composants isolés.
📋 Description
Setup Initial
-
Créer IDataTree avec structure complète:
- config/ - Configuration modules (units, gameplay, network)
- data/ - State persistence (player, world, economy)
- runtime/ - State temporaire (fps, metrics, active_entities)
-
Créer 4 modules avec IO + DataNode:
- ConfigWatcherModule - Surveille config/, publie changements via IO
- PlayerModule - Gère state joueur, persiste via data/, publie events
- EconomyModule - Souscrit à player events, met à jour economy data/
- MetricsModule - Collecte metrics dans runtime/, publie stats
-
Total: 4 modules communicant via IO et partageant data via DataNode
Test Séquence
Test 1: Config Hot-Reload → IO Broadcast (60s)
- ConfigWatcherModule souscrit à hot-reload callbacks
- Modifier
config/gameplay.json(changer difficulty) - Quand callback déclenché:
- ConfigWatcherModule publie "config:gameplay:changed" via IO
- PlayerModule souscrit et reçoit notification
- PlayerModule lit nouvelle config via DataNode
- PlayerModule ajuste son comportement
- Vérifier:
- Callback → publish → subscribe → read chain fonctionne
- Nouvelle config appliquée dans PlayerModule
- Latence totale < 100ms
Test 2: State Persistence + Event Publishing (60s)
- PlayerModule crée state:
data/player/profile- {name, level, gold}data/player/inventory- {items[]}
- PlayerModule sauvegarde via
tree->saveNode() - PlayerModule publie "player:level_up" via IO
- EconomyModule souscrit à "player:*"
- EconomyModule reçoit event, lit player data via DataNode
- EconomyModule calcule bonus, met à jour
data/economy/bonuses - EconomyModule sauvegarde via
tree->saveNode() - Vérifier:
- Save → publish → subscribe → read → save chain fonctionne
- Data persistence correcte
- Pas de race conditions
Test 3: Multi-Module State Synchronization (90s)
- PlayerModule met à jour
data/player/gold= 1000 - PlayerModule publie "player:gold:updated" avec {gold: 1000}
- EconomyModule reçoit event via IO
- EconomyModule lit
data/player/goldvia DataNode - Vérifier cohérence:
- Valeur dans message IO = valeur dans DataNode
- Pas de désynchronisation
- Order des events préservé
- Répéter 100 fois avec updates rapides
- Vérifier consistency finale
Test 4: Runtime Metrics Collection (60s)
- MetricsModule collecte metrics toutes les 100ms:
runtime/fps- FPS actuelruntime/memory- Memory usageruntime/message_count- Messages IO
- MetricsModule publie "metrics:snapshot" toutes les secondes
- ConfigWatcherModule souscrit et log metrics
- Vérifier:
- Runtime data pas persisté (pas de fichiers)
- Metrics publishing fonctionne
- Low-frequency batching optimise (pas 10 msg/s mais 1 msg/s)
Test 5: Concurrent Access (IO + DataNode) (90s)
- Lancer 4 threads:
- Thread 1: PlayerModule publie events à 100 Hz
- Thread 2: EconomyModule lit data/ à 50 Hz
- Thread 3: MetricsModule écrit runtime/ à 100 Hz
- Thread 4: ConfigWatcherModule lit config/ à 10 Hz
- Exécuter pendant 60 secondes
- Vérifier:
- Aucun crash
- Aucune corruption de data
- Aucun deadlock
- Performance acceptable (< 10% overhead)
Test 6: Hot-Reload Module + Preserve State (90s)
- PlayerModule a state actif:
- 50 entities dans
runtime/entities - Gold = 5000 dans
data/player/gold - Active quest dans
runtime/quest
- 50 entities dans
- Déclencher hot-reload de PlayerModule:
getState()extrait tout (data/ + runtime/)- Recompile module
setState()restaure
- Pendant reload:
- EconomyModule continue de publier via IO
- Messages accumulés dans queue PlayerModule
- Après reload:
- PlayerModule pull messages accumulés
- Vérifie state préservé (50 entities, 5000 gold, quest)
- Continue processing normalement
- Vérifier:
- State complet préservé (DataNode + runtime)
- Messages pas perdus (IO queue)
- Pas de corruption
Test 7: Config Change Cascades (60s)
- Modifier
config/gameplay.json→ difficulty = "hard" - ConfigWatcherModule détecte → publie "config:gameplay:changed"
- PlayerModule reçoit → reload config → ajuste HP multiplier
- PlayerModule publie "player:config:updated"
- EconomyModule reçoit → reload config → ajuste prices
- EconomyModule publie "economy:config:updated"
- MetricsModule reçoit → log cascade
- Vérifier:
- Cascade complète en < 500ms
- Tous modules synchronisés
- Ordre des events correct
Test 8: Large State + High-Frequency IO (60s)
- Créer large DataNode tree (1000 nodes)
- Publier 10k messages/s via IO
- Modules lisent DataNode pendant IO flood
- Mesurer:
- Latence IO: < 10ms p99
- Latence DataNode read: < 5ms p99
- Memory growth: < 20MB
- CPU usage: < 80%
- Vérifier:
- Systèmes restent performants
- Pas de dégradation mutuelle
🏗️ Implémentation
ConfigWatcherModule Structure
// ConfigWatcherModule.h
class ConfigWatcherModule : public IModule {
public:
void initialize(std::shared_ptr<IDataNode> config) override;
void process(float deltaTime) override;
std::shared_ptr<IDataNode> getState() const override;
void setState(std::shared_ptr<IDataNode> state) override;
bool isIdle() const override { return true; }
private:
std::shared_ptr<IIO> io;
std::shared_ptr<IDataTree> tree;
void onConfigReloaded();
void publishConfigChange(const std::string& configName);
};
PlayerModule Structure
// PlayerModule.h
class PlayerModule : public IModule {
public:
void initialize(std::shared_ptr<IDataNode> config) override;
void process(float deltaTime) override;
std::shared_ptr<IDataNode> getState() const override;
void setState(std::shared_ptr<IDataNode> state) override;
bool isIdle() const override { return true; }
private:
std::shared_ptr<IIO> io;
std::shared_ptr<IDataTree> tree;
int gold = 0;
int level = 1;
std::vector<std::string> inventory;
void handleConfigChange();
void savePlayerData();
void publishLevelUp();
};
Test Principal
// test_13_cross_system.cpp
#include "helpers/TestMetrics.h"
#include "helpers/TestAssertions.h"
#include "helpers/TestReporter.h"
#include <atomic>
#include <thread>
int main() {
TestReporter reporter("Cross-System Integration");
TestMetrics metrics;
// === SETUP ===
std::filesystem::create_directories("test_cross/config");
std::filesystem::create_directories("test_cross/data");
auto tree = std::make_shared<JsonDataTree>("test_cross");
DebugEngine engine;
engine.setDataTree(tree);
// Charger modules
engine.loadModule("ConfigWatcherModule", "build/modules/libConfigWatcherModule.so");
engine.loadModule("PlayerModule", "build/modules/libPlayerModule.so");
engine.loadModule("EconomyModule", "build/modules/libEconomyModule.so");
engine.loadModule("MetricsModule", "build/modules/libMetricsModule.so");
auto config = createJsonConfig({
{"transport", "intra"},
{"instanceId", "test_cross"}
});
engine.initializeModule("ConfigWatcherModule", config);
engine.initializeModule("PlayerModule", config);
engine.initializeModule("EconomyModule", config);
engine.initializeModule("MetricsModule", config);
// ========================================================================
// TEST 1: Config Hot-Reload → IO Broadcast
// ========================================================================
std::cout << "\n=== TEST 1: Config Hot-Reload → IO Broadcast ===\n";
// Créer config initial
nlohmann::json gameplayConfig = {
{"difficulty", "normal"},
{"hpMultiplier", 1.0}
};
std::ofstream configFile("test_cross/config/gameplay.json");
configFile << gameplayConfig.dump(2);
configFile.close();
tree->loadConfigFile("gameplay.json");
// Setup reload callback
std::atomic<int> configChangedEvents{0};
auto playerIO = engine.getModuleIO("PlayerModule");
playerIO->subscribe("config:gameplay:changed", {});
// ConfigWatcherModule setup callback
tree->onTreeReloaded([&]() {
std::cout << " → Config reloaded, publishing event...\n";
auto watcherIO = engine.getModuleIO("ConfigWatcherModule");
auto data = std::make_unique<JsonDataNode>(nlohmann::json{
{"config", "gameplay"},
{"timestamp", std::time(nullptr)}
});
watcherIO->publish("config:gameplay:changed", std::move(data));
});
// Modifier config
std::this_thread::sleep_for(std::chrono::milliseconds(100));
gameplayConfig["difficulty"] = "hard";
gameplayConfig["hpMultiplier"] = 1.5;
std::ofstream configFile2("test_cross/config/gameplay.json");
configFile2 << gameplayConfig.dump(2);
configFile2.close();
auto reloadStart = std::chrono::high_resolution_clock::now();
// Trigger reload
tree->reloadIfChanged();
// Process pour permettre IO routing
engine.update(1.0f/60.0f);
// PlayerModule vérifie message
if (playerIO->hasMessages() > 0) {
auto msg = playerIO->pullMessage();
configChangedEvents++;
// PlayerModule lit nouvelle config
auto gameplay = tree->getConfigRoot()->getChild("gameplay");
std::string difficulty = gameplay->getString("difficulty");
double hpMult = gameplay->getDouble("hpMultiplier");
std::cout << " PlayerModule received config change: difficulty=" << difficulty
<< ", hpMult=" << hpMult << "\n";
ASSERT_EQ(difficulty, "hard", "Difficulty should be updated");
ASSERT_EQ(hpMult, 1.5, "HP multiplier should be updated");
}
auto reloadEnd = std::chrono::high_resolution_clock::now();
float reloadLatency = std::chrono::duration<float, std::milli>(reloadEnd - reloadStart).count();
std::cout << "Total latency (reload + publish + subscribe + read): " << reloadLatency << "ms\n";
ASSERT_LT(reloadLatency, 100.0f, "Total latency should be < 100ms");
ASSERT_EQ(configChangedEvents, 1, "Should receive exactly 1 config change event");
reporter.addMetric("config_reload_latency_ms", reloadLatency);
reporter.addAssertion("config_hotreload_chain", true);
std::cout << "✓ TEST 1 PASSED\n";
// ========================================================================
// TEST 2: State Persistence + Event Publishing
// ========================================================================
std::cout << "\n=== TEST 2: State Persistence + Event Publishing ===\n";
auto dataRoot = tree->getDataRoot();
// PlayerModule crée state
auto player = std::make_shared<JsonDataNode>("player", nlohmann::json::object());
auto profile = std::make_shared<JsonDataNode>("profile", nlohmann::json{
{"name", "TestPlayer"},
{"level", 5},
{"gold", 1000}
});
player->setChild("profile", profile);
dataRoot->setChild("player", player);
// Save
tree->saveNode("data/player");
// Verify file
ASSERT_TRUE(std::filesystem::exists("test_cross/data/player/profile.json"),
"Profile should be saved");
// PlayerModule publie level up
auto levelUpData = std::make_unique<JsonDataNode>(nlohmann::json{
{"event", "level_up"},
{"newLevel", 6},
{"goldBonus", 500}
});
playerIO->publish("player:level_up", std::move(levelUpData));
// EconomyModule souscrit
auto economyIO = engine.getModuleIO("EconomyModule");
economyIO->subscribe("player:*", {});
engine.update(1.0f/60.0f);
// EconomyModule reçoit et traite
if (economyIO->hasMessages() > 0) {
auto msg = economyIO->pullMessage();
std::cout << " EconomyModule received: " << msg.topic << "\n";
// EconomyModule lit player data
auto playerData = tree->getDataRoot()->getChild("player")->getChild("profile");
int gold = playerData->getInt("gold");
std::cout << " Player gold: " << gold << "\n";
// EconomyModule calcule bonus
int goldBonus = 500;
int newGold = gold + goldBonus;
// Update data
playerData->setInt("gold", newGold);
// Create economy bonuses
auto economy = std::make_shared<JsonDataNode>("economy", nlohmann::json::object());
auto bonuses = std::make_shared<JsonDataNode>("bonuses", nlohmann::json{
{"levelUpBonus", goldBonus},
{"appliedAt", std::time(nullptr)}
});
economy->setChild("bonuses", bonuses);
dataRoot->setChild("economy", economy);
// Save economy data
tree->saveNode("data/economy");
std::cout << " EconomyModule updated bonuses and saved\n";
}
// Verify full chain
ASSERT_TRUE(std::filesystem::exists("test_cross/data/economy/bonuses.json"),
"Economy bonuses should be saved");
reporter.addAssertion("state_persistence_chain", true);
std::cout << "✓ TEST 2 PASSED\n";
// ========================================================================
// TEST 3: Multi-Module State Synchronization
// ========================================================================
std::cout << "\n=== TEST 3: Multi-Module State Synchronization ===\n";
int syncErrors = 0;
for (int i = 0; i < 100; i++) {
// PlayerModule met à jour gold
int goldValue = 1000 + i * 10;
auto playerProfile = tree->getDataRoot()->getChild("player")->getChild("profile");
playerProfile->setInt("gold", goldValue);
// PlayerModule publie event avec valeur
auto goldUpdate = std::make_unique<JsonDataNode>(nlohmann::json{
{"event", "gold_updated"},
{"gold", goldValue}
});
playerIO->publish("player:gold:updated", std::move(goldUpdate));
engine.update(1.0f/60.0f);
// EconomyModule reçoit et vérifie cohérence
if (economyIO->hasMessages() > 0) {
auto msg = economyIO->pullMessage();
auto* msgData = dynamic_cast<JsonDataNode*>(msg.data.get());
int msgGold = msgData->getJsonData()["gold"];
// Lire DataNode
auto playerData = tree->getDataRoot()->getChild("player")->getChild("profile");
int dataGold = playerData->getInt("gold");
if (msgGold != dataGold) {
std::cerr << " SYNC ERROR: msg=" << msgGold << " data=" << dataGold << "\n";
syncErrors++;
}
}
}
std::cout << "Synchronization errors: " << syncErrors << " / 100\n";
ASSERT_EQ(syncErrors, 0, "Should have zero synchronization errors");
reporter.addMetric("sync_errors", syncErrors);
reporter.addAssertion("state_synchronization", syncErrors == 0);
std::cout << "✓ TEST 3 PASSED\n";
// ========================================================================
// TEST 4: Runtime Metrics Collection
// ========================================================================
std::cout << "\n=== TEST 4: Runtime Metrics Collection ===\n";
auto runtimeRoot = tree->getRuntimeRoot();
auto metricsIO = engine.getModuleIO("MetricsModule");
// MetricsModule publie metrics avec low-freq batching
IIO::SubscriptionConfig metricsConfig;
metricsConfig.replaceable = true;
metricsConfig.batchInterval = 1000; // 1 second
playerIO->subscribeLowFreq("metrics:*", metricsConfig);
// Simulate 3 seconds de metrics collection
for (int sec = 0; sec < 3; sec++) {
for (int i = 0; i < 10; i++) {
// MetricsModule collecte metrics
auto metrics = std::make_shared<JsonDataNode>("metrics", nlohmann::json{
{"fps", 60.0},
{"memory", 125000000 + i * 1000},
{"messageCount", i * 100}
});
runtimeRoot->setChild("metrics", metrics);
// Publie snapshot
auto snapshot = std::make_unique<JsonDataNode>(nlohmann::json{
{"fps", 60.0},
{"memory", 125000000 + i * 1000},
{"timestamp", std::time(nullptr)}
});
metricsIO->publish("metrics:snapshot", std::move(snapshot));
std::this_thread::sleep_for(std::chrono::milliseconds(100));
engine.update(1.0f/60.0f);
}
}
// Vérifier batching
int snapshotsReceived = 0;
while (playerIO->hasMessages() > 0) {
playerIO->pullMessage();
snapshotsReceived++;
}
std::cout << "Snapshots received: " << snapshotsReceived << " (expected ~3 due to batching)\n";
ASSERT_TRUE(snapshotsReceived >= 2 && snapshotsReceived <= 4,
"Should receive ~3 batched snapshots");
// Vérifier runtime pas persisté
ASSERT_FALSE(std::filesystem::exists("test_cross/runtime"),
"Runtime data should not be persisted");
reporter.addMetric("batched_snapshots", snapshotsReceived);
reporter.addAssertion("runtime_metrics", true);
std::cout << "✓ TEST 4 PASSED\n";
// ========================================================================
// TEST 5: Concurrent Access (IO + DataNode)
// ========================================================================
std::cout << "\n=== TEST 5: Concurrent Access ===\n";
std::atomic<bool> running{true};
std::atomic<int> publishCount{0};
std::atomic<int> readCount{0};
std::atomic<int> writeCount{0};
std::atomic<int> errors{0};
// Thread 1: PlayerModule publie events
std::thread pubThread([&]() {
while (running) {
try {
auto data = std::make_unique<JsonDataNode>(nlohmann::json{{"id", publishCount++}});
playerIO->publish("concurrent:test", std::move(data));
std::this_thread::sleep_for(std::chrono::milliseconds(10));
} catch (...) {
errors++;
}
}
});
// Thread 2: EconomyModule lit data/
std::thread readThread([&]() {
while (running) {
try {
auto playerData = tree->getDataRoot()->getChild("player");
if (playerData) {
auto profile = playerData->getChild("profile");
if (profile) {
int gold = profile->getInt("gold", 0);
readCount++;
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(20));
} catch (...) {
errors++;
}
}
});
// Thread 3: MetricsModule écrit runtime/
std::thread writeThread([&]() {
while (running) {
try {
auto metrics = std::make_shared<JsonDataNode>("metrics", nlohmann::json{
{"counter", writeCount++}
});
runtimeRoot->setChild("metrics", metrics);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
} catch (...) {
errors++;
}
}
});
// Thread 4: ConfigWatcherModule lit config/
std::thread configThread([&]() {
while (running) {
try {
auto gameplay = tree->getConfigRoot()->getChild("gameplay");
if (gameplay) {
std::string diff = gameplay->getString("difficulty", "normal");
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
} catch (...) {
errors++;
}
}
});
// Run for 5 seconds
auto concurrentStart = std::chrono::high_resolution_clock::now();
std::this_thread::sleep_for(std::chrono::seconds(5));
running = false;
auto concurrentEnd = std::chrono::high_resolution_clock::now();
pubThread.join();
readThread.join();
writeThread.join();
configThread.join();
float duration = std::chrono::duration<float>(concurrentEnd - concurrentStart).count();
std::cout << "Concurrent test ran for " << duration << "s\n";
std::cout << " Publishes: " << publishCount << "\n";
std::cout << " Reads: " << readCount << "\n";
std::cout << " Writes: " << writeCount << "\n";
std::cout << " Errors: " << errors << "\n";
ASSERT_EQ(errors, 0, "Should have zero errors during concurrent access");
ASSERT_GT(publishCount, 0, "Should have published messages");
ASSERT_GT(readCount, 0, "Should have read data");
ASSERT_GT(writeCount, 0, "Should have written data");
reporter.addMetric("concurrent_publishes", publishCount);
reporter.addMetric("concurrent_reads", readCount);
reporter.addMetric("concurrent_writes", writeCount);
reporter.addMetric("concurrent_errors", errors);
reporter.addAssertion("concurrent_access", errors == 0);
std::cout << "✓ TEST 5 PASSED\n";
// ========================================================================
// TEST 6: Hot-Reload Module + Preserve State
// ========================================================================
std::cout << "\n=== TEST 6: Hot-Reload Module + Preserve State ===\n";
// PlayerModule crée state complexe
auto entities = std::make_shared<JsonDataNode>("entities", nlohmann::json::array());
for (int i = 0; i < 50; i++) {
entities->getJsonData().push_back({{"id", i}, {"hp", 100}});
}
runtimeRoot->setChild("entities", entities);
auto playerGold = tree->getDataRoot()->getChild("player")->getChild("profile");
playerGold->setInt("gold", 5000);
tree->saveNode("data/player/profile");
auto quest = std::make_shared<JsonDataNode>("quest", nlohmann::json{
{"active", true},
{"questId", 42}
});
runtimeRoot->setChild("quest", quest);
std::cout << "State before reload: 50 entities, 5000 gold, quest #42 active\n";
// EconomyModule publie messages pendant reload
std::thread spamThread([&]() {
for (int i = 0; i < 100; i++) {
auto data = std::make_unique<JsonDataNode>(nlohmann::json{{"spam", i}});
economyIO->publish("player:spam", std::move(data));
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
});
// Trigger hot-reload de PlayerModule
std::this_thread::sleep_for(std::chrono::milliseconds(200));
auto stateBefore = engine.getModuleState("PlayerModule");
modifySourceFile("tests/modules/PlayerModule.cpp", "v1.0", "v2.0");
system("cmake --build build --target PlayerModule 2>&1 > /dev/null");
engine.reloadModule("PlayerModule");
spamThread.join();
// Vérifier state après reload
auto stateAfter = engine.getModuleState("PlayerModule");
auto entitiesAfter = runtimeRoot->getChild("entities");
int entityCount = entitiesAfter->getJsonData().size();
std::cout << "Entities after reload: " << entityCount << "\n";
ASSERT_EQ(entityCount, 50, "Should preserve 50 entities");
auto goldAfter = tree->getDataRoot()->getChild("player")->getChild("profile");
int goldValue = goldAfter->getInt("gold");
std::cout << "Gold after reload: " << goldValue << "\n";
ASSERT_EQ(goldValue, 5000, "Should preserve 5000 gold");
auto questAfter = runtimeRoot->getChild("quest");
bool questActive = questAfter->getBool("active");
int questId = questAfter->getInt("questId");
std::cout << "Quest after reload: active=" << questActive << ", id=" << questId << "\n";
ASSERT_EQ(questActive, true, "Quest should still be active");
ASSERT_EQ(questId, 42, "Quest ID should be preserved");
// Vérifier messages pas perdus
int spamReceived = 0;
while (playerIO->hasMessages() > 0) {
playerIO->pullMessage();
spamReceived++;
}
std::cout << "Spam messages received after reload: " << spamReceived << "\n";
ASSERT_GT(spamReceived, 0, "Should receive queued messages after reload");
reporter.addAssertion("hotreload_preserve_state", true);
reporter.addMetric("spam_messages_queued", spamReceived);
std::cout << "✓ TEST 6 PASSED\n";
// ========================================================================
// TEST 7: Config Change Cascades
// ========================================================================
std::cout << "\n=== TEST 7: Config Change Cascades ===\n";
// Subscribe chain
playerIO->subscribe("config:*", {});
economyIO->subscribe("player:*", {});
metricsIO->subscribe("economy:*", {});
auto cascadeStart = std::chrono::high_resolution_clock::now();
// 1. Modifier config
gameplayConfig["difficulty"] = "extreme";
std::ofstream configFile3("test_cross/config/gameplay.json");
configFile3 << gameplayConfig.dump(2);
configFile3.close();
// 2. Trigger reload
tree->reloadIfChanged();
auto watcherIO = engine.getModuleIO("ConfigWatcherModule");
watcherIO->publish("config:gameplay:changed", std::make_unique<JsonDataNode>(nlohmann::json{{"config", "gameplay"}}));
engine.update(1.0f/60.0f);
// 3. PlayerModule reçoit et publie
if (playerIO->hasMessages() > 0) {
playerIO->pullMessage();
playerIO->publish("player:config:updated", std::make_unique<JsonDataNode>(nlohmann::json{{"hpMult", 2.0}}));
}
engine.update(1.0f/60.0f);
// 4. EconomyModule reçoit et publie
if (economyIO->hasMessages() > 0) {
economyIO->pullMessage();
economyIO->publish("economy:config:updated", std::make_unique<JsonDataNode>(nlohmann::json{{"pricesMult", 1.5}}));
}
engine.update(1.0f/60.0f);
// 5. MetricsModule reçoit et log
if (metricsIO->hasMessages() > 0) {
metricsIO->pullMessage();
std::cout << " → Cascade complete!\n";
}
auto cascadeEnd = std::chrono::high_resolution_clock::now();
float cascadeTime = std::chrono::duration<float, std::milli>(cascadeEnd - cascadeStart).count();
std::cout << "Cascade latency: " << cascadeTime << "ms\n";
ASSERT_LT(cascadeTime, 500.0f, "Cascade should complete in < 500ms");
reporter.addMetric("cascade_latency_ms", cascadeTime);
reporter.addAssertion("config_cascade", true);
std::cout << "✓ TEST 7 PASSED\n";
// ========================================================================
// TEST 8: Large State + High-Frequency IO
// ========================================================================
std::cout << "\n=== TEST 8: Large State + High-Frequency IO ===\n";
// Créer large tree (1000 nodes)
auto largeRoot = tree->getDataRoot();
for (int i = 0; i < 100; i++) {
auto category = std::make_shared<JsonDataNode>("cat_" + std::to_string(i), nlohmann::json::object());
for (int j = 0; j < 10; j++) {
auto item = std::make_shared<JsonDataNode>("item_" + std::to_string(j), nlohmann::json{
{"id", i * 10 + j},
{"value", (i * 10 + j) * 100}
});
category->setChild("item_" + std::to_string(j), item);
}
largeRoot->setChild("cat_" + std::to_string(i), category);
}
std::cout << "Created large DataNode tree (1000 nodes)\n";
// High-frequency IO + concurrent DataNode reads
std::atomic<int> ioPublished{0};
std::atomic<int> dataReads{0};
std::vector<float> ioLatencies;
std::vector<float> dataLatencies;
running = true;
std::thread ioThread([&]() {
while (running) {
auto start = std::chrono::high_resolution_clock::now();
auto data = std::make_unique<JsonDataNode>(nlohmann::json{{"id", ioPublished++}});
playerIO->publish("stress:test", std::move(data));
auto end = std::chrono::high_resolution_clock::now();
float latency = std::chrono::duration<float, std::milli>(end - start).count();
ioLatencies.push_back(latency);
// Target: 10k msg/s = 0.1ms interval
std::this_thread::sleep_for(std::chrono::microseconds(100));
}
});
std::thread dataThread([&]() {
while (running) {
auto start = std::chrono::high_resolution_clock::now();
auto cat = largeRoot->getChild("cat_50");
if (cat) {
auto item = cat->getChild("item_5");
if (item) {
int value = item->getInt("value", 0);
dataReads++;
}
}
auto end = std::chrono::high_resolution_clock::now();
float latency = std::chrono::duration<float, std::milli>(end - start).count();
dataLatencies.push_back(latency);
std::this_thread::sleep_for(std::chrono::microseconds(500));
}
});
auto memBefore = getCurrentMemoryUsage();
std::this_thread::sleep_for(std::chrono::seconds(5));
running = false;
ioThread.join();
dataThread.join();
auto memAfter = getCurrentMemoryUsage();
long memGrowth = static_cast<long>(memAfter) - static_cast<long>(memBefore);
// Calculate p99 latencies
std::sort(ioLatencies.begin(), ioLatencies.end());
std::sort(dataLatencies.begin(), dataLatencies.end());
float ioP99 = ioLatencies[static_cast<size_t>(ioLatencies.size() * 0.99)];
float dataP99 = dataLatencies[static_cast<size_t>(dataLatencies.size() * 0.99)];
std::cout << "Performance results:\n";
std::cout << " IO published: " << ioPublished << " messages\n";
std::cout << " IO p99 latency: " << ioP99 << "ms\n";
std::cout << " DataNode reads: " << dataReads << "\n";
std::cout << " DataNode p99 latency: " << dataP99 << "ms\n";
std::cout << " Memory growth: " << (memGrowth / 1024.0 / 1024.0) << "MB\n";
ASSERT_LT(ioP99, 10.0f, "IO p99 latency should be < 10ms");
ASSERT_LT(dataP99, 5.0f, "DataNode p99 latency should be < 5ms");
ASSERT_LT(memGrowth, 20 * 1024 * 1024, "Memory growth should be < 20MB");
reporter.addMetric("io_p99_latency_ms", ioP99);
reporter.addMetric("datanode_p99_latency_ms", dataP99);
reporter.addMetric("memory_growth_mb", memGrowth / 1024.0 / 1024.0);
reporter.addAssertion("performance_under_load", true);
std::cout << "✓ TEST 8 PASSED\n";
// ========================================================================
// CLEANUP
// ========================================================================
std::filesystem::remove_all("test_cross");
// ========================================================================
// RAPPORT FINAL
// ========================================================================
metrics.printReport();
reporter.printFinalReport();
return reporter.getExitCode();
}
📊 Métriques Collectées
| Métrique | Description | Seuil |
|---|---|---|
| config_reload_latency_ms | Latence reload→publish→subscribe→read | < 100ms |
| sync_errors | Erreurs synchronisation IO/DataNode | 0 |
| batched_snapshots | Snapshots reçus avec batching | 2-4 |
| concurrent_publishes | Messages publiés en concurrence | > 0 |
| concurrent_reads | Lectures DataNode concurrentes | > 0 |
| concurrent_writes | Écritures DataNode concurrentes | > 0 |
| concurrent_errors | Erreurs pendant concurrence | 0 |
| spam_messages_queued | Messages queued pendant reload | > 0 |
| cascade_latency_ms | Latence cascade config changes | < 500ms |
| io_p99_latency_ms | P99 latence IO sous charge | < 10ms |
| datanode_p99_latency_ms | P99 latence DataNode sous charge | < 5ms |
| memory_growth_mb | Croissance mémoire sous charge | < 20MB |
✅ Critères de Succès
MUST PASS
- ✅ Config hot-reload chain fonctionne (< 100ms)
- ✅ State persistence + event publishing chain fonctionne
- ✅ Synchronization IO/DataNode sans erreurs
- ✅ Runtime metrics avec batching
- ✅ Concurrent access sans crashes/corruption
- ✅ Hot-reload préserve state complet
- ✅ Messages IO pas perdus pendant reload
- ✅ Config cascades propagent correctement
- ✅ Performance acceptable sous charge
NICE TO HAVE
- ✅ Config reload latency < 50ms (optimal)
- ✅ Cascade latency < 200ms (optimal)
- ✅ IO p99 < 5ms (optimal)
- ✅ DataNode p99 < 2ms (optimal)
🐛 Cas d'Erreur Attendus
| Erreur | Cause | Action |
|---|---|---|
| Config change pas propagé | Callback pas déclenché | FAIL - fix onTreeReloaded |
| Sync errors > 0 | Race condition IO/DataNode | FAIL - add locking |
| Messages perdus | Queue overflow pendant reload | WARN - increase queue size |
| Concurrent crashes | Missing mutex | FAIL - add thread safety |
| State corrompu après reload | setState() bug | FAIL - fix state restoration |
| Cascade timeout | Deadlock dans chain | FAIL - fix event routing |
| Performance degradation | O(n²) algorithm | FAIL - optimize |
| Memory leak | Resources not freed | FAIL - fix destructors |
📝 Output Attendu
================================================================================
TEST: Cross-System Integration (IO + DataNode)
================================================================================
=== TEST 1: Config Hot-Reload → IO Broadcast ===
→ Config reloaded, publishing event...
PlayerModule received config change: difficulty=hard, hpMult=1.5
Total latency (reload + publish + subscribe + read): 87ms
✓ TEST 1 PASSED
=== TEST 2: State Persistence + Event Publishing ===
EconomyModule received: player:level_up
Player gold: 1000
EconomyModule updated bonuses and saved
✓ TEST 2 PASSED
=== TEST 3: Multi-Module State Synchronization ===
Synchronization errors: 0 / 100
✓ TEST 3 PASSED
=== TEST 4: Runtime Metrics Collection ===
Snapshots received: 3 (expected ~3 due to batching)
✓ TEST 4 PASSED
=== TEST 5: Concurrent Access ===
Concurrent test ran for 5.001s
Publishes: 487
Reads: 243
Writes: 489
Errors: 0
✓ TEST 5 PASSED
=== TEST 6: Hot-Reload Module + Preserve State ===
State before reload: 50 entities, 5000 gold, quest #42 active
Entities after reload: 50
Gold after reload: 5000
Quest after reload: active=true, id=42
Spam messages received after reload: 94
✓ TEST 6 PASSED
=== TEST 7: Config Change Cascades ===
→ Cascade complete!
Cascade latency: 234ms
✓ TEST 7 PASSED
=== TEST 8: Large State + High-Frequency IO ===
Created large DataNode tree (1000 nodes)
Performance results:
IO published: 48723 messages
IO p99 latency: 8.3ms
DataNode reads: 9745
DataNode p99 latency: 3.2ms
Memory growth: 14.7MB
✓ TEST 8 PASSED
================================================================================
METRICS
================================================================================
Config reload latency: 87ms (threshold: < 100ms) ✓
Sync errors: 0 (threshold: 0) ✓
Batched snapshots: 3
Concurrent publishes: 487
Concurrent reads: 243
Concurrent writes: 489
Concurrent errors: 0 (threshold: 0) ✓
Spam messages queued: 94
Cascade latency: 234ms (threshold: < 500ms) ✓
IO p99 latency: 8.3ms (threshold: < 10ms) ✓
DataNode p99 latency: 3.2ms (threshold: < 5ms) ✓
Memory growth: 14.7MB (threshold: < 20MB) ✓
================================================================================
ASSERTIONS
================================================================================
✓ config_hotreload_chain
✓ state_persistence_chain
✓ state_synchronization
✓ runtime_metrics
✓ concurrent_access
✓ hotreload_preserve_state
✓ config_cascade
✓ performance_under_load
Result: ✅ PASSED (8/8 tests)
================================================================================
📅 Planning
Jour 1 (4h):
- Implémenter ConfigWatcherModule, PlayerModule, EconomyModule, MetricsModule
- Setup IDataTree avec structure config/data/runtime
- Tests 1-3 (config reload, persistence, sync)
Jour 2 (4h):
- Tests 4-6 (metrics, concurrent, hot-reload)
- Tests 7-8 (cascades, performance)
- Debug + validation
Conclusion: Ces 3 nouveaux scénarios (11, 12, 13) complètent la suite de tests d'intégration en couvrant les systèmes IO et DataNode, ainsi que leur intégration.