#include #include #include "unity.h" #include "fake_flash.h" #include "config/nl_flash_layout.h" #include "config/nl_settings.h" #include "light/nl_light_types.h" #include "util/nl_crc.h" void setUp(void) { fake_flash_reset(); nl_settings_init(NL_FL_SETTINGS_A, NL_FL_SETTINGS_B); } void tearDown(void) { TEST_ASSERT_EQUAL_UINT(0, fake_flash_dirty_writes); } static nl_settings_t sample(const char *name) { nl_settings_t s; nl_settings_defaults(&s); strcpy(s.wifi_ssid, "ninHome"); strcpy(s.wifi_pass, "secret"); strcpy(s.name, name); s.auth_all = 1; s.power_on_behaviour = NL_POB_ON; s.fade_in_ms = 1200; return s; } static void test_defaults(void) { nl_settings_t s; TEST_ASSERT_FALSE(nl_settings_load(&s)); TEST_ASSERT_EQUAL_STRING("", s.wifi_ssid); TEST_ASSERT_EQUAL_STRING("aabbccddeeff0011223344", s.token); TEST_ASSERT_EQUAL_STRING("", s.name); TEST_ASSERT_EQUAL_UINT8(0, s.auth_all); TEST_ASSERT_EQUAL_UINT8(NL_POB_LAST, s.power_on_behaviour); TEST_ASSERT_EQUAL_UINT16(500, s.fade_in_ms); TEST_ASSERT_EQUAL_UINT16(500, s.fade_out_ms); TEST_ASSERT_EQUAL_UINT16(300, s.transition_ms); TEST_ASSERT_EQUAL_UINT8(0, s.wake_on_change); } static void test_save_and_load(void) { nl_settings_t in = sample("Wohnzimmer"), out; TEST_ASSERT_EQUAL_INT(0, nl_settings_save(&in)); TEST_ASSERT_TRUE(nl_settings_load(&out)); TEST_ASSERT_EQUAL_MEMORY(&in, &out, sizeof in); } static void test_alternates_slots(void) { nl_settings_t s, out; s = sample("a"); nl_settings_save(&s); s = sample("b"); nl_settings_save(&s); s = sample("c"); nl_settings_save(&s); TEST_ASSERT_EQUAL_UINT(2, fake_flash_erases[0]); TEST_ASSERT_EQUAL_UINT(1, fake_flash_erases[1]); TEST_ASSERT_TRUE(nl_settings_load(&out)); TEST_ASSERT_EQUAL_STRING("c", out.name); } static void test_power_loss_keeps_previous(void) { nl_settings_t s = sample("old"), out; nl_settings_save(&s); s = sample("new"); fake_flash_fail_after(50); TEST_ASSERT_NOT_EQUAL(0, nl_settings_save(&s)); fake_flash_fail_after(-1); TEST_ASSERT_TRUE(nl_settings_load(&out)); TEST_ASSERT_EQUAL_STRING("old", out.name); } static void test_corrupt_newest_falls_back(void) { nl_settings_t s, out; s = sample("first"); nl_settings_save(&s); /* slot A */ s = sample("second"); nl_settings_save(&s); /* slot B */ fake_flash_mem[NL_FL_SETTINGS_B + 40] ^= 0x01; TEST_ASSERT_TRUE(nl_settings_load(&out)); TEST_ASSERT_EQUAL_STRING("first", out.name); } static void test_load_accepts_shorter_record(void) { nl_settings_t in, out; uint32_t crc; /* simulate a record written by a firmware that predates fade_in_ms/fade_out_ms/ * ota_pending: length ends right before fade_in_ms. Those fields must come back as * their real defaults (500 ms / 0), not as zero just because the record didn't carry * them - that's the whole point of the length tolerance. */ in = sample("alt"); nl_settings_save(&in); { uint16_t shorter = (uint16_t)offsetof(nl_settings_t, fade_in_ms); memcpy(&fake_flash_mem[NL_FL_SETTINGS_A + 6], &shorter, 2); /* header.length */ crc = nl_crc32(0, &fake_flash_mem[NL_FL_SETTINGS_A], 12 + shorter); memcpy(&fake_flash_mem[NL_FL_SETTINGS_A + 12 + shorter], &crc, 4); } TEST_ASSERT_TRUE(nl_settings_load(&out)); TEST_ASSERT_EQUAL_STRING("alt", out.name); TEST_ASSERT_EQUAL_STRING("ninHome", out.wifi_ssid); TEST_ASSERT_EQUAL_UINT16(500, out.fade_in_ms); TEST_ASSERT_EQUAL_UINT16(500, out.fade_out_ms); TEST_ASSERT_EQUAL_UINT8(0, out.ota_pending); TEST_ASSERT_EQUAL_UINT16(300, out.transition_ms); TEST_ASSERT_EQUAL_UINT8(0, out.wake_on_change); } static void test_transition_and_wake_roundtrip(void) { nl_settings_t in = sample("neu"), out; in.transition_ms = 1500; in.wake_on_change = 1; TEST_ASSERT_EQUAL_INT(0, nl_settings_save(&in)); TEST_ASSERT_TRUE(nl_settings_load(&out)); TEST_ASSERT_EQUAL_UINT16(1500, out.transition_ms); TEST_ASSERT_EQUAL_UINT8(1, out.wake_on_change); } static void test_load_rejects_longer_record(void) { nl_settings_t in = sample("x"), out; uint16_t longer = (uint16_t)(sizeof(nl_settings_t) + 1); nl_settings_save(&in); /* read_rec() rejects hdr.length > sizeof(nl_settings_t) outright, before it even reads * the payload or the CRC - so the stored CRC is irrelevant here and deliberately left * as whatever nl_settings_save() wrote for the original (shorter) length. */ memcpy(&fake_flash_mem[NL_FL_SETTINGS_A + 6], &longer, 2); /* header.length */ TEST_ASSERT_FALSE(nl_settings_load(&out)); /* unknown newer layout -> defaults */ } static void test_ota_pending_default_and_roundtrip(void) { nl_settings_t in, out; TEST_ASSERT_FALSE(nl_settings_load(&out)); TEST_ASSERT_EQUAL_UINT8(0, out.ota_pending); in = sample("ota"); in.ota_pending = 1; TEST_ASSERT_EQUAL_INT(0, nl_settings_save(&in)); TEST_ASSERT_TRUE(nl_settings_load(&out)); TEST_ASSERT_EQUAL_UINT8(1, out.ota_pending); } /* I6: the writer must place the CRC exactly where the reader looks for it - directly behind * `length` payload bytes. Both sides only agreed by accident while sizeof(nl_settings_t) is a * multiple of 4; one extra uint16_t in the struct used to move the written CRC two bytes past * the read offset, which would have failed every stored record's CRC after an update. This test * nails the invariant independently of the struct size. */ static void test_crc_offset_matches_reader(void) { nl_settings_t in = sample("crc"), out; uint16_t length; uint32_t crc, stored; const uint8_t *rec = &fake_flash_mem[NL_FL_SETTINGS_A]; TEST_ASSERT_EQUAL_INT(0, nl_settings_save(&in)); memcpy(&length, rec + 6, 2); /* header.length */ TEST_ASSERT_EQUAL_UINT16(sizeof(nl_settings_t), length); crc = nl_crc32(0, rec, 12u + length); /* header + exactly `length` payload bytes */ memcpy(&stored, rec + 12u + length, 4); /* where read_rec() takes the CRC from */ TEST_ASSERT_EQUAL_HEX32(crc, stored); TEST_ASSERT_TRUE(nl_settings_load(&out)); /* and the record really does read back */ TEST_ASSERT_EQUAL_MEMORY(&in, &out, sizeof in); } static void test_erase(void) { nl_settings_t s = sample("x"), out; nl_settings_save(&s); TEST_ASSERT_EQUAL_INT(0, nl_settings_erase()); TEST_ASSERT_FALSE(nl_settings_load(&out)); } static void test_sacn_defaults(void) { nl_settings_t s; TEST_ASSERT_FALSE(nl_settings_load(&s)); TEST_ASSERT_EQUAL_UINT8(1, s.sacn_enabled); TEST_ASSERT_EQUAL_UINT16(1, s.sacn_universe); TEST_ASSERT_EQUAL_UINT16(1, s.sacn_start_slot); TEST_ASSERT_EQUAL_UINT8(0, s.sacn_hold); } static void test_sacn_roundtrip(void) { nl_settings_t in = sample("dmx"), out; in.sacn_enabled = 0; in.sacn_universe = 4711; in.sacn_start_slot = 300; in.sacn_hold = 1; TEST_ASSERT_EQUAL_INT(0, nl_settings_save(&in)); TEST_ASSERT_TRUE(nl_settings_load(&out)); TEST_ASSERT_EQUAL_UINT8(0, out.sacn_enabled); TEST_ASSERT_EQUAL_UINT16(4711, out.sacn_universe); TEST_ASSERT_EQUAL_UINT16(300, out.sacn_start_slot); TEST_ASSERT_EQUAL_UINT8(1, out.sacn_hold); } /* Ein Datensatz aus einer Firmware VOR sACN: die vier Felder muessen als echte Vorgaben * zurueckkommen, nicht als Null - sonst waere sACN auf jeder aktualisierten Lampe aus. * Die Laenge ist bewusst die Literalzahl 208: das war sizeof(nl_settings_t) vor diesem * Zweig und damit die Laenge, die auf jeder ausgelieferten Lampe im Flash steht. Ein * offsetof() auf ein heutiges Feld wuerde mit der Struktur mitwandern und genau den Fall * verfehlen, um den es hier geht. Das letzte Byte (207) war im 208-Byte-Satz reines * Fuellbyte und daher 0 - es wird hier nachgestellt, denn genau dieses Byte legt sich * beim Lesen ueber das erste neue Feld. */ static void test_record_without_sacn_gets_defaults(void) { nl_settings_t in = sample("alt"), out; uint32_t crc; nl_settings_save(&in); { uint16_t shorter = 208u; /* sizeof(nl_settings_t) vor sACN */ fake_flash_mem[NL_FL_SETTINGS_A + 12 + 207] = 0; /* Fuellbyte des alten Satzes */ memcpy(&fake_flash_mem[NL_FL_SETTINGS_A + 6], &shorter, 2); crc = nl_crc32(0, &fake_flash_mem[NL_FL_SETTINGS_A], 12 + shorter); memcpy(&fake_flash_mem[NL_FL_SETTINGS_A + 12 + shorter], &crc, 4); } TEST_ASSERT_TRUE(nl_settings_load(&out)); TEST_ASSERT_EQUAL_STRING("alt", out.name); TEST_ASSERT_EQUAL_STRING("ninHome", out.wifi_ssid); TEST_ASSERT_EQUAL_UINT8(1, out.sacn_enabled); TEST_ASSERT_EQUAL_UINT16(1, out.sacn_universe); TEST_ASSERT_EQUAL_UINT16(1, out.sacn_start_slot); TEST_ASSERT_EQUAL_UINT8(0, out.sacn_hold); } /* Die Satzlaenge muss durch 4 teilbar bleiben, sonst laufen Schreiber und Leser beim CRC * auseinander (siehe NL_SETTINGS_CRC_OFF in nl_settings.c). */ static void test_sacn_layout_defaults_to_rgb(void) { nl_settings_t s; TEST_ASSERT_FALSE(nl_settings_load(&s)); TEST_ASSERT_EQUAL_UINT8(1, s.sacn_layout); } static void test_sacn_layout_roundtrip(void) { nl_settings_t in = sample("layout"), out; in.sacn_layout = 0; /* weg von der Vorgabe, sonst prueft der Test nur diese */ TEST_ASSERT_EQUAL_INT(0, nl_settings_save(&in)); TEST_ASSERT_TRUE(nl_settings_load(&out)); TEST_ASSERT_EQUAL_UINT8(0, out.sacn_layout); } /* Derselbe Fallstrick wie bei sacn_enabled, eine Feldgeneration weiter: ein Satz, der von * einer Firmware MIT sACN, aber OHNE layout geschrieben wurde, ist 216 Byte lang. Laege * sacn_layout darunter, kaeme es als Fuellbyte dieses Satzes zurueck statt als Vorgabe. */ static void test_record_without_layout_gets_the_default(void) { nl_settings_t in = sample("vor-layout"), out; uint32_t crc; in.sacn_layout = 0; /* der abgeschnittene Satz darf diese 0 nicht zurueckliefern */ nl_settings_save(&in); { uint16_t shorter = 216u; /* sizeof(nl_settings_t) vor dem Layout-Feld */ memcpy(&fake_flash_mem[NL_FL_SETTINGS_A + 6], &shorter, 2); crc = nl_crc32(0, &fake_flash_mem[NL_FL_SETTINGS_A], 12 + shorter); memcpy(&fake_flash_mem[NL_FL_SETTINGS_A + 12 + shorter], &crc, 4); } TEST_ASSERT_TRUE(nl_settings_load(&out)); TEST_ASSERT_EQUAL_STRING("vor-layout", out.name); TEST_ASSERT_EQUAL_UINT8(1, out.sacn_enabled); TEST_ASSERT_EQUAL_UINT8(1, out.sacn_layout); } static void test_standalone_defaults(void) { nl_settings_t s; TEST_ASSERT_FALSE(nl_settings_load(&s)); TEST_ASSERT_EQUAL_UINT8(0, s.standalone); TEST_ASSERT_EQUAL_UINT8(0, s.ap_channel); /* 0 heisst "Vorgabe 6" */ TEST_ASSERT_EQUAL_UINT8(1, s.standalone_pulse); TEST_ASSERT_EQUAL_STRING("", s.ap_ssid); TEST_ASSERT_EQUAL_STRING("", s.ap_pass); } static void test_standalone_roundtrip(void) { nl_settings_t in = sample("ap"), out; in.standalone = 1; in.ap_channel = 11; in.standalone_pulse = 0; strcpy(in.ap_ssid, "Wohnzimmerlampe"); strcpy(in.ap_pass, "geheim123"); TEST_ASSERT_EQUAL_INT(0, nl_settings_save(&in)); TEST_ASSERT_TRUE(nl_settings_load(&out)); TEST_ASSERT_EQUAL_UINT8(1, out.standalone); TEST_ASSERT_EQUAL_UINT8(11, out.ap_channel); TEST_ASSERT_EQUAL_UINT8(0, out.standalone_pulse); TEST_ASSERT_EQUAL_STRING("Wohnzimmerlampe", out.ap_ssid); TEST_ASSERT_EQUAL_STRING("geheim123", out.ap_pass); } /* Ein Datensatz aus einer Firmware VOR dem Standalone-Modus: die neuen Felder muessen * als echte Vorgaben zurueckkommen, nicht als Null. Sonst stuende standalone_pulse nach * dem Update auf jeder Lampe auf 0 - der Startpuls waere still abgeschaltet. */ static void test_record_without_standalone_gets_defaults(void) { nl_settings_t in = sample("alt"), out; uint32_t crc; nl_settings_save(&in); { uint16_t shorter = 220u; /* sizeof(nl_settings_t) vor diesem Zweig */ memset(&fake_flash_mem[NL_FL_SETTINGS_A + 12 + 217], 0, 3); /* Fuellbytes */ memcpy(&fake_flash_mem[NL_FL_SETTINGS_A + 6], &shorter, 2); crc = nl_crc32(0, &fake_flash_mem[NL_FL_SETTINGS_A], 12 + shorter); memcpy(&fake_flash_mem[NL_FL_SETTINGS_A + 12 + shorter], &crc, 4); } TEST_ASSERT_TRUE(nl_settings_load(&out)); TEST_ASSERT_EQUAL_STRING("alt", out.name); TEST_ASSERT_EQUAL_UINT8(0, out.standalone); TEST_ASSERT_EQUAL_UINT8(1, out.standalone_pulse); } static void test_record_size_stays_aligned(void) { TEST_ASSERT_EQUAL_UINT(0, sizeof(nl_settings_t) % 4u); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_defaults); RUN_TEST(test_transition_and_wake_roundtrip); RUN_TEST(test_save_and_load); RUN_TEST(test_alternates_slots); RUN_TEST(test_power_loss_keeps_previous); RUN_TEST(test_corrupt_newest_falls_back); RUN_TEST(test_load_accepts_shorter_record); RUN_TEST(test_load_rejects_longer_record); RUN_TEST(test_ota_pending_default_and_roundtrip); RUN_TEST(test_crc_offset_matches_reader); RUN_TEST(test_erase); RUN_TEST(test_sacn_defaults); RUN_TEST(test_sacn_roundtrip); RUN_TEST(test_record_without_sacn_gets_defaults); RUN_TEST(test_sacn_layout_defaults_to_rgb); RUN_TEST(test_sacn_layout_roundtrip); RUN_TEST(test_record_without_layout_gets_the_default); RUN_TEST(test_record_size_stays_aligned); RUN_TEST(test_standalone_defaults); RUN_TEST(test_standalone_roundtrip); RUN_TEST(test_record_without_standalone_gets_defaults); return UNITY_END(); }