GroveEngine/tests/modules/TestModule.cpp
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

121 lines
3.7 KiB
C++

#include <grove/IModule.h>
#include <grove/JsonDataNode.h>
#include <grove/JsonDataValue.h>
#include <iostream>
#include <memory>
namespace grove {
/**
* @brief Simple test module for hot-reload validation
*
* This module demonstrates:
* - State preservation across reloads
* - IDataNode-based configuration
* - Simple counter logic
*/
class TestModule : public IModule {
private:
int counter = 0;
std::string moduleVersion = "v2.0 RELOADED";
IIO* io = nullptr;
ITaskScheduler* scheduler = nullptr;
std::unique_ptr<IDataNode> config;
public:
TestModule() {
std::cout << "[TestModule] Constructor called - " << moduleVersion << std::endl;
}
~TestModule() override {
std::cout << "[TestModule] Destructor called" << std::endl;
}
void process(const IDataNode& input) override {
counter++;
std::cout << "[TestModule] Process #" << counter
<< " - Version: " << moduleVersion << std::endl;
// Print input if available
std::string message = input.getString("message", "");
if (!message.empty()) {
std::cout << "[TestModule] Received message: " << message << std::endl;
}
}
void setConfiguration(const IDataNode& configNode, IIO* ioPtr, ITaskScheduler* schedulerPtr) override {
std::cout << "[TestModule] Configuration set" << std::endl;
this->io = ioPtr;
this->scheduler = schedulerPtr;
// Clone configuration for storage
config = std::make_unique<JsonDataNode>("config", nlohmann::json::object());
// Extract version if available (use current moduleVersion as default)
moduleVersion = configNode.getString("version", moduleVersion);
std::cout << "[TestModule] Version set to: " << moduleVersion << std::endl;
}
const IDataNode& getConfiguration() override {
if (!config) {
config = std::make_unique<JsonDataNode>("config", nlohmann::json::object());
}
return *config;
}
std::unique_ptr<IDataNode> getHealthStatus() override {
nlohmann::json health = {
{"status", "healthy"},
{"counter", counter},
{"version", moduleVersion}
};
return std::make_unique<JsonDataNode>("health", health);
}
void shutdown() override {
std::cout << "[TestModule] Shutdown called - Counter at: " << counter << std::endl;
}
std::unique_ptr<IDataNode> getState() override {
std::cout << "[TestModule] getState() - Saving counter: " << counter << std::endl;
nlohmann::json state = {
{"counter", counter},
{"version", moduleVersion}
};
return std::make_unique<JsonDataNode>("state", state);
}
void setState(const IDataNode& state) override {
counter = state.getInt("counter", 0);
std::cout << "[TestModule] setState() - Restored counter: " << counter << std::endl;
std::string oldVersion = state.getString("version", "unknown");
std::cout << "[TestModule] setState() - Previous version was: " << oldVersion << std::endl;
}
std::string getType() const override {
return "TestModule";
}
bool isIdle() const override {
// TestModule has no async operations, always idle
return true;
}
};
} // namespace grove
// Module factory function - required for dynamic loading
extern "C" {
grove::IModule* createModule() {
return new grove::TestModule();
}
void destroyModule(grove::IModule* module) {
delete module;
}
}