Files
MeshCore/test/test_emergency_sleep_backends.py
T

348 lines
16 KiB
Python

"""Execute resumable emergency CPU sleeps with native RTOS/SDK boundaries."""
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
SANITIZERS = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
"-fno-pie", "-no-pie"] if sys.platform.startswith("linux") else [])
HARNESS = r'''
#include <cassert>
#include <cstdint>
#include <limits>
#include <string>
#include <vector>
static std::vector<std::string> events;
static bool radio_test = false, interrupt_context = false, wireless_ready = true;
static bool usb_held = false;
static bool attach_on_delay = false;
static unsigned primask = 0, basepri = 0, faultmask = 0;
static unsigned __get_IPSR() { return interrupt_context; }
static unsigned __get_PRIMASK() { return primask; }
static unsigned __get_BASEPRI() { return basepri; }
static unsigned __get_FAULTMASK() { return faultmask; }
static bool xPortInIsrContext() { return interrupt_context; }
static constexpr int taskSCHEDULER_RUNNING = 2;
static int scheduler = taskSCHEDULER_RUNNING;
static int xTaskGetSchedulerState() { return scheduler; }
static struct { unsigned __cr_flag = 0; } nrf_nvic_state;
using TickType_t = uint32_t;
static uint32_t tick_ms = 1, delayed_ticks = 0;
#define pdMS_TO_TICKS(ms) ((ms) / tick_ms)
static void vTaskDelay(TickType_t ticks) {
assert(ticks != 0);
delayed_ticks = ticks;
events.push_back("delay");
if (attach_on_delay) usb_held = true; // Model the USB SOF hook observing a new host.
}
static unsigned nrf_feeds = 0;
static constexpr uint32_t WDT_RR_RR_Reload = 0x6e524635;
struct ReloadRegister {
uint32_t value = 0;
void operator=(uint32_t reload) {
assert(reload == WDT_RR_RR_Reload);
value = reload; ++nrf_feeds; events.push_back("nrf_feed");
}
};
static struct {
uint32_t RUNSTATUS = 0, CRV = 60 * 32768UL, RREN = 1;
ReloadRegister RR[8]{};
} nrf_watchdog;
#define NRF_WDT (&nrf_watchdog)
@NRF_RELOAD@
using esp_err_t = int;
static constexpr int ESP_OK = 0, ESP_ERR_NOT_FOUND = 1, ESP_ERR_INVALID_STATE = 2;
static constexpr int ESP_SLEEP_WAKEUP_TIMER = 3;
static esp_err_t task_watchdog_status = ESP_ERR_INVALID_STATE;
static unsigned task_watchdog_feeds = 0, fail_watchdog_feed = 0;
static esp_err_t esp_task_wdt_status(void*) { return task_watchdog_status; }
static esp_err_t esp_task_wdt_reset() {
events.push_back("task_feed");
return ++task_watchdog_feeds == fail_watchdog_feed ? 9 : ESP_OK;
}
static uint64_t timer_us = 0, last_armed_us = 0;
static unsigned timer_calls = 0, sleep_calls = 0, clear_calls = 0;
static uint32_t other_wake_sources = 0x71;
static int timer_error = 0, sleep_error = 0, clear_error = 0;
static esp_err_t esp_sleep_enable_timer_wakeup(uint64_t value) {
events.push_back("timer"); ++timer_calls;
if (timer_error) return timer_error;
timer_us = last_armed_us = value;
return ESP_OK;
}
static esp_err_t esp_light_sleep_start() {
events.push_back("sleep"); ++sleep_calls;
return sleep_error;
}
static esp_err_t esp_sleep_disable_wakeup_source(int source) {
assert(source == ESP_SLEEP_WAKEUP_TIMER); // Never disable GPIO/ALL sources.
events.push_back("clear_timer"); ++clear_calls;
if (clear_error) return clear_error;
timer_us = 0;
return ESP_OK;
}
namespace mesh { namespace esp32 {
static bool manualLightSleepReady() { return wireless_ready; }
} }
struct Base {
@DEFAULT_SUPPORT@
@DEFAULT_SLEEP@
};
struct NRF52Board : Base {
bool watchdog_disable_reset_pending = false;
bool isRadioTestActive() const { return radio_test; }
@NRF_SUPPORT@
bool emergencySleepMilliseconds(uint32_t millisecs) override;
};
struct ESP32Board : Base {
bool isRadioTestActive() const { return radio_test; }
bool isUsbSleepHeld() const { return usb_held; }
@ESP_SUPPORT@
bool emergencySleepMilliseconds(uint32_t millisecs) override;
};
@NRF_SLEEP@
@ESP_SLEEP@
static void reset() {
events.clear(); radio_test = interrupt_context = false; wireless_ready = true;
usb_held = false;
attach_on_delay = false;
primask = basepri = faultmask = 0; nrf_nvic_state.__cr_flag = 0;
scheduler = taskSCHEDULER_RUNNING;
tick_ms = 1; delayed_ticks = nrf_feeds = 0;
nrf_watchdog = {0, 60 * 32768UL};
task_watchdog_status = ESP_ERR_INVALID_STATE;
task_watchdog_feeds = fail_watchdog_feed = 0;
timer_us = last_armed_us = 0; timer_calls = sleep_calls = clear_calls = 0;
timer_error = sleep_error = clear_error = 0; other_wake_sources = 0x71;
}
int main() {
Base unsupported;
assert(!unsupported.supportsEmergencySleep());
assert(!unsupported.emergencySleepMilliseconds(1000));
NRF52Board nrf; ESP32Board esp;
Base* supported[] = {&nrf, &esp};
for (Base* board : supported) assert(board->supportsEmergencySleep());
for (Base* board : supported) {
reset(); assert(!board->emergencySleepMilliseconds(0)); assert(events.empty());
radio_test = true;
assert(!board->emergencySleepMilliseconds(100)); assert(events.empty());
reset(); interrupt_context = true;
assert(!board->emergencySleepMilliseconds(100)); assert(events.empty());
reset(); scheduler = 0;
assert(!board->emergencySleepMilliseconds(100)); assert(events.empty());
}
for (unsigned* masked : {&primask, &basepri, &faultmask, &nrf_nvic_state.__cr_flag}) {
reset(); *masked = 1;
assert(!nrf.emergencySleepMilliseconds(100)); assert(events.empty());
}
reset(); nrf.watchdog_disable_reset_pending = true;
assert(!nrf.emergencySleepMilliseconds(100)); assert(events.empty());
nrf.watchdog_disable_reset_pending = false;
reset(); tick_ms = 100;
assert(!nrf.emergencySleepMilliseconds(1)); assert(events.empty());
reset(); assert(nrf.emergencySleepMilliseconds(123));
assert(delayed_ticks == 123 && !nrf_watchdog.RUNSTATUS && !nrf_feeds);
assert(events == std::vector<std::string>{"delay"});
reset(); assert(nrf.emergencySleepMilliseconds(UINT32_MAX));
assert(delayed_ticks == 10000); // Bound the maintenance absence.
reset(); nrf_watchdog.RUNSTATUS = 1;
assert(nrf.emergencySleepMilliseconds(10000));
assert(delayed_ticks == 10000 && nrf_feeds == 2);
assert((events == std::vector<std::string>{"nrf_feed", "delay", "nrf_feed"}));
reset(); nrf_watchdog.RUNSTATUS = 1; nrf_watchdog.RREN = 0x82;
assert(nrf.emergencySleepMilliseconds(100)); assert(nrf_feeds == 4);
assert(nrf_watchdog.RR[1].value == WDT_RR_RR_Reload);
assert(nrf_watchdog.RR[7].value == WDT_RR_RR_Reload);
assert(nrf_watchdog.RR[0].value == 0 && nrf_watchdog.RR[6].value == 0);
reset(); nrf_watchdog.RUNSTATUS = 1; nrf_watchdog.CRV = 2 * 32768;
assert(nrf.emergencySleepMilliseconds(10000));
assert(delayed_ticks == 1000 && nrf_feeds == 2); // Short inherited watchdog.
reset(); nrf_watchdog.RUNSTATUS = 1; nrf_watchdog.CRV = 32767;
assert(!nrf.emergencySleepMilliseconds(10000)); assert(events.empty());
reset(); wireless_ready = false;
assert(!esp.emergencySleepMilliseconds(100)); assert(events.empty());
reset(); usb_held = true; wireless_ready = false; timer_us = 555;
assert(esp.emergencySleepMilliseconds(UINT32_MAX));
assert(delayed_ticks == 100 && events == std::vector<std::string>{"delay"});
assert(!timer_calls && !sleep_calls && !clear_calls && timer_us == 555);
assert(other_wake_sources == 0x71); // USB, RF and existing wake ownership are untouched.
usb_held = false; events.clear();
assert(!esp.emergencySleepMilliseconds(100) && events.empty()); // Teardown is still live.
wireless_ready = true;
assert(esp.emergencySleepMilliseconds(100)); // Recheck the host on every call; detach permits light sleep.
assert(last_armed_us == 100000 && sleep_calls == 1 && !timer_us);
reset(); usb_held = true; task_watchdog_status = ESP_OK;
assert(esp.emergencySleepMilliseconds(250));
assert(delayed_ticks == 100 && task_watchdog_feeds == 2);
assert((events == std::vector<std::string>{"task_feed", "delay", "task_feed"}));
assert(!timer_calls && !sleep_calls && !clear_calls);
reset(); usb_held = true; tick_ms = 1000;
assert(!esp.emergencySleepMilliseconds(100) && events.empty());
for (unsigned failure : {1U, 2U}) {
reset(); usb_held = true; task_watchdog_status = ESP_OK; fail_watchdog_feed = failure;
assert(!esp.emergencySleepMilliseconds(100));
assert(delayed_ticks == (failure == 1 ? 0U : 100U));
assert(!timer_calls && !sleep_calls && !clear_calls);
}
reset(); task_watchdog_status = 99;
assert(!esp.emergencySleepMilliseconds(100)); assert(events.empty());
reset(); assert(esp.emergencySleepMilliseconds(UINT32_MAX));
assert(sleep_calls == 1 && timer_calls == 1 && clear_calls == 1);
assert(last_armed_us == 10000000ULL);
assert(!timer_us && !task_watchdog_feeds && other_wake_sources == 0x71);
assert((events == std::vector<std::string>{"timer", "sleep", "clear_timer"}));
reset(); task_watchdog_status = ESP_ERR_NOT_FOUND;
assert(esp.emergencySleepMilliseconds(250)); assert(!task_watchdog_feeds);
reset(); task_watchdog_status = ESP_OK;
assert(esp.emergencySleepMilliseconds(10000));
assert(last_armed_us == 2000000ULL);
assert(task_watchdog_feeds == 2 && other_wake_sources == 0x71);
assert((events == std::vector<std::string>{"task_feed", "timer", "sleep", "clear_timer", "task_feed"}));
for (int* failed : {&sleep_error, &clear_error}) {
reset(); *failed = 9;
assert(!esp.emergencySleepMilliseconds(100));
assert(sleep_calls == 1 && clear_calls == 1 && other_wake_sources == 0x71);
}
reset(); timer_us = 555; timer_error = 9;
assert(!esp.emergencySleepMilliseconds(100));
assert(timer_us == 555 && sleep_calls == 0 && clear_calls == 0);
reset(); task_watchdog_status = ESP_OK; fail_watchdog_feed = 1;
assert(!esp.emergencySleepMilliseconds(100));
assert(timer_calls == 0 && sleep_calls == 0 && clear_calls == 0);
reset(); task_watchdog_status = ESP_OK; fail_watchdog_feed = 2;
assert(!esp.emergencySleepMilliseconds(100)); assert(clear_calls == 1 && !timer_us);
}
'''
class EmergencySleepBackendTests(unittest.TestCase):
def platform_code(self):
sources = {name: (ROOT / path).read_text() for name, path in (
("core", "src/MeshCore.h"),
("nrf", "src/helpers/NRF52Board.cpp"),
("nrf_header", "src/helpers/NRF52Board.h"),
("esp", "src/helpers/ESP32Board.cpp"),
("esp_header", "src/helpers/ESP32Board.h"))}
code = HARNESS
for marker, source, signature in (
("DEFAULT_SUPPORT", "core", "virtual bool supportsEmergencySleep() const"),
("DEFAULT_SLEEP", "core", "virtual bool emergencySleepMilliseconds("),
("NRF_RELOAD", "nrf", "static void reloadWatchdogChannels("),
("NRF_SUPPORT", "nrf_header", "bool supportsEmergencySleep() const override"),
("ESP_SUPPORT", "esp_header", "bool supportsEmergencySleep() const override"),
("NRF_SLEEP", "nrf", "bool NRF52Board::emergencySleepMilliseconds("),
("ESP_SLEEP", "esp", "bool ESP32Board::emergencySleepMilliseconds(")):
code = code.replace("@" + marker + "@", extract_braced(sources[source], signature))
return code
def run_code(self, code, flags=()):
compiler = shutil.which("g++") or shutil.which("clang++")
self.assertIsNotNone(compiler)
with tempfile.TemporaryDirectory(prefix="emergency-sleep-backends-") as directory:
work = Path(directory)
cpp, binary = work / "test.cpp", work / "test"
cpp.write_text(code)
subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-DCONFIG_ESP_TASK_WDT_TIMEOUT_S=4", *flags, *SANITIZERS,
str(cpp), "-o", str(binary)], check=True,
capture_output=True, text=True, timeout=60)
subprocess.run([str(binary)], check=True, capture_output=True,
text=True, timeout=20)
def test_actual_platform_waits_and_cleanup(self):
self.run_code(self.platform_code())
def test_native_usb_timer_wake_reserves_enumeration_service_without_extending_budget(self):
code = self.platform_code().split("int main() {", 1)[0] + r'''
int main() {
ESP32Board esp;
reset(); assert(esp.emergencySleepMilliseconds(10000));
assert(last_armed_us == 9900000ULL && delayed_ticks == 100);
assert((events == std::vector<std::string>{"timer", "sleep", "clear_timer", "delay"}));
assert(timer_us == 0 && other_wake_sources == 0x71);
reset(); task_watchdog_status = ESP_OK;
assert(esp.emergencySleepMilliseconds(10000));
assert(last_armed_us == 1900000ULL && delayed_ticks == 100 && task_watchdog_feeds == 2);
assert((events == std::vector<std::string>{"task_feed", "timer", "sleep", "clear_timer", "delay", "task_feed"}));
reset(); assert(esp.emergencySleepMilliseconds(125));
assert(last_armed_us == 25000 && delayed_ticks == 100);
reset(); assert(esp.emergencySleepMilliseconds(30));
assert(delayed_ticks == 30 && events == std::vector<std::string>{"delay"});
assert(!timer_calls && !sleep_calls && !clear_calls);
reset(); tick_ms = 10;
assert(esp.emergencySleepMilliseconds(129));
assert(last_armed_us == 29000 && delayed_ticks == 10); // Timer + floored RTOS wait <=129ms.
reset(); tick_ms = 1000;
assert(!esp.emergencySleepMilliseconds(10000) && events.empty());
for (int* failed : {&sleep_error, &clear_error}) {
reset(); *failed = 9;
assert(!esp.emergencySleepMilliseconds(10000));
assert(!delayed_ticks && clear_calls == 1);
assert((events == std::vector<std::string>{"timer", "sleep", "clear_timer"}));
}
reset(); timer_error = 9;
assert(!esp.emergencySleepMilliseconds(10000));
assert(!delayed_ticks && !sleep_calls && !clear_calls);
reset(); usb_held = true; wireless_ready = false;
assert(esp.emergencySleepMilliseconds(10000));
assert(delayed_ticks == 100 && !sleep_calls); // Existing host still bypasses physical USB sleep.
reset(); attach_on_delay = true;
assert(esp.emergencySleepMilliseconds(10000) && usb_held);
assert(sleep_calls == 1 && clear_calls == 1);
events.clear();
assert(esp.emergencySleepMilliseconds(10000));
assert(events == std::vector<std::string>{"delay"} && sleep_calls == 1);
}
'''
self.run_code(code, ("-DMESH_ESP32_USB_CONSOLE_COOPERATIVE=1",))
def test_disabled_feature_has_no_new_base_virtuals(self):
compiler = shutil.which("g++") or shutil.which("clang++")
self.assertIsNotNone(compiler)
core = (ROOT / "src/MeshCore.h").read_text()
start = core.index("#if MESH_ENABLE_EMERGENCY_POWER\n", core.index("class MainBoard"))
block = core[start:core.index("#endif", start) + len("#endif")]
code = r'''
#include <cassert>
#include <cstdint>
#include <type_traits>
#include <utility>
struct Board {
@API@
};
template<typename T, typename = void> struct HasApi : std::false_type {};
template<typename T> struct HasApi<T, std::void_t<decltype(
std::declval<T>().supportsEmergencySleep())>> : std::true_type {};
static_assert(HasApi<Board>::value == bool(MESH_ENABLE_EMERGENCY_POWER));
int main() {
#if MESH_ENABLE_EMERGENCY_POWER
Board b; assert(!b.supportsEmergencySleep() && !b.emergencySleepMilliseconds(1));
#endif
}
'''.replace("@API@", block)
with tempfile.TemporaryDirectory(prefix="emergency-sleep-feature-") as directory:
work = Path(directory)
cpp, binary = work / "test.cpp", work / "test"
cpp.write_text(code)
for enabled in (0, 1):
subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
f"-DMESH_ENABLE_EMERGENCY_POWER={enabled}", str(cpp),
"-o", str(binary)], check=True,
capture_output=True, text=True, timeout=60)
subprocess.run([str(binary)], check=True, capture_output=True,
text=True, timeout=20)
if __name__ == "__main__":
unittest.main()