GroveEngine/include/grove/ModuleLoader.h
StillHammer d8c5f93429 feat: Add comprehensive hot-reload test suite with 3 integration scenarios
This commit implements a complete test infrastructure for validating
hot-reload stability and robustness across multiple scenarios.

## New Test Infrastructure

### Test Helpers (tests/helpers/)
- TestMetrics: FPS, memory, reload time tracking with statistics
- TestReporter: Assertion tracking and formatted test reports
- SystemUtils: Memory usage monitoring via /proc/self/status
- TestAssertions: Macro-based assertion framework

### Test Modules
- TankModule: Realistic module with 50 tanks for production testing
- ChaosModule: Crash-injection module for robustness validation
- StressModule: Lightweight module for long-duration stability tests

## Integration Test Scenarios

### Scenario 1: Production Hot-Reload (test_01_production_hotreload.cpp)
 PASSED - End-to-end hot-reload validation
- 30 seconds simulation (1800 frames @ 60 FPS)
- TankModule with 50 tanks, realistic state
- Source modification (v1.0 → v2.0), recompilation, reload
- State preservation: positions, velocities, frameCount
- Metrics: ~163ms reload time, 0.88MB memory growth

### Scenario 2: Chaos Monkey (test_02_chaos_monkey.cpp)
 PASSED - Extreme robustness testing
- 150+ random crashes per run (5% crash probability per frame)
- 5 crash types: runtime_error, logic_error, out_of_range, domain_error, state corruption
- 100% recovery rate via automatic hot-reload
- Corrupted state detection and rejection
- Random seed for unpredictable crash patterns
- Proof of real reload: temporary files in /tmp/grove_module_*.so

### Scenario 3: Stress Test (test_03_stress_test.cpp)
 PASSED - Long-duration stability validation
- 10 minutes simulation (36000 frames @ 60 FPS)
- 120 hot-reloads (every 5 seconds)
- 100% reload success rate (120/120)
- Memory growth: 2 MB (threshold: 50 MB)
- Avg reload time: 160ms (threshold: 500ms)
- No memory leaks, no file descriptor leaks

## Core Engine Enhancements

### ModuleLoader (src/ModuleLoader.cpp)
- Temporary file copy to /tmp/ for Linux dlopen cache bypass
- Robust reload() method: getState() → unload() → load() → setState()
- Automatic cleanup of temporary files
- Comprehensive error handling and logging

### DebugEngine (src/DebugEngine.cpp)
- Automatic recovery in processModuleSystems()
- Exception catching → logging → module reload → continue
- Module state dump utilities for debugging

### SequentialModuleSystem (src/SequentialModuleSystem.cpp)
- extractModule() for safe module extraction
- registerModule() for module re-registration
- Enhanced processModules() with error handling

## Build System
- CMake configuration for test infrastructure
- Shared library compilation for test modules (.so)
- CTest integration for all scenarios
- PIC flag management for spdlog compatibility

## Documentation (planTI/)
- Complete test architecture documentation
- Detailed scenario specifications with success criteria
- Global test plan and validation thresholds

## Validation Results
All 3 integration scenarios pass successfully:
- Production hot-reload: State preservation validated
- Chaos Monkey: 100% recovery from 150+ crashes
- Stress Test: Stable over 120 reloads, minimal memory growth

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-13 22:13:07 +08:00

109 lines
3.3 KiB
C++

#pragma once
#include <string>
#include <memory>
#include <functional>
#include <dlfcn.h>
#include <spdlog/spdlog.h>
#include "IModule.h"
namespace grove {
/**
* @brief Handles dynamic loading/unloading of module .so files
*
* ModuleLoader provides:
* - Dynamic library loading with dlopen
* - Module factory function resolution
* - State preservation across reloads
* - Hot-reload capability with <1ms latency
* - Comprehensive error handling and logging
*/
class ModuleLoader {
private:
std::shared_ptr<spdlog::logger> logger;
void* libraryHandle = nullptr;
std::string libraryPath;
std::string moduleName;
std::string tempLibraryPath; // Temp copy path for hot-reload cache bypass
// Factory function signature: IModule* createModule()
using CreateModuleFunc = IModule* (*)();
CreateModuleFunc createFunc = nullptr;
void logLoadStart(const std::string& path);
void logLoadSuccess(float loadTime);
void logLoadError(const std::string& error);
void logUnloadStart();
void logUnloadSuccess();
public:
ModuleLoader();
~ModuleLoader();
/**
* @brief Load a module from .so file
* @param path Path to .so file
* @param moduleName Name for logging/identification
* @param isReload If true, use temp copy to bypass dlopen cache (default: false)
* @return Unique pointer to loaded module
* @throws std::runtime_error if loading fails
*/
std::unique_ptr<IModule> load(const std::string& path, const std::string& name, bool isReload = false);
/**
* @brief Unload currently loaded module
* Closes the library handle and frees resources
*/
void unload();
/**
* @brief Hot-reload: save state, unload, reload, restore state
* @param currentModule Module with state to preserve
* @return New module instance with preserved state
* @throws std::runtime_error if reload fails
*
* This is the core hot-reload operation:
* 1. Extract state from old module
* 2. Unload old .so
* 3. Load new .so (recompiled)
* 4. Inject state into new module
*/
std::unique_ptr<IModule> reload(std::unique_ptr<IModule> currentModule);
/**
* @brief Check if a module is currently loaded
*/
bool isLoaded() const { return libraryHandle != nullptr; }
/**
* @brief Get path to currently loaded module
*/
const std::string& getLoadedPath() const { return libraryPath; }
/**
* @brief Get name of currently loaded module
*/
const std::string& getModuleName() const { return moduleName; }
/**
* @brief Wait for module to reach clean state (idle + no pending tasks)
* @param module Module to wait for
* @param moduleSystem Module system to check for pending tasks
* @param timeoutSeconds Maximum time to wait in seconds (default: 5.0)
* @return True if clean state reached, false if timeout
*
* Used by hot-reload to ensure safe reload timing. Waits until:
* - module->isIdle() returns true
* - moduleSystem->getPendingTaskCount() returns 0
*/
bool waitForCleanState(
IModule* module,
class IModuleSystem* moduleSystem,
float timeoutSeconds = 5.0f
);
};
} // namespace grove