# Scénario 7: Limite Tests **Priorité**: ⭐ NICE TO HAVE **Phase**: 3 (NICE TO HAVE) **Durée estimée**: ~3 minutes **Effort implémentation**: ~3-4 heures --- ## 🎯 Objectif Valider que le système de hot-reload reste robuste face aux conditions extrêmes: - État très large (100MB+ en mémoire) - Initialisation longue (>5 secondes) - Timeouts de reload - Limites de sérialisation/désérialisation - Gestion de la mémoire sous contrainte --- ## 📋 Description ### Setup Initial 1. Charger `HeavyStateModule` avec configuration extrême 2. Créer un état massif: - 1 million de particules (x, y, vx, vy, lifetime, color) - Matrice de terrain 10000x10000 (100M cellules) - Historique des 10000 dernières frames - Cache de 50000 textures simulées 3. État sérialisé estimé: ~120MB en JSON 4. Temps d'initialisation: ~8 secondes ### Test Séquence #### Test 1: Large State Serialization (60s) 1. Exécuter pendant 10 secondes (600 frames) 2. Trigger hot-reload avec extraction de l'état complet 3. Mesurer: - Temps de `getState()`: doit être < 2000ms - Taille de l'état sérialisé - Temps de `setState()`: doit être < 2000ms 4. Vérifier intégrité des données post-reload: - Nombre de particules = 1M - Dimensions terrain = 10000x10000 - Historique complet préservé 5. Continuer pendant 10 secondes supplémentaires #### Test 2: Long Initialization Timeout (30s) 1. Charger `HeavyStateModule` avec init duration = 12s 2. Configurer timeout à 10s (volontairement trop court) 3. Vérifier que le système: - Détecte le timeout correctement - Annule le chargement proprement - Libère toutes les ressources - Log un message d'erreur clair 4. Recharger avec timeout = 15s (suffisant) 5. Vérifier chargement réussi #### Test 3: Memory Pressure During Reload (60s) 1. Démarrer avec état 100MB 2. Pendant reload: - Allouer 200MB supplémentaires (simuler pic mémoire) - Vérifier que le système ne crash pas (OOM) - Mesurer temps de reload sous pression 3. Après reload: - Vérifier retour à 100MB baseline - Aucune fuite détectée - État intact #### Test 4: Incremental State Save/Load (30s) 1. Activer mode "incremental state" (seuls les deltas) 2. Faire 10 reloads successifs 3. Chaque reload modifie 1% de l'état (10k particules) 4. Mesurer: - Temps de reload incrémental: doit être < 100ms - Taille des deltas: doit être ~1MB - Intégrité finale: 100% des particules correctes #### Test 5: State Corruption Detection (20s) 1. Créer un état volontairement corrompu: - JSON mal formé - Valeurs hors limites (NaN, Infinity) - Champs manquants 2. Tenter `setState()` avec état corrompu 3. Vérifier: - Détection de corruption avant application - État précédent non affecté - Message d'erreur descriptif - Module reste fonctionnel --- ## 🏗️ Implémentation ### HeavyStateModule Structure ```cpp // HeavyStateModule.h class HeavyStateModule : public IModule { public: struct Particle { float x, y; // Position float vx, vy; // Vélocité float lifetime; // Temps restant uint32_t color; // RGBA }; struct TerrainCell { uint8_t height; // 0-255 uint8_t type; // Grass, water, rock, etc. uint8_t metadata; // Flags uint8_t reserved; }; struct FrameSnapshot { uint32_t frameId; float avgFPS; size_t particleCount; uint64_t timestamp; }; void initialize(std::shared_ptr config) override; void process(float deltaTime) override; std::shared_ptr getState() const override; void setState(std::shared_ptr state) override; bool isIdle() const override { return true; } private: std::vector particles; // 1M particules = ~32MB std::vector terrain; // 100M cells = ~100MB std::deque history; // 10k frames = ~160KB std::unordered_map> textureCache; // 50k textures simulées float initDuration = 8.0f; // Temps d'init simulé int frameCount = 0; std::string version = "v1.0"; void updateParticles(float dt); void spawnParticles(size_t count); void initializeTerrain(int width, int height); bool validateState(std::shared_ptr state) const; }; ``` ### State Format (JSON) - Optimisé ```json { "version": "v1.0", "frameCount": 600, "config": { "particleCount": 1000000, "terrainWidth": 10000, "terrainHeight": 10000, "historySize": 10000 }, "particles": { "count": 1000000, "data": "base64_encoded_binary_data" // Compression pour réduire taille JSON }, "terrain": { "width": 10000, "height": 10000, "compressed": true, "data": "base64_zlib_compressed" // Terrain compressé }, "history": [ {"frame": 590, "fps": 60, "particles": 1000000, "ts": 1234567890}, // ... 9999 autres frames ], "textureCache": { "count": 50000, "totalSize": 25600000 } } ``` ### Test Principal ```cpp // test_07_limits.cpp #include "helpers/TestMetrics.h" #include "helpers/TestAssertions.h" #include "helpers/TestReporter.h" #include int main() { TestReporter reporter("Limite Tests"); TestMetrics metrics; // ======================================================================== // TEST 1: Large State Serialization // ======================================================================== std::cout << "\n=== TEST 1: Large State Serialization ===\n"; DebugEngine engine; engine.loadModule("HeavyStateModule", "build/modules/libHeavyStateModule.so"); auto config = createJsonConfig({ {"version", "v1.0"}, {"particleCount", 1000000}, {"terrainSize", 10000}, {"initDuration", 8.0f} }); // Initialisation (devrait prendre ~8s) auto initStart = std::chrono::high_resolution_clock::now(); engine.initializeModule("HeavyStateModule", config); auto initEnd = std::chrono::high_resolution_clock::now(); float initTime = std::chrono::duration(initEnd - initStart).count(); std::cout << "Initialization took: " << initTime << "s\n"; ASSERT_GT(initTime, 7.0f, "Init should take at least 7s (simulated heavy init)"); ASSERT_LT(initTime, 10.0f, "Init should not take more than 10s"); reporter.addMetric("init_time_s", initTime); // Exécuter 10s for (int i = 0; i < 600; i++) { engine.update(1.0f/60.0f); } // Mesurer temps de getState() auto getStateStart = std::chrono::high_resolution_clock::now(); auto state = engine.getModuleState("HeavyStateModule"); auto getStateEnd = std::chrono::high_resolution_clock::now(); float getStateTime = std::chrono::duration(getStateEnd - getStateStart).count(); std::cout << "getState() took: " << getStateTime << "ms\n"; ASSERT_LT(getStateTime, 2000.0f, "getState() should be < 2000ms"); reporter.addMetric("getstate_time_ms", getStateTime); // Estimer taille de l'état auto* jsonNode = dynamic_cast(state.get()); std::string stateStr = jsonNode->getJsonData().dump(); size_t stateSize = stateStr.size(); std::cout << "State size: " << (stateSize / 1024.0f / 1024.0f) << " MB\n"; reporter.addMetric("state_size_mb", stateSize / 1024.0f / 1024.0f); // Hot-reload avec setState() modifySourceFile("tests/modules/HeavyStateModule.cpp", "v1.0", "v2.0"); system("cmake --build build --target HeavyStateModule 2>&1 > /dev/null"); auto setStateStart = std::chrono::high_resolution_clock::now(); engine.reloadModule("HeavyStateModule"); auto setStateEnd = std::chrono::high_resolution_clock::now(); float setStateTime = std::chrono::duration(setStateEnd - setStateStart).count(); std::cout << "setState() + reload took: " << setStateTime << "ms\n"; ASSERT_LT(setStateTime, 2000.0f, "setState() should be < 2000ms"); reporter.addMetric("setstate_time_ms", setStateTime); // Vérifier intégrité auto stateAfter = engine.getModuleState("HeavyStateModule"); auto* jsonNodeAfter = dynamic_cast(stateAfter.get()); const auto& dataAfter = jsonNodeAfter->getJsonData(); int particleCount = dataAfter["config"]["particleCount"]; ASSERT_EQ(particleCount, 1000000, "Should have 1M particles after reload"); reporter.addAssertion("particles_preserved", particleCount == 1000000); // Continuer 10s for (int i = 0; i < 600; i++) { engine.update(1.0f/60.0f); } std::cout << "✓ TEST 1 PASSED\n"; // ======================================================================== // TEST 2: Long Initialization Timeout // ======================================================================== std::cout << "\n=== TEST 2: Long Initialization Timeout ===\n"; DebugEngine engine2; engine2.loadModule("HeavyStateModule", "build/modules/libHeavyStateModule.so"); // Config avec init très long + timeout trop court auto configTimeout = createJsonConfig({ {"version", "v1.0"}, {"particleCount", 1000000}, {"terrainSize", 10000}, {"initDuration", 12.0f}, // Init va prendre 12s {"initTimeout", 10.0f} // Mais timeout à 10s }); bool timedOut = false; try { engine2.initializeModule("HeavyStateModule", configTimeout); } catch (const std::runtime_error& e) { std::string msg = e.what(); if (msg.find("timeout") != std::string::npos || msg.find("Timeout") != std::string::npos) { timedOut = true; std::cout << "✓ Timeout detected correctly: " << msg << "\n"; } } ASSERT_TRUE(timedOut, "Should timeout with init > timeout threshold"); reporter.addAssertion("timeout_detection", timedOut); // Réessayer avec timeout suffisant auto configOk = createJsonConfig({ {"version", "v1.0"}, {"particleCount", 1000000}, {"terrainSize", 10000}, {"initDuration", 8.0f}, {"initTimeout", 15.0f} }); bool success = true; try { engine2.initializeModule("HeavyStateModule", configOk); engine2.update(1.0f/60.0f); } catch (...) { success = false; } ASSERT_TRUE(success, "Should succeed with adequate timeout"); reporter.addAssertion("timeout_recovery", success); std::cout << "✓ TEST 2 PASSED\n"; // ======================================================================== // TEST 3: Memory Pressure During Reload // ======================================================================== std::cout << "\n=== TEST 3: Memory Pressure During Reload ===\n"; size_t memBefore = getCurrentMemoryUsage(); std::cout << "Memory before: " << (memBefore / 1024.0f / 1024.0f) << " MB\n"; // Exécuter quelques frames for (int i = 0; i < 300; i++) { engine.update(1.0f/60.0f); } // Allouer temporairement 200MB pendant reload std::vector tempAlloc; auto reloadStart = std::chrono::high_resolution_clock::now(); // Démarrer reload dans un thread std::thread reloadThread([&]() { modifySourceFile("tests/modules/HeavyStateModule.cpp", "v2.0", "v3.0"); system("cmake --build build --target HeavyStateModule 2>&1 > /dev/null"); engine.reloadModule("HeavyStateModule"); }); // Pendant reload, allouer massivement std::this_thread::sleep_for(std::chrono::milliseconds(100)); tempAlloc.resize(200 * 1024 * 1024); // 200MB std::fill(tempAlloc.begin(), tempAlloc.end(), 0x42); size_t memDuringReload = getCurrentMemoryUsage(); std::cout << "Memory during reload: " << (memDuringReload / 1024.0f / 1024.0f) << " MB\n"; reloadThread.join(); auto reloadEnd = std::chrono::high_resolution_clock::now(); float reloadTimeUnderPressure = std::chrono::duration(reloadEnd - reloadStart).count(); std::cout << "Reload under pressure took: " << reloadTimeUnderPressure << "ms\n"; reporter.addMetric("reload_under_pressure_ms", reloadTimeUnderPressure); // Libérer allocation temporaire tempAlloc.clear(); tempAlloc.shrink_to_fit(); // Attendre GC std::this_thread::sleep_for(std::chrono::milliseconds(500)); size_t memAfter = getCurrentMemoryUsage(); std::cout << "Memory after: " << (memAfter / 1024.0f / 1024.0f) << " MB\n"; long memGrowth = static_cast(memAfter) - static_cast(memBefore); std::cout << "Net memory growth: " << (memGrowth / 1024.0f / 1024.0f) << " MB\n"; // Tolérance: max 10MB de croissance nette ASSERT_LT(std::abs(memGrowth), 10 * 1024 * 1024, "Memory growth should be < 10MB"); reporter.addMetric("memory_growth_mb", memGrowth / 1024.0f / 1024.0f); std::cout << "✓ TEST 3 PASSED\n"; // ======================================================================== // TEST 4: Incremental State Save/Load // ======================================================================== std::cout << "\n=== TEST 4: Incremental State Save/Load ===\n"; // Activer mode incrémental (si supporté) auto configIncremental = createJsonConfig({ {"version", "v1.0"}, {"particleCount", 100000}, // Réduit pour test rapide {"terrainSize", 1000}, {"incrementalState", true} }); DebugEngine engine3; engine3.loadModule("HeavyStateModule", "build/modules/libHeavyStateModule.so"); engine3.initializeModule("HeavyStateModule", configIncremental); std::vector incrementalTimes; for (int reload = 0; reload < 10; reload++) { // Exécuter 60 frames (1s) for (int i = 0; i < 60; i++) { engine3.update(1.0f/60.0f); } // Reload incrémental auto incStart = std::chrono::high_resolution_clock::now(); // Modifier légèrement le code std::string oldVer = "v" + std::to_string(reload) + ".0"; std::string newVer = "v" + std::to_string(reload + 1) + ".0"; modifySourceFile("tests/modules/HeavyStateModule.cpp", oldVer, newVer); system("cmake --build build --target HeavyStateModule 2>&1 > /dev/null"); engine3.reloadModule("HeavyStateModule"); auto incEnd = std::chrono::high_resolution_clock::now(); float incTime = std::chrono::duration(incEnd - incStart).count(); incrementalTimes.push_back(incTime); std::cout << "Reload #" << reload << ": " << incTime << "ms\n"; } float avgIncremental = std::accumulate(incrementalTimes.begin(), incrementalTimes.end(), 0.0f) / incrementalTimes.size(); std::cout << "Average incremental reload: " << avgIncremental << "ms\n"; // Note: Pour un vrai système incrémental, devrait être < 100ms // Ici on vérifie juste cohérence ASSERT_LT(avgIncremental, 2000.0f, "Incremental reloads should be reasonably fast"); reporter.addMetric("avg_incremental_reload_ms", avgIncremental); std::cout << "✓ TEST 4 PASSED\n"; // ======================================================================== // TEST 5: State Corruption Detection // ======================================================================== std::cout << "\n=== TEST 5: State Corruption Detection ===\n"; DebugEngine engine4; engine4.loadModule("HeavyStateModule", "build/modules/libHeavyStateModule.so"); auto configNormal = createJsonConfig({ {"version", "v1.0"}, {"particleCount", 10000}, {"terrainSize", 100} }); engine4.initializeModule("HeavyStateModule", configNormal); // Exécuter un peu for (int i = 0; i < 60; i++) { engine4.update(1.0f/60.0f); } // Créer un état corrompu nlohmann::json corruptedState = { {"version", "v1.0"}, {"frameCount", "INVALID_NOT_A_NUMBER"}, // Type incorrect {"config", { {"particleCount", -500}, // Valeur négative invalide {"terrainSize", std::numeric_limits::quiet_NaN()} // NaN }}, {"particles", { {"count", 10000}, {"data", "CORRUPTED_BASE64!!!"} // Données invalides }} // Champ "terrain" manquant (requis) }; auto corruptedNode = std::make_shared(corruptedState); bool detectedCorruption = false; try { engine4.setModuleState("HeavyStateModule", corruptedNode); } catch (const std::exception& e) { std::string msg = e.what(); std::cout << "✓ Corruption detected: " << msg << "\n"; detectedCorruption = true; } ASSERT_TRUE(detectedCorruption, "Should detect corrupted state"); reporter.addAssertion("corruption_detection", detectedCorruption); // Vérifier que le module reste fonctionnel bool stillFunctional = true; try { for (int i = 0; i < 60; i++) { engine4.update(1.0f/60.0f); } } catch (...) { stillFunctional = false; } ASSERT_TRUE(stillFunctional, "Module should remain functional after rejected corrupted state"); reporter.addAssertion("functional_after_corruption", stillFunctional); std::cout << "✓ TEST 5 PASSED\n"; // ======================================================================== // RAPPORT FINAL // ======================================================================== metrics.printReport(); reporter.printFinalReport(); return reporter.getExitCode(); } ``` --- ## 📊 Métriques Collectées | Métrique | Description | Seuil | |----------|-------------|-------| | **init_time_s** | Temps d'initialisation module lourd | 7-10s | | **getstate_time_ms** | Temps extraction état 120MB | < 2000ms | | **setstate_time_ms** | Temps restauration état 120MB | < 2000ms | | **state_size_mb** | Taille état sérialisé | ~120MB | | **reload_under_pressure_ms** | Reload sous contrainte mémoire | < 3000ms | | **memory_growth_mb** | Croissance mémoire nette | < 10MB | | **avg_incremental_reload_ms** | Temps moyen reload incrémental | < 2000ms | --- ## ✅ Critères de Succès ### MUST PASS 1. ✅ getState() < 2000ms pour état 120MB 2. ✅ setState() < 2000ms pour état 120MB 3. ✅ Timeout détecté correctement (init > timeout) 4. ✅ Recovery après timeout fonctionne 5. ✅ Reload sous pression mémoire ne crash pas 6. ✅ Memory growth < 10MB 7. ✅ Corruption détectée et rejetée 8. ✅ Module reste fonctionnel après corruption ### NICE TO HAVE 1. ✅ getState() < 1000ms (optimal) 2. ✅ setState() < 1000ms (optimal) 3. ✅ Incremental reload < 100ms 4. ✅ Message d'erreur descriptif pour corruption --- ## 🔧 Helpers Nécessaires ### Compression Helper ```cpp // Pour réduire taille JSON #include std::string compressData(const std::vector& data) { uLongf compressedSize = compressBound(data.size()); std::vector compressed(compressedSize); int result = compress(compressed.data(), &compressedSize, data.data(), data.size()); if (result != Z_OK) { throw std::runtime_error("Compression failed"); } compressed.resize(compressedSize); // Base64 encode return base64_encode(compressed); } std::vector decompressData(const std::string& base64Str) { auto compressed = base64_decode(base64Str); // Taille décompressée doit être dans metadata uLongf uncompressedSize = getUncompressedSize(compressed); std::vector uncompressed(uncompressedSize); int result = uncompress(uncompressed.data(), &uncompressedSize, compressed.data(), compressed.size()); if (result != Z_OK) { throw std::runtime_error("Decompression failed"); } return uncompressed; } ``` ### State Validator ```cpp bool HeavyStateModule::validateState(std::shared_ptr state) const { auto* jsonNode = dynamic_cast(state.get()); if (!jsonNode) return false; const auto& data = jsonNode->getJsonData(); // Vérifier champs requis if (!data.contains("version") || !data.contains("config") || !data.contains("particles") || !data.contains("terrain")) { std::cerr << "ERROR: Missing required fields\n"; return false; } // Vérifier types if (!data["frameCount"].is_number_integer()) { std::cerr << "ERROR: frameCount must be integer\n"; return false; } // Vérifier limites int particleCount = data["config"]["particleCount"]; if (particleCount < 0 || particleCount > 10000000) { std::cerr << "ERROR: Invalid particle count: " << particleCount << "\n"; return false; } // Vérifier NaN/Infinity int terrainSize = data["config"]["terrainSize"]; if (std::isnan(terrainSize) || std::isinf(terrainSize)) { std::cerr << "ERROR: terrain size is NaN/Inf\n"; return false; } return true; } ``` --- ## 🐛 Cas d'Erreur Attendus | Erreur | Cause | Action | |--------|-------|--------| | getState() > 2000ms | Sérialisation trop lente | FAIL - optimiser ou compresser | | setState() > 2000ms | Désérialisation lente | FAIL - optimiser parsing JSON | | Timeout non détecté | Init bloque sans timeout | FAIL - implémenter timeout thread | | OOM pendant reload | Pic mémoire trop élevé | FAIL - limiter allocation simultanée | | Corruption non détectée | Validation insuffisante | FAIL - renforcer validateState() | | Memory leak > 10MB | Ressources non libérées | FAIL - check destructeurs | --- ## 📝 Output Attendu ``` ================================================================================ TEST: Limite Tests ================================================================================ === TEST 1: Large State Serialization === Initialization took: 8.2s getState() took: 1243ms State size: 118.4 MB setState() + reload took: 1389ms ✓ Particles preserved: 1000000/1000000 ✓ TEST 1 PASSED === TEST 2: Long Initialization Timeout === ✓ Timeout detected correctly: Module init exceeded timeout (12s > 10s) ✓ Recovery successful with timeout=15s ✓ TEST 2 PASSED === TEST 3: Memory Pressure During Reload === Memory before: 124.3 MB Memory during reload: 332.7 MB Reload under pressure took: 1876ms Memory after: 127.1 MB Net memory growth: 2.8 MB ✓ TEST 3 PASSED === TEST 4: Incremental State Save/Load === Reload #0: 245ms Reload #1: 238ms Reload #2: 251ms ... Reload #9: 242ms Average incremental reload: 244ms ✓ TEST 4 PASSED === TEST 5: State Corruption Detection === ✓ Corruption detected: Invalid particle count (negative value) ✓ Module remains functional after rejecting corrupted state ✓ TEST 5 PASSED ================================================================================ METRICS ================================================================================ Init time: 8.2s getState time: 1243ms (threshold: < 2000ms) ✓ setState time: 1389ms (threshold: < 2000ms) ✓ State size: 118.4MB Reload under pressure: 1876ms (threshold: < 3000ms) ✓ Memory growth: 2.8MB (threshold: < 10MB) ✓ Avg incremental reload: 244ms ================================================================================ ASSERTIONS ================================================================================ ✓ particles_preserved ✓ timeout_detection ✓ timeout_recovery ✓ corruption_detection ✓ functional_after_corruption Result: ✅ PASSED (5/5 tests) ================================================================================ ``` --- ## 📅 Planning **Jour 1 (3h):** - Implémenter HeavyStateModule avec gestion large state - Implémenter compression/décompression - Implémenter state validation **Jour 2 (1h):** - Implémenter test_07_limits.cpp - Debug + validation --- **Prochaine étape**: `scenario_08_config_hotreload.md` (optionnel)