diff --git a/CMakeLists.txt b/CMakeLists.txt index 6b285cb..4782af1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -199,6 +199,24 @@ option(GROVE_BUILD_TESTS "Build GroveEngine tests" ON) if(GROVE_BUILD_TESTS) enable_testing() + + # Windows/MinGW: Copy runtime DLLs to build directories for CTest + if(WIN32 AND MINGW) + get_filename_component(MINGW_BIN_DIR ${CMAKE_CXX_COMPILER} DIRECTORY) + set(MINGW_RUNTIME_DLLS libgcc_s_seh-1.dll libstdc++-6.dll libwinpthread-1.dll) + + foreach(DLL ${MINGW_RUNTIME_DLLS}) + set(DLL_PATH "${MINGW_BIN_DIR}/${DLL}") + if(EXISTS "${DLL_PATH}") + file(COPY "${DLL_PATH}" DESTINATION "${CMAKE_BINARY_DIR}") + file(COPY "${DLL_PATH}" DESTINATION "${CMAKE_BINARY_DIR}/tests") + file(COPY "${DLL_PATH}" DESTINATION "${CMAKE_BINARY_DIR}/external/StillHammer/topictree/tests") + endif() + endforeach() + + message(STATUS "MinGW runtime DLLs copied for CTest") + endif() + add_subdirectory(tests) endif() diff --git a/external/StillHammer/topictree/tests/CMakeLists.txt b/external/StillHammer/topictree/tests/CMakeLists.txt index b3a8fc8..3823669 100644 --- a/external/StillHammer/topictree/tests/CMakeLists.txt +++ b/external/StillHammer/topictree/tests/CMakeLists.txt @@ -9,6 +9,17 @@ FetchContent_Declare( ) FetchContent_MakeAvailable(Catch2) +# Windows/MinGW: Copy runtime DLLs at configure time +if(WIN32 AND MINGW) + get_filename_component(MINGW_BIN_DIR ${CMAKE_CXX_COMPILER} DIRECTORY) + set(MINGW_DLLS libgcc_s_seh-1.dll libstdc++-6.dll libwinpthread-1.dll) + foreach(DLL ${MINGW_DLLS}) + if(EXISTS "${MINGW_BIN_DIR}/${DLL}") + file(COPY "${MINGW_BIN_DIR}/${DLL}" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}") + endif() + endforeach() +endif() + # Helper macro to create test executables macro(add_topictree_test test_name) add_executable(${test_name} ${test_name}.cpp) @@ -17,7 +28,14 @@ macro(add_topictree_test test_name) Catch2::Catch2WithMain ) target_compile_features(${test_name} PRIVATE cxx_std_17) - add_test(NAME ${test_name} COMMAND ${test_name}) + if(WIN32) + # Use cmd.exe start to properly find DLLs in working directory + add_test(NAME ${test_name} + COMMAND ${CMAKE_COMMAND} -E chdir ${CMAKE_CURRENT_BINARY_DIR} + $) + else() + add_test(NAME ${test_name} COMMAND ${test_name} WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + endif() endmacro() # Add all scenario tests diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d72db06..57a5776 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,1186 +1,1213 @@ -# Hot-reload test suite - -# Test module as shared library (.so) for hot-reload -add_library(TestModule SHARED - modules/TestModule.cpp -) - -target_link_libraries(TestModule PRIVATE - GroveEngine::core - GroveEngine::impl # For JsonDataNode implementation -) - -# Don't add "lib" prefix on Linux (we want TestModule.so, not libTestModule.so) -set_target_properties(TestModule PROPERTIES PREFIX "lib") -set_target_properties(TestModule PROPERTIES OUTPUT_NAME "TestModule") - -# Basic hot-reload test executable (manual dlopen/dlclose) -add_executable(test_hotreload - hotreload/test_hotreload.cpp -) - -target_link_libraries(test_hotreload PRIVATE - GroveEngine::core - GroveEngine::impl # For JsonDataNode implementation - ${CMAKE_DL_LIBS} # For dlopen/dlclose -) - -# Make sure test module is built before test executable -add_dependencies(test_hotreload TestModule) - -# Copy test module to test executable directory after build -add_custom_command(TARGET test_hotreload POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy - $ - $/ - COMMENT "Copying TestModule.so to test directory" -) - -# Engine hot-reload test (uses DebugEngine + SequentialModuleSystem + FileWatcher) -add_executable(test_engine_hotreload - hotreload/test_engine_hotreload.cpp -) - -target_link_libraries(test_engine_hotreload PRIVATE - GroveEngine::core - GroveEngine::impl - ${CMAKE_DL_LIBS} -) - -add_dependencies(test_engine_hotreload TestModule) - -add_custom_command(TARGET test_engine_hotreload POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy - $ - $/ - COMMENT "Copying TestModule.so to engine test directory" -) - -# ================================================================================ -# Integration Tests -# ================================================================================ - -# Helpers library (partagée par tous les tests) -add_library(test_helpers STATIC - helpers/TestMetrics.cpp - helpers/TestReporter.cpp - helpers/SystemUtils.cpp - helpers/AutoCompiler.cpp -) - -target_include_directories(test_helpers PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR} -) - -target_link_libraries(test_helpers PUBLIC - GroveEngine::core - spdlog::spdlog -) - -# Set PIC for static library -set_target_properties(test_helpers PROPERTIES POSITION_INDEPENDENT_CODE ON) - -# TankModule pour tests d'intégration -add_library(TankModule SHARED - modules/TankModule.cpp -) - -target_link_libraries(TankModule PRIVATE - GroveEngine::core - GroveEngine::impl - spdlog::spdlog -) - -# Ensure spdlog is compiled with PIC -set_target_properties(spdlog PROPERTIES POSITION_INDEPENDENT_CODE ON) - -# Test 01: Production Hot-Reload -add_executable(test_01_production_hotreload - integration/test_01_production_hotreload.cpp -) - -target_link_libraries(test_01_production_hotreload PRIVATE - test_helpers - GroveEngine::core - GroveEngine::impl -) - -add_dependencies(test_01_production_hotreload TankModule) - -# CTest integration -add_test(NAME ProductionHotReload COMMAND test_01_production_hotreload WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) - -# ChaosModule pour tests de robustesse -add_library(ChaosModule SHARED - modules/ChaosModule.cpp -) - -target_link_libraries(ChaosModule PRIVATE - GroveEngine::core - GroveEngine::impl - spdlog::spdlog -) - -# Test 02: Chaos Monkey -add_executable(test_02_chaos_monkey - integration/test_02_chaos_monkey.cpp -) - -target_link_libraries(test_02_chaos_monkey PRIVATE - test_helpers - GroveEngine::core - GroveEngine::impl -) - -add_dependencies(test_02_chaos_monkey ChaosModule) - -# CTest integration -add_test(NAME ChaosMonkey COMMAND test_02_chaos_monkey WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) - -# StressModule pour tests de stabilité long-terme -add_library(StressModule SHARED - modules/StressModule.cpp -) - -target_link_libraries(StressModule PRIVATE - GroveEngine::core - GroveEngine::impl - spdlog::spdlog -) - -# Test 03: Stress Test - 10 minutes stability -add_executable(test_03_stress_test - integration/test_03_stress_test.cpp -) - -target_link_libraries(test_03_stress_test PRIVATE - test_helpers - GroveEngine::core - GroveEngine::impl -) - -add_dependencies(test_03_stress_test StressModule) - -# CTest integration -add_test(NAME StressTest COMMAND test_03_stress_test WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) - -# Test 04: Race Condition Hunter - Concurrent compilation & reload -add_executable(test_04_race_condition - integration/test_04_race_condition.cpp -) - -target_link_libraries(test_04_race_condition PRIVATE - test_helpers - GroveEngine::core - GroveEngine::impl -) - -# This test uses TestModule (not TankModule) -add_dependencies(test_04_race_condition TestModule) - -# CTest integration -add_test(NAME RaceConditionHunter COMMAND test_04_race_condition WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) - -# LeakTestModule pour memory leak detection -add_library(LeakTestModule SHARED - modules/LeakTestModule.cpp -) - -target_link_libraries(LeakTestModule PRIVATE - GroveEngine::core - GroveEngine::impl -) - -# Test 05: Memory Leak Hunter - 200 reload cycles -add_executable(test_05_memory_leak - integration/test_05_memory_leak.cpp -) - -target_link_libraries(test_05_memory_leak PRIVATE - test_helpers - GroveEngine::core - GroveEngine::impl -) - -add_dependencies(test_05_memory_leak LeakTestModule) - -# CTest integration -add_test(NAME MemoryLeakHunter COMMAND test_05_memory_leak WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) - -# Memory leak profiler (detailed analysis) -# TODO: Implement profile_memory_leak.cpp -# add_executable(profile_memory_leak -# profile_memory_leak.cpp -# ) -# -# target_link_libraries(profile_memory_leak PRIVATE -# test_helpers -# GroveEngine::core -# GroveEngine::impl -# ) -# -# add_dependencies(profile_memory_leak LeakTestModule) - -# ErrorRecoveryModule pour test de recovery automatique -add_library(ErrorRecoveryModule SHARED - modules/ErrorRecoveryModule.cpp -) - -target_link_libraries(ErrorRecoveryModule PRIVATE - GroveEngine::core - GroveEngine::impl - spdlog::spdlog -) - -# Test 06: Error Recovery - Crash detection & auto-recovery -add_executable(test_06_error_recovery - integration/test_06_error_recovery.cpp -) - -target_link_libraries(test_06_error_recovery PRIVATE - test_helpers - GroveEngine::core - GroveEngine::impl -) - -add_dependencies(test_06_error_recovery ErrorRecoveryModule) - -# CTest integration -add_test(NAME ErrorRecovery COMMAND test_06_error_recovery WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) - -# HeavyStateModule pour tests de limites -add_library(HeavyStateModule SHARED - modules/HeavyStateModule.cpp -) - -target_link_libraries(HeavyStateModule PRIVATE - GroveEngine::core - GroveEngine::impl - spdlog::spdlog -) - -# Test 07: Limite Tests - Large state, timeouts, corruption detection -add_executable(test_07_limits - integration/test_07_limits.cpp -) - -target_link_libraries(test_07_limits PRIVATE - test_helpers - GroveEngine::core - GroveEngine::impl -) - -add_dependencies(test_07_limits HeavyStateModule) - -# Test 12: DataNode Integration Test -add_executable(test_12_datanode - integration/test_12_datanode.cpp -) - -target_link_libraries(test_12_datanode PRIVATE - test_helpers - GroveEngine::core - GroveEngine::impl -) - -# CTest integration -add_test(NAME LimitsTest COMMAND test_07_limits WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) -add_test(NAME DataNodeTest COMMAND test_12_datanode WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) - -# ConfigWatcherModule for cross-system integration tests -add_library(ConfigWatcherModule SHARED - modules/ConfigWatcherModule.cpp -) - -target_link_libraries(ConfigWatcherModule PRIVATE - GroveEngine::core - GroveEngine::impl - spdlog::spdlog -) - -# PlayerModule for cross-system integration tests -add_library(PlayerModule SHARED - modules/PlayerModule.cpp -) - -target_link_libraries(PlayerModule PRIVATE - GroveEngine::core - GroveEngine::impl - spdlog::spdlog -) - -# EconomyModule for cross-system integration tests -add_library(EconomyModule SHARED - modules/EconomyModule.cpp -) - -target_link_libraries(EconomyModule PRIVATE - GroveEngine::core - GroveEngine::impl - spdlog::spdlog -) - -# MetricsModule for cross-system integration tests -add_library(MetricsModule SHARED - modules/MetricsModule.cpp -) - -target_link_libraries(MetricsModule PRIVATE - GroveEngine::core - GroveEngine::impl - spdlog::spdlog -) - -# Test 13: Cross-System Integration (IO + DataNode) -add_executable(test_13_cross_system - integration/test_13_cross_system.cpp -) - -target_link_libraries(test_13_cross_system PRIVATE - test_helpers - GroveEngine::core - GroveEngine::impl -) - -add_dependencies(test_13_cross_system - ConfigWatcherModule - PlayerModule - EconomyModule - MetricsModule -) - -# CTest integration -add_test(NAME CrossSystemIntegration COMMAND test_13_cross_system WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) - -# ConfigurableModule pour tests de config hot-reload -add_library(ConfigurableModule SHARED - modules/ConfigurableModule.cpp -) - -target_link_libraries(ConfigurableModule PRIVATE - GroveEngine::core - GroveEngine::impl - spdlog::spdlog -) - -# Test 08: Config Hot-Reload - Runtime config changes without code reload -add_executable(test_08_config_hotreload - integration/test_08_config_hotreload.cpp -) - -target_link_libraries(test_08_config_hotreload PRIVATE - test_helpers - GroveEngine::core - GroveEngine::impl -) - -add_dependencies(test_08_config_hotreload ConfigurableModule) - -# CTest integration -add_test(NAME ConfigHotReload COMMAND test_08_config_hotreload WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) - -# BaseModule for dependency testing (no dependencies) -add_library(BaseModule SHARED - modules/BaseModule.cpp -) - -target_link_libraries(BaseModule PRIVATE - GroveEngine::core - GroveEngine::impl - spdlog::spdlog -) - -# DependentModule for dependency testing (depends on BaseModule) -add_library(DependentModule SHARED - modules/DependentModule.cpp -) - -target_link_libraries(DependentModule PRIVATE - GroveEngine::core - GroveEngine::impl - spdlog::spdlog -) - -# IndependentModule for dependency testing (isolated witness) -add_library(IndependentModule SHARED - modules/IndependentModule.cpp -) - -target_link_libraries(IndependentModule PRIVATE - GroveEngine::core - GroveEngine::impl - spdlog::spdlog -) - -# Test 09: Module Dependencies - Cascade reload, unload protection, cycle detection -add_executable(test_09_module_dependencies - integration/test_09_module_dependencies.cpp -) - -target_link_libraries(test_09_module_dependencies PRIVATE - test_helpers - GroveEngine::core - GroveEngine::impl -) - -add_dependencies(test_09_module_dependencies BaseModule DependentModule IndependentModule) - -# CTest integration -add_test(NAME ModuleDependencies COMMAND test_09_module_dependencies WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) - -# GameLogicModuleV1 for multi-version testing (baseline version) -add_library(GameLogicModuleV1 SHARED - modules/GameLogicModuleV1.cpp -) - -target_link_libraries(GameLogicModuleV1 PRIVATE - GroveEngine::core - GroveEngine::impl - spdlog::spdlog -) - -# GameLogicModuleV2 for multi-version testing (with collision detection) -add_library(GameLogicModuleV2 SHARED - modules/GameLogicModuleV2.cpp -) - -target_link_libraries(GameLogicModuleV2 PRIVATE - GroveEngine::core - GroveEngine::impl - spdlog::spdlog -) - -# GameLogicModuleV3 for multi-version testing (with advanced physics) -add_library(GameLogicModuleV3 SHARED - modules/GameLogicModuleV3.cpp -) - -target_link_libraries(GameLogicModuleV3 PRIVATE - GroveEngine::core - GroveEngine::impl - spdlog::spdlog -) - -# Test 10: Multi-Version Coexistence - Canary deployment, progressive migration, rollback -add_executable(test_10_multiversion_coexistence - integration/test_10_multiversion_coexistence.cpp -) - -target_link_libraries(test_10_multiversion_coexistence PRIVATE - test_helpers - GroveEngine::core - GroveEngine::impl -) - -add_dependencies(test_10_multiversion_coexistence GameLogicModuleV1 GameLogicModuleV2 GameLogicModuleV3) - -# CTest integration -add_test(NAME MultiVersionCoexistence COMMAND test_10_multiversion_coexistence WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) - -# ================================================================================ -# IO System Test Modules (Scenario 11) -# ================================================================================ - -# ProducerModule for IO testing -add_library(ProducerModule SHARED - modules/ProducerModule.cpp -) - -target_link_libraries(ProducerModule PRIVATE - GroveEngine::core - GroveEngine::impl - spdlog::spdlog -) - -# ConsumerModule for IO testing -add_library(ConsumerModule SHARED - modules/ConsumerModule.cpp -) - -target_link_libraries(ConsumerModule PRIVATE - GroveEngine::core - GroveEngine::impl - spdlog::spdlog -) - -# BroadcastModule for IO testing -add_library(BroadcastModule SHARED - modules/BroadcastModule.cpp -) - -target_link_libraries(BroadcastModule PRIVATE - GroveEngine::core - GroveEngine::impl - spdlog::spdlog -) - -# BatchModule for IO testing -add_library(BatchModule SHARED - modules/BatchModule.cpp -) - -target_link_libraries(BatchModule PRIVATE - GroveEngine::core - GroveEngine::impl - spdlog::spdlog -) - -# IOStressModule for IO testing -add_library(IOStressModule SHARED - modules/IOStressModule.cpp -) - -target_link_libraries(IOStressModule PRIVATE - GroveEngine::core - GroveEngine::impl - spdlog::spdlog -) - -# Test 11: IO System Stress Test - IntraIO pub/sub validation -add_executable(test_11_io_system - integration/test_11_io_system.cpp -) - -target_link_libraries(test_11_io_system PRIVATE - test_helpers - GroveEngine::core - GroveEngine::impl -) - -add_dependencies(test_11_io_system ProducerModule ConsumerModule BroadcastModule BatchModule IOStressModule) - -# CTest integration -add_test(NAME IOSystemStress COMMAND test_11_io_system WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) - -# ================================================================================ -# Benchmarks -# ================================================================================ - -# Benchmark helpers demo -add_executable(benchmark_helpers_demo - benchmarks/benchmark_helpers_demo.cpp -) - -target_include_directories(benchmark_helpers_demo PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/benchmarks -) - -target_link_libraries(benchmark_helpers_demo PRIVATE - GroveEngine::core -) - -# TopicTree routing benchmark -add_executable(benchmark_topictree - benchmarks/benchmark_topictree.cpp -) - -target_include_directories(benchmark_topictree PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/benchmarks -) - -target_link_libraries(benchmark_topictree PRIVATE - GroveEngine::core - topictree::topictree -) - -# IntraIO batching benchmark -add_executable(benchmark_batching - benchmarks/benchmark_batching.cpp -) - -target_include_directories(benchmark_batching PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/benchmarks -) - -target_link_libraries(benchmark_batching PRIVATE - GroveEngine::core - GroveEngine::impl - topictree::topictree -) - -# DataNode read-only API benchmark -add_executable(benchmark_readonly - benchmarks/benchmark_readonly.cpp -) - -target_include_directories(benchmark_readonly PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/benchmarks -) - -target_link_libraries(benchmark_readonly PRIVATE - GroveEngine::core - GroveEngine::impl -) - -# End-to-end real world benchmark -add_executable(benchmark_e2e - benchmarks/benchmark_e2e.cpp -) - -target_include_directories(benchmark_e2e PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/benchmarks -) - -target_link_libraries(benchmark_e2e PRIVATE - GroveEngine::core - GroveEngine::impl - topictree::topictree -) - -# ================================================================================ -# BgfxRenderer Tests (only if GROVE_BUILD_BGFX_RENDERER is ON) -# ================================================================================ - -if(GROVE_BUILD_BGFX_RENDERER) - # Test 20: BgfxRenderer RHI Unit Tests (no window required) - add_executable(test_20_bgfx_rhi - integration/test_20_bgfx_rhi.cpp - ../modules/BgfxRenderer/RHI/RHICommandBuffer.cpp - ../modules/BgfxRenderer/Frame/FrameAllocator.cpp - ) - - target_include_directories(test_20_bgfx_rhi PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/../modules/BgfxRenderer - ) - - target_link_libraries(test_20_bgfx_rhi PRIVATE - GroveEngine::core - ) - - # CTest integration - add_test(NAME BgfxRHI COMMAND test_20_bgfx_rhi WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) - - # Test 21: Visual Triangle Test (requires SDL2 and display) - find_package(SDL2 QUIET) - if(SDL2_FOUND OR EXISTS "/usr/include/SDL2/SDL.h") - add_executable(test_21_bgfx_triangle - visual/test_bgfx_triangle.cpp - ) - - target_include_directories(test_21_bgfx_triangle PRIVATE - /usr/include/SDL2 - ) - - target_link_libraries(test_21_bgfx_triangle PRIVATE - bgfx - bx - SDL2 - pthread - dl - X11 - GL - ) - - # Not added to CTest (requires display) - message(STATUS "Visual test 'test_21_bgfx_triangle' enabled (run manually)") - - # Test 22: Sprite Integration Test (requires SDL2, display, and BgfxRenderer module) - add_executable(test_22_bgfx_sprites - visual/test_bgfx_sprites.cpp - ) - - target_include_directories(test_22_bgfx_sprites PRIVATE - /usr/include/SDL2 - ) - - target_link_libraries(test_22_bgfx_sprites PRIVATE - GroveEngine::impl - bgfx - bx - SDL2 - pthread - dl - X11 - ) - - # Not added to CTest (requires display) - message(STATUS "Visual test 'test_22_bgfx_sprites' enabled (run manually)") - - # Test 23: Visual Sprites Test via Module + IIO - add_executable(test_23_bgfx_sprites_visual - visual/test_23_bgfx_sprites_visual.cpp - ) - - target_include_directories(test_23_bgfx_sprites_visual PRIVATE - /usr/include/SDL2 - ) - - target_link_libraries(test_23_bgfx_sprites_visual PRIVATE - GroveEngine::impl - SDL2 - pthread - dl - X11 - ) - - # Not added to CTest (requires display) - message(STATUS "Visual test 'test_23_bgfx_sprites_visual' enabled (run manually)") - - # Test 24: UIModule Visual Test (requires SDL2, display, BgfxRenderer and UIModule) - if(GROVE_BUILD_UI_MODULE) - add_executable(test_24_ui_basic - visual/test_24_ui_basic.cpp - ) - - target_include_directories(test_24_ui_basic PRIVATE - /usr/include/SDL2 - ) - - target_link_libraries(test_24_ui_basic PRIVATE - GroveEngine::impl - SDL2 - pthread - dl - X11 - ) - - # Not added to CTest (requires display) - message(STATUS "Visual test 'test_24_ui_basic' enabled (run manually)") - - # Test 25: UIModule Layout System Test (Phase 2) - add_executable(test_25_ui_layout - visual/test_25_ui_layout.cpp - ) - - target_include_directories(test_25_ui_layout PRIVATE - /usr/include/SDL2 - ) - - target_link_libraries(test_25_ui_layout PRIVATE - GroveEngine::impl - SDL2 - pthread - dl - X11 - ) - - # Not added to CTest (requires display) - message(STATUS "Visual test 'test_25_ui_layout' enabled (run manually)") - - # Test 26: UIModule Interactive Buttons Test (Phase 3) - add_executable(test_26_ui_buttons - visual/test_26_ui_buttons.cpp - ) - - target_include_directories(test_26_ui_buttons PRIVATE - /usr/include/SDL2 - ) - - target_link_libraries(test_26_ui_buttons PRIVATE - GroveEngine::impl - SDL2 - pthread - dl - X11 - ) - - # Not added to CTest (requires display) - message(STATUS "Visual test 'test_26_ui_buttons' enabled (run manually)") - - # Test 28: UIModule ScrollPanel Test (Phase 7.1) - add_executable(test_28_ui_scroll - visual/test_28_ui_scroll.cpp - ) - - target_include_directories(test_28_ui_scroll PRIVATE - /usr/include/SDL2 - ) - - target_link_libraries(test_28_ui_scroll PRIVATE - GroveEngine::impl - SDL2 - pthread - dl - X11 - ) - - # Not added to CTest (requires display) - message(STATUS "Visual test 'test_28_ui_scroll' enabled (run manually)") - - # Test 29: UIModule Advanced Features Test (Phase 7.2 - Tooltips) - add_executable(test_29_ui_advanced - visual/test_29_ui_advanced.cpp - ) - - target_include_directories(test_29_ui_advanced PRIVATE - /usr/include/SDL2 - ) - - target_link_libraries(test_29_ui_advanced PRIVATE - GroveEngine::impl - SDL2 - pthread - dl - X11 - ) - - # Not added to CTest (requires display) - message(STATUS "Visual test 'test_29_ui_advanced' enabled (run manually)") - endif() - - # Test 30: InputModule Visual Test (requires SDL2, display, and InputModule) - if(GROVE_BUILD_INPUT_MODULE) - add_executable(test_30_input_module - visual/test_30_input_module.cpp - ) - - target_include_directories(test_30_input_module PRIVATE - /usr/include/SDL2 - ${CMAKE_SOURCE_DIR}/modules - ) - - target_link_libraries(test_30_input_module PRIVATE - GroveEngine::impl - SDL2 - pthread - dl - X11 - ) - - # Not added to CTest (requires display and user interaction) - message(STATUS "Visual test 'test_30_input_module' enabled (run manually)") - endif() - - # Full Stack Interactive Test (BgfxRenderer + UIModule + InputModule) - if(GROVE_BUILD_INPUT_MODULE AND GROVE_BUILD_UI_MODULE) - add_executable(test_full_stack_interactive - visual/test_full_stack_interactive.cpp - ) - - target_include_directories(test_full_stack_interactive PRIVATE - ${CMAKE_SOURCE_DIR}/modules - ) - - # Platform-specific SDL2 and window system libraries - if(WIN32) - target_link_libraries(test_full_stack_interactive PRIVATE - GroveEngine::impl - SDL2::SDL2 - spdlog::spdlog - ) - else() - target_include_directories(test_full_stack_interactive PRIVATE - /usr/include/SDL2 - ) - target_link_libraries(test_full_stack_interactive PRIVATE - GroveEngine::impl - SDL2 - pthread - dl - X11 - spdlog::spdlog - ) - endif() - - # Not added to CTest (requires display and user interaction) - message(STATUS "Visual test 'test_full_stack_interactive' enabled (BgfxRenderer + UIModule + InputModule)") - endif() - - # Minimal SDL test (for debugging SDL issues on Windows) - add_executable(test_minimal_sdl - visual/test_minimal_sdl.cpp - ) - - # Platform-specific SDL2 linking - if(WIN32) - target_link_libraries(test_minimal_sdl PRIVATE - SDL2::SDL2 - ) - else() - target_include_directories(test_minimal_sdl PRIVATE - /usr/include/SDL2 - ) - target_link_libraries(test_minimal_sdl PRIVATE - SDL2 - ) - endif() - - message(STATUS "Minimal SDL test 'test_minimal_sdl' enabled (debugging tool)") - - # Progressive test (debugging full stack issues) - if(GROVE_BUILD_INPUT_MODULE) - add_executable(test_progressive - visual/test_progressive.cpp - ) - - target_include_directories(test_progressive PRIVATE - ${CMAKE_SOURCE_DIR}/modules - ) - - if(WIN32) - target_link_libraries(test_progressive PRIVATE - GroveEngine::impl - SDL2::SDL2 - spdlog::spdlog - ) - else() - target_include_directories(test_progressive PRIVATE - /usr/include/SDL2 - ) - target_link_libraries(test_progressive PRIVATE - GroveEngine::impl - SDL2 - pthread - dl - spdlog::spdlog - ) - endif() - - message(STATUS "Progressive test 'test_progressive' enabled (debugging tool)") - endif() - else() - message(STATUS "SDL2 not found - visual tests disabled") - endif() - - # Test: GroveEngine Link Test (minimal - just link, don't use) - add_executable(test_groveengine_link - visual/test_groveengine_link.cpp - ) - target_link_libraries(test_groveengine_link PRIVATE - GroveEngine::impl - ) - - # Test: spdlog only (isolate spdlog issues) - add_executable(test_spdlog_only - visual/test_spdlog_only.cpp - ) - target_link_libraries(test_spdlog_only PRIVATE - spdlog::spdlog - ) - - # SDL2-dependent visual tests (debugging tools) - if(SDL2_FOUND) - # Test: Headers progressive (find which header crashes) - add_executable(test_headers_progressive - visual/test_headers_progressive.cpp - ) - target_link_libraries(test_headers_progressive PRIVATE - GroveEngine::impl - SDL2::SDL2 - spdlog::spdlog - ) - - # Test: SDL + GroveEngine linked (same as test_progressive but don't use functions) - add_executable(test_sdl_groveengine - visual/test_sdl_groveengine.cpp - ) - target_link_libraries(test_sdl_groveengine PRIVATE - GroveEngine::impl - SDL2::SDL2 - spdlog::spdlog - ) - - # Test: With modules/ in include directories (like test_progressive) - add_executable(test_with_modules_include - visual/test_with_modules_include.cpp - ) - target_include_directories(test_with_modules_include PRIVATE - ${CMAKE_SOURCE_DIR}/modules - ) - target_link_libraries(test_with_modules_include PRIVATE - GroveEngine::impl - SDL2::SDL2 - spdlog::spdlog - ) - - # Test: Actually USE SDL_Init - add_executable(test_use_sdl - visual/test_use_sdl.cpp - ) - target_link_libraries(test_use_sdl PRIVATE - SDL2::SDL2 - ) - - # Test: USE SDL + IntraIOManager together (like test_progressive) - add_executable(test_use_sdl_and_iio - visual/test_use_sdl_and_iio.cpp - ) - target_link_libraries(test_use_sdl_and_iio PRIVATE - GroveEngine::impl - SDL2::SDL2 - ) - endif() - - # Test: IntraIOManager::getInstance() only (no SDL) - add_executable(test_iio_only - visual/test_iio_only.cpp - ) - target_link_libraries(test_iio_only PRIVATE - GroveEngine::impl - ) - - # Test: Just stillhammer logger (no GroveEngine) - add_executable(test_logger_only - visual/test_logger_only.cpp - ) - target_link_libraries(test_logger_only PRIVATE - stillhammer_logger - ) - - # Test: Just include - add_executable(test_filesystem - visual/test_filesystem.cpp - ) - - # Test: spdlog with register_logger - add_executable(test_spdlog_register - visual/test_spdlog_register.cpp - ) - target_link_libraries(test_spdlog_register PRIVATE - spdlog::spdlog - ) - - # Test: Logger.cpp compiled directly (not as library) - add_executable(test_logger_direct - visual/test_logger_direct.cpp - ${CMAKE_SOURCE_DIR}/external/StillHammer/logger/src/Logger.cpp - ) - target_include_directories(test_logger_direct PRIVATE - ${CMAKE_SOURCE_DIR}/external/StillHammer/logger/include - ) - target_link_libraries(test_logger_direct PRIVATE - spdlog::spdlog - ) - - # Test: spdlog + filesystem combined - add_executable(test_spdlog_filesystem - visual/test_spdlog_filesystem.cpp - ) - target_link_libraries(test_spdlog_filesystem PRIVATE - spdlog::spdlog - ) - - # Test 22b: Headless sprite integration test (no display required) - add_executable(test_22_bgfx_sprites_headless - integration/test_22_bgfx_sprites_headless.cpp - ) - - target_link_libraries(test_22_bgfx_sprites_headless PRIVATE - GroveEngine::impl - Catch2::Catch2WithMain - ) - - # ======================================== - # Phase 6.5 Sprint 3: Pipeline Headless Tests - # ======================================== - - # Test: Pipeline Headless - End-to-end rendering flow - add_executable(test_pipeline_headless - integration/test_pipeline_headless.cpp - ../modules/BgfxRenderer/Scene/SceneCollector.cpp - ../modules/BgfxRenderer/Frame/FrameAllocator.cpp - ../modules/BgfxRenderer/RenderGraph/RenderGraph.cpp - ../modules/BgfxRenderer/RHI/RHICommandBuffer.cpp - ) - target_include_directories(test_pipeline_headless PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/../modules/BgfxRenderer - ${CMAKE_CURRENT_SOURCE_DIR} - ) - target_link_libraries(test_pipeline_headless PRIVATE - GroveEngine::impl - - Catch2::Catch2WithMain - ) - add_test(NAME PipelineHeadless COMMAND test_pipeline_headless WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) - - add_test(NAME BgfxSpritesHeadless COMMAND test_22_bgfx_sprites_headless WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) -endif() - -# ================================================================================ -# Phase 5 Integration Tests - UIModule -# ================================================================================ - -# TestControllerModule - Simulates game logic for UI integration tests -add_library(TestControllerModule SHARED - modules/TestControllerModule.cpp -) - -target_link_libraries(TestControllerModule PRIVATE - GroveEngine::core - GroveEngine::impl - spdlog::spdlog -) - -# IT_014: UIModule Full Integration Test -if(GROVE_BUILD_UI_MODULE AND GROVE_BUILD_BGFX_RENDERER) - add_executable(IT_014_ui_module_integration - integration/IT_014_ui_module_integration.cpp - ) - - target_link_libraries(IT_014_ui_module_integration PRIVATE - test_helpers - GroveEngine::core - GroveEngine::impl - Catch2::Catch2WithMain - ) - - add_dependencies(IT_014_ui_module_integration TestControllerModule) - - # CTest integration - add_test(NAME UIModuleIntegration COMMAND IT_014_ui_module_integration WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) - - message(STATUS "Integration test 'IT_014_ui_module_integration' enabled") -endif() - -# IT_015: InputModule + UIModule Integration Test -if(GROVE_BUILD_UI_MODULE) - # IT_015: Simplified UIModule input integration test (no InputModule dependency) - # This test publishes IIO messages directly to test UIModule input processing - add_executable(IT_015_input_ui_integration - integration/IT_015_input_ui_integration.cpp - ) - - target_link_libraries(IT_015_input_ui_integration PRIVATE - test_helpers - GroveEngine::core - GroveEngine::impl - Catch2::Catch2WithMain - ) - - # CTest integration - add_test(NAME InputUIIntegration COMMAND IT_015_input_ui_integration WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) - - message(STATUS "Integration test 'IT_015_input_ui_integration' enabled (simplified, no SDL2)") -endif() - -# IT_015_Minimal: IIO-only integration test (no module loading, no DLL issues) -add_executable(IT_015_input_ui_integration_minimal - integration/IT_015_input_ui_integration_minimal.cpp -) - -target_link_libraries(IT_015_input_ui_integration_minimal PRIVATE - test_helpers - GroveEngine::core - GroveEngine::impl - Catch2::Catch2WithMain -) - -# CTest integration -add_test(NAME InputUIIntegration_Minimal COMMAND IT_015_input_ui_integration_minimal WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) - -message(STATUS "Integration test 'IT_015_input_ui_integration_minimal' enabled (IIO-only)") - -# ============================================ -# UIModule Interactive Showcase Demo -# ============================================ - -if(GROVE_BUILD_UI_MODULE AND GROVE_BUILD_BGFX_RENDERER) - add_executable(demo_ui_showcase - demo/demo_ui_showcase.cpp - ) - - target_link_libraries(demo_ui_showcase PRIVATE - GroveEngine::core - GroveEngine::impl - SDL2 - pthread - dl - ) - - # Add X11 on Linux for SDL window integration - if(UNIX AND NOT APPLE) - target_link_libraries(demo_ui_showcase PRIVATE X11) - endif() - - message(STATUS "UIModule showcase demo 'demo_ui_showcase' enabled") -endif() +# Hot-reload test suite + +# Test module as shared library (.so) for hot-reload +add_library(TestModule SHARED + modules/TestModule.cpp +) + +target_link_libraries(TestModule PRIVATE + GroveEngine::core + GroveEngine::impl # For JsonDataNode implementation +) + +# Don't add "lib" prefix on Linux (we want TestModule.so, not libTestModule.so) +set_target_properties(TestModule PROPERTIES PREFIX "lib") +set_target_properties(TestModule PROPERTIES OUTPUT_NAME "TestModule") + +# Basic hot-reload test executable (manual dlopen/dlclose) +add_executable(test_hotreload + hotreload/test_hotreload.cpp +) + +target_link_libraries(test_hotreload PRIVATE + GroveEngine::core + GroveEngine::impl # For JsonDataNode implementation + ${CMAKE_DL_LIBS} # For dlopen/dlclose +) + +# Make sure test module is built before test executable +add_dependencies(test_hotreload TestModule) + +# Copy test module to test executable directory after build +add_custom_command(TARGET test_hotreload POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + $ + $/ + COMMENT "Copying TestModule.so to test directory" +) + +# Engine hot-reload test (uses DebugEngine + SequentialModuleSystem + FileWatcher) +add_executable(test_engine_hotreload + hotreload/test_engine_hotreload.cpp +) + +target_link_libraries(test_engine_hotreload PRIVATE + GroveEngine::core + GroveEngine::impl + ${CMAKE_DL_LIBS} +) + +add_dependencies(test_engine_hotreload TestModule) + +add_custom_command(TARGET test_engine_hotreload POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + $ + $/ + COMMENT "Copying TestModule.so to engine test directory" +) + +# ================================================================================ +# Integration Tests +# ================================================================================ + + +# ================================================================================ +# Windows/MinGW: CTest helper for DLL loading +# ================================================================================ +# Copy MinGW runtime DLLs at configure time +if(WIN32 AND MINGW) + get_filename_component(MINGW_BIN_DIR ${CMAKE_CXX_COMPILER} DIRECTORY) + set(MINGW_DLLS libgcc_s_seh-1.dll libstdc++-6.dll libwinpthread-1.dll) + foreach(DLL ${MINGW_DLLS}) + if(EXISTS "${MINGW_BIN_DIR}/${DLL}") + file(COPY "${MINGW_BIN_DIR}/${DLL}" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}") + endif() + endforeach() +endif() + +# Test macro - uses cmake -E chdir on Windows to ensure proper DLL loading +macro(grove_add_test test_name test_target working_dir) + if(WIN32 AND MINGW) + add_test(NAME ${test_name} + COMMAND ${CMAKE_COMMAND} -E chdir ${working_dir} $) + else() + add_test(NAME ${test_name} COMMAND ${test_target} WORKING_DIRECTORY ${working_dir}) + endif() +endmacro() + + + +# Helpers library (partagée par tous les tests) +add_library(test_helpers STATIC + helpers/TestMetrics.cpp + helpers/TestReporter.cpp + helpers/SystemUtils.cpp + helpers/AutoCompiler.cpp +) + +target_include_directories(test_helpers PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_link_libraries(test_helpers PUBLIC + GroveEngine::core + spdlog::spdlog +) + +# Set PIC for static library +set_target_properties(test_helpers PROPERTIES POSITION_INDEPENDENT_CODE ON) + +# TankModule pour tests d'intégration +add_library(TankModule SHARED + modules/TankModule.cpp +) + +target_link_libraries(TankModule PRIVATE + GroveEngine::core + GroveEngine::impl + spdlog::spdlog +) + +# Ensure spdlog is compiled with PIC +set_target_properties(spdlog PROPERTIES POSITION_INDEPENDENT_CODE ON) + +# Test 01: Production Hot-Reload +add_executable(test_01_production_hotreload + integration/test_01_production_hotreload.cpp +) + +target_link_libraries(test_01_production_hotreload PRIVATE + test_helpers + GroveEngine::core + GroveEngine::impl +) + +add_dependencies(test_01_production_hotreload TankModule) + +# CTest integration +grove_add_test(ProductionHotReload test_01_production_hotreload ${CMAKE_CURRENT_BINARY_DIR}) + +# ChaosModule pour tests de robustesse +add_library(ChaosModule SHARED + modules/ChaosModule.cpp +) + +target_link_libraries(ChaosModule PRIVATE + GroveEngine::core + GroveEngine::impl + spdlog::spdlog +) + +# Test 02: Chaos Monkey +add_executable(test_02_chaos_monkey + integration/test_02_chaos_monkey.cpp +) + +target_link_libraries(test_02_chaos_monkey PRIVATE + test_helpers + GroveEngine::core + GroveEngine::impl +) + +add_dependencies(test_02_chaos_monkey ChaosModule) + +# CTest integration +grove_add_test(ChaosMonkey test_02_chaos_monkey ${CMAKE_CURRENT_BINARY_DIR}) + +# StressModule pour tests de stabilité long-terme +add_library(StressModule SHARED + modules/StressModule.cpp +) + +target_link_libraries(StressModule PRIVATE + GroveEngine::core + GroveEngine::impl + spdlog::spdlog +) + +# Test 03: Stress Test - 10 minutes stability +add_executable(test_03_stress_test + integration/test_03_stress_test.cpp +) + +target_link_libraries(test_03_stress_test PRIVATE + test_helpers + GroveEngine::core + GroveEngine::impl +) + +add_dependencies(test_03_stress_test StressModule) + +# CTest integration +grove_add_test(StressTest test_03_stress_test ${CMAKE_CURRENT_BINARY_DIR}) + +# Test 04: Race Condition Hunter - Concurrent compilation & reload +add_executable(test_04_race_condition + integration/test_04_race_condition.cpp +) + +target_link_libraries(test_04_race_condition PRIVATE + test_helpers + GroveEngine::core + GroveEngine::impl +) + +# This test uses TestModule (not TankModule) +add_dependencies(test_04_race_condition TestModule) + +# CTest integration +grove_add_test(RaceConditionHunter test_04_race_condition ${CMAKE_CURRENT_BINARY_DIR}) + +# LeakTestModule pour memory leak detection +add_library(LeakTestModule SHARED + modules/LeakTestModule.cpp +) + +target_link_libraries(LeakTestModule PRIVATE + GroveEngine::core + GroveEngine::impl +) + +# Test 05: Memory Leak Hunter - 200 reload cycles +add_executable(test_05_memory_leak + integration/test_05_memory_leak.cpp +) + +target_link_libraries(test_05_memory_leak PRIVATE + test_helpers + GroveEngine::core + GroveEngine::impl +) + +add_dependencies(test_05_memory_leak LeakTestModule) + +# CTest integration +grove_add_test(MemoryLeakHunter test_05_memory_leak ${CMAKE_CURRENT_BINARY_DIR}) + +# Memory leak profiler (detailed analysis) +# TODO: Implement profile_memory_leak.cpp +# add_executable(profile_memory_leak +# profile_memory_leak.cpp +# ) +# +# target_link_libraries(profile_memory_leak PRIVATE +# test_helpers +# GroveEngine::core +# GroveEngine::impl +# ) +# +# add_dependencies(profile_memory_leak LeakTestModule) + +# ErrorRecoveryModule pour test de recovery automatique +add_library(ErrorRecoveryModule SHARED + modules/ErrorRecoveryModule.cpp +) + +target_link_libraries(ErrorRecoveryModule PRIVATE + GroveEngine::core + GroveEngine::impl + spdlog::spdlog +) + +# Test 06: Error Recovery - Crash detection & auto-recovery +add_executable(test_06_error_recovery + integration/test_06_error_recovery.cpp +) + +target_link_libraries(test_06_error_recovery PRIVATE + test_helpers + GroveEngine::core + GroveEngine::impl +) + +add_dependencies(test_06_error_recovery ErrorRecoveryModule) + +# CTest integration +grove_add_test(ErrorRecovery test_06_error_recovery ${CMAKE_CURRENT_BINARY_DIR}) + +# HeavyStateModule pour tests de limites +add_library(HeavyStateModule SHARED + modules/HeavyStateModule.cpp +) + +target_link_libraries(HeavyStateModule PRIVATE + GroveEngine::core + GroveEngine::impl + spdlog::spdlog +) + +# Test 07: Limite Tests - Large state, timeouts, corruption detection +add_executable(test_07_limits + integration/test_07_limits.cpp +) + +target_link_libraries(test_07_limits PRIVATE + test_helpers + GroveEngine::core + GroveEngine::impl +) + +add_dependencies(test_07_limits HeavyStateModule) + +# Test 12: DataNode Integration Test +add_executable(test_12_datanode + integration/test_12_datanode.cpp +) + +target_link_libraries(test_12_datanode PRIVATE + test_helpers + GroveEngine::core + GroveEngine::impl +) + +# CTest integration +grove_add_test(LimitsTest test_07_limits ${CMAKE_CURRENT_BINARY_DIR}) +grove_add_test(DataNodeTest test_12_datanode ${CMAKE_CURRENT_BINARY_DIR}) + +# ConfigWatcherModule for cross-system integration tests +add_library(ConfigWatcherModule SHARED + modules/ConfigWatcherModule.cpp +) + +target_link_libraries(ConfigWatcherModule PRIVATE + GroveEngine::core + GroveEngine::impl + spdlog::spdlog +) + +# PlayerModule for cross-system integration tests +add_library(PlayerModule SHARED + modules/PlayerModule.cpp +) + +target_link_libraries(PlayerModule PRIVATE + GroveEngine::core + GroveEngine::impl + spdlog::spdlog +) + +# EconomyModule for cross-system integration tests +add_library(EconomyModule SHARED + modules/EconomyModule.cpp +) + +target_link_libraries(EconomyModule PRIVATE + GroveEngine::core + GroveEngine::impl + spdlog::spdlog +) + +# MetricsModule for cross-system integration tests +add_library(MetricsModule SHARED + modules/MetricsModule.cpp +) + +target_link_libraries(MetricsModule PRIVATE + GroveEngine::core + GroveEngine::impl + spdlog::spdlog +) + +# Test 13: Cross-System Integration (IO + DataNode) +add_executable(test_13_cross_system + integration/test_13_cross_system.cpp +) + +target_link_libraries(test_13_cross_system PRIVATE + test_helpers + GroveEngine::core + GroveEngine::impl +) + +add_dependencies(test_13_cross_system + ConfigWatcherModule + PlayerModule + EconomyModule + MetricsModule +) + +# CTest integration +grove_add_test(CrossSystemIntegration test_13_cross_system ${CMAKE_CURRENT_BINARY_DIR}) + +# ConfigurableModule pour tests de config hot-reload +add_library(ConfigurableModule SHARED + modules/ConfigurableModule.cpp +) + +target_link_libraries(ConfigurableModule PRIVATE + GroveEngine::core + GroveEngine::impl + spdlog::spdlog +) + +# Test 08: Config Hot-Reload - Runtime config changes without code reload +add_executable(test_08_config_hotreload + integration/test_08_config_hotreload.cpp +) + +target_link_libraries(test_08_config_hotreload PRIVATE + test_helpers + GroveEngine::core + GroveEngine::impl +) + +add_dependencies(test_08_config_hotreload ConfigurableModule) + +# CTest integration +grove_add_test(ConfigHotReload test_08_config_hotreload ${CMAKE_CURRENT_BINARY_DIR}) + +# BaseModule for dependency testing (no dependencies) +add_library(BaseModule SHARED + modules/BaseModule.cpp +) + +target_link_libraries(BaseModule PRIVATE + GroveEngine::core + GroveEngine::impl + spdlog::spdlog +) + +# DependentModule for dependency testing (depends on BaseModule) +add_library(DependentModule SHARED + modules/DependentModule.cpp +) + +target_link_libraries(DependentModule PRIVATE + GroveEngine::core + GroveEngine::impl + spdlog::spdlog +) + +# IndependentModule for dependency testing (isolated witness) +add_library(IndependentModule SHARED + modules/IndependentModule.cpp +) + +target_link_libraries(IndependentModule PRIVATE + GroveEngine::core + GroveEngine::impl + spdlog::spdlog +) + +# Test 09: Module Dependencies - Cascade reload, unload protection, cycle detection +add_executable(test_09_module_dependencies + integration/test_09_module_dependencies.cpp +) + +target_link_libraries(test_09_module_dependencies PRIVATE + test_helpers + GroveEngine::core + GroveEngine::impl +) + +add_dependencies(test_09_module_dependencies BaseModule DependentModule IndependentModule) + +# CTest integration +grove_add_test(ModuleDependencies test_09_module_dependencies ${CMAKE_CURRENT_BINARY_DIR}) + +# GameLogicModuleV1 for multi-version testing (baseline version) +add_library(GameLogicModuleV1 SHARED + modules/GameLogicModuleV1.cpp +) + +target_link_libraries(GameLogicModuleV1 PRIVATE + GroveEngine::core + GroveEngine::impl + spdlog::spdlog +) + +# GameLogicModuleV2 for multi-version testing (with collision detection) +add_library(GameLogicModuleV2 SHARED + modules/GameLogicModuleV2.cpp +) + +target_link_libraries(GameLogicModuleV2 PRIVATE + GroveEngine::core + GroveEngine::impl + spdlog::spdlog +) + +# GameLogicModuleV3 for multi-version testing (with advanced physics) +add_library(GameLogicModuleV3 SHARED + modules/GameLogicModuleV3.cpp +) + +target_link_libraries(GameLogicModuleV3 PRIVATE + GroveEngine::core + GroveEngine::impl + spdlog::spdlog +) + +# Test 10: Multi-Version Coexistence - Canary deployment, progressive migration, rollback +add_executable(test_10_multiversion_coexistence + integration/test_10_multiversion_coexistence.cpp +) + +target_link_libraries(test_10_multiversion_coexistence PRIVATE + test_helpers + GroveEngine::core + GroveEngine::impl +) + +add_dependencies(test_10_multiversion_coexistence GameLogicModuleV1 GameLogicModuleV2 GameLogicModuleV3) + +# CTest integration +grove_add_test(MultiVersionCoexistence test_10_multiversion_coexistence ${CMAKE_CURRENT_BINARY_DIR}) + +# ================================================================================ +# IO System Test Modules (Scenario 11) +# ================================================================================ + +# ProducerModule for IO testing +add_library(ProducerModule SHARED + modules/ProducerModule.cpp +) + +target_link_libraries(ProducerModule PRIVATE + GroveEngine::core + GroveEngine::impl + spdlog::spdlog +) + +# ConsumerModule for IO testing +add_library(ConsumerModule SHARED + modules/ConsumerModule.cpp +) + +target_link_libraries(ConsumerModule PRIVATE + GroveEngine::core + GroveEngine::impl + spdlog::spdlog +) + +# BroadcastModule for IO testing +add_library(BroadcastModule SHARED + modules/BroadcastModule.cpp +) + +target_link_libraries(BroadcastModule PRIVATE + GroveEngine::core + GroveEngine::impl + spdlog::spdlog +) + +# BatchModule for IO testing +add_library(BatchModule SHARED + modules/BatchModule.cpp +) + +target_link_libraries(BatchModule PRIVATE + GroveEngine::core + GroveEngine::impl + spdlog::spdlog +) + +# IOStressModule for IO testing +add_library(IOStressModule SHARED + modules/IOStressModule.cpp +) + +target_link_libraries(IOStressModule PRIVATE + GroveEngine::core + GroveEngine::impl + spdlog::spdlog +) + +# Test 11: IO System Stress Test - IntraIO pub/sub validation +add_executable(test_11_io_system + integration/test_11_io_system.cpp +) + +target_link_libraries(test_11_io_system PRIVATE + test_helpers + GroveEngine::core + GroveEngine::impl +) + +add_dependencies(test_11_io_system ProducerModule ConsumerModule BroadcastModule BatchModule IOStressModule) + +# CTest integration +grove_add_test(IOSystemStress test_11_io_system ${CMAKE_CURRENT_BINARY_DIR}) + +# ================================================================================ +# Benchmarks +# ================================================================================ + +# Benchmark helpers demo +add_executable(benchmark_helpers_demo + benchmarks/benchmark_helpers_demo.cpp +) + +target_include_directories(benchmark_helpers_demo PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/benchmarks +) + +target_link_libraries(benchmark_helpers_demo PRIVATE + GroveEngine::core +) + +# TopicTree routing benchmark +add_executable(benchmark_topictree + benchmarks/benchmark_topictree.cpp +) + +target_include_directories(benchmark_topictree PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/benchmarks +) + +target_link_libraries(benchmark_topictree PRIVATE + GroveEngine::core + topictree::topictree +) + +# IntraIO batching benchmark +add_executable(benchmark_batching + benchmarks/benchmark_batching.cpp +) + +target_include_directories(benchmark_batching PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/benchmarks +) + +target_link_libraries(benchmark_batching PRIVATE + GroveEngine::core + GroveEngine::impl + topictree::topictree +) + +# DataNode read-only API benchmark +add_executable(benchmark_readonly + benchmarks/benchmark_readonly.cpp +) + +target_include_directories(benchmark_readonly PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/benchmarks +) + +target_link_libraries(benchmark_readonly PRIVATE + GroveEngine::core + GroveEngine::impl +) + +# End-to-end real world benchmark +add_executable(benchmark_e2e + benchmarks/benchmark_e2e.cpp +) + +target_include_directories(benchmark_e2e PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/benchmarks +) + +target_link_libraries(benchmark_e2e PRIVATE + GroveEngine::core + GroveEngine::impl + topictree::topictree +) + +# ================================================================================ +# BgfxRenderer Tests (only if GROVE_BUILD_BGFX_RENDERER is ON) +# ================================================================================ + +if(GROVE_BUILD_BGFX_RENDERER) + # Test 20: BgfxRenderer RHI Unit Tests (no window required) + add_executable(test_20_bgfx_rhi + integration/test_20_bgfx_rhi.cpp + ../modules/BgfxRenderer/RHI/RHICommandBuffer.cpp + ../modules/BgfxRenderer/Frame/FrameAllocator.cpp + ) + + target_include_directories(test_20_bgfx_rhi PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../modules/BgfxRenderer + ) + + target_link_libraries(test_20_bgfx_rhi PRIVATE + GroveEngine::core + ) + + # CTest integration + grove_add_test(BgfxRHI test_20_bgfx_rhi ${CMAKE_CURRENT_BINARY_DIR}) + + # Test 21: Visual Triangle Test (requires SDL2 and display) + find_package(SDL2 QUIET) + if(SDL2_FOUND OR EXISTS "/usr/include/SDL2/SDL.h") + add_executable(test_21_bgfx_triangle + visual/test_bgfx_triangle.cpp + ) + + target_include_directories(test_21_bgfx_triangle PRIVATE + /usr/include/SDL2 + ) + + target_link_libraries(test_21_bgfx_triangle PRIVATE + bgfx + bx + SDL2 + pthread + dl + X11 + GL + ) + + # Not added to CTest (requires display) + message(STATUS "Visual test 'test_21_bgfx_triangle' enabled (run manually)") + + # Test 22: Sprite Integration Test (requires SDL2, display, and BgfxRenderer module) + add_executable(test_22_bgfx_sprites + visual/test_bgfx_sprites.cpp + ) + + target_include_directories(test_22_bgfx_sprites PRIVATE + /usr/include/SDL2 + ) + + target_link_libraries(test_22_bgfx_sprites PRIVATE + GroveEngine::impl + bgfx + bx + SDL2 + pthread + dl + X11 + ) + + # Not added to CTest (requires display) + message(STATUS "Visual test 'test_22_bgfx_sprites' enabled (run manually)") + + # Test 23: Visual Sprites Test via Module + IIO + add_executable(test_23_bgfx_sprites_visual + visual/test_23_bgfx_sprites_visual.cpp + ) + + target_include_directories(test_23_bgfx_sprites_visual PRIVATE + /usr/include/SDL2 + ) + + target_link_libraries(test_23_bgfx_sprites_visual PRIVATE + GroveEngine::impl + SDL2 + pthread + dl + X11 + ) + + # Not added to CTest (requires display) + message(STATUS "Visual test 'test_23_bgfx_sprites_visual' enabled (run manually)") + + # Test 24: UIModule Visual Test (requires SDL2, display, BgfxRenderer and UIModule) + if(GROVE_BUILD_UI_MODULE) + add_executable(test_24_ui_basic + visual/test_24_ui_basic.cpp + ) + + target_include_directories(test_24_ui_basic PRIVATE + /usr/include/SDL2 + ) + + target_link_libraries(test_24_ui_basic PRIVATE + GroveEngine::impl + SDL2 + pthread + dl + X11 + ) + + # Not added to CTest (requires display) + message(STATUS "Visual test 'test_24_ui_basic' enabled (run manually)") + + # Test 25: UIModule Layout System Test (Phase 2) + add_executable(test_25_ui_layout + visual/test_25_ui_layout.cpp + ) + + target_include_directories(test_25_ui_layout PRIVATE + /usr/include/SDL2 + ) + + target_link_libraries(test_25_ui_layout PRIVATE + GroveEngine::impl + SDL2 + pthread + dl + X11 + ) + + # Not added to CTest (requires display) + message(STATUS "Visual test 'test_25_ui_layout' enabled (run manually)") + + # Test 26: UIModule Interactive Buttons Test (Phase 3) + add_executable(test_26_ui_buttons + visual/test_26_ui_buttons.cpp + ) + + target_include_directories(test_26_ui_buttons PRIVATE + /usr/include/SDL2 + ) + + target_link_libraries(test_26_ui_buttons PRIVATE + GroveEngine::impl + SDL2 + pthread + dl + X11 + ) + + # Not added to CTest (requires display) + message(STATUS "Visual test 'test_26_ui_buttons' enabled (run manually)") + + # Test 28: UIModule ScrollPanel Test (Phase 7.1) + add_executable(test_28_ui_scroll + visual/test_28_ui_scroll.cpp + ) + + target_include_directories(test_28_ui_scroll PRIVATE + /usr/include/SDL2 + ) + + target_link_libraries(test_28_ui_scroll PRIVATE + GroveEngine::impl + SDL2 + pthread + dl + X11 + ) + + # Not added to CTest (requires display) + message(STATUS "Visual test 'test_28_ui_scroll' enabled (run manually)") + + # Test 29: UIModule Advanced Features Test (Phase 7.2 - Tooltips) + add_executable(test_29_ui_advanced + visual/test_29_ui_advanced.cpp + ) + + target_include_directories(test_29_ui_advanced PRIVATE + /usr/include/SDL2 + ) + + target_link_libraries(test_29_ui_advanced PRIVATE + GroveEngine::impl + SDL2 + pthread + dl + X11 + ) + + # Not added to CTest (requires display) + message(STATUS "Visual test 'test_29_ui_advanced' enabled (run manually)") + endif() + + # Test 30: InputModule Visual Test (requires SDL2, display, and InputModule) + if(GROVE_BUILD_INPUT_MODULE) + add_executable(test_30_input_module + visual/test_30_input_module.cpp + ) + + target_include_directories(test_30_input_module PRIVATE + /usr/include/SDL2 + ${CMAKE_SOURCE_DIR}/modules + ) + + target_link_libraries(test_30_input_module PRIVATE + GroveEngine::impl + SDL2 + pthread + dl + X11 + ) + + # Not added to CTest (requires display and user interaction) + message(STATUS "Visual test 'test_30_input_module' enabled (run manually)") + endif() + + # Full Stack Interactive Test (BgfxRenderer + UIModule + InputModule) + if(GROVE_BUILD_INPUT_MODULE AND GROVE_BUILD_UI_MODULE) + add_executable(test_full_stack_interactive + visual/test_full_stack_interactive.cpp + ) + + target_include_directories(test_full_stack_interactive PRIVATE + ${CMAKE_SOURCE_DIR}/modules + ) + + # Platform-specific SDL2 and window system libraries + if(WIN32) + target_link_libraries(test_full_stack_interactive PRIVATE + GroveEngine::impl + SDL2::SDL2 + spdlog::spdlog + ) + else() + target_include_directories(test_full_stack_interactive PRIVATE + /usr/include/SDL2 + ) + target_link_libraries(test_full_stack_interactive PRIVATE + GroveEngine::impl + SDL2 + pthread + dl + X11 + spdlog::spdlog + ) + endif() + + # Not added to CTest (requires display and user interaction) + message(STATUS "Visual test 'test_full_stack_interactive' enabled (BgfxRenderer + UIModule + InputModule)") + endif() + + # Minimal SDL test (for debugging SDL issues on Windows) + add_executable(test_minimal_sdl + visual/test_minimal_sdl.cpp + ) + + # Platform-specific SDL2 linking + if(WIN32) + target_link_libraries(test_minimal_sdl PRIVATE + SDL2::SDL2 + ) + else() + target_include_directories(test_minimal_sdl PRIVATE + /usr/include/SDL2 + ) + target_link_libraries(test_minimal_sdl PRIVATE + SDL2 + ) + endif() + + message(STATUS "Minimal SDL test 'test_minimal_sdl' enabled (debugging tool)") + + # Progressive test (debugging full stack issues) + if(GROVE_BUILD_INPUT_MODULE) + add_executable(test_progressive + visual/test_progressive.cpp + ) + + target_include_directories(test_progressive PRIVATE + ${CMAKE_SOURCE_DIR}/modules + ) + + if(WIN32) + target_link_libraries(test_progressive PRIVATE + GroveEngine::impl + SDL2::SDL2 + spdlog::spdlog + ) + else() + target_include_directories(test_progressive PRIVATE + /usr/include/SDL2 + ) + target_link_libraries(test_progressive PRIVATE + GroveEngine::impl + SDL2 + pthread + dl + spdlog::spdlog + ) + endif() + + message(STATUS "Progressive test 'test_progressive' enabled (debugging tool)") + endif() + else() + message(STATUS "SDL2 not found - visual tests disabled") + endif() + + # Test: GroveEngine Link Test (minimal - just link, don't use) + add_executable(test_groveengine_link + visual/test_groveengine_link.cpp + ) + target_link_libraries(test_groveengine_link PRIVATE + GroveEngine::impl + ) + + # Test: spdlog only (isolate spdlog issues) + add_executable(test_spdlog_only + visual/test_spdlog_only.cpp + ) + target_link_libraries(test_spdlog_only PRIVATE + spdlog::spdlog + ) + + # SDL2-dependent visual tests (debugging tools) + if(SDL2_FOUND) + # Test: Headers progressive (find which header crashes) + add_executable(test_headers_progressive + visual/test_headers_progressive.cpp + ) + target_link_libraries(test_headers_progressive PRIVATE + GroveEngine::impl + SDL2::SDL2 + spdlog::spdlog + ) + + # Test: SDL + GroveEngine linked (same as test_progressive but don't use functions) + add_executable(test_sdl_groveengine + visual/test_sdl_groveengine.cpp + ) + target_link_libraries(test_sdl_groveengine PRIVATE + GroveEngine::impl + SDL2::SDL2 + spdlog::spdlog + ) + + # Test: With modules/ in include directories (like test_progressive) + add_executable(test_with_modules_include + visual/test_with_modules_include.cpp + ) + target_include_directories(test_with_modules_include PRIVATE + ${CMAKE_SOURCE_DIR}/modules + ) + target_link_libraries(test_with_modules_include PRIVATE + GroveEngine::impl + SDL2::SDL2 + spdlog::spdlog + ) + + # Test: Actually USE SDL_Init + add_executable(test_use_sdl + visual/test_use_sdl.cpp + ) + target_link_libraries(test_use_sdl PRIVATE + SDL2::SDL2 + ) + + # Test: USE SDL + IntraIOManager together (like test_progressive) + add_executable(test_use_sdl_and_iio + visual/test_use_sdl_and_iio.cpp + ) + target_link_libraries(test_use_sdl_and_iio PRIVATE + GroveEngine::impl + SDL2::SDL2 + ) + endif() + + # Test: IntraIOManager::getInstance() only (no SDL) + add_executable(test_iio_only + visual/test_iio_only.cpp + ) + target_link_libraries(test_iio_only PRIVATE + GroveEngine::impl + ) + + # Test: Just stillhammer logger (no GroveEngine) + add_executable(test_logger_only + visual/test_logger_only.cpp + ) + target_link_libraries(test_logger_only PRIVATE + stillhammer_logger + ) + + # Test: Just include + add_executable(test_filesystem + visual/test_filesystem.cpp + ) + + # Test: spdlog with register_logger + add_executable(test_spdlog_register + visual/test_spdlog_register.cpp + ) + target_link_libraries(test_spdlog_register PRIVATE + spdlog::spdlog + ) + + # Test: Logger.cpp compiled directly (not as library) + add_executable(test_logger_direct + visual/test_logger_direct.cpp + ${CMAKE_SOURCE_DIR}/external/StillHammer/logger/src/Logger.cpp + ) + target_include_directories(test_logger_direct PRIVATE + ${CMAKE_SOURCE_DIR}/external/StillHammer/logger/include + ) + target_link_libraries(test_logger_direct PRIVATE + spdlog::spdlog + ) + + # Test: spdlog + filesystem combined + add_executable(test_spdlog_filesystem + visual/test_spdlog_filesystem.cpp + ) + target_link_libraries(test_spdlog_filesystem PRIVATE + spdlog::spdlog + ) + + # Test 22b: Headless sprite integration test (no display required) + add_executable(test_22_bgfx_sprites_headless + integration/test_22_bgfx_sprites_headless.cpp + ) + + target_link_libraries(test_22_bgfx_sprites_headless PRIVATE + GroveEngine::impl + Catch2::Catch2WithMain + ) + + # ======================================== + # Phase 6.5 Sprint 3: Pipeline Headless Tests + # ======================================== + + # Test: Pipeline Headless - End-to-end rendering flow + add_executable(test_pipeline_headless + integration/test_pipeline_headless.cpp + ../modules/BgfxRenderer/Scene/SceneCollector.cpp + ../modules/BgfxRenderer/Frame/FrameAllocator.cpp + ../modules/BgfxRenderer/RenderGraph/RenderGraph.cpp + ../modules/BgfxRenderer/RHI/RHICommandBuffer.cpp + ) + target_include_directories(test_pipeline_headless PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../modules/BgfxRenderer + ${CMAKE_CURRENT_SOURCE_DIR} + ) + target_link_libraries(test_pipeline_headless PRIVATE + GroveEngine::impl + + Catch2::Catch2WithMain + ) + grove_add_test(PipelineHeadless test_pipeline_headless ${CMAKE_BINARY_DIR}) + + grove_add_test(BgfxSpritesHeadless test_22_bgfx_sprites_headless ${CMAKE_CURRENT_BINARY_DIR}) +endif() + +# ================================================================================ +# Phase 5 Integration Tests - UIModule +# ================================================================================ + +# TestControllerModule - Simulates game logic for UI integration tests +add_library(TestControllerModule SHARED + modules/TestControllerModule.cpp +) + +target_link_libraries(TestControllerModule PRIVATE + GroveEngine::core + GroveEngine::impl + spdlog::spdlog +) + +# IT_014: UIModule Full Integration Test +if(GROVE_BUILD_UI_MODULE AND GROVE_BUILD_BGFX_RENDERER) + add_executable(IT_014_ui_module_integration + integration/IT_014_ui_module_integration.cpp + ) + + target_link_libraries(IT_014_ui_module_integration PRIVATE + test_helpers + GroveEngine::core + GroveEngine::impl + Catch2::Catch2WithMain + ) + + add_dependencies(IT_014_ui_module_integration TestControllerModule) + + # CTest integration + grove_add_test(UIModuleIntegration IT_014_ui_module_integration ${CMAKE_CURRENT_BINARY_DIR}) + + message(STATUS "Integration test 'IT_014_ui_module_integration' enabled") +endif() + +# IT_015: InputModule + UIModule Integration Test +if(GROVE_BUILD_UI_MODULE) + # IT_015: Simplified UIModule input integration test (no InputModule dependency) + # This test publishes IIO messages directly to test UIModule input processing + add_executable(IT_015_input_ui_integration + integration/IT_015_input_ui_integration.cpp + ) + + target_link_libraries(IT_015_input_ui_integration PRIVATE + test_helpers + GroveEngine::core + GroveEngine::impl + Catch2::Catch2WithMain + ) + + # CTest integration + grove_add_test(InputUIIntegration IT_015_input_ui_integration ${CMAKE_CURRENT_BINARY_DIR}) + + message(STATUS "Integration test 'IT_015_input_ui_integration' enabled (simplified, no SDL2)") +endif() + +# IT_015_Minimal: IIO-only integration test (no module loading, no DLL issues) +add_executable(IT_015_input_ui_integration_minimal + integration/IT_015_input_ui_integration_minimal.cpp +) + +target_link_libraries(IT_015_input_ui_integration_minimal PRIVATE + test_helpers + GroveEngine::core + GroveEngine::impl + Catch2::Catch2WithMain +) + +# CTest integration +grove_add_test(InputUIIntegration_Minimal IT_015_input_ui_integration_minimal ${CMAKE_CURRENT_BINARY_DIR}) + +message(STATUS "Integration test 'IT_015_input_ui_integration_minimal' enabled (IIO-only)") + +# ============================================ +# UIModule Interactive Showcase Demo +# ============================================ + +if(GROVE_BUILD_UI_MODULE AND GROVE_BUILD_BGFX_RENDERER) + add_executable(demo_ui_showcase + demo/demo_ui_showcase.cpp + ) + + target_link_libraries(demo_ui_showcase PRIVATE + GroveEngine::core + GroveEngine::impl + SDL2 + pthread + dl + ) + + # Add X11 on Linux for SDL window integration + if(UNIX AND NOT APPLE) + target_link_libraries(demo_ui_showcase PRIVATE X11) + endif() + + message(STATUS "UIModule showcase demo 'demo_ui_showcase' enabled") +endif() diff --git a/tests/integration/test_01_production_hotreload.cpp b/tests/integration/test_01_production_hotreload.cpp index 1fe6f1c..b0f6b9a 100644 --- a/tests/integration/test_01_production_hotreload.cpp +++ b/tests/integration/test_01_production_hotreload.cpp @@ -1,267 +1,270 @@ -#include "grove/ModuleLoader.h" -#include "grove/SequentialModuleSystem.h" -#include "grove/JsonDataNode.h" -#include "../helpers/TestMetrics.h" -#include "../helpers/TestAssertions.h" -#include "../helpers/TestReporter.h" -#include "../helpers/SystemUtils.h" -#include -#include -#include -#include -#include - -using namespace grove; - -int main() { - TestReporter reporter("Production Hot-Reload"); - TestMetrics metrics; - - std::cout << "================================================================================\n"; - std::cout << "TEST: Production Hot-Reload\n"; - std::cout << "================================================================================\n\n"; - - // === SETUP === - std::cout << "Setup: Loading TankModule...\n"; - - ModuleLoader loader; - auto moduleSystem = std::make_unique(); - - // Charger module - std::string modulePath = "./libTankModule.so"; - auto module = loader.load(modulePath, "TankModule", false); - - // Config - nlohmann::json configJson; - configJson["tankCount"] = 50; - configJson["version"] = "v1.0"; - auto config = std::make_unique("config", configJson); - - // Initialiser (setConfiguration) - module->setConfiguration(*config, nullptr, nullptr); - - // Enregistrer dans system - moduleSystem->registerModule("TankModule", std::move(module)); - - std::cout << " ✓ Module loaded and initialized\n\n"; - - // === PHASE 1: Pre-Reload (15s = 900 frames) === - std::cout << "Phase 1: Running 15s before reload...\n"; - - // Créer input avec deltaTime - nlohmann::json inputJson; - inputJson["deltaTime"] = 1.0 / 60.0; - auto inputNode = std::make_unique("input", inputJson); - - for (int frame = 0; frame < 900; frame++) { - auto frameStart = std::chrono::high_resolution_clock::now(); - - moduleSystem->processModules(1.0f / 60.0f); - - auto frameEnd = std::chrono::high_resolution_clock::now(); - float frameTime = std::chrono::duration(frameEnd - frameStart).count(); - - metrics.recordFPS(1000.0f / frameTime); - - if (frame % 60 == 0) { - metrics.recordMemoryUsage(getCurrentMemoryUsage()); - } - - if (frame % 300 == 0) { - std::cout << " Frame " << frame << "/900\n"; - } - } - - // Snapshot state AVANT reload - auto tankModule = moduleSystem->extractModule(); - auto preReloadState = tankModule->getState(); - - // Cast to JsonDataNode to access JSON - auto* jsonNodeBefore = dynamic_cast(preReloadState.get()); - if (!jsonNodeBefore) { - std::cerr << "❌ Failed to cast state to JsonDataNode\n"; - return 1; - } - - const auto& stateJsonBefore = jsonNodeBefore->getJsonData(); - - int tankCountBefore = stateJsonBefore["tanks"].size(); - std::string versionBefore = stateJsonBefore.value("version", "unknown"); - int frameCountBefore = stateJsonBefore.value("frameCount", 0); - - std::cout << "\nState snapshot BEFORE reload:\n"; - std::cout << " Version: " << versionBefore << "\n"; - std::cout << " Tank count: " << tankCountBefore << "\n"; - std::cout << " Frame: " << frameCountBefore << "\n\n"; - - ASSERT_EQ(tankCountBefore, 50, "Should have 50 tanks before reload"); - - // Ré-enregistrer le module temporairement - moduleSystem->registerModule("TankModule", std::move(tankModule)); - - // === HOT-RELOAD === - std::cout << "Triggering hot-reload...\n"; - - // Modifier version dans source (HEADER) - std::cout << " 1. Modifying source code (v1.0 -> v2.0 HOT-RELOADED)...\n"; - - // Test runs from build/tests/, so source files are at ../../tests/modules/ - std::ifstream input("../../tests/modules/TankModule.h"); - std::string content((std::istreambuf_iterator(input)), std::istreambuf_iterator()); - input.close(); - - size_t pos = content.find("std::string moduleVersion = \"v1.0\";"); - if (pos != std::string::npos) { - content.replace(pos, 39, "std::string moduleVersion = \"v2.0 HOT-RELOADED\";"); - } - - std::ofstream output("../../tests/modules/TankModule.h"); - output << content; - output.close(); - - // Recompiler - std::cout << " 2. Recompiling module...\n"; - // Note: This test runs from build/tests/, so we use make -C .. to build from build directory - int buildResult = system("make -C .. TankModule 2>&1 > /dev/null"); - if (buildResult != 0) { - std::cerr << "❌ Compilation failed!\n"; - return 1; - } - std::cout << " ✓ Compilation succeeded\n"; - - // Wait for file to be ready (simulate file stability check) - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - - // Reload - std::cout << " 3. Reloading module...\n"; - auto reloadStart = std::chrono::high_resolution_clock::now(); - - // Extract module from system - tankModule = moduleSystem->extractModule(); - - // Use ModuleLoader::reload() - auto newModule = loader.reload(std::move(tankModule)); - - // Re-register - moduleSystem->registerModule("TankModule", std::move(newModule)); - - auto reloadEnd = std::chrono::high_resolution_clock::now(); - float reloadTime = std::chrono::duration(reloadEnd - reloadStart).count(); - - metrics.recordReloadTime(reloadTime); - reporter.addMetric("reload_time_ms", reloadTime); - - std::cout << " ✓ Reload completed in " << reloadTime << "ms\n\n"; - - // === VÉRIFICATIONS POST-RELOAD === - std::cout << "Verifying state preservation...\n"; - - tankModule = moduleSystem->extractModule(); - auto postReloadState = tankModule->getState(); - auto* jsonNodeAfter = dynamic_cast(postReloadState.get()); - - if (!jsonNodeAfter) { - std::cerr << "❌ Failed to cast post-reload state to JsonDataNode\n"; - return 1; - } - - const auto& stateJsonAfter = jsonNodeAfter->getJsonData(); - - int tankCountAfter = stateJsonAfter["tanks"].size(); - std::string versionAfter = stateJsonAfter.value("version", "unknown"); - int frameCountAfter = stateJsonAfter.value("frameCount", 0); - - std::cout << "\nState snapshot AFTER reload:\n"; - std::cout << " Version: " << versionAfter << "\n"; - std::cout << " Tank count: " << tankCountAfter << "\n"; - std::cout << " Frame: " << frameCountAfter << "\n\n"; - - // Vérification 1: Nombre de tanks - ASSERT_EQ(tankCountAfter, 50, "Should still have 50 tanks after reload"); - reporter.addAssertion("tank_count_preserved", tankCountAfter == 50); - - // Vérification 2: Version mise à jour - bool versionUpdated = versionAfter.find("v2.0") != std::string::npos; - ASSERT_TRUE(versionUpdated, "Version should be updated to v2.0"); - reporter.addAssertion("version_updated", versionUpdated); - - // Vérification 3: Frame count préservé - ASSERT_EQ(frameCountAfter, frameCountBefore, "Frame count should be preserved"); - reporter.addAssertion("framecount_preserved", frameCountAfter == frameCountBefore); - - std::cout << " ✓ State preserved correctly\n"; - - // Ré-enregistrer module - moduleSystem->registerModule("TankModule", std::move(tankModule)); - - // === PHASE 2: Post-Reload (15s = 900 frames) === - std::cout << "\nPhase 2: Running 15s after reload...\n"; - - for (int frame = 0; frame < 900; frame++) { - auto frameStart = std::chrono::high_resolution_clock::now(); - - moduleSystem->processModules(1.0f / 60.0f); - - auto frameEnd = std::chrono::high_resolution_clock::now(); - float frameTime = std::chrono::duration(frameEnd - frameStart).count(); - - metrics.recordFPS(1000.0f / frameTime); - - if (frame % 60 == 0) { - metrics.recordMemoryUsage(getCurrentMemoryUsage()); - } - - if (frame % 300 == 0) { - std::cout << " Frame " << frame << "/900\n"; - } - } - - // === VÉRIFICATIONS FINALES === - std::cout << "\nFinal verifications...\n"; - - // Memory growth - size_t memGrowth = metrics.getMemoryGrowth(); - float memGrowthMB = memGrowth / (1024.0f * 1024.0f); - ASSERT_LT(memGrowthMB, 5.0f, "Memory growth should be < 5MB"); - reporter.addMetric("memory_growth_mb", memGrowthMB); - - // FPS - float minFPS = metrics.getFPSMin(); - ASSERT_GT(minFPS, 30.0f, "Min FPS should be > 30"); - reporter.addMetric("fps_min", minFPS); - reporter.addMetric("fps_avg", metrics.getFPSAvg()); - reporter.addMetric("fps_max", metrics.getFPSMax()); - - // Reload time - ASSERT_LT(reloadTime, 1000.0f, "Reload time should be < 1000ms"); - - // No crashes - reporter.addAssertion("no_crashes", true); - - // === CLEANUP === - std::cout << "\nCleaning up...\n"; - - // Restaurer version originale (HEADER) - std::ifstream inputRestore("../../tests/modules/TankModule.h"); - std::string contentRestore((std::istreambuf_iterator(inputRestore)), std::istreambuf_iterator()); - inputRestore.close(); - - pos = contentRestore.find("std::string moduleVersion = \"v2.0 HOT-RELOADED\";"); - if (pos != std::string::npos) { - contentRestore.replace(pos, 50, "std::string moduleVersion = \"v1.0\";"); - } - - std::ofstream outputRestore("../../tests/modules/TankModule.h"); - outputRestore << contentRestore; - outputRestore.close(); - - // Rebuild to restore original version (test runs from build/tests/) - system("make -C .. TankModule 2>&1 > /dev/null"); - - // === RAPPORTS === - std::cout << "\n"; - metrics.printReport(); - reporter.printFinalReport(); - - return reporter.getExitCode(); -} +#include "grove/ModuleLoader.h" +#include "grove/SequentialModuleSystem.h" +#include "grove/JsonDataNode.h" +#include "../helpers/TestMetrics.h" +#include "../helpers/TestAssertions.h" +#include "../helpers/TestReporter.h" +#include "../helpers/SystemUtils.h" +#include +#include +#include +#include +#include + +using namespace grove; + +int main() { + TestReporter reporter("Production Hot-Reload"); + TestMetrics metrics; + + std::cout << "================================================================================\n"; + std::cout << "TEST: Production Hot-Reload\n"; + std::cout << "================================================================================\n\n"; + + // === SETUP === + std::cout << "Setup: Loading TankModule...\n"; + + ModuleLoader loader; + auto moduleSystem = std::make_unique(); + + // Charger module + std::string modulePath = "./libTankModule.so"; +#ifdef _WIN32 + modulePath = "./libTankModule.dll"; +#endif + auto module = loader.load(modulePath, "TankModule", false); + + // Config + nlohmann::json configJson; + configJson["tankCount"] = 50; + configJson["version"] = "v1.0"; + auto config = std::make_unique("config", configJson); + + // Initialiser (setConfiguration) + module->setConfiguration(*config, nullptr, nullptr); + + // Enregistrer dans system + moduleSystem->registerModule("TankModule", std::move(module)); + + std::cout << " ✓ Module loaded and initialized\n\n"; + + // === PHASE 1: Pre-Reload (15s = 900 frames) === + std::cout << "Phase 1: Running 15s before reload...\n"; + + // Créer input avec deltaTime + nlohmann::json inputJson; + inputJson["deltaTime"] = 1.0 / 60.0; + auto inputNode = std::make_unique("input", inputJson); + + for (int frame = 0; frame < 900; frame++) { + auto frameStart = std::chrono::high_resolution_clock::now(); + + moduleSystem->processModules(1.0f / 60.0f); + + auto frameEnd = std::chrono::high_resolution_clock::now(); + float frameTime = std::chrono::duration(frameEnd - frameStart).count(); + + metrics.recordFPS(1000.0f / frameTime); + + if (frame % 60 == 0) { + metrics.recordMemoryUsage(getCurrentMemoryUsage()); + } + + if (frame % 300 == 0) { + std::cout << " Frame " << frame << "/900\n"; + } + } + + // Snapshot state AVANT reload + auto tankModule = moduleSystem->extractModule(); + auto preReloadState = tankModule->getState(); + + // Cast to JsonDataNode to access JSON + auto* jsonNodeBefore = dynamic_cast(preReloadState.get()); + if (!jsonNodeBefore) { + std::cerr << "❌ Failed to cast state to JsonDataNode\n"; + return 1; + } + + const auto& stateJsonBefore = jsonNodeBefore->getJsonData(); + + int tankCountBefore = stateJsonBefore["tanks"].size(); + std::string versionBefore = stateJsonBefore.value("version", "unknown"); + int frameCountBefore = stateJsonBefore.value("frameCount", 0); + + std::cout << "\nState snapshot BEFORE reload:\n"; + std::cout << " Version: " << versionBefore << "\n"; + std::cout << " Tank count: " << tankCountBefore << "\n"; + std::cout << " Frame: " << frameCountBefore << "\n\n"; + + ASSERT_EQ(tankCountBefore, 50, "Should have 50 tanks before reload"); + + // Ré-enregistrer le module temporairement + moduleSystem->registerModule("TankModule", std::move(tankModule)); + + // === HOT-RELOAD === + std::cout << "Triggering hot-reload...\n"; + + // Modifier version dans source (HEADER) + std::cout << " 1. Modifying source code (v1.0 -> v2.0 HOT-RELOADED)...\n"; + + // Test runs from build/tests/, so source files are at ../../tests/modules/ + std::ifstream input("../../tests/modules/TankModule.h"); + std::string content((std::istreambuf_iterator(input)), std::istreambuf_iterator()); + input.close(); + + size_t pos = content.find("std::string moduleVersion = \"v1.0\";"); + if (pos != std::string::npos) { + content.replace(pos, 39, "std::string moduleVersion = \"v2.0 HOT-RELOADED\";"); + } + + std::ofstream output("../../tests/modules/TankModule.h"); + output << content; + output.close(); + + // Recompiler + std::cout << " 2. Recompiling module...\n"; + // Note: This test runs from build/tests/, so we use make -C .. to build from build directory + int buildResult = system("make -C .. TankModule 2>&1 > /dev/null"); + if (buildResult != 0) { + std::cerr << "❌ Compilation failed!\n"; + return 1; + } + std::cout << " ✓ Compilation succeeded\n"; + + // Wait for file to be ready (simulate file stability check) + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + + // Reload + std::cout << " 3. Reloading module...\n"; + auto reloadStart = std::chrono::high_resolution_clock::now(); + + // Extract module from system + tankModule = moduleSystem->extractModule(); + + // Use ModuleLoader::reload() + auto newModule = loader.reload(std::move(tankModule)); + + // Re-register + moduleSystem->registerModule("TankModule", std::move(newModule)); + + auto reloadEnd = std::chrono::high_resolution_clock::now(); + float reloadTime = std::chrono::duration(reloadEnd - reloadStart).count(); + + metrics.recordReloadTime(reloadTime); + reporter.addMetric("reload_time_ms", reloadTime); + + std::cout << " ✓ Reload completed in " << reloadTime << "ms\n\n"; + + // === VÉRIFICATIONS POST-RELOAD === + std::cout << "Verifying state preservation...\n"; + + tankModule = moduleSystem->extractModule(); + auto postReloadState = tankModule->getState(); + auto* jsonNodeAfter = dynamic_cast(postReloadState.get()); + + if (!jsonNodeAfter) { + std::cerr << "❌ Failed to cast post-reload state to JsonDataNode\n"; + return 1; + } + + const auto& stateJsonAfter = jsonNodeAfter->getJsonData(); + + int tankCountAfter = stateJsonAfter["tanks"].size(); + std::string versionAfter = stateJsonAfter.value("version", "unknown"); + int frameCountAfter = stateJsonAfter.value("frameCount", 0); + + std::cout << "\nState snapshot AFTER reload:\n"; + std::cout << " Version: " << versionAfter << "\n"; + std::cout << " Tank count: " << tankCountAfter << "\n"; + std::cout << " Frame: " << frameCountAfter << "\n\n"; + + // Vérification 1: Nombre de tanks + ASSERT_EQ(tankCountAfter, 50, "Should still have 50 tanks after reload"); + reporter.addAssertion("tank_count_preserved", tankCountAfter == 50); + + // Vérification 2: Version mise à jour + bool versionUpdated = versionAfter.find("v2.0") != std::string::npos; + ASSERT_TRUE(versionUpdated, "Version should be updated to v2.0"); + reporter.addAssertion("version_updated", versionUpdated); + + // Vérification 3: Frame count préservé + ASSERT_EQ(frameCountAfter, frameCountBefore, "Frame count should be preserved"); + reporter.addAssertion("framecount_preserved", frameCountAfter == frameCountBefore); + + std::cout << " ✓ State preserved correctly\n"; + + // Ré-enregistrer module + moduleSystem->registerModule("TankModule", std::move(tankModule)); + + // === PHASE 2: Post-Reload (15s = 900 frames) === + std::cout << "\nPhase 2: Running 15s after reload...\n"; + + for (int frame = 0; frame < 900; frame++) { + auto frameStart = std::chrono::high_resolution_clock::now(); + + moduleSystem->processModules(1.0f / 60.0f); + + auto frameEnd = std::chrono::high_resolution_clock::now(); + float frameTime = std::chrono::duration(frameEnd - frameStart).count(); + + metrics.recordFPS(1000.0f / frameTime); + + if (frame % 60 == 0) { + metrics.recordMemoryUsage(getCurrentMemoryUsage()); + } + + if (frame % 300 == 0) { + std::cout << " Frame " << frame << "/900\n"; + } + } + + // === VÉRIFICATIONS FINALES === + std::cout << "\nFinal verifications...\n"; + + // Memory growth + size_t memGrowth = metrics.getMemoryGrowth(); + float memGrowthMB = memGrowth / (1024.0f * 1024.0f); + ASSERT_LT(memGrowthMB, 5.0f, "Memory growth should be < 5MB"); + reporter.addMetric("memory_growth_mb", memGrowthMB); + + // FPS + float minFPS = metrics.getFPSMin(); + ASSERT_GT(minFPS, 30.0f, "Min FPS should be > 30"); + reporter.addMetric("fps_min", minFPS); + reporter.addMetric("fps_avg", metrics.getFPSAvg()); + reporter.addMetric("fps_max", metrics.getFPSMax()); + + // Reload time + ASSERT_LT(reloadTime, 1000.0f, "Reload time should be < 1000ms"); + + // No crashes + reporter.addAssertion("no_crashes", true); + + // === CLEANUP === + std::cout << "\nCleaning up...\n"; + + // Restaurer version originale (HEADER) + std::ifstream inputRestore("../../tests/modules/TankModule.h"); + std::string contentRestore((std::istreambuf_iterator(inputRestore)), std::istreambuf_iterator()); + inputRestore.close(); + + pos = contentRestore.find("std::string moduleVersion = \"v2.0 HOT-RELOADED\";"); + if (pos != std::string::npos) { + contentRestore.replace(pos, 50, "std::string moduleVersion = \"v1.0\";"); + } + + std::ofstream outputRestore("../../tests/modules/TankModule.h"); + outputRestore << contentRestore; + outputRestore.close(); + + // Rebuild to restore original version (test runs from build/tests/) + system("make -C .. TankModule 2>&1 > /dev/null"); + + // === RAPPORTS === + std::cout << "\n"; + metrics.printReport(); + reporter.printFinalReport(); + + return reporter.getExitCode(); +} diff --git a/tests/integration/test_02_chaos_monkey.cpp b/tests/integration/test_02_chaos_monkey.cpp index 4f7b8a9..3b76c92 100644 --- a/tests/integration/test_02_chaos_monkey.cpp +++ b/tests/integration/test_02_chaos_monkey.cpp @@ -1,255 +1,258 @@ -// PREUVE : Décommenter cette ligne pour désactiver la recovery et voir le test ÉCHOUER -// #define DISABLE_RECOVERY_FOR_TEST - -#include "grove/ModuleLoader.h" -#include "grove/SequentialModuleSystem.h" -#include "grove/JsonDataNode.h" -#include "../helpers/TestMetrics.h" -#include "../helpers/TestAssertions.h" -#include "../helpers/TestReporter.h" -#include "../helpers/SystemUtils.h" -#include -#include -#include -#include -#include - -using namespace grove; - -// Global for crash detection -static std::atomic engineCrashed{false}; - -void signalHandler(int signal) { - if (signal == SIGSEGV || signal == SIGABRT) { - engineCrashed.store(true); - std::cerr << "❌ FATAL: Signal " << signal << " received (SIGSEGV or SIGABRT)\n"; - std::cerr << "Engine has crashed unrecoverably.\n"; - std::exit(1); - } -} - -int main() { - TestReporter reporter("Chaos Monkey"); - TestMetrics metrics; - - std::cout << "================================================================================\n"; - std::cout << "TEST: Chaos Monkey\n"; - std::cout << "================================================================================\n\n"; - - // Setup signal handlers - std::signal(SIGSEGV, signalHandler); - std::signal(SIGABRT, signalHandler); - - // === SETUP === - std::cout << "Setup: Loading ChaosModule...\n"; - - ModuleLoader loader; - auto moduleSystem = std::make_unique(); - - // Load module - std::string modulePath = "./libChaosModule.so"; - auto module = loader.load(modulePath, "ChaosModule", false); - - // Configure module avec seed ALÉATOIRE basé sur le temps - // Chaque run sera différent - VRAI chaos - unsigned int randomSeed = static_cast(std::chrono::system_clock::now().time_since_epoch().count()); - - nlohmann::json configJson; - configJson["seed"] = randomSeed; - configJson["hotReloadProbability"] = 0.30; // Non utilisé maintenant - configJson["crashProbability"] = 0.05; // 5% par frame = crash fréquent - configJson["corruptionProbability"] = 0.10; // Non utilisé - configJson["invalidConfigProbability"] = 0.05; // Non utilisé - auto config = std::make_unique("config", configJson); - - std::cout << " Random seed: " << randomSeed << " (time-based, unpredictable)\n"; - - module->setConfiguration(*config, nullptr, nullptr); - - // Register in module system - moduleSystem->registerModule("ChaosModule", std::move(module)); - - std::cout << " ✓ ChaosModule loaded and configured\n\n"; - - // === CHAOS LOOP (30 seconds = 1800 frames @ 60 FPS) === - // NOTE: Reduced from 5 minutes for faster testing - std::cout << "Starting Chaos Monkey (30 seconds simulation)...\n"; - std::cout << "REAL CHAOS MODE:\n"; - std::cout << " - 5% crash probability PER FRAME (not per second)\n"; - std::cout << " - Expected crashes: ~90 crashes (5% of 1800 frames)\n"; - std::cout << " - Random seed (time-based): unpredictable pattern\n"; - std::cout << " - Multiple crash types: runtime_error, logic_error, out_of_range, domain_error, state corruption\n"; - std::cout << " - Corrupted state validation: module must reject corrupted state\n\n"; - - const int totalFrames = 1800; // 30 * 60 - int crashesDetected = 0; - int reloadsTriggered = 0; - int recoverySuccesses = 0; - bool hadDeadlock = false; - - auto testStart = std::chrono::high_resolution_clock::now(); - - for (int frame = 0; frame < totalFrames; frame++) { - auto frameStart = std::chrono::high_resolution_clock::now(); - bool didRecoveryThisFrame = false; - - try { - // Process module (1/60th of a second) - moduleSystem->processModules(1.0f / 60.0f); - - } catch (const std::exception& e) { - // CRASH DETECTED - Attempt recovery - crashesDetected++; - std::cout << " [Frame " << frame << "] ⚠️ Crash detected: " << e.what() << "\n"; - - // PREUVE QUE LE TEST PEUT ÉCHOUER : désactiver la recovery - #ifdef DISABLE_RECOVERY_FOR_TEST - std::cout << " [Frame " << frame << "] ❌ RECOVERY DISABLED - Test will fail\n"; - reporter.addAssertion("recovery_disabled", false); - break; // Le test DOIT échouer - #endif - - // Recovery attempt - try { - std::cout << " [Frame " << frame << "] 🔄 Attempting recovery...\n"; - - auto recoveryStart = std::chrono::high_resolution_clock::now(); - - // Extract module from system - auto crashedModule = moduleSystem->extractModule(); - - // Reload module - auto reloadedModule = loader.reload(std::move(crashedModule)); - - // Re-register - moduleSystem->registerModule("ChaosModule", std::move(reloadedModule)); - - auto recoveryEnd = std::chrono::high_resolution_clock::now(); - float recoveryTime = std::chrono::duration(recoveryEnd - recoveryStart).count(); - - metrics.recordReloadTime(recoveryTime); - recoverySuccesses++; - didRecoveryThisFrame = true; - - std::cout << " [Frame " << frame << "] ✅ Recovery successful (" << recoveryTime << "ms)\n"; - - } catch (const std::exception& recoveryError) { - std::cout << " [Frame " << frame << "] ❌ Recovery FAILED: " << recoveryError.what() << "\n"; - reporter.addAssertion("recovery_failed", false); - break; // Stop test - recovery failed - } - } - - // Metrics - auto frameEnd = std::chrono::high_resolution_clock::now(); - float frameTime = std::chrono::duration(frameEnd - frameStart).count(); - - // Only record FPS for normal frames (not recovery frames) - // Recovery frames are slow by design (100+ ms for hot-reload) - if (!didRecoveryThisFrame) { - metrics.recordFPS(1000.0f / frameTime); - } - - if (frame % 60 == 0) { - metrics.recordMemoryUsage(getCurrentMemoryUsage()); - } - - // Deadlock detection (frame > 100ms) - // NOTE: Skip deadlock check if we just did a recovery (recovery takes >100ms by design) - if (frameTime > 100.0f && !didRecoveryThisFrame) { - std::cout << " [Frame " << frame << "] ⚠️ Potential deadlock (frame time: " << frameTime << "ms)\n"; - hadDeadlock = true; - } - - // Progress (every 600 frames = 10 seconds) - if (frame % 600 == 0 && frame > 0) { - float elapsedSec = frame / 60.0f; - float progress = (frame * 100.0f) / totalFrames; - std::cout << "Progress: " << elapsedSec << "/30.0 seconds (" << (int)progress << "%)\n"; - - // Show current metrics - std::cout << " FPS: min=" << metrics.getFPSMin() << ", avg=" << metrics.getFPSAvg() << ", max=" << metrics.getFPSMax() << "\n"; - std::cout << " Memory: " << (getCurrentMemoryUsage() / (1024.0f * 1024.0f)) << " MB\n"; - } - - // Check if engine crashed externally - if (engineCrashed.load()) { - std::cout << " [Frame " << frame << "] ❌ Engine crashed externally (signal received)\n"; - reporter.addAssertion("engine_crashed_externally", false); - break; - } - } - - auto testEnd = std::chrono::high_resolution_clock::now(); - float totalDuration = std::chrono::duration(testEnd - testStart).count(); - - std::cout << "\nTest completed!\n\n"; - - // === FINAL VERIFICATIONS === - std::cout << "Final verifications...\n"; - - // Engine still alive - bool engineAlive = !engineCrashed.load(); - ASSERT_TRUE(engineAlive, "Engine should still be alive"); - reporter.addAssertion("engine_alive", engineAlive); - - // No deadlocks - ASSERT_FALSE(hadDeadlock, "Should not have deadlocks"); - reporter.addAssertion("no_deadlocks", !hadDeadlock); - - // Memory growth < 10MB - size_t memGrowth = metrics.getMemoryGrowth(); - float memGrowthMB = memGrowth / (1024.0f * 1024.0f); - ASSERT_LT(memGrowthMB, 10.0f, "Memory growth should be < 10MB"); - reporter.addMetric("memory_growth_mb", memGrowthMB); - - // Test runs as fast as possible (not real-time) - // Just check it completed within reasonable bounds (< 60 seconds wall time) - ASSERT_LT(totalDuration, 60.0f, "Total duration should be < 60 seconds"); - reporter.addMetric("total_duration_sec", totalDuration); - - // FPS metrics - float minFPS = metrics.getFPSMin(); - float avgFPS = metrics.getFPSAvg(); - float maxFPS = metrics.getFPSMax(); - - // Min FPS should be reasonable (> 10 even with crashes) - ASSERT_GT(minFPS, 10.0f, "Min FPS should be > 10"); - reporter.addMetric("fps_min", minFPS); - reporter.addMetric("fps_avg", avgFPS); - reporter.addMetric("fps_max", maxFPS); - - // Recovery rate > 95% - float recoveryRate = (crashesDetected > 0) ? (recoverySuccesses * 100.0f / crashesDetected) : 100.0f; - ASSERT_GT(recoveryRate, 95.0f, "Recovery rate should be > 95%"); - reporter.addMetric("recovery_rate_percent", recoveryRate); - - // === STATISTICS === - std::cout << "\n"; - std::cout << "================================================================================\n"; - std::cout << "CHAOS MONKEY STATISTICS\n"; - std::cout << "================================================================================\n"; - std::cout << " Total frames: " << totalFrames << "\n"; - std::cout << " Duration: " << totalDuration << "s (wall time, not simulation time)\n"; - std::cout << " Crashes detected: " << crashesDetected << "\n"; - std::cout << " Recovery successes: " << recoverySuccesses << "\n"; - std::cout << " Recovery rate: " << recoveryRate << "%\n"; - std::cout << " Memory growth: " << memGrowthMB << " MB (max: 10MB)\n"; - std::cout << " Had deadlocks: " << (hadDeadlock ? "YES ❌" : "NO ✅") << "\n"; - std::cout << " FPS min/avg/max: " << minFPS << " / " << avgFPS << " / " << maxFPS << "\n"; - std::cout << "================================================================================\n\n"; - - std::cout << "Note: ChaosModule generates random crashes internally.\n"; - std::cout << "The test should recover from ALL crashes automatically via hot-reload.\n\n"; - - // === CLEANUP === - std::cout << "Cleaning up...\n"; - moduleSystem.reset(); - std::cout << " ✓ Module system shutdown complete\n\n"; - - // === REPORTS === - metrics.printReport(); - reporter.printFinalReport(); - - return reporter.getExitCode(); -} +// PREUVE : Décommenter cette ligne pour désactiver la recovery et voir le test ÉCHOUER +// #define DISABLE_RECOVERY_FOR_TEST + +#include "grove/ModuleLoader.h" +#include "grove/SequentialModuleSystem.h" +#include "grove/JsonDataNode.h" +#include "../helpers/TestMetrics.h" +#include "../helpers/TestAssertions.h" +#include "../helpers/TestReporter.h" +#include "../helpers/SystemUtils.h" +#include +#include +#include +#include +#include + +using namespace grove; + +// Global for crash detection +static std::atomic engineCrashed{false}; + +void signalHandler(int signal) { + if (signal == SIGSEGV || signal == SIGABRT) { + engineCrashed.store(true); + std::cerr << "❌ FATAL: Signal " << signal << " received (SIGSEGV or SIGABRT)\n"; + std::cerr << "Engine has crashed unrecoverably.\n"; + std::exit(1); + } +} + +int main() { + TestReporter reporter("Chaos Monkey"); + TestMetrics metrics; + + std::cout << "================================================================================\n"; + std::cout << "TEST: Chaos Monkey\n"; + std::cout << "================================================================================\n\n"; + + // Setup signal handlers + std::signal(SIGSEGV, signalHandler); + std::signal(SIGABRT, signalHandler); + + // === SETUP === + std::cout << "Setup: Loading ChaosModule...\n"; + + ModuleLoader loader; + auto moduleSystem = std::make_unique(); + + // Load module + std::string modulePath = "./libChaosModule.so"; +#ifdef _WIN32 + modulePath = "./libChaosModule.dll"; +#endif + auto module = loader.load(modulePath, "ChaosModule", false); + + // Configure module avec seed ALÉATOIRE basé sur le temps + // Chaque run sera différent - VRAI chaos + unsigned int randomSeed = static_cast(std::chrono::system_clock::now().time_since_epoch().count()); + + nlohmann::json configJson; + configJson["seed"] = randomSeed; + configJson["hotReloadProbability"] = 0.30; // Non utilisé maintenant + configJson["crashProbability"] = 0.05; // 5% par frame = crash fréquent + configJson["corruptionProbability"] = 0.10; // Non utilisé + configJson["invalidConfigProbability"] = 0.05; // Non utilisé + auto config = std::make_unique("config", configJson); + + std::cout << " Random seed: " << randomSeed << " (time-based, unpredictable)\n"; + + module->setConfiguration(*config, nullptr, nullptr); + + // Register in module system + moduleSystem->registerModule("ChaosModule", std::move(module)); + + std::cout << " ✓ ChaosModule loaded and configured\n\n"; + + // === CHAOS LOOP (30 seconds = 1800 frames @ 60 FPS) === + // NOTE: Reduced from 5 minutes for faster testing + std::cout << "Starting Chaos Monkey (30 seconds simulation)...\n"; + std::cout << "REAL CHAOS MODE:\n"; + std::cout << " - 5% crash probability PER FRAME (not per second)\n"; + std::cout << " - Expected crashes: ~90 crashes (5% of 1800 frames)\n"; + std::cout << " - Random seed (time-based): unpredictable pattern\n"; + std::cout << " - Multiple crash types: runtime_error, logic_error, out_of_range, domain_error, state corruption\n"; + std::cout << " - Corrupted state validation: module must reject corrupted state\n\n"; + + const int totalFrames = 1800; // 30 * 60 + int crashesDetected = 0; + int reloadsTriggered = 0; + int recoverySuccesses = 0; + bool hadDeadlock = false; + + auto testStart = std::chrono::high_resolution_clock::now(); + + for (int frame = 0; frame < totalFrames; frame++) { + auto frameStart = std::chrono::high_resolution_clock::now(); + bool didRecoveryThisFrame = false; + + try { + // Process module (1/60th of a second) + moduleSystem->processModules(1.0f / 60.0f); + + } catch (const std::exception& e) { + // CRASH DETECTED - Attempt recovery + crashesDetected++; + std::cout << " [Frame " << frame << "] ⚠️ Crash detected: " << e.what() << "\n"; + + // PREUVE QUE LE TEST PEUT ÉCHOUER : désactiver la recovery + #ifdef DISABLE_RECOVERY_FOR_TEST + std::cout << " [Frame " << frame << "] ❌ RECOVERY DISABLED - Test will fail\n"; + reporter.addAssertion("recovery_disabled", false); + break; // Le test DOIT échouer + #endif + + // Recovery attempt + try { + std::cout << " [Frame " << frame << "] 🔄 Attempting recovery...\n"; + + auto recoveryStart = std::chrono::high_resolution_clock::now(); + + // Extract module from system + auto crashedModule = moduleSystem->extractModule(); + + // Reload module + auto reloadedModule = loader.reload(std::move(crashedModule)); + + // Re-register + moduleSystem->registerModule("ChaosModule", std::move(reloadedModule)); + + auto recoveryEnd = std::chrono::high_resolution_clock::now(); + float recoveryTime = std::chrono::duration(recoveryEnd - recoveryStart).count(); + + metrics.recordReloadTime(recoveryTime); + recoverySuccesses++; + didRecoveryThisFrame = true; + + std::cout << " [Frame " << frame << "] ✅ Recovery successful (" << recoveryTime << "ms)\n"; + + } catch (const std::exception& recoveryError) { + std::cout << " [Frame " << frame << "] ❌ Recovery FAILED: " << recoveryError.what() << "\n"; + reporter.addAssertion("recovery_failed", false); + break; // Stop test - recovery failed + } + } + + // Metrics + auto frameEnd = std::chrono::high_resolution_clock::now(); + float frameTime = std::chrono::duration(frameEnd - frameStart).count(); + + // Only record FPS for normal frames (not recovery frames) + // Recovery frames are slow by design (100+ ms for hot-reload) + if (!didRecoveryThisFrame) { + metrics.recordFPS(1000.0f / frameTime); + } + + if (frame % 60 == 0) { + metrics.recordMemoryUsage(getCurrentMemoryUsage()); + } + + // Deadlock detection (frame > 100ms) + // NOTE: Skip deadlock check if we just did a recovery (recovery takes >100ms by design) + if (frameTime > 100.0f && !didRecoveryThisFrame) { + std::cout << " [Frame " << frame << "] ⚠️ Potential deadlock (frame time: " << frameTime << "ms)\n"; + hadDeadlock = true; + } + + // Progress (every 600 frames = 10 seconds) + if (frame % 600 == 0 && frame > 0) { + float elapsedSec = frame / 60.0f; + float progress = (frame * 100.0f) / totalFrames; + std::cout << "Progress: " << elapsedSec << "/30.0 seconds (" << (int)progress << "%)\n"; + + // Show current metrics + std::cout << " FPS: min=" << metrics.getFPSMin() << ", avg=" << metrics.getFPSAvg() << ", max=" << metrics.getFPSMax() << "\n"; + std::cout << " Memory: " << (getCurrentMemoryUsage() / (1024.0f * 1024.0f)) << " MB\n"; + } + + // Check if engine crashed externally + if (engineCrashed.load()) { + std::cout << " [Frame " << frame << "] ❌ Engine crashed externally (signal received)\n"; + reporter.addAssertion("engine_crashed_externally", false); + break; + } + } + + auto testEnd = std::chrono::high_resolution_clock::now(); + float totalDuration = std::chrono::duration(testEnd - testStart).count(); + + std::cout << "\nTest completed!\n\n"; + + // === FINAL VERIFICATIONS === + std::cout << "Final verifications...\n"; + + // Engine still alive + bool engineAlive = !engineCrashed.load(); + ASSERT_TRUE(engineAlive, "Engine should still be alive"); + reporter.addAssertion("engine_alive", engineAlive); + + // No deadlocks + ASSERT_FALSE(hadDeadlock, "Should not have deadlocks"); + reporter.addAssertion("no_deadlocks", !hadDeadlock); + + // Memory growth < 10MB + size_t memGrowth = metrics.getMemoryGrowth(); + float memGrowthMB = memGrowth / (1024.0f * 1024.0f); + ASSERT_LT(memGrowthMB, 10.0f, "Memory growth should be < 10MB"); + reporter.addMetric("memory_growth_mb", memGrowthMB); + + // Test runs as fast as possible (not real-time) + // Just check it completed within reasonable bounds (< 60 seconds wall time) + ASSERT_LT(totalDuration, 60.0f, "Total duration should be < 60 seconds"); + reporter.addMetric("total_duration_sec", totalDuration); + + // FPS metrics + float minFPS = metrics.getFPSMin(); + float avgFPS = metrics.getFPSAvg(); + float maxFPS = metrics.getFPSMax(); + + // Min FPS should be reasonable (> 10 even with crashes) + ASSERT_GT(minFPS, 10.0f, "Min FPS should be > 10"); + reporter.addMetric("fps_min", minFPS); + reporter.addMetric("fps_avg", avgFPS); + reporter.addMetric("fps_max", maxFPS); + + // Recovery rate > 95% + float recoveryRate = (crashesDetected > 0) ? (recoverySuccesses * 100.0f / crashesDetected) : 100.0f; + ASSERT_GT(recoveryRate, 95.0f, "Recovery rate should be > 95%"); + reporter.addMetric("recovery_rate_percent", recoveryRate); + + // === STATISTICS === + std::cout << "\n"; + std::cout << "================================================================================\n"; + std::cout << "CHAOS MONKEY STATISTICS\n"; + std::cout << "================================================================================\n"; + std::cout << " Total frames: " << totalFrames << "\n"; + std::cout << " Duration: " << totalDuration << "s (wall time, not simulation time)\n"; + std::cout << " Crashes detected: " << crashesDetected << "\n"; + std::cout << " Recovery successes: " << recoverySuccesses << "\n"; + std::cout << " Recovery rate: " << recoveryRate << "%\n"; + std::cout << " Memory growth: " << memGrowthMB << " MB (max: 10MB)\n"; + std::cout << " Had deadlocks: " << (hadDeadlock ? "YES ❌" : "NO ✅") << "\n"; + std::cout << " FPS min/avg/max: " << minFPS << " / " << avgFPS << " / " << maxFPS << "\n"; + std::cout << "================================================================================\n\n"; + + std::cout << "Note: ChaosModule generates random crashes internally.\n"; + std::cout << "The test should recover from ALL crashes automatically via hot-reload.\n\n"; + + // === CLEANUP === + std::cout << "Cleaning up...\n"; + moduleSystem.reset(); + std::cout << " ✓ Module system shutdown complete\n\n"; + + // === REPORTS === + metrics.printReport(); + reporter.printFinalReport(); + + return reporter.getExitCode(); +} diff --git a/tests/integration/test_03_stress_test.cpp b/tests/integration/test_03_stress_test.cpp index 326c6f3..e8b9a5f 100644 --- a/tests/integration/test_03_stress_test.cpp +++ b/tests/integration/test_03_stress_test.cpp @@ -1,247 +1,253 @@ -/** - * @file test_03_stress_test.cpp - * @brief Scenario 3: Stress Test - Long-duration stability validation - * - * OBJECTIVE: - * Validate hot-reload system stability over extended duration with repeated reloads. - * - * TEST PARAMETERS: - * - Duration: 10 minutes (36000 frames @ 60 FPS) - * - Reload frequency: Every 5 seconds (300 frames) - * - Total reloads: 120 - * - No random crashes - focus on hot-reload stability - * - * SUCCESS CRITERIA: - * ✅ All 120 reloads succeed - * ✅ Memory growth < 50MB over 10 minutes - * ✅ Average reload time < 500ms - * ✅ FPS remains stable (no degradation) - * ✅ No file descriptor leaks - * ✅ State preserved across all reloads - * - * WHAT THIS VALIDATES: - * - No memory leaks in hot-reload system - * - No file descriptor leaks (dlopen/dlclose) - * - Reload performance doesn't degrade over time - * - State preservation is reliable at scale - * - System remains stable under repeated reload stress - */ - -#include "grove/ModuleLoader.h" -#include "grove/SequentialModuleSystem.h" -#include "grove/JsonDataNode.h" -#include "../helpers/TestMetrics.h" -#include "../helpers/TestAssertions.h" -#include "../helpers/TestReporter.h" -#include "../helpers/SystemUtils.h" - -#include -#include -#include - -using namespace grove; - -// Test configuration -constexpr int TARGET_FPS = 60; -constexpr float FRAME_TIME = 1.0f / TARGET_FPS; -constexpr int RELOAD_INTERVAL = 300; // Reload every 5 seconds (300 frames) -constexpr int EXPECTED_RELOADS = 120; // 120 reloads -constexpr int TOTAL_FRAMES = EXPECTED_RELOADS * RELOAD_INTERVAL; // 36000 frames = 10 minutes @ 60 FPS - -// Memory threshold -constexpr size_t MAX_MEMORY_GROWTH_MB = 50; - -// Paths -const std::string MODULE_PATH = "./libStressModule.so"; - -int main() { - TestReporter reporter("Stress Test - 10 Minute Stability"); - TestMetrics metrics; - - std::cout << "═══════════════════════════════════════════════════════════════\n"; - std::cout << " SCENARIO 3: STRESS TEST - LONG DURATION STABILITY\n"; - std::cout << "═══════════════════════════════════════════════════════════════\n"; - std::cout << "Duration: 10 minutes (" << TOTAL_FRAMES << " frames @ " << TARGET_FPS << " FPS)\n"; - std::cout << "Reload interval: Every " << RELOAD_INTERVAL << " frames (5 seconds)\n"; - std::cout << "Expected reloads: " << EXPECTED_RELOADS << "\n"; - std::cout << "Memory threshold: < " << MAX_MEMORY_GROWTH_MB << " MB growth\n"; - std::cout << "═══════════════════════════════════════════════════════════════\n\n"; - - size_t initialMemory = grove::getCurrentMemoryUsage() / (1024 * 1024); - size_t peakMemory = initialMemory; - - int successfulReloads = 0; - int failedReloads = 0; - - try { - // === SETUP === - std::cout << "Setup: Loading StressModule...\n"; - - ModuleLoader loader; - auto moduleSystem = std::make_unique(); - - // Load module - auto module = loader.load(MODULE_PATH, "StressModule", false); - - // Configure module with empty config - nlohmann::json configJson; - auto config = std::make_unique("config", configJson); - - module->setConfiguration(*config, nullptr, nullptr); - - // Register in module system - moduleSystem->registerModule("StressModule", std::move(module)); - - std::cout << " ✓ StressModule loaded and configured\n\n"; - - std::cout << "🚀 Starting 10-minute stress test...\n\n"; - - auto startTime = std::chrono::high_resolution_clock::now(); - - // Main simulation loop - for (int frame = 1; frame <= TOTAL_FRAMES; ++frame) { - auto frameStart = std::chrono::high_resolution_clock::now(); - - // Process modules - try { - moduleSystem->processModules(FRAME_TIME); - } catch (const std::exception& e) { - std::cerr << " [Frame " << frame << "] ❌ Unexpected error during process: " << e.what() << "\n"; - reporter.addAssertion("process_error", false); - break; - } - - auto frameEnd = std::chrono::high_resolution_clock::now(); - auto frameDuration = std::chrono::duration(frameEnd - frameStart).count(); - float fps = frameDuration > 0.0f ? 1000.0f / frameDuration : 0.0f; - metrics.recordFPS(fps); - - // Hot-reload every RELOAD_INTERVAL frames - if (frame % RELOAD_INTERVAL == 0) { - int reloadNumber = frame / RELOAD_INTERVAL; - std::cout << " [Frame " << frame << "/" << TOTAL_FRAMES << "] 🔄 Triggering hot-reload #" << reloadNumber << "...\n"; - - auto reloadStart = std::chrono::high_resolution_clock::now(); - - try { - // Extract module from system - auto extractedModule = moduleSystem->extractModule(); - if (!extractedModule) { - std::cerr << " ❌ Failed to extract StressModule\n"; - failedReloads++; - continue; - } - - // Perform hot-reload - auto reloadedModule = loader.reload(std::move(extractedModule)); - - // Re-register reloaded module - moduleSystem->registerModule("StressModule", std::move(reloadedModule)); - - auto reloadEnd = std::chrono::high_resolution_clock::now(); - auto reloadDuration = std::chrono::duration_cast( - reloadEnd - reloadStart).count(); - - metrics.recordReloadTime(static_cast(reloadDuration)); - successfulReloads++; - - std::cout << " ✅ Hot-reload #" << reloadNumber << " succeeded in " << reloadDuration << "ms\n"; - - } catch (const std::exception& e) { - std::cerr << " ❌ Exception during hot-reload: " << e.what() << "\n"; - failedReloads++; - } - } - - // Memory monitoring every 60 seconds (3600 frames) - if (frame % 3600 == 0 && frame > 0) { - size_t currentMemory = grove::getCurrentMemoryUsage() / (1024 * 1024); - size_t memoryGrowth = currentMemory - initialMemory; - peakMemory = std::max(peakMemory, currentMemory); - - int minutesElapsed = frame / 3600; - std::cout << "\n📊 Checkpoint at " << minutesElapsed << " minute(s):\n"; - std::cout << " Current memory: " << currentMemory << " MB\n"; - std::cout << " Growth: " << memoryGrowth << " MB\n"; - std::cout << " Peak: " << peakMemory << " MB\n"; - std::cout << " Avg FPS: " << metrics.getFPSAvg() << "\n"; - std::cout << " Reloads: " << successfulReloads << "/" << EXPECTED_RELOADS << "\n"; - std::cout << " Avg reload time: " << metrics.getReloadTimeAvg() << "ms\n\n"; - } - - // Progress reporting every minute (without memory details) - if (frame % 3600 == 0 && frame > 0) { - int minutesElapsed = frame / 3600; - int minutesRemaining = (TOTAL_FRAMES - frame) / 3600; - std::cout << "⏱️ Progress: " << minutesElapsed << " minutes elapsed, " << minutesRemaining << " minutes remaining\n"; - } - } - - auto endTime = std::chrono::high_resolution_clock::now(); - auto totalDuration = std::chrono::duration_cast( - endTime - startTime).count(); - - // Final metrics - size_t finalMemory = grove::getCurrentMemoryUsage() / (1024 * 1024); - size_t totalMemoryGrowth = finalMemory - initialMemory; - - std::cout << "\n═══════════════════════════════════════════════════════════════\n"; - std::cout << " STRESS TEST COMPLETED\n"; - std::cout << "═══════════════════════════════════════════════════════════════\n"; - std::cout << "Total frames: " << TOTAL_FRAMES << "\n"; - std::cout << "Real time: " << totalDuration << "s\n"; - std::cout << "Simulated time: " << (TOTAL_FRAMES / TARGET_FPS) << "s (10 minutes)\n"; - std::cout << "Successful reloads: " << successfulReloads << "/" << EXPECTED_RELOADS << "\n"; - std::cout << "Failed reloads: " << failedReloads << "\n"; - std::cout << "\n📊 PERFORMANCE METRICS:\n"; - std::cout << "Average FPS: " << metrics.getFPSAvg() << "\n"; - std::cout << "Min FPS: " << metrics.getFPSMin() << "\n"; - std::cout << "Max FPS: " << metrics.getFPSMax() << "\n"; - std::cout << "\n🔥 HOT-RELOAD METRICS:\n"; - std::cout << "Avg reload time: " << metrics.getReloadTimeAvg() << "ms\n"; - std::cout << "Min reload time: " << metrics.getReloadTimeMin() << "ms\n"; - std::cout << "Max reload time: " << metrics.getReloadTimeMax() << "ms\n"; - std::cout << "\n💾 MEMORY METRICS:\n"; - std::cout << "Initial memory: " << initialMemory << " MB\n"; - std::cout << "Final memory: " << finalMemory << " MB\n"; - std::cout << "Peak memory: " << peakMemory << " MB\n"; - std::cout << "Total growth: " << totalMemoryGrowth << " MB\n"; - std::cout << "═══════════════════════════════════════════════════════════════\n\n"; - - // Validate results - bool allReloadsSucceeded = (successfulReloads == EXPECTED_RELOADS && failedReloads == 0); - bool memoryWithinThreshold = (totalMemoryGrowth < MAX_MEMORY_GROWTH_MB); - bool avgReloadTimeAcceptable = (metrics.getReloadTimeAvg() < 500.0f); - bool fpsStable = (metrics.getFPSMin() > 30.0f); // Ensure FPS doesn't drop too much - - reporter.addAssertion("all_reloads_succeeded", allReloadsSucceeded); - reporter.addAssertion("memory_within_threshold", memoryWithinThreshold); - reporter.addAssertion("avg_reload_time_acceptable", avgReloadTimeAcceptable); - reporter.addAssertion("fps_stable", fpsStable); - - if (allReloadsSucceeded && memoryWithinThreshold && - avgReloadTimeAcceptable && fpsStable) { - std::cout << "✅ STRESS TEST PASSED - System is stable over 10 minutes\n"; - } else { - if (!allReloadsSucceeded) { - std::cerr << "❌ Reload success rate: " << successfulReloads << "/" << EXPECTED_RELOADS << "\n"; - } - if (!memoryWithinThreshold) { - std::cerr << "❌ Memory growth: " << totalMemoryGrowth << " MB (threshold: " << MAX_MEMORY_GROWTH_MB << " MB)\n"; - } - if (!avgReloadTimeAcceptable) { - std::cerr << "❌ Avg reload time: " << metrics.getReloadTimeAvg() << "ms (threshold: 500ms)\n"; - } - if (!fpsStable) { - std::cerr << "❌ Min FPS: " << metrics.getFPSMin() << " (threshold: 30.0)\n"; - } - } - - } catch (const std::exception& e) { - std::cerr << "Test failed with exception: " << e.what() << "\n"; - reporter.addAssertion("exception", false); - } - - reporter.printFinalReport(); - return reporter.getExitCode(); -} +/** + * @file test_03_stress_test.cpp + * @brief Scenario 3: Stress Test - Long-duration stability validation + * + * OBJECTIVE: + * Validate hot-reload system stability over extended duration with repeated reloads. + * + * TEST PARAMETERS: + * - Duration: 10 minutes (36000 frames @ 60 FPS) + * - Reload frequency: Every 5 seconds (300 frames) + * - Total reloads: 120 + * - No random crashes - focus on hot-reload stability + * + * SUCCESS CRITERIA: + * ✅ All 120 reloads succeed + * ✅ Memory growth < 50MB over 10 minutes + * ✅ Average reload time < 500ms + * ✅ FPS remains stable (no degradation) + * ✅ No file descriptor leaks + * ✅ State preserved across all reloads + * + * WHAT THIS VALIDATES: + * - No memory leaks in hot-reload system + * - No file descriptor leaks (dlopen/dlclose) + * - Reload performance doesn't degrade over time + * - State preservation is reliable at scale + * - System remains stable under repeated reload stress + */ + +#include "grove/ModuleLoader.h" +#include "grove/SequentialModuleSystem.h" +#include "grove/JsonDataNode.h" +#include "../helpers/TestMetrics.h" +#include "../helpers/TestAssertions.h" +#include "../helpers/TestReporter.h" +#include "../helpers/SystemUtils.h" + +#include +#include +#include + +using namespace grove; + +// Test configuration +constexpr int TARGET_FPS = 60; +constexpr float FRAME_TIME = 1.0f / TARGET_FPS; +constexpr int RELOAD_INTERVAL = 300; // Reload every 5 seconds (300 frames) +constexpr int EXPECTED_RELOADS = 120; // 120 reloads +constexpr int TOTAL_FRAMES = EXPECTED_RELOADS * RELOAD_INTERVAL; // 36000 frames = 10 minutes @ 60 FPS + +// Memory threshold +constexpr size_t MAX_MEMORY_GROWTH_MB = 50; + +// Paths +#ifdef _WIN32 +const std::string MODULE_PATH = "./libStressModule.dll"; +#else +const std::string MODULE_PATH = "./libStressModule.so"; +#endif +#ifdef _WIN32 +#endif + +int main() { + TestReporter reporter("Stress Test - 10 Minute Stability"); + TestMetrics metrics; + + std::cout << "═══════════════════════════════════════════════════════════════\n"; + std::cout << " SCENARIO 3: STRESS TEST - LONG DURATION STABILITY\n"; + std::cout << "═══════════════════════════════════════════════════════════════\n"; + std::cout << "Duration: 10 minutes (" << TOTAL_FRAMES << " frames @ " << TARGET_FPS << " FPS)\n"; + std::cout << "Reload interval: Every " << RELOAD_INTERVAL << " frames (5 seconds)\n"; + std::cout << "Expected reloads: " << EXPECTED_RELOADS << "\n"; + std::cout << "Memory threshold: < " << MAX_MEMORY_GROWTH_MB << " MB growth\n"; + std::cout << "═══════════════════════════════════════════════════════════════\n\n"; + + size_t initialMemory = grove::getCurrentMemoryUsage() / (1024 * 1024); + size_t peakMemory = initialMemory; + + int successfulReloads = 0; + int failedReloads = 0; + + try { + // === SETUP === + std::cout << "Setup: Loading StressModule...\n"; + + ModuleLoader loader; + auto moduleSystem = std::make_unique(); + + // Load module + auto module = loader.load(MODULE_PATH, "StressModule", false); + + // Configure module with empty config + nlohmann::json configJson; + auto config = std::make_unique("config", configJson); + + module->setConfiguration(*config, nullptr, nullptr); + + // Register in module system + moduleSystem->registerModule("StressModule", std::move(module)); + + std::cout << " ✓ StressModule loaded and configured\n\n"; + + std::cout << "🚀 Starting 10-minute stress test...\n\n"; + + auto startTime = std::chrono::high_resolution_clock::now(); + + // Main simulation loop + for (int frame = 1; frame <= TOTAL_FRAMES; ++frame) { + auto frameStart = std::chrono::high_resolution_clock::now(); + + // Process modules + try { + moduleSystem->processModules(FRAME_TIME); + } catch (const std::exception& e) { + std::cerr << " [Frame " << frame << "] ❌ Unexpected error during process: " << e.what() << "\n"; + reporter.addAssertion("process_error", false); + break; + } + + auto frameEnd = std::chrono::high_resolution_clock::now(); + auto frameDuration = std::chrono::duration(frameEnd - frameStart).count(); + float fps = frameDuration > 0.0f ? 1000.0f / frameDuration : 0.0f; + metrics.recordFPS(fps); + + // Hot-reload every RELOAD_INTERVAL frames + if (frame % RELOAD_INTERVAL == 0) { + int reloadNumber = frame / RELOAD_INTERVAL; + std::cout << " [Frame " << frame << "/" << TOTAL_FRAMES << "] 🔄 Triggering hot-reload #" << reloadNumber << "...\n"; + + auto reloadStart = std::chrono::high_resolution_clock::now(); + + try { + // Extract module from system + auto extractedModule = moduleSystem->extractModule(); + if (!extractedModule) { + std::cerr << " ❌ Failed to extract StressModule\n"; + failedReloads++; + continue; + } + + // Perform hot-reload + auto reloadedModule = loader.reload(std::move(extractedModule)); + + // Re-register reloaded module + moduleSystem->registerModule("StressModule", std::move(reloadedModule)); + + auto reloadEnd = std::chrono::high_resolution_clock::now(); + auto reloadDuration = std::chrono::duration_cast( + reloadEnd - reloadStart).count(); + + metrics.recordReloadTime(static_cast(reloadDuration)); + successfulReloads++; + + std::cout << " ✅ Hot-reload #" << reloadNumber << " succeeded in " << reloadDuration << "ms\n"; + + } catch (const std::exception& e) { + std::cerr << " ❌ Exception during hot-reload: " << e.what() << "\n"; + failedReloads++; + } + } + + // Memory monitoring every 60 seconds (3600 frames) + if (frame % 3600 == 0 && frame > 0) { + size_t currentMemory = grove::getCurrentMemoryUsage() / (1024 * 1024); + size_t memoryGrowth = currentMemory - initialMemory; + peakMemory = std::max(peakMemory, currentMemory); + + int minutesElapsed = frame / 3600; + std::cout << "\n📊 Checkpoint at " << minutesElapsed << " minute(s):\n"; + std::cout << " Current memory: " << currentMemory << " MB\n"; + std::cout << " Growth: " << memoryGrowth << " MB\n"; + std::cout << " Peak: " << peakMemory << " MB\n"; + std::cout << " Avg FPS: " << metrics.getFPSAvg() << "\n"; + std::cout << " Reloads: " << successfulReloads << "/" << EXPECTED_RELOADS << "\n"; + std::cout << " Avg reload time: " << metrics.getReloadTimeAvg() << "ms\n\n"; + } + + // Progress reporting every minute (without memory details) + if (frame % 3600 == 0 && frame > 0) { + int minutesElapsed = frame / 3600; + int minutesRemaining = (TOTAL_FRAMES - frame) / 3600; + std::cout << "⏱️ Progress: " << minutesElapsed << " minutes elapsed, " << minutesRemaining << " minutes remaining\n"; + } + } + + auto endTime = std::chrono::high_resolution_clock::now(); + auto totalDuration = std::chrono::duration_cast( + endTime - startTime).count(); + + // Final metrics + size_t finalMemory = grove::getCurrentMemoryUsage() / (1024 * 1024); + size_t totalMemoryGrowth = finalMemory - initialMemory; + + std::cout << "\n═══════════════════════════════════════════════════════════════\n"; + std::cout << " STRESS TEST COMPLETED\n"; + std::cout << "═══════════════════════════════════════════════════════════════\n"; + std::cout << "Total frames: " << TOTAL_FRAMES << "\n"; + std::cout << "Real time: " << totalDuration << "s\n"; + std::cout << "Simulated time: " << (TOTAL_FRAMES / TARGET_FPS) << "s (10 minutes)\n"; + std::cout << "Successful reloads: " << successfulReloads << "/" << EXPECTED_RELOADS << "\n"; + std::cout << "Failed reloads: " << failedReloads << "\n"; + std::cout << "\n📊 PERFORMANCE METRICS:\n"; + std::cout << "Average FPS: " << metrics.getFPSAvg() << "\n"; + std::cout << "Min FPS: " << metrics.getFPSMin() << "\n"; + std::cout << "Max FPS: " << metrics.getFPSMax() << "\n"; + std::cout << "\n🔥 HOT-RELOAD METRICS:\n"; + std::cout << "Avg reload time: " << metrics.getReloadTimeAvg() << "ms\n"; + std::cout << "Min reload time: " << metrics.getReloadTimeMin() << "ms\n"; + std::cout << "Max reload time: " << metrics.getReloadTimeMax() << "ms\n"; + std::cout << "\n💾 MEMORY METRICS:\n"; + std::cout << "Initial memory: " << initialMemory << " MB\n"; + std::cout << "Final memory: " << finalMemory << " MB\n"; + std::cout << "Peak memory: " << peakMemory << " MB\n"; + std::cout << "Total growth: " << totalMemoryGrowth << " MB\n"; + std::cout << "═══════════════════════════════════════════════════════════════\n\n"; + + // Validate results + bool allReloadsSucceeded = (successfulReloads == EXPECTED_RELOADS && failedReloads == 0); + bool memoryWithinThreshold = (totalMemoryGrowth < MAX_MEMORY_GROWTH_MB); + bool avgReloadTimeAcceptable = (metrics.getReloadTimeAvg() < 500.0f); + bool fpsStable = (metrics.getFPSMin() > 30.0f); // Ensure FPS doesn't drop too much + + reporter.addAssertion("all_reloads_succeeded", allReloadsSucceeded); + reporter.addAssertion("memory_within_threshold", memoryWithinThreshold); + reporter.addAssertion("avg_reload_time_acceptable", avgReloadTimeAcceptable); + reporter.addAssertion("fps_stable", fpsStable); + + if (allReloadsSucceeded && memoryWithinThreshold && + avgReloadTimeAcceptable && fpsStable) { + std::cout << "✅ STRESS TEST PASSED - System is stable over 10 minutes\n"; + } else { + if (!allReloadsSucceeded) { + std::cerr << "❌ Reload success rate: " << successfulReloads << "/" << EXPECTED_RELOADS << "\n"; + } + if (!memoryWithinThreshold) { + std::cerr << "❌ Memory growth: " << totalMemoryGrowth << " MB (threshold: " << MAX_MEMORY_GROWTH_MB << " MB)\n"; + } + if (!avgReloadTimeAcceptable) { + std::cerr << "❌ Avg reload time: " << metrics.getReloadTimeAvg() << "ms (threshold: 500ms)\n"; + } + if (!fpsStable) { + std::cerr << "❌ Min FPS: " << metrics.getFPSMin() << " (threshold: 30.0)\n"; + } + } + + } catch (const std::exception& e) { + std::cerr << "Test failed with exception: " << e.what() << "\n"; + reporter.addAssertion("exception", false); + } + + reporter.printFinalReport(); + return reporter.getExitCode(); +} diff --git a/tests/integration/test_04_race_condition.cpp b/tests/integration/test_04_race_condition.cpp index abda7c1..22d634a 100644 --- a/tests/integration/test_04_race_condition.cpp +++ b/tests/integration/test_04_race_condition.cpp @@ -1,390 +1,393 @@ -#include "grove/ModuleLoader.h" -#include "grove/SequentialModuleSystem.h" -#include "grove/JsonDataNode.h" -#include "../helpers/TestMetrics.h" -#include "../helpers/TestAssertions.h" -#include "../helpers/TestReporter.h" -#include "../helpers/SystemUtils.h" -#include "../helpers/AutoCompiler.h" -#include -#include -#include -#include -#include -#include - -using namespace grove; -using namespace TestHelpers; - -int main() { - TestReporter reporter("Race Condition Hunter"); - - std::cout << "================================================================================\n"; - std::cout << "TEST: Race Condition Hunter - Concurrent Compilation & Reload\n"; - std::cout << "================================================================================\n\n"; - - // === CONFIGURATION === - const int TOTAL_COMPILATIONS = 10; // Reduced for WSL2 compatibility - const int COMPILE_INTERVAL_MS = 2000; // 2 seconds between compilations (allows for slower filesystems) - const int FILE_CHECK_INTERVAL_MS = 50; // Check file changes every 50ms - const float TARGET_FPS = 60.0f; - const float FRAME_TIME = 1.0f / TARGET_FPS; - - std::string modulePath = "./libTestModule.so"; - // Test runs from build/tests/, so source files are at ../../tests/modules/ - std::string sourcePath = "../../tests/modules/TestModule.cpp"; - std::string buildDir = "build"; - - // === ATOMIC COUNTERS (Thread-safe) === - std::atomic reloadAttempts{0}; - std::atomic reloadSuccesses{0}; - std::atomic reloadFailures{0}; - std::atomic corruptedLoads{0}; - std::atomic crashes{0}; - std::atomic engineRunning{true}; - std::atomic watcherRunning{true}; - - // CRITICAL: Mutex to protect moduleSystem access between threads - std::mutex moduleSystemMutex; - - // Reload timing - std::mutex reloadTimesMutex; - std::vector reloadTimes; - - // Metrics - TestMetrics metrics; - - // === SETUP === - std::cout << "Setup:\n"; - std::cout << " Module path: " << modulePath << "\n"; - std::cout << " Source path: " << sourcePath << "\n"; - std::cout << " Compilations: " << TOTAL_COMPILATIONS << "\n"; - std::cout << " Interval: " << COMPILE_INTERVAL_MS << "ms\n"; - std::cout << " Expected time: ~" << (TOTAL_COMPILATIONS * COMPILE_INTERVAL_MS / 1000) << "s\n\n"; - - // Load module initially - ModuleLoader loader; - auto moduleSystem = std::make_unique(); - - try { - auto module = loader.load(modulePath, "TestModule", false); - nlohmann::json configJson = nlohmann::json::object(); - auto config = std::make_unique("config", configJson); - module->setConfiguration(*config, nullptr, nullptr); - moduleSystem->registerModule("TestModule", std::move(module)); - std::cout << " ✓ Initial module loaded\n\n"; - } catch (const std::exception& e) { - std::cerr << "❌ Failed to load initial module: " << e.what() << "\n"; - return 1; - } - - // === THREAD 1: AUTO-COMPILER === - std::cout << "Starting AutoCompiler thread...\n"; - AutoCompiler compiler("TestModule", buildDir, sourcePath); - compiler.start(TOTAL_COMPILATIONS, COMPILE_INTERVAL_MS); - - // === THREAD 2: FILE WATCHER === - std::cout << "Starting FileWatcher thread...\n"; - std::thread watcherThread([&]() { - try { - auto lastWriteTime = std::filesystem::last_write_time(modulePath); - - while (watcherRunning.load() && engineRunning.load()) { - try { - auto currentTime = std::filesystem::last_write_time(modulePath); - - if (currentTime != lastWriteTime) { - reloadAttempts++; - - // Measure reload time - auto reloadStart = std::chrono::high_resolution_clock::now(); - - try { - // CRITICAL: Lock moduleSystem during entire reload - std::lock_guard lock(moduleSystemMutex); - - // Extract module and save state - auto module = moduleSystem->extractModule(); - auto state = module->getState(); - - // CRITICAL: Destroy old module BEFORE reloading - // The loader.load() will unload the old .so - module.reset(); - - // Reload - auto newModule = loader.load(modulePath, "TestModule", true); - - // Check if module loaded correctly - if (!newModule) { - corruptedLoads++; - reloadFailures++; - // Can't recover - old module already destroyed - } else { - // VALIDATE MODULE INTEGRITY - bool isCorrupted = false; - try { - // Test 1: Can we get health status? - auto health = newModule->getHealthStatus(); - std::string version = health->getString("version", ""); - - // Test 2: Is version valid? - if (version.empty() || version == "unknown") { - isCorrupted = true; - } - - // Test 3: Can we set configuration? - nlohmann::json configJson; - configJson["test"] = "validation"; - auto testConfig = std::make_unique("config", configJson); - newModule->setConfiguration(*testConfig, nullptr, nullptr); - - } catch (const std::exception& e) { - // Module crashes on basic operations = corrupted - isCorrupted = true; - } - - if (isCorrupted) { - corruptedLoads++; - reloadFailures++; - // Can't recover - old module already destroyed - } else { - // Module is valid, restore state and register - newModule->setState(*state); - moduleSystem->registerModule("TestModule", std::move(newModule)); - reloadSuccesses++; - - // Record reload time - auto reloadEnd = std::chrono::high_resolution_clock::now(); - float reloadTimeMs = std::chrono::duration(reloadEnd - reloadStart).count(); - - { - std::lock_guard timeLock(reloadTimesMutex); - reloadTimes.push_back(reloadTimeMs); - } - } - } - } catch (const std::exception& e) { - reloadFailures++; - // Module might already be registered, continue - } - - lastWriteTime = currentTime; - } - } catch (const std::filesystem::filesystem_error&) { - // File might be being written, ignore - } - - std::this_thread::sleep_for(std::chrono::milliseconds(FILE_CHECK_INTERVAL_MS)); - } - } catch (const std::exception& e) { - std::cerr << "[FileWatcher] Exception: " << e.what() << "\n"; - } - }); - - // === THREAD 3: ENGINE LOOP === - std::cout << "Starting Engine thread (60 FPS)...\n\n"; - std::thread engineThread([&]() { - try { - auto lastMemoryCheck = std::chrono::steady_clock::now(); - - while (engineRunning.load()) { - auto frameStart = std::chrono::high_resolution_clock::now(); - - try { - // TRY to lock moduleSystem (non-blocking) - // If reload is happening, skip this frame - if (moduleSystemMutex.try_lock()) { - try { - moduleSystem->processModules(FRAME_TIME); - moduleSystemMutex.unlock(); - } catch (const std::exception& e) { - moduleSystemMutex.unlock(); - throw; - } - } - // else: reload in progress, skip frame - } catch (const std::exception& e) { - crashes++; - std::cerr << "[Engine] Crash detected: " << e.what() << "\n"; - } - - auto frameEnd = std::chrono::high_resolution_clock::now(); - float frameTime = std::chrono::duration(frameEnd - frameStart).count(); - metrics.recordFPS(1000.0f / std::max(frameTime, 0.1f)); - - // Check memory every second - auto now = std::chrono::steady_clock::now(); - if (std::chrono::duration_cast(now - lastMemoryCheck).count() >= 1) { - metrics.recordMemoryUsage(getCurrentMemoryUsage()); - lastMemoryCheck = now; - } - - // Sleep to maintain target FPS (if frame finished early) - auto targetFrameTime = std::chrono::milliseconds(static_cast(FRAME_TIME * 1000)); - auto elapsed = frameEnd - frameStart; - if (elapsed < targetFrameTime) { - std::this_thread::sleep_for(targetFrameTime - elapsed); - } - } - } catch (const std::exception& e) { - std::cerr << "[Engine] Thread exception: " << e.what() << "\n"; - } - }); - - // === MONITORING LOOP === - std::cout << "Test running...\n"; - auto startTime = std::chrono::steady_clock::now(); - int lastPrintedPercent = 0; - const int MAX_TEST_TIME_SECONDS = 90; // Maximum 1.5 minutes (allows all 20 compilations) - - while (compiler.isRunning() || compiler.getCurrentIteration() < TOTAL_COMPILATIONS) { - std::this_thread::sleep_for(std::chrono::seconds(2)); - - int currentIteration = compiler.getCurrentIteration(); - int percent = (currentIteration * 100) / TOTAL_COMPILATIONS; - - // Check for timeout - auto now = std::chrono::steady_clock::now(); - auto elapsed = std::chrono::duration_cast(now - startTime).count(); - - if (elapsed > MAX_TEST_TIME_SECONDS) { - std::cout << "\n⚠️ Test timeout after " << elapsed << "s - stopping...\n"; - break; - } - - // Print progress every 10% - if (percent >= lastPrintedPercent + 10 && percent <= 100) { - std::cout << "\nProgress: " << percent << "% (" << currentIteration << "/" << TOTAL_COMPILATIONS << " compilations)\n"; - std::cout << " Elapsed: " << elapsed << "s\n"; - std::cout << " Compilations: " << compiler.getSuccessCount() << " OK, " << compiler.getFailureCount() << " FAIL\n"; - std::cout << " Reloads: " << reloadSuccesses.load() << " OK, " << reloadFailures.load() << " FAIL\n"; - std::cout << " Corrupted: " << corruptedLoads.load() << "\n"; - std::cout << " Crashes: " << crashes.load() << "\n"; - - lastPrintedPercent = percent; - } - } - - // === CLEANUP === - std::cout << "\n\nStopping threads...\n"; - - compiler.stop(); - watcherRunning = false; - engineRunning = false; - - if (watcherThread.joinable()) { - watcherThread.join(); - } - if (engineThread.joinable()) { - engineThread.join(); - } - - std::cout << " ✓ All threads stopped\n\n"; - - // === CALCULATE STATISTICS === - float compileSuccessRate = (compiler.getSuccessCount() * 100.0f) / std::max(1, TOTAL_COMPILATIONS); - float reloadSuccessRate = (reloadSuccesses.load() * 100.0f) / std::max(1, reloadAttempts.load()); - - float avgReloadTime = 0.0f; - { - std::lock_guard lock(reloadTimesMutex); - if (!reloadTimes.empty()) { - float sum = 0.0f; - for (float t : reloadTimes) { - sum += t; - } - avgReloadTime = sum / reloadTimes.size(); - } - } - - auto endTime = std::chrono::steady_clock::now(); - auto totalTimeSeconds = std::chrono::duration_cast(endTime - startTime).count(); - - // === PRINT SUMMARY === - std::cout << "================================================================================\n"; - std::cout << "RACE CONDITION HUNTER SUMMARY\n"; - std::cout << "================================================================================\n\n"; - - std::cout << "Duration: " << totalTimeSeconds << "s\n\n"; - - std::cout << "Compilations:\n"; - std::cout << " Total: " << TOTAL_COMPILATIONS << "\n"; - std::cout << " Successes: " << compiler.getSuccessCount() << " (" << std::fixed << std::setprecision(1) << compileSuccessRate << "%)\n"; - std::cout << " Failures: " << compiler.getFailureCount() << "\n\n"; - - std::cout << "Reloads:\n"; - std::cout << " Attempts: " << reloadAttempts.load() << "\n"; - std::cout << " Successes: " << reloadSuccesses.load() << " (" << std::fixed << std::setprecision(1) << reloadSuccessRate << "%)\n"; - std::cout << " Failures: " << reloadFailures.load() << "\n"; - std::cout << " Corrupted: " << corruptedLoads.load() << "\n"; - std::cout << " Avg time: " << std::fixed << std::setprecision(0) << avgReloadTime << "ms\n\n"; - - std::cout << "Stability:\n"; - std::cout << " Crashes: " << crashes.load() << "\n"; - std::cout << " Avg FPS: " << std::fixed << std::setprecision(1) << metrics.getFPSAvg() << "\n"; - std::cout << " Memory: " << std::fixed << std::setprecision(2) << metrics.getMemoryGrowth() << " MB\n\n"; - - // === ASSERTIONS === - bool passed = true; - - std::cout << "Validating results...\n"; - - // MUST PASS criteria - // Note: Lowered from 95% to 70% for WSL2/slower filesystem compatibility - // The important thing is that compilations don't fail, they just might timeout - if (compileSuccessRate < 70.0f) { - std::cout << " ❌ Compile success rate too low: " << compileSuccessRate << "% (need > 70%)\n"; - passed = false; - } else { - std::cout << " ✓ Compile success rate: " << compileSuccessRate << "%\n"; - } - - if (corruptedLoads.load() > 0) { - std::cout << " ❌ Corrupted loads detected: " << corruptedLoads.load() << " (need 0)\n"; - passed = false; - } else { - std::cout << " ✓ No corrupted loads\n"; - } - - if (crashes.load() > 0) { - std::cout << " ❌ Crashes detected: " << crashes.load() << " (need 0)\n"; - passed = false; - } else { - std::cout << " ✓ No crashes\n"; - } - - if (reloadAttempts.load() > 0 && reloadSuccessRate < 99.0f) { - std::cout << " ❌ Reload success rate too low: " << reloadSuccessRate << "% (need > 99%)\n"; - passed = false; - } else if (reloadAttempts.load() > 0) { - std::cout << " ✓ Reload success rate: " << reloadSuccessRate << "%\n"; - } - - // File stability validation: reload time should be >= 100ms - // This proves that ModuleLoader is waiting for file stability - if (reloadAttempts.load() > 0) { - if (avgReloadTime < 100.0f) { - std::cout << " ❌ Reload time too fast: " << avgReloadTime << "ms (need >= 100ms)\n"; - std::cout << " File stability check is NOT working properly!\n"; - passed = false; - } else if (avgReloadTime > 600.0f) { - std::cout << " ⚠️ Reload time very slow: " << avgReloadTime << "ms (> 600ms)\n"; - std::cout << " File stability might be waiting too long\n"; - } else { - std::cout << " ✓ Reload time: " << avgReloadTime << "ms (file stability working)\n"; - } - } - - std::cout << "\n"; - - // === FINAL RESULT === - std::cout << "================================================================================\n"; - if (passed) { - std::cout << "Result: ✅ PASSED\n"; - } else { - std::cout << "Result: ❌ FAILED\n"; - } - std::cout << "================================================================================\n"; - - return passed ? 0 : 1; -} +#include "grove/ModuleLoader.h" +#include "grove/SequentialModuleSystem.h" +#include "grove/JsonDataNode.h" +#include "../helpers/TestMetrics.h" +#include "../helpers/TestAssertions.h" +#include "../helpers/TestReporter.h" +#include "../helpers/SystemUtils.h" +#include "../helpers/AutoCompiler.h" +#include +#include +#include +#include +#include +#include + +using namespace grove; +using namespace TestHelpers; + +int main() { + TestReporter reporter("Race Condition Hunter"); + + std::cout << "================================================================================\n"; + std::cout << "TEST: Race Condition Hunter - Concurrent Compilation & Reload\n"; + std::cout << "================================================================================\n\n"; + + // === CONFIGURATION === + const int TOTAL_COMPILATIONS = 10; // Reduced for WSL2 compatibility + const int COMPILE_INTERVAL_MS = 2000; // 2 seconds between compilations (allows for slower filesystems) + const int FILE_CHECK_INTERVAL_MS = 50; // Check file changes every 50ms + const float TARGET_FPS = 60.0f; + const float FRAME_TIME = 1.0f / TARGET_FPS; + + std::string modulePath = "./libTestModule.so"; +#ifdef _WIN32 + modulePath = "./libTestModule.dll"; +#endif + // Test runs from build/tests/, so source files are at ../../tests/modules/ + std::string sourcePath = "../../tests/modules/TestModule.cpp"; + std::string buildDir = "build"; + + // === ATOMIC COUNTERS (Thread-safe) === + std::atomic reloadAttempts{0}; + std::atomic reloadSuccesses{0}; + std::atomic reloadFailures{0}; + std::atomic corruptedLoads{0}; + std::atomic crashes{0}; + std::atomic engineRunning{true}; + std::atomic watcherRunning{true}; + + // CRITICAL: Mutex to protect moduleSystem access between threads + std::mutex moduleSystemMutex; + + // Reload timing + std::mutex reloadTimesMutex; + std::vector reloadTimes; + + // Metrics + TestMetrics metrics; + + // === SETUP === + std::cout << "Setup:\n"; + std::cout << " Module path: " << modulePath << "\n"; + std::cout << " Source path: " << sourcePath << "\n"; + std::cout << " Compilations: " << TOTAL_COMPILATIONS << "\n"; + std::cout << " Interval: " << COMPILE_INTERVAL_MS << "ms\n"; + std::cout << " Expected time: ~" << (TOTAL_COMPILATIONS * COMPILE_INTERVAL_MS / 1000) << "s\n\n"; + + // Load module initially + ModuleLoader loader; + auto moduleSystem = std::make_unique(); + + try { + auto module = loader.load(modulePath, "TestModule", false); + nlohmann::json configJson = nlohmann::json::object(); + auto config = std::make_unique("config", configJson); + module->setConfiguration(*config, nullptr, nullptr); + moduleSystem->registerModule("TestModule", std::move(module)); + std::cout << " ✓ Initial module loaded\n\n"; + } catch (const std::exception& e) { + std::cerr << "❌ Failed to load initial module: " << e.what() << "\n"; + return 1; + } + + // === THREAD 1: AUTO-COMPILER === + std::cout << "Starting AutoCompiler thread...\n"; + AutoCompiler compiler("TestModule", buildDir, sourcePath); + compiler.start(TOTAL_COMPILATIONS, COMPILE_INTERVAL_MS); + + // === THREAD 2: FILE WATCHER === + std::cout << "Starting FileWatcher thread...\n"; + std::thread watcherThread([&]() { + try { + auto lastWriteTime = std::filesystem::last_write_time(modulePath); + + while (watcherRunning.load() && engineRunning.load()) { + try { + auto currentTime = std::filesystem::last_write_time(modulePath); + + if (currentTime != lastWriteTime) { + reloadAttempts++; + + // Measure reload time + auto reloadStart = std::chrono::high_resolution_clock::now(); + + try { + // CRITICAL: Lock moduleSystem during entire reload + std::lock_guard lock(moduleSystemMutex); + + // Extract module and save state + auto module = moduleSystem->extractModule(); + auto state = module->getState(); + + // CRITICAL: Destroy old module BEFORE reloading + // The loader.load() will unload the old .so + module.reset(); + + // Reload + auto newModule = loader.load(modulePath, "TestModule", true); + + // Check if module loaded correctly + if (!newModule) { + corruptedLoads++; + reloadFailures++; + // Can't recover - old module already destroyed + } else { + // VALIDATE MODULE INTEGRITY + bool isCorrupted = false; + try { + // Test 1: Can we get health status? + auto health = newModule->getHealthStatus(); + std::string version = health->getString("version", ""); + + // Test 2: Is version valid? + if (version.empty() || version == "unknown") { + isCorrupted = true; + } + + // Test 3: Can we set configuration? + nlohmann::json configJson; + configJson["test"] = "validation"; + auto testConfig = std::make_unique("config", configJson); + newModule->setConfiguration(*testConfig, nullptr, nullptr); + + } catch (const std::exception& e) { + // Module crashes on basic operations = corrupted + isCorrupted = true; + } + + if (isCorrupted) { + corruptedLoads++; + reloadFailures++; + // Can't recover - old module already destroyed + } else { + // Module is valid, restore state and register + newModule->setState(*state); + moduleSystem->registerModule("TestModule", std::move(newModule)); + reloadSuccesses++; + + // Record reload time + auto reloadEnd = std::chrono::high_resolution_clock::now(); + float reloadTimeMs = std::chrono::duration(reloadEnd - reloadStart).count(); + + { + std::lock_guard timeLock(reloadTimesMutex); + reloadTimes.push_back(reloadTimeMs); + } + } + } + } catch (const std::exception& e) { + reloadFailures++; + // Module might already be registered, continue + } + + lastWriteTime = currentTime; + } + } catch (const std::filesystem::filesystem_error&) { + // File might be being written, ignore + } + + std::this_thread::sleep_for(std::chrono::milliseconds(FILE_CHECK_INTERVAL_MS)); + } + } catch (const std::exception& e) { + std::cerr << "[FileWatcher] Exception: " << e.what() << "\n"; + } + }); + + // === THREAD 3: ENGINE LOOP === + std::cout << "Starting Engine thread (60 FPS)...\n\n"; + std::thread engineThread([&]() { + try { + auto lastMemoryCheck = std::chrono::steady_clock::now(); + + while (engineRunning.load()) { + auto frameStart = std::chrono::high_resolution_clock::now(); + + try { + // TRY to lock moduleSystem (non-blocking) + // If reload is happening, skip this frame + if (moduleSystemMutex.try_lock()) { + try { + moduleSystem->processModules(FRAME_TIME); + moduleSystemMutex.unlock(); + } catch (const std::exception& e) { + moduleSystemMutex.unlock(); + throw; + } + } + // else: reload in progress, skip frame + } catch (const std::exception& e) { + crashes++; + std::cerr << "[Engine] Crash detected: " << e.what() << "\n"; + } + + auto frameEnd = std::chrono::high_resolution_clock::now(); + float frameTime = std::chrono::duration(frameEnd - frameStart).count(); + metrics.recordFPS(1000.0f / std::max(frameTime, 0.1f)); + + // Check memory every second + auto now = std::chrono::steady_clock::now(); + if (std::chrono::duration_cast(now - lastMemoryCheck).count() >= 1) { + metrics.recordMemoryUsage(getCurrentMemoryUsage()); + lastMemoryCheck = now; + } + + // Sleep to maintain target FPS (if frame finished early) + auto targetFrameTime = std::chrono::milliseconds(static_cast(FRAME_TIME * 1000)); + auto elapsed = frameEnd - frameStart; + if (elapsed < targetFrameTime) { + std::this_thread::sleep_for(targetFrameTime - elapsed); + } + } + } catch (const std::exception& e) { + std::cerr << "[Engine] Thread exception: " << e.what() << "\n"; + } + }); + + // === MONITORING LOOP === + std::cout << "Test running...\n"; + auto startTime = std::chrono::steady_clock::now(); + int lastPrintedPercent = 0; + const int MAX_TEST_TIME_SECONDS = 90; // Maximum 1.5 minutes (allows all 20 compilations) + + while (compiler.isRunning() || compiler.getCurrentIteration() < TOTAL_COMPILATIONS) { + std::this_thread::sleep_for(std::chrono::seconds(2)); + + int currentIteration = compiler.getCurrentIteration(); + int percent = (currentIteration * 100) / TOTAL_COMPILATIONS; + + // Check for timeout + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(now - startTime).count(); + + if (elapsed > MAX_TEST_TIME_SECONDS) { + std::cout << "\n⚠️ Test timeout after " << elapsed << "s - stopping...\n"; + break; + } + + // Print progress every 10% + if (percent >= lastPrintedPercent + 10 && percent <= 100) { + std::cout << "\nProgress: " << percent << "% (" << currentIteration << "/" << TOTAL_COMPILATIONS << " compilations)\n"; + std::cout << " Elapsed: " << elapsed << "s\n"; + std::cout << " Compilations: " << compiler.getSuccessCount() << " OK, " << compiler.getFailureCount() << " FAIL\n"; + std::cout << " Reloads: " << reloadSuccesses.load() << " OK, " << reloadFailures.load() << " FAIL\n"; + std::cout << " Corrupted: " << corruptedLoads.load() << "\n"; + std::cout << " Crashes: " << crashes.load() << "\n"; + + lastPrintedPercent = percent; + } + } + + // === CLEANUP === + std::cout << "\n\nStopping threads...\n"; + + compiler.stop(); + watcherRunning = false; + engineRunning = false; + + if (watcherThread.joinable()) { + watcherThread.join(); + } + if (engineThread.joinable()) { + engineThread.join(); + } + + std::cout << " ✓ All threads stopped\n\n"; + + // === CALCULATE STATISTICS === + float compileSuccessRate = (compiler.getSuccessCount() * 100.0f) / std::max(1, TOTAL_COMPILATIONS); + float reloadSuccessRate = (reloadSuccesses.load() * 100.0f) / std::max(1, reloadAttempts.load()); + + float avgReloadTime = 0.0f; + { + std::lock_guard lock(reloadTimesMutex); + if (!reloadTimes.empty()) { + float sum = 0.0f; + for (float t : reloadTimes) { + sum += t; + } + avgReloadTime = sum / reloadTimes.size(); + } + } + + auto endTime = std::chrono::steady_clock::now(); + auto totalTimeSeconds = std::chrono::duration_cast(endTime - startTime).count(); + + // === PRINT SUMMARY === + std::cout << "================================================================================\n"; + std::cout << "RACE CONDITION HUNTER SUMMARY\n"; + std::cout << "================================================================================\n\n"; + + std::cout << "Duration: " << totalTimeSeconds << "s\n\n"; + + std::cout << "Compilations:\n"; + std::cout << " Total: " << TOTAL_COMPILATIONS << "\n"; + std::cout << " Successes: " << compiler.getSuccessCount() << " (" << std::fixed << std::setprecision(1) << compileSuccessRate << "%)\n"; + std::cout << " Failures: " << compiler.getFailureCount() << "\n\n"; + + std::cout << "Reloads:\n"; + std::cout << " Attempts: " << reloadAttempts.load() << "\n"; + std::cout << " Successes: " << reloadSuccesses.load() << " (" << std::fixed << std::setprecision(1) << reloadSuccessRate << "%)\n"; + std::cout << " Failures: " << reloadFailures.load() << "\n"; + std::cout << " Corrupted: " << corruptedLoads.load() << "\n"; + std::cout << " Avg time: " << std::fixed << std::setprecision(0) << avgReloadTime << "ms\n\n"; + + std::cout << "Stability:\n"; + std::cout << " Crashes: " << crashes.load() << "\n"; + std::cout << " Avg FPS: " << std::fixed << std::setprecision(1) << metrics.getFPSAvg() << "\n"; + std::cout << " Memory: " << std::fixed << std::setprecision(2) << metrics.getMemoryGrowth() << " MB\n\n"; + + // === ASSERTIONS === + bool passed = true; + + std::cout << "Validating results...\n"; + + // MUST PASS criteria + // Note: Lowered from 95% to 70% for WSL2/slower filesystem compatibility + // The important thing is that compilations don't fail, they just might timeout + if (compileSuccessRate < 70.0f) { + std::cout << " ❌ Compile success rate too low: " << compileSuccessRate << "% (need > 70%)\n"; + passed = false; + } else { + std::cout << " ✓ Compile success rate: " << compileSuccessRate << "%\n"; + } + + if (corruptedLoads.load() > 0) { + std::cout << " ❌ Corrupted loads detected: " << corruptedLoads.load() << " (need 0)\n"; + passed = false; + } else { + std::cout << " ✓ No corrupted loads\n"; + } + + if (crashes.load() > 0) { + std::cout << " ❌ Crashes detected: " << crashes.load() << " (need 0)\n"; + passed = false; + } else { + std::cout << " ✓ No crashes\n"; + } + + if (reloadAttempts.load() > 0 && reloadSuccessRate < 99.0f) { + std::cout << " ❌ Reload success rate too low: " << reloadSuccessRate << "% (need > 99%)\n"; + passed = false; + } else if (reloadAttempts.load() > 0) { + std::cout << " ✓ Reload success rate: " << reloadSuccessRate << "%\n"; + } + + // File stability validation: reload time should be >= 100ms + // This proves that ModuleLoader is waiting for file stability + if (reloadAttempts.load() > 0) { + if (avgReloadTime < 100.0f) { + std::cout << " ❌ Reload time too fast: " << avgReloadTime << "ms (need >= 100ms)\n"; + std::cout << " File stability check is NOT working properly!\n"; + passed = false; + } else if (avgReloadTime > 600.0f) { + std::cout << " ⚠️ Reload time very slow: " << avgReloadTime << "ms (> 600ms)\n"; + std::cout << " File stability might be waiting too long\n"; + } else { + std::cout << " ✓ Reload time: " << avgReloadTime << "ms (file stability working)\n"; + } + } + + std::cout << "\n"; + + // === FINAL RESULT === + std::cout << "================================================================================\n"; + if (passed) { + std::cout << "Result: ✅ PASSED\n"; + } else { + std::cout << "Result: ❌ FAILED\n"; + } + std::cout << "================================================================================\n"; + + return passed ? 0 : 1; +} diff --git a/tests/integration/test_05_memory_leak.cpp b/tests/integration/test_05_memory_leak.cpp index 90a93fa..e021526 100644 --- a/tests/integration/test_05_memory_leak.cpp +++ b/tests/integration/test_05_memory_leak.cpp @@ -1,428 +1,431 @@ -// ============================================================================ -// test_05_memory_leak.cpp - Memory Leak Hunter -// ============================================================================ -// Tests that repeated hot-reload cycles do not leak memory -// -// Strategy: -// - Load the same .so file 200 times (no recompilation) -// - Measure memory usage every 5 seconds -// - Verify temp file cleanup -// - Check for library handle leaks -// -// Success criteria: -// - < 10 MB total memory growth -// - < 50 KB average memory per reload -// - Temp files cleaned up (≤ 2 at any time) -// - No increase in mapped .so count -// - 100% reload success rate -// ============================================================================ - -#include "grove/ModuleLoader.h" -#include "grove/SequentialModuleSystem.h" -#include "grove/JsonDataNode.h" -#include "../helpers/SystemUtils.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace grove; -namespace fs = std::filesystem; - -// ============================================================================ -// Configuration -// ============================================================================ - -const int TOTAL_RELOADS = 200; -const int RELOAD_INTERVAL_MS = 500; -const int MEMORY_CHECK_INTERVAL_MS = 5000; -const float TARGET_FPS = 60.0f; -const int MAX_TEST_TIME_SECONDS = 180; - -// ============================================================================ -// State tracking -// ============================================================================ - -struct MemorySnapshot { - int reloadCount; - size_t memoryBytes; - int tempFiles; - int mappedLibs; - std::chrono::steady_clock::time_point timestamp; -}; - -std::atomic g_running{true}; -std::atomic g_reloadCount{0}; -std::atomic g_reloadSuccesses{0}; -std::atomic g_reloadFailures{0}; -std::atomic g_crashes{0}; -std::vector g_snapshots; -std::mutex g_snapshotMutex; - -// ============================================================================ -// Reload Scheduler Thread -// ============================================================================ - -void reloadSchedulerThread(ModuleLoader& loader, SequentialModuleSystem* moduleSystem, - const fs::path& modulePath, std::mutex& moduleSystemMutex) { - std::cout << " Starting ReloadScheduler thread...\n"; - - while (g_running && g_reloadCount < TOTAL_RELOADS) { - std::this_thread::sleep_for(std::chrono::milliseconds(RELOAD_INTERVAL_MS)); - - if (!g_running) break; - - try { - std::lock_guard lock(moduleSystemMutex); - - // Extract current module and save state - auto module = moduleSystem->extractModule(); - auto state = module->getState(); - auto config = std::make_unique("config", - dynamic_cast(module->getConfiguration()).getJsonData()); - - // CRITICAL: Destroy old module BEFORE reloading to avoid use-after-free - // The loader.load() will unload the old .so, so we must destroy the module first - module.reset(); - - // Reload the same .so file - auto newModule = loader.load(modulePath.string(), "LeakTestModule", true); - - g_reloadCount++; - - if (newModule) { - // Restore state - newModule->setConfiguration(*config, nullptr, nullptr); - newModule->setState(*state); - - // Register new module - moduleSystem->registerModule("LeakTestModule", std::move(newModule)); - g_reloadSuccesses++; - } else { - // Reload failed - we can't recover (old module destroyed) - g_reloadFailures++; - } - } catch (...) { - g_crashes++; - g_reloadFailures++; - g_reloadCount++; - } - } - - std::cout << " ReloadScheduler thread finished.\n"; -} - -// ============================================================================ -// Memory Monitor Thread -// ============================================================================ - -void memoryMonitorThread() { - std::cout << " Starting MemoryMonitor thread...\n"; - - auto startTime = std::chrono::steady_clock::now(); - - while (g_running) { - std::this_thread::sleep_for(std::chrono::milliseconds(MEMORY_CHECK_INTERVAL_MS)); - - if (!g_running) break; - - MemorySnapshot snapshot; - snapshot.reloadCount = g_reloadCount; - snapshot.memoryBytes = getCurrentMemoryUsage(); - snapshot.tempFiles = countTempFiles("/tmp/grove_module_*"); - snapshot.mappedLibs = getMappedLibraryCount(); - snapshot.timestamp = std::chrono::steady_clock::now(); - - { - std::lock_guard lock(g_snapshotMutex); - g_snapshots.push_back(snapshot); - } - - // Print progress - float memoryMB = snapshot.memoryBytes / (1024.0f * 1024.0f); - int progress = (snapshot.reloadCount * 100) / TOTAL_RELOADS; - - std::cout << "\n Progress: " << snapshot.reloadCount << " reloads (" << progress << "%)\n"; - std::cout << " Memory: " << std::fixed << std::setprecision(1) << memoryMB << " MB\n"; - std::cout << " Temp files: " << snapshot.tempFiles << "\n"; - std::cout << " Mapped .so: " << snapshot.mappedLibs << "\n"; - } - - std::cout << " MemoryMonitor thread finished.\n"; -} - -// ============================================================================ -// Engine Thread -// ============================================================================ - -void engineThread(SequentialModuleSystem* moduleSystem, std::mutex& moduleSystemMutex) { - std::cout << " Starting Engine thread (60 FPS)...\n"; - - const float frameTime = 1.0f / TARGET_FPS; - auto lastFrame = std::chrono::steady_clock::now(); - int frameCount = 0; - - while (g_running && g_reloadCount < TOTAL_RELOADS) { - auto now = std::chrono::steady_clock::now(); - float deltaTime = std::chrono::duration(now - lastFrame).count(); - - if (deltaTime >= frameTime) { - try { - std::lock_guard lock(moduleSystemMutex); - moduleSystem->processModules(deltaTime); - frameCount++; - } catch (...) { - g_crashes++; - } - - lastFrame = now; - } else { - // Sleep for remaining time - int sleepMs = static_cast((frameTime - deltaTime) * 1000); - if (sleepMs > 0) { - std::this_thread::sleep_for(std::chrono::milliseconds(sleepMs)); - } - } - } - - std::cout << " Engine thread finished (" << frameCount << " frames processed).\n"; -} - -// ============================================================================ -// Main Test -// ============================================================================ - -int main() { - std::cout << "================================================================================\n"; - std::cout << "TEST: Memory Leak Hunter - " << TOTAL_RELOADS << " Reload Cycles\n"; - std::cout << "================================================================================\n\n"; - - // Find module path - fs::path modulePath = "./libLeakTestModule.so"; - if (!fs::exists(modulePath)) { - std::cerr << "❌ Module not found: " << modulePath << "\n"; - return 1; - } - - std::cout << "Setup:\n"; - std::cout << " Module path: " << modulePath << "\n"; - std::cout << " Total reloads: " << TOTAL_RELOADS << "\n"; - std::cout << " Interval: " << RELOAD_INTERVAL_MS << "ms\n"; - std::cout << " Expected time: ~" << (TOTAL_RELOADS * RELOAD_INTERVAL_MS / 1000) << "s\n\n"; - - // Create module loader and system - ModuleLoader loader; - auto moduleSystem = std::make_unique(); - std::mutex moduleSystemMutex; - - // Disable verbose logging for performance - moduleSystem->setLogLevel(spdlog::level::err); - - // Load initial module - try { - auto module = loader.load(modulePath.string(), "LeakTestModule", false); - if (!module) { - std::cerr << "❌ Failed to load LeakTestModule\n"; - return 1; - } - - nlohmann::json configJson = nlohmann::json::object(); - auto config = std::make_unique("config", configJson); - module->setConfiguration(*config, nullptr, nullptr); - moduleSystem->registerModule("LeakTestModule", std::move(module)); - std::cout << "✓ Initial module loaded\n\n"; - } catch (const std::exception& e) { - std::cerr << "❌ Failed to load initial module: " << e.what() << "\n"; - return 1; - } - - // Baseline memory - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - size_t baselineMemory = getCurrentMemoryUsage(); - int baselineMappedLibs = getMappedLibraryCount(); - float baselineMB = baselineMemory / (1024.0f * 1024.0f); - - std::cout << "Baseline memory: " << std::fixed << std::setprecision(1) << baselineMB << " MB\n"; - std::cout << "Baseline mapped .so: " << baselineMappedLibs << "\n\n"; - - // Start threads - auto startTime = std::chrono::steady_clock::now(); - - std::thread reloadThread(reloadSchedulerThread, std::ref(loader), moduleSystem.get(), - modulePath, std::ref(moduleSystemMutex)); - std::thread monitorThread(memoryMonitorThread); - std::thread engThread(engineThread, moduleSystem.get(), std::ref(moduleSystemMutex)); - - // Wait for completion or timeout - auto deadline = startTime + std::chrono::seconds(MAX_TEST_TIME_SECONDS); - - while (g_running && g_reloadCount < TOTAL_RELOADS) { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - - if (std::chrono::steady_clock::now() > deadline) { - std::cout << "\n⚠️ Test timeout after " << MAX_TEST_TIME_SECONDS << " seconds\n"; - break; - } - } - - // Stop threads - g_running = false; - - reloadThread.join(); - monitorThread.join(); - engThread.join(); - - auto endTime = std::chrono::steady_clock::now(); - float durationSeconds = std::chrono::duration(endTime - startTime).count(); - - // Final measurements - size_t finalMemory = getCurrentMemoryUsage(); - int finalMappedLibs = getMappedLibraryCount(); - int finalTempFiles = countTempFiles("/tmp/grove_module_*"); - - float finalMB = finalMemory / (1024.0f * 1024.0f); - float growthMB = (finalMemory - baselineMemory) / (1024.0f * 1024.0f); - - // ======================================================================== - // Results Summary - // ======================================================================== - - std::cout << "\n================================================================================\n"; - std::cout << "MEMORY LEAK HUNTER SUMMARY\n"; - std::cout << "================================================================================\n\n"; - - std::cout << "Duration: " << static_cast(durationSeconds) << "s\n\n"; - - std::cout << "Reloads:\n"; - std::cout << " Total: " << g_reloadCount << "\n"; - std::cout << " Successes: " << g_reloadSuccesses; - if (g_reloadCount > 0) { - float successRate = (g_reloadSuccesses * 100.0f) / g_reloadCount; - std::cout << " (" << std::fixed << std::setprecision(1) << successRate << "%)"; - } - std::cout << "\n"; - std::cout << " Failures: " << g_reloadFailures << "\n\n"; - - std::cout << "Memory Analysis:\n"; - std::cout << " Baseline: " << std::fixed << std::setprecision(1) << baselineMB << " MB\n"; - std::cout << " Final: " << finalMB << " MB\n"; - std::cout << " Growth: " << growthMB << " MB"; - - if (growthMB < 10.0f) { - std::cout << " ✅"; - } else { - std::cout << " ❌"; - } - std::cout << "\n"; - - float memoryPerReloadKB = 0.0f; - if (g_reloadCount > 0) { - memoryPerReloadKB = (growthMB * 1024.0f) / g_reloadCount; - } - std::cout << " Per reload: " << std::fixed << std::setprecision(1) << memoryPerReloadKB << " KB"; - if (memoryPerReloadKB < 50.0f) { - std::cout << " ✅"; - } else { - std::cout << " ❌"; - } - std::cout << "\n\n"; - - std::cout << "Resource Cleanup:\n"; - std::cout << " Temp files: " << finalTempFiles; - if (finalTempFiles <= 2) { - std::cout << " ✅"; - } else { - std::cout << " ❌"; - } - std::cout << "\n"; - - std::cout << " Mapped .so: " << finalMappedLibs; - if (finalMappedLibs <= baselineMappedLibs + 2) { - std::cout << " (stable) ✅"; - } else { - std::cout << " (leak: +" << (finalMappedLibs - baselineMappedLibs) << ") ❌"; - } - std::cout << "\n\n"; - - std::cout << "Stability:\n"; - std::cout << " Crashes: " << g_crashes; - if (g_crashes == 0) { - std::cout << " ✅"; - } else { - std::cout << " ❌"; - } - std::cout << "\n\n"; - - // ======================================================================== - // Validation - // ======================================================================== - - std::cout << "Validating results...\n"; - - bool passed = true; - - // 1. Memory growth < 10 MB - if (growthMB > 10.0f) { - std::cout << " ❌ Memory growth too high: " << growthMB << " MB (need < 10 MB)\n"; - passed = false; - } else { - std::cout << " ✓ Memory growth: " << growthMB << " MB (< 10 MB)\n"; - } - - // 2. Memory per reload < 50 KB - if (memoryPerReloadKB > 50.0f) { - std::cout << " ❌ Memory per reload too high: " << memoryPerReloadKB << " KB (need < 50 KB)\n"; - passed = false; - } else { - std::cout << " ✓ Memory per reload: " << memoryPerReloadKB << " KB (< 50 KB)\n"; - } - - // 3. Temp files cleaned - if (finalTempFiles > 2) { - std::cout << " ❌ Temp files not cleaned up: " << finalTempFiles << " (need ≤ 2)\n"; - passed = false; - } else { - std::cout << " ✓ Temp files cleaned: " << finalTempFiles << " (≤ 2)\n"; - } - - // 4. No .so handle leaks - if (finalMappedLibs > baselineMappedLibs + 2) { - std::cout << " ❌ Library handle leak: +" << (finalMappedLibs - baselineMappedLibs) << "\n"; - passed = false; - } else { - std::cout << " ✓ No .so handle leaks\n"; - } - - // 5. Reload success rate - float successRate = g_reloadCount > 0 ? (g_reloadSuccesses * 100.0f) / g_reloadCount : 0.0f; - if (successRate < 100.0f) { - std::cout << " ❌ Reload success rate: " << std::fixed << std::setprecision(1) - << successRate << "% (need 100%)\n"; - passed = false; - } else { - std::cout << " ✓ Reload success rate: 100%\n"; - } - - // 6. No crashes - if (g_crashes > 0) { - std::cout << " ❌ Crashes detected: " << g_crashes << "\n"; - passed = false; - } else { - std::cout << " ✓ No crashes\n"; - } - - std::cout << "\n================================================================================\n"; - if (passed) { - std::cout << "Result: ✅ PASSED\n"; - } else { - std::cout << "Result: ❌ FAILED\n"; - } - std::cout << "================================================================================\n"; - - return passed ? 0 : 1; -} +// ============================================================================ +// test_05_memory_leak.cpp - Memory Leak Hunter +// ============================================================================ +// Tests that repeated hot-reload cycles do not leak memory +// +// Strategy: +// - Load the same .so file 200 times (no recompilation) +// - Measure memory usage every 5 seconds +// - Verify temp file cleanup +// - Check for library handle leaks +// +// Success criteria: +// - < 10 MB total memory growth +// - < 50 KB average memory per reload +// - Temp files cleaned up (≤ 2 at any time) +// - No increase in mapped .so count +// - 100% reload success rate +// ============================================================================ + +#include "grove/ModuleLoader.h" +#include "grove/SequentialModuleSystem.h" +#include "grove/JsonDataNode.h" +#include "../helpers/SystemUtils.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace grove; +namespace fs = std::filesystem; + +// ============================================================================ +// Configuration +// ============================================================================ + +const int TOTAL_RELOADS = 200; +const int RELOAD_INTERVAL_MS = 500; +const int MEMORY_CHECK_INTERVAL_MS = 5000; +const float TARGET_FPS = 60.0f; +const int MAX_TEST_TIME_SECONDS = 180; + +// ============================================================================ +// State tracking +// ============================================================================ + +struct MemorySnapshot { + int reloadCount; + size_t memoryBytes; + int tempFiles; + int mappedLibs; + std::chrono::steady_clock::time_point timestamp; +}; + +std::atomic g_running{true}; +std::atomic g_reloadCount{0}; +std::atomic g_reloadSuccesses{0}; +std::atomic g_reloadFailures{0}; +std::atomic g_crashes{0}; +std::vector g_snapshots; +std::mutex g_snapshotMutex; + +// ============================================================================ +// Reload Scheduler Thread +// ============================================================================ + +void reloadSchedulerThread(ModuleLoader& loader, SequentialModuleSystem* moduleSystem, + const fs::path& modulePath, std::mutex& moduleSystemMutex) { + std::cout << " Starting ReloadScheduler thread...\n"; + + while (g_running && g_reloadCount < TOTAL_RELOADS) { + std::this_thread::sleep_for(std::chrono::milliseconds(RELOAD_INTERVAL_MS)); + + if (!g_running) break; + + try { + std::lock_guard lock(moduleSystemMutex); + + // Extract current module and save state + auto module = moduleSystem->extractModule(); + auto state = module->getState(); + auto config = std::make_unique("config", + dynamic_cast(module->getConfiguration()).getJsonData()); + + // CRITICAL: Destroy old module BEFORE reloading to avoid use-after-free + // The loader.load() will unload the old .so, so we must destroy the module first + module.reset(); + + // Reload the same .so file + auto newModule = loader.load(modulePath.string(), "LeakTestModule", true); + + g_reloadCount++; + + if (newModule) { + // Restore state + newModule->setConfiguration(*config, nullptr, nullptr); + newModule->setState(*state); + + // Register new module + moduleSystem->registerModule("LeakTestModule", std::move(newModule)); + g_reloadSuccesses++; + } else { + // Reload failed - we can't recover (old module destroyed) + g_reloadFailures++; + } + } catch (...) { + g_crashes++; + g_reloadFailures++; + g_reloadCount++; + } + } + + std::cout << " ReloadScheduler thread finished.\n"; +} + +// ============================================================================ +// Memory Monitor Thread +// ============================================================================ + +void memoryMonitorThread() { + std::cout << " Starting MemoryMonitor thread...\n"; + + auto startTime = std::chrono::steady_clock::now(); + + while (g_running) { + std::this_thread::sleep_for(std::chrono::milliseconds(MEMORY_CHECK_INTERVAL_MS)); + + if (!g_running) break; + + MemorySnapshot snapshot; + snapshot.reloadCount = g_reloadCount; + snapshot.memoryBytes = getCurrentMemoryUsage(); + snapshot.tempFiles = countTempFiles("/tmp/grove_module_*"); + snapshot.mappedLibs = getMappedLibraryCount(); + snapshot.timestamp = std::chrono::steady_clock::now(); + + { + std::lock_guard lock(g_snapshotMutex); + g_snapshots.push_back(snapshot); + } + + // Print progress + float memoryMB = snapshot.memoryBytes / (1024.0f * 1024.0f); + int progress = (snapshot.reloadCount * 100) / TOTAL_RELOADS; + + std::cout << "\n Progress: " << snapshot.reloadCount << " reloads (" << progress << "%)\n"; + std::cout << " Memory: " << std::fixed << std::setprecision(1) << memoryMB << " MB\n"; + std::cout << " Temp files: " << snapshot.tempFiles << "\n"; + std::cout << " Mapped .so: " << snapshot.mappedLibs << "\n"; + } + + std::cout << " MemoryMonitor thread finished.\n"; +} + +// ============================================================================ +// Engine Thread +// ============================================================================ + +void engineThread(SequentialModuleSystem* moduleSystem, std::mutex& moduleSystemMutex) { + std::cout << " Starting Engine thread (60 FPS)...\n"; + + const float frameTime = 1.0f / TARGET_FPS; + auto lastFrame = std::chrono::steady_clock::now(); + int frameCount = 0; + + while (g_running && g_reloadCount < TOTAL_RELOADS) { + auto now = std::chrono::steady_clock::now(); + float deltaTime = std::chrono::duration(now - lastFrame).count(); + + if (deltaTime >= frameTime) { + try { + std::lock_guard lock(moduleSystemMutex); + moduleSystem->processModules(deltaTime); + frameCount++; + } catch (...) { + g_crashes++; + } + + lastFrame = now; + } else { + // Sleep for remaining time + int sleepMs = static_cast((frameTime - deltaTime) * 1000); + if (sleepMs > 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(sleepMs)); + } + } + } + + std::cout << " Engine thread finished (" << frameCount << " frames processed).\n"; +} + +// ============================================================================ +// Main Test +// ============================================================================ + +int main() { + std::cout << "================================================================================\n"; + std::cout << "TEST: Memory Leak Hunter - " << TOTAL_RELOADS << " Reload Cycles\n"; + std::cout << "================================================================================\n\n"; + + // Find module path + fs::path modulePath = "./libLeakTestModule.so"; +#ifdef _WIN32 + modulePath = "./libLeakTestModule.dll"; +#endif + if (!fs::exists(modulePath)) { + std::cerr << "❌ Module not found: " << modulePath << "\n"; + return 1; + } + + std::cout << "Setup:\n"; + std::cout << " Module path: " << modulePath << "\n"; + std::cout << " Total reloads: " << TOTAL_RELOADS << "\n"; + std::cout << " Interval: " << RELOAD_INTERVAL_MS << "ms\n"; + std::cout << " Expected time: ~" << (TOTAL_RELOADS * RELOAD_INTERVAL_MS / 1000) << "s\n\n"; + + // Create module loader and system + ModuleLoader loader; + auto moduleSystem = std::make_unique(); + std::mutex moduleSystemMutex; + + // Disable verbose logging for performance + moduleSystem->setLogLevel(spdlog::level::err); + + // Load initial module + try { + auto module = loader.load(modulePath.string(), "LeakTestModule", false); + if (!module) { + std::cerr << "❌ Failed to load LeakTestModule\n"; + return 1; + } + + nlohmann::json configJson = nlohmann::json::object(); + auto config = std::make_unique("config", configJson); + module->setConfiguration(*config, nullptr, nullptr); + moduleSystem->registerModule("LeakTestModule", std::move(module)); + std::cout << "✓ Initial module loaded\n\n"; + } catch (const std::exception& e) { + std::cerr << "❌ Failed to load initial module: " << e.what() << "\n"; + return 1; + } + + // Baseline memory + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + size_t baselineMemory = getCurrentMemoryUsage(); + int baselineMappedLibs = getMappedLibraryCount(); + float baselineMB = baselineMemory / (1024.0f * 1024.0f); + + std::cout << "Baseline memory: " << std::fixed << std::setprecision(1) << baselineMB << " MB\n"; + std::cout << "Baseline mapped .so: " << baselineMappedLibs << "\n\n"; + + // Start threads + auto startTime = std::chrono::steady_clock::now(); + + std::thread reloadThread(reloadSchedulerThread, std::ref(loader), moduleSystem.get(), + modulePath, std::ref(moduleSystemMutex)); + std::thread monitorThread(memoryMonitorThread); + std::thread engThread(engineThread, moduleSystem.get(), std::ref(moduleSystemMutex)); + + // Wait for completion or timeout + auto deadline = startTime + std::chrono::seconds(MAX_TEST_TIME_SECONDS); + + while (g_running && g_reloadCount < TOTAL_RELOADS) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + if (std::chrono::steady_clock::now() > deadline) { + std::cout << "\n⚠️ Test timeout after " << MAX_TEST_TIME_SECONDS << " seconds\n"; + break; + } + } + + // Stop threads + g_running = false; + + reloadThread.join(); + monitorThread.join(); + engThread.join(); + + auto endTime = std::chrono::steady_clock::now(); + float durationSeconds = std::chrono::duration(endTime - startTime).count(); + + // Final measurements + size_t finalMemory = getCurrentMemoryUsage(); + int finalMappedLibs = getMappedLibraryCount(); + int finalTempFiles = countTempFiles("/tmp/grove_module_*"); + + float finalMB = finalMemory / (1024.0f * 1024.0f); + float growthMB = (finalMemory - baselineMemory) / (1024.0f * 1024.0f); + + // ======================================================================== + // Results Summary + // ======================================================================== + + std::cout << "\n================================================================================\n"; + std::cout << "MEMORY LEAK HUNTER SUMMARY\n"; + std::cout << "================================================================================\n\n"; + + std::cout << "Duration: " << static_cast(durationSeconds) << "s\n\n"; + + std::cout << "Reloads:\n"; + std::cout << " Total: " << g_reloadCount << "\n"; + std::cout << " Successes: " << g_reloadSuccesses; + if (g_reloadCount > 0) { + float successRate = (g_reloadSuccesses * 100.0f) / g_reloadCount; + std::cout << " (" << std::fixed << std::setprecision(1) << successRate << "%)"; + } + std::cout << "\n"; + std::cout << " Failures: " << g_reloadFailures << "\n\n"; + + std::cout << "Memory Analysis:\n"; + std::cout << " Baseline: " << std::fixed << std::setprecision(1) << baselineMB << " MB\n"; + std::cout << " Final: " << finalMB << " MB\n"; + std::cout << " Growth: " << growthMB << " MB"; + + if (growthMB < 10.0f) { + std::cout << " ✅"; + } else { + std::cout << " ❌"; + } + std::cout << "\n"; + + float memoryPerReloadKB = 0.0f; + if (g_reloadCount > 0) { + memoryPerReloadKB = (growthMB * 1024.0f) / g_reloadCount; + } + std::cout << " Per reload: " << std::fixed << std::setprecision(1) << memoryPerReloadKB << " KB"; + if (memoryPerReloadKB < 50.0f) { + std::cout << " ✅"; + } else { + std::cout << " ❌"; + } + std::cout << "\n\n"; + + std::cout << "Resource Cleanup:\n"; + std::cout << " Temp files: " << finalTempFiles; + if (finalTempFiles <= 2) { + std::cout << " ✅"; + } else { + std::cout << " ❌"; + } + std::cout << "\n"; + + std::cout << " Mapped .so: " << finalMappedLibs; + if (finalMappedLibs <= baselineMappedLibs + 2) { + std::cout << " (stable) ✅"; + } else { + std::cout << " (leak: +" << (finalMappedLibs - baselineMappedLibs) << ") ❌"; + } + std::cout << "\n\n"; + + std::cout << "Stability:\n"; + std::cout << " Crashes: " << g_crashes; + if (g_crashes == 0) { + std::cout << " ✅"; + } else { + std::cout << " ❌"; + } + std::cout << "\n\n"; + + // ======================================================================== + // Validation + // ======================================================================== + + std::cout << "Validating results...\n"; + + bool passed = true; + + // 1. Memory growth < 10 MB + if (growthMB > 10.0f) { + std::cout << " ❌ Memory growth too high: " << growthMB << " MB (need < 10 MB)\n"; + passed = false; + } else { + std::cout << " ✓ Memory growth: " << growthMB << " MB (< 10 MB)\n"; + } + + // 2. Memory per reload < 50 KB + if (memoryPerReloadKB > 50.0f) { + std::cout << " ❌ Memory per reload too high: " << memoryPerReloadKB << " KB (need < 50 KB)\n"; + passed = false; + } else { + std::cout << " ✓ Memory per reload: " << memoryPerReloadKB << " KB (< 50 KB)\n"; + } + + // 3. Temp files cleaned + if (finalTempFiles > 2) { + std::cout << " ❌ Temp files not cleaned up: " << finalTempFiles << " (need ≤ 2)\n"; + passed = false; + } else { + std::cout << " ✓ Temp files cleaned: " << finalTempFiles << " (≤ 2)\n"; + } + + // 4. No .so handle leaks + if (finalMappedLibs > baselineMappedLibs + 2) { + std::cout << " ❌ Library handle leak: +" << (finalMappedLibs - baselineMappedLibs) << "\n"; + passed = false; + } else { + std::cout << " ✓ No .so handle leaks\n"; + } + + // 5. Reload success rate + float successRate = g_reloadCount > 0 ? (g_reloadSuccesses * 100.0f) / g_reloadCount : 0.0f; + if (successRate < 100.0f) { + std::cout << " ❌ Reload success rate: " << std::fixed << std::setprecision(1) + << successRate << "% (need 100%)\n"; + passed = false; + } else { + std::cout << " ✓ Reload success rate: 100%\n"; + } + + // 6. No crashes + if (g_crashes > 0) { + std::cout << " ❌ Crashes detected: " << g_crashes << "\n"; + passed = false; + } else { + std::cout << " ✓ No crashes\n"; + } + + std::cout << "\n================================================================================\n"; + if (passed) { + std::cout << "Result: ✅ PASSED\n"; + } else { + std::cout << "Result: ❌ FAILED\n"; + } + std::cout << "================================================================================\n"; + + return passed ? 0 : 1; +} diff --git a/tests/integration/test_06_error_recovery.cpp b/tests/integration/test_06_error_recovery.cpp index 80ec7db..4f2248c 100644 --- a/tests/integration/test_06_error_recovery.cpp +++ b/tests/integration/test_06_error_recovery.cpp @@ -1,272 +1,275 @@ -#include "grove/ModuleLoader.h" -#include "grove/SequentialModuleSystem.h" -#include "grove/JsonDataNode.h" -#include "../helpers/TestMetrics.h" -#include "../helpers/TestAssertions.h" -#include "../helpers/TestReporter.h" -#include "../helpers/SystemUtils.h" -#include -#include -#include -#include - -using namespace grove; - -/** - * Test 06: Error Recovery - * - * Objectif: Valider que le système peut détecter et récupérer automatiquement - * d'un crash de module via hot-reload. - * - * Scénario: - * 1. Charger ErrorRecoveryModule avec crash planifié à frame 60 - * 2. Lancer execution jusqu'au crash - * 3. Détecter le crash (exception) - * 4. Trigger hot-reload automatique - * 5. Vérifier que le module récupère (auto-recovery) - * 6. Continuer execution normalement - * - * Métriques: - * - Crash detection time - * - Recovery success rate - * - State preservation après recovery - * - Stabilité du moteur - */ - -int main() { - TestReporter reporter("Error Recovery"); - TestMetrics metrics; - - std::cout << "================================================================================\n"; - std::cout << "TEST: Error Recovery - Crash Detection & Auto-Recovery\n"; - std::cout << "================================================================================\n\n"; - - // === SETUP === - std::cout << "Setup: Loading ErrorRecoveryModule with crash trigger...\n"; - - ModuleLoader loader; - auto moduleSystem = std::make_unique(); - - // Charger module - std::string modulePath = "./libErrorRecoveryModule.so"; - auto module = loader.load(modulePath, "ErrorRecoveryModule", false); - - // Config: crash à frame 60, type runtime_error - nlohmann::json configJson; - configJson["crashAtFrame"] = 60; - configJson["crashType"] = 0; // runtime_error - configJson["enableAutoRecovery"] = true; - configJson["versionTag"] = "v1.0"; - auto config = std::make_unique("config", configJson); - - module->setConfiguration(*config, nullptr, nullptr); - moduleSystem->registerModule("ErrorRecoveryModule", std::move(module)); - - std::cout << " ✓ Module loaded with crash trigger at frame 60\n\n"; - - // === PHASE 1: Run until crash === - std::cout << "Phase 1: Running until crash (target frame: 60)...\n"; - - bool crashDetected = false; - int crashFrame = -1; - auto crashDetectionStart = std::chrono::high_resolution_clock::now(); - - for (int frame = 1; frame <= 100; frame++) { - try { - auto frameStart = std::chrono::high_resolution_clock::now(); - - moduleSystem->processModules(1.0f / 60.0f); - - auto frameEnd = std::chrono::high_resolution_clock::now(); - float frameTime = std::chrono::duration(frameEnd - frameStart).count(); - metrics.recordFPS(1000.0f / frameTime); - - if (frame % 20 == 0) { - std::cout << " Frame " << frame << "/100 - OK\n"; - } - - } catch (const std::exception& e) { - // CRASH DÉTECTÉ ! - auto crashDetectionEnd = std::chrono::high_resolution_clock::now(); - float detectionTime = std::chrono::duration( - crashDetectionEnd - crashDetectionStart).count(); - - crashDetected = true; - crashFrame = frame; - - std::cout << "\n💥 CRASH DETECTED at frame " << frame << "\n"; - std::cout << " Exception: " << e.what() << "\n"; - std::cout << " Detection time: " << detectionTime << "ms\n\n"; - - metrics.recordCrash("runtime_error at frame " + std::to_string(frame)); - reporter.addMetric("crash_detection_time_ms", detectionTime); - - break; - } - } - - ASSERT_TRUE(crashDetected, "Crash should have been detected"); - ASSERT_EQ(crashFrame, 60, "Crash should occur at frame 60"); - reporter.addAssertion("crash_detected", crashDetected); - reporter.addAssertion("crash_at_expected_frame", crashFrame == 60); - - // === PHASE 2: Extract state before recovery === - std::cout << "Phase 2: Extracting state before recovery...\n"; - - auto crashedModule = moduleSystem->extractModule(); - auto preRecoveryState = crashedModule->getState(); - - auto* jsonNodeBefore = dynamic_cast(preRecoveryState.get()); - if (!jsonNodeBefore) { - std::cerr << "❌ Failed to extract state before recovery\n"; - return 1; - } - - const auto& stateBefore = jsonNodeBefore->getJsonData(); - int frameCountBefore = stateBefore.value("frameCount", 0); - int crashCountBefore = stateBefore.value("crashCount", 0); - bool hasCrashedBefore = stateBefore.value("hasCrashed", false); - - std::cout << " State before recovery:\n"; - std::cout << " Frame count: " << frameCountBefore << "\n"; - std::cout << " Crash count: " << crashCountBefore << "\n"; - std::cout << " Has crashed: " << (hasCrashedBefore ? "YES" : "NO") << "\n\n"; - - ASSERT_TRUE(hasCrashedBefore, "Module should be in crashed state"); - - // === PHASE 3: Trigger hot-reload (recovery) === - std::cout << "Phase 3: Triggering hot-reload for recovery...\n"; - - auto recoveryStart = std::chrono::high_resolution_clock::now(); - - // Hot-reload via ModuleLoader - auto recoveredModule = loader.reload(std::move(crashedModule)); - - auto recoveryEnd = std::chrono::high_resolution_clock::now(); - float recoveryTime = std::chrono::duration(recoveryEnd - recoveryStart).count(); - - std::cout << " ✓ Hot-reload completed in " << recoveryTime << "ms\n"; - - metrics.recordReloadTime(recoveryTime); - reporter.addMetric("recovery_time_ms", recoveryTime); - - // Ré-enregistrer module récupéré - moduleSystem->registerModule("ErrorRecoveryModule", std::move(recoveredModule)); - - // === PHASE 4: Verify recovery === - std::cout << "\nPhase 4: Verifying recovery...\n"; - - auto recoveredModuleRef = moduleSystem->extractModule(); - auto postRecoveryState = recoveredModuleRef->getState(); - - auto* jsonNodeAfter = dynamic_cast(postRecoveryState.get()); - if (!jsonNodeAfter) { - std::cerr << "❌ Failed to extract state after recovery\n"; - return 1; - } - - const auto& stateAfter = jsonNodeAfter->getJsonData(); - int frameCountAfter = stateAfter.value("frameCount", 0); - int crashCountAfter = stateAfter.value("crashCount", 0); - int recoveryCountAfter = stateAfter.value("recoveryCount", 0); - bool hasCrashedAfter = stateAfter.value("hasCrashed", false); - int crashAtFrameAfter = stateAfter.value("crashAtFrame", -1); - - std::cout << " State after recovery:\n"; - std::cout << " Frame count: " << frameCountAfter << "\n"; - std::cout << " Crash count: " << crashCountAfter << "\n"; - std::cout << " Recovery count: " << recoveryCountAfter << "\n"; - std::cout << " Has crashed: " << (hasCrashedAfter ? "YES" : "NO") << "\n"; - std::cout << " Crash trigger: " << crashAtFrameAfter << "\n\n"; - - // Vérifications de recovery - ASSERT_EQ(frameCountAfter, frameCountBefore, "Frame count should be preserved"); - ASSERT_FALSE(hasCrashedAfter, "Module should no longer be in crashed state"); - ASSERT_EQ(recoveryCountAfter, 1, "Recovery count should be 1"); - ASSERT_EQ(crashAtFrameAfter, -1, "Crash trigger should be disabled"); - - reporter.addAssertion("frame_count_preserved", frameCountAfter == frameCountBefore); - reporter.addAssertion("crash_state_cleared", !hasCrashedAfter); - reporter.addAssertion("recovery_count_incremented", recoveryCountAfter == 1); - reporter.addAssertion("crash_trigger_disabled", crashAtFrameAfter == -1); - - std::cout << " ✅ RECOVERY SUCCESSFUL - Module is healthy again\n\n"; - - // Ré-enregistrer pour phase 5 - moduleSystem->registerModule("ErrorRecoveryModule", std::move(recoveredModuleRef)); - - // === PHASE 5: Continue execution (stability check) === - std::cout << "Phase 5: Stability check - Running 120 more frames...\n"; - - bool stableExecution = true; - int framesAfterRecovery = 0; - - for (int frame = 1; frame <= 120; frame++) { - try { - auto frameStart = std::chrono::high_resolution_clock::now(); - - moduleSystem->processModules(1.0f / 60.0f); - - auto frameEnd = std::chrono::high_resolution_clock::now(); - float frameTime = std::chrono::duration(frameEnd - frameStart).count(); - metrics.recordFPS(1000.0f / frameTime); - - framesAfterRecovery++; - - if (frame % 30 == 0) { - std::cout << " Frame " << frame << "/120 - Stable\n"; - } - - } catch (const std::exception& e) { - std::cout << "\n❌ UNEXPECTED CRASH after recovery at frame " << frame << "\n"; - std::cout << " Exception: " << e.what() << "\n"; - stableExecution = false; - break; - } - } - - ASSERT_TRUE(stableExecution, "Module should execute stably after recovery"); - ASSERT_EQ(framesAfterRecovery, 120, "Should complete all 120 frames"); - - reporter.addAssertion("stable_after_recovery", stableExecution); - reporter.addMetric("frames_after_recovery", static_cast(framesAfterRecovery)); - - std::cout << " ✅ Stability verified - " << framesAfterRecovery << " frames executed without issues\n\n"; - - // === VÉRIFICATIONS FINALES === - std::cout << "Final verifications...\n"; - - // Memory growth - size_t memGrowth = metrics.getMemoryGrowth(); - float memGrowthMB = memGrowth / (1024.0f * 1024.0f); - ASSERT_LT(memGrowthMB, 10.0f, "Memory growth should be < 10MB"); - reporter.addMetric("memory_growth_mb", memGrowthMB); - - // FPS (moins strict pour test de recovery - focus sur stability) - float minFPS = metrics.getFPSMin(); - ASSERT_GT(minFPS, 5.0f, "Min FPS should be > 5 (recovery test allows slower frames)"); - reporter.addMetric("fps_min", minFPS); - reporter.addMetric("fps_avg", metrics.getFPSAvg()); - - // Recovery time threshold - ASSERT_LT(recoveryTime, 500.0f, "Recovery time should be < 500ms"); - - // Crash count - int totalCrashes = metrics.getCrashCount(); - ASSERT_EQ(totalCrashes, 1, "Should have exactly 1 controlled crash"); - reporter.addMetric("total_crashes", static_cast(totalCrashes)); - - // === RAPPORTS === - std::cout << "\n"; - std::cout << "Summary:\n"; - std::cout << " 🎯 Crash detected at frame " << crashFrame << " (expected: 60)\n"; - std::cout << " 🔄 Recovery time: " << recoveryTime << "ms\n"; - std::cout << " ✅ Stable execution: " << framesAfterRecovery << " frames after recovery\n"; - std::cout << " 💾 Memory growth: " << memGrowthMB << " MB\n"; - std::cout << " 📊 FPS: min=" << minFPS << ", avg=" << metrics.getFPSAvg() << "\n\n"; - - metrics.printReport(); - reporter.printFinalReport(); - - return reporter.getExitCode(); -} +#include "grove/ModuleLoader.h" +#include "grove/SequentialModuleSystem.h" +#include "grove/JsonDataNode.h" +#include "../helpers/TestMetrics.h" +#include "../helpers/TestAssertions.h" +#include "../helpers/TestReporter.h" +#include "../helpers/SystemUtils.h" +#include +#include +#include +#include + +using namespace grove; + +/** + * Test 06: Error Recovery + * + * Objectif: Valider que le système peut détecter et récupérer automatiquement + * d'un crash de module via hot-reload. + * + * Scénario: + * 1. Charger ErrorRecoveryModule avec crash planifié à frame 60 + * 2. Lancer execution jusqu'au crash + * 3. Détecter le crash (exception) + * 4. Trigger hot-reload automatique + * 5. Vérifier que le module récupère (auto-recovery) + * 6. Continuer execution normalement + * + * Métriques: + * - Crash detection time + * - Recovery success rate + * - State preservation après recovery + * - Stabilité du moteur + */ + +int main() { + TestReporter reporter("Error Recovery"); + TestMetrics metrics; + + std::cout << "================================================================================\n"; + std::cout << "TEST: Error Recovery - Crash Detection & Auto-Recovery\n"; + std::cout << "================================================================================\n\n"; + + // === SETUP === + std::cout << "Setup: Loading ErrorRecoveryModule with crash trigger...\n"; + + ModuleLoader loader; + auto moduleSystem = std::make_unique(); + + // Charger module + std::string modulePath = "./libErrorRecoveryModule.so"; +#ifdef _WIN32 + modulePath = "./libErrorRecoveryModule.dll"; +#endif + auto module = loader.load(modulePath, "ErrorRecoveryModule", false); + + // Config: crash à frame 60, type runtime_error + nlohmann::json configJson; + configJson["crashAtFrame"] = 60; + configJson["crashType"] = 0; // runtime_error + configJson["enableAutoRecovery"] = true; + configJson["versionTag"] = "v1.0"; + auto config = std::make_unique("config", configJson); + + module->setConfiguration(*config, nullptr, nullptr); + moduleSystem->registerModule("ErrorRecoveryModule", std::move(module)); + + std::cout << " ✓ Module loaded with crash trigger at frame 60\n\n"; + + // === PHASE 1: Run until crash === + std::cout << "Phase 1: Running until crash (target frame: 60)...\n"; + + bool crashDetected = false; + int crashFrame = -1; + auto crashDetectionStart = std::chrono::high_resolution_clock::now(); + + for (int frame = 1; frame <= 100; frame++) { + try { + auto frameStart = std::chrono::high_resolution_clock::now(); + + moduleSystem->processModules(1.0f / 60.0f); + + auto frameEnd = std::chrono::high_resolution_clock::now(); + float frameTime = std::chrono::duration(frameEnd - frameStart).count(); + metrics.recordFPS(1000.0f / frameTime); + + if (frame % 20 == 0) { + std::cout << " Frame " << frame << "/100 - OK\n"; + } + + } catch (const std::exception& e) { + // CRASH DÉTECTÉ ! + auto crashDetectionEnd = std::chrono::high_resolution_clock::now(); + float detectionTime = std::chrono::duration( + crashDetectionEnd - crashDetectionStart).count(); + + crashDetected = true; + crashFrame = frame; + + std::cout << "\n💥 CRASH DETECTED at frame " << frame << "\n"; + std::cout << " Exception: " << e.what() << "\n"; + std::cout << " Detection time: " << detectionTime << "ms\n\n"; + + metrics.recordCrash("runtime_error at frame " + std::to_string(frame)); + reporter.addMetric("crash_detection_time_ms", detectionTime); + + break; + } + } + + ASSERT_TRUE(crashDetected, "Crash should have been detected"); + ASSERT_EQ(crashFrame, 60, "Crash should occur at frame 60"); + reporter.addAssertion("crash_detected", crashDetected); + reporter.addAssertion("crash_at_expected_frame", crashFrame == 60); + + // === PHASE 2: Extract state before recovery === + std::cout << "Phase 2: Extracting state before recovery...\n"; + + auto crashedModule = moduleSystem->extractModule(); + auto preRecoveryState = crashedModule->getState(); + + auto* jsonNodeBefore = dynamic_cast(preRecoveryState.get()); + if (!jsonNodeBefore) { + std::cerr << "❌ Failed to extract state before recovery\n"; + return 1; + } + + const auto& stateBefore = jsonNodeBefore->getJsonData(); + int frameCountBefore = stateBefore.value("frameCount", 0); + int crashCountBefore = stateBefore.value("crashCount", 0); + bool hasCrashedBefore = stateBefore.value("hasCrashed", false); + + std::cout << " State before recovery:\n"; + std::cout << " Frame count: " << frameCountBefore << "\n"; + std::cout << " Crash count: " << crashCountBefore << "\n"; + std::cout << " Has crashed: " << (hasCrashedBefore ? "YES" : "NO") << "\n\n"; + + ASSERT_TRUE(hasCrashedBefore, "Module should be in crashed state"); + + // === PHASE 3: Trigger hot-reload (recovery) === + std::cout << "Phase 3: Triggering hot-reload for recovery...\n"; + + auto recoveryStart = std::chrono::high_resolution_clock::now(); + + // Hot-reload via ModuleLoader + auto recoveredModule = loader.reload(std::move(crashedModule)); + + auto recoveryEnd = std::chrono::high_resolution_clock::now(); + float recoveryTime = std::chrono::duration(recoveryEnd - recoveryStart).count(); + + std::cout << " ✓ Hot-reload completed in " << recoveryTime << "ms\n"; + + metrics.recordReloadTime(recoveryTime); + reporter.addMetric("recovery_time_ms", recoveryTime); + + // Ré-enregistrer module récupéré + moduleSystem->registerModule("ErrorRecoveryModule", std::move(recoveredModule)); + + // === PHASE 4: Verify recovery === + std::cout << "\nPhase 4: Verifying recovery...\n"; + + auto recoveredModuleRef = moduleSystem->extractModule(); + auto postRecoveryState = recoveredModuleRef->getState(); + + auto* jsonNodeAfter = dynamic_cast(postRecoveryState.get()); + if (!jsonNodeAfter) { + std::cerr << "❌ Failed to extract state after recovery\n"; + return 1; + } + + const auto& stateAfter = jsonNodeAfter->getJsonData(); + int frameCountAfter = stateAfter.value("frameCount", 0); + int crashCountAfter = stateAfter.value("crashCount", 0); + int recoveryCountAfter = stateAfter.value("recoveryCount", 0); + bool hasCrashedAfter = stateAfter.value("hasCrashed", false); + int crashAtFrameAfter = stateAfter.value("crashAtFrame", -1); + + std::cout << " State after recovery:\n"; + std::cout << " Frame count: " << frameCountAfter << "\n"; + std::cout << " Crash count: " << crashCountAfter << "\n"; + std::cout << " Recovery count: " << recoveryCountAfter << "\n"; + std::cout << " Has crashed: " << (hasCrashedAfter ? "YES" : "NO") << "\n"; + std::cout << " Crash trigger: " << crashAtFrameAfter << "\n\n"; + + // Vérifications de recovery + ASSERT_EQ(frameCountAfter, frameCountBefore, "Frame count should be preserved"); + ASSERT_FALSE(hasCrashedAfter, "Module should no longer be in crashed state"); + ASSERT_EQ(recoveryCountAfter, 1, "Recovery count should be 1"); + ASSERT_EQ(crashAtFrameAfter, -1, "Crash trigger should be disabled"); + + reporter.addAssertion("frame_count_preserved", frameCountAfter == frameCountBefore); + reporter.addAssertion("crash_state_cleared", !hasCrashedAfter); + reporter.addAssertion("recovery_count_incremented", recoveryCountAfter == 1); + reporter.addAssertion("crash_trigger_disabled", crashAtFrameAfter == -1); + + std::cout << " ✅ RECOVERY SUCCESSFUL - Module is healthy again\n\n"; + + // Ré-enregistrer pour phase 5 + moduleSystem->registerModule("ErrorRecoveryModule", std::move(recoveredModuleRef)); + + // === PHASE 5: Continue execution (stability check) === + std::cout << "Phase 5: Stability check - Running 120 more frames...\n"; + + bool stableExecution = true; + int framesAfterRecovery = 0; + + for (int frame = 1; frame <= 120; frame++) { + try { + auto frameStart = std::chrono::high_resolution_clock::now(); + + moduleSystem->processModules(1.0f / 60.0f); + + auto frameEnd = std::chrono::high_resolution_clock::now(); + float frameTime = std::chrono::duration(frameEnd - frameStart).count(); + metrics.recordFPS(1000.0f / frameTime); + + framesAfterRecovery++; + + if (frame % 30 == 0) { + std::cout << " Frame " << frame << "/120 - Stable\n"; + } + + } catch (const std::exception& e) { + std::cout << "\n❌ UNEXPECTED CRASH after recovery at frame " << frame << "\n"; + std::cout << " Exception: " << e.what() << "\n"; + stableExecution = false; + break; + } + } + + ASSERT_TRUE(stableExecution, "Module should execute stably after recovery"); + ASSERT_EQ(framesAfterRecovery, 120, "Should complete all 120 frames"); + + reporter.addAssertion("stable_after_recovery", stableExecution); + reporter.addMetric("frames_after_recovery", static_cast(framesAfterRecovery)); + + std::cout << " ✅ Stability verified - " << framesAfterRecovery << " frames executed without issues\n\n"; + + // === VÉRIFICATIONS FINALES === + std::cout << "Final verifications...\n"; + + // Memory growth + size_t memGrowth = metrics.getMemoryGrowth(); + float memGrowthMB = memGrowth / (1024.0f * 1024.0f); + ASSERT_LT(memGrowthMB, 10.0f, "Memory growth should be < 10MB"); + reporter.addMetric("memory_growth_mb", memGrowthMB); + + // FPS (moins strict pour test de recovery - focus sur stability) + float minFPS = metrics.getFPSMin(); + ASSERT_GT(minFPS, 5.0f, "Min FPS should be > 5 (recovery test allows slower frames)"); + reporter.addMetric("fps_min", minFPS); + reporter.addMetric("fps_avg", metrics.getFPSAvg()); + + // Recovery time threshold + ASSERT_LT(recoveryTime, 500.0f, "Recovery time should be < 500ms"); + + // Crash count + int totalCrashes = metrics.getCrashCount(); + ASSERT_EQ(totalCrashes, 1, "Should have exactly 1 controlled crash"); + reporter.addMetric("total_crashes", static_cast(totalCrashes)); + + // === RAPPORTS === + std::cout << "\n"; + std::cout << "Summary:\n"; + std::cout << " 🎯 Crash detected at frame " << crashFrame << " (expected: 60)\n"; + std::cout << " 🔄 Recovery time: " << recoveryTime << "ms\n"; + std::cout << " ✅ Stable execution: " << framesAfterRecovery << " frames after recovery\n"; + std::cout << " 💾 Memory growth: " << memGrowthMB << " MB\n"; + std::cout << " 📊 FPS: min=" << minFPS << ", avg=" << metrics.getFPSAvg() << "\n\n"; + + metrics.printReport(); + reporter.printFinalReport(); + + return reporter.getExitCode(); +} diff --git a/tests/integration/test_07_limits.cpp b/tests/integration/test_07_limits.cpp index 3dbb069..e087ba7 100644 --- a/tests/integration/test_07_limits.cpp +++ b/tests/integration/test_07_limits.cpp @@ -1,418 +1,421 @@ -#include "grove/ModuleLoader.h" -#include "grove/SequentialModuleSystem.h" -#include "grove/JsonDataNode.h" -#include "../helpers/TestMetrics.h" -#include "../helpers/TestAssertions.h" -#include "../helpers/TestReporter.h" -#include "../helpers/SystemUtils.h" -#include -#include -#include -#include -#include -#include - -using namespace grove; - -/** - * Test 07: Limite Tests - * - * Objectif: Valider la robustesse du système face aux conditions extrêmes: - * - Large state (100MB+) - * - Long initialization - * - Timeouts - * - Memory pressure - * - State corruption detection - */ - -int main() { - TestReporter reporter("Limite Tests"); - TestMetrics metrics; - - std::cout << "================================================================================\n"; - std::cout << "TEST: Limite Tests - Extreme Conditions & Edge Cases\n"; - std::cout << "================================================================================\n\n"; - - // ======================================================================== - // TEST 1: Large State Serialization - // ======================================================================== - std::cout << "=== TEST 1: Large State Serialization ===\n\n"; - - ModuleLoader loader; - auto moduleSystem = std::make_unique(); - - std::string modulePath = "./libHeavyStateModule.so"; - auto module = loader.load(modulePath, "HeavyStateModule", false); - - // Config: particules réduites pour test rapide, mais assez pour être significatif - nlohmann::json configJson; - configJson["version"] = "v1.0"; - configJson["particleCount"] = 100000; // 100k au lieu de 1M pour test rapide - configJson["terrainSize"] = 1000; // 1000x1000 au lieu de 10000x10000 - configJson["initDuration"] = 2.0f; // 2s au lieu de 8s - configJson["initTimeout"] = 5.0f; - - auto config = std::make_unique("config", configJson); - - // Mesurer temps d'initialisation - std::cout << "Initializing module...\n"; - auto initStart = std::chrono::high_resolution_clock::now(); - - module->setConfiguration(*config, nullptr, nullptr); - - auto initEnd = std::chrono::high_resolution_clock::now(); - float initTime = std::chrono::duration(initEnd - initStart).count(); - - std::cout << " Init time: " << initTime << "s\n"; - ASSERT_GT(initTime, 1.5f, "Init should take at least 1.5s (simulated heavy init)"); - ASSERT_LT(initTime, 3.0f, "Init should not exceed 3s"); - reporter.addMetric("init_time_s", initTime); - - moduleSystem->registerModule("HeavyStateModule", std::move(module)); - - // Exécuter quelques frames - std::cout << "Running 300 frames...\n"; - for (int i = 0; i < 300; i++) { - moduleSystem->processModules(1.0f / 60.0f); - } - - // Mesurer temps de getState() - std::cout << "Extracting state (getState)...\n"; - auto getStateStart = std::chrono::high_resolution_clock::now(); - - auto heavyModule = moduleSystem->extractModule(); - auto state = heavyModule->getState(); - - auto getStateEnd = std::chrono::high_resolution_clock::now(); - float getStateTime = std::chrono::duration(getStateEnd - getStateStart).count(); - - std::cout << " getState time: " << 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()); - if (jsonNode) { - std::string stateStr = jsonNode->getJsonData().dump(); - size_t stateSize = stateStr.size(); - float stateSizeMB = stateSize / 1024.0f / 1024.0f; - std::cout << " State size: " << stateSizeMB << " MB\n"; - reporter.addMetric("state_size_mb", stateSizeMB); - } - - // Recharger le module (simuler hot-reload) - std::cout << "Reloading module...\n"; - auto reloadStart = std::chrono::high_resolution_clock::now(); - - // setState() est appelé automatiquement par reload() - auto moduleReloaded = loader.reload(std::move(heavyModule)); - - auto reloadEnd = std::chrono::high_resolution_clock::now(); - float reloadTime = std::chrono::duration(reloadEnd - reloadStart).count(); - std::cout << " Total reload time: " << reloadTime << "ms\n"; - reporter.addMetric("reload_time_ms", reloadTime); - - // Ré-enregistrer - moduleSystem->registerModule("HeavyStateModule", std::move(moduleReloaded)); - - // Vérifier intégrité après reload - auto heavyModuleAfter = moduleSystem->extractModule(); - auto stateAfter = heavyModuleAfter->getState(); - auto* jsonNodeAfter = dynamic_cast(stateAfter.get()); - if (jsonNodeAfter) { - const auto& dataAfter = jsonNodeAfter->getJsonData(); - int particleCount = dataAfter["config"]["particleCount"]; - ASSERT_EQ(particleCount, 100000, "Should have 100k particles after reload"); - reporter.addAssertion("particles_preserved", particleCount == 100000); - std::cout << " ✓ Particles preserved: " << particleCount << "\n"; - } - - // Ré-enregistrer pour continuer - moduleSystem->registerModule("HeavyStateModule", std::move(heavyModuleAfter)); - - // Continuer exécution - std::cout << "Running 300 more frames post-reload...\n"; - for (int i = 0; i < 300; i++) { - moduleSystem->processModules(1.0f / 60.0f); - } - - std::cout << "\n✅ TEST 1 PASSED\n\n"; - - // ======================================================================== - // TEST 2: Long Initialization Timeout - // ======================================================================== - std::cout << "=== TEST 2: Long Initialization Timeout ===\n\n"; - - auto moduleSystem2 = std::make_unique(); - auto moduleTimeout = loader.load(modulePath, "HeavyStateModule", false); - - // Config avec init long + timeout court (va échouer) - nlohmann::json configTimeout; - configTimeout["version"] = "v1.0"; - configTimeout["particleCount"] = 100000; - configTimeout["terrainSize"] = 1000; - configTimeout["initDuration"] = 4.0f; // Init va prendre 4s - configTimeout["initTimeout"] = 3.0f; // Timeout à 3s (trop court) - - auto configTimeoutNode = std::make_unique("config", configTimeout); - - bool timedOut = false; - std::cout << "Attempting init with timeout=3s (duration=4s)...\n"; - try { - moduleTimeout->setConfiguration(*configTimeoutNode, nullptr, nullptr); - } catch (const std::exception& e) { - std::string msg = e.what(); - if (msg.find("timeout") != std::string::npos || - msg.find("Timeout") != std::string::npos || - msg.find("exceeded") != std::string::npos) { - timedOut = true; - std::cout << " ✓ Timeout detected: " << msg << "\n"; - } else { - std::cout << " ✗ Unexpected error: " << msg << "\n"; - } - } - - ASSERT_TRUE(timedOut, "Should timeout when init > timeout threshold"); - reporter.addAssertion("timeout_detection", timedOut); - - // Réessayer avec timeout suffisant - std::cout << "\nRetrying with adequate timeout=6s...\n"; - auto moduleTimeout2 = loader.load(modulePath, "HeavyStateModule", false); - - nlohmann::json configOk; - configOk["version"] = "v1.0"; - configOk["particleCount"] = 50000; - configOk["terrainSize"] = 500; - configOk["initDuration"] = 2.0f; - configOk["initTimeout"] = 5.0f; - - auto configOkNode = std::make_unique("config", configOk); - - bool success = true; - try { - moduleTimeout2->setConfiguration(*configOkNode, nullptr, nullptr); - moduleSystem2->registerModule("HeavyStateModule", std::move(moduleTimeout2)); - moduleSystem2->processModules(1.0f / 60.0f); - std::cout << " ✓ Init succeeded with adequate timeout\n"; - } catch (const std::exception& e) { - success = false; - std::cout << " ✗ Failed: " << e.what() << "\n"; - } - - ASSERT_TRUE(success, "Should succeed with adequate timeout"); - reporter.addAssertion("timeout_recovery", success); - - std::cout << "\n✅ TEST 2 PASSED\n\n"; - - // ======================================================================== - // TEST 3: Memory Pressure During Reload - // ======================================================================== - std::cout << "=== TEST 3: Memory Pressure During Reload ===\n\n"; - - // Créer un nouveau system pour ce test - auto moduleSystem3Pressure = std::make_unique(); - auto modulePressureInit = loader.load(modulePath, "HeavyStateModule", false); - - nlohmann::json configPressure; - configPressure["version"] = "v1.0"; - configPressure["particleCount"] = 10000; - configPressure["terrainSize"] = 100; - configPressure["initDuration"] = 0.5f; - auto configPressureNode = std::make_unique("config", configPressure); - modulePressureInit->setConfiguration(*configPressureNode, nullptr, nullptr); - moduleSystem3Pressure->registerModule("HeavyStateModule", std::move(modulePressureInit)); - - size_t memBefore = getCurrentMemoryUsage(); - std::cout << "Memory before: " << (memBefore / 1024.0f / 1024.0f) << " MB\n"; - - // Exécuter quelques frames - std::cout << "Running 300 frames...\n"; - for (int i = 0; i < 300; i++) { - moduleSystem3Pressure->processModules(1.0f / 60.0f); - } - - // Allouer temporairement beaucoup de mémoire - std::cout << "Allocating temporary 50MB during reload...\n"; - std::vector tempAlloc; - - auto reloadPressureStart = std::chrono::high_resolution_clock::now(); - - // Allouer 50MB - tempAlloc.resize(50 * 1024 * 1024); - std::fill(tempAlloc.begin(), tempAlloc.end(), 0x42); - - size_t memDuringAlloc = getCurrentMemoryUsage(); - std::cout << " Memory with allocation: " << (memDuringAlloc / 1024.0f / 1024.0f) << " MB\n"; - - // Reload pendant la pression mémoire - auto modulePressure = moduleSystem3Pressure->extractModule(); - auto modulePressureReloaded = loader.reload(std::move(modulePressure)); - moduleSystem3Pressure->registerModule("HeavyStateModule", std::move(modulePressureReloaded)); - - auto reloadPressureEnd = std::chrono::high_resolution_clock::now(); - float reloadPressureTime = std::chrono::duration( - reloadPressureEnd - reloadPressureStart).count(); - - std::cout << " Reload under pressure: " << reloadPressureTime << "ms\n"; - reporter.addMetric("reload_under_pressure_ms", reloadPressureTime); - - // Libérer allocation temporaire - tempAlloc.clear(); - tempAlloc.shrink_to_fit(); - - std::this_thread::sleep_for(std::chrono::milliseconds(200)); - - size_t memAfter = getCurrentMemoryUsage(); - std::cout << " Memory after cleanup: " << (memAfter / 1024.0f / 1024.0f) << " MB\n"; - - long memGrowth = static_cast(memAfter) - static_cast(memBefore); - float memGrowthMB = memGrowth / 1024.0f / 1024.0f; - std::cout << " Net memory growth: " << memGrowthMB << " MB\n"; - - // Tolérance: max 10MB de croissance - ASSERT_LT(std::abs(memGrowth), 10 * 1024 * 1024, "Memory growth should be < 10MB"); - reporter.addMetric("memory_growth_mb", memGrowthMB); - - std::cout << "\n✅ TEST 3 PASSED\n\n"; - - // ======================================================================== - // TEST 4: Incremental Reloads - // ======================================================================== - std::cout << "=== TEST 4: Incremental Reloads ===\n\n"; - - auto moduleSystem3 = std::make_unique(); - - nlohmann::json configIncremental; - configIncremental["version"] = "v1.0"; - configIncremental["particleCount"] = 10000; // Petit pour test rapide - configIncremental["terrainSize"] = 100; - configIncremental["initDuration"] = 0.5f; - configIncremental["incrementalState"] = true; - - auto configIncrNode = std::make_unique("config", configIncremental); - auto moduleIncr = loader.load(modulePath, "HeavyStateModule", false); - moduleIncr->setConfiguration(*configIncrNode, nullptr, nullptr); - moduleSystem3->registerModule("HeavyStateModule", std::move(moduleIncr)); - - std::vector incrementalTimes; - - std::cout << "Performing 5 incremental reloads...\n"; - for (int reload = 0; reload < 5; reload++) { - // Exécuter 60 frames - for (int i = 0; i < 60; i++) { - moduleSystem3->processModules(1.0f / 60.0f); - } - - // Reload - auto incStart = std::chrono::high_resolution_clock::now(); - - auto moduleInc = moduleSystem3->extractModule(); - auto moduleIncReloaded = loader.reload(std::move(moduleInc)); - moduleSystem3->registerModule("HeavyStateModule", std::move(moduleIncReloaded)); - - 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 << "\nAverage incremental reload: " << avgIncremental << "ms\n"; - - ASSERT_LT(avgIncremental, 2000.0f, "Incremental reloads should be reasonably fast"); - reporter.addMetric("avg_incremental_reload_ms", avgIncremental); - - std::cout << "\n✅ TEST 4 PASSED\n\n"; - - // ======================================================================== - // TEST 5: State Corruption Detection - // ======================================================================== - std::cout << "=== TEST 5: State Corruption Detection ===\n\n"; - - auto moduleSystem4 = std::make_unique(); - - nlohmann::json configNormal; - configNormal["version"] = "v1.0"; - configNormal["particleCount"] = 1000; - configNormal["terrainSize"] = 50; - configNormal["initDuration"] = 0.2f; - - auto configNormalNode = std::make_unique("config", configNormal); - auto moduleNormal = loader.load(modulePath, "HeavyStateModule", false); - moduleNormal->setConfiguration(*configNormalNode, nullptr, nullptr); - moduleSystem4->registerModule("HeavyStateModule", std::move(moduleNormal)); - - // Exécuter un peu - for (int i = 0; i < 60; i++) { - moduleSystem4->processModules(1.0f / 60.0f); - } - - // Créer un état corrompu - std::cout << "Creating corrupted state...\n"; - nlohmann::json corruptedState; - corruptedState["version"] = "v1.0"; - corruptedState["frameCount"] = "INVALID_STRING"; // Type incorrect - corruptedState["config"]["particleCount"] = -500; // Valeur invalide - corruptedState["config"]["terrainWidth"] = 100; - corruptedState["config"]["terrainHeight"] = 100; - corruptedState["particles"]["count"] = 1000; - corruptedState["particles"]["data"] = "CORRUPTED"; - corruptedState["terrain"]["width"] = 50; - corruptedState["terrain"]["height"] = 50; - corruptedState["terrain"]["compressed"] = true; - corruptedState["terrain"]["data"] = "CORRUPTED"; - corruptedState["history"] = nlohmann::json::array(); - - auto corruptedNode = std::make_unique("corrupted", corruptedState); - - bool detectedCorruption = false; - std::cout << "Attempting to apply corrupted state...\n"; - try { - auto moduleCorrupt = loader.load(modulePath, "HeavyStateModule", false); - moduleCorrupt->setState(*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 d'origine reste fonctionnel - std::cout << "Verifying original module still functional...\n"; - bool stillFunctional = true; - try { - for (int i = 0; i < 60; i++) { - moduleSystem4->processModules(1.0f / 60.0f); - } - std::cout << " ✓ Module remains functional\n"; - } catch (const std::exception& e) { - stillFunctional = false; - std::cout << " ✗ Module broken: " << e.what() << "\n"; - } - - ASSERT_TRUE(stillFunctional, "Module should remain functional after rejected corrupted state"); - reporter.addAssertion("functional_after_corruption", stillFunctional); - - std::cout << "\n✅ TEST 5 PASSED\n\n"; - - // ======================================================================== - // RAPPORT FINAL - // ======================================================================== - - std::cout << "================================================================================\n"; - std::cout << "SUMMARY\n"; - std::cout << "================================================================================\n\n"; - - metrics.printReport(); - reporter.printFinalReport(); - - std::cout << "\n================================================================================\n"; - std::cout << "Result: " << (reporter.getExitCode() == 0 ? "✅ ALL TESTS PASSED" : "❌ SOME TESTS FAILED") << "\n"; - std::cout << "================================================================================\n"; - - return reporter.getExitCode(); -} +#include "grove/ModuleLoader.h" +#include "grove/SequentialModuleSystem.h" +#include "grove/JsonDataNode.h" +#include "../helpers/TestMetrics.h" +#include "../helpers/TestAssertions.h" +#include "../helpers/TestReporter.h" +#include "../helpers/SystemUtils.h" +#include +#include +#include +#include +#include +#include + +using namespace grove; + +/** + * Test 07: Limite Tests + * + * Objectif: Valider la robustesse du système face aux conditions extrêmes: + * - Large state (100MB+) + * - Long initialization + * - Timeouts + * - Memory pressure + * - State corruption detection + */ + +int main() { + TestReporter reporter("Limite Tests"); + TestMetrics metrics; + + std::cout << "================================================================================\n"; + std::cout << "TEST: Limite Tests - Extreme Conditions & Edge Cases\n"; + std::cout << "================================================================================\n\n"; + + // ======================================================================== + // TEST 1: Large State Serialization + // ======================================================================== + std::cout << "=== TEST 1: Large State Serialization ===\n\n"; + + ModuleLoader loader; + auto moduleSystem = std::make_unique(); + + std::string modulePath = "./libHeavyStateModule.so"; +#ifdef _WIN32 + modulePath = "./libHeavyStateModule.dll"; +#endif + auto module = loader.load(modulePath, "HeavyStateModule", false); + + // Config: particules réduites pour test rapide, mais assez pour être significatif + nlohmann::json configJson; + configJson["version"] = "v1.0"; + configJson["particleCount"] = 100000; // 100k au lieu de 1M pour test rapide + configJson["terrainSize"] = 1000; // 1000x1000 au lieu de 10000x10000 + configJson["initDuration"] = 2.0f; // 2s au lieu de 8s + configJson["initTimeout"] = 5.0f; + + auto config = std::make_unique("config", configJson); + + // Mesurer temps d'initialisation + std::cout << "Initializing module...\n"; + auto initStart = std::chrono::high_resolution_clock::now(); + + module->setConfiguration(*config, nullptr, nullptr); + + auto initEnd = std::chrono::high_resolution_clock::now(); + float initTime = std::chrono::duration(initEnd - initStart).count(); + + std::cout << " Init time: " << initTime << "s\n"; + ASSERT_GT(initTime, 1.5f, "Init should take at least 1.5s (simulated heavy init)"); + ASSERT_LT(initTime, 3.0f, "Init should not exceed 3s"); + reporter.addMetric("init_time_s", initTime); + + moduleSystem->registerModule("HeavyStateModule", std::move(module)); + + // Exécuter quelques frames + std::cout << "Running 300 frames...\n"; + for (int i = 0; i < 300; i++) { + moduleSystem->processModules(1.0f / 60.0f); + } + + // Mesurer temps de getState() + std::cout << "Extracting state (getState)...\n"; + auto getStateStart = std::chrono::high_resolution_clock::now(); + + auto heavyModule = moduleSystem->extractModule(); + auto state = heavyModule->getState(); + + auto getStateEnd = std::chrono::high_resolution_clock::now(); + float getStateTime = std::chrono::duration(getStateEnd - getStateStart).count(); + + std::cout << " getState time: " << 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()); + if (jsonNode) { + std::string stateStr = jsonNode->getJsonData().dump(); + size_t stateSize = stateStr.size(); + float stateSizeMB = stateSize / 1024.0f / 1024.0f; + std::cout << " State size: " << stateSizeMB << " MB\n"; + reporter.addMetric("state_size_mb", stateSizeMB); + } + + // Recharger le module (simuler hot-reload) + std::cout << "Reloading module...\n"; + auto reloadStart = std::chrono::high_resolution_clock::now(); + + // setState() est appelé automatiquement par reload() + auto moduleReloaded = loader.reload(std::move(heavyModule)); + + auto reloadEnd = std::chrono::high_resolution_clock::now(); + float reloadTime = std::chrono::duration(reloadEnd - reloadStart).count(); + std::cout << " Total reload time: " << reloadTime << "ms\n"; + reporter.addMetric("reload_time_ms", reloadTime); + + // Ré-enregistrer + moduleSystem->registerModule("HeavyStateModule", std::move(moduleReloaded)); + + // Vérifier intégrité après reload + auto heavyModuleAfter = moduleSystem->extractModule(); + auto stateAfter = heavyModuleAfter->getState(); + auto* jsonNodeAfter = dynamic_cast(stateAfter.get()); + if (jsonNodeAfter) { + const auto& dataAfter = jsonNodeAfter->getJsonData(); + int particleCount = dataAfter["config"]["particleCount"]; + ASSERT_EQ(particleCount, 100000, "Should have 100k particles after reload"); + reporter.addAssertion("particles_preserved", particleCount == 100000); + std::cout << " ✓ Particles preserved: " << particleCount << "\n"; + } + + // Ré-enregistrer pour continuer + moduleSystem->registerModule("HeavyStateModule", std::move(heavyModuleAfter)); + + // Continuer exécution + std::cout << "Running 300 more frames post-reload...\n"; + for (int i = 0; i < 300; i++) { + moduleSystem->processModules(1.0f / 60.0f); + } + + std::cout << "\n✅ TEST 1 PASSED\n\n"; + + // ======================================================================== + // TEST 2: Long Initialization Timeout + // ======================================================================== + std::cout << "=== TEST 2: Long Initialization Timeout ===\n\n"; + + auto moduleSystem2 = std::make_unique(); + auto moduleTimeout = loader.load(modulePath, "HeavyStateModule", false); + + // Config avec init long + timeout court (va échouer) + nlohmann::json configTimeout; + configTimeout["version"] = "v1.0"; + configTimeout["particleCount"] = 100000; + configTimeout["terrainSize"] = 1000; + configTimeout["initDuration"] = 4.0f; // Init va prendre 4s + configTimeout["initTimeout"] = 3.0f; // Timeout à 3s (trop court) + + auto configTimeoutNode = std::make_unique("config", configTimeout); + + bool timedOut = false; + std::cout << "Attempting init with timeout=3s (duration=4s)...\n"; + try { + moduleTimeout->setConfiguration(*configTimeoutNode, nullptr, nullptr); + } catch (const std::exception& e) { + std::string msg = e.what(); + if (msg.find("timeout") != std::string::npos || + msg.find("Timeout") != std::string::npos || + msg.find("exceeded") != std::string::npos) { + timedOut = true; + std::cout << " ✓ Timeout detected: " << msg << "\n"; + } else { + std::cout << " ✗ Unexpected error: " << msg << "\n"; + } + } + + ASSERT_TRUE(timedOut, "Should timeout when init > timeout threshold"); + reporter.addAssertion("timeout_detection", timedOut); + + // Réessayer avec timeout suffisant + std::cout << "\nRetrying with adequate timeout=6s...\n"; + auto moduleTimeout2 = loader.load(modulePath, "HeavyStateModule", false); + + nlohmann::json configOk; + configOk["version"] = "v1.0"; + configOk["particleCount"] = 50000; + configOk["terrainSize"] = 500; + configOk["initDuration"] = 2.0f; + configOk["initTimeout"] = 5.0f; + + auto configOkNode = std::make_unique("config", configOk); + + bool success = true; + try { + moduleTimeout2->setConfiguration(*configOkNode, nullptr, nullptr); + moduleSystem2->registerModule("HeavyStateModule", std::move(moduleTimeout2)); + moduleSystem2->processModules(1.0f / 60.0f); + std::cout << " ✓ Init succeeded with adequate timeout\n"; + } catch (const std::exception& e) { + success = false; + std::cout << " ✗ Failed: " << e.what() << "\n"; + } + + ASSERT_TRUE(success, "Should succeed with adequate timeout"); + reporter.addAssertion("timeout_recovery", success); + + std::cout << "\n✅ TEST 2 PASSED\n\n"; + + // ======================================================================== + // TEST 3: Memory Pressure During Reload + // ======================================================================== + std::cout << "=== TEST 3: Memory Pressure During Reload ===\n\n"; + + // Créer un nouveau system pour ce test + auto moduleSystem3Pressure = std::make_unique(); + auto modulePressureInit = loader.load(modulePath, "HeavyStateModule", false); + + nlohmann::json configPressure; + configPressure["version"] = "v1.0"; + configPressure["particleCount"] = 10000; + configPressure["terrainSize"] = 100; + configPressure["initDuration"] = 0.5f; + auto configPressureNode = std::make_unique("config", configPressure); + modulePressureInit->setConfiguration(*configPressureNode, nullptr, nullptr); + moduleSystem3Pressure->registerModule("HeavyStateModule", std::move(modulePressureInit)); + + size_t memBefore = getCurrentMemoryUsage(); + std::cout << "Memory before: " << (memBefore / 1024.0f / 1024.0f) << " MB\n"; + + // Exécuter quelques frames + std::cout << "Running 300 frames...\n"; + for (int i = 0; i < 300; i++) { + moduleSystem3Pressure->processModules(1.0f / 60.0f); + } + + // Allouer temporairement beaucoup de mémoire + std::cout << "Allocating temporary 50MB during reload...\n"; + std::vector tempAlloc; + + auto reloadPressureStart = std::chrono::high_resolution_clock::now(); + + // Allouer 50MB + tempAlloc.resize(50 * 1024 * 1024); + std::fill(tempAlloc.begin(), tempAlloc.end(), 0x42); + + size_t memDuringAlloc = getCurrentMemoryUsage(); + std::cout << " Memory with allocation: " << (memDuringAlloc / 1024.0f / 1024.0f) << " MB\n"; + + // Reload pendant la pression mémoire + auto modulePressure = moduleSystem3Pressure->extractModule(); + auto modulePressureReloaded = loader.reload(std::move(modulePressure)); + moduleSystem3Pressure->registerModule("HeavyStateModule", std::move(modulePressureReloaded)); + + auto reloadPressureEnd = std::chrono::high_resolution_clock::now(); + float reloadPressureTime = std::chrono::duration( + reloadPressureEnd - reloadPressureStart).count(); + + std::cout << " Reload under pressure: " << reloadPressureTime << "ms\n"; + reporter.addMetric("reload_under_pressure_ms", reloadPressureTime); + + // Libérer allocation temporaire + tempAlloc.clear(); + tempAlloc.shrink_to_fit(); + + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + size_t memAfter = getCurrentMemoryUsage(); + std::cout << " Memory after cleanup: " << (memAfter / 1024.0f / 1024.0f) << " MB\n"; + + long memGrowth = static_cast(memAfter) - static_cast(memBefore); + float memGrowthMB = memGrowth / 1024.0f / 1024.0f; + std::cout << " Net memory growth: " << memGrowthMB << " MB\n"; + + // Tolérance: max 10MB de croissance + ASSERT_LT(std::abs(memGrowth), 10 * 1024 * 1024, "Memory growth should be < 10MB"); + reporter.addMetric("memory_growth_mb", memGrowthMB); + + std::cout << "\n✅ TEST 3 PASSED\n\n"; + + // ======================================================================== + // TEST 4: Incremental Reloads + // ======================================================================== + std::cout << "=== TEST 4: Incremental Reloads ===\n\n"; + + auto moduleSystem3 = std::make_unique(); + + nlohmann::json configIncremental; + configIncremental["version"] = "v1.0"; + configIncremental["particleCount"] = 10000; // Petit pour test rapide + configIncremental["terrainSize"] = 100; + configIncremental["initDuration"] = 0.5f; + configIncremental["incrementalState"] = true; + + auto configIncrNode = std::make_unique("config", configIncremental); + auto moduleIncr = loader.load(modulePath, "HeavyStateModule", false); + moduleIncr->setConfiguration(*configIncrNode, nullptr, nullptr); + moduleSystem3->registerModule("HeavyStateModule", std::move(moduleIncr)); + + std::vector incrementalTimes; + + std::cout << "Performing 5 incremental reloads...\n"; + for (int reload = 0; reload < 5; reload++) { + // Exécuter 60 frames + for (int i = 0; i < 60; i++) { + moduleSystem3->processModules(1.0f / 60.0f); + } + + // Reload + auto incStart = std::chrono::high_resolution_clock::now(); + + auto moduleInc = moduleSystem3->extractModule(); + auto moduleIncReloaded = loader.reload(std::move(moduleInc)); + moduleSystem3->registerModule("HeavyStateModule", std::move(moduleIncReloaded)); + + 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 << "\nAverage incremental reload: " << avgIncremental << "ms\n"; + + ASSERT_LT(avgIncremental, 2000.0f, "Incremental reloads should be reasonably fast"); + reporter.addMetric("avg_incremental_reload_ms", avgIncremental); + + std::cout << "\n✅ TEST 4 PASSED\n\n"; + + // ======================================================================== + // TEST 5: State Corruption Detection + // ======================================================================== + std::cout << "=== TEST 5: State Corruption Detection ===\n\n"; + + auto moduleSystem4 = std::make_unique(); + + nlohmann::json configNormal; + configNormal["version"] = "v1.0"; + configNormal["particleCount"] = 1000; + configNormal["terrainSize"] = 50; + configNormal["initDuration"] = 0.2f; + + auto configNormalNode = std::make_unique("config", configNormal); + auto moduleNormal = loader.load(modulePath, "HeavyStateModule", false); + moduleNormal->setConfiguration(*configNormalNode, nullptr, nullptr); + moduleSystem4->registerModule("HeavyStateModule", std::move(moduleNormal)); + + // Exécuter un peu + for (int i = 0; i < 60; i++) { + moduleSystem4->processModules(1.0f / 60.0f); + } + + // Créer un état corrompu + std::cout << "Creating corrupted state...\n"; + nlohmann::json corruptedState; + corruptedState["version"] = "v1.0"; + corruptedState["frameCount"] = "INVALID_STRING"; // Type incorrect + corruptedState["config"]["particleCount"] = -500; // Valeur invalide + corruptedState["config"]["terrainWidth"] = 100; + corruptedState["config"]["terrainHeight"] = 100; + corruptedState["particles"]["count"] = 1000; + corruptedState["particles"]["data"] = "CORRUPTED"; + corruptedState["terrain"]["width"] = 50; + corruptedState["terrain"]["height"] = 50; + corruptedState["terrain"]["compressed"] = true; + corruptedState["terrain"]["data"] = "CORRUPTED"; + corruptedState["history"] = nlohmann::json::array(); + + auto corruptedNode = std::make_unique("corrupted", corruptedState); + + bool detectedCorruption = false; + std::cout << "Attempting to apply corrupted state...\n"; + try { + auto moduleCorrupt = loader.load(modulePath, "HeavyStateModule", false); + moduleCorrupt->setState(*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 d'origine reste fonctionnel + std::cout << "Verifying original module still functional...\n"; + bool stillFunctional = true; + try { + for (int i = 0; i < 60; i++) { + moduleSystem4->processModules(1.0f / 60.0f); + } + std::cout << " ✓ Module remains functional\n"; + } catch (const std::exception& e) { + stillFunctional = false; + std::cout << " ✗ Module broken: " << e.what() << "\n"; + } + + ASSERT_TRUE(stillFunctional, "Module should remain functional after rejected corrupted state"); + reporter.addAssertion("functional_after_corruption", stillFunctional); + + std::cout << "\n✅ TEST 5 PASSED\n\n"; + + // ======================================================================== + // RAPPORT FINAL + // ======================================================================== + + std::cout << "================================================================================\n"; + std::cout << "SUMMARY\n"; + std::cout << "================================================================================\n\n"; + + metrics.printReport(); + reporter.printFinalReport(); + + std::cout << "\n================================================================================\n"; + std::cout << "Result: " << (reporter.getExitCode() == 0 ? "✅ ALL TESTS PASSED" : "❌ SOME TESTS FAILED") << "\n"; + std::cout << "================================================================================\n"; + + return reporter.getExitCode(); +} diff --git a/tests/integration/test_08_config_hotreload.cpp b/tests/integration/test_08_config_hotreload.cpp index b4aa5e3..a8cd6e6 100644 --- a/tests/integration/test_08_config_hotreload.cpp +++ b/tests/integration/test_08_config_hotreload.cpp @@ -1,349 +1,352 @@ -#include "grove/ModuleLoader.h" -#include "grove/SequentialModuleSystem.h" -#include "grove/JsonDataNode.h" -#include "../helpers/TestMetrics.h" -#include "../helpers/TestAssertions.h" -#include "../helpers/TestReporter.h" -#include "../helpers/SystemUtils.h" -#include -#include -#include -#include - -using namespace grove; - -/** - * Test 08: Config Hot-Reload - * - * Objectif: Valider que le système peut modifier la configuration d'un module - * à la volée sans redémarrage, avec validation et rollback. - * - * Scénario: - * Phase 0: Baseline avec config initiale (10s) - * Phase 1: Doubler spawn rate et speed (10s) - * Phase 2: Changements complexes (couleurs, physique, limites) (10s) - * Phase 3: Config invalide + rollback (5s) - * Phase 4: Partial config update (5s) - * - * Métriques: - * - Config update time - * - Config validation - * - Rollback functionality - * - Partial merge accuracy - */ - -int main() { - TestReporter reporter("Config Hot-Reload"); - TestMetrics metrics; - - std::cout << "================================================================================\n"; - std::cout << "TEST: Config Hot-Reload\n"; - std::cout << "================================================================================\n\n"; - - // === SETUP === - std::cout << "Setup: Loading ConfigurableModule with initial config...\n"; - - ModuleLoader loader; - auto moduleSystem = std::make_unique(); - - // Charger module - std::string modulePath = "./libConfigurableModule.so"; - auto module = loader.load(modulePath, "ConfigurableModule", false); - - // Config initiale - nlohmann::json configJson; - configJson["spawnRate"] = 10; - configJson["maxEntities"] = 150; // Higher limit for Phase 0 - configJson["entitySpeed"] = 5.0; - configJson["colors"] = nlohmann::json::array({"red", "blue"}); - configJson["physics"]["gravity"] = 9.8; - configJson["physics"]["friction"] = 0.5; - auto config = std::make_unique("config", configJson); - - module->setConfiguration(*config, nullptr, nullptr); - moduleSystem->registerModule("ConfigurableModule", std::move(module)); - - std::cout << " Initial config:\n"; - std::cout << " Spawn rate: 10/s\n"; - std::cout << " Max entities: 150\n"; - std::cout << " Entity speed: 5.0\n"; - std::cout << " Colors: [red, blue]\n\n"; - - // === PHASE 0: Baseline (10s) === - std::cout << "=== Phase 0: Initial config (10s) ===\n"; - - for (int i = 0; i < 600; i++) { // 10s * 60 FPS - auto frameStart = std::chrono::high_resolution_clock::now(); - - moduleSystem->processModules(1.0f / 60.0f); - - auto frameEnd = std::chrono::high_resolution_clock::now(); - float frameTime = std::chrono::duration(frameEnd - frameStart).count(); - metrics.recordFPS(1000.0f / frameTime); - metrics.recordMemoryUsage(grove::getCurrentMemoryUsage()); - } - - auto state0 = moduleSystem->extractModule()->getState(); - auto* json0 = dynamic_cast(state0.get()); - ASSERT_TRUE(json0 != nullptr, "State should be JsonDataNode"); - - const auto& state0Data = json0->getJsonData(); - int entityCount0 = state0Data["entities"].size(); - - std::cout << "✓ Baseline: " << entityCount0 << " entities spawned (~100 expected)\n"; - ASSERT_WITHIN(entityCount0, 100, 20, "Should have ~100 entities after 10s"); - reporter.addAssertion("initial_spawn_rate", true); - - // Vérifier vitesse des entités initiales - for (const auto& entity : state0Data["entities"]) { - float speed = entity["speed"]; - ASSERT_EQ_FLOAT(speed, 5.0f, 0.01f, "Initial entity speed should be 5.0"); - } - - // Re-register module - auto module0 = loader.load(modulePath, "ConfigurableModule", false); - module0->setState(*state0); - moduleSystem->registerModule("ConfigurableModule", std::move(module0)); - - // === PHASE 1: Simple Config Change (10s) === - std::cout << "\n=== Phase 1: Doubling spawn rate and speed (10s) ===\n"; - - nlohmann::json newConfig1; - newConfig1["spawnRate"] = 20; // Double spawn rate - newConfig1["maxEntities"] = 150; // Keep same limit - newConfig1["entitySpeed"] = 10.0; // Double speed - newConfig1["colors"] = nlohmann::json::array({"red", "blue"}); - newConfig1["physics"]["gravity"] = 9.8; - newConfig1["physics"]["friction"] = 0.5; - auto newConfigNode1 = std::make_unique("config", newConfig1); - - auto updateStart1 = std::chrono::high_resolution_clock::now(); - - // Extract, update config, re-register - auto modulePhase1 = moduleSystem->extractModule(); - bool updateResult1 = modulePhase1->updateConfig(*newConfigNode1); - - auto updateEnd1 = std::chrono::high_resolution_clock::now(); - float updateTime1 = std::chrono::duration(updateEnd1 - updateStart1).count(); - - ASSERT_TRUE(updateResult1, "Config update should succeed"); - reporter.addAssertion("config_update_simple", updateResult1); - reporter.addMetric("config_update_time_ms", updateTime1); - - std::cout << " Config updated in " << updateTime1 << "ms\n"; - - moduleSystem->registerModule("ConfigurableModule", std::move(modulePhase1)); - - // Run 10s - for (int i = 0; i < 600; i++) { - moduleSystem->processModules(1.0f / 60.0f); - metrics.recordMemoryUsage(grove::getCurrentMemoryUsage()); - } - - auto state1 = moduleSystem->extractModule()->getState(); - auto* json1 = dynamic_cast(state1.get()); - const auto& state1Data = json1->getJsonData(); - int entityCount1 = state1Data["entities"].size(); - - // Should have reached limit (150) - std::cout << "✓ Phase 1: " << entityCount1 << " entities (max: 150)\n"; - ASSERT_LE(entityCount1, 150, "Should respect maxEntities limit"); - reporter.addAssertion("max_entities_respected", entityCount1 <= 150); - - // Vérifier que nouvelles entités ont speed = 10.0 - int newEntityCount = 0; - for (const auto& entity : state1Data["entities"]) { - if (entity["id"] >= entityCount0) { // Nouvelle entité - float speed = entity["speed"]; - ASSERT_EQ_FLOAT(speed, 10.0f, 0.01f, "New entities should have speed 10.0"); - newEntityCount++; - } - } - std::cout << " " << newEntityCount << " new entities with speed 10.0\n"; - - // Re-register - auto module1 = loader.load(modulePath, "ConfigurableModule", false); - module1->setState(*state1); - moduleSystem->registerModule("ConfigurableModule", std::move(module1)); - - // === PHASE 2: Complex Config Change (10s) === - std::cout << "\n=== Phase 2: Complex config changes (10s) ===\n"; - - nlohmann::json newConfig2; - newConfig2["spawnRate"] = 15; - newConfig2["maxEntities"] = 200; // Augmenter limite (was 150) - newConfig2["entitySpeed"] = 7.5; - newConfig2["colors"] = nlohmann::json::array({"green", "yellow", "purple"}); // Nouvelles couleurs - newConfig2["physics"]["gravity"] = 1.6; // Gravité lunaire - newConfig2["physics"]["friction"] = 0.2; - auto newConfigNode2 = std::make_unique("config", newConfig2); - - auto modulePhase2 = moduleSystem->extractModule(); - bool updateResult2 = modulePhase2->updateConfig(*newConfigNode2); - ASSERT_TRUE(updateResult2, "Config update 2 should succeed"); - - int entitiesBeforePhase2 = entityCount1; - std::cout << " Config updated: new colors [green, yellow, purple]\n"; - std::cout << " Max entities increased to 200\n"; - - moduleSystem->registerModule("ConfigurableModule", std::move(modulePhase2)); - - // Run 10s - for (int i = 0; i < 600; i++) { - moduleSystem->processModules(1.0f / 60.0f); - } - - auto state2 = moduleSystem->extractModule()->getState(); - auto* json2 = dynamic_cast(state2.get()); - const auto& state2Data = json2->getJsonData(); - int entityCount2 = state2Data["entities"].size(); - - std::cout << "✓ Phase 2: " << entityCount2 << " total entities\n"; - ASSERT_GT(entityCount2, entitiesBeforePhase2, "Entity count should have increased"); - ASSERT_LE(entityCount2, 200, "Should respect new maxEntities = 200"); - - // Vérifier couleurs des nouvelles entités - std::set newColors; - for (const auto& entity : state2Data["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 << " New colors found: "; - for (const auto& color : newColors) std::cout << color << " "; - std::cout << "\n"; - - // Re-register - auto module2 = loader.load(modulePath, "ConfigurableModule", false); - module2->setState(*state2); - moduleSystem->registerModule("ConfigurableModule", std::move(module2)); - - // === PHASE 3: Invalid Config + Rollback (5s) === - std::cout << "\n=== Phase 3: Invalid config rejection (5s) ===\n"; - - nlohmann::json invalidConfig; - invalidConfig["spawnRate"] = -5; // INVALIDE: négatif - invalidConfig["maxEntities"] = 1000000; // INVALIDE: trop grand - invalidConfig["entitySpeed"] = 5.0; - invalidConfig["colors"] = nlohmann::json::array({"red"}); - invalidConfig["physics"]["gravity"] = 9.8; - invalidConfig["physics"]["friction"] = 0.5; - auto invalidConfigNode = std::make_unique("config", invalidConfig); - - auto modulePhase3 = moduleSystem->extractModule(); - bool updateResult3 = modulePhase3->updateConfig(*invalidConfigNode); - - std::cout << " Invalid config rejected: " << (!updateResult3 ? "YES" : "NO") << "\n"; - ASSERT_FALSE(updateResult3, "Invalid config should be rejected"); - reporter.addAssertion("invalid_config_rejected", !updateResult3); - - moduleSystem->registerModule("ConfigurableModule", std::move(modulePhase3)); - - // Continuer - devrait utiliser la config précédente (valide) - for (int i = 0; i < 300; i++) { // 5s - moduleSystem->processModules(1.0f / 60.0f); - } - - auto state3 = moduleSystem->extractModule()->getState(); - auto* json3 = dynamic_cast(state3.get()); - const auto& state3Data = json3->getJsonData(); - int entityCount3 = state3Data["entities"].size(); - - std::cout << "✓ Rollback successful: " << (entityCount3 - entityCount2) << " entities spawned with previous config\n"; - // Note: We might already be at maxEntities (200), so we just verify no crash and config stayed valid - ASSERT_GE(entityCount3, entityCount2, "Entity count should not decrease"); - reporter.addAssertion("config_rollback_works", entityCount3 >= entityCount2); - - // Re-register - auto module3 = loader.load(modulePath, "ConfigurableModule", false); - module3->setState(*state3); - moduleSystem->registerModule("ConfigurableModule", std::move(module3)); - - // === PHASE 4: Partial Config Update (5s) === - std::cout << "\n=== Phase 4: Partial config update (5s) ===\n"; - - nlohmann::json partialConfig; - partialConfig["entitySpeed"] = 2.0; // Modifier seulement la vitesse - auto partialConfigNode = std::make_unique("config", partialConfig); - - auto modulePhase4 = moduleSystem->extractModule(); - bool updateResult4 = modulePhase4->updateConfigPartial(*partialConfigNode); - - std::cout << " Partial update (entitySpeed only): " << (updateResult4 ? "SUCCESS" : "FAILED") << "\n"; - ASSERT_TRUE(updateResult4, "Partial config update should succeed"); - - moduleSystem->registerModule("ConfigurableModule", std::move(modulePhase4)); - - // Run 5s - for (int i = 0; i < 300; i++) { - moduleSystem->processModules(1.0f / 60.0f); - } - - auto state4 = moduleSystem->extractModule()->getState(); - auto* json4 = dynamic_cast(state4.get()); - const auto& state4Data = json4->getJsonData(); - - // Vérifier que nouvelles entités ont speed = 2.0 - // Et que colors sont toujours ceux de Phase 2 - // Note: We might be at maxEntities, so check if any new entities were spawned - bool foundNewSpeed = false; - bool foundOldColors = false; - int newEntitiesPhase4 = 0; - - for (const auto& entity : state4Data["entities"]) { - if (entity["id"] >= entityCount3) { - newEntitiesPhase4++; - float speed = entity["speed"]; - if (std::abs(speed - 2.0f) < 0.01f) foundNewSpeed = true; - - std::string color = entity["color"]; - if (color == "green" || color == "yellow" || color == "purple") { - foundOldColors = true; - } - } - } - - std::cout << "✓ Partial update: speed changed to 2.0, other params preserved\n"; - std::cout << " New entities in Phase 4: " << newEntitiesPhase4 << " (may be 0 if at maxEntities)\n"; - - // If we spawned new entities, verify they have the new speed - // Otherwise, just verify the partial update succeeded (which it did above) - if (newEntitiesPhase4 > 0) { - 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); - } else { - // At maxEntities, just verify no crash and config updated - std::cout << " (At maxEntities, cannot verify new entity speed)\n"; - reporter.addAssertion("partial_update_works", true); - } - - // === VÉRIFICATIONS FINALES === - std::cout << "\n================================================================================\n"; - std::cout << "FINAL VERIFICATION\n"; - std::cout << "================================================================================\n"; - - // Memory stability - size_t memGrowth = metrics.getMemoryGrowth(); - float memGrowthMB = memGrowth / (1024.0f * 1024.0f); - - std::cout << "Memory growth: " << memGrowthMB << " MB (threshold: < 10 MB)\n"; - ASSERT_LT(memGrowth, 10 * 1024 * 1024, "Memory growth should be < 10MB"); - reporter.addMetric("memory_growth_mb", memGrowthMB); - - // No crashes - reporter.addAssertion("no_crashes", true); - - std::cout << "\n"; - - // === RAPPORT FINAL === - metrics.printReport(); - reporter.printFinalReport(); - - return reporter.getExitCode(); -} +#include "grove/ModuleLoader.h" +#include "grove/SequentialModuleSystem.h" +#include "grove/JsonDataNode.h" +#include "../helpers/TestMetrics.h" +#include "../helpers/TestAssertions.h" +#include "../helpers/TestReporter.h" +#include "../helpers/SystemUtils.h" +#include +#include +#include +#include + +using namespace grove; + +/** + * Test 08: Config Hot-Reload + * + * Objectif: Valider que le système peut modifier la configuration d'un module + * à la volée sans redémarrage, avec validation et rollback. + * + * Scénario: + * Phase 0: Baseline avec config initiale (10s) + * Phase 1: Doubler spawn rate et speed (10s) + * Phase 2: Changements complexes (couleurs, physique, limites) (10s) + * Phase 3: Config invalide + rollback (5s) + * Phase 4: Partial config update (5s) + * + * Métriques: + * - Config update time + * - Config validation + * - Rollback functionality + * - Partial merge accuracy + */ + +int main() { + TestReporter reporter("Config Hot-Reload"); + TestMetrics metrics; + + std::cout << "================================================================================\n"; + std::cout << "TEST: Config Hot-Reload\n"; + std::cout << "================================================================================\n\n"; + + // === SETUP === + std::cout << "Setup: Loading ConfigurableModule with initial config...\n"; + + ModuleLoader loader; + auto moduleSystem = std::make_unique(); + + // Charger module + std::string modulePath = "./libConfigurableModule.so"; +#ifdef _WIN32 + modulePath = "./libConfigurableModule.dll"; +#endif + auto module = loader.load(modulePath, "ConfigurableModule", false); + + // Config initiale + nlohmann::json configJson; + configJson["spawnRate"] = 10; + configJson["maxEntities"] = 150; // Higher limit for Phase 0 + configJson["entitySpeed"] = 5.0; + configJson["colors"] = nlohmann::json::array({"red", "blue"}); + configJson["physics"]["gravity"] = 9.8; + configJson["physics"]["friction"] = 0.5; + auto config = std::make_unique("config", configJson); + + module->setConfiguration(*config, nullptr, nullptr); + moduleSystem->registerModule("ConfigurableModule", std::move(module)); + + std::cout << " Initial config:\n"; + std::cout << " Spawn rate: 10/s\n"; + std::cout << " Max entities: 150\n"; + std::cout << " Entity speed: 5.0\n"; + std::cout << " Colors: [red, blue]\n\n"; + + // === PHASE 0: Baseline (10s) === + std::cout << "=== Phase 0: Initial config (10s) ===\n"; + + for (int i = 0; i < 600; i++) { // 10s * 60 FPS + auto frameStart = std::chrono::high_resolution_clock::now(); + + moduleSystem->processModules(1.0f / 60.0f); + + auto frameEnd = std::chrono::high_resolution_clock::now(); + float frameTime = std::chrono::duration(frameEnd - frameStart).count(); + metrics.recordFPS(1000.0f / frameTime); + metrics.recordMemoryUsage(grove::getCurrentMemoryUsage()); + } + + auto state0 = moduleSystem->extractModule()->getState(); + auto* json0 = dynamic_cast(state0.get()); + ASSERT_TRUE(json0 != nullptr, "State should be JsonDataNode"); + + const auto& state0Data = json0->getJsonData(); + int entityCount0 = state0Data["entities"].size(); + + std::cout << "✓ Baseline: " << entityCount0 << " entities spawned (~100 expected)\n"; + ASSERT_WITHIN(entityCount0, 100, 20, "Should have ~100 entities after 10s"); + reporter.addAssertion("initial_spawn_rate", true); + + // Vérifier vitesse des entités initiales + for (const auto& entity : state0Data["entities"]) { + float speed = entity["speed"]; + ASSERT_EQ_FLOAT(speed, 5.0f, 0.01f, "Initial entity speed should be 5.0"); + } + + // Re-register module + auto module0 = loader.load(modulePath, "ConfigurableModule", false); + module0->setState(*state0); + moduleSystem->registerModule("ConfigurableModule", std::move(module0)); + + // === PHASE 1: Simple Config Change (10s) === + std::cout << "\n=== Phase 1: Doubling spawn rate and speed (10s) ===\n"; + + nlohmann::json newConfig1; + newConfig1["spawnRate"] = 20; // Double spawn rate + newConfig1["maxEntities"] = 150; // Keep same limit + newConfig1["entitySpeed"] = 10.0; // Double speed + newConfig1["colors"] = nlohmann::json::array({"red", "blue"}); + newConfig1["physics"]["gravity"] = 9.8; + newConfig1["physics"]["friction"] = 0.5; + auto newConfigNode1 = std::make_unique("config", newConfig1); + + auto updateStart1 = std::chrono::high_resolution_clock::now(); + + // Extract, update config, re-register + auto modulePhase1 = moduleSystem->extractModule(); + bool updateResult1 = modulePhase1->updateConfig(*newConfigNode1); + + auto updateEnd1 = std::chrono::high_resolution_clock::now(); + float updateTime1 = std::chrono::duration(updateEnd1 - updateStart1).count(); + + ASSERT_TRUE(updateResult1, "Config update should succeed"); + reporter.addAssertion("config_update_simple", updateResult1); + reporter.addMetric("config_update_time_ms", updateTime1); + + std::cout << " Config updated in " << updateTime1 << "ms\n"; + + moduleSystem->registerModule("ConfigurableModule", std::move(modulePhase1)); + + // Run 10s + for (int i = 0; i < 600; i++) { + moduleSystem->processModules(1.0f / 60.0f); + metrics.recordMemoryUsage(grove::getCurrentMemoryUsage()); + } + + auto state1 = moduleSystem->extractModule()->getState(); + auto* json1 = dynamic_cast(state1.get()); + const auto& state1Data = json1->getJsonData(); + int entityCount1 = state1Data["entities"].size(); + + // Should have reached limit (150) + std::cout << "✓ Phase 1: " << entityCount1 << " entities (max: 150)\n"; + ASSERT_LE(entityCount1, 150, "Should respect maxEntities limit"); + reporter.addAssertion("max_entities_respected", entityCount1 <= 150); + + // Vérifier que nouvelles entités ont speed = 10.0 + int newEntityCount = 0; + for (const auto& entity : state1Data["entities"]) { + if (entity["id"] >= entityCount0) { // Nouvelle entité + float speed = entity["speed"]; + ASSERT_EQ_FLOAT(speed, 10.0f, 0.01f, "New entities should have speed 10.0"); + newEntityCount++; + } + } + std::cout << " " << newEntityCount << " new entities with speed 10.0\n"; + + // Re-register + auto module1 = loader.load(modulePath, "ConfigurableModule", false); + module1->setState(*state1); + moduleSystem->registerModule("ConfigurableModule", std::move(module1)); + + // === PHASE 2: Complex Config Change (10s) === + std::cout << "\n=== Phase 2: Complex config changes (10s) ===\n"; + + nlohmann::json newConfig2; + newConfig2["spawnRate"] = 15; + newConfig2["maxEntities"] = 200; // Augmenter limite (was 150) + newConfig2["entitySpeed"] = 7.5; + newConfig2["colors"] = nlohmann::json::array({"green", "yellow", "purple"}); // Nouvelles couleurs + newConfig2["physics"]["gravity"] = 1.6; // Gravité lunaire + newConfig2["physics"]["friction"] = 0.2; + auto newConfigNode2 = std::make_unique("config", newConfig2); + + auto modulePhase2 = moduleSystem->extractModule(); + bool updateResult2 = modulePhase2->updateConfig(*newConfigNode2); + ASSERT_TRUE(updateResult2, "Config update 2 should succeed"); + + int entitiesBeforePhase2 = entityCount1; + std::cout << " Config updated: new colors [green, yellow, purple]\n"; + std::cout << " Max entities increased to 200\n"; + + moduleSystem->registerModule("ConfigurableModule", std::move(modulePhase2)); + + // Run 10s + for (int i = 0; i < 600; i++) { + moduleSystem->processModules(1.0f / 60.0f); + } + + auto state2 = moduleSystem->extractModule()->getState(); + auto* json2 = dynamic_cast(state2.get()); + const auto& state2Data = json2->getJsonData(); + int entityCount2 = state2Data["entities"].size(); + + std::cout << "✓ Phase 2: " << entityCount2 << " total entities\n"; + ASSERT_GT(entityCount2, entitiesBeforePhase2, "Entity count should have increased"); + ASSERT_LE(entityCount2, 200, "Should respect new maxEntities = 200"); + + // Vérifier couleurs des nouvelles entités + std::set newColors; + for (const auto& entity : state2Data["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 << " New colors found: "; + for (const auto& color : newColors) std::cout << color << " "; + std::cout << "\n"; + + // Re-register + auto module2 = loader.load(modulePath, "ConfigurableModule", false); + module2->setState(*state2); + moduleSystem->registerModule("ConfigurableModule", std::move(module2)); + + // === PHASE 3: Invalid Config + Rollback (5s) === + std::cout << "\n=== Phase 3: Invalid config rejection (5s) ===\n"; + + nlohmann::json invalidConfig; + invalidConfig["spawnRate"] = -5; // INVALIDE: négatif + invalidConfig["maxEntities"] = 1000000; // INVALIDE: trop grand + invalidConfig["entitySpeed"] = 5.0; + invalidConfig["colors"] = nlohmann::json::array({"red"}); + invalidConfig["physics"]["gravity"] = 9.8; + invalidConfig["physics"]["friction"] = 0.5; + auto invalidConfigNode = std::make_unique("config", invalidConfig); + + auto modulePhase3 = moduleSystem->extractModule(); + bool updateResult3 = modulePhase3->updateConfig(*invalidConfigNode); + + std::cout << " Invalid config rejected: " << (!updateResult3 ? "YES" : "NO") << "\n"; + ASSERT_FALSE(updateResult3, "Invalid config should be rejected"); + reporter.addAssertion("invalid_config_rejected", !updateResult3); + + moduleSystem->registerModule("ConfigurableModule", std::move(modulePhase3)); + + // Continuer - devrait utiliser la config précédente (valide) + for (int i = 0; i < 300; i++) { // 5s + moduleSystem->processModules(1.0f / 60.0f); + } + + auto state3 = moduleSystem->extractModule()->getState(); + auto* json3 = dynamic_cast(state3.get()); + const auto& state3Data = json3->getJsonData(); + int entityCount3 = state3Data["entities"].size(); + + std::cout << "✓ Rollback successful: " << (entityCount3 - entityCount2) << " entities spawned with previous config\n"; + // Note: We might already be at maxEntities (200), so we just verify no crash and config stayed valid + ASSERT_GE(entityCount3, entityCount2, "Entity count should not decrease"); + reporter.addAssertion("config_rollback_works", entityCount3 >= entityCount2); + + // Re-register + auto module3 = loader.load(modulePath, "ConfigurableModule", false); + module3->setState(*state3); + moduleSystem->registerModule("ConfigurableModule", std::move(module3)); + + // === PHASE 4: Partial Config Update (5s) === + std::cout << "\n=== Phase 4: Partial config update (5s) ===\n"; + + nlohmann::json partialConfig; + partialConfig["entitySpeed"] = 2.0; // Modifier seulement la vitesse + auto partialConfigNode = std::make_unique("config", partialConfig); + + auto modulePhase4 = moduleSystem->extractModule(); + bool updateResult4 = modulePhase4->updateConfigPartial(*partialConfigNode); + + std::cout << " Partial update (entitySpeed only): " << (updateResult4 ? "SUCCESS" : "FAILED") << "\n"; + ASSERT_TRUE(updateResult4, "Partial config update should succeed"); + + moduleSystem->registerModule("ConfigurableModule", std::move(modulePhase4)); + + // Run 5s + for (int i = 0; i < 300; i++) { + moduleSystem->processModules(1.0f / 60.0f); + } + + auto state4 = moduleSystem->extractModule()->getState(); + auto* json4 = dynamic_cast(state4.get()); + const auto& state4Data = json4->getJsonData(); + + // Vérifier que nouvelles entités ont speed = 2.0 + // Et que colors sont toujours ceux de Phase 2 + // Note: We might be at maxEntities, so check if any new entities were spawned + bool foundNewSpeed = false; + bool foundOldColors = false; + int newEntitiesPhase4 = 0; + + for (const auto& entity : state4Data["entities"]) { + if (entity["id"] >= entityCount3) { + newEntitiesPhase4++; + float speed = entity["speed"]; + if (std::abs(speed - 2.0f) < 0.01f) foundNewSpeed = true; + + std::string color = entity["color"]; + if (color == "green" || color == "yellow" || color == "purple") { + foundOldColors = true; + } + } + } + + std::cout << "✓ Partial update: speed changed to 2.0, other params preserved\n"; + std::cout << " New entities in Phase 4: " << newEntitiesPhase4 << " (may be 0 if at maxEntities)\n"; + + // If we spawned new entities, verify they have the new speed + // Otherwise, just verify the partial update succeeded (which it did above) + if (newEntitiesPhase4 > 0) { + 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); + } else { + // At maxEntities, just verify no crash and config updated + std::cout << " (At maxEntities, cannot verify new entity speed)\n"; + reporter.addAssertion("partial_update_works", true); + } + + // === VÉRIFICATIONS FINALES === + std::cout << "\n================================================================================\n"; + std::cout << "FINAL VERIFICATION\n"; + std::cout << "================================================================================\n"; + + // Memory stability + size_t memGrowth = metrics.getMemoryGrowth(); + float memGrowthMB = memGrowth / (1024.0f * 1024.0f); + + std::cout << "Memory growth: " << memGrowthMB << " MB (threshold: < 10 MB)\n"; + ASSERT_LT(memGrowth, 10 * 1024 * 1024, "Memory growth should be < 10MB"); + reporter.addMetric("memory_growth_mb", memGrowthMB); + + // No crashes + reporter.addAssertion("no_crashes", true); + + std::cout << "\n"; + + // === RAPPORT FINAL === + metrics.printReport(); + reporter.printFinalReport(); + + return reporter.getExitCode(); +} diff --git a/tests/integration/test_09_module_dependencies.cpp b/tests/integration/test_09_module_dependencies.cpp index 29682cb..33c7811 100644 --- a/tests/integration/test_09_module_dependencies.cpp +++ b/tests/integration/test_09_module_dependencies.cpp @@ -1,557 +1,557 @@ -/** - * Scenario 9: Module Dependencies Test - * - * Tests module dependency system with cascade reload, isolation, and cycle detection. - * - * Phases: - * - Setup: Load BaseModule, DependentModule (depends on Base), IndependentModule (isolated) - * - Phase 1: Cascade reload (BaseModule → DependentModule) - * - Phase 2: Unload protection (cannot unload BaseModule while DependentModule active) - * - Phase 3: Reload dependent only (no reverse cascade) - * - Phase 4: Cycle detection - * - Phase 5: Cascade unload - */ - -#include "grove/IModule.h" -#include "grove/JsonDataNode.h" -#include "../helpers/TestMetrics.h" -#include "../helpers/TestAssertions.h" -#include "../helpers/TestReporter.h" - -#ifdef _WIN32 -#include -#else -#include -#endif -#include -#include -#include -#include -#include -#include -#include -#include - -// Cross-platform dlopen wrappers -#ifdef _WIN32 -inline void* grove_dlopen(const char* path, int flags) { - (void)flags; - return LoadLibraryA(path); -} -inline void* grove_dlsym(void* handle, const char* symbol) { - return (void*)GetProcAddress((HMODULE)handle, symbol); -} -inline int grove_dlclose(void* handle) { - return FreeLibrary((HMODULE)handle) ? 0 : -1; -} -inline const char* grove_dlerror() { - static thread_local char buf[256]; - DWORD err = GetLastError(); - snprintf(buf, sizeof(buf), "Windows error code: %lu", err); - return buf; -} -#define RTLD_NOW 0 -#define RTLD_LOCAL 0 -#else -#define grove_dlopen dlopen -#define grove_dlsym dlsym -#define grove_dlclose dlclose -#define grove_dlerror dlerror -#endif - -using namespace grove; -using json = nlohmann::json; - -// Simple module handle with dependency tracking -struct ModuleHandle { - void* dlHandle = nullptr; - IModule* instance = nullptr; - std::string modulePath; - int version = 1; - std::vector dependencies; - - bool isLoaded() const { return dlHandle != nullptr && instance != nullptr; } -}; - -// Simplified module system for testing dependencies -class DependencyTestEngine { -public: - DependencyTestEngine() { - logger_ = spdlog::default_logger(); - logger_->set_level(spdlog::level::info); - } - - ~DependencyTestEngine() { - // Collect names first to avoid iterator invalidation during unload - std::vector names; - names.reserve(modules_.size()); - for (const auto& [name, _] : modules_) { - names.push_back(name); - } - // Unload in reverse order (dependents before dependencies) - for (auto it = names.rbegin(); it != names.rend(); ++it) { - unloadModule(*it); - } - } - - bool loadModule(const std::string& name, const std::string& path) { - if (modules_.count(name) > 0) { - logger_->warn("Module {} already loaded", name); - return false; - } - - void* dlHandle = grove_dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); - if (!dlHandle) { - logger_->error("Failed to load module {}: {}", name, grove_dlerror()); - return false; - } - - auto createFunc = (IModule* (*)())grove_dlsym(dlHandle, "createModule"); - if (!createFunc) { - logger_->error("Failed to find createModule in {}: {}", name, grove_dlerror()); - grove_dlclose(dlHandle); - return false; - } - - IModule* instance = createFunc(); - if (!instance) { - logger_->error("createModule returned nullptr for {}", name); - grove_dlclose(dlHandle); - return false; - } - - ModuleHandle handle; - handle.dlHandle = dlHandle; - handle.instance = instance; - handle.modulePath = path; - handle.version = instance->getVersion(); - handle.dependencies = instance->getDependencies(); - - modules_[name] = handle; - - // Initialize module - auto config = std::make_unique("config", nlohmann::json::object()); - instance->setConfiguration(*config, nullptr, nullptr); - - logger_->info("Loaded {} v{} with {} dependencies", - name, handle.version, handle.dependencies.size()); - - return true; - } - - bool reloadModule(const std::string& name, bool cascadeDependents = true) { - auto it = modules_.find(name); - if (it == modules_.end()) { - logger_->error("Module {} not found for reload", name); - return false; - } - - auto startTime = std::chrono::high_resolution_clock::now(); - - // Find dependents if cascade is enabled - std::vector dependents; - if (cascadeDependents) { - dependents = findDependents(name); - } - - logger_->info("Reloading {} (cascade: {} dependents)", name, dependents.size()); - - // Save states of all affected modules - std::map> savedStates; - savedStates[name] = it->second.instance->getState(); - for (const auto& dep : dependents) { - savedStates[dep] = modules_[dep].instance->getState(); - } - - // Reload the target module - if (!reloadModuleSingle(name)) { - logger_->error("Failed to reload {}", name); - return false; - } - - // Cascade reload dependents - for (const auto& dep : dependents) { - logger_->info(" → Cascade reloading dependent: {}", dep); - if (!reloadModuleSingle(dep)) { - logger_->error("Failed to cascade reload {}", dep); - return false; - } - } - - // Restore states - it->second.instance->setState(*savedStates[name]); - for (const auto& dep : dependents) { - modules_[dep].instance->setState(*savedStates[dep]); - } - - auto endTime = std::chrono::high_resolution_clock::now(); - auto duration = std::chrono::duration_cast(endTime - startTime); - logger_->info("Cascade reload completed in {}ms", duration.count()); - - return true; - } - - bool canUnloadModule(const std::string& name, std::string& errorMsg) { - auto dependents = findDependents(name); - if (!dependents.empty()) { - errorMsg = "Cannot unload " + name + ": required by "; - for (size_t i = 0; i < dependents.size(); i++) { - if (i > 0) errorMsg += ", "; - errorMsg += dependents[i]; - } - return false; - } - return true; - } - - bool unloadModule(const std::string& name) { - auto it = modules_.find(name); - if (it == modules_.end()) { - return false; - } - - // Check if any other modules depend on this one - std::string errorMsg; - if (!canUnloadModule(name, errorMsg)) { - logger_->error("{}", errorMsg); - return false; - } - - auto& handle = it->second; - handle.instance->shutdown(); - - auto destroyFunc = (void (*)(IModule*))grove_dlsym(handle.dlHandle, "destroyModule"); - if (destroyFunc) { - destroyFunc(handle.instance); - } else { - delete handle.instance; - } - - grove_dlclose(handle.dlHandle); - - modules_.erase(it); - logger_->info("Unloaded {}", name); - - return true; - } - - std::vector findDependents(const std::string& moduleName) { - std::vector dependents; - for (const auto& [name, handle] : modules_) { - for (const auto& dep : handle.dependencies) { - if (dep == moduleName) { - dependents.push_back(name); - break; - } - } - } - return dependents; - } - - bool hasCircularDependencies(const std::string& moduleName, - std::set& visited, - std::set& recursionStack) { - visited.insert(moduleName); - recursionStack.insert(moduleName); - - auto it = modules_.find(moduleName); - if (it != modules_.end()) { - for (const auto& dep : it->second.dependencies) { - if (recursionStack.count(dep) > 0) { - // Cycle detected - return true; - } - if (visited.count(dep) == 0) { - if (hasCircularDependencies(dep, visited, recursionStack)) { - return true; - } - } - } - } - - recursionStack.erase(moduleName); - return false; - } - - IModule* getModule(const std::string& name) { - auto it = modules_.find(name); - return (it != modules_.end()) ? it->second.instance : nullptr; - } - - int getModuleVersion(const std::string& name) { - auto it = modules_.find(name); - return (it != modules_.end()) ? it->second.instance->getVersion() : 0; - } - - void process(float deltaTime) { - auto input = std::make_unique("input", nlohmann::json::object()); - for (auto& [name, handle] : modules_) { - if (handle.instance) { - handle.instance->process(*input); - } - } - } - - void injectDependency(const std::string& dependentName, const std::string& baseName) { - // For this test, we don't actually inject dependencies at the C++ level - // The modules are designed to work independently - // This is just a placeholder for demonstration - logger_->info("Dependency declared: {} depends on {}", dependentName, baseName); - } - -private: - bool reloadModuleSingle(const std::string& name) { - auto it = modules_.find(name); - if (it == modules_.end()) { - return false; - } - - auto& handle = it->second; - std::string path = handle.modulePath; - - // Destroy old instance - handle.instance->shutdown(); - auto destroyFunc = (void (*)(IModule*))grove_dlsym(handle.dlHandle, "destroyModule"); - if (destroyFunc) { - destroyFunc(handle.instance); - } else { - delete handle.instance; - } - grove_dlclose(handle.dlHandle); - - // Reload shared library - void* newHandle = grove_dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); - if (!newHandle) { - logger_->error("Failed to reload {}: {}", name, grove_dlerror()); - return false; - } - - auto createFunc = (IModule* (*)())grove_dlsym(newHandle, "createModule"); - if (!createFunc) { - logger_->error("Failed to find createModule in reloaded {}", name); - grove_dlclose(newHandle); - return false; - } - - IModule* newInstance = createFunc(); - if (!newInstance) { - logger_->error("createModule returned nullptr for reloaded {}", name); - grove_dlclose(newHandle); - return false; - } - - handle.dlHandle = newHandle; - handle.instance = newInstance; - handle.version = newInstance->getVersion(); - - // Re-initialize - auto config = std::make_unique("config", nlohmann::json::object()); - newInstance->setConfiguration(*config, nullptr, nullptr); - - return true; - } - - std::map modules_; - std::shared_ptr logger_; -}; - -int main() { - TestReporter reporter("Module Dependencies"); - TestMetrics metrics; - - std::cout << "================================================================================\n"; - std::cout << "TEST: Module Dependencies\n"; - std::cout << "================================================================================\n\n"; - - DependencyTestEngine engine; - - // === SETUP: Load modules with dependencies === - std::cout << "=== Setup: Load modules with dependencies ===\n"; - - ASSERT_TRUE(engine.loadModule("BaseModule", "./libBaseModule.so"), - "Should load BaseModule"); - ASSERT_TRUE(engine.loadModule("DependentModule", "./libDependentModule.so"), - "Should load DependentModule"); - ASSERT_TRUE(engine.loadModule("IndependentModule", "./libIndependentModule.so"), - "Should load IndependentModule"); - - reporter.addAssertion("modules_loaded", true); - - // Inject dependency - engine.injectDependency("DependentModule", "BaseModule"); - - // Verify dependencies - auto baseModule = engine.getModule("BaseModule"); - auto depModule = engine.getModule("DependentModule"); - auto indModule = engine.getModule("IndependentModule"); - - ASSERT_TRUE(baseModule != nullptr, "BaseModule should be loaded"); - ASSERT_TRUE(depModule != nullptr, "DependentModule should be loaded"); - ASSERT_TRUE(indModule != nullptr, "IndependentModule should be loaded"); - - auto baseDeps = baseModule->getDependencies(); - auto depDeps = depModule->getDependencies(); - auto indDeps = indModule->getDependencies(); - - ASSERT_TRUE(baseDeps.empty(), "BaseModule should have no dependencies"); - ASSERT_EQ(depDeps.size(), 1, "DependentModule should have 1 dependency"); - ASSERT_EQ(depDeps[0], "BaseModule", "DependentModule should depend on BaseModule"); - ASSERT_TRUE(indDeps.empty(), "IndependentModule should have no dependencies"); - - int baseV1 = engine.getModuleVersion("BaseModule"); - int depV1 = engine.getModuleVersion("DependentModule"); - int indV1 = engine.getModuleVersion("IndependentModule"); - - std::cout << "✓ BaseModule loaded (v" << baseV1 << ", no dependencies)\n"; - std::cout << "✓ DependentModule loaded (v" << depV1 << ", depends on: BaseModule)\n"; - std::cout << "✓ IndependentModule loaded (v" << indV1 << ", no dependencies)\n\n"; - - std::cout << "Dependency graph:\n"; - std::cout << " IndependentModule → (none)\n"; - std::cout << " BaseModule → (none)\n"; - std::cout << " DependentModule → BaseModule\n\n"; - - // Run for a bit - for (int i = 0; i < 60; i++) { - engine.process(1.0f / 60.0f); - } - - // === PHASE 1: Cascade Reload (30s) === - std::cout << "=== Phase 1: Cascade Reload (30s) ===\n"; - - // Verify BaseModule is loaded (we can't access generateNumber() directly in this test) - ASSERT_TRUE(engine.getModule("BaseModule") != nullptr, "BaseModule should be loaded"); - - std::cout << "Reloading BaseModule...\n"; - auto cascadeStart = std::chrono::high_resolution_clock::now(); - - // In a real system, this would reload the .so file with new code - // For this test, we simulate by reloading the same module - ASSERT_TRUE(engine.reloadModule("BaseModule", true), "BaseModule reload should succeed"); - - auto cascadeEnd = std::chrono::high_resolution_clock::now(); - auto cascadeTime = std::chrono::duration_cast(cascadeEnd - cascadeStart).count(); - - std::cout << " → BaseModule reload triggered\n"; - std::cout << " → Cascade reload triggered for DependentModule\n"; - - // Re-inject dependency after reload - engine.injectDependency("DependentModule", "BaseModule"); - - int baseV2 = engine.getModuleVersion("BaseModule"); - int depV2 = engine.getModuleVersion("DependentModule"); - int indV2 = engine.getModuleVersion("IndependentModule"); - - std::cout << "✓ BaseModule reloaded: v" << baseV1 << " → v" << baseV2 << "\n"; - std::cout << "✓ DependentModule cascade reloaded: v" << depV1 << " → v" << depV2 << "\n"; - std::cout << "✓ IndependentModule NOT reloaded (v" << indV2 << " unchanged)\n"; - - reporter.addAssertion("cascade_reload_triggered", true); - reporter.addAssertion("independent_isolated", indV2 == indV1); - reporter.addMetric("cascade_reload_time_ms", cascadeTime); - - std::cout << "\nMetrics:\n"; - std::cout << " Cascade reload time: " << cascadeTime << "ms "; - std::cout << (cascadeTime < 200 ? "✓" : "✗") << "\n\n"; - - // Run for 30 seconds - for (int i = 0; i < 1800; i++) { - engine.process(1.0f / 60.0f); - } - - // === PHASE 2: Unload Protection (10s) === - std::cout << "=== Phase 2: Unload Protection (10s) ===\n"; - - std::cout << "Attempting to unload BaseModule...\n"; - std::string errorMsg; - bool canUnload = engine.canUnloadModule("BaseModule", errorMsg); - - ASSERT_FALSE(canUnload, "BaseModule should not be unloadable while DependentModule active"); - reporter.addAssertion("unload_protection_works", !canUnload); - - std::cout << " ✗ Unload rejected: " << errorMsg << "\n"; - std::cout << "✓ BaseModule still loaded and functional\n"; - std::cout << "✓ All modules stable\n\n"; - - // Run for 10 seconds - for (int i = 0; i < 600; i++) { - engine.process(1.0f / 60.0f); - } - - // === PHASE 3: Reload Dependent Only (20s) === - std::cout << "=== Phase 3: Reload Dependent Only (20s) ===\n"; - - std::cout << "Reloading DependentModule...\n"; - int baseV3Before = engine.getModuleVersion("BaseModule"); - - ASSERT_TRUE(engine.reloadModule("DependentModule", false), "DependentModule reload should succeed"); - - // Re-inject dependency after reload - engine.injectDependency("DependentModule", "BaseModule"); - - int baseV3After = engine.getModuleVersion("BaseModule"); - int depV3 = engine.getModuleVersion("DependentModule"); - - std::cout << "✓ DependentModule reloaded: v" << depV2 << " → v" << depV3 << "\n"; - std::cout << "✓ BaseModule NOT reloaded (v" << baseV3After << " unchanged)\n"; - std::cout << "✓ IndependentModule still isolated\n"; - std::cout << "✓ DependentModule still connected to BaseModule\n\n"; - - ASSERT_EQ(baseV3Before, baseV3After, "BaseModule should not reload when dependent reloads"); - reporter.addAssertion("no_reverse_cascade", baseV3Before == baseV3After); - - // Run for 20 seconds - for (int i = 0; i < 1200; i++) { - engine.process(1.0f / 60.0f); - } - - // === PHASE 4: Cyclic Dependency Detection (20s) === - std::cout << "=== Phase 4: Cyclic Dependency Detection (20s) ===\n"; - - // For this phase, we would need to create cyclic modules, which is complex - // Instead, we'll verify the cycle detection algorithm works - std::cout << "Simulating cyclic dependency check...\n"; - - std::set visited, recursionStack; - bool hasCycle = engine.hasCircularDependencies("BaseModule", visited, recursionStack); - ASSERT_FALSE(hasCycle, "Current module graph should not have cycles"); - - std::cout << "✓ No cycles detected in current module graph\n"; - std::cout << " (In a real scenario, cyclic modules would be rejected at load time)\n\n"; - - reporter.addAssertion("cycle_detected", true); - - // Run for 20 seconds - for (int i = 0; i < 1200; i++) { - engine.process(1.0f / 60.0f); - } - - // === PHASE 5: Cascade Unload (20s) === - std::cout << "=== Phase 5: Cascade Unload (20s) ===\n"; - - std::cout << "Unloading DependentModule...\n"; - ASSERT_TRUE(engine.unloadModule("DependentModule"), "DependentModule should unload"); - std::cout << "✓ DependentModule unloaded (dependency released)\n"; - - baseModule = engine.getModule("BaseModule"); - ASSERT_TRUE(baseModule != nullptr, "BaseModule should still be loaded"); - std::cout << "✓ BaseModule still loaded\n\n"; - - std::cout << "Attempting to unload BaseModule...\n"; - ASSERT_TRUE(engine.unloadModule("BaseModule"), "BaseModule should now unload"); - std::cout << "✓ BaseModule unload succeeded (no dependents)\n\n"; - - std::cout << "Final state:\n"; - std::cout << " IndependentModule: loaded (v" << engine.getModuleVersion("IndependentModule") << ")\n"; - std::cout << " BaseModule: unloaded\n"; - std::cout << " DependentModule: unloaded\n\n"; - - reporter.addAssertion("cascade_unload_works", true); - reporter.addAssertion("state_preserved", true); - reporter.addAssertion("no_crashes", true); - - // === FINAL REPORT === - reporter.printFinalReport(); - - return reporter.getExitCode(); -} +/** + * Scenario 9: Module Dependencies Test + * + * Tests module dependency system with cascade reload, isolation, and cycle detection. + * + * Phases: + * - Setup: Load BaseModule, DependentModule (depends on Base), IndependentModule (isolated) + * - Phase 1: Cascade reload (BaseModule → DependentModule) + * - Phase 2: Unload protection (cannot unload BaseModule while DependentModule active) + * - Phase 3: Reload dependent only (no reverse cascade) + * - Phase 4: Cycle detection + * - Phase 5: Cascade unload + */ + +#include "grove/IModule.h" +#include "grove/JsonDataNode.h" +#include "../helpers/TestMetrics.h" +#include "../helpers/TestAssertions.h" +#include "../helpers/TestReporter.h" + +#ifdef _WIN32 +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include + +// Cross-platform dlopen wrappers +#ifdef _WIN32 +inline void* grove_dlopen(const char* path, int flags) { + (void)flags; + return LoadLibraryA(path); +} +inline void* grove_dlsym(void* handle, const char* symbol) { + return (void*)GetProcAddress((HMODULE)handle, symbol); +} +inline int grove_dlclose(void* handle) { + return FreeLibrary((HMODULE)handle) ? 0 : -1; +} +inline const char* grove_dlerror() { + static thread_local char buf[256]; + DWORD err = GetLastError(); + snprintf(buf, sizeof(buf), "Windows error code: %lu", err); + return buf; +} +#define RTLD_NOW 0 +#define RTLD_LOCAL 0 +#else +#define grove_dlopen dlopen +#define grove_dlsym dlsym +#define grove_dlclose dlclose +#define grove_dlerror dlerror +#endif + +using namespace grove; +using json = nlohmann::json; + +// Simple module handle with dependency tracking +struct ModuleHandle { + void* dlHandle = nullptr; + IModule* instance = nullptr; + std::string modulePath; + int version = 1; + std::vector dependencies; + + bool isLoaded() const { return dlHandle != nullptr && instance != nullptr; } +}; + +// Simplified module system for testing dependencies +class DependencyTestEngine { +public: + DependencyTestEngine() { + logger_ = spdlog::default_logger(); + logger_->set_level(spdlog::level::info); + } + + ~DependencyTestEngine() { + // Collect names first to avoid iterator invalidation during unload + std::vector names; + names.reserve(modules_.size()); + for (const auto& [name, _] : modules_) { + names.push_back(name); + } + // Unload in reverse order (dependents before dependencies) + for (auto it = names.rbegin(); it != names.rend(); ++it) { + unloadModule(*it); + } + } + + bool loadModule(const std::string& name, const std::string& path) { + if (modules_.count(name) > 0) { + logger_->warn("Module {} already loaded", name); + return false; + } + + void* dlHandle = grove_dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); + if (!dlHandle) { + logger_->error("Failed to load module {}: {}", name, grove_dlerror()); + return false; + } + + auto createFunc = (IModule* (*)())grove_dlsym(dlHandle, "createModule"); + if (!createFunc) { + logger_->error("Failed to find createModule in {}: {}", name, grove_dlerror()); + grove_dlclose(dlHandle); + return false; + } + + IModule* instance = createFunc(); + if (!instance) { + logger_->error("createModule returned nullptr for {}", name); + grove_dlclose(dlHandle); + return false; + } + + ModuleHandle handle; + handle.dlHandle = dlHandle; + handle.instance = instance; + handle.modulePath = path; + handle.version = instance->getVersion(); + handle.dependencies = instance->getDependencies(); + + modules_[name] = handle; + + // Initialize module + auto config = std::make_unique("config", nlohmann::json::object()); + instance->setConfiguration(*config, nullptr, nullptr); + + logger_->info("Loaded {} v{} with {} dependencies", + name, handle.version, handle.dependencies.size()); + + return true; + } + + bool reloadModule(const std::string& name, bool cascadeDependents = true) { + auto it = modules_.find(name); + if (it == modules_.end()) { + logger_->error("Module {} not found for reload", name); + return false; + } + + auto startTime = std::chrono::high_resolution_clock::now(); + + // Find dependents if cascade is enabled + std::vector dependents; + if (cascadeDependents) { + dependents = findDependents(name); + } + + logger_->info("Reloading {} (cascade: {} dependents)", name, dependents.size()); + + // Save states of all affected modules + std::map> savedStates; + savedStates[name] = it->second.instance->getState(); + for (const auto& dep : dependents) { + savedStates[dep] = modules_[dep].instance->getState(); + } + + // Reload the target module + if (!reloadModuleSingle(name)) { + logger_->error("Failed to reload {}", name); + return false; + } + + // Cascade reload dependents + for (const auto& dep : dependents) { + logger_->info(" → Cascade reloading dependent: {}", dep); + if (!reloadModuleSingle(dep)) { + logger_->error("Failed to cascade reload {}", dep); + return false; + } + } + + // Restore states + it->second.instance->setState(*savedStates[name]); + for (const auto& dep : dependents) { + modules_[dep].instance->setState(*savedStates[dep]); + } + + auto endTime = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(endTime - startTime); + logger_->info("Cascade reload completed in {}ms", duration.count()); + + return true; + } + + bool canUnloadModule(const std::string& name, std::string& errorMsg) { + auto dependents = findDependents(name); + if (!dependents.empty()) { + errorMsg = "Cannot unload " + name + ": required by "; + for (size_t i = 0; i < dependents.size(); i++) { + if (i > 0) errorMsg += ", "; + errorMsg += dependents[i]; + } + return false; + } + return true; + } + + bool unloadModule(const std::string& name) { + auto it = modules_.find(name); + if (it == modules_.end()) { + return false; + } + + // Check if any other modules depend on this one + std::string errorMsg; + if (!canUnloadModule(name, errorMsg)) { + logger_->error("{}", errorMsg); + return false; + } + + auto& handle = it->second; + handle.instance->shutdown(); + + auto destroyFunc = (void (*)(IModule*))grove_dlsym(handle.dlHandle, "destroyModule"); + if (destroyFunc) { + destroyFunc(handle.instance); + } else { + delete handle.instance; + } + + grove_dlclose(handle.dlHandle); + + modules_.erase(it); + logger_->info("Unloaded {}", name); + + return true; + } + + std::vector findDependents(const std::string& moduleName) { + std::vector dependents; + for (const auto& [name, handle] : modules_) { + for (const auto& dep : handle.dependencies) { + if (dep == moduleName) { + dependents.push_back(name); + break; + } + } + } + return dependents; + } + + bool hasCircularDependencies(const std::string& moduleName, + std::set& visited, + std::set& recursionStack) { + visited.insert(moduleName); + recursionStack.insert(moduleName); + + auto it = modules_.find(moduleName); + if (it != modules_.end()) { + for (const auto& dep : it->second.dependencies) { + if (recursionStack.count(dep) > 0) { + // Cycle detected + return true; + } + if (visited.count(dep) == 0) { + if (hasCircularDependencies(dep, visited, recursionStack)) { + return true; + } + } + } + } + + recursionStack.erase(moduleName); + return false; + } + + IModule* getModule(const std::string& name) { + auto it = modules_.find(name); + return (it != modules_.end()) ? it->second.instance : nullptr; + } + + int getModuleVersion(const std::string& name) { + auto it = modules_.find(name); + return (it != modules_.end()) ? it->second.instance->getVersion() : 0; + } + + void process(float deltaTime) { + auto input = std::make_unique("input", nlohmann::json::object()); + for (auto& [name, handle] : modules_) { + if (handle.instance) { + handle.instance->process(*input); + } + } + } + + void injectDependency(const std::string& dependentName, const std::string& baseName) { + // For this test, we don't actually inject dependencies at the C++ level + // The modules are designed to work independently + // This is just a placeholder for demonstration + logger_->info("Dependency declared: {} depends on {}", dependentName, baseName); + } + +private: + bool reloadModuleSingle(const std::string& name) { + auto it = modules_.find(name); + if (it == modules_.end()) { + return false; + } + + auto& handle = it->second; + std::string path = handle.modulePath; + + // Destroy old instance + handle.instance->shutdown(); + auto destroyFunc = (void (*)(IModule*))grove_dlsym(handle.dlHandle, "destroyModule"); + if (destroyFunc) { + destroyFunc(handle.instance); + } else { + delete handle.instance; + } + grove_dlclose(handle.dlHandle); + + // Reload shared library + void* newHandle = grove_dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); + if (!newHandle) { + logger_->error("Failed to reload {}: {}", name, grove_dlerror()); + return false; + } + + auto createFunc = (IModule* (*)())grove_dlsym(newHandle, "createModule"); + if (!createFunc) { + logger_->error("Failed to find createModule in reloaded {}", name); + grove_dlclose(newHandle); + return false; + } + + IModule* newInstance = createFunc(); + if (!newInstance) { + logger_->error("createModule returned nullptr for reloaded {}", name); + grove_dlclose(newHandle); + return false; + } + + handle.dlHandle = newHandle; + handle.instance = newInstance; + handle.version = newInstance->getVersion(); + + // Re-initialize + auto config = std::make_unique("config", nlohmann::json::object()); + newInstance->setConfiguration(*config, nullptr, nullptr); + + return true; + } + + std::map modules_; + std::shared_ptr logger_; +}; + +int main() { + TestReporter reporter("Module Dependencies"); + TestMetrics metrics; + + std::cout << "================================================================================\n"; + std::cout << "TEST: Module Dependencies\n"; + std::cout << "================================================================================\n\n"; + + DependencyTestEngine engine; + + // === SETUP: Load modules with dependencies === + std::cout << "=== Setup: Load modules with dependencies ===\n"; + + ASSERT_TRUE(engine.loadModule("BaseModule", "./libBaseModule.dll"), + "Should load BaseModule"); + ASSERT_TRUE(engine.loadModule("DependentModule", "./libDependentModule.dll"), + "Should load DependentModule"); + ASSERT_TRUE(engine.loadModule("IndependentModule", "./libIndependentModule.dll"), + "Should load IndependentModule"); + + reporter.addAssertion("modules_loaded", true); + + // Inject dependency + engine.injectDependency("DependentModule", "BaseModule"); + + // Verify dependencies + auto baseModule = engine.getModule("BaseModule"); + auto depModule = engine.getModule("DependentModule"); + auto indModule = engine.getModule("IndependentModule"); + + ASSERT_TRUE(baseModule != nullptr, "BaseModule should be loaded"); + ASSERT_TRUE(depModule != nullptr, "DependentModule should be loaded"); + ASSERT_TRUE(indModule != nullptr, "IndependentModule should be loaded"); + + auto baseDeps = baseModule->getDependencies(); + auto depDeps = depModule->getDependencies(); + auto indDeps = indModule->getDependencies(); + + ASSERT_TRUE(baseDeps.empty(), "BaseModule should have no dependencies"); + ASSERT_EQ(depDeps.size(), 1, "DependentModule should have 1 dependency"); + ASSERT_EQ(depDeps[0], "BaseModule", "DependentModule should depend on BaseModule"); + ASSERT_TRUE(indDeps.empty(), "IndependentModule should have no dependencies"); + + int baseV1 = engine.getModuleVersion("BaseModule"); + int depV1 = engine.getModuleVersion("DependentModule"); + int indV1 = engine.getModuleVersion("IndependentModule"); + + std::cout << "✓ BaseModule loaded (v" << baseV1 << ", no dependencies)\n"; + std::cout << "✓ DependentModule loaded (v" << depV1 << ", depends on: BaseModule)\n"; + std::cout << "✓ IndependentModule loaded (v" << indV1 << ", no dependencies)\n\n"; + + std::cout << "Dependency graph:\n"; + std::cout << " IndependentModule → (none)\n"; + std::cout << " BaseModule → (none)\n"; + std::cout << " DependentModule → BaseModule\n\n"; + + // Run for a bit + for (int i = 0; i < 60; i++) { + engine.process(1.0f / 60.0f); + } + + // === PHASE 1: Cascade Reload (30s) === + std::cout << "=== Phase 1: Cascade Reload (30s) ===\n"; + + // Verify BaseModule is loaded (we can't access generateNumber() directly in this test) + ASSERT_TRUE(engine.getModule("BaseModule") != nullptr, "BaseModule should be loaded"); + + std::cout << "Reloading BaseModule...\n"; + auto cascadeStart = std::chrono::high_resolution_clock::now(); + + // In a real system, this would reload the .so file with new code + // For this test, we simulate by reloading the same module + ASSERT_TRUE(engine.reloadModule("BaseModule", true), "BaseModule reload should succeed"); + + auto cascadeEnd = std::chrono::high_resolution_clock::now(); + auto cascadeTime = std::chrono::duration_cast(cascadeEnd - cascadeStart).count(); + + std::cout << " → BaseModule reload triggered\n"; + std::cout << " → Cascade reload triggered for DependentModule\n"; + + // Re-inject dependency after reload + engine.injectDependency("DependentModule", "BaseModule"); + + int baseV2 = engine.getModuleVersion("BaseModule"); + int depV2 = engine.getModuleVersion("DependentModule"); + int indV2 = engine.getModuleVersion("IndependentModule"); + + std::cout << "✓ BaseModule reloaded: v" << baseV1 << " → v" << baseV2 << "\n"; + std::cout << "✓ DependentModule cascade reloaded: v" << depV1 << " → v" << depV2 << "\n"; + std::cout << "✓ IndependentModule NOT reloaded (v" << indV2 << " unchanged)\n"; + + reporter.addAssertion("cascade_reload_triggered", true); + reporter.addAssertion("independent_isolated", indV2 == indV1); + reporter.addMetric("cascade_reload_time_ms", cascadeTime); + + std::cout << "\nMetrics:\n"; + std::cout << " Cascade reload time: " << cascadeTime << "ms "; + std::cout << (cascadeTime < 200 ? "✓" : "✗") << "\n\n"; + + // Run for 30 seconds + for (int i = 0; i < 1800; i++) { + engine.process(1.0f / 60.0f); + } + + // === PHASE 2: Unload Protection (10s) === + std::cout << "=== Phase 2: Unload Protection (10s) ===\n"; + + std::cout << "Attempting to unload BaseModule...\n"; + std::string errorMsg; + bool canUnload = engine.canUnloadModule("BaseModule", errorMsg); + + ASSERT_FALSE(canUnload, "BaseModule should not be unloadable while DependentModule active"); + reporter.addAssertion("unload_protection_works", !canUnload); + + std::cout << " ✗ Unload rejected: " << errorMsg << "\n"; + std::cout << "✓ BaseModule still loaded and functional\n"; + std::cout << "✓ All modules stable\n\n"; + + // Run for 10 seconds + for (int i = 0; i < 600; i++) { + engine.process(1.0f / 60.0f); + } + + // === PHASE 3: Reload Dependent Only (20s) === + std::cout << "=== Phase 3: Reload Dependent Only (20s) ===\n"; + + std::cout << "Reloading DependentModule...\n"; + int baseV3Before = engine.getModuleVersion("BaseModule"); + + ASSERT_TRUE(engine.reloadModule("DependentModule", false), "DependentModule reload should succeed"); + + // Re-inject dependency after reload + engine.injectDependency("DependentModule", "BaseModule"); + + int baseV3After = engine.getModuleVersion("BaseModule"); + int depV3 = engine.getModuleVersion("DependentModule"); + + std::cout << "✓ DependentModule reloaded: v" << depV2 << " → v" << depV3 << "\n"; + std::cout << "✓ BaseModule NOT reloaded (v" << baseV3After << " unchanged)\n"; + std::cout << "✓ IndependentModule still isolated\n"; + std::cout << "✓ DependentModule still connected to BaseModule\n\n"; + + ASSERT_EQ(baseV3Before, baseV3After, "BaseModule should not reload when dependent reloads"); + reporter.addAssertion("no_reverse_cascade", baseV3Before == baseV3After); + + // Run for 20 seconds + for (int i = 0; i < 1200; i++) { + engine.process(1.0f / 60.0f); + } + + // === PHASE 4: Cyclic Dependency Detection (20s) === + std::cout << "=== Phase 4: Cyclic Dependency Detection (20s) ===\n"; + + // For this phase, we would need to create cyclic modules, which is complex + // Instead, we'll verify the cycle detection algorithm works + std::cout << "Simulating cyclic dependency check...\n"; + + std::set visited, recursionStack; + bool hasCycle = engine.hasCircularDependencies("BaseModule", visited, recursionStack); + ASSERT_FALSE(hasCycle, "Current module graph should not have cycles"); + + std::cout << "✓ No cycles detected in current module graph\n"; + std::cout << " (In a real scenario, cyclic modules would be rejected at load time)\n\n"; + + reporter.addAssertion("cycle_detected", true); + + // Run for 20 seconds + for (int i = 0; i < 1200; i++) { + engine.process(1.0f / 60.0f); + } + + // === PHASE 5: Cascade Unload (20s) === + std::cout << "=== Phase 5: Cascade Unload (20s) ===\n"; + + std::cout << "Unloading DependentModule...\n"; + ASSERT_TRUE(engine.unloadModule("DependentModule"), "DependentModule should unload"); + std::cout << "✓ DependentModule unloaded (dependency released)\n"; + + baseModule = engine.getModule("BaseModule"); + ASSERT_TRUE(baseModule != nullptr, "BaseModule should still be loaded"); + std::cout << "✓ BaseModule still loaded\n\n"; + + std::cout << "Attempting to unload BaseModule...\n"; + ASSERT_TRUE(engine.unloadModule("BaseModule"), "BaseModule should now unload"); + std::cout << "✓ BaseModule unload succeeded (no dependents)\n\n"; + + std::cout << "Final state:\n"; + std::cout << " IndependentModule: loaded (v" << engine.getModuleVersion("IndependentModule") << ")\n"; + std::cout << " BaseModule: unloaded\n"; + std::cout << " DependentModule: unloaded\n\n"; + + reporter.addAssertion("cascade_unload_works", true); + reporter.addAssertion("state_preserved", true); + reporter.addAssertion("no_crashes", true); + + // === FINAL REPORT === + reporter.printFinalReport(); + + return reporter.getExitCode(); +} diff --git a/tests/integration/test_10_multiversion_coexistence.cpp b/tests/integration/test_10_multiversion_coexistence.cpp index b48211e..ddeb60d 100644 --- a/tests/integration/test_10_multiversion_coexistence.cpp +++ b/tests/integration/test_10_multiversion_coexistence.cpp @@ -1,544 +1,544 @@ -/** - * Scenario 10: Multi-Version Module Coexistence Test - * - * Tests ability to load multiple versions of the same module simultaneously - * with canary deployment, progressive migration, and instant rollback. - * - * Phases: - * - Phase 0: Setup baseline (v1 with 100 entities) - * - Phase 1: Canary deployment (10% v2, 90% v1) - * - Phase 2: Progressive migration (v1 → v2: 30%, 50%, 80%, 100%) - * - Phase 3: Auto garbage collection (unload v1) - * - Phase 4: Emergency rollback (v2 → v1) - * - Phase 5: Three-way coexistence (20% v1, 30% v2, 50% v3) - */ - -#include "grove/IModule.h" -#include "grove/JsonDataNode.h" -#include "../helpers/TestMetrics.h" -#include "../helpers/TestAssertions.h" -#include "../helpers/TestReporter.h" - -#ifdef _WIN32 -#include -#else -#include -#endif -#include -#include -#include -#include -#include -#include -#include -#include - -// Cross-platform dlopen wrappers -#ifdef _WIN32 -inline void* grove_dlopen(const char* path, int flags) { - (void)flags; - return LoadLibraryA(path); -} -inline void* grove_dlsym(void* handle, const char* symbol) { - return (void*)GetProcAddress((HMODULE)handle, symbol); -} -inline int grove_dlclose(void* handle) { - return FreeLibrary((HMODULE)handle) ? 0 : -1; -} -inline const char* grove_dlerror() { - static thread_local char buf[256]; - DWORD err = GetLastError(); - snprintf(buf, sizeof(buf), "Windows error code: %lu", err); - return buf; -} -#define RTLD_NOW 0 -#define RTLD_LOCAL 0 -#else -#define grove_dlopen dlopen -#define grove_dlsym dlsym -#define grove_dlclose dlclose -#define grove_dlerror dlerror -#endif - -using namespace grove; -using json = nlohmann::json; - -// Version-specific module handle -struct VersionHandle { - void* dlHandle = nullptr; - IModule* instance = nullptr; - std::string modulePath; - int version = 0; - float trafficPercent = 0.0f; // % of traffic routed to this version - std::chrono::steady_clock::time_point lastUsed; - size_t processedEntities = 0; - - bool isLoaded() const { return dlHandle != nullptr && instance != nullptr; } -}; - -// Multi-version test engine -class MultiVersionTestEngine { -public: - MultiVersionTestEngine() { - logger_ = spdlog::default_logger(); - logger_->set_level(spdlog::level::info); - } - - ~MultiVersionTestEngine() { - // Copy keys to avoid iterator invalidation during unload - std::vector keys; - for (const auto& [key, handle] : versions_) { - keys.push_back(key); - } - for (const auto& key : keys) { - unloadVersion(key); - } - } - - // Load a specific version of a module - bool loadModuleVersion(const std::string& moduleName, int version, const std::string& path) { - std::string key = moduleName + ":v" + std::to_string(version); - - if (versions_.count(key) > 0) { - logger_->warn("Version {} already loaded", key); - return false; - } - - void* dlHandle = grove_dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); - if (!dlHandle) { - logger_->error("Failed to load {}: {}", key, grove_dlerror()); - return false; - } - - auto createFunc = (IModule* (*)())grove_dlsym(dlHandle, "createModule"); - if (!createFunc) { - logger_->error("Failed to find createModule in {}: {}", key, grove_dlerror()); - grove_dlclose(dlHandle); - return false; - } - - IModule* instance = createFunc(); - if (!instance) { - logger_->error("createModule returned nullptr for {}", key); - grove_dlclose(dlHandle); - return false; - } - - VersionHandle handle; - handle.dlHandle = dlHandle; - handle.instance = instance; - handle.modulePath = path; - handle.version = instance->getVersion(); - handle.trafficPercent = 0.0f; - handle.lastUsed = std::chrono::steady_clock::now(); - - versions_[key] = handle; - - // Initialize module - json configJson; - configJson["entityCount"] = 100; - auto config = std::make_unique("config", configJson); - instance->setConfiguration(*config, nullptr, nullptr); - - logger_->info("✓ Loaded {} (actual version: {})", key, handle.version); - - return true; - } - - // Unload a specific version - bool unloadVersion(const std::string& key) { - auto it = versions_.find(key); - if (it == versions_.end()) return false; - - auto& handle = it->second; - if (handle.instance) { - handle.instance->shutdown(); - auto destroyFunc = (void (*)(IModule*))grove_dlsym(handle.dlHandle, "destroyModule"); - if (destroyFunc) { - destroyFunc(handle.instance); - } - } - - if (handle.dlHandle) { - grove_dlclose(handle.dlHandle); - } - - versions_.erase(it); - logger_->info("✓ Unloaded {}", key); - return true; - } - - // Set traffic split across versions - bool setTrafficSplit(const std::string& moduleName, const std::map& weights) { - // Validate weights sum to ~1.0 - float sum = 0.0f; - for (const auto& [version, weight] : weights) { - sum += weight; - } - - if (std::abs(sum - 1.0f) > 0.01f) { - logger_->error("Traffic weights must sum to 1.0 (got {})", sum); - return false; - } - - // Apply weights - for (const auto& [version, weight] : weights) { - std::string key = moduleName + ":v" + std::to_string(version); - if (versions_.count(key) > 0) { - versions_[key].trafficPercent = weight * 100.0f; - } - } - - logger_->info("✓ Traffic split configured:"); - for (const auto& [version, weight] : weights) { - logger_->info(" v{}: {}%", version, weight * 100.0f); - } - - return true; - } - - // Get current traffic split - std::map getTrafficSplit(const std::string& moduleName) const { - std::map split; - for (const auto& [key, handle] : versions_) { - if (key.find(moduleName + ":v") == 0) { - split[handle.version] = handle.trafficPercent; - } - } - return split; - } - - // Migrate state from one version to another - bool migrateState(const std::string& moduleName, int fromVersion, int toVersion) { - std::string fromKey = moduleName + ":v" + std::to_string(fromVersion); - std::string toKey = moduleName + ":v" + std::to_string(toVersion); - - if (versions_.count(fromKey) == 0 || versions_.count(toKey) == 0) { - logger_->error("Cannot migrate: version not loaded"); - return false; - } - - auto startTime = std::chrono::high_resolution_clock::now(); - - // Extract state from source version - auto oldState = versions_[fromKey].instance->getState(); - - // Migrate to target version - bool success = versions_[toKey].instance->migrateStateFrom(fromVersion, *oldState); - - auto endTime = std::chrono::high_resolution_clock::now(); - auto duration = std::chrono::duration_cast(endTime - startTime); - - if (success) { - logger_->info("✓ State migrated v{} → v{} in {}ms", fromVersion, toVersion, duration.count()); - } else { - logger_->error("✗ State migration v{} → v{} failed", fromVersion, toVersion); - } - - return success; - } - - // Check if version is loaded - bool isVersionLoaded(const std::string& moduleName, int version) const { - std::string key = moduleName + ":v" + std::to_string(version); - return versions_.count(key) > 0; - } - - // Get entity count for a version - size_t getEntityCount(const std::string& moduleName, int version) const { - std::string key = moduleName + ":v" + std::to_string(version); - auto it = versions_.find(key); - if (it == versions_.end()) return 0; - - auto state = it->second.instance->getState(); - const auto* jsonState = dynamic_cast(state.get()); - if (jsonState) { - const auto& jsonData = jsonState->getJsonData(); - if (jsonData.contains("entityCount")) { - return jsonData["entityCount"]; - } - } - return 0; - } - - // Process all versions (simulates traffic routing) - void processAllVersions(float deltaTime) { - json inputJson; - inputJson["deltaTime"] = deltaTime; - auto input = std::make_unique("input", inputJson); - - for (auto& [key, handle] : versions_) { - if (handle.isLoaded() && handle.trafficPercent > 0.0f) { - handle.instance->process(*input); - handle.lastUsed = std::chrono::steady_clock::now(); - } - } - } - - // Auto garbage collection of unused versions - void autoGC(float unusedThresholdSeconds = 10.0f) { - auto now = std::chrono::steady_clock::now(); - std::vector toUnload; - - for (const auto& [key, handle] : versions_) { - if (handle.trafficPercent == 0.0f) { - auto elapsed = std::chrono::duration_cast(now - handle.lastUsed); - if (elapsed.count() >= unusedThresholdSeconds) { - toUnload.push_back(key); - } - } - } - - for (const auto& key : toUnload) { - logger_->info("Auto GC: unloading unused version {}", key); - unloadVersion(key); - } - } - - // Get all loaded versions for a module - std::vector getLoadedVersions(const std::string& moduleName) const { - std::vector versions; - for (const auto& [key, handle] : versions_) { - if (key.find(moduleName + ":v") == 0) { - versions.push_back(handle.version); - } - } - return versions; - } - -private: - std::map versions_; - std::shared_ptr logger_; -}; - -int main() { - std::cout << "================================================================================\n"; - std::cout << "TEST: Multi-Version Module Coexistence\n"; - std::cout << "================================================================================\n\n"; - - TestReporter reporter("Multi-Version Module Coexistence"); - - // Local metrics storage - std::map metrics; - - MultiVersionTestEngine engine; - - try { - std::cout << "=== Phase 0: Setup Baseline (v1 with 100 entities) ===\n"; - - // Load v1 - std::string v1Path = "./libGameLogicModuleV1.so"; - ASSERT_TRUE(engine.loadModuleVersion("GameLogic", 1, v1Path), - "Load GameLogic v1"); - - // Configure 100% traffic to v1 - engine.setTrafficSplit("GameLogic", {{1, 1.0f}}); - - // Verify - ASSERT_EQ(engine.getEntityCount("GameLogic", 1), 100, "v1 has 100 entities"); - - std::cout << "✓ Baseline established: v1 with 100 entities\n"; - - std::this_thread::sleep_for(std::chrono::seconds(2)); - - // ======================================== - std::cout << "\n=== Phase 1: Canary Deployment (10% v2, 90% v1) ===\n"; - - auto phase1Start = std::chrono::high_resolution_clock::now(); - - // Load v2 - std::string v2Path = "./libGameLogicModuleV2.so"; - ASSERT_TRUE(engine.loadModuleVersion("GameLogic", 2, v2Path), - "Load GameLogic v2"); - - auto phase1End = std::chrono::high_resolution_clock::now(); - auto loadTime = std::chrono::duration_cast(phase1End - phase1Start); - metrics["version_load_time_ms"] = loadTime.count(); - - std::cout << "Version load time: " << loadTime.count() << "ms\n"; - ASSERT_LT(loadTime.count(), 200, "Load time < 200ms"); - - // Configure canary: 10% v2, 90% v1 - engine.setTrafficSplit("GameLogic", {{1, 0.9f}, {2, 0.1f}}); - - // Verify both versions loaded - ASSERT_TRUE(engine.isVersionLoaded("GameLogic", 1), "v1 still loaded"); - ASSERT_TRUE(engine.isVersionLoaded("GameLogic", 2), "v2 loaded"); - - auto split = engine.getTrafficSplit("GameLogic"); - ASSERT_NEAR(split[1], 90.0f, 2.0f, "v1 traffic ~90%"); - ASSERT_NEAR(split[2], 10.0f, 2.0f, "v2 traffic ~10%"); - - std::cout << "✓ Canary deployment active: 10% v2, 90% v1\n"; - - std::this_thread::sleep_for(std::chrono::seconds(5)); - - // ======================================== - std::cout << "\n=== Phase 2: Progressive Migration v1 → v2 ===\n"; - - // Step 1: 30% v2, 70% v1 - std::cout << "t=0s: Traffic split → 30% v2, 70% v1\n"; - engine.setTrafficSplit("GameLogic", {{1, 0.7f}, {2, 0.3f}}); - std::this_thread::sleep_for(std::chrono::seconds(3)); - - split = engine.getTrafficSplit("GameLogic"); - ASSERT_NEAR(split[2], 30.0f, 2.0f, "v2 traffic ~30%"); - - // Step 2: 50% v2, 50% v1 - std::cout << "t=3s: Traffic split → 50% v2, 50% v1\n"; - engine.setTrafficSplit("GameLogic", {{1, 0.5f}, {2, 0.5f}}); - std::this_thread::sleep_for(std::chrono::seconds(3)); - - split = engine.getTrafficSplit("GameLogic"); - ASSERT_NEAR(split[2], 50.0f, 2.0f, "v2 traffic ~50%"); - - // Step 3: 80% v2, 20% v1 - std::cout << "t=6s: Traffic split → 80% v2, 20% v1\n"; - engine.setTrafficSplit("GameLogic", {{1, 0.2f}, {2, 0.8f}}); - std::this_thread::sleep_for(std::chrono::seconds(3)); - - split = engine.getTrafficSplit("GameLogic"); - ASSERT_NEAR(split[2], 80.0f, 2.0f, "v2 traffic ~80%"); - - // Step 4: 100% v2, 0% v1 (migration complete) - std::cout << "t=9s: Traffic split → 100% v2, 0% v1 (migration complete)\n"; - - auto migrationStart = std::chrono::high_resolution_clock::now(); - bool migrated = engine.migrateState("GameLogic", 1, 2); - auto migrationEnd = std::chrono::high_resolution_clock::now(); - auto migrationTime = std::chrono::duration_cast(migrationEnd - migrationStart); - - metrics["state_migration_time_ms"] = migrationTime.count(); - ASSERT_TRUE(migrated, "State migration successful"); - ASSERT_LT(migrationTime.count(), 500, "Migration time < 500ms"); - - engine.setTrafficSplit("GameLogic", {{1, 0.0f}, {2, 1.0f}}); - - split = engine.getTrafficSplit("GameLogic"); - ASSERT_NEAR(split[2], 100.0f, 2.0f, "v2 traffic ~100%"); - - std::cout << "✓ Progressive migration complete\n"; - - std::this_thread::sleep_for(std::chrono::seconds(3)); - - // ======================================== - std::cout << "\n=== Phase 3: Garbage Collection (unload v1) ===\n"; - - std::cout << "v1 unused for 10s → triggering auto GC...\n"; - - // Wait for GC threshold - std::this_thread::sleep_for(std::chrono::seconds(12)); - - auto gcStart = std::chrono::high_resolution_clock::now(); - engine.autoGC(10.0f); - auto gcEnd = std::chrono::high_resolution_clock::now(); - auto gcTime = std::chrono::duration_cast(gcEnd - gcStart); - - metrics["gc_time_ms"] = gcTime.count(); - ASSERT_LT(gcTime.count(), 100, "GC time < 100ms"); - - // Verify v1 unloaded - ASSERT_FALSE(engine.isVersionLoaded("GameLogic", 1), "v1 unloaded"); - ASSERT_TRUE(engine.isVersionLoaded("GameLogic", 2), "v2 still loaded"); - - std::cout << "✓ Auto GC complete: v1 unloaded, v2 continues\n"; - - std::this_thread::sleep_for(std::chrono::seconds(2)); - - // ======================================== - std::cout << "\n=== Phase 4: Emergency Rollback (v2 → v1) ===\n"; - - std::cout << "Simulating critical bug in v2 → triggering emergency rollback\n"; - - auto rollbackStart = std::chrono::high_resolution_clock::now(); - - // Reload v1 - ASSERT_TRUE(engine.loadModuleVersion("GameLogic", 1, v1Path), - "Reload v1 for rollback"); - - // Migrate state back v2 → v1 (if possible, otherwise fresh start) - // Note: v1 cannot migrate from v2 (fields missing), so this will fail gracefully - engine.migrateState("GameLogic", 2, 1); - - // Redirect traffic to v1 - engine.setTrafficSplit("GameLogic", {{1, 1.0f}, {2, 0.0f}}); - - auto rollbackEnd = std::chrono::high_resolution_clock::now(); - auto rollbackTime = std::chrono::duration_cast(rollbackEnd - rollbackStart); - - metrics["rollback_time_ms"] = rollbackTime.count(); - ASSERT_LT(rollbackTime.count(), 300, "Rollback time < 300ms"); - - // Verify rollback - ASSERT_TRUE(engine.isVersionLoaded("GameLogic", 1), "v1 reloaded"); - split = engine.getTrafficSplit("GameLogic"); - ASSERT_NEAR(split[1], 100.0f, 2.0f, "v1 traffic ~100%"); - - std::cout << "✓ Emergency rollback complete in " << rollbackTime.count() << "ms\n"; - - std::this_thread::sleep_for(std::chrono::seconds(3)); - - // ======================================== - std::cout << "\n=== Phase 5: Three-Way Coexistence (v1, v2, v3) ===\n"; - - // Load v3 - std::string v3Path = "./libGameLogicModuleV3.so"; - ASSERT_TRUE(engine.loadModuleVersion("GameLogic", 3, v3Path), - "Load GameLogic v3"); - - // Configure 3-way split: 20% v1, 30% v2, 50% v3 - engine.setTrafficSplit("GameLogic", {{1, 0.2f}, {2, 0.3f}, {3, 0.5f}}); - - // Verify 3 versions coexisting - ASSERT_TRUE(engine.isVersionLoaded("GameLogic", 1), "v1 loaded"); - ASSERT_TRUE(engine.isVersionLoaded("GameLogic", 2), "v2 loaded"); - ASSERT_TRUE(engine.isVersionLoaded("GameLogic", 3), "v3 loaded"); - - auto versions = engine.getLoadedVersions("GameLogic"); - ASSERT_EQ(versions.size(), 3, "3 versions loaded"); - - split = engine.getTrafficSplit("GameLogic"); - ASSERT_NEAR(split[1], 20.0f, 2.0f, "v1 traffic ~20%"); - ASSERT_NEAR(split[2], 30.0f, 2.0f, "v2 traffic ~30%"); - ASSERT_NEAR(split[3], 50.0f, 2.0f, "v3 traffic ~50%"); - - metrics["multi_version_count"] = versions.size(); - - std::cout << "✓ Three-way coexistence active:\n"; - std::cout << " v1: 20% traffic\n"; - std::cout << " v2: 30% traffic\n"; - std::cout << " v3: 50% traffic\n"; - - std::this_thread::sleep_for(std::chrono::seconds(5)); - - // ======================================== - std::cout << "\n=== Metrics Summary ===\n"; - reporter.addMetric("version_load_time_ms", metrics["version_load_time_ms"]); - reporter.addMetric("state_migration_time_ms", metrics["state_migration_time_ms"]); - reporter.addMetric("rollback_time_ms", metrics["rollback_time_ms"]); - reporter.addMetric("gc_time_ms", metrics["gc_time_ms"]); - reporter.addMetric("multi_version_count", metrics["multi_version_count"]); - - // Validate metrics - ASSERT_LT(metrics["version_load_time_ms"], 200.0, - "Version load time < 200ms"); - ASSERT_LT(metrics["state_migration_time_ms"], 500.0, - "State migration < 500ms"); - ASSERT_LT(metrics["rollback_time_ms"], 300.0, - "Rollback time < 300ms"); - ASSERT_LT(metrics["gc_time_ms"], 100.0, - "GC time < 100ms"); - ASSERT_EQ(metrics["multi_version_count"], 3.0, - "3 versions coexisting"); - - // ======================================== - std::cout << "\n=== Final Report ===\n"; - reporter.printFinalReport(); - - return reporter.getExitCode(); - - } catch (const std::exception& e) { - std::cerr << "Exception: " << e.what() << "\n"; - return 1; - } -} +/** + * Scenario 10: Multi-Version Module Coexistence Test + * + * Tests ability to load multiple versions of the same module simultaneously + * with canary deployment, progressive migration, and instant rollback. + * + * Phases: + * - Phase 0: Setup baseline (v1 with 100 entities) + * - Phase 1: Canary deployment (10% v2, 90% v1) + * - Phase 2: Progressive migration (v1 → v2: 30%, 50%, 80%, 100%) + * - Phase 3: Auto garbage collection (unload v1) + * - Phase 4: Emergency rollback (v2 → v1) + * - Phase 5: Three-way coexistence (20% v1, 30% v2, 50% v3) + */ + +#include "grove/IModule.h" +#include "grove/JsonDataNode.h" +#include "../helpers/TestMetrics.h" +#include "../helpers/TestAssertions.h" +#include "../helpers/TestReporter.h" + +#ifdef _WIN32 +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include + +// Cross-platform dlopen wrappers +#ifdef _WIN32 +inline void* grove_dlopen(const char* path, int flags) { + (void)flags; + return LoadLibraryA(path); +} +inline void* grove_dlsym(void* handle, const char* symbol) { + return (void*)GetProcAddress((HMODULE)handle, symbol); +} +inline int grove_dlclose(void* handle) { + return FreeLibrary((HMODULE)handle) ? 0 : -1; +} +inline const char* grove_dlerror() { + static thread_local char buf[256]; + DWORD err = GetLastError(); + snprintf(buf, sizeof(buf), "Windows error code: %lu", err); + return buf; +} +#define RTLD_NOW 0 +#define RTLD_LOCAL 0 +#else +#define grove_dlopen dlopen +#define grove_dlsym dlsym +#define grove_dlclose dlclose +#define grove_dlerror dlerror +#endif + +using namespace grove; +using json = nlohmann::json; + +// Version-specific module handle +struct VersionHandle { + void* dlHandle = nullptr; + IModule* instance = nullptr; + std::string modulePath; + int version = 0; + float trafficPercent = 0.0f; // % of traffic routed to this version + std::chrono::steady_clock::time_point lastUsed; + size_t processedEntities = 0; + + bool isLoaded() const { return dlHandle != nullptr && instance != nullptr; } +}; + +// Multi-version test engine +class MultiVersionTestEngine { +public: + MultiVersionTestEngine() { + logger_ = spdlog::default_logger(); + logger_->set_level(spdlog::level::info); + } + + ~MultiVersionTestEngine() { + // Copy keys to avoid iterator invalidation during unload + std::vector keys; + for (const auto& [key, handle] : versions_) { + keys.push_back(key); + } + for (const auto& key : keys) { + unloadVersion(key); + } + } + + // Load a specific version of a module + bool loadModuleVersion(const std::string& moduleName, int version, const std::string& path) { + std::string key = moduleName + ":v" + std::to_string(version); + + if (versions_.count(key) > 0) { + logger_->warn("Version {} already loaded", key); + return false; + } + + void* dlHandle = grove_dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); + if (!dlHandle) { + logger_->error("Failed to load {}: {}", key, grove_dlerror()); + return false; + } + + auto createFunc = (IModule* (*)())grove_dlsym(dlHandle, "createModule"); + if (!createFunc) { + logger_->error("Failed to find createModule in {}: {}", key, grove_dlerror()); + grove_dlclose(dlHandle); + return false; + } + + IModule* instance = createFunc(); + if (!instance) { + logger_->error("createModule returned nullptr for {}", key); + grove_dlclose(dlHandle); + return false; + } + + VersionHandle handle; + handle.dlHandle = dlHandle; + handle.instance = instance; + handle.modulePath = path; + handle.version = instance->getVersion(); + handle.trafficPercent = 0.0f; + handle.lastUsed = std::chrono::steady_clock::now(); + + versions_[key] = handle; + + // Initialize module + json configJson; + configJson["entityCount"] = 100; + auto config = std::make_unique("config", configJson); + instance->setConfiguration(*config, nullptr, nullptr); + + logger_->info("✓ Loaded {} (actual version: {})", key, handle.version); + + return true; + } + + // Unload a specific version + bool unloadVersion(const std::string& key) { + auto it = versions_.find(key); + if (it == versions_.end()) return false; + + auto& handle = it->second; + if (handle.instance) { + handle.instance->shutdown(); + auto destroyFunc = (void (*)(IModule*))grove_dlsym(handle.dlHandle, "destroyModule"); + if (destroyFunc) { + destroyFunc(handle.instance); + } + } + + if (handle.dlHandle) { + grove_dlclose(handle.dlHandle); + } + + versions_.erase(it); + logger_->info("✓ Unloaded {}", key); + return true; + } + + // Set traffic split across versions + bool setTrafficSplit(const std::string& moduleName, const std::map& weights) { + // Validate weights sum to ~1.0 + float sum = 0.0f; + for (const auto& [version, weight] : weights) { + sum += weight; + } + + if (std::abs(sum - 1.0f) > 0.01f) { + logger_->error("Traffic weights must sum to 1.0 (got {})", sum); + return false; + } + + // Apply weights + for (const auto& [version, weight] : weights) { + std::string key = moduleName + ":v" + std::to_string(version); + if (versions_.count(key) > 0) { + versions_[key].trafficPercent = weight * 100.0f; + } + } + + logger_->info("✓ Traffic split configured:"); + for (const auto& [version, weight] : weights) { + logger_->info(" v{}: {}%", version, weight * 100.0f); + } + + return true; + } + + // Get current traffic split + std::map getTrafficSplit(const std::string& moduleName) const { + std::map split; + for (const auto& [key, handle] : versions_) { + if (key.find(moduleName + ":v") == 0) { + split[handle.version] = handle.trafficPercent; + } + } + return split; + } + + // Migrate state from one version to another + bool migrateState(const std::string& moduleName, int fromVersion, int toVersion) { + std::string fromKey = moduleName + ":v" + std::to_string(fromVersion); + std::string toKey = moduleName + ":v" + std::to_string(toVersion); + + if (versions_.count(fromKey) == 0 || versions_.count(toKey) == 0) { + logger_->error("Cannot migrate: version not loaded"); + return false; + } + + auto startTime = std::chrono::high_resolution_clock::now(); + + // Extract state from source version + auto oldState = versions_[fromKey].instance->getState(); + + // Migrate to target version + bool success = versions_[toKey].instance->migrateStateFrom(fromVersion, *oldState); + + auto endTime = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(endTime - startTime); + + if (success) { + logger_->info("✓ State migrated v{} → v{} in {}ms", fromVersion, toVersion, duration.count()); + } else { + logger_->error("✗ State migration v{} → v{} failed", fromVersion, toVersion); + } + + return success; + } + + // Check if version is loaded + bool isVersionLoaded(const std::string& moduleName, int version) const { + std::string key = moduleName + ":v" + std::to_string(version); + return versions_.count(key) > 0; + } + + // Get entity count for a version + size_t getEntityCount(const std::string& moduleName, int version) const { + std::string key = moduleName + ":v" + std::to_string(version); + auto it = versions_.find(key); + if (it == versions_.end()) return 0; + + auto state = it->second.instance->getState(); + const auto* jsonState = dynamic_cast(state.get()); + if (jsonState) { + const auto& jsonData = jsonState->getJsonData(); + if (jsonData.contains("entityCount")) { + return jsonData["entityCount"]; + } + } + return 0; + } + + // Process all versions (simulates traffic routing) + void processAllVersions(float deltaTime) { + json inputJson; + inputJson["deltaTime"] = deltaTime; + auto input = std::make_unique("input", inputJson); + + for (auto& [key, handle] : versions_) { + if (handle.isLoaded() && handle.trafficPercent > 0.0f) { + handle.instance->process(*input); + handle.lastUsed = std::chrono::steady_clock::now(); + } + } + } + + // Auto garbage collection of unused versions + void autoGC(float unusedThresholdSeconds = 10.0f) { + auto now = std::chrono::steady_clock::now(); + std::vector toUnload; + + for (const auto& [key, handle] : versions_) { + if (handle.trafficPercent == 0.0f) { + auto elapsed = std::chrono::duration_cast(now - handle.lastUsed); + if (elapsed.count() >= unusedThresholdSeconds) { + toUnload.push_back(key); + } + } + } + + for (const auto& key : toUnload) { + logger_->info("Auto GC: unloading unused version {}", key); + unloadVersion(key); + } + } + + // Get all loaded versions for a module + std::vector getLoadedVersions(const std::string& moduleName) const { + std::vector versions; + for (const auto& [key, handle] : versions_) { + if (key.find(moduleName + ":v") == 0) { + versions.push_back(handle.version); + } + } + return versions; + } + +private: + std::map versions_; + std::shared_ptr logger_; +}; + +int main() { + std::cout << "================================================================================\n"; + std::cout << "TEST: Multi-Version Module Coexistence\n"; + std::cout << "================================================================================\n\n"; + + TestReporter reporter("Multi-Version Module Coexistence"); + + // Local metrics storage + std::map metrics; + + MultiVersionTestEngine engine; + + try { + std::cout << "=== Phase 0: Setup Baseline (v1 with 100 entities) ===\n"; + + // Load v1 + std::string v1Path = "./libGameLogicModuleV1.dll"; + ASSERT_TRUE(engine.loadModuleVersion("GameLogic", 1, v1Path), + "Load GameLogic v1"); + + // Configure 100% traffic to v1 + engine.setTrafficSplit("GameLogic", {{1, 1.0f}}); + + // Verify + ASSERT_EQ(engine.getEntityCount("GameLogic", 1), 100, "v1 has 100 entities"); + + std::cout << "✓ Baseline established: v1 with 100 entities\n"; + + std::this_thread::sleep_for(std::chrono::seconds(2)); + + // ======================================== + std::cout << "\n=== Phase 1: Canary Deployment (10% v2, 90% v1) ===\n"; + + auto phase1Start = std::chrono::high_resolution_clock::now(); + + // Load v2 + std::string v2Path = "./libGameLogicModuleV2.dll"; + ASSERT_TRUE(engine.loadModuleVersion("GameLogic", 2, v2Path), + "Load GameLogic v2"); + + auto phase1End = std::chrono::high_resolution_clock::now(); + auto loadTime = std::chrono::duration_cast(phase1End - phase1Start); + metrics["version_load_time_ms"] = loadTime.count(); + + std::cout << "Version load time: " << loadTime.count() << "ms\n"; + ASSERT_LT(loadTime.count(), 200, "Load time < 200ms"); + + // Configure canary: 10% v2, 90% v1 + engine.setTrafficSplit("GameLogic", {{1, 0.9f}, {2, 0.1f}}); + + // Verify both versions loaded + ASSERT_TRUE(engine.isVersionLoaded("GameLogic", 1), "v1 still loaded"); + ASSERT_TRUE(engine.isVersionLoaded("GameLogic", 2), "v2 loaded"); + + auto split = engine.getTrafficSplit("GameLogic"); + ASSERT_NEAR(split[1], 90.0f, 2.0f, "v1 traffic ~90%"); + ASSERT_NEAR(split[2], 10.0f, 2.0f, "v2 traffic ~10%"); + + std::cout << "✓ Canary deployment active: 10% v2, 90% v1\n"; + + std::this_thread::sleep_for(std::chrono::seconds(5)); + + // ======================================== + std::cout << "\n=== Phase 2: Progressive Migration v1 → v2 ===\n"; + + // Step 1: 30% v2, 70% v1 + std::cout << "t=0s: Traffic split → 30% v2, 70% v1\n"; + engine.setTrafficSplit("GameLogic", {{1, 0.7f}, {2, 0.3f}}); + std::this_thread::sleep_for(std::chrono::seconds(3)); + + split = engine.getTrafficSplit("GameLogic"); + ASSERT_NEAR(split[2], 30.0f, 2.0f, "v2 traffic ~30%"); + + // Step 2: 50% v2, 50% v1 + std::cout << "t=3s: Traffic split → 50% v2, 50% v1\n"; + engine.setTrafficSplit("GameLogic", {{1, 0.5f}, {2, 0.5f}}); + std::this_thread::sleep_for(std::chrono::seconds(3)); + + split = engine.getTrafficSplit("GameLogic"); + ASSERT_NEAR(split[2], 50.0f, 2.0f, "v2 traffic ~50%"); + + // Step 3: 80% v2, 20% v1 + std::cout << "t=6s: Traffic split → 80% v2, 20% v1\n"; + engine.setTrafficSplit("GameLogic", {{1, 0.2f}, {2, 0.8f}}); + std::this_thread::sleep_for(std::chrono::seconds(3)); + + split = engine.getTrafficSplit("GameLogic"); + ASSERT_NEAR(split[2], 80.0f, 2.0f, "v2 traffic ~80%"); + + // Step 4: 100% v2, 0% v1 (migration complete) + std::cout << "t=9s: Traffic split → 100% v2, 0% v1 (migration complete)\n"; + + auto migrationStart = std::chrono::high_resolution_clock::now(); + bool migrated = engine.migrateState("GameLogic", 1, 2); + auto migrationEnd = std::chrono::high_resolution_clock::now(); + auto migrationTime = std::chrono::duration_cast(migrationEnd - migrationStart); + + metrics["state_migration_time_ms"] = migrationTime.count(); + ASSERT_TRUE(migrated, "State migration successful"); + ASSERT_LT(migrationTime.count(), 500, "Migration time < 500ms"); + + engine.setTrafficSplit("GameLogic", {{1, 0.0f}, {2, 1.0f}}); + + split = engine.getTrafficSplit("GameLogic"); + ASSERT_NEAR(split[2], 100.0f, 2.0f, "v2 traffic ~100%"); + + std::cout << "✓ Progressive migration complete\n"; + + std::this_thread::sleep_for(std::chrono::seconds(3)); + + // ======================================== + std::cout << "\n=== Phase 3: Garbage Collection (unload v1) ===\n"; + + std::cout << "v1 unused for 10s → triggering auto GC...\n"; + + // Wait for GC threshold + std::this_thread::sleep_for(std::chrono::seconds(12)); + + auto gcStart = std::chrono::high_resolution_clock::now(); + engine.autoGC(10.0f); + auto gcEnd = std::chrono::high_resolution_clock::now(); + auto gcTime = std::chrono::duration_cast(gcEnd - gcStart); + + metrics["gc_time_ms"] = gcTime.count(); + ASSERT_LT(gcTime.count(), 100, "GC time < 100ms"); + + // Verify v1 unloaded + ASSERT_FALSE(engine.isVersionLoaded("GameLogic", 1), "v1 unloaded"); + ASSERT_TRUE(engine.isVersionLoaded("GameLogic", 2), "v2 still loaded"); + + std::cout << "✓ Auto GC complete: v1 unloaded, v2 continues\n"; + + std::this_thread::sleep_for(std::chrono::seconds(2)); + + // ======================================== + std::cout << "\n=== Phase 4: Emergency Rollback (v2 → v1) ===\n"; + + std::cout << "Simulating critical bug in v2 → triggering emergency rollback\n"; + + auto rollbackStart = std::chrono::high_resolution_clock::now(); + + // Reload v1 + ASSERT_TRUE(engine.loadModuleVersion("GameLogic", 1, v1Path), + "Reload v1 for rollback"); + + // Migrate state back v2 → v1 (if possible, otherwise fresh start) + // Note: v1 cannot migrate from v2 (fields missing), so this will fail gracefully + engine.migrateState("GameLogic", 2, 1); + + // Redirect traffic to v1 + engine.setTrafficSplit("GameLogic", {{1, 1.0f}, {2, 0.0f}}); + + auto rollbackEnd = std::chrono::high_resolution_clock::now(); + auto rollbackTime = std::chrono::duration_cast(rollbackEnd - rollbackStart); + + metrics["rollback_time_ms"] = rollbackTime.count(); + ASSERT_LT(rollbackTime.count(), 300, "Rollback time < 300ms"); + + // Verify rollback + ASSERT_TRUE(engine.isVersionLoaded("GameLogic", 1), "v1 reloaded"); + split = engine.getTrafficSplit("GameLogic"); + ASSERT_NEAR(split[1], 100.0f, 2.0f, "v1 traffic ~100%"); + + std::cout << "✓ Emergency rollback complete in " << rollbackTime.count() << "ms\n"; + + std::this_thread::sleep_for(std::chrono::seconds(3)); + + // ======================================== + std::cout << "\n=== Phase 5: Three-Way Coexistence (v1, v2, v3) ===\n"; + + // Load v3 + std::string v3Path = "./libGameLogicModuleV3.dll"; + ASSERT_TRUE(engine.loadModuleVersion("GameLogic", 3, v3Path), + "Load GameLogic v3"); + + // Configure 3-way split: 20% v1, 30% v2, 50% v3 + engine.setTrafficSplit("GameLogic", {{1, 0.2f}, {2, 0.3f}, {3, 0.5f}}); + + // Verify 3 versions coexisting + ASSERT_TRUE(engine.isVersionLoaded("GameLogic", 1), "v1 loaded"); + ASSERT_TRUE(engine.isVersionLoaded("GameLogic", 2), "v2 loaded"); + ASSERT_TRUE(engine.isVersionLoaded("GameLogic", 3), "v3 loaded"); + + auto versions = engine.getLoadedVersions("GameLogic"); + ASSERT_EQ(versions.size(), 3, "3 versions loaded"); + + split = engine.getTrafficSplit("GameLogic"); + ASSERT_NEAR(split[1], 20.0f, 2.0f, "v1 traffic ~20%"); + ASSERT_NEAR(split[2], 30.0f, 2.0f, "v2 traffic ~30%"); + ASSERT_NEAR(split[3], 50.0f, 2.0f, "v3 traffic ~50%"); + + metrics["multi_version_count"] = versions.size(); + + std::cout << "✓ Three-way coexistence active:\n"; + std::cout << " v1: 20% traffic\n"; + std::cout << " v2: 30% traffic\n"; + std::cout << " v3: 50% traffic\n"; + + std::this_thread::sleep_for(std::chrono::seconds(5)); + + // ======================================== + std::cout << "\n=== Metrics Summary ===\n"; + reporter.addMetric("version_load_time_ms", metrics["version_load_time_ms"]); + reporter.addMetric("state_migration_time_ms", metrics["state_migration_time_ms"]); + reporter.addMetric("rollback_time_ms", metrics["rollback_time_ms"]); + reporter.addMetric("gc_time_ms", metrics["gc_time_ms"]); + reporter.addMetric("multi_version_count", metrics["multi_version_count"]); + + // Validate metrics + ASSERT_LT(metrics["version_load_time_ms"], 200.0, + "Version load time < 200ms"); + ASSERT_LT(metrics["state_migration_time_ms"], 500.0, + "State migration < 500ms"); + ASSERT_LT(metrics["rollback_time_ms"], 300.0, + "Rollback time < 300ms"); + ASSERT_LT(metrics["gc_time_ms"], 100.0, + "GC time < 100ms"); + ASSERT_EQ(metrics["multi_version_count"], 3.0, + "3 versions coexisting"); + + // ======================================== + std::cout << "\n=== Final Report ===\n"; + reporter.printFinalReport(); + + return reporter.getExitCode(); + + } catch (const std::exception& e) { + std::cerr << "Exception: " << e.what() << "\n"; + return 1; + } +} diff --git a/tests/integration/test_11_io_system.cpp b/tests/integration/test_11_io_system.cpp index 729f4bd..02a3268 100644 --- a/tests/integration/test_11_io_system.cpp +++ b/tests/integration/test_11_io_system.cpp @@ -1,491 +1,491 @@ -/** - * Scenario 11: IO System Stress Test - * - * Tests IntraIO pub/sub system with: - * - Basic publish/subscribe - * - Pattern matching with wildcards - * - Multi-module routing (1-to-many) - * - Message batching (low-frequency subscriptions) - * - Backpressure and queue overflow - * - Thread safety - * - Health monitoring - * - * Known bug to validate: IntraIOManager may route only to first subscriber (std::move limitation) - */ - -#include "grove/IModule.h" -#include "grove/IOFactory.h" -#include "grove/IntraIOManager.h" -#include "grove/JsonDataNode.h" -#include "../helpers/TestMetrics.h" -#include "../helpers/TestAssertions.h" -#include "../helpers/TestReporter.h" - -#ifdef _WIN32 -#include -#else -#include -#endif -#include -#include -#include -#include -#include -#include - -// Cross-platform dlopen wrappers -#ifdef _WIN32 -inline void* grove_dlopen(const char* path, int flags) { - (void)flags; - return LoadLibraryA(path); -} -inline void* grove_dlsym(void* handle, const char* symbol) { - return (void*)GetProcAddress((HMODULE)handle, symbol); -} -inline int grove_dlclose(void* handle) { - return FreeLibrary((HMODULE)handle) ? 0 : -1; -} -inline const char* grove_dlerror() { - static thread_local char buf[256]; - DWORD err = GetLastError(); - snprintf(buf, sizeof(buf), "Windows error code: %lu", err); - return buf; -} -#define RTLD_NOW 0 -#define RTLD_LOCAL 0 -#else -#define grove_dlopen dlopen -#define grove_dlsym dlsym -#define grove_dlclose dlclose -#define grove_dlerror dlerror -#endif - -using namespace grove; - -// Module handle for testing -struct ModuleHandle { - void* dlHandle = nullptr; - grove::IModule* instance = nullptr; - std::unique_ptr io; - std::string modulePath; -}; - -// Simple module loader for IO testing -class IOTestEngine { -public: - IOTestEngine() {} - - ~IOTestEngine() { - for (auto& [name, handle] : modules_) { - unloadModule(name); - } - } - - bool loadModule(const std::string& name, const std::string& path) { - if (modules_.count(name) > 0) { - std::cerr << "Module " << name << " already loaded\n"; - return false; - } - - void* dlHandle = grove_dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); - if (!dlHandle) { - std::cerr << "Failed to load module " << name << ": " << grove_dlerror() << "\n"; - return false; - } - - auto createFunc = (grove::IModule* (*)())grove_dlsym(dlHandle, "createModule"); - if (!createFunc) { - std::cerr << "Failed to find createModule in " << name << ": " << grove_dlerror() << "\n"; - grove_dlclose(dlHandle); - return false; - } - - grove::IModule* instance = createFunc(); - if (!instance) { - std::cerr << "createModule returned nullptr for " << name << "\n"; - grove_dlclose(dlHandle); - return false; - } - - // Create IntraIO instance for this module - auto io = IOFactory::create("intra", name); - - ModuleHandle handle; - handle.dlHandle = dlHandle; - handle.instance = instance; - handle.io = std::move(io); - handle.modulePath = path; - - modules_[name] = std::move(handle); - - // Initialize module with IO - auto config = std::make_unique("config", nlohmann::json::object()); - instance->setConfiguration(*config, modules_[name].io.get(), nullptr); - - std::cout << " ✓ Loaded " << name << "\n"; - return true; - } - - void unloadModule(const std::string& name) { - auto it = modules_.find(name); - if (it == modules_.end()) return; - - auto& handle = it->second; - - if (handle.instance) { - handle.instance->shutdown(); - delete handle.instance; - handle.instance = nullptr; - } - - if (handle.dlHandle) { - grove_dlclose(handle.dlHandle); - handle.dlHandle = nullptr; - } - - modules_.erase(it); - } - - grove::IModule* getModule(const std::string& name) { - auto it = modules_.find(name); - return (it != modules_.end()) ? it->second.instance : nullptr; - } - - IIO* getIO(const std::string& name) { - auto it = modules_.find(name); - return (it != modules_.end()) ? it->second.io.get() : nullptr; - } - - void processAll(const IDataNode& input) { - for (auto& [name, handle] : modules_) { - if (handle.instance) { - handle.instance->process(input); - } - } - } - -private: - std::map modules_; -}; - -int main() { - TestReporter reporter("IO System Stress Test"); - TestMetrics metrics; - - std::cout << "================================================================================\n"; - std::cout << "TEST: IO System Stress Test (Scenario 11)\n"; - std::cout << "================================================================================\n\n"; - - // === SETUP === - std::cout << "Setup: Loading IO modules...\n"; - - IOTestEngine engine; - - // Load all IO test modules - bool loadSuccess = true; - loadSuccess &= engine.loadModule("ProducerModule", "./libProducerModule.so"); - loadSuccess &= engine.loadModule("ConsumerModule", "./libConsumerModule.so"); - loadSuccess &= engine.loadModule("BroadcastModule", "./libBroadcastModule.so"); - loadSuccess &= engine.loadModule("BatchModule", "./libBatchModule.so"); - loadSuccess &= engine.loadModule("IOStressModule", "./libIOStressModule.so"); - - if (!loadSuccess) { - std::cerr << "❌ Failed to load required modules\n"; - return 1; - } - - std::cout << "\n"; - - auto producerIO = engine.getIO("ProducerModule"); - auto consumerIO = engine.getIO("ConsumerModule"); - auto broadcastIO = engine.getIO("BroadcastModule"); - auto batchIO = engine.getIO("BatchModule"); - auto stressIO = engine.getIO("IOStressModule"); - - if (!producerIO || !consumerIO || !broadcastIO || !batchIO || !stressIO) { - std::cerr << "❌ Failed to get IO instances\n"; - return 1; - } - - auto emptyInput = std::make_unique("input", nlohmann::json::object()); - - // ======================================================================== - // TEST 1: Basic Publish-Subscribe - // ======================================================================== - std::cout << "=== TEST 1: Basic Publish-Subscribe ===\n"; - - // Consumer subscribes to "test:basic" - consumerIO->subscribe("test:basic"); - - // Publish 100 messages - for (int i = 0; i < 100; i++) { - auto data = std::make_unique("data", nlohmann::json{ - {"id", i}, - {"payload", "test_message_" + std::to_string(i)} - }); - producerIO->publish("test:basic", std::move(data)); - } - - // Process to allow routing - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - // Count received messages - int receivedCount = 0; - while (consumerIO->hasMessages() > 0) { - auto msg = consumerIO->pullMessage(); - receivedCount++; - } - - ASSERT_EQ(receivedCount, 100, "Should receive all 100 messages"); - reporter.addAssertion("basic_pubsub", receivedCount == 100); - reporter.addMetric("basic_pubsub_count", receivedCount); - std::cout << " ✓ Received " << receivedCount << "/100 messages\n"; - std::cout << "✓ TEST 1 PASSED\n\n"; - - // ======================================================================== - // TEST 2: Pattern Matching with Wildcards - // ======================================================================== - std::cout << "=== TEST 2: Pattern Matching ===\n"; - - // Subscribe to patterns - consumerIO->subscribe("player:.*"); - - // Publish test messages - std::vector testTopics = { - "player:001:position", - "player:001:health", - "player:002:position", - "enemy:001:position" - }; - - for (const auto& topic : testTopics) { - auto data = std::make_unique("data", nlohmann::json{{"topic", topic}}); - producerIO->publish(topic, std::move(data)); - } - - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - // Count player messages (should match 3 of 4) - int playerMsgCount = 0; - while (consumerIO->hasMessages() > 0) { - auto msg = consumerIO->pullMessage(); - if (msg.topic.find("player:") == 0) { - playerMsgCount++; - } - } - - std::cout << " Pattern 'player:.*' matched " << playerMsgCount << " messages\n"; - ASSERT_GE(playerMsgCount, 3, "Should match at least 3 player messages"); - reporter.addAssertion("pattern_matching", playerMsgCount >= 3); - reporter.addMetric("pattern_match_count", playerMsgCount); - std::cout << "✓ TEST 2 PASSED\n\n"; - - // ======================================================================== - // TEST 3: Multi-Module Routing (1-to-many) - Bug Detection - // ======================================================================== - std::cout << "=== TEST 3: Multi-Module Routing (1-to-many) ===\n"; - std::cout << " Testing for known bug: std::move limitation in routing\n"; - - // All modules subscribe to "broadcast:.*" - consumerIO->subscribe("broadcast:.*"); - broadcastIO->subscribe("broadcast:.*"); - batchIO->subscribe("broadcast:.*"); - stressIO->subscribe("broadcast:.*"); - - // Publish 10 broadcast messages - for (int i = 0; i < 10; i++) { - auto data = std::make_unique("data", nlohmann::json{{"broadcast_id", i}}); - producerIO->publish("broadcast:data", std::move(data)); - } - - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - // Check which modules received messages - int consumerReceived = consumerIO->hasMessages(); - int broadcastReceived = broadcastIO->hasMessages(); - int batchReceived = batchIO->hasMessages(); - int stressReceived = stressIO->hasMessages(); - - std::cout << " Broadcast distribution:\n"; - std::cout << " ConsumerModule: " << consumerReceived << " messages\n"; - std::cout << " BroadcastModule: " << broadcastReceived << " messages\n"; - std::cout << " BatchModule: " << batchReceived << " messages\n"; - std::cout << " IOStressModule: " << stressReceived << " messages\n"; - - int totalReceived = consumerReceived + broadcastReceived + batchReceived + stressReceived; - - if (totalReceived == 10) { - std::cout << " ⚠️ BUG CONFIRMED: Only one module received all messages\n"; - std::cout << " This confirms the clone() limitation in routing\n"; - reporter.addMetric("broadcast_bug_present", 1.0f); - } else if (totalReceived >= 40) { - std::cout << " ✓ FIXED: All modules received copies (clone() implemented!)\n"; - reporter.addMetric("broadcast_bug_present", 0.0f); - } else { - std::cout << " ⚠️ Unexpected: " << totalReceived << " messages received (expected 10 or 40)\n"; - reporter.addMetric("broadcast_bug_present", 0.5f); - } - - reporter.addAssertion("multi_module_routing_tested", true); - std::cout << "✓ TEST 3 COMPLETED (bug documented)\n\n"; - - // Clean up for next test - while (consumerIO->hasMessages() > 0) consumerIO->pullMessage(); - while (broadcastIO->hasMessages() > 0) broadcastIO->pullMessage(); - while (batchIO->hasMessages() > 0) batchIO->pullMessage(); - while (stressIO->hasMessages() > 0) stressIO->pullMessage(); - - // ======================================================================== - // TEST 4: Low-Frequency Subscriptions (Batching) - // ======================================================================== - std::cout << "=== TEST 4: Low-Frequency Subscriptions ===\n"; - - SubscriptionConfig batchConfig; - batchConfig.replaceable = true; - batchConfig.batchInterval = 1000; // 1 second - batchIO->subscribeLowFreq("batch:.*", batchConfig); - - std::cout << " Publishing 100 messages over 2 seconds...\n"; - int batchPublished = 0; - auto batchStart = std::chrono::high_resolution_clock::now(); - - for (int i = 0; i < 100; i++) { - auto data = std::make_unique("data", nlohmann::json{ - {"timestamp", i}, - {"value", i * 0.1f} - }); - producerIO->publish("batch:metric", std::move(data)); - batchPublished++; - std::this_thread::sleep_for(std::chrono::milliseconds(20)); // 50 Hz - } - - auto batchEnd = std::chrono::high_resolution_clock::now(); - float batchDuration = std::chrono::duration(batchEnd - batchStart).count(); - - // Check batched messages - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - int batchesReceived = 0; - while (batchIO->hasMessages() > 0) { - auto msg = batchIO->pullMessage(); - batchesReceived++; - } - - std::cout << " Published: " << batchPublished << " messages over " << batchDuration << "s\n"; - std::cout << " Received: " << batchesReceived << " batches\n"; - std::cout << " Expected: ~" << static_cast(batchDuration) << " batches (1/second)\n"; - - // With 1s batching, expect fewer messages than published - ASSERT_LT(batchesReceived, batchPublished, "Batching should reduce message count"); - reporter.addMetric("batch_count", batchesReceived); - reporter.addMetric("batch_published", batchPublished); - reporter.addAssertion("batching_reduces_messages", batchesReceived < batchPublished); - std::cout << "✓ TEST 4 PASSED\n\n"; - - // ======================================================================== - // TEST 5: Backpressure & Queue Overflow - // ======================================================================== - std::cout << "=== TEST 5: Backpressure & Queue Overflow ===\n"; - - consumerIO->subscribe("stress:flood"); - - std::cout << " Publishing 10000 messages without pulling...\n"; - for (int i = 0; i < 10000; i++) { - auto data = std::make_unique("data", nlohmann::json{{"flood_id", i}}); - producerIO->publish("stress:flood", std::move(data)); - } - - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - - // Check health - auto health = consumerIO->getHealth(); - std::cout << " Health status:\n"; - std::cout << " Queue size: " << health.queueSize << " / " << health.maxQueueSize << "\n"; - std::cout << " Dropping: " << (health.dropping ? "YES" : "NO") << "\n"; - std::cout << " Dropped count: " << health.droppedMessageCount << "\n"; - - ASSERT_GT(health.queueSize, 0, "Queue should have messages"); - reporter.addMetric("queue_size", health.queueSize); - reporter.addMetric("dropped_messages", health.droppedMessageCount); - reporter.addAssertion("backpressure_monitoring", true); - std::cout << "✓ TEST 5 PASSED\n\n"; - - // Clean up queue - while (consumerIO->hasMessages() > 0) consumerIO->pullMessage(); - - // ======================================================================== - // TEST 6: Thread Safety (Concurrent Pub/Pull) - // ======================================================================== - std::cout << "=== TEST 6: Thread Safety ===\n"; - - consumerIO->subscribe("thread:.*"); - - std::atomic publishedTotal{0}; - std::atomic receivedTotal{0}; - std::atomic running{true}; - - std::cout << " Launching 5 publisher threads...\n"; - std::vector publishers; - for (int t = 0; t < 5; t++) { - publishers.emplace_back([&, t]() { - for (int i = 0; i < 100; i++) { - auto data = std::make_unique("data", nlohmann::json{ - {"thread", t}, - {"id", i} - }); - producerIO->publish("thread:test", std::move(data)); - publishedTotal++; - std::this_thread::sleep_for(std::chrono::microseconds(100)); - } - }); - } - - std::cout << " Launching 3 consumer threads...\n"; - std::vector consumers; - for (int t = 0; t < 3; t++) { - consumers.emplace_back([&]() { - while (running || consumerIO->hasMessages() > 0) { - if (consumerIO->hasMessages() > 0) { - try { - auto msg = consumerIO->pullMessage(); - receivedTotal++; - } catch (...) { - // Expected: may have race conditions - } - } - std::this_thread::sleep_for(std::chrono::microseconds(500)); - } - }); - } - - // Wait for publishers - for (auto& t : publishers) { - t.join(); - } - - std::cout << " All publishers done: " << publishedTotal << " messages\n"; - - // Let consumers finish - std::this_thread::sleep_for(std::chrono::milliseconds(200)); - running = false; - - for (auto& t : consumers) { - t.join(); - } - - std::cout << " All consumers done: " << receivedTotal << " messages\n"; - - ASSERT_GT(receivedTotal, 0, "Should receive at least some messages"); - reporter.addMetric("concurrent_published", publishedTotal); - reporter.addMetric("concurrent_received", receivedTotal); - reporter.addAssertion("thread_safety", true); // No crash = success - std::cout << "✓ TEST 6 PASSED (no crashes)\n\n"; - - // ======================================================================== - // FINAL REPORT - // ======================================================================== - - metrics.printReport(); - reporter.printFinalReport(); - - return reporter.getExitCode(); -} +/** + * Scenario 11: IO System Stress Test + * + * Tests IntraIO pub/sub system with: + * - Basic publish/subscribe + * - Pattern matching with wildcards + * - Multi-module routing (1-to-many) + * - Message batching (low-frequency subscriptions) + * - Backpressure and queue overflow + * - Thread safety + * - Health monitoring + * + * Known bug to validate: IntraIOManager may route only to first subscriber (std::move limitation) + */ + +#include "grove/IModule.h" +#include "grove/IOFactory.h" +#include "grove/IntraIOManager.h" +#include "grove/JsonDataNode.h" +#include "../helpers/TestMetrics.h" +#include "../helpers/TestAssertions.h" +#include "../helpers/TestReporter.h" + +#ifdef _WIN32 +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include + +// Cross-platform dlopen wrappers +#ifdef _WIN32 +inline void* grove_dlopen(const char* path, int flags) { + (void)flags; + return LoadLibraryA(path); +} +inline void* grove_dlsym(void* handle, const char* symbol) { + return (void*)GetProcAddress((HMODULE)handle, symbol); +} +inline int grove_dlclose(void* handle) { + return FreeLibrary((HMODULE)handle) ? 0 : -1; +} +inline const char* grove_dlerror() { + static thread_local char buf[256]; + DWORD err = GetLastError(); + snprintf(buf, sizeof(buf), "Windows error code: %lu", err); + return buf; +} +#define RTLD_NOW 0 +#define RTLD_LOCAL 0 +#else +#define grove_dlopen dlopen +#define grove_dlsym dlsym +#define grove_dlclose dlclose +#define grove_dlerror dlerror +#endif + +using namespace grove; + +// Module handle for testing +struct ModuleHandle { + void* dlHandle = nullptr; + grove::IModule* instance = nullptr; + std::unique_ptr io; + std::string modulePath; +}; + +// Simple module loader for IO testing +class IOTestEngine { +public: + IOTestEngine() {} + + ~IOTestEngine() { + for (auto& [name, handle] : modules_) { + unloadModule(name); + } + } + + bool loadModule(const std::string& name, const std::string& path) { + if (modules_.count(name) > 0) { + std::cerr << "Module " << name << " already loaded\n"; + return false; + } + + void* dlHandle = grove_dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); + if (!dlHandle) { + std::cerr << "Failed to load module " << name << ": " << grove_dlerror() << "\n"; + return false; + } + + auto createFunc = (grove::IModule* (*)())grove_dlsym(dlHandle, "createModule"); + if (!createFunc) { + std::cerr << "Failed to find createModule in " << name << ": " << grove_dlerror() << "\n"; + grove_dlclose(dlHandle); + return false; + } + + grove::IModule* instance = createFunc(); + if (!instance) { + std::cerr << "createModule returned nullptr for " << name << "\n"; + grove_dlclose(dlHandle); + return false; + } + + // Create IntraIO instance for this module + auto io = IOFactory::create("intra", name); + + ModuleHandle handle; + handle.dlHandle = dlHandle; + handle.instance = instance; + handle.io = std::move(io); + handle.modulePath = path; + + modules_[name] = std::move(handle); + + // Initialize module with IO + auto config = std::make_unique("config", nlohmann::json::object()); + instance->setConfiguration(*config, modules_[name].io.get(), nullptr); + + std::cout << " ✓ Loaded " << name << "\n"; + return true; + } + + void unloadModule(const std::string& name) { + auto it = modules_.find(name); + if (it == modules_.end()) return; + + auto& handle = it->second; + + if (handle.instance) { + handle.instance->shutdown(); + delete handle.instance; + handle.instance = nullptr; + } + + if (handle.dlHandle) { + grove_dlclose(handle.dlHandle); + handle.dlHandle = nullptr; + } + + modules_.erase(it); + } + + grove::IModule* getModule(const std::string& name) { + auto it = modules_.find(name); + return (it != modules_.end()) ? it->second.instance : nullptr; + } + + IIO* getIO(const std::string& name) { + auto it = modules_.find(name); + return (it != modules_.end()) ? it->second.io.get() : nullptr; + } + + void processAll(const IDataNode& input) { + for (auto& [name, handle] : modules_) { + if (handle.instance) { + handle.instance->process(input); + } + } + } + +private: + std::map modules_; +}; + +int main() { + TestReporter reporter("IO System Stress Test"); + TestMetrics metrics; + + std::cout << "================================================================================\n"; + std::cout << "TEST: IO System Stress Test (Scenario 11)\n"; + std::cout << "================================================================================\n\n"; + + // === SETUP === + std::cout << "Setup: Loading IO modules...\n"; + + IOTestEngine engine; + + // Load all IO test modules + bool loadSuccess = true; + loadSuccess &= engine.loadModule("ProducerModule", "./libProducerModule.dll"); + loadSuccess &= engine.loadModule("ConsumerModule", "./libConsumerModule.dll"); + loadSuccess &= engine.loadModule("BroadcastModule", "./libBroadcastModule.dll"); + loadSuccess &= engine.loadModule("BatchModule", "./libBatchModule.dll"); + loadSuccess &= engine.loadModule("IOStressModule", "./libIOStressModule.dll"); + + if (!loadSuccess) { + std::cerr << "❌ Failed to load required modules\n"; + return 1; + } + + std::cout << "\n"; + + auto producerIO = engine.getIO("ProducerModule"); + auto consumerIO = engine.getIO("ConsumerModule"); + auto broadcastIO = engine.getIO("BroadcastModule"); + auto batchIO = engine.getIO("BatchModule"); + auto stressIO = engine.getIO("IOStressModule"); + + if (!producerIO || !consumerIO || !broadcastIO || !batchIO || !stressIO) { + std::cerr << "❌ Failed to get IO instances\n"; + return 1; + } + + auto emptyInput = std::make_unique("input", nlohmann::json::object()); + + // ======================================================================== + // TEST 1: Basic Publish-Subscribe + // ======================================================================== + std::cout << "=== TEST 1: Basic Publish-Subscribe ===\n"; + + // Consumer subscribes to "test:basic" + consumerIO->subscribe("test:basic"); + + // Publish 100 messages + for (int i = 0; i < 100; i++) { + auto data = std::make_unique("data", nlohmann::json{ + {"id", i}, + {"payload", "test_message_" + std::to_string(i)} + }); + producerIO->publish("test:basic", std::move(data)); + } + + // Process to allow routing + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + // Count received messages + int receivedCount = 0; + while (consumerIO->hasMessages() > 0) { + auto msg = consumerIO->pullMessage(); + receivedCount++; + } + + ASSERT_EQ(receivedCount, 100, "Should receive all 100 messages"); + reporter.addAssertion("basic_pubsub", receivedCount == 100); + reporter.addMetric("basic_pubsub_count", receivedCount); + std::cout << " ✓ Received " << receivedCount << "/100 messages\n"; + std::cout << "✓ TEST 1 PASSED\n\n"; + + // ======================================================================== + // TEST 2: Pattern Matching with Wildcards + // ======================================================================== + std::cout << "=== TEST 2: Pattern Matching ===\n"; + + // Subscribe to patterns + consumerIO->subscribe("player:.*"); + + // Publish test messages + std::vector testTopics = { + "player:001:position", + "player:001:health", + "player:002:position", + "enemy:001:position" + }; + + for (const auto& topic : testTopics) { + auto data = std::make_unique("data", nlohmann::json{{"topic", topic}}); + producerIO->publish(topic, std::move(data)); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + // Count player messages (should match 3 of 4) + int playerMsgCount = 0; + while (consumerIO->hasMessages() > 0) { + auto msg = consumerIO->pullMessage(); + if (msg.topic.find("player:") == 0) { + playerMsgCount++; + } + } + + std::cout << " Pattern 'player:.*' matched " << playerMsgCount << " messages\n"; + ASSERT_GE(playerMsgCount, 3, "Should match at least 3 player messages"); + reporter.addAssertion("pattern_matching", playerMsgCount >= 3); + reporter.addMetric("pattern_match_count", playerMsgCount); + std::cout << "✓ TEST 2 PASSED\n\n"; + + // ======================================================================== + // TEST 3: Multi-Module Routing (1-to-many) - Bug Detection + // ======================================================================== + std::cout << "=== TEST 3: Multi-Module Routing (1-to-many) ===\n"; + std::cout << " Testing for known bug: std::move limitation in routing\n"; + + // All modules subscribe to "broadcast:.*" + consumerIO->subscribe("broadcast:.*"); + broadcastIO->subscribe("broadcast:.*"); + batchIO->subscribe("broadcast:.*"); + stressIO->subscribe("broadcast:.*"); + + // Publish 10 broadcast messages + for (int i = 0; i < 10; i++) { + auto data = std::make_unique("data", nlohmann::json{{"broadcast_id", i}}); + producerIO->publish("broadcast:data", std::move(data)); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + // Check which modules received messages + int consumerReceived = consumerIO->hasMessages(); + int broadcastReceived = broadcastIO->hasMessages(); + int batchReceived = batchIO->hasMessages(); + int stressReceived = stressIO->hasMessages(); + + std::cout << " Broadcast distribution:\n"; + std::cout << " ConsumerModule: " << consumerReceived << " messages\n"; + std::cout << " BroadcastModule: " << broadcastReceived << " messages\n"; + std::cout << " BatchModule: " << batchReceived << " messages\n"; + std::cout << " IOStressModule: " << stressReceived << " messages\n"; + + int totalReceived = consumerReceived + broadcastReceived + batchReceived + stressReceived; + + if (totalReceived == 10) { + std::cout << " ⚠️ BUG CONFIRMED: Only one module received all messages\n"; + std::cout << " This confirms the clone() limitation in routing\n"; + reporter.addMetric("broadcast_bug_present", 1.0f); + } else if (totalReceived >= 40) { + std::cout << " ✓ FIXED: All modules received copies (clone() implemented!)\n"; + reporter.addMetric("broadcast_bug_present", 0.0f); + } else { + std::cout << " ⚠️ Unexpected: " << totalReceived << " messages received (expected 10 or 40)\n"; + reporter.addMetric("broadcast_bug_present", 0.5f); + } + + reporter.addAssertion("multi_module_routing_tested", true); + std::cout << "✓ TEST 3 COMPLETED (bug documented)\n\n"; + + // Clean up for next test + while (consumerIO->hasMessages() > 0) consumerIO->pullMessage(); + while (broadcastIO->hasMessages() > 0) broadcastIO->pullMessage(); + while (batchIO->hasMessages() > 0) batchIO->pullMessage(); + while (stressIO->hasMessages() > 0) stressIO->pullMessage(); + + // ======================================================================== + // TEST 4: Low-Frequency Subscriptions (Batching) + // ======================================================================== + std::cout << "=== TEST 4: Low-Frequency Subscriptions ===\n"; + + SubscriptionConfig batchConfig; + batchConfig.replaceable = true; + batchConfig.batchInterval = 1000; // 1 second + batchIO->subscribeLowFreq("batch:.*", batchConfig); + + std::cout << " Publishing 100 messages over 2 seconds...\n"; + int batchPublished = 0; + auto batchStart = std::chrono::high_resolution_clock::now(); + + for (int i = 0; i < 100; i++) { + auto data = std::make_unique("data", nlohmann::json{ + {"timestamp", i}, + {"value", i * 0.1f} + }); + producerIO->publish("batch:metric", std::move(data)); + batchPublished++; + std::this_thread::sleep_for(std::chrono::milliseconds(20)); // 50 Hz + } + + auto batchEnd = std::chrono::high_resolution_clock::now(); + float batchDuration = std::chrono::duration(batchEnd - batchStart).count(); + + // Check batched messages + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + int batchesReceived = 0; + while (batchIO->hasMessages() > 0) { + auto msg = batchIO->pullMessage(); + batchesReceived++; + } + + std::cout << " Published: " << batchPublished << " messages over " << batchDuration << "s\n"; + std::cout << " Received: " << batchesReceived << " batches\n"; + std::cout << " Expected: ~" << static_cast(batchDuration) << " batches (1/second)\n"; + + // With 1s batching, expect fewer messages than published + ASSERT_LT(batchesReceived, batchPublished, "Batching should reduce message count"); + reporter.addMetric("batch_count", batchesReceived); + reporter.addMetric("batch_published", batchPublished); + reporter.addAssertion("batching_reduces_messages", batchesReceived < batchPublished); + std::cout << "✓ TEST 4 PASSED\n\n"; + + // ======================================================================== + // TEST 5: Backpressure & Queue Overflow + // ======================================================================== + std::cout << "=== TEST 5: Backpressure & Queue Overflow ===\n"; + + consumerIO->subscribe("stress:flood"); + + std::cout << " Publishing 10000 messages without pulling...\n"; + for (int i = 0; i < 10000; i++) { + auto data = std::make_unique("data", nlohmann::json{{"flood_id", i}}); + producerIO->publish("stress:flood", std::move(data)); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + + // Check health + auto health = consumerIO->getHealth(); + std::cout << " Health status:\n"; + std::cout << " Queue size: " << health.queueSize << " / " << health.maxQueueSize << "\n"; + std::cout << " Dropping: " << (health.dropping ? "YES" : "NO") << "\n"; + std::cout << " Dropped count: " << health.droppedMessageCount << "\n"; + + ASSERT_GT(health.queueSize, 0, "Queue should have messages"); + reporter.addMetric("queue_size", health.queueSize); + reporter.addMetric("dropped_messages", health.droppedMessageCount); + reporter.addAssertion("backpressure_monitoring", true); + std::cout << "✓ TEST 5 PASSED\n\n"; + + // Clean up queue + while (consumerIO->hasMessages() > 0) consumerIO->pullMessage(); + + // ======================================================================== + // TEST 6: Thread Safety (Concurrent Pub/Pull) + // ======================================================================== + std::cout << "=== TEST 6: Thread Safety ===\n"; + + consumerIO->subscribe("thread:.*"); + + std::atomic publishedTotal{0}; + std::atomic receivedTotal{0}; + std::atomic running{true}; + + std::cout << " Launching 5 publisher threads...\n"; + std::vector publishers; + for (int t = 0; t < 5; t++) { + publishers.emplace_back([&, t]() { + for (int i = 0; i < 100; i++) { + auto data = std::make_unique("data", nlohmann::json{ + {"thread", t}, + {"id", i} + }); + producerIO->publish("thread:test", std::move(data)); + publishedTotal++; + std::this_thread::sleep_for(std::chrono::microseconds(100)); + } + }); + } + + std::cout << " Launching 3 consumer threads...\n"; + std::vector consumers; + for (int t = 0; t < 3; t++) { + consumers.emplace_back([&]() { + while (running || consumerIO->hasMessages() > 0) { + if (consumerIO->hasMessages() > 0) { + try { + auto msg = consumerIO->pullMessage(); + receivedTotal++; + } catch (...) { + // Expected: may have race conditions + } + } + std::this_thread::sleep_for(std::chrono::microseconds(500)); + } + }); + } + + // Wait for publishers + for (auto& t : publishers) { + t.join(); + } + + std::cout << " All publishers done: " << publishedTotal << " messages\n"; + + // Let consumers finish + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + running = false; + + for (auto& t : consumers) { + t.join(); + } + + std::cout << " All consumers done: " << receivedTotal << " messages\n"; + + ASSERT_GT(receivedTotal, 0, "Should receive at least some messages"); + reporter.addMetric("concurrent_published", publishedTotal); + reporter.addMetric("concurrent_received", receivedTotal); + reporter.addAssertion("thread_safety", true); // No crash = success + std::cout << "✓ TEST 6 PASSED (no crashes)\n\n"; + + // ======================================================================== + // FINAL REPORT + // ======================================================================== + + metrics.printReport(); + reporter.printFinalReport(); + + return reporter.getExitCode(); +} diff --git a/tests/modules/TankModule.h b/tests/modules/TankModule.h index ac52606..529fc72 100644 --- a/tests/modules/TankModule.h +++ b/tests/modules/TankModule.h @@ -32,7 +32,7 @@ public: private: std::vector tanks; int frameCount = 0; - std::string moduleVersion = "v1.0";std::shared_ptr logger; + std::string moduleVersion = "v2.0 HOT-RELOADED";:shared_ptr logger; std::unique_ptr config; void updateTank(Tank& tank, float dt); diff --git a/tests/modules/TestModule.cpp b/tests/modules/TestModule.cpp index 79ea062..d42813d 100644 --- a/tests/modules/TestModule.cpp +++ b/tests/modules/TestModule.cpp @@ -5,7 +5,7 @@ #include // This line will be modified by AutoCompiler during race condition tests -std::string moduleVersion = "v1"; +std::string moduleVersion = "v10"; namespace grove {