Files
MeshCore/test/test_profile_idle.py
T

231 lines
11 KiB
Python

"""Exercise production radio2 idle admission and its final role rechecks."""
import os
from pathlib import Path
import subprocess
import tempfile
import unittest
from cpp_source import body
ROOT = Path(__file__).resolve().parents[1]
HARNESS = r'''
#include <cassert>
#include <cstdint>
#include <RadioProfiles.h>
#define STATE_RX 1
#define MESH_ENABLE_TX_BEACONS 1
#define MESH_ENABLE_MORSE_RX 1
struct Rtc { uint32_t COUNTER = 200; } rtc;
#define NRF_RTC2 (&rtc)
uint32_t profile_clock_raw=0, profile_clock_ticks=0;
struct NRF52Board {
static bool ready;
static bool canIdleRadioProfile() { return ready; }
};
bool NRF52Board::ready = true;
struct RadioLibWrapper {
mesh::RadioProfiles _profiles;
uint32_t _profile_idle_visit_ticks=1000, _profile_idle_stamp=100;
uint32_t _profile_visit_stamp=100, _profile_idle_switch_us=1100;
uint16_t _profile_idle_preamble=32, _physical_preamble=32;
uint32_t _profile_generation=1, _profile_scan_generation[2]={1,1};
uint32_t _profile_retry_at=0, _wd_observe_until=0;
uint8_t state=STATE_RX, _active_profile=0;
bool _params_valid=true, _profile_rxps_suspended=true;
bool _rx_ps_enabled=false, _rx_ps_armed=false, _rx_hold_continuous=false;
bool _rx_retry_pending=false, _profile_refresh_required=false;
bool _nf_refresh_requested=false, _nf_calib_active=false;
bool _noise_floor_valid=true, _noise_floor_secondary_valid=true;
bool _cw_active=false, _morse_owned=false, _beacon_owned=false, _morse_rx_owned=false;
bool irq=false;
bool hasPendingRadioInterrupt() const { return irq; }
bool getProfileIdleDelay(uint32_t&) const;
RadioLibWrapper() {
_profiles.secondary.mode=mesh::RadioProfileMode::RxTx;
_profiles.switch_test_samples[0]=_profiles.switch_test_samples[1]=4;
_profiles.switch_test_max_us[0]=_profiles.switch_test_max_us[1]=1000;
}
};
@METHOD@
int main() {
uint32_t delay=999;
RadioLibWrapper w;
#if !defined(NRF52_PLATFORM)
assert(!w.getProfileIdleDelay(delay) && delay==0);
#else
assert(w.getProfileIdleDelay(delay) && delay==26);
// Identical raw RTC values after a full hardware wrap must not resurrect
// an old idle proof. Both stamps use the software-extended tick domain.
profile_clock_ticks=0x01000000UL;
assert(!w.getProfileIdleDelay(delay));
w._profile_visit_stamp=w._profile_idle_stamp=0x01000064UL;
assert(w.getProfileIdleDelay(delay) && delay==26);
profile_clock_ticks=0;w=RadioLibWrapper();
// Exact early-wake boundary, rounding, already expired, and RTC wrap.
for (uint32_t remaining=0; remaining<=48; ++remaining) {
rtc.COUNTER=100+1000-remaining;
assert(!w.getProfileIdleDelay(delay) && delay==0);
}
rtc.COUNTER=100+1000-49;
assert(w.getProfileIdleDelay(delay) && delay==1);
rtc.COUNTER=1101;
assert(!w.getProfileIdleDelay(delay));
w._profile_visit_stamp=w._profile_idle_stamp=0xfffffff0;
rtc.COUNTER=0x10;
assert(w.getProfileIdleDelay(delay) && delay==29);
w=RadioLibWrapper(); rtc.COUNTER=200;
w._profile_idle_visit_ticks=0x00800000;
assert(!w.getProfileIdleDelay(delay));
// Every software owner, cached timing proof, and asynchronous IRQ fails closed.
auto denied=[&](auto mutate) {
w=RadioLibWrapper(); rtc.COUNTER=200; delay=999;
mutate(); assert(!w.getProfileIdleDelay(delay) && delay==0);
};
denied([&]{NRF52Board::ready=false;}); NRF52Board::ready=true;
denied([&]{w._profiles.secondary.mode=mesh::RadioProfileMode::Off;});
denied([&]{w._profile_idle_visit_ticks=0;});
denied([&]{w._params_valid=false;});
denied([&]{w.state=3;});
denied([&]{w._profile_rxps_suspended=false;});
denied([&]{w._rx_ps_enabled=true;});
denied([&]{w._rx_ps_armed=true;});
denied([&]{w._rx_hold_continuous=true;});
denied([&]{w._rx_retry_pending=true;});
denied([&]{w._profile_retry_at=1;});
denied([&]{w._profile_refresh_required=true;});
denied([&]{w._nf_refresh_requested=true;});
denied([&]{w._nf_calib_active=true;});
denied([&]{w._noise_floor_valid=false;});
denied([&]{w._noise_floor_secondary_valid=false;});
denied([&]{w._wd_observe_until=1;});
denied([&]{w._cw_active=true;});
denied([&]{w._morse_owned=true;});
denied([&]{w._beacon_owned=true;});
denied([&]{w._morse_rx_owned=true;});
denied([&]{w.irq=true;});
denied([&]{w._profiles.resetSwitchTest();});
denied([&]{++w._profiles.generation[0];});
denied([&]{++w._profiles.generation[1];});
denied([&]{w._active_profile=2;});
denied([&]{++w._profile_generation;});
denied([&]{++w._profile_visit_stamp;});
denied([&]{++w._physical_preamble;});
denied([&]{++w._profiles.switch_test_max_us[1];});
// A second query must reject an interrupt or elapsed deadline after admission.
w=RadioLibWrapper();
assert(w.getProfileIdleDelay(delay)); w.irq=true;
assert(!w.getProfileIdleDelay(delay));
w.irq=false; rtc.COUNTER=1099;
assert(!w.getProfileIdleDelay(delay));
mesh::RadioProfileParams longfast;
longfast.bw=250; longfast.sf=11;
longfast.protocol=mesh::RadioProfileProtocol::Meshtext;
assert(mesh::RadioProfiles::minimumListenUs(longfast,true)==37684);
w._profiles.secondary.params=longfast;
assert(w._profiles.preamble(1,32)==16);
#endif
}
'''
class ProfileIdleTests(unittest.TestCase):
def test_scanner_cache_tracks_visits_and_invalidates_early_returns(self):
from test_radio_profile_scan import HARNESS as scan_harness
source = (ROOT / "src/helpers/radiolib/RadioLibWrappers.cpp").read_text()
names = [("uint8_t", "beginReconfigure"), ("void", "endReconfigure"),
("void", "serviceProfileScan")] + [("mesh::RadioParamApplyResult", name) for name in
("tuneProfile", "prepareTransmitProfile", "tryRestoreCodingRate",
"trySetParams", "trySetPrimaryParams")]
methods = "\n".join(body(source, f"{kind} RadioLibWrapper::{name}(") for kind, name in names)
methods += "\n" + body(source,
"mesh::RadioParamApplyResult RadioLibWrapper::prepareTransmitProfile(uint8_t profile, bool")
wrapper = (ROOT / "src/helpers/radiolib/CustomSX1262Wrapper.h").read_text()
methods += "\n" + body(wrapper, "void setProfileStandbyWarm(").replace(
"void setProfileStandbyWarm(bool enabled) override",
"void RadioLibWrapper::setProfileStandbyWarm(bool enabled)")
fixture = scan_harness.split("int main()", 1)[0].replace("@METHODS@", methods)
fixture = fixture.replace(" bool _cw_active=false;", """
uint32_t _profile_idle_visit_ticks=0, _profile_idle_stamp=0, _profile_idle_switch_us=0;
uint16_t _profile_idle_preamble=0;
bool _cw_active=false;
""")
fixture += r'''
int main() {
RadioLibWrapper w; w.enable();
assert(w._profile_idle_visit_ticks==0); // restart needs a fresh pass
w.serviceProfileScan();
const auto ticks=w._profile_idle_visit_ticks;
assert(ticks>0 && w._profile_idle_stamp==w._profile_visit_stamp);
assert(ticks==uint64_t(w._profiles.listenUs(w._active_profile,
w.profilePreamble(w._profiles.slowerProfile())))*512/15625);
w._cw_active=true; w.serviceProfileScan();
assert(w._profile_idle_visit_ticks==0);
w._cw_active=false; w.serviceProfileScan(); assert(w._profile_idle_visit_ticks);
w._profile_retry_at=millis()+1000; w.serviceProfileScan();
assert(w._profile_idle_visit_ticks==0);
w._profile_retry_at=0; w.serviceProfileScan(); assert(w._profile_idle_visit_ticks);
w.busy=true; w.serviceProfileScan(); assert(w._profile_idle_visit_ticks==0);
w.busy=false; w.packet=true; w.serviceProfileScan(); assert(w._profile_idle_visit_ticks==0);
w.packet=false; w.events.record(); w.serviceProfileScan(); assert(w._profile_idle_visit_ticks==0);
w.events.claim(); w.serviceProfileScan(); assert(w._profile_idle_visit_ticks);
elapsed_us+=1000000; // hop cannot publish old visit with new timestamp
const auto old=w._active_profile; w.serviceProfileScan();
assert(w._active_profile!=old && w._profile_idle_visit_ticks==0);
w.serviceProfileScan(); assert(w._profile_idle_visit_ticks);
w._profiles.setSecondary({},false); w.serviceProfileScan();
assert(w._profile_idle_visit_ticks==0);
}
'''
with tempfile.TemporaryDirectory() as folder:
path = Path(folder)
(path / "Arduino.h").write_text("#pragma once\n#include <cstdint>\n#include <cmath>\n")
(path / "test.cpp").write_text(fixture)
result = subprocess.run([os.environ.get("CXX", "g++"), "-std=c++17",
"-DNRF52_PLATFORM=1", "-Wall", "-Wextra", "-I", folder,
"-I", str(ROOT / "src"), str(path / "test.cpp"), "-o", str(path / "test")],
capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
result = subprocess.run([str(path / "test")], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
def test_integer_admission_and_unsupported_platform(self):
source = (ROOT / "src/helpers/radiolib/RadioLibWrappers.cpp").read_text()
method = body(source, "bool RadioLibWrapper::getProfileIdleDelay(")
# Keep the final admission free of SPI, float work and clock mutation.
for forbidden in ("profilePreamble(", "listenUs(", "profileTimestamp(",
"isChipBusy(", "isPacketPendingOrReceiving("):
self.assertNotIn(forbidden, method)
with tempfile.TemporaryDirectory() as folder:
path = Path(folder)
(path / "Arduino.h").write_text("#pragma once\n#include <cstdint>\n#include <cmath>\n")
(path / "test.cpp").write_text(HARNESS.replace("@METHOD@", method))
for defines in ([], ["-DNRF52_PLATFORM=1"]):
result = subprocess.run([os.environ.get("CXX", "g++"), "-std=c++17",
"-Wall", "-Wextra", "-Werror", *defines, "-I", folder,
"-I", str(ROOT / "src"), str(path / "test.cpp"), "-o", str(path / "test")],
capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
result = subprocess.run([str(path / "test")], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
def test_late_preparation_precedes_all_guards_and_final_recheck(self):
for role in ("companion_radio", "simple_repeater", "simple_room_server", "simple_sensor"):
source = (ROOT / "examples" / role / "main.cpp").read_text()
loop = body(source, "\nvoid loop()")
prepare = loop.index("board.prepareProfileIdleCycle()")
predicate = loop.index("sleep_reason =" if role == "companion_radio" else "const auto sleep_guard")
self.assertLess(prepare, predicate, role)
final = loop.index("the_mesh.canIdleDualRadio()", predicate)
sleep = loop.index("board.sleep", final)
self.assertLess(final, sleep, role)
self.assertNotIn("prepareProfileIdleCycle", loop[predicate:], role)
if role == "simple_sensor":
arm = loop.index("armTrackerSleepWake(", predicate)
timer_recheck = loop.index("the_mesh.canIdleDualRadio()", arm)
self.assertLess(timer_recheck, loop.index("board.sleep", arm))
if __name__ == "__main__":
unittest.main()