ESPSomfy-RTS/test/test_utils/test_utils.cpp
cjkas 293e013aa2 test: cover the roam threshold and who gets memStatus
Move the roam check into worthRoaming() and the memStatus routing into
heapEmitTarget() in Utils so the native tests reach them.

worthRoaming() roams for a gain of at least ROAM_MIN_GAIN_DB, as the
comment and the 6 dB commit said. The old check needed one dB more.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 15:37:12 +02:00

318 lines
12 KiB
C++

#include <unity.h>
#include "Utils.h"
// The C runtime only picks up TZ after it is re-read.
static void setTZ(const char *tz) {
#ifdef _WIN32
_putenv_s("TZ", tz);
_tzset();
#else
setenv("TZ", tz, 1);
tzset();
#endif
}
// 2024-03-05T07:08:09Z
static const time_t SAMPLE_UTC = 1709622489;
void setUp() { setTZ("UTC0"); }
void tearDown() {}
/*********************************************************************
* _trim
********************************************************************/
static void test_trim_strips_whitespace_and_quotes() {
char str[] = " \t\"Living Room\"\r\n";
_trim(str);
TEST_ASSERT_EQUAL_STRING("Living Room", str);
}
static void test_trim_keeps_clean_string() {
char str[] = "Kitchen";
_trim(str);
TEST_ASSERT_EQUAL_STRING("Kitchen", str);
}
static void test_trim_all_whitespace_becomes_empty() {
char str[] = " \t\r\n\" ";
_trim(str);
TEST_ASSERT_EQUAL_STRING("", str);
}
static void test_trim_empty_string() {
char str[] = "";
_trim(str);
TEST_ASSERT_EQUAL_STRING("", str);
}
/*********************************************************************
* toBoolean
********************************************************************/
static void test_toBoolean_missing_uses_default() {
TEST_ASSERT_TRUE(toBoolean(nullptr, true));
TEST_ASSERT_FALSE(toBoolean(nullptr, false));
TEST_ASSERT_TRUE(toBoolean("", true));
TEST_ASSERT_FALSE(toBoolean("", false));
}
static void test_toBoolean_true_values() {
TEST_ASSERT_TRUE(toBoolean("true", false));
TEST_ASSERT_TRUE(toBoolean("True", false));
TEST_ASSERT_TRUE(toBoolean("1", false));
}
static void test_toBoolean_false_values() {
TEST_ASSERT_FALSE(toBoolean("false", true));
TEST_ASSERT_FALSE(toBoolean("False", true));
TEST_ASSERT_FALSE(toBoolean("0", true));
}
static void test_toBoolean_unknown_uses_default() {
TEST_ASSERT_TRUE(toBoolean("yes", true));
TEST_ASSERT_FALSE(toBoolean("yes", false));
}
/*********************************************************************
* SETCHARPROP
********************************************************************/
static void test_setcharprop_copies_short_value() {
char prop[8];
SETCHARPROP(prop, "abc", sizeof(prop));
TEST_ASSERT_EQUAL_STRING("abc", prop);
}
static void test_setcharprop_truncates_long_value() {
char prop[5];
SETCHARPROP(prop, "abcdefgh", sizeof(prop));
TEST_ASSERT_EQUAL_STRING("abcd", prop);
}
static void test_setcharprop_unterminated_source() {
// A coredump's app_elf_sha256 can be full of hex digits with no terminator.
// The copy must stop inside the array and come out as a comparable string.
const char crashed[17] = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f','9'};
char sha[sizeof(crashed)];
SETCHARPROP(sha, crashed, sizeof(sha));
TEST_ASSERT_EQUAL_STRING("0123456789abcdef", sha);
TEST_ASSERT_EQUAL_INT(0, strcmp(sha, "0123456789abcdef"));
}
/*********************************************************************
* sortArray / sortArrayReverse
********************************************************************/
static bool absLarger(int a, int b) { return abs(a) > abs(b); }
static void test_sortArray_ascending() {
int arr[] = {5, -1, 3, 0, 3};
const int expected[] = {-1, 0, 3, 3, 5};
sortArray(arr, 5);
TEST_ASSERT_EQUAL_INT_ARRAY(expected, arr, 5);
}
static void test_sortArrayReverse_descending() {
int arr[] = {5, -1, 3, 0, 3};
const int expected[] = {5, 3, 3, 0, -1};
sortArrayReverse(arr, 5);
TEST_ASSERT_EQUAL_INT_ARRAY(expected, arr, 5);
}
static void test_sortArray_custom_comparator() {
int arr[] = {-4, 1, 3, -2};
const int expected[] = {1, -2, 3, -4};
sortArray(arr, 4, absLarger);
TEST_ASSERT_EQUAL_INT_ARRAY(expected, arr, 4);
const int reversed[] = {-4, 3, -2, 1};
sortArrayReverse(arr, 4, absLarger);
TEST_ASSERT_EQUAL_INT_ARRAY(reversed, arr, 4);
}
static void test_sortArray_empty_and_single() {
int one[] = {42};
sortArray(one, 1);
TEST_ASSERT_EQUAL_INT(42, one[0]);
int none[] = {7};
sortArray(none, 0);
TEST_ASSERT_EQUAL_INT(7, none[0]);
}
static void test_sortArray_moves_free_ids_last() {
// Free shade id slots are 255 and must end up after the used ones.
uint8_t ids[] = {3, 255, 1, 255, 2};
const uint8_t expected[] = {1, 2, 3, 255, 255};
sortArray<uint8_t>(ids, sizeof(ids));
TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, ids, sizeof(ids));
}
/*********************************************************************
* Timestamp
********************************************************************/
static struct tm sampleTm() {
struct tm dt = {};
dt.tm_year = 2024 - 1900;
dt.tm_mon = 2;
dt.tm_mday = 5;
dt.tm_hour = 7;
dt.tm_min = 8;
dt.tm_sec = 9;
return dt;
}
static void test_formatISO_offsets() {
Timestamp ts;
struct tm dt = sampleTm();
TEST_ASSERT_EQUAL_STRING("2024-03-05T07:08:09.000+0000", ts.formatISO(&dt, 0));
TEST_ASSERT_EQUAL_STRING("2024-03-05T07:08:09.000-0500", ts.formatISO(&dt, -500));
TEST_ASSERT_EQUAL_STRING("2024-03-05T07:08:09.000+0530", ts.formatISO(&dt, 530));
}
static void test_calcTZOffset_whole_hours() {
time_t t = SAMPLE_UTC;
setTZ("UTC0");
TEST_ASSERT_EQUAL_INT(0, Timestamp::calcTZOffset(&t));
setTZ("EST5");
TEST_ASSERT_EQUAL_INT(-500, Timestamp::calcTZOffset(&t));
}
static void test_calcTZOffset_half_hour_east() {
time_t t = SAMPLE_UTC;
setTZ("IST-5:30");
TEST_ASSERT_EQUAL_INT(530, Timestamp::calcTZOffset(&t));
}
static void test_calcTZOffset_half_hour_west() {
time_t t = SAMPLE_UTC;
setTZ("NST3:30");
TEST_ASSERT_EQUAL_INT(-330, Timestamp::calcTZOffset(&t));
}
static void test_parseUTCTime_in_utc() {
setTZ("UTC0");
TEST_ASSERT_EQUAL_INT64((int64_t)SAMPLE_UTC, (int64_t)Timestamp::parseUTCTime("2024-03-05T07:08:09Z"));
}
static void test_parseUTCTime_ignores_local_zone() {
setTZ("IST-5:30");
TEST_ASSERT_EQUAL_INT64((int64_t)SAMPLE_UTC, (int64_t)Timestamp::parseUTCTime("2024-03-05T07:08:09Z"));
}
/*********************************************************************
* rssiNeedsEmit
********************************************************************/
static void test_rssiNeedsEmit_channel_change_always_emits() {
TEST_ASSERT_TRUE(rssiNeedsEmit(-70, -70, 6, 11, 0, 3, 10000));
}
static void test_rssiNeedsEmit_small_change_is_ignored() {
TEST_ASSERT_FALSE(rssiNeedsEmit(-72, -70, 6, 6, 60000, 3, 10000));
}
static void test_rssiNeedsEmit_real_change_after_interval_emits() {
TEST_ASSERT_TRUE(rssiNeedsEmit(-73, -70, 6, 6, 10000, 3, 10000));
}
static void test_rssiNeedsEmit_real_change_too_soon_waits() {
TEST_ASSERT_FALSE(rssiNeedsEmit(-73, -70, 6, 6, 5000, 3, 10000));
}
static void test_rssiNeedsEmit_either_direction() {
TEST_ASSERT_TRUE(rssiNeedsEmit(-67, -70, 6, 6, 10000, 3, 10000));
TEST_ASSERT_TRUE(rssiNeedsEmit(-73, -70, 6, 6, 10000, 3, 10000));
}
/*********************************************************************
* RssiBroadcast
********************************************************************/
static RssiBroadcast broadcastAt(int rssi, int channel, uint32_t at) {
RssiBroadcast b;
b.sent(true, rssi, channel, at);
return b;
}
static void test_rssiBroadcast_client_emit_keeps_baseline() {
// A new client gets the current RSSI right as it drops 5 dBm. The clients
// already connected must still get the change.
RssiBroadcast b = broadcastAt(-60, 6, 0);
b.sent(false, -65, 6, 20000);
TEST_ASSERT_EQUAL_INT(-60, b.rssi);
TEST_ASSERT_EQUAL_UINT32(0, b.at);
TEST_ASSERT_TRUE(b.due(-65, 6, 20000, 3, 10000));
}
static void test_rssiBroadcast_broadcast_moves_baseline() {
RssiBroadcast b = broadcastAt(-60, 6, 0);
b.sent(true, -65, 6, 20000);
TEST_ASSERT_EQUAL_INT(-65, b.rssi);
TEST_ASSERT_EQUAL_UINT32(20000, b.at);
// Measured from the new baseline and too soon after it.
TEST_ASSERT_FALSE(b.due(-66, 6, 40000, 3, 10000));
TEST_ASSERT_FALSE(b.due(-70, 6, 25000, 3, 10000));
TEST_ASSERT_TRUE(b.due(-70, 6, 30000, 3, 10000));
}
static void test_rssiBroadcast_channel_change_is_due() {
RssiBroadcast b = broadcastAt(-60, 6, 1000);
TEST_ASSERT_TRUE(b.due(-60, 11, 1000, 3, 10000));
}
static void test_rssiBroadcast_client_emit_on_disconnect_keeps_baseline() {
RssiBroadcast b = broadcastAt(-60, 6, 0);
b.sent(false, -100, -1, 5000);
TEST_ASSERT_EQUAL_INT(6, b.channel);
TEST_ASSERT_FALSE(b.due(-60, 6, 5000, 3, 10000));
}
static void test_rssiBroadcast_hold_off_across_millis_wrap() {
RssiBroadcast b = broadcastAt(-60, 6, 0xFFFFF000u);
TEST_ASSERT_FALSE(b.due(-70, 6, 0x00001000u, 3, 10000)); // 8192ms later
TEST_ASSERT_TRUE(b.due(-70, 6, 0x00002000u, 3, 10000)); // 12288ms later
}
/*********************************************************************
* worthRoaming
********************************************************************/
static void test_worthRoaming_needs_the_full_gain() {
TEST_ASSERT_FALSE(worthRoaming(true, -70, -65, 6));
TEST_ASSERT_TRUE(worthRoaming(true, -70, -64, 6));
TEST_ASSERT_TRUE(worthRoaming(true, -70, -50, 6));
}
static void test_worthRoaming_never_to_a_weaker_ap() {
// The scan also lists the AP we are on.
TEST_ASSERT_FALSE(worthRoaming(true, -70, -70, 6));
TEST_ASSERT_FALSE(worthRoaming(true, -70, -80, 6));
}
static void test_worthRoaming_any_ap_while_disconnected() {
TEST_ASSERT_TRUE(worthRoaming(false, 0, -90, 6));
}
/*********************************************************************
* heapEmitTarget
********************************************************************/
static void test_heapEmitTarget_one_client_always_gets_it() {
TEST_ASSERT_TRUE(heap_emit_t::client == heapEmitTarget(3, 0, 30000, false));
TEST_ASSERT_TRUE(heap_emit_t::client == heapEmitTarget(3, 60000, 30000, true));
}
static void test_heapEmitTarget_broadcasts_once_per_interval() {
TEST_ASSERT_TRUE(heap_emit_t::broadcast == heapEmitTarget(255, 30000, 30000, false));
TEST_ASSERT_TRUE(heap_emit_t::broadcast == heapEmitTarget(255, 30000, 30000, true));
TEST_ASSERT_TRUE(heap_emit_t::none == heapEmitTarget(255, 29999, 30000, false));
}
static void test_heapEmitTarget_room_in_between() {
TEST_ASSERT_TRUE(heap_emit_t::room == heapEmitTarget(255, 1000, 30000, true));
}
int main(int argc, char **argv) {
(void)argc;
(void)argv;
UNITY_BEGIN();
RUN_TEST(test_trim_strips_whitespace_and_quotes);
RUN_TEST(test_trim_keeps_clean_string);
RUN_TEST(test_trim_all_whitespace_becomes_empty);
RUN_TEST(test_trim_empty_string);
RUN_TEST(test_toBoolean_missing_uses_default);
RUN_TEST(test_toBoolean_true_values);
RUN_TEST(test_toBoolean_false_values);
RUN_TEST(test_toBoolean_unknown_uses_default);
RUN_TEST(test_setcharprop_copies_short_value);
RUN_TEST(test_setcharprop_truncates_long_value);
RUN_TEST(test_setcharprop_unterminated_source);
RUN_TEST(test_sortArray_ascending);
RUN_TEST(test_sortArrayReverse_descending);
RUN_TEST(test_sortArray_custom_comparator);
RUN_TEST(test_sortArray_empty_and_single);
RUN_TEST(test_sortArray_moves_free_ids_last);
RUN_TEST(test_formatISO_offsets);
RUN_TEST(test_calcTZOffset_whole_hours);
RUN_TEST(test_calcTZOffset_half_hour_east);
RUN_TEST(test_calcTZOffset_half_hour_west);
RUN_TEST(test_parseUTCTime_in_utc);
RUN_TEST(test_parseUTCTime_ignores_local_zone);
RUN_TEST(test_rssiNeedsEmit_channel_change_always_emits);
RUN_TEST(test_rssiNeedsEmit_small_change_is_ignored);
RUN_TEST(test_rssiNeedsEmit_real_change_after_interval_emits);
RUN_TEST(test_rssiNeedsEmit_real_change_too_soon_waits);
RUN_TEST(test_rssiNeedsEmit_either_direction);
RUN_TEST(test_rssiBroadcast_client_emit_keeps_baseline);
RUN_TEST(test_rssiBroadcast_broadcast_moves_baseline);
RUN_TEST(test_rssiBroadcast_channel_change_is_due);
RUN_TEST(test_rssiBroadcast_client_emit_on_disconnect_keeps_baseline);
RUN_TEST(test_rssiBroadcast_hold_off_across_millis_wrap);
RUN_TEST(test_worthRoaming_needs_the_full_gain);
RUN_TEST(test_worthRoaming_never_to_a_weaker_ap);
RUN_TEST(test_worthRoaming_any_ap_while_disconnected);
RUN_TEST(test_heapEmitTarget_one_client_always_gets_it);
RUN_TEST(test_heapEmitTarget_broadcasts_once_per_interval);
RUN_TEST(test_heapEmitTarget_room_in_between);
return UNITY_END();
}