#include #include "../src/modules/mc_specific/ExpeditionModule.h" #include #include #include #include #include #include /** * ExpeditionModule Test Suite * * These tests validate the MC-specific ExpeditionModule functionality: * - Expedition lifecycle (start -> progress -> arrival -> return) * - Team and drone management * - Event triggering during travel * - Loot distribution on return * - Hot-reload state preservation * - Pub/sub communication with other modules */ class ExpeditionModuleTest : public ::testing::Test { protected: void SetUp() override { // Create IO manager m_io = std::make_unique(); // Create task scheduler m_scheduler = std::make_unique(); // Create module m_module = std::make_unique(); // Create configuration auto config = std::make_unique("config"); config->setBool("debugMode", true); config->setInt("maxActiveExpeditions", 1); config->setDouble("eventProbability", 0.3); config->setDouble("suppliesConsumptionRate", 1.0); // Initialize module m_module->setConfiguration(*config, m_io.get(), m_scheduler.get()); // Subscribe to expedition events m_io->subscribe("expedition:*"); m_io->subscribe("event:*"); } void TearDown() override { if (m_module) { m_module->shutdown(); } } // Helper to process module for a duration void processModuleFor(float seconds, float tickRate = 10.0f) { float deltaTime = 1.0f / tickRate; int iterations = static_cast(seconds * tickRate); for (int i = 0; i < iterations; ++i) { auto input = std::make_unique("input"); input->setDouble("deltaTime", static_cast(deltaTime)); m_module->process(*input); } } // Helper to pull all pending messages std::vector pullAllMessages() { std::vector messages; while (m_io->hasMessages() > 0) { messages.push_back(m_io->pullMessage()); } return messages; } std::unique_ptr m_io; std::unique_ptr m_scheduler; std::unique_ptr m_module; }; // Test 1: Expedition starts correctly TEST_F(ExpeditionModuleTest, ExpeditionStartsCorrectly) { // Request expedition start auto request = std::make_unique("request"); request->setString("destination_id", "village"); m_io->publish("expedition:request_start", std::move(request)); // Process module auto input = std::make_unique("input"); input->setDouble("deltaTime", 0.1); m_module->process(*input); // Check for expedition:started event auto messages = pullAllMessages(); bool foundStartedEvent = false; for (const auto& msg : messages) { if (msg.topic == "expedition:started") { foundStartedEvent = true; EXPECT_EQ(msg.data->getString("destination_type", ""), "village"); EXPECT_GT(msg.data->getInt("team_size", 0), 0); } } EXPECT_TRUE(foundStartedEvent) << "expedition:started event should be published"; // Check module is not idle (expedition active) EXPECT_FALSE(m_module->isIdle()); } // Test 2: Progress updates (A->B movement) TEST_F(ExpeditionModuleTest, ProgressUpdatesAtoB) { // Start expedition to village (8000m, 40m/s travel speed) auto request = std::make_unique("request"); request->setString("destination_id", "village"); m_io->publish("expedition:request_start", std::move(request)); auto input = std::make_unique("input"); input->setDouble("deltaTime", 0.1); m_module->process(*input); pullAllMessages(); // Clear started event // Process for some time processModuleFor(50.0f); // 50 seconds // Check for progress events auto messages = pullAllMessages(); bool foundProgressEvent = false; for (const auto& msg : messages) { if (msg.topic == "expedition:progress") { foundProgressEvent = true; double progress = msg.data->getDouble("progress", 0.0); EXPECT_GT(progress, 0.0) << "Progress should increase over time"; EXPECT_LE(progress, 1.0) << "Progress should not exceed 100%"; } } EXPECT_TRUE(foundProgressEvent) << "expedition:progress events should be published"; } // Test 3: Events trigger during travel TEST_F(ExpeditionModuleTest, EventsTriggerDuringTravel) { // Start expedition to military depot (high danger) auto request = std::make_unique("request"); request->setString("destination_id", "military_depot"); m_io->publish("expedition:request_start", std::move(request)); auto input = std::make_unique("input"); input->setDouble("deltaTime", 0.1); m_module->process(*input); pullAllMessages(); // Clear started event // Process for extended time (events are probabilistic) processModuleFor(200.0f); // 200 seconds // Check for event triggers auto messages = pullAllMessages(); // Note: Events are random, so we just verify the system can publish them // In a real game, we'd see event:combat_triggered or expedition:event_triggered int eventCount = 0; for (const auto& msg : messages) { if (msg.topic == "expedition:event_triggered" || msg.topic == "event:combat_triggered") { eventCount++; } } // Events are probabilistic, so this test just verifies the mechanism works // (We can't guarantee events will trigger in a short test) EXPECT_GE(eventCount, 0) << "Event system should be functional"; } // Test 4: Loot distributed on return TEST_F(ExpeditionModuleTest, LootDistributedOnReturn) { // Start expedition to village (shortest trip) auto request = std::make_unique("request"); request->setString("destination_id", "village"); m_io->publish("expedition:request_start", std::move(request)); auto input = std::make_unique("input"); input->setDouble("deltaTime", 0.1); m_module->process(*input); pullAllMessages(); // Clear started event // Process until expedition completes (village: 8000m / 40m/s = 200s each way) processModuleFor(450.0f); // Should complete round trip // Check for return and loot events auto messages = pullAllMessages(); bool foundReturnEvent = false; bool foundLootEvent = false; for (const auto& msg : messages) { if (msg.topic == "expedition:returned") { foundReturnEvent = true; } if (msg.topic == "expedition:loot_collected") { foundLootEvent = true; // Verify loot data int scrap = msg.data->getInt("scrap_metal", 0); int components = msg.data->getInt("components", 0); int food = msg.data->getInt("food", 0); EXPECT_GT(scrap, 0) << "Should receive scrap metal loot"; } } EXPECT_TRUE(foundReturnEvent) << "expedition:returned event should be published"; EXPECT_TRUE(foundLootEvent) << "expedition:loot_collected event should be published"; // Module should be idle now EXPECT_TRUE(m_module->isIdle()); } // Test 5: Casualties applied correctly TEST_F(ExpeditionModuleTest, CasualtiesAppliedAfterCombat) { // Start expedition auto request = std::make_unique("request"); request->setString("destination_id", "urban_ruins"); m_io->publish("expedition:request_start", std::move(request)); auto input = std::make_unique("input"); input->setDouble("deltaTime", 0.1); m_module->process(*input); pullAllMessages(); // Simulate combat defeat auto combatResult = std::make_unique("combat_ended"); combatResult->setBool("victory", false); m_io->publish("combat:ended", std::move(combatResult)); // Process module m_module->process(*input); // Expedition should be returning after defeat processModuleFor(100.0f); auto messages = pullAllMessages(); // Check that expedition responds to combat outcome // (In full implementation, would verify team casualties) bool expeditionResponded = false; for (const auto& msg : messages) { if (msg.topic.find("expedition:") != std::string::npos) { expeditionResponded = true; } } EXPECT_TRUE(expeditionResponded) << "Expedition should respond to combat events"; } // Test 6: Hot-reload state preservation TEST_F(ExpeditionModuleTest, HotReloadStatePreservation) { // Start expedition auto request = std::make_unique("request"); request->setString("destination_id", "village"); m_io->publish("expedition:request_start", std::move(request)); auto input = std::make_unique("input"); input->setDouble("deltaTime", 0.1); m_module->process(*input); pullAllMessages(); // Process for a bit processModuleFor(50.0f); // Get state before hot-reload auto stateBefore = m_module->getState(); int completedBefore = stateBefore->getInt("totalExpeditionsCompleted", 0); int nextIdBefore = stateBefore->getInt("nextExpeditionId", 0); // Simulate hot-reload: create new module and restore state auto newModule = std::make_unique(); auto config = std::make_unique("config"); config->setBool("debugMode", true); newModule->setConfiguration(*config, m_io.get(), m_scheduler.get()); newModule->setState(*stateBefore); // Get state after hot-reload auto stateAfter = newModule->getState(); int completedAfter = stateAfter->getInt("totalExpeditionsCompleted", 0); int nextIdAfter = stateAfter->getInt("nextExpeditionId", 0); // Verify state preservation EXPECT_EQ(completedBefore, completedAfter) << "Completed expeditions count should be preserved"; EXPECT_EQ(nextIdBefore, nextIdAfter) << "Next expedition ID should be preserved"; newModule->shutdown(); } // Test 7: Multiple destinations available TEST_F(ExpeditionModuleTest, MultipleDestinationsAvailable) { // Check health status for destination count auto health = m_module->getHealthStatus(); int availableDestinations = health->getInt("availableDestinations", 0); EXPECT_GE(availableDestinations, 3) << "Should have at least 3 destinations available"; // Try starting expeditions to different destinations std::vector destinations = {"village", "urban_ruins", "military_depot"}; for (const auto& dest : destinations) { // Create fresh module for each test auto testModule = std::make_unique(); auto config = std::make_unique("config"); testModule->setConfiguration(*config, m_io.get(), m_scheduler.get()); // Request expedition auto request = std::make_unique("request"); request->setString("destination_id", dest); m_io->publish("expedition:request_start", std::move(request)); auto input = std::make_unique("input"); input->setDouble("deltaTime", 0.1); testModule->process(*input); // Check for started event auto messages = pullAllMessages(); bool foundStarted = false; for (const auto& msg : messages) { if (msg.topic == "expedition:started") { foundStarted = true; EXPECT_EQ(msg.data->getString("destination_id", ""), dest); } } EXPECT_TRUE(foundStarted) << "Should be able to start expedition to " << dest; testModule->shutdown(); } } // Test 8: Drone availability updates TEST_F(ExpeditionModuleTest, DroneAvailabilityUpdates) { // Simulate drone crafting auto craftEvent = std::make_unique("craft_complete"); craftEvent->setString("recipe", "drone_recon"); craftEvent->setInt("quantity", 2); m_io->publish("resource:craft_complete", std::move(craftEvent)); // Process module auto input = std::make_unique("input"); input->setDouble("deltaTime", 0.1); m_module->process(*input); // Check for drone availability event auto messages = pullAllMessages(); bool foundDroneAvailable = false; for (const auto& msg : messages) { if (msg.topic == "expedition:drone_available") { foundDroneAvailable = true; std::string droneType = msg.data->getString("drone_type", ""); int available = msg.data->getInt("total_available", 0); EXPECT_EQ(droneType, "recon"); EXPECT_EQ(available, 2); } } EXPECT_TRUE(foundDroneAvailable) << "expedition:drone_available event should be published"; } // Test 9: Module health reporting TEST_F(ExpeditionModuleTest, ModuleHealthReporting) { auto health = m_module->getHealthStatus(); EXPECT_EQ(health->getString("status", ""), "healthy"); EXPECT_EQ(health->getInt("activeExpeditions", -1), 0); EXPECT_EQ(health->getInt("totalCompleted", -1), 0); EXPECT_GE(health->getInt("availableDestinations", 0), 1); // Start an expedition auto request = std::make_unique("request"); request->setString("destination_id", "village"); m_io->publish("expedition:request_start", std::move(request)); auto input = std::make_unique("input"); input->setDouble("deltaTime", 0.1); m_module->process(*input); pullAllMessages(); // Check health again health = m_module->getHealthStatus(); EXPECT_EQ(health->getInt("activeExpeditions", -1), 1) << "Should report 1 active expedition"; } // Test 10: Module type identification TEST_F(ExpeditionModuleTest, ModuleTypeIdentification) { EXPECT_EQ(m_module->getType(), "ExpeditionModule"); } int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }