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.
 
 
 
 
 
 

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