#include #include "ScheduleValidator.h" #include "TimbreService.h" using namespace timbre_core; void setUp(void) {} void tearDown(void) {} static AppState validState() { AppState state; state.profiles.push_back(Profile{7, "Principal"}); state.activeProfileId = 7; state.patterns[0] = Pattern{PatternId::A, {1, 1, 1, 1, 1, 1}}; state.patterns[1] = Pattern{PatternId::B, {2, 1, 2, 1, 2, 1}}; state.patterns[2] = Pattern{PatternId::C, {1, 2, 1, 2, 1, 2}}; return state; } static void test_limits_and_canonical_patterns(void) { TEST_ASSERT_EQUAL_size_t(8, kMaxProfiles); TEST_ASSERT_EQUAL_size_t(3, kMaxPatterns); TEST_ASSERT_EQUAL_size_t(100, kMaxSchedules); TEST_ASSERT_EQUAL_size_t(64, kMaxHolidays); TEST_ASSERT_EQUAL_size_t(8, kPatternQueueCapacity); TEST_ASSERT_EQUAL_UINT8(99, kMaxPhaseDurationSeconds); TEST_ASSERT_EQUAL_UINT16(600, kMaxPatternTotalSeconds); TEST_ASSERT_TRUE(isPatternId(PatternId::A)); TEST_ASSERT_TRUE(isPatternId(PatternId::B)); TEST_ASSERT_TRUE(isPatternId(PatternId::C)); TEST_ASSERT_FALSE(isPatternId(static_cast(3))); TEST_ASSERT_EQUAL_UINT8(22, gpioForRelay(RelayId::Main)); TEST_ASSERT_EQUAL_UINT8(23, gpioForRelay(RelayId::Reserved)); TEST_ASSERT_TRUE(isReservedRelay(RelayId::Reserved)); TEST_ASSERT_FALSE(isReservedRelay(RelayId::Main)); } static void test_valid_state_and_phase_limits(void) { AppState state = validState(); TEST_ASSERT_TRUE(ScheduleValidator::validate(state).valid); state.patterns[0].phases[0] = 100; ValidationResult result = ScheduleValidator::validate(state); TEST_ASSERT_FALSE(result.valid); TEST_ASSERT_EQUAL_UINT8(static_cast(ValidationCode::InvalidPhaseDuration), static_cast(result.code)); state = validState(); state.patterns[1].phaseCount = kMaxPatternPhases + 1; result = ScheduleValidator::validate(state); TEST_ASSERT_FALSE(result.valid); TEST_ASSERT_EQUAL_UINT8(static_cast(ValidationCode::InvalidPhaseCount), static_cast(result.code)); state = validState(); state.patterns[2].phaseCount = 6; for (std::size_t i = 0; i < 6; ++i) { state.patterns[2].phases[i] = 99; } TEST_ASSERT_TRUE(ScheduleValidator::validate(state).valid); } static void test_schedule_references_and_stable_ids(void) { AppState state = validState(); Schedule schedule{10, 7, PatternId::A, 8 * 60 + 30, 0x7F, true}; state.schedules.push_back(schedule); TEST_ASSERT_TRUE(ScheduleValidator::validate(state).valid); Schedule* found = state.schedule(10); TEST_ASSERT_NOT_NULL(found); TEST_ASSERT_EQUAL_UINT16(510, found->minuteOfDay); state.schedules[0].profileId = 99; ValidationResult result = ScheduleValidator::validate(state); TEST_ASSERT_FALSE(result.valid); TEST_ASSERT_EQUAL_UINT8(static_cast(ValidationCode::UnknownProfile), static_cast(result.code)); state = validState(); state.profiles.push_back(Profile{7, "Duplicado"}); result = ScheduleValidator::validate(state); TEST_ASSERT_FALSE(result.valid); TEST_ASSERT_EQUAL_UINT8(static_cast(ValidationCode::DuplicateProfileId), static_cast(result.code)); } static void test_holiday_validation_and_capacity(void) { AppState state = validState(); state.holidays.push_back(Holiday{1, 1, 1, true}); state.holidays.push_back(Holiday{1, 1, 2, true}); ValidationResult result = ScheduleValidator::validate(state); TEST_ASSERT_FALSE(result.valid); TEST_ASSERT_EQUAL_UINT8(static_cast(ValidationCode::DuplicateHolidayId), static_cast(result.code)); state = validState(); for (std::size_t i = 0; i < 64; ++i) { const std::uint8_t month = static_cast((i % 12) + 1); const std::uint8_t day = static_cast((i / 12) + 1); TEST_ASSERT_TRUE(state.holidays.push_back( Holiday{static_cast(i + 1), month, day, true})); } TEST_ASSERT_EQUAL_size_t(64, state.holidays.size()); TEST_ASSERT_TRUE(ScheduleValidator::validate(state).valid); state.holidays[0].month = 0; result = ScheduleValidator::validate(state); TEST_ASSERT_FALSE(result.valid); TEST_ASSERT_EQUAL_UINT8(static_cast(ValidationCode::InvalidHolidayDate), static_cast(result.code)); } static void test_schedule_and_profile_capacity(void) { AppState state; for (std::size_t i = 0; i < kMaxProfiles; ++i) { TEST_ASSERT_TRUE(state.profiles.push_back( Profile{static_cast(i + 1), "P"})); } state.activeProfileId = 1; for (std::size_t i = 0; i < kMaxSchedules; ++i) { TEST_ASSERT_TRUE(state.schedules.push_back( Schedule{static_cast(i + 1), 1, PatternId::A, 0, 0x7F, true})); } TEST_ASSERT_TRUE(ScheduleValidator::validate(state).valid); TEST_ASSERT_EQUAL_size_t(kMaxProfiles, state.profiles.capacity()); TEST_ASSERT_EQUAL_size_t(kMaxSchedules, state.schedules.capacity()); } static void test_transaction_publishes_only_after_commit(void) { MemoryStateRepository repository; AppState initial = validState(); repository.seed(initial); TimbreService service(repository); TEST_ASSERT_TRUE(service.load()); const std::size_t before = service.state().schedules.size(); repository.setCommitSucceeds(false); TEST_ASSERT_FALSE(service.transact([](AppState& candidate) { candidate.schedules.push_back(Schedule{44, 7, PatternId::B, 60, 0x7F, true}); })); TEST_ASSERT_EQUAL_size_t(before, service.state().schedules.size()); TEST_ASSERT_EQUAL_UINT8(static_cast(ServiceCode::CommitFailed), static_cast(service.lastError())); repository.setCommitSucceeds(true); TEST_ASSERT_TRUE(service.transact([](AppState& candidate) { candidate.schedules.push_back(Schedule{44, 7, PatternId::B, 60, 0x7F, true}); })); TEST_ASSERT_EQUAL_size_t(before + 1, service.state().schedules.size()); TEST_ASSERT_EQUAL_UINT16(before == 0 ? 1 : 0, service.state().schedules.back().id == 44 ? 1 : 0); } int main(int, char**) { UNITY_BEGIN(); RUN_TEST(test_limits_and_canonical_patterns); RUN_TEST(test_valid_state_and_phase_limits); RUN_TEST(test_schedule_references_and_stable_ids); RUN_TEST(test_holiday_validation_and_capacity); RUN_TEST(test_schedule_and_profile_capacity); RUN_TEST(test_transaction_publishes_only_after_commit); return UNITY_END(); }