Fist commit

This commit is contained in:
Angel Ivan
2026-10-02 12:08:21 -06:00
commit 2a9eabce14
54 changed files with 10737 additions and 0 deletions

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