You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
436 lines
14 KiB
436 lines
14 KiB
#include "application/services/TaskScheduler.h" |
|
#include "mocks/TestLogger.h" |
|
#include "mocks/MockTimeProvider.h" |
|
#include "mocks/MockThreadManager.h" |
|
#include "mocks/MockBlocker.h" |
|
#include <catch2/catch_test_macros.hpp> |
|
#include <catch2/matchers/catch_matchers_string.hpp> |
|
#include <memory> |
|
#include <atomic> |
|
|
|
using trompeloeil::_; |
|
|
|
using namespace nxl::autostore; |
|
using namespace std::chrono; |
|
using nxl::autostore::application::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<test::TestLogger>(); |
|
auto timeProvider = std::make_unique<test::MockTimeProvider>(); |
|
auto threadMgr = std::make_unique<test::MockThreadManager>(); |
|
auto blocker = std::make_unique<test::MockBlocker>(); |
|
|
|
SECTION("when start is called then createThread is called") |
|
{ |
|
// Given |
|
// Expect createThread to be called |
|
REQUIRE_CALL(*threadMgr, createThread(_)) |
|
.RETURN(std::make_unique<test::MockThreadHandle>()); |
|
|
|
TaskScheduler scheduler(logger, *timeProvider, *threadMgr, |
|
std::move(blocker)); |
|
|
|
// When |
|
scheduler.start(); |
|
} |
|
|
|
SECTION("when scheduler is created then it is not running") |
|
{ |
|
// Given - recreate blocker for this test since it was moved in previous |
|
// section |
|
auto testBlocker = std::make_unique<test::MockBlocker>(); |
|
|
|
// When |
|
TaskScheduler scheduler(logger, *timeProvider, *threadMgr, |
|
std::move(testBlocker)); |
|
|
|
// 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<void()> threadFn; |
|
|
|
// Recreate blocker for this test |
|
auto testBlocker = std::make_unique<test::MockBlocker>(); |
|
|
|
// Expect createThread to be called, save thread function |
|
REQUIRE_CALL(*threadMgr, createThread(_)) |
|
.RETURN(std::make_unique<test::MockThreadHandle>()) |
|
.LR_SIDE_EFFECT(threadFn = std::move(_1)); |
|
|
|
ALLOW_CALL(*timeProvider, now()).LR_RETURN(test::TIMEPOINT_NOW); |
|
FORBID_CALL(*testBlocker, blockFor(_)); |
|
|
|
TaskScheduler scheduler(logger, *timeProvider, *threadMgr, |
|
std::move(testBlocker)); |
|
|
|
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<test::MockThreadHandle>(); |
|
bool taskExecuted = false; |
|
std::function<void()> 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); |
|
|
|
// Recreate blocker for this test |
|
auto testBlocker = std::make_unique<test::MockBlocker>(); |
|
|
|
// 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(*testBlocker, blockFor(_)) |
|
.LR_SIDE_EFFECT(actualDelay += _1; currentTime += _1 // let the time flow |
|
); |
|
ALLOW_CALL(*testBlocker, notify()); |
|
|
|
TaskScheduler scheduler(logger, *timeProvider, *threadMgr, |
|
std::move(testBlocker)); |
|
|
|
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<test::MockThreadHandle>(); |
|
std::function<void()> 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); |
|
|
|
// Recreate blocker for this test |
|
auto testBlocker = std::make_unique<test::MockBlocker>(); |
|
|
|
// 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(*testBlocker, blockFor(_)).LR_SIDE_EFFECT(currentTime += _1); |
|
ALLOW_CALL(*testBlocker, notify()); |
|
|
|
TaskScheduler scheduler(logger, *timeProvider, *threadMgr, |
|
std::move(testBlocker)); |
|
|
|
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 - recreate blocker for this test |
|
auto testBlocker = std::make_unique<test::MockBlocker>(); |
|
|
|
TaskScheduler scheduler(logger, *timeProvider, *threadMgr, |
|
std::move(testBlocker)); |
|
|
|
// 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); |
|
} |
|
|
|
SECTION("when invalid mode combination is used then exception is thrown") |
|
{ |
|
// Given - recreate blocker for this test |
|
auto testBlocker = std::make_unique<test::MockBlocker>(); |
|
|
|
TaskScheduler scheduler(logger, *timeProvider, *threadMgr, |
|
std::move(testBlocker)); |
|
|
|
// 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<test::MockThreadHandle>(); |
|
std::function<void()> 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); |
|
|
|
// Recreate blocker for this test |
|
auto testBlocker = std::make_unique<test::MockBlocker>(); |
|
|
|
// 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(*testBlocker, blockFor(_)); |
|
ALLOW_CALL(*testBlocker, notify()); |
|
|
|
TaskScheduler scheduler(logger, *timeProvider, *threadMgr, |
|
std::move(testBlocker)); |
|
|
|
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<test::MockThreadHandle>(); |
|
std::function<void()> 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); |
|
|
|
// Recreate blocker for this test |
|
auto testBlocker = std::make_unique<test::MockBlocker>(); |
|
|
|
// 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(*testBlocker, blockFor(_)).LR_SIDE_EFFECT(currentTime += _1); |
|
ALLOW_CALL(*testBlocker, notify()); |
|
|
|
TaskScheduler scheduler(logger, *timeProvider, *threadMgr, |
|
std::move(testBlocker)); |
|
|
|
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); |
|
} |
|
|
|
SECTION("when task is scheduled with Forever and OnStart mode then it " |
|
"executes on start and at scheduled time only") |
|
{ |
|
// Given |
|
auto threadHandle = std::make_unique<test::MockThreadHandle>(); |
|
std::function<void()> threadFn; |
|
std::vector<std::chrono::system_clock::time_point> executionTimes; |
|
auto currentTime = test::TIMEPOINT_NOW; // 2020-01-01 12:00:00 |
|
|
|
// Schedule task for 12:00:05 (5 seconds after current time) |
|
auto scheduledTimeDelta = std::chrono::seconds{5}; |
|
auto scheduledTime = currentTime + scheduledTimeDelta; |
|
|
|
// Set up thread handle expectations before moving it |
|
ALLOW_CALL(*threadHandle, join()); |
|
ALLOW_CALL(*threadHandle, joinable()).RETURN(true); |
|
|
|
// Recreate blocker for this test |
|
auto testBlocker = std::make_unique<test::MockBlocker>(); |
|
|
|
// 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); |
|
|
|
// Also add a timeout mechanism in case the scheduler doesn't execute as |
|
// expected |
|
auto timeoutTime = test::TIMEPOINT_NOW + std::chrono::minutes(2); |
|
|
|
// Allow blocker calls and simulate time passage |
|
ALLOW_CALL(*testBlocker, blockFor(_)) |
|
.LR_SIDE_EFFECT( |
|
// Advance time by the blocked amount |
|
currentTime += _1;); |
|
ALLOW_CALL(*testBlocker, notify()); |
|
|
|
TaskScheduler scheduler(logger, *timeProvider, *threadMgr, |
|
std::move(testBlocker)); |
|
|
|
auto taskFunction = [&]() { |
|
// Record the current time when this execution happens |
|
executionTimes.push_back(currentTime); |
|
|
|
// Stop after 2 executions (the expected behavior) |
|
if (executionTimes.size() >= 2) { |
|
scheduler.stop(); |
|
} |
|
}; |
|
|
|
// When - schedule task with both Forever and OnStart modes |
|
// Set time to 12:00:05 (5 seconds after our test TIMEPOINT_NOW) |
|
scheduler.schedule(taskFunction, 12, 0, 5, |
|
TaskScheduler::RunMode::Forever |
|
| TaskScheduler::RunMode::OnStart); |
|
scheduler.start(); |
|
|
|
// Execute the thread function to simulate the scheduler thread |
|
threadFn(); |
|
|
|
// Then - task should have executed exactly twice: |
|
// 1. Once immediately due to OnStart (at TIMEPOINT_NOW) |
|
// 2. Once at the scheduled time (12:00:05) |
|
// But NOT more than that (which would indicate the bug) |
|
|
|
// With the bug, executionTimes will have many entries due to infinite loop |
|
// Without the bug, we should have exactly 2 entries |
|
REQUIRE(executionTimes.size() == 2); |
|
|
|
// First execution should be at the initial time (OnStart) |
|
REQUIRE(executionTimes[0] == test::TIMEPOINT_NOW); |
|
|
|
// Second execution should be at or after the scheduled time |
|
REQUIRE(executionTimes[1] >= scheduledTime); |
|
|
|
// Verify that time has advanced appropriately |
|
// Current time should be at or after the scheduled time |
|
REQUIRE(currentTime >= scheduledTime); |
|
} |
|
}
|
|
|