GroveEngine/planTI/scenario_08_config_hotreload.md
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

562 lines
18 KiB
Markdown

# Scénario 8: Config Hot-Reload
**Priorité**: ⭐ NICE TO HAVE
**Phase**: 3 (NICE TO HAVE)
**Durée estimée**: ~1 minute
**Effort implémentation**: ~3-4 heures
---
## 🎯 Objectif
Valider que le système peut modifier la configuration d'un module à la volée sans redémarrage, avec:
- Changement de paramètres en temps réel
- Reconfiguration sans perte de state
- Support de multiples types de configuration (scalaires, listes, objets)
- Validation des valeurs et rollback en cas d'erreur
---
## 📋 Description
### Setup Initial
1. Charger `ConfigurableModule` avec configuration de base:
- `spawnRate`: 10 entités/seconde
- `maxEntities`: 50
- `entitySpeed`: 5.0
- `colors`: ["red", "blue"]
- `physics.gravity`: 9.8
- `physics.friction`: 0.5
2. Spawner des entités pendant 10 secondes
3. Vérifier que les entités respectent la config initiale
### Phase 1: Config Hot-Reload Simple
1. À t=10s, modifier configuration:
- `spawnRate`: 20 (doubler le taux de spawn)
- `entitySpeed`: 10.0 (doubler la vitesse)
2. Appliquer config via `updateConfig()` sans reload du module
3. Vérifier que:
- Nouvelles entités utilisent nouvelle config
- Entités existantes **gardent** leur config d'origine (continuité)
- Pas de perte de state
### Phase 2: Config Hot-Reload Complexe
1. À t=20s, modifier configuration avancée:
- `maxEntities`: 100 (augmenter limite)
- `colors`: ["green", "yellow", "purple"] (nouvelles couleurs)
- `physics.gravity`: 1.6 (gravité lunaire)
2. Vérifier que:
- Nouvelles entités spawnent avec nouvelles couleurs
- Physique appliquée correctement
- Limite respectée
### Phase 3: Config Invalide + Rollback
1. À t=30s, tenter d'appliquer config invalide:
- `spawnRate`: -5 (négatif = invalide)
- `maxEntities`: 1000000 (trop grand)
2. Vérifier que:
- Config invalide rejetée
- Config précédente (valide) restaurée
- Aucune corruption de state
- Erreur loggée clairement
### Phase 4: Config Partielle
1. À t=40s, modifier seulement une partie de la config:
- `entitySpeed`: 2.0
- (laisser tous les autres paramètres inchangés)
2. Vérifier que:
- Seul `entitySpeed` est modifié
- Autres paramètres préservés
- Merge correct
---
## 🏗️ Implémentation
### ConfigurableModule Structure
```cpp
// ConfigurableModule.h
class ConfigurableModule : public IModule {
public:
struct Entity {
float x, y;
float vx, vy;
std::string color;
float speed; // Config snapshot à la création
int id;
};
struct Config {
int spawnRate; // Entités par seconde
int maxEntities; // Limite totale
float entitySpeed; // Vitesse de déplacement
std::vector<std::string> colors; // Couleurs disponibles
struct Physics {
float gravity;
float friction;
} physics;
// Validation
bool validate(std::string& errorMsg) const {
if (spawnRate < 0 || spawnRate > 1000) {
errorMsg = "spawnRate must be in [0, 1000]";
return false;
}
if (maxEntities < 1 || maxEntities > 10000) {
errorMsg = "maxEntities must be in [1, 10000]";
return false;
}
if (entitySpeed < 0.0f) {
errorMsg = "entitySpeed must be >= 0";
return false;
}
if (colors.empty()) {
errorMsg = "colors list cannot be empty";
return false;
}
return true;
}
};
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;
// NOUVELLE API: Config hot-reload
bool updateConfig(std::shared_ptr<IDataNode> newConfig);
bool isIdle() const override { return true; }
private:
Config currentConfig;
Config previousConfig; // Pour rollback
std::vector<Entity> entities;
float spawnAccumulator = 0.0f;
int nextEntityId = 0;
void spawnEntity();
void updateEntity(Entity& entity, float dt);
Config parseConfig(std::shared_ptr<IDataNode> configNode);
};
```
### Configuration Format (JSON)
```json
{
"spawnRate": 10,
"maxEntities": 50,
"entitySpeed": 5.0,
"colors": ["red", "blue"],
"physics": {
"gravity": 9.8,
"friction": 0.5
}
}
```
### Test Principal
```cpp
// test_08_config_hotreload.cpp
#include "helpers/TestMetrics.h"
#include "helpers/TestAssertions.h"
#include "helpers/TestReporter.h"
int main() {
TestReporter reporter("Config Hot-Reload");
TestMetrics metrics;
// === SETUP ===
DebugEngine engine;
engine.loadModule("ConfigurableModule", "build/modules/libConfigurableModule.so");
// Config initiale
auto config = createJsonConfig({
{"spawnRate", 10},
{"maxEntities", 50},
{"entitySpeed", 5.0},
{"colors", json::array({"red", "blue"})},
{"physics", {
{"gravity", 9.8},
{"friction", 0.5}
}}
});
engine.initializeModule("ConfigurableModule", config);
std::cout << "=== Phase 0: Initial config (10s) ===\n";
// === PHASE 0: Baseline (10s) ===
for (int i = 0; i < 600; i++) { // 10s * 60 FPS
engine.update(1.0f/60.0f);
metrics.recordFPS(60.0f);
}
auto state0 = engine.getModuleState("ConfigurableModule");
auto json0 = getJsonFromState(state0);
int entityCount0 = json0["entities"].size();
// On devrait avoir ~100 entités (10/s * 10s)
ASSERT_WITHIN(entityCount0, 100, 10, "Should have ~100 entities after 10s");
reporter.addAssertion("initial_spawn_rate", true);
// Vérifier vitesse des entités
for (const auto& entity : json0["entities"]) {
float speed = entity["speed"];
ASSERT_EQ(speed, 5.0f, "Initial entity speed should be 5.0");
}
std::cout << "✓ Baseline: " << entityCount0 << " entities spawned\n";
// === PHASE 1: Simple Config Change (10s) ===
std::cout << "\n=== Phase 1: Doubling spawn rate and speed (10s) ===\n";
auto newConfig1 = createJsonConfig({
{"spawnRate", 20}, // Double spawn rate
{"maxEntities", 50},
{"entitySpeed", 10.0}, // Double speed
{"colors", json::array({"red", "blue"})},
{"physics", {
{"gravity", 9.8},
{"friction", 0.5}
}}
});
bool updateResult1 = engine.updateModuleConfig("ConfigurableModule", newConfig1);
ASSERT_TRUE(updateResult1, "Config update should succeed");
reporter.addAssertion("config_update_simple", updateResult1);
for (int i = 0; i < 600; i++) { // 10s * 60 FPS
engine.update(1.0f/60.0f);
}
auto state1 = engine.getModuleState("ConfigurableModule");
auto json1 = getJsonFromState(state1);
int entityCount1 = json1["entities"].size();
// On devrait maintenant avoir ~100 (initial) + ~200 (20/s * 10s) = ~300 entités
// Mais max = 50, donc on devrait avoir exactement 50
ASSERT_EQ(entityCount1, 50, "Should respect maxEntities limit");
reporter.addAssertion("max_entities_respected", entityCount1 == 50);
// Vérifier que les NOUVELLES entités ont speed = 10.0
int newEntityCount = 0;
for (const auto& entity : json1["entities"]) {
if (entity["id"] >= entityCount0) { // Nouvelle entité
float speed = entity["speed"];
ASSERT_EQ(speed, 10.0f, "New entities should have speed 10.0");
newEntityCount++;
}
}
std::cout << "✓ Config applied: " << newEntityCount << " new entities with speed 10.0\n";
// === PHASE 2: Complex Config Change (10s) ===
std::cout << "\n=== Phase 2: Complex config changes (10s) ===\n";
auto newConfig2 = createJsonConfig({
{"spawnRate", 15},
{"maxEntities", 100}, // Augmenter limite
{"entitySpeed", 7.5},
{"colors", json::array({"green", "yellow", "purple"})}, // Nouvelles couleurs
{"physics", {
{"gravity", 1.6}, // Gravité lunaire
{"friction", 0.2}
}}
});
bool updateResult2 = engine.updateModuleConfig("ConfigurableModule", newConfig2);
ASSERT_TRUE(updateResult2, "Config update 2 should succeed");
int entitiesBeforePhase2 = entityCount1;
for (int i = 0; i < 600; i++) { // 10s * 60 FPS
engine.update(1.0f/60.0f);
}
auto state2 = engine.getModuleState("ConfigurableModule");
auto json2 = getJsonFromState(state2);
int entityCount2 = json2["entities"].size();
// Vérifier que la limite a augmenté
ASSERT_LT(entitiesBeforePhase2, entityCount2, "Entity count should have increased");
ASSERT_LE(entityCount2, 100, "Should respect new maxEntities = 100");
// Vérifier couleurs des nouvelles entités
std::set<std::string> newColors;
for (const auto& entity : json2["entities"]) {
if (entity["id"] >= entitiesBeforePhase2) {
newColors.insert(entity["color"]);
}
}
bool hasNewColors = newColors.count("green") || newColors.count("yellow") || newColors.count("purple");
ASSERT_TRUE(hasNewColors, "New entities should use new color palette");
reporter.addAssertion("new_colors_applied", hasNewColors);
std::cout << "✓ Complex config applied: " << entityCount2 << " total entities, new colors: ";
for (const auto& color : newColors) std::cout << color << " ";
std::cout << "\n";
// === PHASE 3: Invalid Config + Rollback (5s) ===
std::cout << "\n=== Phase 3: Invalid config rejection (5s) ===\n";
auto invalidConfig = createJsonConfig({
{"spawnRate", -5}, // INVALIDE: négatif
{"maxEntities", 1000000}, // INVALIDE: trop grand
{"entitySpeed", 5.0},
{"colors", json::array({"red"})},
{"physics", {
{"gravity", 9.8},
{"friction", 0.5}
}}
});
bool updateResult3 = engine.updateModuleConfig("ConfigurableModule", invalidConfig);
ASSERT_FALSE(updateResult3, "Invalid config should be rejected");
reporter.addAssertion("invalid_config_rejected", !updateResult3);
// Continuer l'exécution - devrait utiliser la config précédente (valide)
for (int i = 0; i < 300; i++) { // 5s * 60 FPS
engine.update(1.0f/60.0f);
}
auto state3 = engine.getModuleState("ConfigurableModule");
auto json3 = getJsonFromState(state3);
// Vérifier que la config précédente (Phase 2) est toujours active
// On devrait avoir spawn à 15/s, pas -5 ni 0
int entityCount3 = json3["entities"].size();
ASSERT_GT(entityCount3, entityCount2, "Should continue spawning with previous valid config");
reporter.addAssertion("config_rollback_works", entityCount3 > entityCount2);
std::cout << "✓ Rollback successful: config unchanged, " << (entityCount3 - entityCount2) << " entities spawned\n";
// === PHASE 4: Partial Config Update (5s) ===
std::cout << "\n=== Phase 4: Partial config update (5s) ===\n";
auto partialConfig = createJsonConfig({
{"entitySpeed", 2.0} // Modifier seulement la vitesse
});
bool updateResult4 = engine.updateModuleConfigPartial("ConfigurableModule", partialConfig);
ASSERT_TRUE(updateResult4, "Partial config update should succeed");
for (int i = 0; i < 300; i++) { // 5s * 60 FPS
engine.update(1.0f/60.0f);
}
auto state4 = engine.getModuleState("ConfigurableModule");
auto json4 = getJsonFromState(state4);
// Vérifier que les nouvelles entités ont speed = 2.0
// Et que les autres paramètres (colors, etc.) sont inchangés de Phase 2
bool foundNewSpeed = false;
bool foundOldColors = false;
for (const auto& entity : json4["entities"]) {
if (entity["id"] >= entityCount3) {
if (entity["speed"] == 2.0f) foundNewSpeed = true;
std::string color = entity["color"];
if (color == "green" || color == "yellow" || color == "purple") {
foundOldColors = true;
}
}
}
ASSERT_TRUE(foundNewSpeed, "New entities should have updated speed");
ASSERT_TRUE(foundOldColors, "Colors should be preserved from Phase 2");
reporter.addAssertion("partial_update_works", foundNewSpeed && foundOldColors);
std::cout << "✓ Partial update: speed changed, other params preserved\n";
// === VÉRIFICATIONS FINALES ===
// Memory stability
size_t memGrowth = metrics.getMemoryGrowth();
ASSERT_LT(memGrowth, 10 * 1024 * 1024, "Memory growth should be < 10MB");
reporter.addMetric("memory_growth_mb", memGrowth / (1024.0f * 1024.0f));
// No crashes
reporter.addAssertion("no_crashes", true);
// === RAPPORT FINAL ===
metrics.printReport();
reporter.printFinalReport();
return reporter.getExitCode();
}
```
---
## 📊 Métriques Collectées
| Métrique | Description | Seuil |
|----------|-------------|-------|
| **config_update_time_ms** | Temps d'application de nouvelle config | < 50ms |
| **memory_growth_mb** | Croissance mémoire totale | < 10MB |
| **config_rollback_time_ms** | Temps de rollback si invalide | < 10ms |
| **partial_update_accuracy** | Précision du merge partiel | 100% |
---
## ✅ Critères de Succès
### MUST PASS
1. Config update appliquée en < 50ms
2. Nouvelles entités utilisent nouvelle config
3. Entités existantes préservent leur config (continuité)
4. Config invalide rejetée + rollback automatique
5. Partial update fusionne correctement
6. Aucun crash
### NICE TO HAVE
1. Config update en < 10ms (très rapide)
2. Validation détaillée avec messages d'erreur clairs
3. Support de nested objects (physics.gravity)
4. Historique des configs (undo/redo)
---
## 🔧 API Nécessaires dans IModule
### Nouvelle méthode à ajouter
```cpp
class IModule {
public:
// Méthodes existantes
virtual void initialize(std::shared_ptr<IDataNode> config) = 0;
virtual void process(float deltaTime) = 0;
virtual std::shared_ptr<IDataNode> getState() const = 0;
virtual void setState(std::shared_ptr<IDataNode> state) = 0;
virtual bool isIdle() const = 0;
// NOUVELLE: Config hot-reload
virtual bool updateConfig(std::shared_ptr<IDataNode> newConfig) {
// Default implementation: reject
return false;
}
// NOUVELLE: Partial config update
virtual bool updateConfigPartial(std::shared_ptr<IDataNode> partialConfig) {
// Default implementation: delegate to full update
return updateConfig(partialConfig);
}
};
```
### DebugEngine Support
```cpp
class DebugEngine {
public:
// Nouvelle méthode
bool updateModuleConfig(const std::string& moduleName, std::shared_ptr<IDataNode> newConfig) {
auto it = modules.find(moduleName);
if (it == modules.end()) return false;
return it->second->updateConfig(newConfig);
}
bool updateModuleConfigPartial(const std::string& moduleName, std::shared_ptr<IDataNode> partialConfig) {
auto it = modules.find(moduleName);
if (it == modules.end()) return false;
return it->second->updateConfigPartial(partialConfig);
}
};
```
---
## 🐛 Cas d'Erreur Attendus
| Erreur | Cause | Action |
|--------|-------|--------|
| Config rejetée alors que valide | Validation trop stricte | FAIL - ajuster règles |
| Config invalide acceptée | Pas de validation | FAIL - implémenter validate() |
| State corrompu après config change | Mauvaise gestion | FAIL - séparer config et state |
| Rollback échoue | Pas de backup | FAIL - sauvegarder prev config |
| Partial update écrase tout | Merge incorrect | FAIL - implémenter merge proper |
---
## 📝 Output Attendu
```
================================================================================
TEST: Config Hot-Reload
================================================================================
=== Phase 0: Initial config (10s) ===
✓ Baseline: 102 entities spawned
=== Phase 1: Doubling spawn rate and speed (10s) ===
✓ Config applied: 47 new entities with speed 10.0
=== Phase 2: Complex config changes (10s) ===
✓ Complex config applied: 98 total entities, new colors: green purple yellow
=== Phase 3: Invalid config rejection (5s) ===
[ERROR] Config validation failed: spawnRate must be in [0, 1000]
[ERROR] Config validation failed: maxEntities must be in [1, 10000]
✓ Rollback successful: config unchanged, 73 entities spawned
=== Phase 4: Partial config update (5s) ===
✓ Partial update: speed changed, other params preserved
================================================================================
METRICS
================================================================================
Config update time: 8ms (threshold: < 50ms) ✓
Memory growth: 4.2MB (threshold: < 10MB) ✓
================================================================================
ASSERTIONS
================================================================================
✓ initial_spawn_rate
✓ config_update_simple
✓ max_entities_respected
✓ new_colors_applied
✓ invalid_config_rejected
✓ config_rollback_works
✓ partial_update_works
✓ no_crashes
Result: ✅ PASSED
================================================================================
```
---
## 📅 Planning
**Jour 8 (3h):**
- Implémenter `updateConfig()` et `updateConfigPartial()` dans IModule
- Implémenter ConfigurableModule avec validation
**Jour 9 (1h):**
- Implémenter test_08_config_hotreload.cpp
- Debug + validation
---
## 🎯 Valeur Ajoutée
Ce test valide une fonctionnalité **cruciale pour le développement** :
- **Tweaking en temps réel** : Ajuster paramètres de gameplay sans redémarrer
- **A/B testing** : Tester différentes configs rapidement
- **Debug facilité** : Modifier valeurs pour reproduire bugs
- **Production-ready** : Hot-config peut servir en prod (feature flags, tuning)
Contrairement au hot-reload de code (recompilation), le hot-reload de config est **instantané** et ne nécessite pas de rebuild.
---
**Prochaine étape**: Implémentation