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

219 lines
6.5 KiB
C++

#include "TankModule.h"
#include "grove/JsonDataNode.h"
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <cmath>
namespace grove {
void TankModule::setConfiguration(const IDataNode& configNode, IIO* io, ITaskScheduler* scheduler) {
// Logger
logger = spdlog::get("TankModule");
if (!logger) {
logger = spdlog::stdout_color_mt("TankModule");
}
logger->set_level(spdlog::level::debug);
// Clone config en JSON (pour pouvoir retourner dans getConfiguration)
const auto* jsonConfigNode = dynamic_cast<const JsonDataNode*>(&configNode);
if (jsonConfigNode) {
config = std::make_unique<JsonDataNode>("config", jsonConfigNode->getJsonData());
} else {
// Fallback: créer un config vide
config = std::make_unique<JsonDataNode>("config");
}
// Lire config
int tankCount = configNode.getInt("tankCount", 50);
moduleVersion = configNode.getString("version", moduleVersion);
logger->info("Initializing TankModule {}", moduleVersion);
logger->info(" Tank count: {}", tankCount);
// Spawner tanks
spawnTanks(tankCount);
frameCount = 0;
}
const IDataNode& TankModule::getConfiguration() {
return *config;
}
void TankModule::process(const IDataNode& input) {
// Extract deltaTime from input (assurez-vous que l'input contient "deltaTime")
float deltaTime = static_cast<float>(input.getDouble("deltaTime", 1.0 / 60.0));
frameCount++;
// Update tous les tanks
for (auto& tank : tanks) {
updateTank(tank, deltaTime);
}
// Log toutes les 60 frames (1 seconde)
if (frameCount % 60 == 0) {
logger->trace("Frame {}: {} tanks active", frameCount, tanks.size());
}
}
std::unique_ptr<IDataNode> TankModule::getHealthStatus() {
nlohmann::json healthJson;
healthJson["status"] = "healthy";
healthJson["tankCount"] = tanks.size();
healthJson["frameCount"] = frameCount;
auto health = std::make_unique<JsonDataNode>("health", healthJson);
return health;
}
void TankModule::shutdown() {
logger->info("Shutting down TankModule");
tanks.clear();
}
std::string TankModule::getType() const {
return "tank";
}
std::unique_ptr<IDataNode> TankModule::getState() {
nlohmann::json json;
json["version"] = moduleVersion;
json["frameCount"] = frameCount;
// Sérialiser tanks
nlohmann::json tanksJson = nlohmann::json::array();
for (const auto& tank : tanks) {
tanksJson.push_back({
{"id", tank.id},
{"x", tank.x},
{"y", tank.y},
{"vx", tank.vx},
{"vy", tank.vy},
{"cooldown", tank.cooldown},
{"targetX", tank.targetX},
{"targetY", tank.targetY}
});
}
json["tanks"] = tanksJson;
return std::make_unique<JsonDataNode>("state", json);
}
void TankModule::setState(const IDataNode& state) {
// Cast to JsonDataNode to access underlying JSON
const auto* jsonNode = dynamic_cast<const JsonDataNode*>(&state);
if (!jsonNode) {
logger->error("setState: Invalid state (not JsonDataNode)");
return;
}
const auto& json = jsonNode->getJsonData();
// Ensure logger is initialized (needed after hot-reload)
if (!logger) {
logger = spdlog::get("TankModule");
if (!logger) {
logger = spdlog::stdout_color_mt("TankModule");
}
}
// Ensure config is initialized (needed after hot-reload)
if (!config) {
config = std::make_unique<JsonDataNode>("config");
}
// Restaurer state
// NOTE: Ne pas restaurer moduleVersion depuis le state!
// La version vient du CODE, pas du state. C'est voulu pour le hot-reload.
// moduleVersion est déjà initialisé à "v1.0" ou "v2.0 HOT-RELOADED" par le code
frameCount = json.value("frameCount", 0);
// Restaurer tanks
tanks.clear();
if (json.contains("tanks") && json["tanks"].is_array()) {
for (const auto& tankJson : json["tanks"]) {
Tank tank;
tank.id = tankJson.value("id", 0);
tank.x = tankJson.value("x", 0.0f);
tank.y = tankJson.value("y", 0.0f);
tank.vx = tankJson.value("vx", 0.0f);
tank.vy = tankJson.value("vy", 0.0f);
tank.cooldown = tankJson.value("cooldown", 0.0f);
tank.targetX = tankJson.value("targetX", 0.0f);
tank.targetY = tankJson.value("targetY", 0.0f);
tanks.push_back(tank);
}
}
logger->info("State restored: {} tanks, frame {}", tanks.size(), frameCount);
}
void TankModule::updateTank(Tank& tank, float dt) {
// Update position
tank.x += tank.vx * dt;
tank.y += tank.vy * dt;
// Bounce sur les bords (map 100x100)
if (tank.x < 0.0f || tank.x > 100.0f) {
tank.vx = -tank.vx;
tank.x = std::clamp(tank.x, 0.0f, 100.0f);
}
if (tank.y < 0.0f || tank.y > 100.0f) {
tank.vy = -tank.vy;
tank.y = std::clamp(tank.y, 0.0f, 100.0f);
}
// Update cooldown
if (tank.cooldown > 0.0f) {
tank.cooldown -= dt;
}
// Déplacer vers target
float dx = tank.targetX - tank.x;
float dy = tank.targetY - tank.y;
float dist = std::sqrt(dx * dx + dy * dy);
if (dist > 0.1f) {
// Normaliser et appliquer velocity
float speed = std::sqrt(tank.vx * tank.vx + tank.vy * tank.vy);
tank.vx = (dx / dist) * speed;
tank.vy = (dy / dist) * speed;
}
}
void TankModule::spawnTanks(int count) {
std::mt19937 rng(42); // Seed fixe pour reproductibilité
std::uniform_real_distribution<float> posDist(0.0f, 100.0f);
std::uniform_real_distribution<float> velDist(-5.0f, 5.0f);
std::uniform_real_distribution<float> cooldownDist(0.0f, 5.0f);
for (int i = 0; i < count; i++) {
Tank tank;
tank.id = i;
tank.x = posDist(rng);
tank.y = posDist(rng);
tank.vx = velDist(rng);
tank.vy = velDist(rng);
tank.cooldown = cooldownDist(rng);
tank.targetX = posDist(rng);
tank.targetY = posDist(rng);
tanks.push_back(tank);
}
logger->debug("Spawned {} tanks", count);
}
} // namespace grove
// Export symbols
extern "C" {
grove::IModule* createModule() {
return new grove::TankModule();
}
void destroyModule(grove::IModule* module) {
delete module;
}
}