feat: Add Scenario 4 - Race Condition Hunter test suite

Add comprehensive concurrent compilation and hot-reload testing infrastructure
to validate thread safety and file stability during race conditions.

## New Components

### AutoCompiler Helper (tests/helpers/AutoCompiler.{h,cpp})
- Automatically modifies source files to bump version numbers
- Compiles modules repeatedly on separate thread (15 iterations @ 1s interval)
- Tracks compilation success/failure rates with atomic counters
- Thread-safe compilation statistics

### Race Condition Test (tests/integration/test_04_race_condition.cpp)
- **3 concurrent threads:**
  - Compiler: Recompiles TestModule.so every 1 second
  - FileWatcher: Detects .so changes and triggers hot-reload with mutex protection
  - Engine: Runs at 60 FPS with try_lock to skip frames during reload
- Validates module integrity (health status, version, configuration)
- Tracks metrics: compilation rate, reload success, corrupted loads, crashes
- 90-second timeout with progress monitoring

### TestModule Enhancements (tests/modules/TestModule.cpp)
- Added global moduleVersion variable for AutoCompiler modification
- Version bumping support for reload validation

## Test Results (Initial Implementation)

```
Duration: 88s
Compilations:  15/15 (100%) 
Reloads:       ~30 (100% success) 
Corrupted:     0 
Crashes:       0 
File Stability: 328ms avg (proves >100ms wait) 
```

## Known Issue (To Fix in Next Commit)
- Module versions not actually changing during reload
- setConfiguration() overwrites compiled version
- Reload mechanism validated but version bumping needs fix

## Files Modified
- tests/CMakeLists.txt: Add AutoCompiler to helpers, add test_04
- tests/modules/TestModule.cpp: Add version bumping support
- .gitignore: Add build/ and logs/

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
StillHammer 2025-11-15 10:55:44 +08:00
parent d8c5f93429
commit 484b9ab5d4
9 changed files with 708 additions and 1 deletions

View File

@ -0,0 +1,33 @@
# Claude Code Configuration Backups
Ce répertoire contient les sauvegardes des modifications apportées aux fichiers de configuration de Claude Code.
## Modifications effectuées
### 2025-11-15 - Désactivation du serveur MCP Blender
**Fichier modifié :** `%APPDATA%\Claude\claude_code_config.json`
**Raison :** Réduire l'usage de contexte (économie de ~5.9% / 11.9k tokens)
**Modification :**
- ❌ Supprimé : Serveur MCP `blender` (17 outils)
- ✅ Conservé : Serveur MCP `n8n-local`
**Sauvegarde :** `claude_code_config_backup_2025-11-15.json` (version originale avec Blender)
**Pour restaurer Blender :**
```bash
# Copier la sauvegarde vers le fichier de config
cp .claude/config_backups/claude_code_config_backup_2025-11-15.json \
"/mnt/c/Users/Alexis Trouvé/AppData/Roaming/Claude/claude_code_config.json"
# Puis redémarrer Claude Code
```
---
**Impact :**
- Avant : 17 outils MCP Blender chargés (11.9k tokens)
- Après : 0 outils Blender (gain de ~6% de contexte)
- Serveur n8n-local toujours actif

View File

@ -0,0 +1,25 @@
{
"mcpServers": {
"n8n-local": {
"command": "npx",
"args": [
"-y",
"supergateway@latest",
"--streamableHttp",
"http://localhost:3333/mcp",
"--oauth2Bearer",
"mon-super-token-secret-123",
"--logLevel",
"debug"
]
},
"blender": {
"command": "cmd",
"args": [
"/c",
"uvx",
"blender-mcp"
]
}
}
}

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
build/
logs/

View File

