Multiple implementations of the same back-end application. The aim is to provide quick, side-by-side comparisons of different technologies (languages, frameworks, libraries) while preserving consistent business logic across all implementations.
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

#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);
}
// }
}