/** * GameModuleTest.cpp * * Unit tests for Mobile Command GameModule * Tests state machine, event subscriptions, and MC-specific logic */ #include #include #include "../src/modules/GameModule.h" #include #include #include #include #include using namespace mc; using namespace grove; // Mock IIO implementation for testing class MockIIO : public IIO { public: // Published messages storage std::vector>> publishedMessages; // Subscribed topics std::vector subscribedTopics; // Message queue for pulling std::queue messageQueue; void publish(const std::string& topic, std::unique_ptr message) override { publishedMessages.push_back({topic, std::move(message)}); } void subscribe(const std::string& topicPattern, const SubscriptionConfig& config = {}) override { subscribedTopics.push_back(topicPattern); } void subscribeLowFreq(const std::string& topicPattern, const SubscriptionConfig& config = {}) override { subscribedTopics.push_back(topicPattern); } int hasMessages() const override { return static_cast(messageQueue.size()); } Message pullMessage() override { if (messageQueue.empty()) { throw std::runtime_error("No messages available"); } Message msg = std::move(messageQueue.front()); messageQueue.pop(); return msg; } IOHealth getHealth() const override { IOHealth health; health.queueSize = static_cast(messageQueue.size()); health.maxQueueSize = 1000; health.dropping = false; health.averageProcessingRate = 100.0f; health.droppedMessageCount = 0; return health; } IOType getType() const override { return IOType::INTRA; } // Helper methods for testing void pushMessage(const std::string& topic, std::unique_ptr data) { Message msg; msg.topic = topic; msg.data = std::move(data); msg.timestamp = 0; messageQueue.push(std::move(msg)); } void clearPublished() { publishedMessages.clear(); } bool hasPublished(const std::string& topic) const { for (const auto& [t, data] : publishedMessages) { if (t == topic) return true; } return false; } }; // Mock ITaskScheduler (not used in tests but required by interface) class MockTaskScheduler : public ITaskScheduler { public: void scheduleTask(const std::string& taskType, std::unique_ptr taskData) override {} int hasCompletedTasks() const override { return 0; } std::unique_ptr getCompletedTask() override { return std::make_unique("empty"); } }; class GameModuleTest : public ::testing::Test { protected: void SetUp() override { module = std::make_unique(); mockIO = std::make_unique(); mockScheduler = std::make_unique(); // Create test configuration config = std::make_unique("config"); config->setString("initialState", "MainMenu"); config->setDouble("tickRate", 10.0); config->setBool("debugMode", true); // Initialize module module->setConfiguration(*config, mockIO.get(), mockScheduler.get()); } void TearDown() override { module->shutdown(); } std::unique_ptr module; std::unique_ptr mockIO; std::unique_ptr mockScheduler; std::unique_ptr config; }; // Test 1: State machine initialization TEST_F(GameModuleTest, InitialStateIsMainMenu) { auto health = module->getHealthStatus(); EXPECT_EQ(health->getString("currentState", ""), "MainMenu"); } // Test 2: State transitions work TEST_F(GameModuleTest, StateTransitionsWork) { // Simulate combat start event auto combatData = std::make_unique("combat_start"); combatData->setString("location", "urban_ruins"); combatData->setString("enemy_type", "scavengers"); mockIO->pushMessage("combat:started", std::move(combatData)); // Process the message auto input = std::make_unique("input"); input->setDouble("deltaTime", 0.1); module->process(*input); // Check state transition auto health = module->getHealthStatus(); EXPECT_EQ(health->getString("currentState", ""), "Combat"); // Verify state change event was published EXPECT_TRUE(mockIO->hasPublished("game:state_changed")); } // Test 3: Event subscriptions are setup correctly TEST_F(GameModuleTest, EventSubscriptionsSetup) { // Check that all expected topics are subscribed auto& topics = mockIO->subscribedTopics; EXPECT_TRUE(std::find(topics.begin(), topics.end(), "resource:craft_complete") != topics.end()); EXPECT_TRUE(std::find(topics.begin(), topics.end(), "resource:inventory_low") != topics.end()); EXPECT_TRUE(std::find(topics.begin(), topics.end(), "storage:save_complete") != topics.end()); EXPECT_TRUE(std::find(topics.begin(), topics.end(), "combat:started") != topics.end()); EXPECT_TRUE(std::find(topics.begin(), topics.end(), "combat:ended") != topics.end()); EXPECT_TRUE(std::find(topics.begin(), topics.end(), "event:triggered") != topics.end()); } // Test 4: Game time advances TEST_F(GameModuleTest, GameTimeAdvances) { auto input = std::make_unique("input"); input->setDouble("deltaTime", 0.1); // Process 10 frames for (int i = 0; i < 10; i++) { module->process(*input); } auto health = module->getHealthStatus(); double gameTime = health->getDouble("gameTime", 0.0); // Should be approximately 1.0 second (10 * 0.1) EXPECT_NEAR(gameTime, 1.0, 0.01); } // Test 5: Hot-reload preserves state TEST_F(GameModuleTest, HotReloadPreservesState) { // Advance game state auto input = std::make_unique("input"); input->setDouble("deltaTime", 0.5); for (int i = 0; i < 10; i++) { module->process(*input); } // Get current state auto state = module->getState(); double originalGameTime = state->getDouble("gameTime", 0.0); int originalFrameCount = state->getInt("frameCount", 0); // Create new module and restore state auto newModule = std::make_unique(); newModule->setConfiguration(*config, mockIO.get(), mockScheduler.get()); newModule->setState(*state); // Verify state was restored auto restoredHealth = newModule->getHealthStatus(); double restoredGameTime = restoredHealth->getDouble("gameTime", 0.0); int restoredFrameCount = restoredHealth->getInt("frameCount", 0); EXPECT_EQ(restoredGameTime, originalGameTime); EXPECT_EQ(restoredFrameCount, originalFrameCount); } // Test 6: Drone crafted event triggers MC-specific logic TEST_F(GameModuleTest, DroneCraftedTriggersCorrectLogic) { mockIO->clearPublished(); // Simulate drone craft completion auto craftData = std::make_unique("craft_complete"); craftData->setString("recipe", "drone_recon"); craftData->setInt("quantity", 1); mockIO->pushMessage("resource:craft_complete", std::move(craftData)); // Process the message auto input = std::make_unique("input"); input->setDouble("deltaTime", 0.1); module->process(*input); // Check that expedition:drone_available was published EXPECT_TRUE(mockIO->hasPublished("expedition:drone_available")); } // Test 7: Low fuel warning triggers MC-specific logic TEST_F(GameModuleTest, LowFuelWarningTriggered) { // Simulate low fuel inventory auto inventoryData = std::make_unique("inventory_low"); inventoryData->setString("resource_id", "fuel_diesel"); inventoryData->setInt("current", 10); inventoryData->setInt("threshold", 50); mockIO->pushMessage("resource:inventory_low", std::move(inventoryData)); // Process the message auto input = std::make_unique("input"); input->setDouble("deltaTime", 0.1); module->process(*input); // In a real test, we'd verify that the warning was shown // For now, just verify the message was processed without error EXPECT_TRUE(true); } // Test 8: Combat victory increments counter TEST_F(GameModuleTest, CombatVictoryIncrementsCounter) { // Start combat auto startData = std::make_unique("combat_start"); startData->setString("location", "urban_ruins"); startData->setString("enemy_type", "scavengers"); mockIO->pushMessage("combat:started", std::move(startData)); auto input = std::make_unique("input"); input->setDouble("deltaTime", 0.1); module->process(*input); // End combat with victory auto input2 = std::make_unique("input"); input2->setDouble("deltaTime", 0.1); auto endData = std::make_unique("combat_end"); endData->setBool("victory", true); mockIO->pushMessage("combat:ended", std::move(endData)); module->process(*input2); // Check that combatsWon was incremented auto health = module->getHealthStatus(); int combatsWon = health->getInt("combatsWon", 0); EXPECT_EQ(combatsWon, 1); } // Test 9: Event triggers state transition TEST_F(GameModuleTest, EventTriggersStateTransition) { // Trigger an event auto eventData = std::make_unique("event"); eventData->setString("event_id", "scavenger_encounter"); mockIO->pushMessage("event:triggered", std::move(eventData)); auto input = std::make_unique("input"); input->setDouble("deltaTime", 0.1); module->process(*input); // Check state transition to Event auto health = module->getHealthStatus(); EXPECT_EQ(health->getString("currentState", ""), "Event"); } // Test 10: Module type is correct TEST_F(GameModuleTest, ModuleTypeIsCorrect) { EXPECT_EQ(module->getType(), "GameModule"); } // Test 11: Module is idle only in MainMenu TEST_F(GameModuleTest, ModuleIdleInMainMenu) { // Initially in MainMenu EXPECT_TRUE(module->isIdle()); // Transition to combat auto combatData = std::make_unique("combat_start"); combatData->setString("location", "urban_ruins"); combatData->setString("enemy_type", "scavengers"); mockIO->pushMessage("combat:started", std::move(combatData)); auto input = std::make_unique("input"); input->setDouble("deltaTime", 0.1); module->process(*input); // No longer idle EXPECT_FALSE(module->isIdle()); } // Test 12: Multiple messages processed in single frame TEST_F(GameModuleTest, MultipleMessagesProcessedInSingleFrame) { // Queue multiple messages auto msg1 = std::make_unique("msg1"); msg1->setString("resource_id", "scrap_metal"); msg1->setInt("delta", 10); msg1->setInt("total", 100); mockIO->pushMessage("resource:inventory_changed", std::move(msg1)); auto msg2 = std::make_unique("msg2"); msg2->setString("filename", "savegame.json"); mockIO->pushMessage("storage:save_complete", std::move(msg2)); auto msg3 = std::make_unique("msg3"); msg3->setString("event_id", "test_event"); mockIO->pushMessage("event:triggered", std::move(msg3)); // Process all in one frame auto input = std::make_unique("input"); input->setDouble("deltaTime", 0.1); module->process(*input); // All messages should be consumed EXPECT_EQ(mockIO->hasMessages(), 0); // State should be Event (last transition) auto health = module->getHealthStatus(); EXPECT_EQ(health->getString("currentState", ""), "Event"); } // Main test runner int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }