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

18 KiB

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

// 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)

{
    "spawnRate": 10,
    "maxEntities": 50,
    "entitySpeed": 5.0,
    "colors": ["red", "blue"],
    "physics": {
        "gravity": 9.8,
        "friction": 0.5
    }
}

Test Principal

// 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

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

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