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

17 KiB

Scénario 9: Module Dependencies

Priorité: ⭐ CRITICAL Phase: 2 (CRITICAL) Durée estimée: ~2 minutes Effort implémentation: ~4-5 heures


🎯 Objectif

Valider que le systĂšme peut gĂ©rer des modules avec dĂ©pendances explicites, oĂč un module A dĂ©pend d'un module B, avec:

  • Cascade reload automatique (reload de B → force reload de A)
  • Ordre de reload correct (B avant A)
  • Protection contre dĂ©chargement si dĂ©pendance active
  • DĂ©tection de cycles de dĂ©pendances
  • PrĂ©servation des donnĂ©es partagĂ©es entre modules

📋 Description

Architecture des Modules de Test

BaseModule: Module de base sans dépendances

  • Expose des services/donnĂ©es (ex: gĂ©nĂšre des nombres alĂ©atoires)
  • Trackable pour valider les reloads
  • Version incrĂ©mentale

DependentModule: Module qui dépend de BaseModule

  • DĂ©clare explicitement BaseModule comme dĂ©pendance
  • Utilise les services de BaseModule
  • Doit ĂȘtre rechargĂ© si BaseModule est rechargĂ©

IndependentModule: Module sans dépendances

  • Sert de tĂ©moin (ne devrait jamais ĂȘtre affectĂ©)
  • Permet de valider que seules les bonnes dĂ©pendances sont reload

Setup Initial

  1. Charger BaseModule (version 1)
    • Expose service: generateNumber() → retourne 42
    • Pas de dĂ©pendances
  2. Charger DependentModule (version 1)
    • DĂ©pend de: ["BaseModule"]
    • Utilise BaseModule.generateNumber()
    • Accumule les rĂ©sultats
  3. Charger IndependentModule (version 1)
    • Pas de dĂ©pendances
    • TĂ©moin indĂ©pendant
  4. Vérifier que tous les modules sont chargés et fonctionnent

Phase 1: Cascade Reload (BaseModule → DependentModule)

Durée: 30 secondes

  1. À t=10s, modifier et reloader BaseModule (version 2)
    • generateNumber() → retourne maintenant 100
  2. Vérifier cascade reload:
    • ✅ BaseModule rechargĂ© automatiquement
    • ✅ DependentModule forcĂ© Ă  reloader (cascade)
    • ✅ IndependentModule non rechargĂ© (isolĂ©)
  3. Vérifier ordre de reload:
    • BaseModule reload avant DependentModule
    • État cohĂ©rent (pas de rĂ©fĂ©rences cassĂ©es)
  4. Vérifier continuité:
    • State de DependentModule prĂ©servĂ©
    • Nouvelles valeurs de BaseModule utilisĂ©es (100 au lieu de 42)

Métriques:

  • Cascade reload time: temps total pour recharger B + A
  • Dependency resolution time: temps pour identifier dĂ©pendants

Phase 2: Protection contre Déchargement

Durée: 10 secondes

  1. À t=40s, tenter de dĂ©charger BaseModule
  2. Vérifier rejet:
    • ❌ DĂ©chargement refusĂ© (DependentModule en dĂ©pend)
    • ⚠ Erreur claire: "Cannot unload BaseModule: required by DependentModule"
    • ✅ BaseModule reste chargĂ© et fonctionnel
  3. Vérifier stabilité:
    • Tous les modules continuent de fonctionner
    • Aucune corruption de state

Phase 3: Reload Module Dépendant (sans cascade inverse)

Durée: 20 secondes

  1. À t=50s, modifier et reloader DependentModule (version 2)
    • Change comportement interne
    • Garde mĂȘme dĂ©pendance sur BaseModule
  2. Vérifier isolation:
    • ✅ DependentModule rechargĂ©
    • ✅ BaseModule non rechargĂ© (pas de cascade inverse)
    • ✅ IndependentModule toujours isolĂ©
  3. Vérifier connexion:
    • DependentModule peut toujours utiliser BaseModule
    • Pas de rĂ©fĂ©rences cassĂ©es

Phase 4: Détection de Cycle de Dépendances

