GroveEngine/tests/integration/test_05_memory_leak.cpp
StillHammer 9105610b29 feat: Add integration tests 8-10 & fix CTest configuration
Added three new integration test scenarios:
- Test 08: Config Hot-Reload (dynamic configuration updates)
- Test 09: Module Dependencies (dependency injection & cascade reload)
- Test 10: Multi-Version Coexistence (canary deployment & progressive migration)

Fixes:
- Fixed CTest working directory for all tests (add WORKING_DIRECTORY)
- Fixed module paths to use relative paths (./ prefix)
- Fixed IModule.h comments for clarity

New test modules:
- ConfigurableModule (for config reload testing)
- BaseModule, DependentModule, IndependentModule (for dependency testing)
- GameLogicModuleV1/V2/V3 (for multi-version testing)

Test coverage now includes 10 comprehensive integration scenarios covering
hot-reload, chaos testing, stress testing, race conditions, memory leaks,
error recovery, limits, config reload, dependencies, and multi-versioning.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 07:34:15 +08:00

429 lines
16 KiB
C++

// ============================================================================
// 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 <spdlog/spdlog.h>
#include <iostream>
#include <iomanip>
#include <thread>
#include <atomic>
#include <chrono>
#include <vector>
#include <filesystem>
#include <cmath>
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<bool> g_running{true};
std::atomic<int> g_reloadCount{0};
std::atomic<int> g_reloadSuccesses{0};
std::atomic<int> g_reloadFailures{0};
std::atomic<int> g_crashes{0};
std::vector<MemorySnapshot> 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<std::mutex> lock(moduleSystemMutex);
// Extract current module and save state
auto module = moduleSystem->extractModule();
auto state = module->getState();
auto config = std::make_unique<JsonDataNode>("config",
dynamic_cast<const JsonDataNode&>(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, "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<std::mutex> 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<float>(now - lastFrame).count();
if (deltaTime >= frameTime) {
try {
std::lock_guard<std::mutex> lock(moduleSystemMutex);
moduleSystem->processModules(deltaTime);
frameCount++;
} catch (...) {
g_crashes++;
}
lastFrame = now;
} else {
// Sleep for remaining time
int sleepMs = static_cast<int>((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<SequentialModuleSystem>();
std::mutex moduleSystemMutex;
// Disable verbose logging for performance
moduleSystem->setLogLevel(spdlog::level::err);
// Load initial module
try {
auto module = loader.load(modulePath, "LeakTestModule", false);
if (!module) {
std::cerr << "❌ Failed to load LeakTestModule\n";
return 1;
}
nlohmann::json configJson = nlohmann::json::object();
auto config = std::make_unique<JsonDataNode>("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<float>(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<int>(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;
}