Files
MeshCore/test/test_nrf52_sleep_cycle.py
T

293 lines
13 KiB
Python

#!/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 <cassert>
#include <thread>
#include <helpers/nrf52/SleepCycleState.h>
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<bool> 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 <cassert>
#include <initializer_list>
#include <helpers/PowerSavingDiagnostics.h>
#include <helpers/nrf52/SleepCycleState.h>
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()