"""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 #include #include #include #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; } 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 getPowerSaveSleepSeconds(uint32_t) const; }; @METHODS@ int main() { Node node; assert(mesh::power::powerSaveSleepSeconds(0) == 0); #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.limitSleepToMillisTimer(0, 30) == 30); // 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; node.queue = true; node.queue_ms = ms; assert(node.getPowerSaveSleepSeconds(30) == expected); node.queue = false; node.retry = true; node.retry_ms = ms; assert(node.getPowerSaveSleepSeconds(30) == expected); node.retry = false; const uint32_t deadline = now + ms; assert(deadline != 0); // Zero has a separate intentional sentinel meaning. assert(node.limitSleepToMillisTimer(deadline, 30) == expected); node.next_flood_advert = deadline; assert(node.getPowerSaveSleepSeconds(30) == expected); 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; #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); #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_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}::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()