#!/usr/bin/env python3 """Execute production repeater scheduling, watchdog and advert service methods.""" from pathlib import Path import os import subprocess import tempfile import unittest ROOT = Path(__file__).resolve().parents[1] def method(text, signature): start = text.index(signature) end = text.index("{", start) + 1 depth = 1 while depth: depth += (text[end] == "{") - (text[end] == "}") end += 1 return text[start:end] HARNESS = r''' #include #include #include #include #include #include #define MESH_DEBUG_PRINTLN(...) ((void)0) #define MESH_ENABLE_TELEMETRY_HISTORY 0 #define MAX_SCHEDULED_RADIO_SETTINGS 8 #define MAX_SCHEDULED_RADIO_SETTINGS_PER_TYPE 4 #define SCHEDULED_RADIO_CLOCK_CHECKPOINT_SECS 60 #define RADIO_APPLY_RETRY_INTERVAL_MILLIS 100 static uint32_t now_ms = 1; uint32_t millis() { return now_ms; } namespace mesh { struct Packet {}; int clampLoRaTxPower(int power, float) { return power; } namespace lr2021 { bool storedSideDetectorCount(const uint8_t*, uint8_t& count) { count = 0; return true; } bool validateSideDetectorSFs(const uint8_t*, uint8_t, uint8_t, float) { return true; } } } struct CommonCLI { template static void recalculateRxPowerSavingFromLevel(T*) {} }; uint32_t nextRadioApplyRetryDelay(uint8_t& failures) { ++failures; return 100; } struct RadioDriver { bool gain_supported = true, gain_success = true, power_success = true; bool side_detector_success = true; unsigned gain_calls = 0, power_calls = 0; bool active_gain = false; int active_power = 0; bool supportsRxBoostedGainMode() const { return gain_supported; } bool setRxBoostedGainMode(bool gain) { ++gain_calls; if (!gain_success) return false; active_gain = gain; return true; } bool setTxPower(int power) { ++power_calls; if (!power_success) return false; active_power = power; return true; } bool configSideDetectors(const uint8_t*, uint8_t, float) { return side_detector_success; } } radio_driver; struct RTC { uint32_t now = 100000; uint32_t getCurrentTime() const { return now; } }; struct RadioPrefs { bool rx_watchdog_enabled = true, rx_boosted_gain = true; uint8_t advert_interval = 60, flood_advert_interval = 24, path_hash_mode = 0; float freq = 909.5f, bw = 62.5f; uint8_t sf = 7, cr = 5; uint8_t extra_sf[4] = {}; int tx_power_dbm = 20; }; struct Board { int reboots = 0; void reboot() { ++reboots; } }; struct CLI { Board board; Board* getBoard() { return &board; } CLI& radioProfiles() { return *this; } RadioPrefs* prefs = nullptr; int* saves = nullptr; bool save_success = true; unsigned save_attempts = 0; unsigned fail_on_save = 0; uint16_t saved_preamble = 0; bool savePrimaryRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, uint16_t preamble) { ++save_attempts; if (!save_success || save_attempts == fail_on_save) return false; ++*saves; prefs->freq = freq; prefs->bw = bw; prefs->sf = sf; prefs->cr = cr; saved_preamble = preamble; return true; } uint16_t primaryPreamble() const { return saved_preamble; } bool hasReplyMutation() const { return false; } bool finishReplyMutation(bool) { return true; } }; struct Radio { uint32_t last_rx = 0; uint32_t getLastRecvMillis() { return last_rx; } }; // This fixture exercises packet-radio scheduling, not a Morse session. The // separate Morse loop-policy suite executes the real ownership predicate. namespace mesh { bool morseTimingActive(const Radio*) { return false; } } struct Barrier { bool held = false; bool waiting() const { return held; } void clear() { held = false; } }; struct ScheduledRadioSetting { bool active = false, temporary = false, started = false; float freq = 0, bw = 0; uint8_t sf = 0, cr = 0; uint16_t preamble = 0; uint32_t start_time = 0, end_time = 0; uint64_t hard_end_uptime_millis = 0; }; class MyMesh { public: RadioPrefs _prefs; CLI _cli; Radio radio; Radio* _radio = &radio; uint32_t last_meshcore_rx = 0; uint32_t getLastMeshCoreRecvMillis() const { return last_meshcore_rx; } RTC rtc; Barrier temp_radio_reply_barrier; uint32_t primary_radio_mutation_generation = 0, radio_reply_deadline = 0; bool primary_radio_mutation_starts_temp = false; bool radio_reply_secondary = false; mesh::RxInactivityWatchdog rx_inactivity_watchdog; mesh::RepeaterRadioTiming radio_timing; ScheduledRadioSetting scheduled_radio_settings[MAX_SCHEDULED_RADIO_SETTINGS]; uint64_t uptime_millis = 1; uint32_t last_millis = 1; uint32_t next_local_advert = 0, next_flood_advert = 0; bool saved_radio_apply_pending = false, temp_radio_handoff_pending = false; bool temp_radio_applied = false, scheduled_temp_radio_started = false; uint32_t next_scheduled_radio_time = 0, next_scheduled_radio_check_at = 0; uint32_t scheduled_temp_radio_end_time = 0, scheduled_temp_radio_end_check_at = 0; bool scheduled_temp_radio_end_check_final = false; uint32_t scheduled_radio_retry_at = 0; uint32_t scheduled_radio_save_retry_at = 0; uint8_t scheduled_radio_retry_failures = 0; bool pending_self_advert = false, pending_self_advert_flood = false; uint32_t pending_self_advert_delay = 0; bool outbound = false, apply_success = true, restore_success = true; int saves = 0, applies = 0, restores = 0, local_adverts = 0, flood_adverts = 0; int default_scope = 0; mesh::Packet packet; MyMesh() { _cli.prefs = &_prefs; _cli.saves = &saves; radio_driver = {}; now_ms = 1; updateAdvertTimer(); updateFloodAdvertTimer(); } RTC* getRTCClock() const { return const_cast(&rtc); } uint32_t futureMillis(uint32_t delay) const { return now_ms + delay; } bool millisHasNowPassed(uint32_t deadline) const { return (int32_t)(now_ms - deadline) >= 0; } bool hasOutbound() const { return outbound; } bool hasStartedScheduledTempRadio() const { return scheduled_temp_radio_started; } bool applyRadioParams(float, float, uint8_t, uint8_t, uint16_t = 0, bool temporary = false) { if (temporary) { ++applies; return apply_success; } ++restores; return restore_success; } bool applySavedRadioParams(); bool isValidScheduledRadioParams(float, float, uint8_t, uint8_t) { return true; } void savePrefs() { ++saves; } int getScheduledRadioSettingIndex(bool, int slot) const { return slot + 1; } void formatScheduledRadioDuration(char* dest, size_t size, uint32_t when) const { snprintf(dest, size, "%lus", (unsigned long)(when - rtc.now)); } void appendTempRadioTimingNote(char* reply, size_t size, uint32_t seconds) const { mesh::RepeaterRadioTiming::appendTempWatchdogNote(reply, size, seconds); } void advance(uint32_t seconds) { now_ms += seconds * 1000UL; uptime_millis += (uint64_t)seconds * 1000UL; last_millis = now_ms; rtc.now += seconds; } mesh::Packet* createSelfAdvert() { return &packet; } void sendFloodScoped(int, mesh::Packet*, uint32_t, int) { ++flood_adverts; } void sendZeroHop(mesh::Packet*) { ++local_adverts; } void sendSelfAdvertisementNow(uint32_t, bool) {} void checkBatteryAlert() {} void expireRecentRepeatersIfDue() {} void checkRxInactivityWatchdog(); void setTempRadioTiming(uint32_t); void updateAdvertTimer(); void updateFloodAdvertTimer(); void queueSavedRadioApply(); void refreshScheduledRadioState(); void processScheduledRadioSettings(); void applyTempRadioParams(float, float, uint8_t, uint8_t, int, uint16_t = 0); bool scheduleNormalRadio(); void clearScheduledRadioSetting(int, bool); int findFreeScheduledRadioSlot() const; int countScheduledRadioSettings(bool) const; bool scheduledRadioConflicts(bool, uint32_t, uint32_t) const; void addScheduledRadioParams(bool, float, float, uint8_t, uint8_t, uint32_t, uint32_t, char*, uint16_t = 0); void servicePostMeshLoop(); }; @METHODS@ static constexpr uint32_t HOUR = 3600; static void startTemp(MyMesh& m, int minutes) { m.applyTempRadioParams(910, 250, 5, 5, minutes); m.advance(2); m.servicePostMeshLoop(); assert(m.temp_radio_applied); } int main() { for (unsigned failure = 0; failure < #ifdef USE_LR2021 4 #else 3 #endif ; ++failure) { // A failure at any independent hardware step retains the saved recovery // work and its backoff until the complete configuration succeeds. MyMesh m; if (failure == 0) radio_driver.gain_success = false; if (failure == 1) m.restore_success = false; if (failure == 2) radio_driver.power_success = false; if (failure == 3) radio_driver.side_detector_success = false; m.queueSavedRadioApply(); m.processScheduledRadioSettings(); assert(m.saved_radio_apply_pending && m.scheduled_radio_retry_at); const unsigned gain_calls = radio_driver.gain_calls; const unsigned power_calls = radio_driver.power_calls; const unsigned restores = m.restores; for (unsigned i = 0; i < 10; ++i) m.processScheduledRadioSettings(); assert(radio_driver.gain_calls == gain_calls); assert(radio_driver.power_calls == power_calls && m.restores == (int)restores); radio_driver.gain_success = radio_driver.power_success = true; radio_driver.side_detector_success = true; m.restore_success = true; m.advance(1); m.processScheduledRadioSettings(); assert(!m.saved_radio_apply_pending && !m.scheduled_radio_retry_at); assert(radio_driver.active_gain == m._prefs.rx_boosted_gain); assert(radio_driver.active_power == m._prefs.tx_power_dbm); } { // A later failed due entry must not undo an earlier durable change. MyMesh m; char reply[160]; m.addScheduledRadioParams(false, 911, 250, 5, 5, m.rtc.now + 2, 0, reply, 48); m.addScheduledRadioParams(false, 912, 125, 6, 6, m.rtc.now + 3, 0, reply, 80); m._cli.fail_on_save = 2; m.advance(3); m.servicePostMeshLoop(); assert(m._cli.save_attempts == 2 && m.saves == 1); assert(m.countScheduledRadioSettings(false) == 1); assert(m._prefs.freq == 911 && m._cli.saved_preamble == 48); assert(!m.saved_radio_apply_pending); m.advance(60); m.servicePostMeshLoop(); assert(m._cli.save_attempts == 3 && m.saves == 2); assert(m.countScheduledRadioSettings(false) == 0); assert(m._prefs.freq == 912 && m._cli.saved_preamble == 80); assert(!m.saved_radio_apply_pending); } { // Radios without boosted gain must not become stuck in recovery. MyMesh m; radio_driver.gain_supported = false; radio_driver.gain_success = false; m.queueSavedRadioApply(); m.processScheduledRadioSettings(); assert(!m.saved_radio_apply_pending && radio_driver.gain_calls == 0); assert(radio_driver.active_power == m._prefs.tx_power_dbm); } { // Boot uses the same complete apply result to arm recovery. MyMesh m; radio_driver.power_success = false; m.saved_radio_apply_pending = !m.applySavedRadioParams(); assert(m.saved_radio_apply_pending); radio_driver.power_success = true; m.processScheduledRadioSettings(); assert(!m.saved_radio_apply_pending); } { // Expiring one scheduled lease must retain a later temporary entry. MyMesh m; char reply[160]; m.addScheduledRadioParams(true, 910, 250, 5, 5, m.rtc.now + 2, m.rtc.now + 62, reply); m.addScheduledRadioParams(true, 911, 250, 5, 5, m.rtc.now + 122, m.rtc.now + 182, reply); assert(m.countScheduledRadioSettings(true) == 2); m.advance(2); m.servicePostMeshLoop(); assert(m.temp_radio_applied); m.advance(60); m.servicePostMeshLoop(); assert(m.countScheduledRadioSettings(true) == 1 && !m.temp_radio_applied); m.advance(60); m.servicePostMeshLoop(); assert(m.temp_radio_applied); const auto before = m.primary_radio_mutation_generation; m.queueSavedRadioApply(); assert(m.primary_radio_mutation_generation != before); } for (bool rollover : {false, true}) { MyMesh m; if (rollover) { now_ms = UINT32_MAX - 30000; m.last_millis = now_ms; } char reply[160]; m.addScheduledRadioParams(false, 912.5, 250, 5, 5, m.rtc.now + 2, 0, reply, 48); assert(!strncmp(reply, "OK", 2)); m._cli.save_success = false; m.advance(2); m.servicePostMeshLoop(); assert(m._cli.save_attempts == 1 && m.saves == 0); assert(m.next_scheduled_radio_check_at == m.scheduled_radio_save_retry_at); assert(m._prefs.freq == 909.5f && m._cli.saved_preamble == 0); assert(m.countScheduledRadioSettings(false) == 1); for (unsigned i = 0; i < 10; ++i) m.servicePostMeshLoop(); assert(m._cli.save_attempts == 1); // storage backoff survives the scheduler's cleanup m._cli.save_success = true; m.advance(59); m.servicePostMeshLoop(); assert(m._cli.save_attempts == 1); m.advance(1); m.servicePostMeshLoop(); assert(m._cli.save_attempts == 2 && m.saves == 1); assert(m._prefs.freq == 912.5f && !m.saved_radio_apply_pending); assert(m._cli.saved_preamble == 48 && m.countScheduledRadioSettings(false) == 0); } { // Backing off a failed permanent save must not delay temporary start/end. MyMesh m; char reply[160]; m.addScheduledRadioParams(false, 912.5, 250, 5, 5, m.rtc.now + 2, 0, reply, 48); m.addScheduledRadioParams(true, 911.5, 250, 5, 5, m.rtc.now + 30, m.rtc.now + 90, reply, 80); m._cli.save_success = false; m.advance(2); m.servicePostMeshLoop(); assert(m._cli.save_attempts == 1); m.advance(28); m.servicePostMeshLoop(); assert(m.temp_radio_applied && m.radio_timing.isTemporary()); assert(m._cli.save_attempts == 1); m.advance(60); m.servicePostMeshLoop(); assert(!m.temp_radio_applied && !m.radio_timing.isTemporary()); assert(m._prefs.freq == 909.5f); } { // Pending schedules do not change current timings, even on an apply failure. MyMesh m; const uint32_t normal_local = m.next_local_advert; char reply[160]; m.addScheduledRadioParams(true, 910, 250, 5, 5, m.rtc.now + 60, m.rtc.now + 60 + 24 * HOUR, reply); assert(strstr(reply, "1440 mins (1d0h0m); rx.watchdog=12hours")); assert(!m.radio_timing.isTemporary()); assert(m.next_local_advert == normal_local); m.apply_success = false; m.advance(60); m.servicePostMeshLoop(); assert(!m.radio_timing.isTemporary()); m.apply_success = true; m.advance(1); m.servicePostMeshLoop(); assert(m.radio_timing.tempDuration() == 24 * HOUR); assert(m.next_local_advert == now_ms + HOUR * 1000); assert(m.next_flood_advert == now_ms + 3 * HOUR * 1000); assert(m.saves == 0); assert(m._prefs.advert_interval == 60 && m._prefs.flood_advert_interval == 24); } { // The exact-reply barrier must still delay activation and its overrides. MyMesh m; m.applyTempRadioParams(910, 250, 5, 5, 1440); m.temp_radio_reply_barrier.held = true; m.advance(3); m.servicePostMeshLoop(); assert(!m.radio_timing.isTemporary()); m.temp_radio_reply_barrier.clear(); m.advance(1); m.servicePostMeshLoop(); assert(m.radio_timing.tempDuration() == 24 * HOUR); } { // Both adverts are sent at hour three; hourly local adverts cannot vanish. MyMesh m; startTemp(m, 1440); for (int hour = 1; hour <= 6; ++hour) { m.advance(HOUR); m.servicePostMeshLoop(); } assert(m.local_adverts == 6 && m.flood_adverts == 2); assert(m._cli.board.reboots == 0 && m.saves == 0); } { // A short session keeps the normal flood setting, including disabled. MyMesh m; m._prefs.flood_advert_interval = 0; m._prefs.advert_interval = 0; startTemp(m, 180); assert(m.next_flood_advert == 0); assert(m.radio_timing.watchdogMillis(true) == 0); m.advance(HOUR); m.servicePostMeshLoop(); assert(m.local_adverts == 1 && m.flood_adverts == 0); m.advance(2 * HOUR); m.servicePostMeshLoop(); assert(!m.radio_timing.isTemporary()); assert(m.next_local_advert == 0 && m.next_flood_advert == 0); assert(m.radio_timing.watchdogMillis(true) == 24 * HOUR * 1000); } { // At exactly 12 hours, expiry wins over the temporary watchdog deadline. MyMesh m; startTemp(m, 720); m.advance(12 * HOUR); m.servicePostMeshLoop(); assert(!m.radio_timing.isTemporary()); assert(m._cli.board.reboots == 0); m.advance(24 * HOUR); m.servicePostMeshLoop(); assert(m._cli.board.reboots == 1); } { // A longer session really does reboot after a 12-hour no-RX window. MyMesh m; startTemp(m, 1440); m.advance(12 * HOUR); m.servicePostMeshLoop(); assert(m._cli.board.reboots == 1); } { // Replacing an active session retains old policy until the new tuple applies. MyMesh m; startTemp(m, 1440); m.applyTempRadioParams(911, 250, 5, 5, 60); assert(m.radio_timing.tempDuration() == 24 * HOUR); m.advance(2); m.servicePostMeshLoop(); assert(m.radio_timing.tempDuration() == HOUR); assert(m.radio_timing.watchdogMillis(true) == 0); } { // Failed restoration must not leave a short session's watchdog disabled. MyMesh m; startTemp(m, 60); m.restore_success = false; m.advance(HOUR); m.servicePostMeshLoop(); assert(m.temp_radio_applied); // hardware restoration is still pending assert(!m.radio_timing.isTemporary()); assert(m.radio_timing.watchdogMillis(true) == 24 * HOUR * 1000); m.restore_success = true; m.advance(1); m.servicePostMeshLoop(); assert(!m.temp_radio_applied && m.saves == 0); } { // Manual cancellation restores saved timings without saving temp overrides. MyMesh m; startTemp(m, 1440); assert(m.scheduleNormalRadio()); assert(!m.radio_timing.isTemporary()); m.servicePostMeshLoop(); assert(!m.temp_radio_applied && m.saves == 0); assert(m.next_local_advert == now_ms + 2 * HOUR * 1000); assert(m.next_flood_advert == now_ms + 24 * HOUR * 1000); } { // Deleting an active scheduled entry has the same restoration behavior. MyMesh m; startTemp(m, 1440); m.clearScheduledRadioSetting(0, true); assert(!m.radio_timing.isTemporary()); m.servicePostMeshLoop(); assert(!m.temp_radio_applied && m.saves == 0); } { // Adjacent scheduled windows adopt the new duration at the boundary. MyMesh m; char reply[160]; uint32_t start = m.rtc.now + 2; m.addScheduledRadioParams(true, 910, 250, 5, 5, start, start + HOUR, reply); m.addScheduledRadioParams(true, 911, 250, 5, 5, start + HOUR, start + 25 * HOUR, reply); m.advance(2); m.servicePostMeshLoop(); assert(m.radio_timing.tempDuration() == HOUR); m.advance(HOUR); m.servicePostMeshLoop(); assert(m.radio_timing.tempDuration() == 24 * HOUR); assert(m._cli.board.reboots == 0 && m.saves == 0); } { // Raw driver receptions must not keep the whole-board watchdog alive. MyMesh m; m.servicePostMeshLoop(); m.advance(23 * HOUR); m.radio.last_rx = now_ms; m.servicePostMeshLoop(); m.advance(HOUR); m.radio.last_rx = now_ms; m.servicePostMeshLoop(); assert(m._cli.board.reboots == 1); } { // A parsed MeshCore reception moves the actual repeater reboot deadline. MyMesh m; m.servicePostMeshLoop(); m.advance(23 * HOUR); m.last_meshcore_rx = now_ms; m.servicePostMeshLoop(); m.advance(HOUR); m.servicePostMeshLoop(); assert(m._cli.board.reboots == 0); m.advance(23 * HOUR); m.servicePostMeshLoop(); assert(m._cli.board.reboots == 1); } puts("Production scheduler, storage-backoff, watchdog and advert scenarios passed"); } ''' CLI_HARNESS = r''' #include #include #include #include #include #include #include #include namespace mesh { struct RadioProfileCLI { static bool parseSuffix(const char* input, unsigned fields, char* legacy, size_t size, uint16_t& preamble) { return cli::parseRadioPreambleSuffix(input, fields, legacy, size, preamble); } bool acceptsPrimary(float, float, uint8_t, uint8_t, uint16_t) { return true; } void appendPrimaryChirpWarning(char*, size_t, uint8_t, float, uint16_t) const {} }; class Utils { public: static int parseTextParts(char*, const char*[], int, char separator=','); }; @PARTS@ } @PARSERS@ struct RTC { uint32_t getCurrentTime() const { return 1789160000UL; } } rtc; RTC* getRTCClock() { return &rtc; } struct Callbacks { int calls = 0; uint32_t duration = 0; void applyTempRadioParams(float, float, uint8_t, uint8_t, int minutes, uint16_t) { ++calls; duration = (uint32_t)minutes * 60; } void appendTempRadioTimingNote(char* reply, size_t size, uint32_t seconds) { mesh::RepeaterRadioTiming::appendTempWatchdogNote(reply, size, seconds); } } callbacks; void appendRxPowerSavingAdjustmentNote(char*, const void*, uint8_t, float) {} void handle(const char* command, char* reply) { mesh::RadioProfileCLI _radio_profiles; char tmp[160]; Callbacks* _callbacks = &callbacks; const void* _prefs = nullptr; if (false) { @BRANCH@ } } int main() { struct Reply { char text[160]; char guard = '!'; } reply; handle("tempradio 910,250,5,5,1440", reply.text); assert(!strcmp(reply.text, "OK - temp params for 1440 mins (1d0h0m); rx.watchdog=12hours")); assert(!strncmp(reply.text, "OK - temp params for ", 21)); // exact reply-barrier marker handle("tempradio 910,250,5,5,90", reply.text); assert(!strcmp(reply.text, "OK - temp params for 90 mins (0d1h30m); rx.watchdog=off (temp<12hours)")); assert(callbacks.calls == 2 && callbacks.duration == 5400); for (const char* minutes : {"0", "-1", "12oops", "1.5", "4294967295", "4294967296"}) { char command[100]; snprintf(command, sizeof(command), "tempradio 910,250,5,5,%s", minutes); handle(command, reply.text); assert(!strcmp(reply.text, "Error, invalid params")); assert(callbacks.calls == 2); } assert(reply.guard == '!'); puts("production TempRadio replies and duration validation passed"); } ''' class RepeaterRadioTimingIntegrationTest(unittest.TestCase): def compile_and_run(self, source): with tempfile.TemporaryDirectory(prefix="meshcore-temp-timing-") as temp: cpp = Path(temp) / "test.cpp" exe = Path(temp) / ("test.exe" if os.name == "nt" else "test") cpp.write_text(source, encoding="utf-8") subprocess.run([os.environ.get("CXX", "g++"), "-std=c++17", "-I", str(ROOT / "src"), str(cpp), "-o", str(exe)], check=True) subprocess.run([str(exe)], check=True) def test_production_scheduler_and_services(self): source = (ROOT / "examples/simple_repeater/MyMesh.cpp").read_text(encoding="utf-8") signatures = [ "void MyMesh::checkRxInactivityWatchdog()", "void MyMesh::setTempRadioTiming(", "void MyMesh::updateAdvertTimer()", "void MyMesh::updateFloodAdvertTimer()", "void MyMesh::queueSavedRadioApply()", "bool MyMesh::applySavedRadioParams()", "void MyMesh::refreshScheduledRadioState()", "void MyMesh::processScheduledRadioSettings()", "void MyMesh::applyTempRadioParams(", "bool MyMesh::scheduleNormalRadio()", "void MyMesh::clearScheduledRadioSetting(", "int MyMesh::findFreeScheduledRadioSlot() const", "int MyMesh::countScheduledRadioSettings(", "bool MyMesh::scheduledRadioConflicts(", "void MyMesh::addScheduledRadioParams(", ] methods = "\n".join(method(source, signature) for signature in signatures) post = method(source, "void __attribute__((noinline)) MyMesh::servicePostMeshLoop()") # The remaining service tail handles unrelated MQTT/OTA/peripheral work. post = post[:post.index("#if defined(WITH_MQTT_BRIDGE) && defined(OTA_MANIFEST_BASE)")] + "}\n" methods += "\n" + post for lr2021 in (False, True): with self.subTest(lr2021=lr2021): defines = "#define USE_LR2021 1\n" if lr2021 else "" self.compile_and_run(defines + HARNESS.replace("@METHODS@", methods)) def test_scheduled_radio_relative_and_absolute_times(self): source = (ROOT / "src/helpers/CommonCLI.cpp").read_text(encoding="utf-8") parsers = "\n".join(method(source, signature) for signature in [ "static bool looksUnsignedInteger(", "static const char* skipSpacesConst(", "static bool parseUint32Strict(", "static int countSeparatedParts(", "static bool parseScheduledRadioArgs(", ]) utils = (ROOT / "src/Utils.cpp").read_text(encoding="utf-8") parts = method(utils, "int Utils::parseTextParts(") self.compile_and_run(r''' #include #include #include #include #include struct StrHelper { static void strncpy(char* dst, const char* src, size_t n) { snprintf(dst, n, "%s", src); } }; namespace mesh { struct Utils { static int parseTextParts(char*, const char*[], int, char); }; @PARTS@ } @PARSERS@ int main() { const uint32_t now = 1700000000; float freq, bw; uint8_t sf, cr; uint32_t start, end; assert(parseScheduledRadioArgs("910.5,500,8,5,+5,+15", true, freq,bw,sf,cr,start,end,now)); assert(start == now + 300 && end == now + 900); assert(parseScheduledRadioArgs("910.5,500,8,5,1700000300,1700000900", true, freq,bw,sf,cr,start,end,now)); assert(start == now + 300 && end == now + 900); assert(parseScheduledRadioArgs("910.5,500,8,5,1700000300,+15", true, freq,bw,sf,cr,start,end,now)); assert(start == now + 300 && end == now + 900); assert(parseScheduledRadioArgs("910.5,500,8,5,+5", false, freq,bw,sf,cr,start,end,now)); assert(start == now + 300 && end == 0); for (const char* invalid : {"+0", "+", "++1", "+ 1", "-1", "+1.5", "+1x", "+4294967295", "4294967296"}) { start = 42; assert(!mesh::cli::parseRadioScheduleTime(invalid, now, start)); assert(start == 42); } assert(mesh::cli::parseRadioScheduleTime(" +1 ", now, start) && start == now + 60); assert(mesh::cli::parseRadioScheduleTime("+1", UINT32_MAX - 60, start) && start == UINT32_MAX); assert(!mesh::cli::parseRadioScheduleTime("+1", UINT32_MAX - 59, start)); } '''.replace("@PARTS@", parts).replace("@PARSERS@", parsers)) def test_production_cli_replies_and_duration_validation(self): source = (ROOT / "src/helpers/CommonCLI.cpp").read_text(encoding="utf-8") parsers = "\n".join(method(source, signature) for signature in [ "static bool looksUnsignedInteger(", "static const char* skipSpacesConst(", "static bool parseUint32Strict(", "static bool bwMatches(", "static bool isValidLoRaBandwidth(", ]) start = source.index(' } else if (memcmp(command, "tempradio ", 10) == 0)') end = source.index(' } else if (memcmp(command, "password ", 9) == 0)', start) utils = (ROOT / "src/Utils.cpp").read_text(encoding="utf-8") parts = method(utils, "int Utils::parseTextParts(") self.compile_and_run(CLI_HARNESS.replace("@PARSERS@", parsers) .replace("@BRANCH@", source[start:end]) .replace("@PARTS@", parts)) if __name__ == "__main__": unittest.main()