#include #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(PatternId::A), static_cast(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(); }