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>
368 lines
12 KiB
C++
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;
|
|
}
|
|
}
|