Durée: 20 secondes

  1. À t=70s, crĂ©er module CyclicModule avec dĂ©pendance circulaire:
    • CyclicModuleA dĂ©pend de CyclicModuleB
    • CyclicModuleB dĂ©pend de CyclicModuleA
  2. Tenter de charger les deux modules
  3. Vérifier détection:
    • ❌ Chargement refusĂ©
    • ⚠ Erreur claire: "Cyclic dependency detected: A → B → A"
    • ✅ Aucun module partiellement chargĂ© (transactional)
  4. Vérifier stabilité:
    • Modules existants non affectĂ©s
    • SystĂšme reste stable

Phase 5: Déchargement en Cascade

Durée: 20 secondes

  1. À t=90s, dĂ©charger DependentModule (libĂšre dĂ©pendance)
  2. Vérifier libération:
    • ✅ DependentModule dĂ©chargĂ©
    • ✅ BaseModule toujours chargĂ© (encore utilisable)
  3. À t=100s, dĂ©charger BaseModule (maintenant possible)
  4. Vérifier succÚs:
    • ✅ BaseModule dĂ©chargĂ© (plus de dĂ©pendants)
    • ✅ IndependentModule toujours actif (isolĂ©)

đŸ—ïž ImplĂ©mentation

Extension de IModule.h

// include/grove/IModule.h
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 (Scenario 8)
    virtual bool updateConfig(std::shared_ptr<IDataNode> newConfig) {
        return false;
    }

    // NOUVELLE: Dépendances (Scenario 9)
    virtual std::vector<std::string> getDependencies() const {
        return {};  // Par défaut: aucune dépendance
    }

    // NOUVELLE: Version du module (pour tracking)
    virtual int getVersion() const {
        return 1;  // Par défaut: version 1
    }

    virtual ~IModule() = default;
};

BaseModule Structure

// tests/modules/BaseModule.h
#pragma once
#include "grove/IModule.h"
#include <memory>
#include <atomic>

class BaseModule : public IModule {
public:
    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;
    bool isIdle() const override { return true; }

    std::vector<std::string> getDependencies() const override {
        return {};  // Pas de dépendances
    }

    int getVersion() const override { return version_; }

    // Service exposé aux autres modules
    int generateNumber() const;

private:
    int version_ = 1;
    std::atomic<int> processCount_{0};
    int generatedValue_ = 42;  // V1: 42, V2: 100
};

// Module factory
extern "C" IModule* createModule() {
    return new BaseModule();
}

extern "C" void destroyModule(IModule* module) {
    delete module;
}

DependentModule Structure

// tests/modules/DependentModule.h
#pragma once
#include "grove/IModule.h"
#include "BaseModule.h"
#include <memory>
#include <vector>

class DependentModule : public IModule {
public:
    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;
    bool isIdle() const override { return true; }

    std::vector<std::string> getDependencies() const override {
        return {"BaseModule"};  // Dépend de BaseModule
    }

    int getVersion() const override { return version_; }

    // Setter pour injecter la référence à BaseModule
    void setBaseModule(BaseModule* baseModule) {
        baseModule_ = baseModule;
    }

private:
    int version_ = 1;
    BaseModule* baseModule_ = nullptr;
    std::vector<int> collectedNumbers_;  // Accumule les valeurs de BaseModule
};

extern "C" IModule* createModule() {
    return new DependentModule();
}

extern "C" void destroyModule(IModule* module) {
    delete module;
}

IndependentModule Structure

// tests/modules/IndependentModule.h
#pragma once
#include "grove/IModule.h"
#include <memory>
#include <atomic>

class IndependentModule : public IModule {
public:
    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;
    bool isIdle() const override { return true; }

    std::vector<std::string> getDependencies() const override {
        return {};  // Témoin: aucune dépendance
    }

    int getVersion() const override { return version_; }

private:
    int version_ = 1;
    std::atomic<int> processCount_{0};
};

extern "C" IModule* createModule() {
    return new IndependentModule();
}

extern "C" void destroyModule(IModule* module) {
    delete module;
}

ModuleLoader Extensions

Le ModuleLoader doit ĂȘtre Ă©tendu pour:

  1. Dependency Resolution:

    // Lors du chargement
    std::vector<std::string> resolveDependencies(const std::string& moduleName);
    bool checkCyclicDependencies(const std::string& moduleName, std::set<std::string>& visited);
    
  2. Cascade Reload:

    // Lors du reload
    std::vector<std::string> findDependents(const std::string& moduleName);
    void reloadWithDependents(const std::string& moduleName);
    
  3. Unload Protection:

    // Lors du déchargement
    bool canUnload(const std::string& moduleName, std::string& errorMsg);
    

Dependency Graph

Phase 1-5:
┌─────────────────┐
│ IndependentModule│  (isolated, never reloaded)
└─────────────────┘

┌──────────────┐
│  BaseModule  │  (no dependencies)
└──────┬───────┘
       │
       │ depends on
       ▌
┌──────────────────┐
│ DependentModule  │  (depends on BaseModule)
└──────────────────┘

Cascade reload:
  BaseModule reload → DependentModule reload (cascade)
  DependentModule reload → BaseModule NOT reloaded (no reverse cascade)

Phase 4 (rejected):
┌────────────────┐        ┌────────────────┐
│ CyclicModuleA  │───────▶│ CyclicModuleB  │
└────────────────┘        └────────┬───────┘
       â–Č                           │
       └───────────────────────────┘
                CYCLE! → REJECTED

📊 MĂ©triques CollectĂ©es

Métrique Description Seuil
cascade_reload_time_ms Temps pour reload B + tous dépendants < 200ms
dependency_resolution_time_ms Temps pour identifier dépendants < 10ms
cycle_detection_time_ms Temps pour détecter cycle < 50ms
memory_growth_mb Croissance mémoire totale < 5MB
reload_count Nombre total de reloads 3 (Base v2, Dep cascade, Dep v2)

✅ Critùres de Succùs

MUST PASS

  1. ✅ Cascade reload: BaseModule reload → DependentModule reload
  2. ✅ Ordre correct: BaseModule avant DependentModule
  3. ✅ Isolation: IndependentModule jamais reload
  4. ✅ Unload protection: BaseModule non dĂ©chargeable si DependentModule actif
  5. ✅ Cycle detection: DĂ©pendances circulaires dĂ©tectĂ©es et rejetĂ©es
  6. ✅ State preservation: State prĂ©servĂ© aprĂšs cascade reload
  7. ✅ No reverse cascade: DependentModule reload ne force pas BaseModule reload
  8. ✅ Aucun crash durant tout le test

NICE TO HAVE

  1. ✅ Cascade reload en < 100ms
  2. ✅ Erreurs explicites avec noms de modules
  3. ✅ Support de dĂ©pendances multiples (A dĂ©pend de B et C)
  4. ✅ Visualisation du graphe de dĂ©pendances

🔧 API NĂ©cessaires dans DebugEngine

Nouvelles méthodes

class DebugEngine {
public:
    // Méthodes existantes...

    // NOUVELLES pour Scenario 9
    bool canUnloadModule(const std::string& moduleName, std::string& errorMsg);
    std::vector<std::string> getModuleDependents(const std::string& moduleName);
    std::vector<std::string> getModuleDependencies(const std::string& moduleName);
    bool hasCircularDependencies(const std::string& moduleName);

    // Reload avec cascade
    bool reloadModuleWithDependents(const std::string& moduleName);
};

🐛 Cas d'Erreur Attendus

Erreur Cause Action
DependentModule non rechargé aprÚs BaseModule reload Cascade pas implémentée FAIL - implémenter cascade
BaseModule rechargé aprÚs DependentModule reload Reverse cascade incorrecte FAIL - vérifier direction
IndependentModule rechargé Isolation cassée FAIL - fix isolation
BaseModule déchargé alors que DependentModule actif Protection pas implémentée FAIL - ajouter check
Cycle non détecté Algorithme DFS manquant FAIL - implémenter cycle detection
Ordre reload incorrect (A avant B) Topological sort manquant FAIL - trier deps
State corrompu aprÚs cascade Mauvaise gestion FAIL - préserver state

📝 Output Attendu

================================================================================
TEST: Module Dependencies
================================================================================

=== Setup: Load modules with dependencies ===
✓ BaseModule loaded (v1, no dependencies)
✓ DependentModule loaded (v1, depends on: BaseModule)
✓ IndependentModule loaded (v1, no dependencies)

