#!/usr/bin/env python3 """Execute the one-cycle SDK snapshot and its actual nRF52 board consumers.""" import os from pathlib import Path import subprocess import tempfile import unittest from test_replay_reset_integration import extract_braced ROOT = Path(__file__).resolve().parents[1] class Nrf52SleepCycleTests(unittest.TestCase): def execute(self, code): with tempfile.TemporaryDirectory(prefix="meshcore-sleep-cycle-") as directory: source = Path(directory) / "test.cpp" binary = Path(directory) / "test" source.write_text(code, encoding="ascii") result = subprocess.run([ os.environ.get("CXX", "c++"), "-std=c++17", "-Wall", "-Wextra", "-pthread", "-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie", "-I", str(ROOT / "src"), str(source), "-o", str(binary), ], capture_output=True, text=True, timeout=60) self.assertEqual(result.returncode, 0, result.stdout + result.stderr) result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=30) self.assertEqual(result.returncode, 0, result.stdout + result.stderr) def test_cache_one_shot_error_invalidation_and_concurrent_publication(self): self.execute(r''' #include #include #include int main() { using namespace mesh_nrf52; SleepCycleState state; SleepCycleSnapshot out; assert(!state.snapshot(out) && !state.take(out) && state.lastError() == 0); auto token = state.begin(); assert(!state.snapshot(out)); assert(state.publish(token, false, 3)); assert(state.snapshot(out) && !out.softdevice_enabled && out.usb_status == 3); assert(state.take(out) && !state.take(out) && !state.snapshot(out)); token = state.begin(); state.invalidate(); // Startup began during an authoritative SDK query. assert(!state.publish(token, false, 1) && !state.snapshot(out)); token = state.begin(); assert(state.publish(token, true, 1, 17)); assert(!state.take(out) && state.lastError() == 17); state.invalidate(); assert(state.lastError() == 0); // Do not attribute the old error to a new transition. token = state.begin(); assert(state.publish(token, true, 0) && state.snapshot(out)); const auto previous = out; token = state.begin(); assert(!state.current(previous.generation) && !state.snapshot(out)); assert(state.publish(token, false, 1) && state.snapshot(out)); assert(!out.softdevice_enabled && out.usb_status == 1); std::atomic done{false}; std::thread writer([&]() { for (unsigned i = 0; i < 100000; ++i) { const auto next = state.begin(); state.publish(next, (next & 1) != 0, next ^ 0x75201346U); } done.store(true, std::memory_order_release); }); // Seed a consistent record before accepting the publication stress cases. state.invalidate(); while (!done.load(std::memory_order_acquire)) { if (state.snapshot(out)) { assert(out.usb_status == (out.generation ^ 0x75201346U)); assert(out.softdevice_enabled == ((out.generation & 1) != 0)); } } writer.join(); } ''') def test_actual_prepare_power_cache_sleep_and_partial_startup_fail_closed(self): source = (ROOT / "src/helpers/NRF52Board.cpp").read_text(encoding="ascii") methods = "\n".join(extract_braced(source, signature) for signature in ( "void NRF52Board::prepareSleepCycle()", "bool NRF52Board::isExternalPowered()", "void NRF52Board::sleep(uint32_t secs)", )) code = r''' #include #include #include #include constexpr uint32_t NRF_SUCCESS = 0, NRF_ERROR_INVALID_STATE = 8; constexpr uint32_t POWER_USBREGSTATUS_VBUSDETECT_Msk = 1; constexpr uint32_t SCB_ICSR_PENDSVSET_Msk = 1UL << 28; constexpr int taskSCHEDULER_RUNNING = 2; #define NRF_CRYPTOCELL 1 constexpr int CRYPTOCELL_IRQn = 42; struct NvicState { volatile uint32_t __cr_flag = 0; } nrf_nvic_state; static bool cc_enabled = false, cc_pending = false; static unsigned cc_clears = 0; static uint32_t NVIC_GetEnableIRQ(int irq) { assert(irq == CRYPTOCELL_IRQn); return cc_enabled; } static void NVIC_ClearPendingIRQ(int irq) { assert(irq == CRYPTOCELL_IRQn); cc_pending = false; ++cc_clears; } struct Scb { uint32_t ICSR = 0; } scb; static Scb* SCB = &scb; struct Power { uint32_t USBREGSTATUS = 0; } power; static Power* NRF_POWER = &power; static bool actual_sd = false, invalidate_during_query = false, event = false; static uint32_t actual_usb = 0, sd_error = 0, usb_error = 0; static unsigned state_queries = 0, usb_queries = 0, sd_waits = 0, bare_waits = 0, delays = 0; static unsigned acknowledgements = 0, forced_yields = 0; static uint32_t ipsr = 0, primask = 0, basepri = 0, faultmask = 0; static int scheduler_state = taskSCHEDULER_RUNNING; static bool yielded_app_irq = false, defer_pendsv = false, invalidate_during_yield = false; static bool critical_during_yield = false; static uint32_t acknowledgement_error = 0; static uint32_t __get_IPSR() { return ipsr; } static uint32_t __get_PRIMASK() { return primask; } static uint32_t __get_BASEPRI() { return basepri; } static uint32_t __get_FAULTMASK() { return faultmask; } static int xTaskGetSchedulerState() { return scheduler_state; } static void taskYIELD() { ++forced_yields; event = true; if (defer_pendsv) scb.ICSR |= SCB_ICSR_PENDSVSET_Msk; else yielded_app_irq = true; if (invalidate_during_yield) mesh_nrf52::sleepCycleState().invalidate(); if (critical_during_yield) nrf_nvic_state.__cr_flag = 1; } static uint32_t now_us = 1000; static uint32_t millis() { return now_us / 1000; } static uint32_t micros() { return now_us; } static void delay(unsigned ms) { delays += ms; } namespace mesh_nrf52 { uint32_t softdeviceIsEnabled(uint8_t& out) { ++state_queries; out = actual_sd; event = true; // Model the SVC-caused event. if (invalidate_during_query) sleepCycleState().invalidate(); return sd_error; }} static uint32_t sd_power_usbregstatus_get(uint32_t* out) { ++usb_queries; *out = actual_usb; event = true; return usb_error; } static uint32_t sd_app_evt_wait() { if (yielded_app_irq) { yielded_app_irq = false; ++acknowledgements; return acknowledgement_error; } ++sd_waits; now_us += 300; return 0; } static void __SEV() { event = true; } static void __WFE() { if (event) event = false; else { ++bare_waits; now_us += 500; } } struct NRF52Board { bool radio_test = false; bool isRadioTestActive() const { return radio_test; } void prepareSleepCycle(); bool isExternalPowered(); void sleep(uint32_t secs); }; @METHODS@ int main() { using namespace mesh::powerdiag; NRF52Board board; board.sleep(0); assert(state_queries == 0 && sd_waits == 0 && bare_waits == 0); power.USBREGSTATUS = 1; board.prepareSleepCycle(); assert(state_queries == 1 && usb_queries == 0 && !event); assert(forced_yields == 0 && acknowledgements == 0); for (int i = 0; i < 20; ++i) assert(board.isExternalPowered()); assert(state_queries == 1 && !event); // UI power polling must not create late SVC events. power.USBREGSTATUS = 0; assert(board.isExternalPowered()); // Stable only within this prepared cycle. board.prepareSleepCycle(); assert(!board.isExternalPowered() && state_queries == 2); board.sleep(0); assert(bare_waits == 1 && sd_waits == 0 && state_queries == 2); board.sleep(0); assert(bare_waits == 1 && state_queries == 2); // One-shot, not a global SD cache. // Startup can partially enable SD before failing. Invalidation is BEFORE // the attempt, not conditional on a successful Bluefruit return value. board.prepareSleepCycle(); mesh_nrf52::sleepCycleState().invalidate(); actual_sd = true; actual_usb = 1; assert(board.isExternalPowered()); // Stale SD-off cycle cannot bypass the SDK. assert(usb_queries == 1); const auto before = state_queries; board.sleep(0); assert(state_queries == before && sd_waits == 0); power.USBREGSTATUS = 0; board.prepareSleepCycle(); assert(!event && board.isExternalPowered() && usb_queries == 2); assert(forced_yields == 1 && acknowledgements == 1); board.sleep(0); assert(sd_waits == 1 && bare_waits == 1); setEnabled(true, millis()); reset(millis()); sd_error = 17; board.prepareSleepCycle(); board.sleep(0); auto value = snapshot(millis()); assert(value.errors == 1 && value.detail == 17 && value.calls == 0); assert(sd_waits == 1 && bare_waits == 1); sd_error = 0; usb_error = 23; reset(millis()); board.prepareSleepCycle(); board.sleep(0); value = snapshot(millis()); assert(value.errors == 1 && value.detail == 23 && value.calls == 0); usb_error = 0; invalidate_during_query = true; reset(millis()); board.prepareSleepCycle(); board.sleep(0); value = snapshot(millis()); assert(value.board_vetoes == 1 && value.detail == 3 && value.calls == 0); invalidate_during_query = false; for (uint32_t* mask : {&ipsr, &primask, &basepri, &faultmask}) { *mask = 1; reset(millis()); const auto before_query = state_queries, before_ack = acknowledgements; board.prepareSleepCycle(); board.sleep(0); value = snapshot(millis()); assert(value.errors == 1 && value.detail == NRF_ERROR_INVALID_STATE); assert(state_queries == before_query && acknowledgements == before_ack); *mask = 0; } scheduler_state = 0; reset(millis()); board.prepareSleepCycle(); board.sleep(0); assert(snapshot(millis()).detail == NRF_ERROR_INVALID_STATE); scheduler_state = taskSCHEDULER_RUNNING; defer_pendsv = true; reset(millis()); const auto before_ack = acknowledgements; board.prepareSleepCycle(); board.sleep(0); assert(acknowledgements == before_ack && snapshot(millis()).detail == NRF_ERROR_INVALID_STATE); defer_pendsv = false; scb.ICSR = 0; acknowledgement_error = 31; reset(millis()); board.prepareSleepCycle(); board.sleep(0); assert(snapshot(millis()).errors == 1 && snapshot(millis()).detail == 31); acknowledgement_error = 0; invalidate_during_yield = true; reset(millis()); const auto before_invalid_ack = acknowledgements; board.prepareSleepCycle(); board.sleep(0); assert(acknowledgements == before_invalid_ack && snapshot(millis()).detail == 3); invalidate_during_yield = false; yielded_app_irq = false; board.prepareSleepCycle(); board.radio_test = true; board.sleep(0); board.radio_test = false; board.sleep(0); assert(sd_waits == 1); // A veto consumed the prior preparation too. // Only the disabled polling CryptoCell IRQ may be cleaned, and only at // entry. An enabled owner and unrelated NVIC sources are never touched. cc_pending = true; const auto before_cc_clear = cc_clears; board.prepareSleepCycle(); assert(!cc_pending && cc_clears == before_cc_clear + 1); cc_enabled = true; cc_pending = true; board.prepareSleepCycle(); assert(cc_pending && cc_clears == before_cc_clear + 1); cc_enabled = false; // A public SDK critical region can virtually mask an enabled owner even // with all CPU mask registers clear. Refuse preparation before cleanup. nrf_nvic_state.__cr_flag = 1; const auto before_critical_query = state_queries; board.prepareSleepCycle(); assert(state_queries == before_critical_query && cc_pending); assert(cc_clears == before_cc_clear + 1); assert(mesh_nrf52::sleepCycleState().lastError() == NRF_ERROR_INVALID_STATE); nrf_nvic_state.__cr_flag = 0; // Recheck the volatile SDK critical flag after taskYIELD, not just at entry. critical_during_yield = true; const auto before_critical_ack = acknowledgements; board.prepareSleepCycle(); assert(acknowledgements == before_critical_ack); assert(mesh_nrf52::sleepCycleState().lastError() == NRF_ERROR_INVALID_STATE); } '''.replace("@METHODS@", methods) self.execute(code) def test_every_application_bluefruit_attempt_invalidates_before_call(self): for filename, call in ( ("src/helpers/NRF52Board.cpp", "if (!Bluefruit.begin(1, 0))"), ("src/helpers/nrf52/SerialBLEInterface.cpp", "if (!Bluefruit.begin())"), ): with self.subTest(filename=filename): source = (ROOT / filename).read_text(encoding="ascii") at = source.index(call) self.assertTrue(source[:at].rstrip().endswith( "mesh_nrf52::sleepCycleState().invalidate();")) board = (ROOT / "src/helpers/NRF52Board.cpp").read_text(encoding="ascii") late = extract_braced(board, "void NRF52Board::sleep(uint32_t secs)") self.assertNotIn("softdeviceIsEnabled", late) self.assertNotIn("sd_power_usbregstatus_get", late) self.assertNotIn("NVIC->", late) if __name__ == "__main__": unittest.main()