mirror of
https://github.com/meshtastic/firmware.git
synced 2026-09-16 16:41:24 -04:00
* fix(test): make the native-windows test suite build and run pio test -e native-windows failed every suite at the build stage. Five independent causes, all Windows-only: - TestUtil.cpp called lstat(), which MinGW-w64 does not provide. The state-checkpoint walk added in #11322 is fenced with ARCH_PORTDUINO, which native-windows also satisfies, so all 53 suites failed to compile. Route it through a stat() shim on _WIN32. - test_default, test_http_content_handler, test_meshpacket_serializer and test_serial define no setUp/tearDown and relied on the weak defaults PlatformIO emits in unity_config.c. GCC lowers a weak definition on PE-COFF to a weak external, leaving the symbol undefined, so it does not satisfy unity.c's reference and the link fails. Define them explicitly, as the other 49 suites already do. - test_mqtt included <arpa/inet.h>, absent on MinGW, for htonl(). Use winsock2.h there. - test_gps_update_scheduling uses TEST_ASSERT_DOUBLE_WITHIN. Unity omits double support unless UNITY_INCLUDE_DOUBLE is defined, so the assertion compiled to an unconditional failure. Define it for the env. - test_getfiles_rejects_overlong_path is excluded on _WIN32. Overrunning the 228-byte file_name needs at least 229 bytes below the portduino root, and that root is already ~34 bytes, so every qualifying path passes the 260-byte MAX_PATH: the nested mkdir() fails, the file is never created, and getFiles() has nothing to drop. No component layout satisfies both limits. Each of the seven suites that failed on Windows was verified individually after the change. test_fscommon_getfiles still fails in a full run, for a cause outside this change: rmDir() does not remove directories on Windows, so empty dirs left by an earlier run survive setUp() and make getFiles() report a depth truncation. That is a pre-existing FSCommon bug, reported separately. No Linux or macOS behaviour changes: every guard is _WIN32-only except UNITY_INCLUDE_DOUBLE, which is scoped to env:native-windows. * fix(test): define UNITY_INCLUDE_DOUBLE for every native env The flag was scoped to env:native-windows, but the gap is not Windows-specific. Verified on Debian with gcc against the Linux env's own Unity 2.6.1 and PlatformIO's generated native unity_config: UNITY_INCLUDE_DOUBLE : NOT defined UNITY_EXCLUDE_DOUBLE : defined test_double_within:FAIL: Unity Double Precision Disabled UNITY_INCLUDE_DOUBLE appears nowhere in the repo, the ini files, the workflow, or PlatformIO's unity runner, which adds only UNITY_INCLUDE_CONFIG_H. So TEST_ASSERT_DOUBLE_* is an always-failing stub on Linux and macOS too, not only on Windows. Moved to portduino_base.build_flags_common, which every native env resolves: native, native-tft, native-fb, native-tft-debug, coverage, coverage-event-policy, native-macos, native-windows and native-wasm. This does change Linux and macOS: TEST_ASSERT_DOUBLE_* becomes a real comparison instead of a stub. test_gps_update_scheduling is the only suite using those macros and its arithmetic is integer-based and bit-identical across platforms, so it should pass wherever it runs. Note it currently reports PASSED on CI in 0.03s while emitting no Unity output at all, so those assertions appear never to execute there; that is tracked separately and is not addressed here.
316 lines
12 KiB
C++
316 lines
12 KiB
C++
// Unit tests for Default::getConfiguredOrDefaultMsScaled
|
|
#include "Default.h"
|
|
#include "MeshRadio.h"
|
|
#include "TestUtil.h"
|
|
#include "meshUtils.h"
|
|
#include "modules/RoutingModule.h"
|
|
#include <algorithm>
|
|
#include <unity.h>
|
|
|
|
// Helper to compute expected ms using same logic as Default::congestionScalingCoefficient
|
|
static uint32_t computeExpectedMs(uint32_t defaultSeconds, uint32_t numOnlineNodes)
|
|
{
|
|
uint32_t baseMs = Default::getConfiguredOrDefaultMs(0, defaultSeconds);
|
|
|
|
// Routers (including ROUTER_LATE) don't scale
|
|
if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER ||
|
|
config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) {
|
|
return baseMs;
|
|
}
|
|
|
|
// Sensors and trackers don't scale
|
|
if ((config.device.role == meshtastic_Config_DeviceConfig_Role_SENSOR) ||
|
|
(config.device.role == meshtastic_Config_DeviceConfig_Role_TRACKER)) {
|
|
return baseMs;
|
|
}
|
|
|
|
if (numOnlineNodes <= 40) {
|
|
return baseMs;
|
|
}
|
|
|
|
float bwKHz =
|
|
config.lora.use_preset ? modemPresetToBwKHz(config.lora.modem_preset, false) : bwCodeToKHz(config.lora.bandwidth);
|
|
|
|
uint8_t sf = config.lora.spread_factor;
|
|
if (sf < 7)
|
|
sf = 7;
|
|
else if (sf > 12)
|
|
sf = 12;
|
|
|
|
float throttlingFactor = static_cast<float>(pow_of_2(sf)) / (bwKHz * 100.0f);
|
|
#if USERPREFS_EVENT_MODE
|
|
throttlingFactor = static_cast<float>(pow_of_2(sf)) / (bwKHz * 25.0f);
|
|
#endif
|
|
|
|
int nodesOverForty = (numOnlineNodes - 40);
|
|
float coeff = 1.0f + (nodesOverForty * throttlingFactor);
|
|
return static_cast<uint32_t>(baseMs * coeff + 0.5f);
|
|
}
|
|
|
|
void test_router_no_scaling()
|
|
{
|
|
config.device.role = meshtastic_Config_DeviceConfig_Role_ROUTER;
|
|
// set some sane lora config so bootstrap paths are deterministic
|
|
config.lora.use_preset = false;
|
|
config.lora.spread_factor = 9;
|
|
config.lora.bandwidth = 250;
|
|
|
|
uint32_t res = Default::getConfiguredOrDefaultMsScaled(0, 60, 100);
|
|
uint32_t expected = computeExpectedMs(60, 100);
|
|
TEST_ASSERT_EQUAL_UINT32(expected, res);
|
|
}
|
|
|
|
void test_client_below_threshold()
|
|
{
|
|
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
|
|
config.lora.use_preset = false;
|
|
config.lora.spread_factor = 9;
|
|
config.lora.bandwidth = 250;
|
|
|
|
uint32_t res = Default::getConfiguredOrDefaultMsScaled(0, 60, 40);
|
|
uint32_t expected = computeExpectedMs(60, 40);
|
|
TEST_ASSERT_EQUAL_UINT32(expected, res);
|
|
}
|
|
|
|
void test_client_default_preset_scaling()
|
|
{
|
|
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
|
|
config.lora.use_preset = false;
|
|
config.lora.spread_factor = 9; // SF9
|
|
config.lora.bandwidth = 250; // 250 kHz
|
|
|
|
uint32_t res = Default::getConfiguredOrDefaultMsScaled(0, 60, 50);
|
|
uint32_t expected = computeExpectedMs(60, 50); // nodesOverForty = 10
|
|
TEST_ASSERT_EQUAL_UINT32(expected, res);
|
|
}
|
|
|
|
void test_client_medium_fast_preset_scaling()
|
|
{
|
|
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
|
|
config.lora.use_preset = true;
|
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST;
|
|
// nodesOverForty = 30 -> test with nodes=70
|
|
uint32_t res = Default::getConfiguredOrDefaultMsScaled(0, 60, 70);
|
|
uint32_t expected = computeExpectedMs(60, 70);
|
|
// Allow ±1 ms tolerance for floating-point rounding
|
|
TEST_ASSERT_INT_WITHIN(1, expected, res);
|
|
}
|
|
|
|
void test_router_uses_router_minimums()
|
|
{
|
|
config.device.role = meshtastic_Config_DeviceConfig_Role_ROUTER;
|
|
|
|
uint32_t telemetry = Default::getConfiguredOrMinimumValue(60, min_default_telemetry_interval_secs);
|
|
uint32_t position = Default::getConfiguredOrMinimumValue(60, min_default_broadcast_interval_secs);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(ONE_DAY / 2, telemetry);
|
|
TEST_ASSERT_EQUAL_UINT32(ONE_DAY / 2, position);
|
|
}
|
|
|
|
void test_router_late_uses_router_minimums()
|
|
{
|
|
config.device.role = meshtastic_Config_DeviceConfig_Role_ROUTER_LATE;
|
|
|
|
uint32_t telemetry = Default::getConfiguredOrMinimumValue(60, min_default_telemetry_interval_secs);
|
|
uint32_t position = Default::getConfiguredOrMinimumValue(60, min_default_broadcast_interval_secs);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(ONE_DAY / 2, telemetry);
|
|
TEST_ASSERT_EQUAL_UINT32(ONE_DAY / 2, position);
|
|
}
|
|
|
|
void test_client_uses_public_channel_minimums()
|
|
{
|
|
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
|
|
|
|
uint32_t telemetry = Default::getConfiguredOrMinimumValue(60, min_default_telemetry_interval_secs);
|
|
uint32_t position = Default::getConfiguredOrMinimumValue(60, min_default_broadcast_interval_secs);
|
|
|
|
TEST_ASSERT_EQUAL_UINT32(30 * 60, telemetry);
|
|
TEST_ASSERT_EQUAL_UINT32(60 * 60, position);
|
|
}
|
|
|
|
// --- Saturation/clamp tests for getConfiguredOrDefaultMs[Scaled] ---
|
|
// These guard the INT32_MAX clamp added to avoid uint32 wrap of secs*1000 and
|
|
// to keep results safe to cast to int32_t for OSThread runOnce returns.
|
|
|
|
void test_ms_below_threshold()
|
|
{
|
|
// Ordinary value passes through unchanged.
|
|
TEST_ASSERT_EQUAL_UINT32(60000U, Default::getConfiguredOrDefaultMs(60, 0));
|
|
}
|
|
|
|
void test_ms_at_threshold()
|
|
{
|
|
// INT32_MAX / 1000 = 2,147,483 - largest secs that does not clamp.
|
|
TEST_ASSERT_EQUAL_UINT32(2147483000U, Default::getConfiguredOrDefaultMs(2147483U, 0));
|
|
}
|
|
|
|
void test_ms_just_above_threshold()
|
|
{
|
|
// One second over the boundary must saturate, not wrap.
|
|
TEST_ASSERT_EQUAL_UINT32(static_cast<uint32_t>(INT32_MAX), Default::getConfiguredOrDefaultMs(2147484U, 0));
|
|
}
|
|
|
|
void test_ms_uint32_max()
|
|
{
|
|
// default_sds_secs == UINT32_MAX on non-routers must not wrap.
|
|
TEST_ASSERT_EQUAL_UINT32(static_cast<uint32_t>(INT32_MAX), Default::getConfiguredOrDefaultMs(UINT32_MAX, 0));
|
|
}
|
|
|
|
void test_ms_default_clamps()
|
|
{
|
|
// Clamp also applies when the default-arg path is taken (configured == 0).
|
|
TEST_ASSERT_EQUAL_UINT32(static_cast<uint32_t>(INT32_MAX), Default::getConfiguredOrDefaultMs(0, UINT32_MAX));
|
|
}
|
|
|
|
void test_ms_result_is_int32_safe()
|
|
{
|
|
// Regression guard for runOnce returns: cast to int32_t must not go negative.
|
|
int32_t result = static_cast<int32_t>(Default::getConfiguredOrDefaultMs(UINT32_MAX, 0));
|
|
TEST_ASSERT_GREATER_OR_EQUAL_INT32(0, result);
|
|
}
|
|
|
|
void test_scaled_overflow_saturates()
|
|
{
|
|
// long_fast (SF11/BW250) with a 24h base and heavy congestion overflows
|
|
// the uint32 result without the double-precision guard. Must saturate.
|
|
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
|
|
config.lora.use_preset = false;
|
|
config.lora.spread_factor = 11;
|
|
config.lora.bandwidth = 250;
|
|
|
|
uint32_t res = Default::getConfiguredOrDefaultMsScaled(0, ONE_DAY, 1000);
|
|
TEST_ASSERT_EQUAL_UINT32(static_cast<uint32_t>(INT32_MAX), res);
|
|
}
|
|
|
|
void test_configured_or_default_hop_limit()
|
|
{
|
|
config.lora.hop_limit = HOP_MAX;
|
|
const uint8_t result = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
|
|
|
#if USERPREFS_EVENT_MODE
|
|
TEST_ASSERT_EQUAL_UINT8(Default::eventModeHopLimit, result);
|
|
TEST_ASSERT_EQUAL_UINT8(Default::eventModeHopLimit, Default::getConfiguredOrDefaultHopLimit(Default::eventModeHopLimit));
|
|
#else
|
|
TEST_ASSERT_EQUAL_UINT8(HOP_MAX, result);
|
|
#endif
|
|
}
|
|
|
|
#if USERPREFS_EVENT_MODE
|
|
void test_event_mode_caps_optimized_response()
|
|
{
|
|
config.lora.hop_limit = HOP_MAX;
|
|
meshtastic_MeshPacket request = meshtastic_MeshPacket_init_zero;
|
|
request.hop_start = HOP_MAX;
|
|
request.hop_limit = HOP_MAX - 4;
|
|
|
|
RoutingModule module;
|
|
TEST_ASSERT_EQUAL_UINT8(std::min<uint8_t>(6, Default::eventModeHopLimit), module.getHopLimitForResponse(request));
|
|
}
|
|
#endif
|
|
|
|
// -----------------------------------------------------------------------
|
|
// getConfiguredOrDefaultMsScaled(..., TrafficType) - the region-throttle overload
|
|
// -----------------------------------------------------------------------
|
|
//
|
|
// This is the overload every telemetry and position module actually calls, and nothing covered it:
|
|
// not the throttle multiply, not the <= 1 short-circuit, not the no-region guard, not the 64-bit
|
|
// overflow clamp. Region throttles are real - EU_866 carries PROFILE_LITE with a x10 on both
|
|
// position and telemetry - so a change here silently changes broadcast spacing in that region.
|
|
//
|
|
// Each test pins numOnlineNodes at or below the congestion threshold and uses a non-scaling role,
|
|
// so the congestion coefficient is 1 and the only variable left is the throttle.
|
|
|
|
static const uint32_t kUnscaledNodes = 40; // at/below the threshold: congestion coefficient is 1
|
|
|
|
static void useRegion(meshtastic_Config_LoRaConfig_RegionCode region)
|
|
{
|
|
config.device.role = meshtastic_Config_DeviceConfig_Role_ROUTER; // routers never congestion-scale
|
|
config.lora.region = region;
|
|
initRegion();
|
|
}
|
|
|
|
void test_trafficType_noRegion_returnsUnthrottled()
|
|
{
|
|
config.device.role = meshtastic_Config_DeviceConfig_Role_ROUTER;
|
|
const RegionInfo *saved = myRegion;
|
|
myRegion = nullptr;
|
|
|
|
const uint32_t base = Default::getConfiguredOrDefaultMsScaled(0, 60u, kUnscaledNodes);
|
|
TEST_ASSERT_EQUAL_UINT32(base, Default::getConfiguredOrDefaultMsScaled(0, 60u, kUnscaledNodes, TrafficType::TELEMETRY));
|
|
myRegion = saved;
|
|
}
|
|
|
|
void test_trafficType_neutralThrottle_returnsUnthrottled()
|
|
{
|
|
// US carries PROFILE_STD, whose position and telemetry throttles are both 1 - the neutral
|
|
// multiplier the implementation short-circuits on.
|
|
useRegion(meshtastic_Config_LoRaConfig_RegionCode_US);
|
|
TEST_ASSERT_EQUAL_INT8(1, myRegion->profile->telemetryThrottle);
|
|
|
|
const uint32_t base = Default::getConfiguredOrDefaultMsScaled(0, 60u, kUnscaledNodes);
|
|
TEST_ASSERT_EQUAL_UINT32(base, Default::getConfiguredOrDefaultMsScaled(0, 60u, kUnscaledNodes, TrafficType::TELEMETRY));
|
|
TEST_ASSERT_EQUAL_UINT32(base, Default::getConfiguredOrDefaultMsScaled(0, 60u, kUnscaledNodes, TrafficType::POSITION));
|
|
}
|
|
|
|
void test_trafficType_regionThrottleMultiplies()
|
|
{
|
|
// EU_866 carries PROFILE_LITE: positionThrottle and telemetryThrottle are both 10.
|
|
useRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_866);
|
|
const int8_t telemetryThrottle = myRegion->profile->telemetryThrottle;
|
|
const int8_t positionThrottle = myRegion->profile->positionThrottle;
|
|
TEST_ASSERT_GREATER_THAN_INT8(1, telemetryThrottle);
|
|
|
|
const uint32_t base = Default::getConfiguredOrDefaultMsScaled(0, 60u, kUnscaledNodes);
|
|
TEST_ASSERT_EQUAL_UINT32(base * telemetryThrottle,
|
|
Default::getConfiguredOrDefaultMsScaled(0, 60u, kUnscaledNodes, TrafficType::TELEMETRY));
|
|
TEST_ASSERT_EQUAL_UINT32(base * positionThrottle,
|
|
Default::getConfiguredOrDefaultMsScaled(0, 60u, kUnscaledNodes, TrafficType::POSITION));
|
|
}
|
|
|
|
void test_trafficType_overflowSaturates()
|
|
{
|
|
// A day-long base times a x10 region throttle exceeds uint32 without the 64-bit guard.
|
|
useRegion(meshtastic_Config_LoRaConfig_RegionCode_EU_866);
|
|
|
|
const uint32_t res = Default::getConfiguredOrDefaultMsScaled(0, 3 * ONE_DAY, kUnscaledNodes, TrafficType::TELEMETRY);
|
|
TEST_ASSERT_EQUAL_UINT32(static_cast<uint32_t>(INT32_MAX), res);
|
|
}
|
|
|
|
// Required by Unity: PlatformIO's weak defaults do not link on MinGW (PE-COFF weak externals).
|
|
void setUp(void) {}
|
|
void tearDown(void) {}
|
|
|
|
void setup()
|
|
{
|
|
// Small delay to match other test mains
|
|
delay(10);
|
|
initializeTestEnvironment();
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_router_no_scaling);
|
|
RUN_TEST(test_client_below_threshold);
|
|
RUN_TEST(test_client_default_preset_scaling);
|
|
RUN_TEST(test_client_medium_fast_preset_scaling);
|
|
RUN_TEST(test_router_uses_router_minimums);
|
|
RUN_TEST(test_router_late_uses_router_minimums);
|
|
RUN_TEST(test_client_uses_public_channel_minimums);
|
|
RUN_TEST(test_ms_below_threshold);
|
|
RUN_TEST(test_ms_at_threshold);
|
|
RUN_TEST(test_ms_just_above_threshold);
|
|
RUN_TEST(test_ms_uint32_max);
|
|
RUN_TEST(test_ms_default_clamps);
|
|
RUN_TEST(test_ms_result_is_int32_safe);
|
|
RUN_TEST(test_scaled_overflow_saturates);
|
|
RUN_TEST(test_configured_or_default_hop_limit);
|
|
RUN_TEST(test_trafficType_noRegion_returnsUnthrottled);
|
|
RUN_TEST(test_trafficType_neutralThrottle_returnsUnthrottled);
|
|
RUN_TEST(test_trafficType_regionThrottleMultiplies);
|
|
RUN_TEST(test_trafficType_overflowSaturates);
|
|
#if USERPREFS_EVENT_MODE
|
|
RUN_TEST(test_event_mode_caps_optimized_response);
|
|
#endif
|
|
exit(UNITY_END());
|
|
}
|
|
|
|
void loop() {}
|