Fist commit
This commit is contained in:
11
test/README
Executable file
11
test/README
Executable file
@@ -0,0 +1,11 @@
|
||||
|
||||
This directory is intended for PlatformIO Test Runner and project tests.
|
||||
|
||||
Unit Testing is a software testing method by which individual units of
|
||||
source code, sets of one or more MCU program modules together with associated
|
||||
control data, usage procedures, and operating procedures, are tested to
|
||||
determine whether they are fit for use. Unit testing finds problems early
|
||||
in the development cycle.
|
||||
|
||||
More information about PlatformIO Unit Testing:
|
||||
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html
|
||||
15
test/test_native_civil_time/FakeMonotonicClock.h
Executable file
15
test/test_native_civil_time/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_;
|
||||
};
|
||||
109
test/test_native_civil_time/test_main.cpp
Executable file
109
test/test_native_civil_time/test_main.cpp
Executable file
@@ -0,0 +1,109 @@
|
||||
#include <unity.h>
|
||||
|
||||
#include "CivilTime.h"
|
||||
#include "SessionClock.h"
|
||||
#include "FakeMonotonicClock.h"
|
||||
|
||||
using namespace timbre_core;
|
||||
|
||||
void setUp(void) {}
|
||||
void tearDown(void) {}
|
||||
|
||||
static void test_fixed_utc_minus_six_conversion(void) {
|
||||
// 2024-01-01 00:00 UTC is 2023-12-31 18:00 in the fixed local zone.
|
||||
const CivilDateTime local = CivilTime::fromUnixSeconds(1704067200LL);
|
||||
TEST_ASSERT_EQUAL_INT(2023, local.year);
|
||||
TEST_ASSERT_EQUAL_INT(12, local.month);
|
||||
TEST_ASSERT_EQUAL_INT(31, local.day);
|
||||
TEST_ASSERT_EQUAL_INT(18, local.hour);
|
||||
TEST_ASSERT_EQUAL_INT(0, local.minute);
|
||||
TEST_ASSERT_EQUAL_INT(0, local.second);
|
||||
TEST_ASSERT_EQUAL_INT(0, local.weekday); // Sunday
|
||||
}
|
||||
|
||||
static void test_leap_day_and_round_trip(void) {
|
||||
CivilDateTime leap;
|
||||
leap.year = 2024;
|
||||
leap.month = 2;
|
||||
leap.day = 29;
|
||||
leap.hour = 12;
|
||||
leap.minute = 34;
|
||||
leap.second = 56;
|
||||
leap.weekday = CivilTime::weekdayForDate(leap.year, leap.month, leap.day);
|
||||
|
||||
TEST_ASSERT_TRUE(leap.valid());
|
||||
std::int64_t epoch = 0;
|
||||
TEST_ASSERT_TRUE(CivilTime::toUnixSeconds(leap, epoch));
|
||||
const CivilDateTime roundTrip = CivilTime::fromUnixSeconds(epoch);
|
||||
TEST_ASSERT_TRUE(roundTrip == leap);
|
||||
|
||||
TEST_ASSERT_TRUE(CivilTime::isLeapYear(2024));
|
||||
TEST_ASSERT_FALSE(CivilTime::isLeapYear(1900));
|
||||
TEST_ASSERT_TRUE(CivilTime::isLeapYear(2000));
|
||||
TEST_ASSERT_EQUAL_INT(29, CivilTime::daysInMonth(2024, 2));
|
||||
TEST_ASSERT_EQUAL_INT(28, CivilTime::daysInMonth(1900, 2));
|
||||
}
|
||||
|
||||
static void test_pre_epoch_values_use_floor_seconds(void) {
|
||||
const CivilDateTime local = CivilTime::fromUnixSeconds(-1);
|
||||
TEST_ASSERT_EQUAL_INT(1969, local.year);
|
||||
TEST_ASSERT_EQUAL_INT(12, local.month);
|
||||
TEST_ASSERT_EQUAL_INT(31, local.day);
|
||||
// UTC-1 is 17:59:59 in the fixed UTC-06:00 local zone.
|
||||
TEST_ASSERT_EQUAL_INT(17, local.hour);
|
||||
TEST_ASSERT_EQUAL_INT(59, local.minute);
|
||||
TEST_ASSERT_EQUAL_INT(59, local.second);
|
||||
|
||||
const CivilDateTime invalid{CivilDateTime{2023, 2, 29, 0, 0, 0, 0}};
|
||||
TEST_ASSERT_FALSE(invalid.valid());
|
||||
}
|
||||
|
||||
static void test_date_and_minute_keys_are_stable(void) {
|
||||
CivilDateTime value{2024, 1, 1, 8, 5, 0, 0};
|
||||
const LocalMinuteKey first = LocalMinuteKey::from(value);
|
||||
TEST_ASSERT_EQUAL_INT64(20240101LL, first.date);
|
||||
TEST_ASSERT_EQUAL_INT(485, first.minuteOfDay);
|
||||
|
||||
// Keep the same local time so the serial difference is exactly one day.
|
||||
CivilDateTime nextDay{2024, 1, 2, 8, 5, 0, 0};
|
||||
const LocalMinuteKey second = LocalMinuteKey::from(nextDay);
|
||||
TEST_ASSERT_TRUE(first < second);
|
||||
TEST_ASSERT_TRUE(first != second);
|
||||
TEST_ASSERT_EQUAL_INT64(1LL * 1440LL, second.serial() - first.serial());
|
||||
}
|
||||
|
||||
static void test_session_clock_anchors_first_observation_without_catchup(void) {
|
||||
FakeMonotonicClock source(100);
|
||||
SessionClock clock(source);
|
||||
|
||||
const SessionClock::Observation first = clock.observe(1704067200LL);
|
||||
TEST_ASSERT_TRUE(first.valid);
|
||||
TEST_ASSERT_TRUE(first.firstObservation);
|
||||
TEST_ASSERT_EQUAL_UINT64(100, first.monotonicMs);
|
||||
|
||||
source.advance(5000);
|
||||
const SessionClock::Observation later = clock.observe(1704067205LL);
|
||||
TEST_ASSERT_TRUE(later.valid);
|
||||
TEST_ASSERT_FALSE(later.firstObservation);
|
||||
TEST_ASSERT_FALSE(later.monotonicRegression);
|
||||
TEST_ASSERT_EQUAL_INT(2023, later.local.year);
|
||||
TEST_ASSERT_EQUAL_INT(18, later.local.hour);
|
||||
TEST_ASSERT_EQUAL_INT(0, later.local.minute);
|
||||
TEST_ASSERT_EQUAL_INT(5, later.local.second);
|
||||
|
||||
source.advance(1000);
|
||||
const SessionClock::Observation tick = clock.tick();
|
||||
TEST_ASSERT_TRUE(tick.valid);
|
||||
TEST_ASSERT_EQUAL_INT(6, tick.local.second);
|
||||
TEST_ASSERT_EQUAL_UINT64(6000, clock.sessionElapsedMs());
|
||||
}
|
||||
|
||||
int main(int, char**) {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_fixed_utc_minus_six_conversion);
|
||||
RUN_TEST(test_leap_day_and_round_trip);
|
||||
RUN_TEST(test_pre_epoch_values_use_floor_seconds);
|
||||
RUN_TEST(test_date_and_minute_keys_are_stable);
|
||||
RUN_TEST(test_session_clock_anchors_first_observation_without_catchup);
|
||||
return UNITY_END();
|
||||
}
|
||||
153
test/test_native_pattern_engine/test_main.cpp
Executable file
153
test/test_native_pattern_engine/test_main.cpp
Executable file
@@ -0,0 +1,153 @@
|
||||
#include <unity.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "PatternEngine.h"
|
||||
|
||||
using namespace timbre_core;
|
||||
|
||||
void setUp(void) {}
|
||||
void tearDown(void) {}
|
||||
|
||||
struct OutputEvent {
|
||||
RelayId relay;
|
||||
bool on;
|
||||
};
|
||||
|
||||
static void test_absolute_deadlines_and_final_off(void) {
|
||||
std::vector<OutputEvent> events;
|
||||
PatternEngine engine([&events](RelayId relay, bool on) {
|
||||
events.push_back(OutputEvent{relay, on});
|
||||
});
|
||||
const Pattern pattern{PatternId::A, {2, 1, 1, 1}};
|
||||
|
||||
TEST_ASSERT_TRUE(engine.start(pattern, 1000));
|
||||
TEST_ASSERT_TRUE(engine.running());
|
||||
TEST_ASSERT_TRUE(engine.outputOn());
|
||||
TEST_ASSERT_EQUAL_UINT64(3000, engine.deadlineMs());
|
||||
TEST_ASSERT_EQUAL_size_t(1, events.size());
|
||||
TEST_ASSERT_TRUE(events[0].on);
|
||||
TEST_ASSERT_EQUAL_UINT8(static_cast<std::uint8_t>(RelayId::Main),
|
||||
static_cast<std::uint8_t>(events[0].relay));
|
||||
|
||||
TEST_ASSERT_FALSE(engine.update(2999));
|
||||
TEST_ASSERT_TRUE(engine.outputOn());
|
||||
TEST_ASSERT_TRUE(engine.update(3000));
|
||||
TEST_ASSERT_FALSE(engine.outputOn());
|
||||
TEST_ASSERT_EQUAL_UINT8(1, engine.activePhase());
|
||||
TEST_ASSERT_EQUAL_UINT64(4000, engine.deadlineMs());
|
||||
|
||||
TEST_ASSERT_TRUE(engine.update(5000));
|
||||
// At 5000 ms the third phase has also reached its boundary; the
|
||||
// absolute-deadline engine therefore advances to the final OFF phase.
|
||||
TEST_ASSERT_FALSE(engine.outputOn());
|
||||
TEST_ASSERT_EQUAL_UINT8(3, engine.activePhase());
|
||||
TEST_ASSERT_EQUAL_UINT64(6000, engine.deadlineMs());
|
||||
|
||||
TEST_ASSERT_TRUE(engine.update(7000));
|
||||
TEST_ASSERT_FALSE(engine.running());
|
||||
TEST_ASSERT_FALSE(engine.outputOn());
|
||||
TEST_ASSERT_EQUAL_UINT8(PatternEngine::kNoPhase, engine.activePhase());
|
||||
TEST_ASSERT_EQUAL_size_t(4, events.size());
|
||||
TEST_ASSERT_FALSE(events.back().on);
|
||||
}
|
||||
|
||||
static void test_clock_regression_turns_output_off(void) {
|
||||
std::vector<OutputEvent> events;
|
||||
PatternEngine engine([&events](RelayId relay, bool on) {
|
||||
events.push_back({relay, on});
|
||||
});
|
||||
const Pattern pattern{PatternId::A, {2, 1}};
|
||||
|
||||
TEST_ASSERT_TRUE(engine.start(pattern, 1000));
|
||||
TEST_ASSERT_TRUE(engine.outputOn());
|
||||
TEST_ASSERT_TRUE(engine.update(500));
|
||||
TEST_ASSERT_FALSE(engine.running());
|
||||
TEST_ASSERT_FALSE(engine.outputOn());
|
||||
TEST_ASSERT_FALSE(events.empty());
|
||||
TEST_ASSERT_FALSE(events.back().on);
|
||||
}
|
||||
|
||||
static void test_late_poll_does_not_accumulate_or_leave_on(void) {
|
||||
std::vector<OutputEvent> events;
|
||||
PatternEngine engine([&events](RelayId, bool on) { events.push_back({RelayId::Main, on}); });
|
||||
const Pattern pattern{PatternId::B, {1, 1, 1, 1, 1, 1}};
|
||||
|
||||
TEST_ASSERT_TRUE(engine.start(pattern, 10));
|
||||
TEST_ASSERT_TRUE(engine.update(1000000));
|
||||
TEST_ASSERT_FALSE(engine.running());
|
||||
TEST_ASSERT_FALSE(engine.outputOn());
|
||||
TEST_ASSERT_EQUAL_UINT64(0, engine.deadlineMs());
|
||||
TEST_ASSERT_FALSE(events.empty());
|
||||
TEST_ASSERT_FALSE(events.back().on);
|
||||
}
|
||||
|
||||
static void test_fixed_queue_capacity_and_absolute_request_time(void) {
|
||||
std::vector<OutputEvent> events;
|
||||
PatternEngine engine([&events](RelayId relay, bool on) {
|
||||
events.push_back({relay, on});
|
||||
});
|
||||
const Pattern pattern{PatternId::C, {1, 1}};
|
||||
|
||||
for (std::size_t i = 0; i < 8; ++i) {
|
||||
TEST_ASSERT_TRUE(engine.enqueue(pattern, 1000));
|
||||
}
|
||||
TEST_ASSERT_EQUAL_size_t(8, engine.queueSize());
|
||||
TEST_ASSERT_FALSE(engine.enqueue(pattern, 1000));
|
||||
TEST_ASSERT_EQUAL_size_t(8, engine.queueSize());
|
||||
TEST_ASSERT_FALSE(engine.running());
|
||||
|
||||
engine.update(999);
|
||||
TEST_ASSERT_FALSE(engine.running());
|
||||
TEST_ASSERT_EQUAL_size_t(8, engine.queueSize());
|
||||
engine.update(1000);
|
||||
TEST_ASSERT_TRUE(engine.running());
|
||||
TEST_ASSERT_EQUAL_size_t(7, engine.queueSize());
|
||||
TEST_ASSERT_TRUE(engine.outputOn());
|
||||
TEST_ASSERT_EQUAL_UINT64(2000, engine.deadlineMs());
|
||||
}
|
||||
|
||||
static void test_invalid_pattern_id_and_zero_phase_are_safe(void) {
|
||||
std::vector<OutputEvent> events;
|
||||
PatternEngine engine([&events](RelayId relay, bool on) {
|
||||
events.push_back({relay, on});
|
||||
});
|
||||
|
||||
TEST_ASSERT_FALSE(engine.start(static_cast<PatternId>(99), 0));
|
||||
TEST_ASSERT_FALSE(engine.running());
|
||||
TEST_ASSERT_TRUE(events.empty());
|
||||
|
||||
const Pattern silent{PatternId::A, {0, 0}};
|
||||
TEST_ASSERT_TRUE(engine.start(silent, 0));
|
||||
TEST_ASSERT_FALSE(engine.running());
|
||||
TEST_ASSERT_FALSE(engine.outputOn());
|
||||
TEST_ASSERT_EQUAL_size_t(0, events.size());
|
||||
}
|
||||
|
||||
static void test_reserved_gpio_is_never_driven(void) {
|
||||
std::vector<OutputEvent> events;
|
||||
PatternEngine engine([&events](RelayId relay, bool on) {
|
||||
events.push_back({relay, on});
|
||||
});
|
||||
const Pattern pattern{PatternId::A, {1, 1}};
|
||||
TEST_ASSERT_TRUE(engine.start(pattern, 0));
|
||||
TEST_ASSERT_TRUE(engine.update(2000));
|
||||
TEST_ASSERT_FALSE(engine.reservedRelayDriven());
|
||||
TEST_ASSERT_EQUAL_UINT8(static_cast<std::uint8_t>(RelayId::Main),
|
||||
static_cast<std::uint8_t>(engine.lastOutputRelay()));
|
||||
for (const OutputEvent& event : events) {
|
||||
TEST_ASSERT_EQUAL_UINT8(static_cast<std::uint8_t>(RelayId::Main),
|
||||
static_cast<std::uint8_t>(event.relay));
|
||||
}
|
||||
}
|
||||
|
||||
int main(int, char**) {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_absolute_deadlines_and_final_off);
|
||||
RUN_TEST(test_clock_regression_turns_output_off);
|
||||
RUN_TEST(test_late_poll_does_not_accumulate_or_leave_on);
|
||||
RUN_TEST(test_fixed_queue_capacity_and_absolute_request_time);
|
||||
RUN_TEST(test_invalid_pattern_id_and_zero_phase_are_safe);
|
||||
RUN_TEST(test_reserved_gpio_is_never_driven);
|
||||
return UNITY_END();
|
||||
}
|
||||
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();
|
||||
}
|
||||
165
test/test_native_validation/test_main.cpp
Executable file
165
test/test_native_validation/test_main.cpp
Executable file
@@ -0,0 +1,165 @@
|
||||
#include <unity.h>
|
||||
|
||||
#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<PatternId>(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<std::uint8_t>(ValidationCode::InvalidPhaseDuration),
|
||||
static_cast<std::uint8_t>(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<std::uint8_t>(ValidationCode::InvalidPhaseCount),
|
||||
static_cast<std::uint8_t>(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<std::uint8_t>(ValidationCode::UnknownProfile),
|
||||
static_cast<std::uint8_t>(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<std::uint8_t>(ValidationCode::DuplicateProfileId),
|
||||
static_cast<std::uint8_t>(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<std::uint8_t>(ValidationCode::DuplicateHolidayId),
|
||||
static_cast<std::uint8_t>(result.code));
|
||||
|
||||
state = validState();
|
||||
for (std::size_t i = 0; i < 64; ++i) {
|
||||
const std::uint8_t month = static_cast<std::uint8_t>((i % 12) + 1);
|
||||
const std::uint8_t day = static_cast<std::uint8_t>((i / 12) + 1);
|
||||
TEST_ASSERT_TRUE(state.holidays.push_back(
|
||||
Holiday{static_cast<HolidayId>(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<std::uint8_t>(ValidationCode::InvalidHolidayDate),
|
||||
static_cast<std::uint8_t>(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<ProfileId>(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<ScheduleId>(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<std::uint8_t>(ServiceCode::CommitFailed),
|
||||
static_cast<std::uint8_t>(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();
|
||||
}
|
||||
Reference in New Issue
Block a user