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>
121 lines
3.7 KiB
C++
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;
|
|
}
|
|
}
|