Files
MeshCore/test/test_power_saving_diagnostics_buckets.py
T

181 lines
8.1 KiB
Python

#!/usr/bin/env python3
"""Exercise real SDK-entry time buckets, protocol bounds and legacy layout."""
from pathlib import Path
import os
import shlex
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
SOURCE = r'''
#include <helpers/PowerSavingDiagnostics.h>
#include <assert.h>
#include <stddef.h>
#include <string.h>
using namespace mesh::powerdiag;
struct LegacySnapshot {
uint32_t blocked_ms[ReasonCount] = {};
uint16_t transitions[ReasonCount] = {};
Mask current = 0, seen = 0;
uint32_t union_blocked_ms = 0, eligible_ms = 0, last_ms = 0, decision_at_ms = 0;
uint32_t calls = 0, returns = 0, short_returns = 0, board_vetoes = 0, errors = 0;
uint32_t call_us = 0, max_call_us = 0, last_call_us = 0, call_begin_us = 0;
int32_t detail = 0;
bool enabled = false, observed = false, pending = false;
};
#if defined(ESP32_PLATFORM)
static void call(uint32_t elapsed_ms) {
sleepBegin(123, state().bucket_start_ms + elapsed_ms);
sleepEnd(1123);
}
static uint32_t sum() {
uint32_t total = state().bucket_outside_calls;
for (size_t i = 0; i < SdkBucketCount; ++i) total += state().sdk_call_bins[i];
return total;
}
#endif
int main(int argc, char** argv) {
assert(argc == 2);
const char* test = argv[1];
#if defined(ESP32_PLATFORM)
static_assert(sizeof(Snapshot) <= 256, "ESP32 diagnostics RAM budget");
static_assert(sizeof(Snapshot) - sizeof(LegacySnapshot) <= 40, "Bucket storage grew");
if (!strcmp(test, "boundaries")) {
setEnabled(true, 1000);
call(0); call(15999); call(16000); call(255999); call(256000);
assert(state().sdk_call_bins[0] == 2 && state().sdk_call_bins[1] == 1);
assert(state().sdk_call_bins[15] == 1 && state().bucket_outside_calls == 1);
assert(state().bucket_flags == SdkBucketOutside && state().calls == 5);
assert(sum() == state().calls && state().returns == state().calls);
// An earlier timestamp becomes explicitly outside; it is never clamped.
sleepBegin(200, 999); sleepEnd(1200);
assert(state().bucket_outside_calls == 2 && state().sdk_call_bins[0] == 2);
} else if (!strcmp(test, "reset_freeze")) {
sleepBegin(12, 12); assert(state().calls == 0 && state().bucket_flags == 0);
setEnabled(true, 1000); call(1);
setEnabled(true, 3000); assert(state().bucket_start_ms == 1000);
setEnabled(false, 4000);
sleepBegin(5000, 9000); sleepEnd(6000);
assert(state().calls == 1 && state().sdk_call_bins[0] == 1);
char text[160]; formatPage(9, text, sizeof(text), 1000000);
assert(state().calls == 1 && state().bucket_start_ms == 1000);
setEnabled(true, 10000);
assert(state().calls == 0 && sum() == 0 && state().bucket_start_ms == 10000);
call(17000); reset(40000);
assert(enabled() && state().calls == 0 && sum() == 0 && state().bucket_start_ms == 40000);
setEnabled(false, 41000); reset(50000);
assert(!enabled() && state().bucket_start_ms == 50000 && sum() == 0);
} else if (!strcmp(test, "clock_wrap")) {
const uint32_t start = UINT32_MAX - 1000;
setEnabled(true, start);
sleepBegin(UINT32_MAX - 31, uint32_t(start + 16000));
sleepEnd(50);
assert(state().sdk_call_bins[1] == 1 && state().last_call_us == 82);
sleepBegin(0, 500); sleepEnd(1000);
assert(state().sdk_call_bins[0] == 1 && state().bucket_flags == 0);
assert(state().bucket_start_ms == start && sum() == state().calls);
} else if (!strcmp(test, "saturation_missing")) {
setEnabled(true, 0);
state().calls = UINT16_MAX - 1; state().sdk_call_bins[0] = UINT16_MAX - 1;
call(1); assert(state().sdk_call_bins[0] == UINT16_MAX && state().bucket_flags == 0);
call(2); assert(state().sdk_call_bins[0] == UINT16_MAX && state().calls == 65536);
assert(state().bucket_flags == SdkBucketSaturated);
reset(1000); state().calls = UINT32_MAX; state().bucket_outside_calls = UINT32_MAX;
call(256000);
assert(state().bucket_outside_calls == UINT32_MAX && state().calls == UINT32_MAX);
assert(state().bucket_flags == (SdkBucketOutside | SdkBucketSaturated));
reset(2000); sleepBegin(30); sleepEnd(1030);
assert(state().calls == 1 && sum() == 0 && state().bucket_flags == SdkBucketMissingClock);
} else if (!strcmp(test, "format_capacity")) {
setEnabled(true, UINT32_MAX);
state().bucket_outside_calls = UINT32_MAX; state().bucket_flags = 255;
for (size_t i = 0; i < SdkBucketCount; ++i) state().sdk_call_bins[i] = UINT16_MAX;
for (unsigned page = 9; page <= 10; ++page) {
char full[160]; memset(full, 'x', sizeof(full));
formatPage(page, full, 157, UINT32_MAX);
assert(strlen(full) < 157 && strstr(full, page == 9 ? "bins0=" : "bins8="));
assert(strstr(full, "outside=4294967295 flags=255"));
for (size_t capacity = 0; capacity <= 157; ++capacity) {
char guarded[162]; memset(guarded, 'q', sizeof(guarded));
formatPage(page, guarded + 1, capacity, UINT32_MAX);
assert(guarded[0] == 'q' && guarded[capacity + 1] == 'q');
if (capacity) assert(memchr(guarded + 1, 0, capacity));
}
char command[40]; snprintf(command, sizeof(command), "get powersaving.diag %u", page);
char reply[160]; assert(handleCommand(command, reply, 157, UINT32_MAX));
assert(!strncmp(reply, "> bucket_ms=16000 start_ms=4294967295", 34));
}
char reply[160];
assert(handleCommand("get powersaving.diag 09", reply, sizeof(reply), 0));
assert(!strncmp(reply, "Error:", 6));
assert(handleCommand("get powersaving.diag 10junk", reply, sizeof(reply), 0));
assert(!strncmp(reply, "Error:", 6));
} else return 2;
#else
static_assert(sizeof(Snapshot) == sizeof(LegacySnapshot), "Legacy diagnostics layout changed");
static_assert(offsetof(Snapshot, enabled) == offsetof(LegacySnapshot, enabled), "Legacy field offset changed");
static_assert(offsetof(Snapshot, pending) == offsetof(LegacySnapshot, pending), "Legacy field offset changed");
if (strcmp(test, "legacy")) return 2;
setEnabled(true, UINT32_MAX - 10); decision(0, UINT32_MAX - 5);
sleepBegin(UINT32_MAX - 31); sleepEnd(50);
assert(state().calls == 1 && state().returns == 1 && state().last_call_us == 82);
char reply[160];
for (unsigned page = 9; page <= 10; ++page) {
char command[40]; snprintf(command, sizeof(command), "get powersaving.diag %u", page);
assert(handleCommand(command, reply, sizeof(reply), 1));
assert(!strcmp(reply, "Error: get powersaving.diag [0-8|state|calls|time|reason]; reason pages 3-8"));
}
#endif
return 0;
}
'''
class SdkBucketTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.temp = tempfile.TemporaryDirectory(prefix='meshcore-sdk-buckets-')
cls.addClassCleanup(cls.temp.cleanup)
source = Path(cls.temp.name) / 'buckets.cpp'
source.write_text(SOURCE, encoding='ascii')
cls.binaries = {}
for platform in ('ESP32_PLATFORM', 'NRF52_PLATFORM'):
binary = Path(cls.temp.name) / platform
result = subprocess.run(shlex.split(os.environ.get('CXX', 'g++')) +
['-std=c++11', '-Wall', '-Wextra', '-Werror', '-pedantic', '-D' + platform,
'-I', str(ROOT / 'src'), str(source), '-o', str(binary)],
capture_output=True, text=True)
if result.returncode:
raise RuntimeError(result.stderr)
cls.binaries[platform] = binary
def run_case(self, name, platform='ESP32_PLATFORM'):
result = subprocess.run([str(self.binaries[platform]), name], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
def test_bin_boundaries_and_outside_calls_are_not_clamped(self):
self.run_case('boundaries')
def test_on_reset_disabled_and_off_freeze(self):
self.run_case('reset_freeze')
def test_millis_wrap_is_independent_of_micros_wrap(self):
self.run_case('clock_wrap')
def test_saturation_and_missing_clock_are_explicit(self):
self.run_case('saturation_missing')
def test_real_pages_fit_protocol_and_every_small_buffer(self):
self.run_case('format_capacity')
def test_nrf52_retains_legacy_layout_counters_and_cli(self):
self.run_case('legacy', 'NRF52_PLATFORM')
if __name__ == '__main__':
unittest.main()