GroveEngine/tests/integration/test_13_cross_system.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

375 lines
14 KiB
C++

/**
* Scenario 13: Cross-System Integration (IO + DataNode)
*
* Tests integration between IntraIO pub/sub system and IDataTree/IDataNode system.
* Validates that modules can communicate via IO while sharing data via DataNode.
*/
#include "grove/JsonDataNode.h"
#include "grove/JsonDataTree.h"
#include "grove/IOFactory.h"
#include "../helpers/TestMetrics.h"
#include "../helpers/TestAssertions.h"
#include "../helpers/TestReporter.h"
#include <iostream>
#include <fstream>
#include <filesystem>
#include <thread>
#include <chrono>
#include <atomic>
using namespace grove;
int main() {
TestReporter reporter("Cross-System Integration Test");
TestMetrics metrics;
std::cout << "================================================================================\n";
std::cout << "TEST: Cross-System Integration (IO + DataNode)\n";
std::cout << "================================================================================\n\n";
// === SETUP ===
std::cout << "Setup: Creating test directories...\n";
std::filesystem::create_directories("test_cross/config");
std::filesystem::create_directories("test_cross/data");
auto tree = std::make_unique<JsonDataTree>("test_cross");
// Create IO instances
auto configWatcherIO = IOFactory::create("intra", "ConfigWatcher");
auto playerIO = IOFactory::create("intra", "Player");
auto economyIO = IOFactory::create("intra", "Economy");
auto metricsIO = IOFactory::create("intra", "Metrics");
if (!configWatcherIO || !playerIO || !economyIO || !metricsIO) {
std::cerr << "❌ Failed to create IO instances\n";
return 1;
}
// ========================================================================
// TEST 1: Config Hot-Reload → IO Broadcast
// ========================================================================
std::cout << "\n=== TEST 1: Config Hot-Reload → IO Broadcast ===\n";
// Create initial config file
nlohmann::json gameplayConfig = {
{"difficulty", "normal"},
{"hpMultiplier", 1.0}
};
std::ofstream configFile("test_cross/config/gameplay.json");
configFile << gameplayConfig.dump(2);
configFile.close();
// Load config
tree->loadConfigFile("gameplay.json");
// Player subscribes to config changes
playerIO->subscribe("config:gameplay:changed");
// Setup reload callback for ConfigWatcher
std::atomic<int> configChangedEvents{0};
tree->onTreeReloaded([&]() {
std::cout << " → Config reloaded, publishing event...\n";
auto data = std::make_unique<JsonDataNode>("configChange", nlohmann::json{
{"config", "gameplay"},
{"timestamp", 12345}
});
configWatcherIO->publish("config:gameplay:changed", std::move(data));
});
// Modify config file
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
if (tree->reloadIfChanged()) {
std::cout << " Config was reloaded\n";
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
// Check if player received message
if (playerIO->hasMessages() > 0) {
auto msg = playerIO->pullMessage();
configChangedEvents++;
// Read new config from tree
auto configRoot = tree->getConfigRoot();
auto gameplay = configRoot->getChild("gameplay");
if (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_TRUE(std::abs(hpMult - 1.5) < 0.001, "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, 200.0f, "Total latency should be reasonable");
ASSERT_EQ(configChangedEvents.load(), 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();
// Create player node
auto player = std::make_unique<JsonDataNode>("player", nlohmann::json::object());
auto profile = std::make_unique<JsonDataNode>("profile", nlohmann::json{
{"name", "TestPlayer"},
{"level", 5},
{"gold", 1000}
});
player->setChild("profile", std::move(profile));
dataRoot->setChild("player", std::move(player));
// Save to disk
bool saved = tree->saveData();
ASSERT_TRUE(saved, "Should save data successfully");
std::cout << " Data saved to disk\n";
// Publish level up event
auto levelUpData = std::make_unique<JsonDataNode>("levelUp", nlohmann::json{
{"event", "level_up"},
{"newLevel", 6},
{"goldBonus", 500}
});
playerIO->publish("player:level_up", std::move(levelUpData));
// Economy subscribes to player events
economyIO->subscribe("player:*");
std::this_thread::sleep_for(std::chrono::milliseconds(10));
// Economy processes message
int messagesReceived = 0;
while (economyIO->hasMessages() > 0) {
auto msg = economyIO->pullMessage();
messagesReceived++;
std::cout << " EconomyModule received: " << msg.topic << "\n";
// Read player data from tree
auto playerData = tree->getDataRoot()->getChild("player");
if (playerData) {
auto profileData = playerData->getChild("profile");
if (profileData) {
int gold = profileData->getInt("gold");
std::cout << " Player gold: " << gold << "\n";
ASSERT_EQ(gold, 1000, "Gold should match saved value");
}
}
}
ASSERT_EQ(messagesReceived, 1, "Should receive 1 player event");
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 < 10; i++) {
// Update gold in DataNode
int goldValue = 1000 + i * 10;
auto playerNode = tree->getDataRoot()->getChild("player");
if (playerNode) {
auto profileNode = playerNode->getChild("profile");
if (profileNode) {
profileNode->setInt("gold", goldValue);
// Save back to tree
playerNode->setChild("profile", std::move(profileNode));
tree->getDataRoot()->setChild("player", std::move(playerNode));
}
}
// Publish event with same value
auto goldUpdate = std::make_unique<JsonDataNode>("goldUpdate", nlohmann::json{
{"event", "gold_updated"},
{"gold", goldValue}
});
playerIO->publish("player:gold:updated", std::move(goldUpdate));
std::this_thread::sleep_for(std::chrono::milliseconds(5));
// Economy verifies synchronization
if (economyIO->hasMessages() > 0) {
auto msg = economyIO->pullMessage();
int msgGold = msg.data->getInt("gold");
// Read from DataNode
auto playerCheck = tree->getDataRoot()->getChild("player");
if (playerCheck) {
auto profileCheck = playerCheck->getChild("profile");
if (profileCheck) {
int dataGold = profileCheck->getInt("gold");
if (msgGold != dataGold) {
std::cerr << " SYNC ERROR: msg=" << msgGold << " data=" << dataGold << "\n";
syncErrors++;
}
}
}
}
}
std::cout << "Synchronization errors: " << syncErrors << " / 10\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();
// Subscribe to metrics with low-frequency
SubscriptionConfig metricsConfig;
metricsConfig.replaceable = true;
metricsConfig.batchInterval = 1000; // 1 second
playerIO->subscribeLowFreq("metrics:*", metricsConfig);
// Publish 20 metrics over 2 seconds
for (int i = 0; i < 20; i++) {
auto metricsData = std::make_unique<JsonDataNode>("metrics", nlohmann::json{
{"fps", 60.0},
{"memory", 125000000 + i * 1000},
{"messageCount", i}
});
metricsIO->publish("metrics:snapshot", std::move(metricsData));
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
// Check batched messages
std::this_thread::sleep_for(std::chrono::milliseconds(200));
int snapshotsReceived = 0;
while (playerIO->hasMessages() > 0) {
playerIO->pullMessage();
snapshotsReceived++;
}
std::cout << "Snapshots received: " << snapshotsReceived << " (expected ~2 due to batching)\n";
ASSERT_TRUE(snapshotsReceived >= 1 && snapshotsReceived <= 4,
"Should receive batched snapshots");
// Verify runtime not persisted
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> errors{0};
// Thread 1: Publish events
std::thread pubThread([&]() {
while (running) {
try {
auto data = std::make_unique<JsonDataNode>("data", nlohmann::json{{"id", publishCount++}});
playerIO->publish("concurrent:test", std::move(data));
std::this_thread::sleep_for(std::chrono::milliseconds(10));
} catch (...) {
errors++;
}
}
});
// Thread 2: Read DataNode
std::thread readThread([&]() {
while (running) {
try {
auto playerData = tree->getDataRoot()->getChild("player");
if (playerData) {
auto profileData = playerData->getChild("profile");
if (profileData) {
int gold = profileData->getInt("gold", 0);
readCount++;
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(20));
} catch (...) {
errors++;
}
}
});
// Run for 2 seconds
std::this_thread::sleep_for(std::chrono::seconds(2));
running = false;
pubThread.join();
readThread.join();
std::cout << "Concurrent test completed:\n";
std::cout << " Publishes: " << publishCount << "\n";
std::cout << " Reads: " << readCount << "\n";
std::cout << " Errors: " << errors << "\n";
ASSERT_EQ(errors.load(), 0, "Should have zero errors during concurrent access");
ASSERT_GT(publishCount.load(), 0, "Should have published messages");
ASSERT_GT(readCount.load(), 0, "Should have read data");
reporter.addMetric("concurrent_publishes", publishCount);
reporter.addMetric("concurrent_reads", readCount);
reporter.addMetric("concurrent_errors", errors);
reporter.addAssertion("concurrent_access", errors == 0);
std::cout << "✓ TEST 5 PASSED\n";
// ========================================================================
// CLEANUP
// ========================================================================
std::filesystem::remove_all("test_cross");
// ========================================================================
// RAPPORT FINAL
// ========================================================================
metrics.printReport();
reporter.printFinalReport();
return reporter.getExitCode();
}