GroveEngine/tests/modules/ConfigurableModule.cpp
StillHammer 9105610b29 feat: Add integration tests 8-10 & fix CTest configuration
Added three new integration test scenarios:
- Test 08: Config Hot-Reload (dynamic configuration updates)
- Test 09: Module Dependencies (dependency injection & cascade reload)
- Test 10: Multi-Version Coexistence (canary deployment & progressive migration)

Fixes:
- Fixed CTest working directory for all tests (add WORKING_DIRECTORY)
- Fixed module paths to use relative paths (./ prefix)
- Fixed IModule.h comments for clarity

New test modules:
- ConfigurableModule (for config reload testing)
- BaseModule, DependentModule, IndependentModule (for dependency testing)
- GameLogicModuleV1/V2/V3 (for multi-version testing)

Test coverage now includes 10 comprehensive integration scenarios covering
hot-reload, chaos testing, stress testing, race conditions, memory leaks,
error recovery, limits, config reload, dependencies, and multi-versioning.

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

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

368 lines
12 KiB
C++

#include "ConfigurableModule.h"
#include "grove/JsonDataNode.h"
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <cmath>
namespace grove {
void ConfigurableModule::setConfiguration(const IDataNode& configNode, IIO* io, ITaskScheduler* scheduler) {
// Logger
logger = spdlog::get("ConfigurableModule");
if (!logger) {
logger = spdlog::stdout_color_mt("ConfigurableModule");
}
logger->set_level(spdlog::level::debug);
// Clone config en JSON
const auto* jsonConfigNode = dynamic_cast<const JsonDataNode*>(&configNode);
if (jsonConfigNode) {
this->configNode = std::make_unique<JsonDataNode>("config", jsonConfigNode->getJsonData());
} else {
this->configNode = std::make_unique<JsonDataNode>("config");
}
// Parse et appliquer config
currentConfig = parseConfig(configNode);
previousConfig = currentConfig; // Backup initial
std::string errorMsg;
if (!currentConfig.validate(errorMsg)) {
logger->error("Invalid initial configuration: {}", errorMsg);
// Fallback to defaults
currentConfig = Config();
}
logger->info("Initializing ConfigurableModule");
logger->info(" Spawn rate: {}/s", currentConfig.spawnRate);
logger->info(" Max entities: {}", currentConfig.maxEntities);
logger->info(" Entity speed: {}", currentConfig.entitySpeed);
logger->info(" Colors: {}", currentConfig.colors.size());
frameCount = 0;
}
const IDataNode& ConfigurableModule::getConfiguration() {
return *configNode;
}
void ConfigurableModule::process(const IDataNode& input) {
float deltaTime = static_cast<float>(input.getDouble("deltaTime", 1.0 / 60.0));
frameCount++;
// Spawning logic
if (static_cast<int>(entities.size()) < currentConfig.maxEntities) {
spawnAccumulator += deltaTime * currentConfig.spawnRate;
while (spawnAccumulator >= 1.0f && static_cast<int>(entities.size()) < currentConfig.maxEntities) {
spawnEntity();
spawnAccumulator -= 1.0f;
}
}
// Update entities
for (auto& entity : entities) {
updateEntity(entity, deltaTime);
}
// Log toutes les 60 frames (1 seconde)
if (frameCount % 60 == 0) {
logger->trace("Frame {}: {} entities active (max: {})", frameCount, entities.size(), currentConfig.maxEntities);
}
}
std::unique_ptr<IDataNode> ConfigurableModule::getHealthStatus() {
nlohmann::json healthJson;
healthJson["status"] = "healthy";
healthJson["entityCount"] = entities.size();
healthJson["frameCount"] = frameCount;
healthJson["maxEntities"] = currentConfig.maxEntities;
auto health = std::make_unique<JsonDataNode>("health", healthJson);
return health;
}
void ConfigurableModule::shutdown() {
logger->info("Shutting down ConfigurableModule");
entities.clear();
}
std::string ConfigurableModule::getType() const {
return "configurable";
}
std::unique_ptr<IDataNode> ConfigurableModule::getState() {
nlohmann::json json;
json["frameCount"] = frameCount;
json["nextEntityId"] = nextEntityId;
json["spawnAccumulator"] = spawnAccumulator;
// Sérialiser config actuelle
json["config"] = configToJson(currentConfig);
// Sérialiser entities
nlohmann::json entitiesJson = nlohmann::json::array();
for (const auto& entity : entities) {
entitiesJson.push_back({
{"id", entity.id},
{"x", entity.x},
{"y", entity.y},
{"vx", entity.vx},
{"vy", entity.vy},
{"color", entity.color},
{"speed", entity.speed}
});
}
json["entities"] = entitiesJson;
return std::make_unique<JsonDataNode>("state", json);
}
void ConfigurableModule::setState(const IDataNode& state) {
const auto* jsonNode = dynamic_cast<const JsonDataNode*>(&state);
if (!jsonNode) {
logger->error("setState: Invalid state (not JsonDataNode)");
return;
}
const auto& json = jsonNode->getJsonData();
// Ensure logger is initialized
if (!logger) {
logger = spdlog::get("ConfigurableModule");
if (!logger) {
logger = spdlog::stdout_color_mt("ConfigurableModule");
}
}
// Ensure configNode is initialized
if (!configNode) {
configNode = std::make_unique<JsonDataNode>("config");
}
// Restaurer state
frameCount = json.value("frameCount", 0);
nextEntityId = json.value("nextEntityId", 0);
spawnAccumulator = json.value("spawnAccumulator", 0.0f);
// Restaurer entities
entities.clear();
if (json.contains("entities") && json["entities"].is_array()) {
for (const auto& entityJson : json["entities"]) {
Entity entity;
entity.id = entityJson.value("id", 0);
entity.x = entityJson.value("x", 0.0f);
entity.y = entityJson.value("y", 0.0f);
entity.vx = entityJson.value("vx", 0.0f);
entity.vy = entityJson.value("vy", 0.0f);
entity.color = entityJson.value("color", "red");
entity.speed = entityJson.value("speed", 5.0f);
entities.push_back(entity);
}
}
logger->info("State restored: {} entities, frame {}", entities.size(), frameCount);
}
bool ConfigurableModule::updateConfig(const IDataNode& newConfigNode) {
logger->info("Attempting config hot-reload...");
// Parse nouvelle config
Config newConfig = parseConfig(newConfigNode);
// Valider
std::string errorMsg;
if (!newConfig.validate(errorMsg)) {
logger->error("Config validation failed: {}", errorMsg);
return false;
}
// Backup current config (pour rollback potentiel)
previousConfig = currentConfig;
// Appliquer nouvelle config
currentConfig = newConfig;
// Update stored config node
const auto* jsonConfigNode = dynamic_cast<const JsonDataNode*>(&newConfigNode);
if (jsonConfigNode) {
configNode = std::make_unique<JsonDataNode>("config", jsonConfigNode->getJsonData());
}
logger->info("Config hot-reload successful!");
logger->info(" New spawn rate: {}/s", currentConfig.spawnRate);
logger->info(" New max entities: {}", currentConfig.maxEntities);
logger->info(" New entity speed: {}", currentConfig.entitySpeed);
return true;
}
bool ConfigurableModule::updateConfigPartial(const IDataNode& partialConfigNode) {
logger->info("Attempting partial config update...");
const auto* jsonNode = dynamic_cast<const JsonDataNode*>(&partialConfigNode);
if (!jsonNode) {
logger->error("Partial config update failed: not a JsonDataNode");
return false;
}
const auto& partialJson = jsonNode->getJsonData();
// Créer une nouvelle config en fusionnant avec l'actuelle
Config mergedConfig = currentConfig;
// Merge chaque champ présent dans partialJson
if (partialJson.contains("spawnRate")) {
mergedConfig.spawnRate = partialJson["spawnRate"];
}
if (partialJson.contains("maxEntities")) {
mergedConfig.maxEntities = partialJson["maxEntities"];
}
if (partialJson.contains("entitySpeed")) {
mergedConfig.entitySpeed = partialJson["entitySpeed"];
}
if (partialJson.contains("colors")) {
mergedConfig.colors.clear();
for (const auto& color : partialJson["colors"]) {
mergedConfig.colors.push_back(color);
}
}
if (partialJson.contains("physics")) {
if (partialJson["physics"].contains("gravity")) {
mergedConfig.physics.gravity = partialJson["physics"]["gravity"];
}
if (partialJson["physics"].contains("friction")) {
mergedConfig.physics.friction = partialJson["physics"]["friction"];
}
}
// Valider config fusionnée
std::string errorMsg;
if (!mergedConfig.validate(errorMsg)) {
logger->error("Partial config validation failed: {}", errorMsg);
return false;
}
// Appliquer
previousConfig = currentConfig;
currentConfig = mergedConfig;
// Update stored config node with merged config
nlohmann::json mergedJson = configToJson(currentConfig);
configNode = std::make_unique<JsonDataNode>("config", mergedJson);
logger->info("Partial config update successful!");
return true;
}
ConfigurableModule::Config ConfigurableModule::parseConfig(const IDataNode& configNode) {
Config cfg;
// Cast to JsonDataNode to access JSON
const auto* jsonNode = dynamic_cast<const JsonDataNode*>(&configNode);
if (!jsonNode) {
logger->warn("Config not a JsonDataNode, using defaults");
return cfg;
}
const auto& json = jsonNode->getJsonData();
cfg.spawnRate = json.value("spawnRate", 10);
cfg.maxEntities = json.value("maxEntities", 50);
cfg.entitySpeed = json.value("entitySpeed", 5.0f);
// Parse colors
cfg.colors.clear();
if (json.contains("colors") && json["colors"].is_array()) {
for (const auto& colorJson : json["colors"]) {
cfg.colors.push_back(colorJson.get<std::string>());
}
}
if (cfg.colors.empty()) {
cfg.colors = {"red", "blue"}; // Default
}
// Parse physics
if (json.contains("physics") && json["physics"].is_object()) {
cfg.physics.gravity = json["physics"].value("gravity", 9.8f);
cfg.physics.friction = json["physics"].value("friction", 0.5f);
}
return cfg;
}
nlohmann::json ConfigurableModule::configToJson(const Config& cfg) const {
nlohmann::json json;
json["spawnRate"] = cfg.spawnRate;
json["maxEntities"] = cfg.maxEntities;
json["entitySpeed"] = cfg.entitySpeed;
json["colors"] = cfg.colors;
json["physics"]["gravity"] = cfg.physics.gravity;
json["physics"]["friction"] = cfg.physics.friction;
return json;
}
void ConfigurableModule::spawnEntity() {
Entity entity;
entity.id = nextEntityId++;
// Position aléatoire
std::uniform_real_distribution<float> posDist(0.0f, 100.0f);
entity.x = posDist(rng);
entity.y = posDist(rng);
// Vélocité aléatoire basée sur speed
std::uniform_real_distribution<float> angleDist(0.0f, 2.0f * 3.14159f);
float angle = angleDist(rng);
entity.vx = std::cos(angle) * currentConfig.entitySpeed;
entity.vy = std::sin(angle) * currentConfig.entitySpeed;
// Couleur aléatoire depuis la palette actuelle
std::uniform_int_distribution<size_t> colorDist(0, currentConfig.colors.size() - 1);
entity.color = currentConfig.colors[colorDist(rng)];
// Snapshot de la config speed au moment de la création
entity.speed = currentConfig.entitySpeed;
entities.push_back(entity);
logger->trace("Spawned entity #{} at ({:.1f}, {:.1f}) color={} speed={}",
entity.id, entity.x, entity.y, entity.color, entity.speed);
}
void ConfigurableModule::updateEntity(Entity& entity, float dt) {
// Update position
entity.x += entity.vx * dt;
entity.y += entity.vy * dt;
// Bounce sur les bords (map 100x100)
if (entity.x < 0.0f || entity.x > 100.0f) {
entity.vx = -entity.vx;
entity.x = std::clamp(entity.x, 0.0f, 100.0f);
}
if (entity.y < 0.0f || entity.y > 100.0f) {
entity.vy = -entity.vy;
entity.y = std::clamp(entity.y, 0.0f, 100.0f);
}
// Appliquer gravité et friction de la config actuelle
entity.vy += currentConfig.physics.gravity * dt;
entity.vx *= (1.0f - currentConfig.physics.friction * dt);
entity.vy *= (1.0f - currentConfig.physics.friction * dt);
}
} // namespace grove
// Export symbols
extern "C" {
grove::IModule* createModule() {
return new grove::ConfigurableModule();
}
void destroyModule(grove::IModule* module) {
delete module;
}
}