// 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 #include "config.h" #include "logic/bitmap_format.h" #include "logic/boot_policy.h" #include "logic/display_controller.h" #include "logic/power_timer.h" #include "logic/schedule.h" #include "logic/settings.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, bool evening) override { char s[24]; snprintf(s, sizeof(s), "clock %02d:%02d%s", hour, minute, evening ? " evening" : ""); shown.push_back(s); } void page() override { shown.push_back("page"); } void live() override { shown.push_back("live"); } void noWifi(int hour, int minute) override { char s[16]; snprintf(s, sizeof(s), "nowifi %02d:%02d", hour, minute); shown.push_back(s); } void off() override { shown.push_back("off"); } std::string last() const { return shown.empty() ? "" : shown.back(); } }; static const Settings defaults; static FakePower power; static FakeFrames frames; static DisplayController *controller; static uint32_t now; static bool wifi; void setUp() { power = FakePower(); frames = FakeFrames(); delete controller; controller = new DisplayController(power, frames); now = 1000; wifi = true; } 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, wifi); now += 100; } while ((int32_t)(end - now) >= 0); } // --------------------------------------------------------------------------- // Schedule // --------------------------------------------------------------------------- void test_schedule_night_until_8() { TEST_ASSERT_TRUE(scheduleFor(0, 0, defaults.schedule) == ScheduleMode::Night); TEST_ASSERT_TRUE(scheduleFor(7, 0, defaults.schedule) == ScheduleMode::Night); TEST_ASSERT_TRUE(scheduleFor(7, 59, defaults.schedule) == ScheduleMode::Night); } void test_schedule_clock_from_8_to_22() { TEST_ASSERT_TRUE(scheduleFor(8, 0, defaults.schedule) == ScheduleMode::Clock); TEST_ASSERT_TRUE(scheduleFor(12, 34, defaults.schedule) == ScheduleMode::Clock); TEST_ASSERT_TRUE(scheduleFor(21, 59, defaults.schedule) == ScheduleMode::Clock); } void test_schedule_quarter_hours_until_23_30() { TEST_ASSERT_TRUE(scheduleFor(22, 0, defaults.schedule) == ScheduleMode::Clock); TEST_ASSERT_TRUE(scheduleFor(22, 1, defaults.schedule) == ScheduleMode::Standby); TEST_ASSERT_TRUE(scheduleFor(22, 14, defaults.schedule) == ScheduleMode::Standby); TEST_ASSERT_TRUE(scheduleFor(22, 15, defaults.schedule) == ScheduleMode::Clock); TEST_ASSERT_TRUE(scheduleFor(22, 45, defaults.schedule) == ScheduleMode::Clock); TEST_ASSERT_TRUE(scheduleFor(23, 0, defaults.schedule) == ScheduleMode::Clock); TEST_ASSERT_TRUE(scheduleFor(23, 29, defaults.schedule) == ScheduleMode::Standby); TEST_ASSERT_TRUE(scheduleFor(23, 30, defaults.schedule) == ScheduleMode::Clock); } void test_schedule_night_from_23_31() { TEST_ASSERT_TRUE(scheduleFor(23, 31, defaults.schedule) == ScheduleMode::Night); TEST_ASSERT_TRUE(scheduleFor(23, 45, defaults.schedule) == ScheduleMode::Night); TEST_ASSERT_TRUE(scheduleFor(23, 59, defaults.schedule) == ScheduleMode::Night); } void test_evening_from_17() { TEST_ASSERT_FALSE(isEvening(16, 59, defaults.clock)); TEST_ASSERT_TRUE(isEvening(17, 0, defaults.clock)); TEST_ASSERT_TRUE(isEvening(23, 0, defaults.clock)); } // --------------------------------------------------------------------------- // 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)); } // --------------------------------------------------------------------------- // Boot policy (keeping OTA alive) // --------------------------------------------------------------------------- void test_power_on_starts_normally_and_clears_crashes() { BootDecision d = decideBoot(ResetKind::PowerOn, 2); TEST_ASSERT_TRUE(d.mode == BootMode::Normal); TEST_ASSERT_EQUAL(0, d.crash_count); } void test_software_reset_starts_normally_and_keeps_count() { // e.g. the restart after an OTA update BootDecision d = decideBoot(ResetKind::Software, 1); TEST_ASSERT_TRUE(d.mode == BootMode::Normal); TEST_ASSERT_EQUAL(1, d.crash_count); } void test_other_reset_starts_normally_and_keeps_count() { BootDecision d = decideBoot(ResetKind::Other, 2); TEST_ASSERT_TRUE(d.mode == BootMode::Normal); TEST_ASSERT_EQUAL(2, d.crash_count); } void test_first_crashes_give_a_panic_window() { BootDecision d = decideBoot(ResetKind::Crash, 0); TEST_ASSERT_TRUE(d.mode == BootMode::PanicWindow); TEST_ASSERT_EQUAL(1, d.crash_count); d = decideBoot(ResetKind::Crash, d.crash_count); TEST_ASSERT_TRUE(d.mode == BootMode::PanicWindow); TEST_ASSERT_EQUAL(2, d.crash_count); } void test_repeated_crashes_give_safe_mode() { uint8_t count = 0; BootDecision d = {BootMode::Normal, 0}; for (int i = 0; i < SAFE_MODE_CRASHES; i++) { d = decideBoot(ResetKind::Crash, count); count = d.crash_count; } TEST_ASSERT_TRUE(d.mode == BootMode::SafeMode); } void test_after_safe_mode_one_more_crash_gives_safe_mode_again() { uint8_t count = crashCountAfterSafeMode(); TEST_ASSERT_TRUE(decideBoot(ResetKind::Software, count).mode == BootMode::Normal); TEST_ASSERT_TRUE(decideBoot(ResetKind::Crash, count).mode == BootMode::SafeMode); } void test_crash_count_does_not_overflow() { BootDecision d = decideBoot(ResetKind::Crash, 255); TEST_ASSERT_EQUAL(255, d.crash_count); TEST_ASSERT_TRUE(d.mode == BootMode::SafeMode); } void test_new_firmware_needs_time_and_wifi_to_be_accepted() { TEST_ASSERT_FALSE(shouldMarkFirmwareValid(FIRMWARE_VALID_TIME - 1, true)); TEST_ASSERT_FALSE(shouldMarkFirmwareValid(FIRMWARE_VALID_TIME, false)); TEST_ASSERT_TRUE(shouldMarkFirmwareValid(FIRMWARE_VALID_TIME, true)); } void test_crash_count_cleared_after_running_long_enough() { TEST_ASSERT_FALSE(shouldClearCrashCount(CRASH_FREE_TIME - 1)); TEST_ASSERT_TRUE(shouldClearCrashCount(CRASH_FREE_TIME)); } void test_quick_power_cycles_request_recovery() { for (uint8_t count = 0; count < POWER_CYCLES_FOR_RECOVERY; count++) TEST_ASSERT_FALSE(powerCyclesRequestRecovery(count)); TEST_ASSERT_TRUE(powerCyclesRequestRecovery(POWER_CYCLES_FOR_RECOVERY)); } void test_recovery_for_wifi_starts_clock_when_wifi_works() { TEST_ASSERT_FALSE(recoveryShouldStartMain(RecoveryReason::Wifi, 0, false, true)); TEST_ASSERT_FALSE(recoveryShouldStartMain(RecoveryReason::Wifi, RECOVERY_TIME * 10, false, true)); TEST_ASSERT_FALSE(recoveryShouldStartMain(RecoveryReason::Wifi, RECOVERY_MIN_TIME - 1, true, true)); TEST_ASSERT_TRUE(recoveryShouldStartMain(RecoveryReason::Wifi, RECOVERY_MIN_TIME, true, true)); } void test_recovery_for_other_reasons_waits_recovery_time() { const RecoveryReason reasons[] = {RecoveryReason::Crash, RecoveryReason::Manual, RecoveryReason::PowerCycle, RecoveryReason::None}; for (RecoveryReason reason : reasons) { TEST_ASSERT_FALSE(recoveryShouldStartMain(reason, RECOVERY_TIME - 1, true, true)); TEST_ASSERT_TRUE(recoveryShouldStartMain(reason, RECOVERY_TIME, true, true)); // The time counts, not the WiFi: an OTA upload over the setup WiFi is possible TEST_ASSERT_TRUE(recoveryShouldStartMain(reason, RECOVERY_TIME, false, true)); } } void test_recovery_never_starts_missing_clock_firmware() { TEST_ASSERT_FALSE(recoveryShouldStartMain(RecoveryReason::Wifi, RECOVERY_TIME, true, false)); TEST_ASSERT_FALSE(recoveryShouldStartMain(RecoveryReason::Manual, RECOVERY_TIME, true, false)); } void test_every_recovery_reason_has_a_text() { for (uint8_t r = 0; r <= (uint8_t)RecoveryReason::PowerCycle; r++) { TEST_ASSERT_TRUE(strlen(recoveryReasonText((RecoveryReason)r)) > 0); // fits on one line of the display TEST_ASSERT_TRUE(strlen(recoveryReasonShort((RecoveryReason)r)) > 0); TEST_ASSERT_TRUE(strlen(recoveryReasonShort((RecoveryReason)r)) <= 14); } TEST_ASSERT_TRUE(strcmp(recoveryReasonText(RecoveryReason::Wifi), recoveryReasonText(RecoveryReason::Crash)) != 0); } // --------------------------------------------------------------------------- // 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, true); 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("off", 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 evening", frames.last().c_str()); // no off screen } 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 evening", 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("off", frames.last().c_str()); TEST_ASSERT_FALSE(power.is_on); } void test_power_switch_off_waits_for_the_refresh_effect() { controller->setEffectTime(3000); run(12, 0); controller->setPowerEnabled(false); run(12, 0, CLEAR_BEFORE_OFF_TIME + 2000); TEST_ASSERT_EQUAL_STRING("off", frames.last().c_str()); TEST_ASSERT_TRUE(power.is_on); // the effect is still flipping dots run(12, 0, 1400); 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_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()); } void test_no_wifi_is_shown_after_a_minute_without_wifi() { run(12, 0); wifi = false; run(12, 0, NO_WIFI_SHOW_TIME - 200); TEST_ASSERT_EQUAL_STRING("clock 12:00", frames.last().c_str()); run(12, 1, 300); TEST_ASSERT_EQUAL_STRING("nowifi 12:01", frames.last().c_str()); TEST_ASSERT_TRUE(power.is_on); } void test_no_wifi_screen_keeps_the_time_running() { wifi = false; run(12, 0, NO_WIFI_SHOW_TIME + 100); run(12, 1); TEST_ASSERT_EQUAL_STRING("nowifi 12:01", frames.last().c_str()); int frames_before = (int)frames.shown.size(); run(12, 1, 5000); TEST_ASSERT_EQUAL(frames_before, (int)frames.shown.size()); // only on change } void test_clock_returns_when_wifi_is_back() { wifi = false; run(12, 0, NO_WIFI_SHOW_TIME + 100); wifi = true; run(12, 0); TEST_ASSERT_EQUAL_STRING("clock 12:00", frames.last().c_str()); } void test_short_wifi_drop_does_not_change_the_display() { run(12, 0); wifi = false; run(12, 0, 10000); wifi = true; run(12, 0, NO_WIFI_SHOW_TIME); TEST_ASSERT_EQUAL(1, (int)frames.shown.size()); } void test_night_without_wifi_still_powers_off() { run(23, 30); wifi = false; run(23, 31, POWER_OFF_DELAY + 200); TEST_ASSERT_FALSE(power.is_on); TEST_ASSERT_EQUAL_STRING("off", frames.last().c_str()); } // --------------------------------------------------------------------------- // Settings // --------------------------------------------------------------------------- void test_default_settings_are_valid() { TEST_ASSERT_NULL(validateSettings(defaults)); } void test_settings_times_must_be_in_order() { Settings s; s.schedule.late_start = 7 * 60; // before day_start TEST_ASSERT_NOT_NULL(validateSettings(s)); s = Settings(); s.schedule.night_start = 21 * 60; // before late_start TEST_ASSERT_NOT_NULL(validateSettings(s)); s = Settings(); s.schedule.night_start = MINUTES_PER_DAY; // never night: fine TEST_ASSERT_NULL(validateSettings(s)); s.schedule.night_start = MINUTES_PER_DAY + 1; TEST_ASSERT_NOT_NULL(validateSettings(s)); } void test_settings_drive_ranges() { Settings s; TEST_ASSERT_NULL(validateSettings(s)); s.drive.flip_gap = 5001; TEST_ASSERT_NOT_NULL(validateSettings(s)); s = Settings(); s.drive.pulses = 0; TEST_ASSERT_NOT_NULL(validateSettings(s)); s.drive.pulses = 4; TEST_ASSERT_NOT_NULL(validateSettings(s)); s = Settings(); s.drive.effect = DRIVE_EFFECT_COUNT; TEST_ASSERT_NOT_NULL(validateSettings(s)); s = Settings(); s.drive.effect_time = 99; TEST_ASSERT_NOT_NULL(validateSettings(s)); s.drive.effect_time = 10001; TEST_ASSERT_NOT_NULL(validateSettings(s)); } void test_effect_time_is_zero_when_instant() { DriveSettings d; TEST_ASSERT_EQUAL(0, (int)effectTime(d)); // default: instant d.effect = 1; d.effect_time = 2500; TEST_ASSERT_EQUAL(2500, (int)effectTime(d)); d.effect = DRIVE_EFFECT_PUSH; // can take much longer TEST_ASSERT_EQUAL(2500 + (int)DRIVE_PUSH_EXTRA_TIME, (int)effectTime(d)); d.effect = DRIVE_EFFECT_RANDOM; // can be a push TEST_ASSERT_EQUAL(2500 + (int)DRIVE_PUSH_EXTRA_TIME, (int)effectTime(d)); } void test_settings_off_page_blank_or_bitmap_name() { Settings s; TEST_ASSERT_EQUAL_STRING("", s.off_page); // default: blank TEST_ASSERT_NULL(validateSettings(s)); strcpy(s.off_page, "Nintendo"); TEST_ASSERT_NULL(validateSettings(s)); strcpy(s.off_page, "../x"); TEST_ASSERT_NOT_NULL(validateSettings(s)); memset(s.off_page, 'a', sizeof(s.off_page)); // not terminated TEST_ASSERT_NOT_NULL(validateSettings(s)); } void test_changed_off_image_is_redrawn_while_the_display_is_on() { run(23, 30); run(23, 31, 1000); TEST_ASSERT_EQUAL_STRING("off", frames.last().c_str()); controller->pageChanged(); run(23, 31, 1000); TEST_ASSERT_EQUAL(2, (int)std::count(frames.shown.begin(), frames.shown.end(), std::string("off"))); TEST_ASSERT_TRUE(power.is_on); } void test_settings_flip_time_and_port_ranges() { Settings s; s.flip_time = 49; TEST_ASSERT_NOT_NULL(validateSettings(s)); s.flip_time = 2001; TEST_ASSERT_NOT_NULL(validateSettings(s)); s = Settings(); s.home_assistant.port = 0; TEST_ASSERT_NOT_NULL(validateSettings(s)); } void test_settings_page_must_be_clock_or_bitmap_name() { Settings s; strcpy(s.page, "My bitmap"); TEST_ASSERT_NULL(validateSettings(s)); strcpy(s.page, "../evil"); TEST_ASSERT_NOT_NULL(validateSettings(s)); } void test_time_parse_and_format() { uint16_t m = 0; TEST_ASSERT_TRUE(parseTime("08:30", m)); TEST_ASSERT_EQUAL(510, m); TEST_ASSERT_TRUE(parseTime("24:00", m)); TEST_ASSERT_EQUAL(MINUTES_PER_DAY, m); TEST_ASSERT_FALSE(parseTime("24:01", m)); TEST_ASSERT_FALSE(parseTime("12:60", m)); TEST_ASSERT_FALSE(parseTime("noon", m)); TEST_ASSERT_FALSE(parseTime("12:30x", m)); TEST_ASSERT_FALSE(parseTime(nullptr, m)); char out[6]; formatTime(23 * 60 + 31, out); TEST_ASSERT_EQUAL_STRING("23:31", out); formatTime(MINUTES_PER_DAY, out); TEST_ASSERT_EQUAL_STRING("24:00", out); } // --------------------------------------------------------------------------- // Bitmap format // --------------------------------------------------------------------------- void test_bitmap_is_84_by_19_in_drawbitmap_format() { TEST_ASSERT_EQUAL(11, BITMAP_ROW_BYTES); TEST_ASSERT_EQUAL(209, BITMAP_BYTES); uint8_t b[BITMAP_BYTES] = {}; setBitmapPixel(b, 0, 0, true); TEST_ASSERT_EQUAL_HEX8(0x80, b[0]); // leftmost dot = most significant bit setBitmapPixel(b, 83, 18, true); TEST_ASSERT_EQUAL_HEX8(0x10, b[18 * 11 + 10]); // x = 83: bit 3 of byte 10 TEST_ASSERT_TRUE(bitmapPixel(b, 83, 18)); TEST_ASSERT_FALSE(bitmapPixel(b, 84, 0)); // outside setBitmapPixel(b, 0, 0, false); TEST_ASSERT_EQUAL_HEX8(0x00, b[0]); } void test_bitmap_hex_round_trip() { uint8_t a[BITMAP_BYTES], b[BITMAP_BYTES]; for (int i = 0; i < BITMAP_BYTES; i++) a[i] = (uint8_t)(i * 37); char hex[BITMAP_HEX_LENGTH + 1]; bitmapToHex(a, hex); TEST_ASSERT_EQUAL(BITMAP_HEX_LENGTH, (int)strlen(hex)); TEST_ASSERT_TRUE(bitmapFromHex(hex, b)); TEST_ASSERT_EQUAL_MEMORY(a, b, BITMAP_BYTES); } void test_bitmap_hex_rejects_wrong_input() { uint8_t b[BITMAP_BYTES]; TEST_ASSERT_FALSE(bitmapFromHex("abcd", b)); TEST_ASSERT_FALSE(bitmapFromHex(nullptr, b)); std::string bad(BITMAP_HEX_LENGTH, 'f'); bad[100] = 'x'; TEST_ASSERT_FALSE(bitmapFromHex(bad.c_str(), b)); std::string upper(BITMAP_HEX_LENGTH, 'F'); TEST_ASSERT_TRUE(bitmapFromHex(upper.c_str(), b)); } void test_bitmap_names() { TEST_ASSERT_TRUE(validBitmapName("Nintendo")); TEST_ASSERT_TRUE(validBitmapName("Happy Birthday 2")); TEST_ASSERT_TRUE(validBitmapName("a-b_c")); TEST_ASSERT_FALSE(validBitmapName("")); TEST_ASSERT_FALSE(validBitmapName(" lead")); TEST_ASSERT_FALSE(validBitmapName("trail ")); TEST_ASSERT_FALSE(validBitmapName("../etc")); TEST_ASSERT_FALSE(validBitmapName("a/b")); TEST_ASSERT_FALSE(validBitmapName("Clock")); // the clock page TEST_ASSERT_TRUE(validBitmapName("123456789012345678901234")); TEST_ASSERT_FALSE(validBitmapName("1234567890123456789012345")); } // --------------------------------------------------------------------------- // Schedule with settings // --------------------------------------------------------------------------- void test_schedule_disabled_is_always_on() { ScheduleSettings s; s.enabled = false; TEST_ASSERT_TRUE(scheduleFor(3, 0, s) == ScheduleMode::Clock); TEST_ASSERT_TRUE(scheduleFor(22, 7, s) == ScheduleMode::Clock); } void test_schedule_custom_times() { ScheduleSettings s; s.day_start = 6 * 60 + 30; s.late_start = 20 * 60; s.night_start = 21 * 60; TEST_ASSERT_TRUE(scheduleFor(6, 29, s) == ScheduleMode::Night); TEST_ASSERT_TRUE(scheduleFor(6, 30, s) == ScheduleMode::Clock); TEST_ASSERT_TRUE(scheduleFor(20, 1, s) == ScheduleMode::Standby); TEST_ASSERT_TRUE(scheduleFor(20, 30, s) == ScheduleMode::Clock); TEST_ASSERT_TRUE(scheduleFor(21, 0, s) == ScheduleMode::Night); } void test_schedule_without_quarter_hours_stays_on_until_night() { ScheduleSettings s; s.quarter_hours = false; TEST_ASSERT_TRUE(scheduleFor(22, 7, s) == ScheduleMode::Clock); TEST_ASSERT_TRUE(scheduleFor(23, 31, s) == ScheduleMode::Night); } void test_evening_can_be_disabled() { ClockSettings c; c.evening_enabled = false; TEST_ASSERT_FALSE(isEvening(20, 0, c)); } // --------------------------------------------------------------------------- // Pages and live mode // --------------------------------------------------------------------------- void test_bitmap_page_is_shown_once() { controller->setPage(Page::Bitmap); run(12, 0); run(12, 1, 5000); TEST_ASSERT_EQUAL(1, (int)frames.shown.size()); TEST_ASSERT_EQUAL_STRING("page", frames.last().c_str()); } void test_bitmap_page_redrawn_when_changed() { controller->setPage(Page::Bitmap); run(12, 0); controller->pageChanged(); run(12, 0); TEST_ASSERT_EQUAL(2, (int)frames.shown.size()); } void test_switching_back_to_the_clock_page() { controller->setPage(Page::Bitmap); run(12, 0); controller->setPage(Page::Clock); run(12, 0); TEST_ASSERT_EQUAL_STRING("clock 12:00", frames.last().c_str()); } void test_bitmap_page_follows_the_schedule() { controller->setPage(Page::Bitmap); run(23, 30); run(23, 31, POWER_OFF_DELAY + 200); TEST_ASSERT_EQUAL_STRING("off", frames.last().c_str()); TEST_ASSERT_FALSE(power.is_on); } void test_evening_clock_redrawn_when_the_evening_starts() { run(16, 59); run(17, 0); TEST_ASSERT_EQUAL_STRING("clock 17:00 evening", frames.last().c_str()); } void test_settings_change_redraws() { run(18, 0); ClockSettings c; c.evening_enabled = false; controller->setSettings(defaults.schedule, c); run(18, 0); TEST_ASSERT_EQUAL_STRING("clock 18:00", frames.last().c_str()); } void test_live_is_shown_right_away_even_at_night() { run(3, 0); controller->showLive(now); run(3, 0); TEST_ASSERT_TRUE(power.is_on); TEST_ASSERT_EQUAL_STRING("live", frames.last().c_str()); } void test_live_works_before_the_time_is_synced() { controller->showLive(now); controller->update(now, false, 0, 0, true); TEST_ASSERT_EQUAL_STRING("live", frames.last().c_str()); } void test_each_live_image_is_drawn() { controller->showLive(now); run(12, 0); controller->showLive(now); run(12, 0); TEST_ASSERT_EQUAL(2, (int)frames.shown.size()); } void test_stop_live_returns_to_the_page() { controller->showLive(now); run(12, 0); controller->stopLive(); run(12, 0); TEST_ASSERT_EQUAL_STRING("clock 12:00", frames.last().c_str()); TEST_ASSERT_FALSE(controller->liveActive()); } void test_live_ends_after_timeout() { controller->showLive(now); run(12, 0, LIVE_TIMEOUT + 200); TEST_ASSERT_FALSE(controller->liveActive()); TEST_ASSERT_EQUAL_STRING("clock 12:00", frames.last().c_str()); } void test_live_respects_the_power_switch() { controller->setPowerEnabled(false); controller->showLive(now); run(12, 0, 5000); TEST_ASSERT_FALSE(power.is_on); TEST_ASSERT_EQUAL(0, (int)std::count(frames.shown.begin(), frames.shown.end(), std::string("live"))); } 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_power_on_starts_normally_and_clears_crashes); RUN_TEST(test_software_reset_starts_normally_and_keeps_count); RUN_TEST(test_other_reset_starts_normally_and_keeps_count); RUN_TEST(test_first_crashes_give_a_panic_window); RUN_TEST(test_repeated_crashes_give_safe_mode); RUN_TEST(test_after_safe_mode_one_more_crash_gives_safe_mode_again); RUN_TEST(test_crash_count_does_not_overflow); RUN_TEST(test_new_firmware_needs_time_and_wifi_to_be_accepted); RUN_TEST(test_crash_count_cleared_after_running_long_enough); RUN_TEST(test_quick_power_cycles_request_recovery); RUN_TEST(test_recovery_for_wifi_starts_clock_when_wifi_works); RUN_TEST(test_recovery_for_other_reasons_waits_recovery_time); RUN_TEST(test_recovery_never_starts_missing_clock_firmware); RUN_TEST(test_every_recovery_reason_has_a_text); 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_waits_for_the_refresh_effect); RUN_TEST(test_power_switch_off_keeps_display_off_during_the_day); RUN_TEST(test_power_switch_on_again_shows_clock); RUN_TEST(test_redraws_after_power_was_switched_off_elsewhere); RUN_TEST(test_no_wifi_is_shown_after_a_minute_without_wifi); RUN_TEST(test_no_wifi_screen_keeps_the_time_running); RUN_TEST(test_clock_returns_when_wifi_is_back); RUN_TEST(test_short_wifi_drop_does_not_change_the_display); RUN_TEST(test_night_without_wifi_still_powers_off); RUN_TEST(test_default_settings_are_valid); RUN_TEST(test_settings_times_must_be_in_order); RUN_TEST(test_settings_drive_ranges); RUN_TEST(test_effect_time_is_zero_when_instant); RUN_TEST(test_settings_off_page_blank_or_bitmap_name); RUN_TEST(test_changed_off_image_is_redrawn_while_the_display_is_on); RUN_TEST(test_settings_flip_time_and_port_ranges); RUN_TEST(test_settings_page_must_be_clock_or_bitmap_name); RUN_TEST(test_time_parse_and_format); RUN_TEST(test_bitmap_is_84_by_19_in_drawbitmap_format); RUN_TEST(test_bitmap_hex_round_trip); RUN_TEST(test_bitmap_hex_rejects_wrong_input); RUN_TEST(test_bitmap_names); RUN_TEST(test_schedule_disabled_is_always_on); RUN_TEST(test_schedule_custom_times); RUN_TEST(test_schedule_without_quarter_hours_stays_on_until_night); RUN_TEST(test_evening_can_be_disabled); RUN_TEST(test_bitmap_page_is_shown_once); RUN_TEST(test_bitmap_page_redrawn_when_changed); RUN_TEST(test_switching_back_to_the_clock_page); RUN_TEST(test_bitmap_page_follows_the_schedule); RUN_TEST(test_evening_clock_redrawn_when_the_evening_starts); RUN_TEST(test_settings_change_redraws); RUN_TEST(test_live_is_shown_right_away_even_at_night); RUN_TEST(test_live_works_before_the_time_is_synced); RUN_TEST(test_each_live_image_is_drawn); RUN_TEST(test_stop_live_returns_to_the_page); RUN_TEST(test_live_ends_after_timeout); RUN_TEST(test_live_respects_the_power_switch); return UNITY_END(); }