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.
326 lines
10 KiB
326 lines
10 KiB
#include "infrastructure/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::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<test::TestLogger>(); |
|
auto timeProvider = std::make_shared<test::MockTimeProvider>(); |
|
auto threadMgr = std::make_shared<test::MockThreadManager>(); |
|
auto blocker = std::make_shared<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, 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<void()> threadFn; |
|
|
|
// 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(*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<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); |
|
|
|
// 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<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); |
|
|
|
// 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<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); |
|
|
|
// 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<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); |
|
|
|
// 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); |
|
} |
|
// } |
|
}
|
|
|