// Tests for the ESP32 logic (src/esp32/logic), run on the PC: pio test -e test_esp32 // // The display controller is tested with a fake power switch and fake frames that record // what the controller asks for. #include #include #include #include #include #include "config.h" #include "logic/display_controller.h" #include "logic/power_timer.h" #include "logic/schedule.h" #include "logic/status_led.h" // --------------------------------------------------------------------------- // Fakes // --------------------------------------------------------------------------- class FakePower : public DisplayPower { public: bool is_on = false; int on_count = 0; int off_count = 0; bool isOn() override { return is_on; } void on() override { is_on = true; on_count++; } void off() override { is_on = false; off_count++; } }; class FakeFrames : public Frames { public: std::vector shown; void clock(int hour, int minute) override { char s[16]; snprintf(s, sizeof(s), "clock %02d:%02d", hour, minute); shown.push_back(s); } void logo() override { shown.push_back("logo"); } void blank() override { shown.push_back("blank"); } std::string last() const { return shown.empty() ? "" : shown.back(); } }; static FakePower power; static FakeFrames frames; static DisplayController *controller; static uint32_t now; void setUp() { power = FakePower(); frames = FakeFrames(); delete controller; controller = new DisplayController(power, frames); now = 1000; } void tearDown() {} // Run the controller at a time of day; `ms` passes in steps like the real loop static void run(int hour, int minute, uint32_t ms = 0) { uint32_t end = now + ms; do { controller->update(now, true, hour, minute); now += 100; } while ((int32_t)(end - now) >= 0); } // --------------------------------------------------------------------------- // Schedule // --------------------------------------------------------------------------- void test_schedule_night_until_8() { TEST_ASSERT_TRUE(scheduleFor(0, 0) == ScheduleMode::Night); TEST_ASSERT_TRUE(scheduleFor(7, 0) == ScheduleMode::Night); TEST_ASSERT_TRUE(scheduleFor(7, 59) == ScheduleMode::Night); } void test_schedule_clock_from_8_to_22() { TEST_ASSERT_TRUE(scheduleFor(8, 0) == ScheduleMode::Clock); TEST_ASSERT_TRUE(scheduleFor(12, 34) == ScheduleMode::Clock); TEST_ASSERT_TRUE(scheduleFor(21, 59) == ScheduleMode::Clock); } void test_schedule_quarter_hours_until_23_30() { TEST_ASSERT_TRUE(scheduleFor(22, 0) == ScheduleMode::Clock); TEST_ASSERT_TRUE(scheduleFor(22, 1) == ScheduleMode::Standby); TEST_ASSERT_TRUE(scheduleFor(22, 14) == ScheduleMode::Standby); TEST_ASSERT_TRUE(scheduleFor(22, 15) == ScheduleMode::Clock); TEST_ASSERT_TRUE(scheduleFor(22, 45) == ScheduleMode::Clock); TEST_ASSERT_TRUE(scheduleFor(23, 0) == ScheduleMode::Clock); TEST_ASSERT_TRUE(scheduleFor(23, 29) == ScheduleMode::Standby); TEST_ASSERT_TRUE(scheduleFor(23, 30) == ScheduleMode::Clock); } void test_schedule_night_from_23_31() { TEST_ASSERT_TRUE(scheduleFor(23, 31) == ScheduleMode::Night); TEST_ASSERT_TRUE(scheduleFor(23, 45) == ScheduleMode::Night); TEST_ASSERT_TRUE(scheduleFor(23, 59) == ScheduleMode::Night); } void test_evening_from_17() { TEST_ASSERT_FALSE(isEvening(16)); TEST_ASSERT_TRUE(isEvening(17)); TEST_ASSERT_TRUE(isEvening(23)); } // --------------------------------------------------------------------------- // Power timer // --------------------------------------------------------------------------- void test_timer_expires_once_after_delay() { PowerTimer t; t.request(1000, 500); TEST_ASSERT_FALSE(t.expired(1499)); TEST_ASSERT_TRUE(t.expired(1500)); TEST_ASSERT_FALSE(t.expired(1600)); } void test_timer_keeps_earlier_deadline() { PowerTimer t; t.request(0, 1000); t.request(500, 1000); // later deadline: ignored TEST_ASSERT_TRUE(t.expired(1000)); t.request(0, 30000); t.request(100, 1000); // earlier deadline: used TEST_ASSERT_TRUE(t.expired(1100)); } void test_timer_cancel() { PowerTimer t; t.request(0, 100); t.cancel(); TEST_ASSERT_FALSE(t.pending()); TEST_ASSERT_FALSE(t.expired(1000)); } void test_timer_across_millis_overflow() { PowerTimer t; t.request(0xFFFFFF00u, 0x200); // deadline wraps past 0 TEST_ASSERT_FALSE(t.expired(0xFFFFFFFFu)); TEST_ASSERT_FALSE(t.expired(0x00000050u)); TEST_ASSERT_TRUE(t.expired(0x00000100u)); } // --------------------------------------------------------------------------- // Status LED // --------------------------------------------------------------------------- void test_led_off_while_display_off() { for (uint32_t t = 0; t < 2000; t += 50) TEST_ASSERT_FALSE(statusLedLevel(t, true, false)); } void test_led_blinks_slow_with_wifi_and_fast_without() { TEST_ASSERT_TRUE(statusLedLevel(0, true, true)); TEST_ASSERT_TRUE(statusLedLevel(499, true, true)); TEST_ASSERT_FALSE(statusLedLevel(500, true, true)); TEST_ASSERT_TRUE(statusLedLevel(0, false, true)); TEST_ASSERT_FALSE(statusLedLevel(100, false, true)); TEST_ASSERT_TRUE(statusLedLevel(200, false, true)); } // --------------------------------------------------------------------------- // Display controller // --------------------------------------------------------------------------- void test_nothing_happens_until_time_is_synced() { power.is_on = true; // showing "Connecting..." controller->update(now, false, 0, 0); TEST_ASSERT_EQUAL(0, power.off_count); TEST_ASSERT_EQUAL(0, (int)frames.shown.size()); } void test_daytime_powers_on_and_shows_clock() { run(12, 34); TEST_ASSERT_TRUE(power.is_on); TEST_ASSERT_EQUAL_STRING("clock 12:34", frames.last().c_str()); } void test_clock_is_only_sent_when_the_minute_changes() { run(12, 34, 5000); TEST_ASSERT_EQUAL(1, (int)frames.shown.size()); run(12, 35); TEST_ASSERT_EQUAL(2, (int)frames.shown.size()); TEST_ASSERT_EQUAL_STRING("clock 12:35", frames.last().c_str()); TEST_ASSERT_EQUAL(1, power.on_count); } void test_night_clears_then_powers_off_after_delay() { run(23, 30); run(23, 31, POWER_OFF_DELAY - 200); TEST_ASSERT_EQUAL_STRING("blank", frames.last().c_str()); TEST_ASSERT_TRUE(power.is_on); run(23, 31, 300); TEST_ASSERT_FALSE(power.is_on); TEST_ASSERT_EQUAL(1, power.off_count); } void test_night_does_not_power_on() { run(3, 0, 60000); TEST_ASSERT_EQUAL(0, power.on_count); TEST_ASSERT_EQUAL(0, (int)frames.shown.size()); } void test_standby_keeps_the_last_time_and_powers_off() { run(22, 0); run(22, 1, POWER_OFF_DELAY + 200); TEST_ASSERT_FALSE(power.is_on); TEST_ASSERT_EQUAL_STRING("clock 22:00", frames.last().c_str()); // no blank } void test_quarter_hour_powers_on_again_and_redraws() { run(22, 1, POWER_OFF_DELAY + 200); run(22, 14); TEST_ASSERT_FALSE(power.is_on); run(22, 15); TEST_ASSERT_TRUE(power.is_on); TEST_ASSERT_EQUAL_STRING("clock 22:15", frames.last().c_str()); } void test_clock_cancels_pending_power_off() { run(22, 0); run(22, 14, 10000); // power off countdown running TEST_ASSERT_TRUE(power.is_on); run(22, 15, POWER_OFF_DELAY); TEST_ASSERT_TRUE(power.is_on); TEST_ASSERT_EQUAL(0, power.off_count); } void test_power_switch_off_clears_and_powers_off_quickly() { run(12, 0); run(22, 14, 1000); // 30 s countdown already running controller->setPowerEnabled(false); run(22, 14, CLEAR_BEFORE_OFF_TIME + 200); TEST_ASSERT_EQUAL_STRING("blank", frames.last().c_str()); TEST_ASSERT_FALSE(power.is_on); } void test_power_switch_off_keeps_display_off_during_the_day() { controller->setPowerEnabled(false); run(12, 0, 60000); TEST_ASSERT_EQUAL(0, power.on_count); } void test_power_switch_on_again_shows_clock() { run(12, 0); controller->setPowerEnabled(false); run(12, 0, CLEAR_BEFORE_OFF_TIME + 200); controller->setPowerEnabled(true); run(12, 1); TEST_ASSERT_TRUE(power.is_on); TEST_ASSERT_EQUAL_STRING("clock 12:01", frames.last().c_str()); } void test_clock_switch_off_shows_logo_then_powers_off() { run(12, 0); controller->setClockEnabled(false); run(12, 0, POWER_OFF_DELAY + 200); TEST_ASSERT_EQUAL_STRING("logo", frames.last().c_str()); TEST_ASSERT_EQUAL(1, (int)std::count(frames.shown.begin(), frames.shown.end(), std::string("logo"))); TEST_ASSERT_FALSE(power.is_on); } void test_clock_switch_off_never_powers_on() { controller->setClockEnabled(false); run(12, 0, 60000); TEST_ASSERT_EQUAL(0, power.on_count); } void test_clock_switch_on_again_shows_clock() { run(12, 0); controller->setClockEnabled(false); run(12, 0); controller->setClockEnabled(true); run(12, 0); TEST_ASSERT_EQUAL_STRING("clock 12:00", frames.last().c_str()); } void test_redraws_after_power_was_switched_off_elsewhere() { // e.g. after flashing the Nano: the Nano restarts without a frame run(12, 0); power.off(); run(12, 0); TEST_ASSERT_TRUE(power.is_on); TEST_ASSERT_EQUAL(2, (int)frames.shown.size()); TEST_ASSERT_EQUAL_STRING("clock 12:00", frames.last().c_str()); } int main() { UNITY_BEGIN(); RUN_TEST(test_schedule_night_until_8); RUN_TEST(test_schedule_clock_from_8_to_22); RUN_TEST(test_schedule_quarter_hours_until_23_30); RUN_TEST(test_schedule_night_from_23_31); RUN_TEST(test_evening_from_17); RUN_TEST(test_timer_expires_once_after_delay); RUN_TEST(test_timer_keeps_earlier_deadline); RUN_TEST(test_timer_cancel); RUN_TEST(test_timer_across_millis_overflow); RUN_TEST(test_led_off_while_display_off); RUN_TEST(test_led_blinks_slow_with_wifi_and_fast_without); RUN_TEST(test_nothing_happens_until_time_is_synced); RUN_TEST(test_daytime_powers_on_and_shows_clock); RUN_TEST(test_clock_is_only_sent_when_the_minute_changes); RUN_TEST(test_night_clears_then_powers_off_after_delay); RUN_TEST(test_night_does_not_power_on); RUN_TEST(test_standby_keeps_the_last_time_and_powers_off); RUN_TEST(test_quarter_hour_powers_on_again_and_redraws); RUN_TEST(test_clock_cancels_pending_power_off); RUN_TEST(test_power_switch_off_clears_and_powers_off_quickly); RUN_TEST(test_power_switch_off_keeps_display_off_during_the_day); RUN_TEST(test_power_switch_on_again_shows_clock); RUN_TEST(test_clock_switch_off_shows_logo_then_powers_off); RUN_TEST(test_clock_switch_off_never_powers_on); RUN_TEST(test_clock_switch_on_again_shows_clock); RUN_TEST(test_redraws_after_power_was_switched_off_elsewhere); return UNITY_END(); }