137 lines
5.2 KiB
C++
Executable File
137 lines
5.2 KiB
C++
Executable File
#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();
|
|
}
|