#include "infrastructure/services/TaskScheduler.h" #include "mocks/TestLogger.h" #include "mocks/MockTimeProvider.h" #include "mocks/MockThreadManager.h" #include "mocks/MockBlocker.h" #include #include #include #include using trompeloeil::_; using namespace nxl::autostore; using namespace std::chrono; using nxl::autostore::infrastructure::TaskScheduler; namespace test { // Fixed test timepoint: 2020-01-01 12:00 constexpr std::chrono::system_clock::time_point TIMEPOINT_NOW = std::chrono::system_clock::time_point(std::chrono::seconds(1577880000)); } // namespace test TEST_CASE("TaskScheduler Unit Tests", "[unit][TaskScheduler]") { // Common mock objects that all sections can use auto logger = std::make_shared(); auto timeProvider = std::make_shared(); auto threadMgr = std::make_shared(); auto blocker = std::make_shared(); SECTION("when start is called then createThread is called") { // Given // Expect createThread to be called REQUIRE_CALL(*threadMgr, createThread(_)) .RETURN(std::make_unique()); TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker); // When scheduler.start(); } SECTION("when scheduler is created then it is not running") { // When TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker); // Then - calling stop on a non-running scheduler should not cause issues // and no thread operations should be called FORBID_CALL(*threadMgr, createThread(_)); scheduler.stop(); } SECTION("when task is scheduled with OnStart mode then it executes " "immediately after start") { // Given bool taskExecuted = false; std::function threadFn; // Expect createThread to be called, save thread function REQUIRE_CALL(*threadMgr, createThread(_)) .RETURN(std::make_unique()) .LR_SIDE_EFFECT(threadFn = std::move(_1)); ALLOW_CALL(*timeProvider, now()).LR_RETURN(test::TIMEPOINT_NOW); FORBID_CALL(*blocker, blockFor(_)); TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker); auto taskFunction = [&]() { taskExecuted = true; scheduler.stop(); // prevent infinite loop in threadFn }; // When scheduler.schedule(taskFunction, 0, 0, 0, TaskScheduler::RunMode::OnStart); scheduler.start(); threadFn(); // Then REQUIRE(taskExecuted); scheduler.stop(); } SECTION( "when task is scheduled with Once mode then it executes at specified time") { // Given auto threadHandle = std::make_unique(); bool taskExecuted = false; std::function threadFn; auto currentTime = test::TIMEPOINT_NOW; // current "now", starts at 12:00 std::chrono::seconds timeDelta{5}; std::chrono::milliseconds actualDelay{0}; auto initialTime = test::TIMEPOINT_NOW; auto expectedExecutionTime = initialTime + timeDelta; // Set up thread handle expectations before moving it ALLOW_CALL(*threadHandle, join()); ALLOW_CALL(*threadHandle, joinable()).RETURN(true); // Expect createThread to be called, save thread function REQUIRE_CALL(*threadMgr, createThread(_)) .LR_RETURN(std::move(threadHandle)) .LR_SIDE_EFFECT(threadFn = std::move(_1)); // Mock time provider calls - return initial time first, then execution time ALLOW_CALL(*timeProvider, now()).LR_RETURN(currentTime); // Allow blocker calls, save delay value ALLOW_CALL(*blocker, blockFor(_)) .LR_SIDE_EFFECT(actualDelay += _1; currentTime += _1 // let the time flow ); ALLOW_CALL(*blocker, notify()); TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker); auto taskFunction = [&]() { taskExecuted = true; scheduler.stop(); // prevent infinite loop in threadFn }; // When scheduler.schedule(taskFunction, 12, 0, timeDelta.count(), TaskScheduler::RunMode::Once); scheduler.start(); // Execute the thread function to simulate the scheduler thread threadFn(); // Then REQUIRE(taskExecuted); REQUIRE(actualDelay == timeDelta); } SECTION("when task is scheduled with Forever and OnStart mode then it " "executes repeatedly") { // Given auto threadHandle = std::make_unique(); std::function threadFn; int executionCount = 0; auto currentTime = test::TIMEPOINT_NOW; // Set up thread handle expectations before moving it ALLOW_CALL(*threadHandle, join()); ALLOW_CALL(*threadHandle, joinable()).RETURN(true); // Expect createThread to be called, save thread function REQUIRE_CALL(*threadMgr, createThread(_)) .LR_RETURN(std::move(threadHandle)) .LR_SIDE_EFFECT(threadFn = std::move(_1)); // Mock time provider calls ALLOW_CALL(*timeProvider, now()).LR_RETURN(currentTime); // Allow blocker calls and simulate time passage ALLOW_CALL(*blocker, blockFor(_)).LR_SIDE_EFFECT(currentTime += _1); ALLOW_CALL(*blocker, notify()); TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker); auto taskFunction = [&]() { executionCount++; if (executionCount >= 3) { scheduler.stop(); // stop after 3 executions } }; // When scheduler.schedule(taskFunction, 0, 0, 0, TaskScheduler::RunMode::Forever | TaskScheduler::RunMode::OnStart); scheduler.start(); // Execute the thread function to simulate the scheduler thread threadFn(); // Then REQUIRE(executionCount >= 3); } SECTION("when invalid time parameters are provided then exception is thrown") { // Given TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker); // When & Then - invalid hour REQUIRE_THROWS_AS( scheduler.schedule([]() {}, -1, 0, 0, TaskScheduler::RunMode::Once), std::invalid_argument); REQUIRE_THROWS_AS( scheduler.schedule([]() {}, 24, 0, 0, TaskScheduler::RunMode::Once), std::invalid_argument); // When & Then - invalid minute REQUIRE_THROWS_AS( scheduler.schedule([]() {}, 0, -1, 0, TaskScheduler::RunMode::Once), std::invalid_argument); REQUIRE_THROWS_AS( scheduler.schedule([]() {}, 0, 60, 0, TaskScheduler::RunMode::Once), std::invalid_argument); // When & Then - invalid second REQUIRE_THROWS_AS( scheduler.schedule([]() {}, 0, 0, -1, TaskScheduler::RunMode::Once), std::invalid_argument); REQUIRE_THROWS_AS( scheduler.schedule([]() {}, 0, 0, 61, TaskScheduler::RunMode::Once), std::invalid_argument); } // std::invalid_argument); // } SECTION("when invalid mode combination is used then exception is thrown") { // Given TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker); // When & Then REQUIRE_THROWS_AS(scheduler.schedule([]() {}, 0, 0, 0, TaskScheduler::RunMode::Forever | TaskScheduler::RunMode::Once), std::invalid_argument); } SECTION("when multiple tasks are scheduled then all execute") { // Given auto threadHandle = std::make_unique(); std::function threadFn; bool task1Executed = false; bool task2Executed = false; // Set up thread handle expectations before moving it ALLOW_CALL(*threadHandle, join()); ALLOW_CALL(*threadHandle, joinable()).RETURN(true); // Expect createThread to be called, save thread function REQUIRE_CALL(*threadMgr, createThread(_)) .LR_RETURN(std::move(threadHandle)) .LR_SIDE_EFFECT(threadFn = std::move(_1)); // Mock time provider calls ALLOW_CALL(*timeProvider, now()).LR_RETURN(test::TIMEPOINT_NOW); // Allow blocker calls ALLOW_CALL(*blocker, blockFor(_)); ALLOW_CALL(*blocker, notify()); TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker); auto taskFunction1 = [&]() { task1Executed = true; }; auto taskFunction2 = [&]() { task2Executed = true; scheduler.stop(); // stop after both tasks have had a chance to execute }; // When scheduler.schedule(taskFunction1, 0, 0, 0, TaskScheduler::RunMode::OnStart); scheduler.schedule(taskFunction2, 0, 0, 0, TaskScheduler::RunMode::OnStart); scheduler.start(); // Execute the thread function to simulate the scheduler thread threadFn(); // Then REQUIRE(task1Executed); REQUIRE(task2Executed); } // } SECTION("when task is scheduled with Forever mode then it repeats") { // Given auto threadHandle = std::make_unique(); std::function threadFn; int executionCount = 0; auto currentTime = test::TIMEPOINT_NOW; // Set up thread handle expectations before moving it ALLOW_CALL(*threadHandle, join()); ALLOW_CALL(*threadHandle, joinable()).RETURN(true); // Expect createThread to be called, save thread function REQUIRE_CALL(*threadMgr, createThread(_)) .LR_RETURN(std::move(threadHandle)) .LR_SIDE_EFFECT(threadFn = std::move(_1)); // Mock time provider calls - simulate time advancing ALLOW_CALL(*timeProvider, now()).LR_RETURN(currentTime); // Allow blocker calls and simulate time passage ALLOW_CALL(*blocker, blockFor(_)).LR_SIDE_EFFECT(currentTime += _1); ALLOW_CALL(*blocker, notify()); TaskScheduler scheduler(logger, timeProvider, threadMgr, blocker); auto taskFunction = [&]() { executionCount++; if (executionCount >= 2) { scheduler.stop(); // stop after 2 executions } }; // Schedule task to run at a specific time (not immediately) and repeat // forever This ensures the task doesn't get stuck in an infinite OnStart // loop scheduler.schedule(taskFunction, 12, 0, 1, TaskScheduler::RunMode::Forever); // When scheduler.start(); // Execute the thread function to simulate the scheduler thread threadFn(); // Then REQUIRE(executionCount >= 2); } // } }