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>
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
- Charger BaseModule (version 1)
- Expose service:
generateNumber()â retourne 42 - Pas de dĂ©pendances
- Expose service:
- Charger DependentModule (version 1)
- Dépend de:
["BaseModule"] - Utilise
BaseModule.generateNumber() - Accumule les résultats
- Dépend de:
- Charger IndependentModule (version 1)
- Pas de dépendances
- Témoin indépendant
- Vérifier que tous les modules sont chargés et fonctionnent
Phase 1: Cascade Reload (BaseModule â DependentModule)
Durée: 30 secondes
- Ă t=10s, modifier et reloader BaseModule (version 2)
generateNumber()â retourne maintenant 100
- Vérifier cascade reload:
- â BaseModule rechargĂ© automatiquement
- â DependentModule forcĂ© Ă reloader (cascade)
- â IndependentModule non rechargĂ© (isolĂ©)
- Vérifier ordre de reload:
- BaseModule reload avant DependentModule
- Ătat cohĂ©rent (pas de rĂ©fĂ©rences cassĂ©es)
- 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
- à t=40s, tenter de décharger BaseModule
- Vérifier rejet:
- â DĂ©chargement refusĂ© (DependentModule en dĂ©pend)
- â ïž Erreur claire: "Cannot unload BaseModule: required by DependentModule"
- â BaseModule reste chargĂ© et fonctionnel
- 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
- Ă t=50s, modifier et reloader DependentModule (version 2)
- Change comportement interne
- Garde mĂȘme dĂ©pendance sur BaseModule
- Vérifier isolation:
- â DependentModule rechargĂ©
- â BaseModule non rechargĂ© (pas de cascade inverse)
- â IndependentModule toujours isolĂ©
- 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
- à t=70s, créer module CyclicModule avec dépendance circulaire:
- CyclicModuleA dépend de CyclicModuleB
- CyclicModuleB dépend de CyclicModuleA
- Tenter de charger les deux modules
- Vérifier détection:
- â Chargement refusĂ©
- â ïž Erreur claire: "Cyclic dependency detected: A â B â A"
- â Aucun module partiellement chargĂ© (transactional)
- Vérifier stabilité:
- Modules existants non affectés
- SystĂšme reste stable
Phase 5: Déchargement en Cascade
Durée: 20 secondes
- à t=90s, décharger DependentModule (libÚre dépendance)
- Vérifier libération:
- â DependentModule dĂ©chargĂ©
- â BaseModule toujours chargĂ© (encore utilisable)
- à t=100s, décharger BaseModule (maintenant possible)
- 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:
-
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); -
Cascade Reload:
// Lors du reload std::vector<std::string> findDependents(const std::string& moduleName); void reloadWithDependents(const std::string& moduleName); -
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
- â Cascade reload: BaseModule reload â DependentModule reload
- â Ordre correct: BaseModule avant DependentModule
- â Isolation: IndependentModule jamais reload
- â Unload protection: BaseModule non dĂ©chargeable si DependentModule actif
- â Cycle detection: DĂ©pendances circulaires dĂ©tectĂ©es et rejetĂ©es
- â State preservation: State prĂ©servĂ© aprĂšs cascade reload
- â No reverse cascade: DependentModule reload ne force pas BaseModule reload
- â Aucun crash durant tout le test
NICE TO HAVE
- â Cascade reload en < 100ms
- â Erreurs explicites avec noms de modules
- â Support de dĂ©pendances multiples (A dĂ©pend de B et C)
- â 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()etgetVersion() - 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