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>
1023 lines
38 KiB
Markdown
1023 lines
38 KiB
Markdown
# 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
|
|
1. 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)
|
|
|
|
2. 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
|
|
|
|
3. Total: 4 modules communicant via IO et partageant data via DataNode
|
|
|
|
### Test Séquence
|
|
|
|
#### Test 1: Config Hot-Reload → IO Broadcast (60s)
|
|
1. ConfigWatcherModule souscrit à hot-reload callbacks
|
|
2. Modifier `config/gameplay.json` (changer difficulty)
|
|
3. 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
|
|
4. Vérifier:
|
|
- Callback → publish → subscribe → read chain fonctionne
|
|
- Nouvelle config appliquée dans PlayerModule
|
|
- Latence totale < 100ms
|
|
|
|
#### Test 2: State Persistence + Event Publishing (60s)
|
|
1. PlayerModule crée state:
|
|
- `data/player/profile` - {name, level, gold}
|
|
- `data/player/inventory` - {items[]}
|
|
2. PlayerModule sauvegarde via `tree->saveNode()`
|
|
3. PlayerModule publie "player:level_up" via IO
|
|
4. EconomyModule souscrit à "player:*"
|
|
5. EconomyModule reçoit event, lit player data via DataNode
|
|
6. EconomyModule calcule bonus, met à jour `data/economy/bonuses`
|
|
7. EconomyModule sauvegarde via `tree->saveNode()`
|
|
8. Vérifier:
|
|
- Save → publish → subscribe → read → save chain fonctionne
|
|
- Data persistence correcte
|
|
- Pas de race conditions
|
|
|
|
#### Test 3: Multi-Module State Synchronization (90s)
|
|
1. PlayerModule met à jour `data/player/gold` = 1000
|
|
2. PlayerModule publie "player:gold:updated" avec {gold: 1000}
|
|
3. EconomyModule reçoit event via IO
|
|
4. EconomyModule lit `data/player/gold` via DataNode
|
|
5. Vérifier cohérence:
|
|
- Valeur dans message IO = valeur dans DataNode
|
|
- Pas de désynchronisation
|
|
- Order des events préservé
|
|
6. Répéter 100 fois avec updates rapides
|
|
7. Vérifier consistency finale
|
|
|
|
#### Test 4: Runtime Metrics Collection (60s)
|
|
1. MetricsModule collecte metrics toutes les 100ms:
|
|
- `runtime/fps` - FPS actuel
|
|
- `runtime/memory` - Memory usage
|
|
- `runtime/message_count` - Messages IO
|
|
2. MetricsModule publie "metrics:snapshot" toutes les secondes
|
|
3. ConfigWatcherModule souscrit et log metrics
|
|
4. 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)
|
|
1. 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
|
|
2. Exécuter pendant 60 secondes
|
|
3. Vérifier:
|
|
- Aucun crash
|
|
- Aucune corruption de data
|
|
- Aucun deadlock
|
|
- Performance acceptable (< 10% overhead)
|
|
|
|
#### Test 6: Hot-Reload Module + Preserve State (90s)
|
|
1. PlayerModule a state actif:
|
|
- 50 entities dans `runtime/entities`
|
|
- Gold = 5000 dans `data/player/gold`
|
|
- Active quest dans `runtime/quest`
|
|
2. Déclencher hot-reload de PlayerModule:
|
|
- `getState()` extrait tout (data/ + runtime/)
|
|
- Recompile module
|
|
- `setState()` restaure
|
|
3. Pendant reload:
|
|
- EconomyModule continue de publier via IO
|
|
- Messages accumulés dans queue PlayerModule
|
|
4. Après reload:
|
|
- PlayerModule pull messages accumulés
|
|
- Vérifie state préservé (50 entities, 5000 gold, quest)
|
|
- Continue processing normalement
|
|
5. Vérifier:
|
|
- State complet préservé (DataNode + runtime)
|
|
- Messages pas perdus (IO queue)
|
|
- Pas de corruption
|
|
|
|
#### Test 7: Config Change Cascades (60s)
|
|
1. Modifier `config/gameplay.json` → difficulty = "hard"
|
|
2. ConfigWatcherModule détecte → publie "config:gameplay:changed"
|
|
3. PlayerModule reçoit → reload config → ajuste HP multiplier
|
|
4. PlayerModule publie "player:config:updated"
|
|
5. EconomyModule reçoit → reload config → ajuste prices
|
|
6. EconomyModule publie "economy:config:updated"
|
|
7. MetricsModule reçoit → log cascade
|
|
8. Vérifier:
|
|
- Cascade complète en < 500ms
|
|
- Tous modules synchronisés
|
|
- Ordre des events correct
|
|
|
|
#### Test 8: Large State + High-Frequency IO (60s)
|
|
1. Créer large DataNode tree (1000 nodes)
|
|
2. Publier 10k messages/s via IO
|
|
3. Modules lisent DataNode pendant IO flood
|
|
4. Mesurer:
|
|
- Latence IO: < 10ms p99
|
|
- Latence DataNode read: < 5ms p99
|
|
- Memory growth: < 20MB
|
|
- CPU usage: < 80%
|
|
5. Vérifier:
|
|
- Systèmes restent performants
|
|
- Pas de dégradation mutuelle
|
|
|
|
---
|
|
|
|
## 🏗️ Implémentation
|
|
|
|
### ConfigWatcherModule Structure
|
|
|
|
```cpp
|
|
// 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
|
|
|
|
```cpp
|
|
// 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
|
|
|
|
```cpp
|
|
// 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
|
|
1. ✅ Config hot-reload chain fonctionne (< 100ms)
|
|
2. ✅ State persistence + event publishing chain fonctionne
|
|
3. ✅ Synchronization IO/DataNode sans erreurs
|
|
4. ✅ Runtime metrics avec batching
|
|
5. ✅ Concurrent access sans crashes/corruption
|
|
6. ✅ Hot-reload préserve state complet
|
|
7. ✅ Messages IO pas perdus pendant reload
|
|
8. ✅ Config cascades propagent correctement
|
|
9. ✅ Performance acceptable sous charge
|
|
|
|
### NICE TO HAVE
|
|
1. ✅ Config reload latency < 50ms (optimal)
|
|
2. ✅ Cascade latency < 200ms (optimal)
|
|
3. ✅ IO p99 < 5ms (optimal)
|
|
4. ✅ 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.
|