Files
Thomas Göttgens 34680833b8 fix(test): make the native-windows test suite build and run (#11482)
* 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.
2026-08-14 00:51:40 +00:00

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() {}