@ -65,6 +65,7 @@ add_library(test_helpers STATIC
helpers/TestMetrics.cpp
helpers/TestReporter.cpp
helpers/SystemUtils.cpp
helpers/AutoCompiler.cpp
)
target_include_directories(test_helpers PUBLIC
@ -162,3 +163,20 @@ add_dependencies(test_03_stress_test StressModule)
# CTest integration
add_test(NAME StressTest COMMAND test_03_stress_test)
# Test 04: Race Condition Hunter - Concurrent compilation & reload
add_executable(test_04_race_condition
integration/test_04_race_condition.cpp
)
target_link_libraries(test_04_race_condition PRIVATE
test_helpers
GroveEngine::core
GroveEngine::impl
)
# This test uses TestModule (not TankModule)
add_dependencies(test_04_race_condition TestModule)
# CTest integration
add_test(NAME RaceConditionHunter COMMAND test_04_race_condition)

View File

@ -0,0 +1,112 @@
#include "AutoCompiler.h"
#include <fstream>
#include <sstream>
#include <regex>
#include <filesystem>
#include <chrono>
#include <iostream>
#include <cstdlib>
namespace TestHelpers {
AutoCompiler::AutoCompiler(const std::string& moduleName,
const std::string& buildDir,
const std::string& sourcePath)
: moduleName_(moduleName)
, buildDir_(buildDir)
, sourcePath_(sourcePath)
{
}
AutoCompiler::~AutoCompiler() {
stop();
}
void AutoCompiler::start(int iterations, int intervalMs) {
if (running_.load()) {
return; // Already running
}
running_ = true;
compilationThread_ = std::thread(&AutoCompiler::compilationLoop, this, iterations, intervalMs);
}
void AutoCompiler::stop() {
running_ = false;
if (compilationThread_.joinable()) {
compilationThread_.join();
}
}
void AutoCompiler::waitForCompletion() {
if (compilationThread_.joinable()) {
compilationThread_.join();
}
}
void AutoCompiler::modifySourceVersion(int iteration) {
// Read entire file
std::ifstream inFile(sourcePath_);
if (!inFile.is_open()) {
std::cerr << "[AutoCompiler] Failed to open source file: " << sourcePath_ << std::endl;
return;
}
std::stringstream buffer;
buffer << inFile.rdbuf();
inFile.close();
std::string content = buffer.str();
// Replace version string: moduleVersion = "vX" → moduleVersion = "vITERATION"
std::regex versionRegex(R"(std::string\s+moduleVersion\s*=\s*"v\d+")");
std::string newVersion = "std::string moduleVersion = \"v" + std::to_string(iteration) + "\"";
content = std::regex_replace(content, versionRegex, newVersion);
// Write back to file
std::ofstream outFile(sourcePath_);
if (!outFile.is_open()) {
std::cerr << "[AutoCompiler] Failed to write source file: " << sourcePath_ << std::endl;
return;
}
outFile << content;
outFile.close();
}
bool AutoCompiler::compile(int iteration) {
// Modify source version before compiling
modifySourceVersion(iteration);
// Small delay to ensure file is written
std::this_thread::sleep_for(std::chrono::milliseconds(10));
// Build the module using CMake
std::string command = "cmake --build " + buildDir_ + " --target " + moduleName_ + " 2>&1 > /dev/null";
int result = std::system(command.c_str());
return (result == 0);
}
void AutoCompiler::compilationLoop(int iterations, int intervalMs) {
for (int i = 1; i <= iterations && running_.load(); ++i) {
currentIteration_ = i;
// Compile
bool success = compile(i);
if (success) {
successCount_++;
} else {
failureCount_++;
}
// Wait for next iteration
if (i < iterations) {
std::this_thread::sleep_for(std::chrono::milliseconds(intervalMs));
}
}
running_ = false;
}
} // namespace TestHelpers

View File

@ -0,0 +1,98 @@
#pragma once
#include <string>
#include <atomic>
#include <thread>
#include <functional>
namespace TestHelpers {
/**
* @brief Helper class to automatically compile a module repeatedly
*
* Designed to test race conditions during hot-reload by:
* - Modifying source files to bump version numbers
* - Triggering CMake builds repeatedly
* - Running on a separate thread with configurable interval
* - Tracking compilation success/failure rates
*/
class AutoCompiler {
public:
/**
* @param moduleName Name of the module to compile (e.g., "TestModule")
* @param buildDir Path to build directory (e.g., "build")
* @param sourcePath Path to the source file to modify (e.g., "tests/modules/TestModule.cpp")
*/
AutoCompiler(const std::string& moduleName,
const std::string& buildDir,
const std::string& sourcePath);
~AutoCompiler();
/**
* @brief Start auto-compilation thread
* @param iterations Total number of compilations to perform
* @param intervalMs Milliseconds between each compilation
*/
void start(int iterations, int intervalMs);
/**
* @brief Stop the compilation thread gracefully
*/
void stop();
/**
* @brief Get current iteration number
*/
int getCurrentIteration() const { return currentIteration_.load(); }
/**
* @brief Get number of successful compilations
*/
int getSuccessCount() const { return successCount_.load(); }
/**
* @brief Get number of failed compilations
*/
int getFailureCount() const { return failureCount_.load(); }
/**
* @brief Check if compilation thread is still running
*/
bool isRunning() const { return running_.load(); }
/**
* @brief Wait for all compilations to complete
*/
void waitForCompletion();
private:
/**
* @brief Modify source file to change version number
*/
void modifySourceVersion(int iteration);
/**
* @brief Compile the module using CMake
* @return true if compilation succeeded
*/
bool compile(int iteration);
/**
* @brief Main compilation loop (runs in separate thread)
*/
void compilationLoop(int iterations, int intervalMs);
std::string moduleName_;
std::string buildDir_;
std::string sourcePath_;
std::atomic<int> currentIteration_{0};
std::atomic<int> successCount_{0};
std::atomic<int> failureCount_{0};
std::atomic<bool> running_{false};
std::thread compilationThread_;
};
} // namespace TestHelpers

View File

@ -0,0 +1,386 @@
#include "grove/ModuleLoader.h"
#include "grove/SequentialModuleSystem.h"
#include "grove/JsonDataNode.h"
#include "../helpers/TestMetrics.h"
#include "../helpers/TestAssertions.h"
#include "../helpers/TestReporter.h"
#include "../helpers/SystemUtils.h"
#include "../helpers/AutoCompiler.h"
#include <iostream>
#include <chrono>
#include <thread>
#include <filesystem>
#include <atomic>
#include <mutex>
using namespace grove;
using namespace TestHelpers;
int main() {
TestReporter reporter("Race Condition Hunter");
std::cout << "================================================================================\n";
std::cout << "TEST: Race Condition Hunter - Concurrent Compilation & Reload\n";
std::cout << "================================================================================\n\n";
// === CONFIGURATION ===
const int TOTAL_COMPILATIONS = 15; // Guaranteed completion within timeout
const int COMPILE_INTERVAL_MS = 1000; // 1 second between compilations
const int FILE_CHECK_INTERVAL_MS = 50; // Check file changes every 50ms
const float TARGET_FPS = 60.0f;
const float FRAME_TIME = 1.0f / TARGET_FPS;
std::string modulePath = "build/tests/libTestModule.so";
std::string sourcePath = "tests/modules/TestModule.cpp";
std::string buildDir = "build";
// === ATOMIC COUNTERS (Thread-safe) ===
std::atomic<int> reloadAttempts{0};
std::atomic<int> reloadSuccesses{0};
std::atomic<int> reloadFailures{0};
std::atomic<int> corruptedLoads{0};
std::atomic<int> crashes{0};
std::atomic<bool> engineRunning{true};
std::atomic<bool> watcherRunning{true};
// CRITICAL: Mutex to protect moduleSystem access between threads
std::mutex moduleSystemMutex;
// Reload timing
std::mutex reloadTimesMutex;
std::vector<float> reloadTimes;
// Metrics
TestMetrics metrics;
// === SETUP ===
std::cout << "Setup:\n";
std::cout << " Module path: " << modulePath << "\n";
std::cout << " Source path: " << sourcePath << "\n";
std::cout << " Compilations: " << TOTAL_COMPILATIONS << "\n";
std::cout << " Interval: " << COMPILE_INTERVAL_MS << "ms\n";
std::cout << " Expected time: ~" << (TOTAL_COMPILATIONS * COMPILE_INTERVAL_MS / 1000) << "s\n\n";
// Load module initially
ModuleLoader loader;
auto moduleSystem = std::make_unique<SequentialModuleSystem>();
try {
auto module = loader.load(modulePath, "TestModule", false);
nlohmann::json configJson;
configJson["version"] = "v1";
auto config = std::make_unique<JsonDataNode>("config", configJson);
module->setConfiguration(*config, nullptr, nullptr);
moduleSystem->registerModule("TestModule", 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;
}
// === THREAD 1: AUTO-COMPILER ===
std::cout << "Starting AutoCompiler thread...\n";
AutoCompiler compiler("TestModule", buildDir, sourcePath);
compiler.start(TOTAL_COMPILATIONS, COMPILE_INTERVAL_MS);
// === THREAD 2: FILE WATCHER ===
std::cout << "Starting FileWatcher thread...\n";
std::thread watcherThread([&]() {
try {
auto lastWriteTime = std::filesystem::last_write_time(modulePath);
while (watcherRunning.load() && engineRunning.load()) {
try {
auto currentTime = std::filesystem::last_write_time(modulePath);
if (currentTime != lastWriteTime) {
reloadAttempts++;
// Measure reload time
auto reloadStart = std::chrono::high_resolution_clock::now();
try {
// CRITICAL: Lock moduleSystem during entire reload
std::lock_guard<std::mutex> lock(moduleSystemMutex);
// Extract module
auto module = moduleSystem->extractModule();
auto state = module->getState();
// Reload
auto newModule = loader.load(modulePath, "TestModule", true);
// Check if module loaded correctly
if (!newModule) {
corruptedLoads++;
reloadFailures++;
// Re-register old module
moduleSystem->registerModule("TestModule", std::move(module));
} else {
// VALIDATE MODULE INTEGRITY
bool isCorrupted = false;
try {
// Test 1: Can we get health status?
auto health = newModule->getHealthStatus();
std::string version = health->getString("version", "");
// Test 2: Is version valid?
if (version.empty() || version == "unknown") {
isCorrupted = true;
}
// Test 3: Can we set configuration?
nlohmann::json configJson;
configJson["test"] = "validation";
auto testConfig = std::make_unique<JsonDataNode>("config", configJson);
newModule->setConfiguration(*testConfig, nullptr, nullptr);
} catch (const std::exception& e) {
// Module crashes on basic operations = corrupted
isCorrupted = true;
}
if (isCorrupted) {
corruptedLoads++;
reloadFailures++;
// Re-register old module
moduleSystem->registerModule("TestModule", std::move(module));
} else {
// Module is valid, restore state and register
newModule->setState(*state);
moduleSystem->registerModule("TestModule", std::move(newModule));
reloadSuccesses++;
// Record reload time
auto reloadEnd = std::chrono::high_resolution_clock::now();
float reloadTimeMs = std::chrono::duration<float, std::milli>(reloadEnd - reloadStart).count();
{
std::lock_guard<std::mutex> timeLock(reloadTimesMutex);
reloadTimes.push_back(reloadTimeMs);
}
}
}
} catch (const std::exception& e) {
reloadFailures++;
// Module might already be registered, continue
}
lastWriteTime = currentTime;
}
} catch (const std::filesystem::filesystem_error&) {
// File might be being written, ignore
}
std::this_thread::sleep_for(std::chrono::milliseconds(FILE_CHECK_INTERVAL_MS));
}
} catch (const std::exception& e) {
std::cerr << "[FileWatcher] Exception: " << e.what() << "\n";
}
});
// === THREAD 3: ENGINE LOOP ===
std::cout << "Starting Engine thread (60 FPS)...\n\n";
std::thread engineThread([&]() {
try {
auto lastMemoryCheck = std::chrono::steady_clock::now();
while (engineRunning.load()) {
auto frameStart = std::chrono::high_resolution_clock::now();
try {
// TRY to lock moduleSystem (non-blocking)
// If reload is happening, skip this frame
if (moduleSystemMutex.try_lock()) {
try {
moduleSystem->processModules(FRAME_TIME);
moduleSystemMutex.unlock();
} catch (const std::exception& e) {
moduleSystemMutex.unlock();
throw;
}
}
// else: reload in progress, skip frame
} catch (const std::exception& e) {
crashes++;
std::cerr << "[Engine] Crash detected: " << e.what() << "\n";
}
auto frameEnd = std::chrono::high_resolution_clock::now();
float frameTime = std::chrono::duration<float, std::milli>(frameEnd - frameStart).count();
metrics.recordFPS(1000.0f / std::max(frameTime, 0.1f));
// Check memory every second
auto now = std::chrono::steady_clock::now();
if (std::chrono::duration_cast<std::chrono::seconds>(now - lastMemoryCheck).count() >= 1) {
metrics.recordMemoryUsage(getCurrentMemoryUsage());
lastMemoryCheck = now;
}
// Sleep to maintain target FPS (if frame finished early)
auto targetFrameTime = std::chrono::milliseconds(static_cast<int>(FRAME_TIME * 1000));
auto elapsed = frameEnd - frameStart;
if (elapsed < targetFrameTime) {
std::this_thread::sleep_for(targetFrameTime - elapsed);
}
}
} catch (const std::exception& e) {
std::cerr << "[Engine] Thread exception: " << e.what() << "\n";
}
});
// === MONITORING LOOP ===
std::cout << "Test running...\n";
auto startTime = std::chrono::steady_clock::now();
int lastPrintedPercent = 0;
const int MAX_TEST_TIME_SECONDS = 90; // Maximum 1.5 minutes (allows all 20 compilations)
while (compiler.isRunning() || compiler.getCurrentIteration() < TOTAL_COMPILATIONS) {
std::this_thread::sleep_for(std::chrono::seconds(2));
int currentIteration = compiler.getCurrentIteration();
int percent = (currentIteration * 100) / TOTAL_COMPILATIONS;
// Check for timeout
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - startTime).count();
if (elapsed > MAX_TEST_TIME_SECONDS) {
std::cout << "\n⚠️ Test timeout after " << elapsed << "s - stopping...\n";
break;
}
// Print progress every 10%
if (percent >= lastPrintedPercent + 10 && percent <= 100) {
std::cout << "\nProgress: " << percent << "% (" << currentIteration << "/" << TOTAL_COMPILATIONS << " compilations)\n";
std::cout << " Elapsed: " << elapsed << "s\n";
std::cout << " Compilations: " << compiler.getSuccessCount() << " OK, " << compiler.getFailureCount() << " FAIL\n";
std::cout << " Reloads: " << reloadSuccesses.load() << " OK, " << reloadFailures.load() << " FAIL\n";
std::cout << " Corrupted: " << corruptedLoads.load() << "\n";
std::cout << " Crashes: " << crashes.load() << "\n";
lastPrintedPercent = percent;
}
}
// === CLEANUP ===
std::cout << "\n\nStopping threads...\n";
compiler.stop();
watcherRunning = false;
engineRunning = false;
if (watcherThread.joinable()) {
watcherThread.join();
}
if (engineThread.joinable()) {
engineThread.join();
}
std::cout << " ✓ All threads stopped\n\n";
// === CALCULATE STATISTICS ===
float compileSuccessRate = (compiler.getSuccessCount() * 100.0f) / std::max(1, TOTAL_COMPILATIONS);
float reloadSuccessRate = (reloadSuccesses.load() * 100.0f) / std::max(1, reloadAttempts.load());
float avgReloadTime = 0.0f;
{
std::lock_guard<std::mutex> lock(reloadTimesMutex);
if (!reloadTimes.empty()) {
float sum = 0.0f;
for (float t : reloadTimes) {
sum += t;
}
avgReloadTime = sum / reloadTimes.size();
}
}
auto endTime = std::chrono::steady_clock::now();
auto totalTimeSeconds = std::chrono::duration_cast<std::chrono::seconds>(endTime - startTime).count();
// === PRINT SUMMARY ===
std::cout << "================================================================================\n";
std::cout << "RACE CONDITION HUNTER SUMMARY\n";
std::cout << "================================================================================\n\n";
std::cout << "Duration: " << totalTimeSeconds << "s\n\n";
std::cout << "Compilations:\n";
std::cout << " Total: " << TOTAL_COMPILATIONS << "\n";
std::cout << " Successes: " << compiler.getSuccessCount() << " (" << std::fixed << std::setprecision(1) << compileSuccessRate << "%)\n";
std::cout << " Failures: " << compiler.getFailureCount() << "\n\n";
std::cout << "Reloads:\n";
std::cout << " Attempts: " << reloadAttempts.load() << "\n";
std::cout << " Successes: " << reloadSuccesses.load() << " (" << std::fixed << std::setprecision(1) << reloadSuccessRate << "%)\n";
std::cout << " Failures: " << reloadFailures.load() << "\n";
std::cout << " Corrupted: " << corruptedLoads.load() << "\n";
std::cout << " Avg time: " << std::fixed << std::setprecision(0) << avgReloadTime << "ms\n\n";
std::cout << "Stability:\n";
std::cout << " Crashes: " << crashes.load() << "\n";
std::cout << " Avg FPS: " << std::fixed << std::setprecision(1) << metrics.getFPSAvg() << "\n";
std::cout << " Memory: " << std::fixed << std::setprecision(2) << metrics.getMemoryGrowth() << " MB\n\n";
// === ASSERTIONS ===
bool passed = true;
std::cout << "Validating results...\n";
// MUST PASS criteria
if (compileSuccessRate < 95.0f) {
std::cout << " ❌ Compile success rate too low: " << compileSuccessRate << "% (need > 95%)\n";
passed = false;
} else {
std::cout << " ✓ Compile success rate: " << compileSuccessRate << "%\n";
}
if (corruptedLoads.load() > 0) {
std::cout << " ❌ Corrupted loads detected: " << corruptedLoads.load() << " (need 0)\n";
passed = false;
} else {
std::cout << " ✓ No corrupted loads\n";
}
if (crashes.load() > 0) {
std::cout << " ❌ Crashes detected: " << crashes.load() << " (need 0)\n";
passed = false;
} else {
std::cout << " ✓ No crashes\n";
}
if (reloadAttempts.load() > 0 && reloadSuccessRate < 99.0f) {
std::cout << " ❌ Reload success rate too low: " << reloadSuccessRate << "% (need > 99%)\n";
passed = false;
} else if (reloadAttempts.load() > 0) {
std::cout << " ✓ Reload success rate: " << reloadSuccessRate << "%\n";
}
// File stability validation: reload time should be >= 100ms
// This proves that ModuleLoader is waiting for file stability
if (reloadAttempts.load() > 0) {
if (avgReloadTime < 100.0f) {
std::cout << " ❌ Reload time too fast: " << avgReloadTime << "ms (need >= 100ms)\n";
std::cout << " File stability check is NOT working properly!\n";
passed = false;
} else if (avgReloadTime > 600.0f) {
std::cout << " ⚠️ Reload time very slow: " << avgReloadTime << "ms (> 600ms)\n";
std::cout << " File stability might be waiting too long\n";
} else {
std::cout << " ✓ Reload time: " << avgReloadTime << "ms (file stability working)\n";
}
}
std::cout << "\n";
// === FINAL RESULT ===
std::cout << "================================================================================\n";
if (passed) {
std::cout << "Result: ✅ PASSED\n";
} else {
std::cout << "Result: ❌ FAILED\n";
}
std::cout << "================================================================================\n";
return passed ? 0 : 1;
}

View File

@ -4,6 +4,9 @@
#include <iostream>
#include <memory>
// This line will be modified by AutoCompiler during race condition tests
std::string moduleVersion = "v1";
namespace grove {
/**
@ -17,7 +20,6 @@ namespace grove {
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;

View File

@ -0,0 +1,31 @@
#pragma once
#include <engine/Module.h>
#include <string>
/**
* @brief Simple test module for race condition testing
*
* This module has a version string that gets modified during compilation
* to test hot-reload during concurrent compilations.
*/
class TestModule : public grove::Module {
public:
void initialize() override;
void update(float deltaTime) override;
void shutdown() override;
const char* getName() const override { return "TestModule"; }
std::string getVersion() const { return version_; }
int getUpdateCount() const { return updateCount_; }
private:
std::string version_;
int updateCount_ = 0;
};
extern "C" {
grove::Module* createModule();
void destroyModule(grove::Module* module);
}