#include "../src/modules/core/StorageModule.h" #include #include #include #include #include #include #include #include #include #include #include namespace fs = std::filesystem; /** * Mock IIO implementation for testing * Simulates the pub/sub system without requiring full GroveEngine */ class MockIO : public grove::IIO { public: void publish(const std::string& topic, std::unique_ptr message) override { m_publishedMessages.push({topic, std::move(message), 0}); std::cout << "[MockIO] Published: " << topic << std::endl; } void subscribe(const std::string& topicPattern, const grove::SubscriptionConfig& config = {}) override { m_subscriptions.push_back(topicPattern); std::cout << "[MockIO] Subscribed: " << topicPattern << std::endl; } void subscribeLowFreq(const std::string& topicPattern, const grove::SubscriptionConfig& config = {}) override { m_subscriptions.push_back(topicPattern); } int hasMessages() const override { return static_cast(m_messageQueue.size()); } grove::Message pullMessage() override { if (m_messageQueue.empty()) { throw std::runtime_error("No messages available"); } auto msg = std::move(m_messageQueue.front()); m_messageQueue.pop(); return std::move(msg); } grove::IOHealth getHealth() const override { grove::IOHealth health; health.queueSize = static_cast(m_messageQueue.size()); health.maxQueueSize = 1000; health.dropping = false; health.averageProcessingRate = 100.0f; health.droppedMessageCount = 0; return health; } grove::IOType getType() const override { return grove::IOType::INTRA; } // Test helpers void injectMessage(const std::string& topic, std::unique_ptr data) { m_messageQueue.push({topic, std::move(data), 0}); } bool wasPublished(const std::string& topic) const { std::queue tempQueue = m_publishedMessages; while (!tempQueue.empty()) { if (tempQueue.front().topic == topic) { return true; } tempQueue.pop(); } return false; } std::unique_ptr getLastPublished(const std::string& topic) { std::vector messages; while (!m_publishedMessages.empty()) { messages.push_back(std::move(m_publishedMessages.front())); m_publishedMessages.pop(); } grove::Message* found = nullptr; for (auto& msg : messages) { if (msg.topic == topic) { found = &msg; } } if (found) { return std::move(found->data); } return nullptr; } void clearPublished() { while (!m_publishedMessages.empty()) { m_publishedMessages.pop(); } } private: std::vector m_subscriptions; std::queue m_messageQueue; std::queue m_publishedMessages; }; /** * Test helper: Clean up test save files */ void cleanupTestSaves(const std::string& savePath = "data/saves/") { try { if (fs::exists(savePath)) { for (const auto& entry : fs::directory_iterator(savePath)) { if (entry.is_regular_file()) { std::string filename = entry.path().filename().string(); if (filename.find("test_") == 0 || filename.find("autosave") == 0) { fs::remove(entry.path()); } } } } } catch (const std::exception& e) { std::cerr << "Failed to cleanup test saves: " << e.what() << std::endl; } } /** * Test 1: Save/Load Cycle Preserves Data */ void testSaveLoadCycle() { std::cout << "\n=== Test 1: Save/Load Cycle ===" << std::endl; cleanupTestSaves(); MockIO mockIO; StorageModule storage; // Configure module auto config = std::make_unique("config"); config->setString("savePath", "data/saves/"); config->setDouble("autoSaveInterval", 300.0); config->setInt("maxAutoSaves", 3); storage.setConfiguration(*config, &mockIO, nullptr); // Simulate module state response auto moduleState = std::make_unique("state"); moduleState->setString("moduleName", "TestModule"); moduleState->setInt("testValue", 42); moduleState->setString("testString", "Hello World"); mockIO.injectMessage("storage:module_state", std::move(moduleState)); // Request save auto saveRequest = std::make_unique("request"); saveRequest->setString("filename", "test_save_load"); mockIO.injectMessage("game:request_save", std::move(saveRequest)); // Process messages auto input = std::make_unique("input"); input->setDouble("deltaTime", 0.016); storage.process(*input); // Verify save_complete was published assert(mockIO.wasPublished("storage:save_complete")); std::cout << "✓ Save completed successfully" << std::endl; // Verify file exists assert(fs::exists("data/saves/test_save_load.json")); std::cout << "✓ Save file created" << std::endl; // Clear published messages mockIO.clearPublished(); // Request load auto loadRequest = std::make_unique("request"); loadRequest->setString("filename", "test_save_load"); mockIO.injectMessage("game:request_load", std::move(loadRequest)); storage.process(*input); // Verify load_complete was published assert(mockIO.wasPublished("storage:load_complete")); std::cout << "✓ Load completed successfully" << std::endl; // Verify restore messages were published assert(mockIO.wasPublished("storage:restore_state:TestModule")); std::cout << "✓ Module state restore requested" << std::endl; std::cout << "✓ Test 1 PASSED\n" << std::endl; } /** * Test 2: Auto-save Triggers Correctly */ void testAutoSave() { std::cout << "\n=== Test 2: Auto-save Triggers ===" << std::endl; cleanupTestSaves(); MockIO mockIO; StorageModule storage; // Configure with short auto-save interval auto config = std::make_unique("config"); config->setString("savePath", "data/saves/"); config->setDouble("autoSaveInterval", 1.0); // 1 second for testing config->setInt("maxAutoSaves", 3); storage.setConfiguration(*config, &mockIO, nullptr); // Process with accumulated time auto input = std::make_unique("input"); input->setDouble("deltaTime", 1.5); // Exceed auto-save interval storage.process(*input); // Verify auto-save was triggered assert(mockIO.wasPublished("storage:save_complete")); std::cout << "✓ Auto-save triggered after interval" << std::endl; // Verify autosave file was created bool foundAutoSave = false; for (const auto& entry : fs::directory_iterator("data/saves/")) { std::string filename = entry.path().filename().string(); if (filename.find("autosave") == 0) { foundAutoSave = true; break; } } assert(foundAutoSave); std::cout << "✓ Auto-save file created" << std::endl; std::cout << "✓ Test 2 PASSED\n" << std::endl; } /** * Test 3: Invalid Save File Handling */ void testInvalidSaveFile() { std::cout << "\n=== Test 3: Invalid Save File Handling ===" << std::endl; cleanupTestSaves(); MockIO mockIO; StorageModule storage; auto config = std::make_unique("config"); config->setString("savePath", "data/saves/"); config->setDouble("autoSaveInterval", 300.0); config->setInt("maxAutoSaves", 3); storage.setConfiguration(*config, &mockIO, nullptr); // Try to load non-existent file auto loadRequest = std::make_unique("request"); loadRequest->setString("filename", "nonexistent_file"); mockIO.injectMessage("game:request_load", std::move(loadRequest)); auto input = std::make_unique("input"); storage.process(*input); // Verify load_failed was published assert(mockIO.wasPublished("storage:load_failed")); std::cout << "✓ Load failure detected and published" << std::endl; // Create corrupted save file std::ofstream corruptedFile("data/saves/corrupted_save.json"); corruptedFile << "{ invalid json content }}}"; corruptedFile.close(); mockIO.clearPublished(); // Try to load corrupted file loadRequest = std::make_unique("request"); loadRequest->setString("filename", "corrupted_save"); mockIO.injectMessage("game:request_load", std::move(loadRequest)); storage.process(*input); // Verify load_failed was published assert(mockIO.wasPublished("storage:load_failed")); std::cout << "✓ Corrupted file detected and handled" << std::endl; std::cout << "✓ Test 3 PASSED\n" << std::endl; } /** * Test 4: Version Compatibility Check */ void testVersionCompatibility() { std::cout << "\n=== Test 4: Version Compatibility ===" << std::endl; cleanupTestSaves(); MockIO mockIO; StorageModule storage; auto config = std::make_unique("config"); config->setString("savePath", "data/saves/"); config->setDouble("autoSaveInterval", 300.0); config->setInt("maxAutoSaves", 3); storage.setConfiguration(*config, &mockIO, nullptr); // Create a save file with different version nlohmann::json saveJson = { {"version", "0.2.0"}, {"game", "MobileCommand"}, {"timestamp", "2025-12-02T10:00:00Z"}, {"gameTime", 3600.0}, {"modules", { {"TestModule", { {"testValue", 123} }} }} }; std::ofstream versionFile("data/saves/version_test.json"); versionFile << saveJson.dump(2); versionFile.close(); // Load file with different version auto loadRequest = std::make_unique("request"); loadRequest->setString("filename", "version_test"); mockIO.injectMessage("game:request_load", std::move(loadRequest)); auto input = std::make_unique("input"); storage.process(*input); // Should load successfully (version is informational, not blocking) assert(mockIO.wasPublished("storage:load_complete")); std::cout << "✓ Different version loaded (with warning)" << std::endl; std::cout << "✓ Test 4 PASSED\n" << std::endl; } /** * Test 5: Auto-save Rotation (Max Auto-saves) */ void testAutoSaveRotation() { std::cout << "\n=== Test 5: Auto-save Rotation ===" << std::endl; cleanupTestSaves(); MockIO mockIO; StorageModule storage; // Configure with max 3 auto-saves auto config = std::make_unique("config"); config->setString("savePath", "data/saves/"); config->setDouble("autoSaveInterval", 0.5); config->setInt("maxAutoSaves", 3); storage.setConfiguration(*config, &mockIO, nullptr); auto input = std::make_unique("input"); // Trigger 5 auto-saves for (int i = 0; i < 5; i++) { input->setDouble("deltaTime", 0.6); // Exceed interval storage.process(*input); mockIO.clearPublished(); // Small delay to ensure different timestamps std::this_thread::sleep_for(std::chrono::milliseconds(100)); } // Count auto-save files int autoSaveCount = 0; for (const auto& entry : fs::directory_iterator("data/saves/")) { std::string filename = entry.path().filename().string(); if (filename.find("autosave") == 0) { autoSaveCount++; } } // Should have exactly 3 (oldest were deleted) assert(autoSaveCount == 3); std::cout << "✓ Auto-save rotation working (kept 3 newest)" << std::endl; std::cout << "✓ Test 5 PASSED\n" << std::endl; } /** * Test 6: State Preservation During Hot-Reload */ void testHotReloadState() { std::cout << "\n=== Test 6: Hot-reload State Preservation ===" << std::endl; MockIO mockIO; StorageModule storage; auto config = std::make_unique("config"); config->setString("savePath", "data/saves/"); config->setDouble("autoSaveInterval", 100.0); config->setInt("maxAutoSaves", 3); storage.setConfiguration(*config, &mockIO, nullptr); // Simulate some time passing auto input = std::make_unique("input"); input->setDouble("deltaTime", 50.0); storage.process(*input); // Get state auto state = storage.getState(); double savedTime = state->getDouble("timeSinceLastAutoSave", 0.0); assert(savedTime == 50.0); std::cout << "✓ State captured: timeSinceLastAutoSave = " << savedTime << std::endl; // Simulate module reload StorageModule newStorage; newStorage.setConfiguration(*config, &mockIO, nullptr); newStorage.setState(*state); // Verify state was restored auto restoredState = newStorage.getState(); double restoredTime = restoredState->getDouble("timeSinceLastAutoSave", 0.0); assert(restoredTime == 50.0); std::cout << "✓ State restored: timeSinceLastAutoSave = " << restoredTime << std::endl; std::cout << "✓ Test 6 PASSED\n" << std::endl; } /** * Main test runner */ int main() { std::cout << "========================================" << std::endl; std::cout << "StorageModule Independent Validation Tests" << std::endl; std::cout << "========================================" << std::endl; try { testSaveLoadCycle(); testAutoSave(); testInvalidSaveFile(); testVersionCompatibility(); testAutoSaveRotation(); testHotReloadState(); std::cout << "\n========================================" << std::endl; std::cout << "ALL TESTS PASSED ✓" << std::endl; std::cout << "========================================\n" << std::endl; cleanupTestSaves(); return 0; } catch (const std::exception& e) { std::cerr << "\n✗ TEST FAILED: " << e.what() << std::endl; cleanupTestSaves(); return 1; } }