Fist commit
This commit is contained in:
15
test/test_native_scheduler/FakeMonotonicClock.h
Executable file
15
test/test_native_scheduler/FakeMonotonicClock.h
Executable file
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "SessionClock.h"
|
||||
|
||||
class FakeMonotonicClock final : public timbre_core::MonotonicClock {
|
||||
public:
|
||||
explicit FakeMonotonicClock(std::uint64_t initial = 0) : value_(initial) {}
|
||||
|
||||
std::uint64_t nowMs() const override { return value_; }
|
||||
void advance(std::uint64_t milliseconds) noexcept { value_ += milliseconds; }
|
||||
void set(std::uint64_t milliseconds) noexcept { value_ = milliseconds; }
|
||||
|
||||
private:
|
||||
std::uint64_t value_;
|
||||
};
|
||||
136
test/test_native_scheduler/test_main.cpp
Executable file
136
test/test_native_scheduler/test_main.cpp
Executable file
@@ -0,0 +1,136 @@
|
||||
#include <unity.h>
|
||||
|
||||
#include "Scheduler.h"
|
||||
#include "FakeMonotonicClock.h"
|
||||
|
||||
using namespace timbre_core;
|
||||
|
||||
void setUp(void) {}
|
||||
void tearDown(void) {}
|
||||
|
||||
static AppState schedulerState() {
|
||||
AppState state;
|
||||
state.profiles.push_back(Profile{4, "Escuela"});
|
||||
state.activeProfileId = 4;
|
||||
state.patterns[0] = Pattern{PatternId::A, {2, 1, 2, 1, 2, 1}};
|
||||
state.patterns[1] = Pattern{PatternId::B, {1, 1, 1, 1, 1, 1}};
|
||||
state.patterns[2] = Pattern{PatternId::C, {1, 1, 1, 1, 1, 1}};
|
||||
// 08:30 Monday through Friday. Bit 1 is Monday.
|
||||
state.schedules.push_back(Schedule{20, 4, PatternId::A, 8 * 60 + 30, 0x3E, true});
|
||||
return state;
|
||||
}
|
||||
|
||||
static CivilDateTime mondayAt(std::uint8_t hour, std::uint8_t minute) {
|
||||
CivilDateTime value{2024, 1, 1, hour, minute, 0, 0};
|
||||
value.weekday = CivilTime::weekdayForDate(value.year, value.month, value.day);
|
||||
return value;
|
||||
}
|
||||
|
||||
static void test_first_observation_and_same_key_are_not_replayed(void) {
|
||||
Scheduler scheduler;
|
||||
const AppState state = schedulerState();
|
||||
|
||||
ScheduleDecision decision = scheduler.poll(state, mondayAt(8, 30), true);
|
||||
TEST_ASSERT_FALSE(decision.due);
|
||||
TEST_ASSERT_TRUE(decision.firstObservation);
|
||||
|
||||
decision = scheduler.poll(state, mondayAt(8, 30), false);
|
||||
TEST_ASSERT_FALSE(decision.due);
|
||||
TEST_ASSERT_TRUE(decision.alreadyProcessed);
|
||||
|
||||
decision = scheduler.poll(state, mondayAt(8, 30), false);
|
||||
TEST_ASSERT_FALSE(decision.due);
|
||||
TEST_ASSERT_TRUE(decision.alreadyProcessed);
|
||||
}
|
||||
|
||||
static void test_current_minute_is_triggered_once(void) {
|
||||
Scheduler scheduler;
|
||||
const AppState state = schedulerState();
|
||||
|
||||
TEST_ASSERT_FALSE(scheduler.poll(state, mondayAt(8, 29), true).due);
|
||||
const ScheduleDecision due = scheduler.poll(state, mondayAt(8, 30), false);
|
||||
TEST_ASSERT_TRUE(due.due);
|
||||
TEST_ASSERT_EQUAL_UINT16(20, due.scheduleId);
|
||||
TEST_ASSERT_EQUAL_UINT8(static_cast<std::uint8_t>(PatternId::A),
|
||||
static_cast<std::uint8_t>(due.patternId));
|
||||
|
||||
const ScheduleDecision duplicate = scheduler.poll(state, mondayAt(8, 30), false);
|
||||
TEST_ASSERT_FALSE(duplicate.due);
|
||||
TEST_ASSERT_TRUE(duplicate.alreadyProcessed);
|
||||
}
|
||||
|
||||
static void test_gap_does_not_replay_intermediate_minutes(void) {
|
||||
Scheduler scheduler;
|
||||
AppState state = schedulerState();
|
||||
// Only 08:31 is configured for this isolated test.
|
||||
state.schedules.clear();
|
||||
state.schedules.push_back(Schedule{21, 4, PatternId::C, 8 * 60 + 31, 0x3E, true});
|
||||
|
||||
TEST_ASSERT_FALSE(scheduler.poll(state, mondayAt(8, 30), true).due);
|
||||
const ScheduleDecision skipped = scheduler.poll(state, mondayAt(8, 33), false);
|
||||
TEST_ASSERT_FALSE(skipped.due);
|
||||
TEST_ASSERT_EQUAL_UINT64(2, skipped.missedMinutes);
|
||||
|
||||
// A wall-clock rollback cannot make the old minute ring again.
|
||||
const ScheduleDecision rollback = scheduler.poll(state, mondayAt(8, 31), false);
|
||||
TEST_ASSERT_FALSE(rollback.due);
|
||||
TEST_ASSERT_TRUE(rollback.alreadyProcessed);
|
||||
}
|
||||
|
||||
static void test_weekday_profile_and_holiday_guards(void) {
|
||||
Scheduler scheduler;
|
||||
AppState state = schedulerState();
|
||||
state.holidays.push_back(Holiday{1, 1, 1, true});
|
||||
|
||||
TEST_ASSERT_FALSE(scheduler.poll(state, mondayAt(8, 29), true).due);
|
||||
const ScheduleDecision holiday = scheduler.poll(state, mondayAt(8, 30), false);
|
||||
TEST_ASSERT_FALSE(holiday.due);
|
||||
TEST_ASSERT_TRUE(holiday.blockedByHoliday);
|
||||
|
||||
state.holidays.clear();
|
||||
state.activeProfileId = 99;
|
||||
const ScheduleDecision wrongProfile = scheduler.poll(state, mondayAt(8, 31), false);
|
||||
TEST_ASSERT_FALSE(wrongProfile.due);
|
||||
|
||||
state.activeProfileId = 4;
|
||||
const CivilDateTime sunday{2024, 1, 7, 8, 30, 0, 0};
|
||||
const ScheduleDecision wrongDay = scheduler.poll(state, sunday, false);
|
||||
TEST_ASSERT_FALSE(wrongDay.due);
|
||||
}
|
||||
|
||||
static void test_session_clock_integration_uses_first_observation_flag(void) {
|
||||
Scheduler scheduler;
|
||||
const AppState state = schedulerState();
|
||||
FakeMonotonicClock source(10);
|
||||
SessionClock clock(source);
|
||||
const CivilDateTime firstLocal = mondayAt(8, 30);
|
||||
const std::int64_t firstEpoch = CivilTime::toUnixSeconds(firstLocal);
|
||||
|
||||
const ScheduleDecision first = scheduler.poll(state, clock, firstEpoch);
|
||||
TEST_ASSERT_FALSE(first.due);
|
||||
TEST_ASSERT_TRUE(first.firstObservation);
|
||||
|
||||
source.advance(60000);
|
||||
const ScheduleDecision due = scheduler.poll(
|
||||
state, clock, CivilTime::toUnixSeconds(mondayAt(8, 31)));
|
||||
TEST_ASSERT_FALSE(due.due); // configured at 08:30, not 08:31
|
||||
|
||||
source.advance(60000);
|
||||
const CivilDateTime sameMinute = mondayAt(8, 30);
|
||||
// The local key is already consumed; moving the fake wall source back
|
||||
// must not replay it.
|
||||
const ScheduleDecision replay = scheduler.poll(state, clock,
|
||||
CivilTime::toUnixSeconds(sameMinute));
|
||||
TEST_ASSERT_FALSE(replay.due);
|
||||
TEST_ASSERT_TRUE(replay.alreadyProcessed);
|
||||
}
|
||||
|
||||
int main(int, char**) {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_first_observation_and_same_key_are_not_replayed);
|
||||
RUN_TEST(test_current_minute_is_triggered_once);
|
||||
RUN_TEST(test_gap_does_not_replay_intermediate_minutes);
|
||||
RUN_TEST(test_weekday_profile_and_holiday_guards);
|
||||
RUN_TEST(test_session_clock_integration_uses_first_observation_flag);
|
||||
return UNITY_END();
|
||||
}
|
||||
Reference in New Issue
Block a user