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:
parent
d8c5f93429
commit
484b9ab5d4
33
.claude/config_backups/README.md
Normal file
33
.claude/config_backups/README.md
Normal 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
|
||||||
@ -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
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
build/
|
||||||
|
logs/
|
||||||
@ -65,6 +65,7 @@ add_library(test_helpers STATIC
|
|||||||
helpers/TestMetrics.cpp
|
helpers/TestMetrics.cpp
|
||||||
helpers/TestReporter.cpp
|
helpers/TestReporter.cpp
|
||||||
helpers/SystemUtils.cpp
|
helpers/SystemUtils.cpp
|
||||||
|
helpers/AutoCompiler.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(test_helpers PUBLIC
|
target_include_directories(test_helpers PUBLIC
|
||||||
@ -162,3 +163,20 @@ add_dependencies(test_03_stress_test StressModule)
|
|||||||
|
|
||||||
# CTest integration
|
# CTest integration
|
||||||
add_test(NAME StressTest COMMAND test_03_stress_test)
|
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)
|
||||||
|
|||||||
112
tests/helpers/AutoCompiler.cpp
Normal file
112
tests/helpers/AutoCompiler.cpp
Normal 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
|
||||||
98
tests/helpers/AutoCompiler.h
Normal file
98
tests/helpers/AutoCompiler.h
Normal 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
|
||||||
386
tests/integration/test_04_race_condition.cpp
Normal file
386
tests/integration/test_04_race_condition.cpp
Normal 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;
|
||||||
|
}
|
||||||
@ -4,6 +4,9 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
|
// This line will be modified by AutoCompiler during race condition tests
|
||||||
|
std::string moduleVersion = "v1";
|
||||||
|
|
||||||
namespace grove {
|
namespace grove {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -17,7 +20,6 @@ namespace grove {
|
|||||||
class TestModule : public IModule {
|
class TestModule : public IModule {
|
||||||
private:
|
private:
|
||||||
int counter = 0;
|
int counter = 0;
|
||||||
std::string moduleVersion = "v2.0 RELOADED";
|
|
||||||
IIO* io = nullptr;
|
IIO* io = nullptr;
|
||||||
ITaskScheduler* scheduler = nullptr;
|
ITaskScheduler* scheduler = nullptr;
|
||||||
std::unique_ptr<IDataNode> config;
|
std::unique_ptr<IDataNode> config;
|
||||||
|
|||||||
31
tests/modules/TestModule.h
Normal file
31
tests/modules/TestModule.h
Normal 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);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user