Dependency graph:
  IndependentModule → (none)
  BaseModule → (none)
  DependentModule → BaseModule

=== Phase 1: Cascade Reload (30s) ===
Reloading BaseModule...
  → BaseModule reload triggered
  → Cascade reload triggered for DependentModule
✓ BaseModule reloaded: v1 → v2
✓ DependentModule cascade reloaded: v1 → v1 (state preserved)
✓ IndependentModule NOT reloaded (isolated)
✓ generateNumber() changed: 42 → 100
✓ DependentModule using new value: 100

Metrics:
  Cascade reload time:        85ms  ✓
  Dependency resolution time:  4ms  ✓

=== Phase 2: Unload Protection (10s) ===
Attempting to unload BaseModule...
  ✗ Unload rejected: Cannot unload BaseModule: required by DependentModule
✓ BaseModule still loaded and functional
✓ All modules stable

=== Phase 3: Reload Dependent Only (20s) ===
Reloading DependentModule...
✓ DependentModule reloaded: v1 → v2
✓ BaseModule NOT reloaded (no reverse cascade)
✓ IndependentModule still isolated
✓ DependentModule still connected to BaseModule

=== Phase 4: Cyclic Dependency Detection (20s) ===
Attempting to load CyclicModuleA and CyclicModuleB...
  ✗ Load rejected: Cyclic dependency detected: CyclicModuleA → CyclicModuleB → CyclicModuleA
✓ No modules partially loaded
✓ Existing modules unaffected

Metrics:
  Cycle detection time:       12ms  ✓

=== Phase 5: Cascade Unload (20s) ===
Unloading DependentModule...
✓ DependentModule unloaded (dependency released)
✓ BaseModule still loaded

Attempting to unload BaseModule...
✓ BaseModule unload succeeded (no dependents)

Final state:
  IndependentModule: loaded (v1)
  BaseModule: unloaded
  DependentModule: unloaded

================================================================================
METRICS
================================================================================
  Cascade reload time:      85ms         (threshold: < 200ms)  ✓
  Dep resolution time:       4ms         (threshold: < 10ms)   ✓
  Cycle detection time:     12ms         (threshold: < 50ms)   ✓
  Memory growth:          2.1MB         (threshold: < 5MB)    ✓
  Total reloads:              3         (expected: 3)         ✓

================================================================================
ASSERTIONS
================================================================================
  ✓ modules_loaded
  ✓ cascade_reload_triggered
  ✓ reload_order_correct
  ✓ independent_isolated
  ✓ unload_protection_works
  ✓ no_reverse_cascade
  ✓ cycle_detected
  ✓ cascade_unload_works
  ✓ state_preserved
  ✓ no_crashes

Result: ✅ PASSED

================================================================================

📅 Planning

Jour 10 (3h):

  • Étendre IModule avec getDependencies() et getVersion()
  • ImplĂ©menter BaseModule, DependentModule, IndependentModule
  • Étendre ModuleLoader avec dependency resolution

Jour 11 (2h):

  • ImplĂ©menter cascade reload dans ModuleLoader
  • ImplĂ©menter cycle detection (DFS)
  • ImplĂ©menter unload protection

Jour 12 (1h):

  • ImplĂ©menter test_09_module_dependencies.cpp
  • Debug + validation
  • IntĂ©gration CMake

🎯 Valeur AjoutĂ©e

Ce test valide une fonctionnalité critique pour les systÚmes complexes :

  • ModularitĂ© avancĂ©e : Permet de construire des architectures avec dĂ©pendances claires
  • SĂ©curitĂ© : EmpĂȘche les dĂ©chargements dangereux qui casseraient des rĂ©fĂ©rences
  • MaintenabilitĂ© : Cascade reload automatique garantit cohĂ©rence
  • Robustesse : DĂ©tection de cycles Ă©vite les deadlocks et Ă©tats invalides

Dans un moteur de jeu réel, typiquement:

  • PhysicsModule dĂ©pend de MathModule
  • RenderModule dĂ©pend de ResourceModule
  • GameplayModule dĂ©pend de PhysicsModule et AudioModule

Le hot-reload de MathModule doit automatiquement recharger PhysicsModule et tous ses dépendants (GameplayModule), dans l'ordre topologique correct.


Prochaine étape: Implémentation