281 lines
13 KiB
Python
281 lines
13 KiB
Python
"""Run real role deadlines while preserving timed sleep and interrupt idle."""
|
|
from pathlib import Path
|
|
import os
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
from test_radio_receive_contract import method
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
HARNESS = r'''
|
|
#include <cassert>
|
|
#include <cstdint>
|
|
#include <initializer_list>
|
|
#include <helpers/PowerManagementUtils.h>
|
|
#include <helpers/TempRadioLeaseDeadline.h>
|
|
#include <helpers/PowerSavingDiagnostics.h>
|
|
#define SENSOR_READ_INTERVAL_SECS 60
|
|
static uint32_t now_ms = 1000;
|
|
uint32_t millis() { return now_ms; }
|
|
struct Node {
|
|
bool pending = false, queue = false, retry = false;
|
|
uint32_t queue_ms = 0, retry_ms = 0;
|
|
struct { bool battery_alert_enabled = false; } _prefs;
|
|
struct { int getNumClients() const { return 0; } } acl;
|
|
unsigned long next_flood_advert = 0, next_local_advert = 0;
|
|
unsigned long dirty_contacts_expiry = 0, next_recent_repeater_sweep = 0;
|
|
unsigned long next_battery_alert_check = 0, next_push = 0;
|
|
unsigned long radio_apply_retry_at = 0, set_radio_at = 0, revert_radio_at = 0;
|
|
uint32_t last_read_time = 100;
|
|
struct Alert { unsigned long send_expiry = 0; } alert;
|
|
Alert* alert_tasks[1] = {&alert};
|
|
int num_alert_tasks = 0;
|
|
struct Clock { uint32_t getCurrentTime() const { return 100; } } clock;
|
|
const Clock* getRTCClock() const { return &clock; }
|
|
bool hasPendingWork() const { return pending; }
|
|
mesh::powerdiag::Mask pendingWorkReason() const {
|
|
return pending ? mesh::powerdiag::mask(mesh::powerdiag::Reason::Mesh) : 0;
|
|
}
|
|
bool getNextQueueWakeDelay(uint32_t& out) const { out = queue_ms; return queue; }
|
|
bool getNextRetryWakeDelay(uint32_t& out) const { out = retry_ms; return retry; }
|
|
bool millisHasNowPassed(unsigned long at) const {
|
|
return int32_t(now_ms - uint32_t(at)) > 0;
|
|
}
|
|
uint32_t limitSleepToScheduledRadioWork(uint32_t seconds) const { return seconds; }
|
|
uint32_t limitSleepToMillisTimer(unsigned long, uint32_t) const;
|
|
uint32_t getPowerSaveSleepMilliseconds(uint32_t) const;
|
|
uint32_t getPowerSaveSleepSeconds(uint32_t) const;
|
|
};
|
|
@METHODS@
|
|
int main() {
|
|
Node node;
|
|
using Lease = mesh::TempRadioLeaseDeadline;
|
|
assert(Lease::millisecondsUntil(10, 0) == UINT32_MAX);
|
|
assert(Lease::millisecondsUntil(100, 100) == 0);
|
|
assert(Lease::millisecondsUntil(101, 100) == 0);
|
|
assert(Lease::millisecondsUntil(100, uint64_t(UINT32_MAX) + 101) == UINT32_MAX);
|
|
for (uint64_t now : {1000ULL, 0x100000001ULL, UINT64_MAX - 100000ULL}) {
|
|
for (uint32_t delay : {1U, 50U, 999U, 1001U, 30000U})
|
|
assert(Lease::millisecondsUntil(now, now + delay) == delay);
|
|
}
|
|
assert(mesh::power::powerSaveSleepSeconds(0) == 0);
|
|
assert(mesh::power::secondsToSleepMilliseconds(0) == 0);
|
|
assert(mesh::power::secondsToSleepMilliseconds(30) == 30000);
|
|
assert(mesh::power::secondsToSleepMilliseconds(4294967) == 4294967000U);
|
|
assert(mesh::power::secondsToSleepMilliseconds(4294968) == UINT32_MAX);
|
|
assert(mesh::power::secondsToSleepMilliseconds(UINT32_MAX) == UINT32_MAX);
|
|
assert(mesh::power::limitSleepMilliseconds(0, 50) == 0);
|
|
assert(mesh::power::limitSleepMilliseconds(30, 0) == 0);
|
|
assert(mesh::power::limitSleepMilliseconds(30, 50) == 50);
|
|
assert(mesh::power::limitSleepMilliseconds(1, 1001) == 1000);
|
|
assert(mesh::power::limitSleepMilliseconds(UINT32_MAX, 50) == 50);
|
|
assert(mesh::power::limitSleepMilliseconds(UINT32_MAX, UINT32_MAX) == UINT32_MAX);
|
|
#if EXPECT_INTERRUPT_IDLE
|
|
assert(mesh::power::powerSaveSleepSeconds(UINT32_MAX) == 4294968U);
|
|
#else
|
|
assert(mesh::power::powerSaveSleepSeconds(UINT32_MAX) == 4294967U);
|
|
#endif
|
|
assert(node.getPowerSaveSleepSeconds(30) == 30);
|
|
assert(node.getPowerSaveSleepSeconds(0) == 0);
|
|
assert(node.getPowerSaveSleepMilliseconds(30000) == 30000);
|
|
assert(node.getPowerSaveSleepMilliseconds(0) == 0);
|
|
assert(node.limitSleepToMillisTimer(0, 30000) == 30000); // Disabled timer sentinel.
|
|
assert(node.limitSleepToMillisTimer(1001, 0) == 0);
|
|
node.pending = true;
|
|
assert(node.getPowerSaveSleepSeconds(30) == 0);
|
|
node.pending = false;
|
|
for (uint32_t now : {1000U, UINT32_MAX - 15U}) {
|
|
now_ms = now;
|
|
// Timed sleep ends before the deadline. Event-driven boards instead permit
|
|
// interrupt idle for every positive remainder, including across rollover.
|
|
for (uint32_t ms : {0U, 1U, 999U, 1000U, 1001U, 1999U, 2000U,
|
|
2500U, 29999U, 30000U, 30001U, 60000U}) {
|
|
const uint32_t whole_seconds = ms / 1000;
|
|
#if EXPECT_INTERRUPT_IDLE
|
|
// Event-driven boards ignore seconds and wake at the next IRQ.
|
|
const uint32_t idle_seconds = whole_seconds + (ms % 1000 != 0);
|
|
#else
|
|
const uint32_t idle_seconds = whole_seconds;
|
|
#endif
|
|
const uint32_t expected = idle_seconds < 30 ? idle_seconds : 30;
|
|
const uint32_t expected_ms = ms < 30000 ? ms : 30000;
|
|
node.queue = true; node.queue_ms = ms;
|
|
assert(node.getPowerSaveSleepSeconds(30) == expected);
|
|
assert(node.getPowerSaveSleepMilliseconds(30000) == expected_ms);
|
|
node.queue = false; node.retry = true; node.retry_ms = ms;
|
|
assert(node.getPowerSaveSleepSeconds(30) == expected);
|
|
assert(node.getPowerSaveSleepMilliseconds(30000) == expected_ms);
|
|
node.retry = false;
|
|
const uint32_t deadline = now + ms;
|
|
assert(deadline != 0); // Zero has a separate intentional sentinel meaning.
|
|
assert(node.limitSleepToMillisTimer(deadline, 30000) == expected_ms);
|
|
node.next_flood_advert = deadline;
|
|
assert(node.getPowerSaveSleepSeconds(30) == expected);
|
|
assert(node.getPowerSaveSleepMilliseconds(30000) == expected_ms);
|
|
node.next_flood_advert = 0;
|
|
}
|
|
assert(node.limitSleepToMillisTimer(uint32_t(now - 1), 30) == 0);
|
|
node.queue = true; node.queue_ms = 2500;
|
|
node.retry = true; node.retry_ms = 1001;
|
|
assert(node.getPowerSaveSleepMilliseconds(30000) == 1001);
|
|
#if EXPECT_INTERRUPT_IDLE
|
|
assert(node.getPowerSaveSleepSeconds(30) == 2); // Interrupt idle is permitted.
|
|
#else
|
|
assert(node.getPowerSaveSleepSeconds(30) == 1); // Earliest timed owner wins.
|
|
#endif
|
|
node.dirty_contacts_expiry = uint32_t(now + 999);
|
|
assert(node.getPowerSaveSleepMilliseconds(30000) == 999);
|
|
#if EXPECT_INTERRUPT_IDLE
|
|
assert(node.getPowerSaveSleepSeconds(30) == 1);
|
|
#else
|
|
assert(node.getPowerSaveSleepSeconds(30) == 0);
|
|
#endif
|
|
node.dirty_contacts_expiry = 0;
|
|
node.queue = node.retry = false;
|
|
}
|
|
}
|
|
'''
|
|
|
|
|
|
class PowerSavingDeadlineTests(unittest.TestCase):
|
|
def test_actual_role_loops_preserve_subsecond_sleep_budgets(self):
|
|
from cpp_source import body
|
|
harness = r'''
|
|
#include <cassert>
|
|
#include <cstdint>
|
|
#include <initializer_list>
|
|
#include <helpers/PowerSavingDiagnostics.h>
|
|
#include <helpers/PowerManagementUtils.h>
|
|
#define POWERSAVING_FIRSTSLEEP_SECS 1
|
|
#define POWERSAVING_FIRST_SLEEP_SECS 1
|
|
static uint32_t millis() { return 100000; }
|
|
struct Mesh {
|
|
uint32_t budget = 30000, noise_delay = 50;
|
|
bool noise = false;
|
|
uint32_t getPowerSaveSleepMilliseconds(uint32_t limit) const {
|
|
assert(limit == 30000); return budget;
|
|
}
|
|
bool getNextNoiseFloorWakeDelay(uint32_t& out) const {
|
|
out = noise_delay; return noise;
|
|
}
|
|
bool millisHasNowPassed(uint32_t) const { return true; }
|
|
} the_mesh;
|
|
struct Board {
|
|
unsigned calls = 0;
|
|
uint32_t last_ms = 0;
|
|
void sleep(uint32_t seconds) { ++calls; last_ms = seconds * 1000; }
|
|
void sleepMilliseconds(uint32_t ms) { ++calls; last_ms = ms; }
|
|
} board;
|
|
void idle(bool can_power_save) { @GATE@ }
|
|
int main() {
|
|
for (uint32_t budget : {0U, 1U, 50U, 999U, 1001U, 29999U, 30000U}) {
|
|
the_mesh.budget = budget;
|
|
for (bool noise : {false, true}) {
|
|
the_mesh.noise = noise;
|
|
const auto before = board.calls;
|
|
idle(true);
|
|
assert(board.calls == before + (budget != 0));
|
|
if (budget) assert(board.last_ms == (noise && budget > 50 ? 50 : budget));
|
|
const auto after = board.calls;
|
|
idle(false); assert(board.calls == after);
|
|
}
|
|
}
|
|
the_mesh.noise_delay = 0;
|
|
const auto before = board.calls;
|
|
idle(true); assert(board.calls == before);
|
|
}
|
|
'''
|
|
with tempfile.TemporaryDirectory(prefix='meshcore-role-ms-idle-') as directory:
|
|
cpp, exe = Path(directory) / 'test.cpp', Path(directory) / 'test'
|
|
for role in ('simple_repeater', 'simple_room_server', 'simple_sensor'):
|
|
with self.subTest(role=role):
|
|
source = (ROOT / 'examples' / role / 'main.cpp').read_text()
|
|
gate = body(source, ' if (can_power_save) {')
|
|
cpp.write_text(harness.replace('@GATE@', gate), encoding='ascii')
|
|
built = subprocess.run([
|
|
os.environ.get('CXX', 'g++'), '-std=c++17', '-Wall', '-Wextra', '-Werror',
|
|
'-I', str(ROOT / 'src'), str(cpp), '-o', str(exe),
|
|
], capture_output=True, text=True)
|
|
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
|
|
ran = subprocess.run([str(exe)], capture_output=True, text=True)
|
|
self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr)
|
|
|
|
def test_real_board_millisecond_idle_preserves_port_wake_contract(self):
|
|
source = (ROOT / 'src/MeshCore.h').read_text(encoding='ascii')
|
|
implementation = method(source, 'virtual void sleepMilliseconds(uint32_t millisecs)')
|
|
code = r'''
|
|
#include <cassert>
|
|
#include <cstdint>
|
|
struct Board {
|
|
unsigned calls = 0;
|
|
uint32_t last = 0;
|
|
virtual void sleep(uint32_t secs) { ++calls; last = secs; }
|
|
@METHOD@
|
|
};
|
|
int main() {
|
|
Board board;
|
|
board.sleepMilliseconds(0);
|
|
assert(board.calls == 0);
|
|
for (uint32_t ms : {1U, 50U, 999U, 1000U, 1001U, UINT32_MAX}) {
|
|
const auto before = board.calls;
|
|
board.sleepMilliseconds(ms);
|
|
#if EXPECT_INTERRUPT_IDLE
|
|
assert(board.calls == before + 1 && board.last == 0);
|
|
#else
|
|
assert(board.calls == before + (ms >= 1000));
|
|
if (ms >= 1000) assert(board.last == ms / 1000);
|
|
#endif
|
|
}
|
|
}
|
|
'''.replace('@METHOD@', implementation).replace(
|
|
'#include <cstdint>', '#include <cstdint>\n#include <initializer_list>')
|
|
with tempfile.TemporaryDirectory(prefix='meshcore-board-ms-idle-') as directory:
|
|
cpp, exe = Path(directory) / 'test.cpp', Path(directory) / 'test'
|
|
cpp.write_text(code, encoding='ascii')
|
|
for platform in ('', 'NRF52_PLATFORM', 'RP2040_PLATFORM', 'STM32_PLATFORM'):
|
|
with self.subTest(platform=platform or 'unknown-timed'):
|
|
built = subprocess.run([
|
|
os.environ.get('CXX', 'g++'), '-std=c++17', '-Wall', '-Wextra', '-Werror',
|
|
'-DEXPECT_INTERRUPT_IDLE=' + ('1' if platform else '0'),
|
|
*(['-D' + platform + '=1'] if platform else []),
|
|
str(cpp), '-o', str(exe),
|
|
], capture_output=True, text=True)
|
|
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
|
|
ran = subprocess.run([str(exe)], capture_output=True, text=True)
|
|
self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr)
|
|
|
|
def test_real_role_deadlines_preserve_timed_and_interrupt_idle(self):
|
|
with tempfile.TemporaryDirectory(prefix='meshcore-sleep-deadlines-') as directory:
|
|
work = Path(directory)
|
|
for role, filename, name in (
|
|
('repeater', 'examples/simple_repeater/MyMesh.cpp', 'MyMesh'),
|
|
('room', 'examples/simple_room_server/MyMesh.cpp', 'MyMesh'),
|
|
('sensor', 'examples/simple_sensor/SensorMesh.cpp', 'SensorMesh')):
|
|
for platform in ('', 'NRF52_PLATFORM', 'RP2040_PLATFORM', 'STM32_PLATFORM'):
|
|
with self.subTest(role=role, platform=platform or 'timed'):
|
|
source = (ROOT / filename).read_text()
|
|
methods = '\n'.join(method(source, signature) for signature in (
|
|
f'uint32_t {name}::limitSleepToMillisTimer(',
|
|
f'uint32_t {name}::getPowerSaveSleepMilliseconds(',
|
|
f'uint32_t {name}::getPowerSaveSleepSeconds('))
|
|
methods = methods.replace(name + '::', 'Node::')
|
|
cpp, exe = work / (role + '.cpp'), work / role
|
|
cpp.write_text(HARNESS.replace('@METHODS@', methods))
|
|
sanitizers = [] if os.name == 'nt' else [
|
|
'-fsanitize=address,undefined', '-fno-sanitize-recover=all',
|
|
'-fno-pie', '-no-pie']
|
|
built = subprocess.run([os.environ.get('CXX', 'g++'), '-std=c++17',
|
|
'-Wall', '-Wextra', '-Werror', *sanitizers,
|
|
'-DEXPECT_INTERRUPT_IDLE=' + ('1' if platform else '0'),
|
|
*(['-D' + platform + '=1'] if platform else []), '-I', str(ROOT / 'src'),
|
|
str(cpp), '-o', str(exe)], capture_output=True, text=True)
|
|
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
|
|
ran = subprocess.run([str(exe)], capture_output=True, text=True)
|
|
self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|