Files
MeshCore/test/test_profile_switch_timing.py
T

227 lines
11 KiB
Python

"""Execute optional RAM timing aggregation without radio hardware."""
from pathlib import Path
import os
import subprocess
import tempfile
import unittest
from test_radio_receive_contract import method
import test_radio_profile_scan as scan
ROOT = Path(__file__).resolve().parents[1]
class ProfileSwitchTimingTest(unittest.TestCase):
def compile_run(self, source):
with tempfile.TemporaryDirectory() as directory:
folder = Path(directory)
(folder / "Arduino.h").write_text(
"#pragma once\n#include <cstdint>\n#include <cmath>\n")
cpp, binary = folder / "test.cpp", folder / "test.exe"
cpp.write_text(source, encoding="ascii")
built = subprocess.run([os.environ.get("CXX", "g++"), "-std=c++17",
"-Wall", "-Wextra", "-I", str(folder), "-I", str(ROOT / "src"),
str(cpp), "-o", str(binary)], capture_output=True, text=True)
self.assertEqual(built.returncode, 0, built.stderr)
ran = subprocess.run([str(binary)], capture_output=True, text=True)
self.assertEqual(ran.returncode, 0, ran.stderr)
@staticmethod
def production_globals():
source = (ROOT / "src/helpers/radiolib/RadioLibWrappers.cpp").read_text()
return source.split("#if MESH_HIL_PROFILE_TIMING\n", 1)[1].split("#endif", 1)[0]
def test_direction_histogram_clock_wrap_failures_and_split_sum(self):
self.compile_run(r'''
#include <cassert>
#include <cstdint>
#define MESH_HIL_PROFILE_TIMING 1
#define NRF52_PLATFORM 1
struct FakeDwt { uint32_t CYCCNT=0; } fakeDwt;
#define DWT (&fakeDwt)
static bool clockEnabled=false;
static unsigned enableCalls=0;
bool dwt_enabled() { return clockEnabled; }
void dwt_enable() { clockEnabled=true; ++enableCalls; }
#include <helpers/radiolib/ProfileSwitchTiming.h>
@GLOBALS@
int main() {
using namespace mesh::profiletiming;
auto& capture=mesh_hil_profile_timing;
assert(capture.magic==Magic && capture.version==3 && capture.size_bytes==2192);
assert(capture.clock_hz==64000000 && capture.sequence==0);
initializeClock();
assert(enableCalls==1 && capture.clock_state==2);
initializeClock();
assert(enableCalls==1 && capture.clock_state==3 && capture.clock_initializations==2);
fakeDwt.CYCCNT=1234; assert(readClock()==1234);
// Raw counter wrap inside an awake hop is handled before /64 conversion.
uint32_t ticks=0xfffff000U;
begin(0,1,ticks);
for(uint32_t stage=0;stage<Whole;++stage) {
ticks+=(40+10*stage)*64;
mark(static_cast<Stage>(stage),ticks);
}
finish(Success,ticks);
assert(capture.sequence==2 && !(capture.sequence&1));
const auto& forward=capture.direction[0];
assert(forward.successes==1 && forward.busy==0 && forward.failures==0);
assert(forward.last_mask==2047 && forward.last_us[Whole]==850);
assert(forward.worst_us[Whole]==850 && forward.worst_us[Frequency]==70);
assert(forward.stage[Whole].sum_low_us==850 && forward.stage[Whole].sum_high_us==0);
assert(forward.stage[Whole].min_us==850 && forward.stage[Whole].max_us==850);
assert(forward.stage[Whole].histogram[13]==1);
for(uint32_t stage=0;stage<Whole;++stage)
assert(forward.stage[stage].last_us==40+10*stage);
// Aborts keep partial evidence but never inflate successful timing stats.
begin(1,0,100); mark(Admission,100+1000*64); finish(Busy,100+1000*64);
assert(capture.direction[1].busy==1 && capture.direction[1].successes==0);
assert(capture.direction[1].stage[Whole].count==0);
assert(capture.direction[1].last_us[Whole]==1000);
begin(1,0,100); mark(Frequency,100+1100*64); finish(Failure,100+1100*64);
assert(capture.direction[1].failures==1 && capture.direction[1].stage[Frequency].count==0);
const uint32_t sequence=capture.sequence;
// Marks after failure, rollback, same-profile refresh, or malformed profile
// values cannot append samples to the old transaction.
mark(Frequency,0); finish(Success,0);
begin(0,0,0); mark(Frequency,100); finish(Success,100);
begin(2,0,0); finish(Success,100);
assert(capture.sequence==sequence);
auto& sum=capture.direction[0].stage[Frequency];
sum.sum_low_us=0xfffffff5U; sum.sum_high_us=3;
begin(0,1,0); mark(Frequency,20*64); finish(Success,20*64);
assert(sum.sum_low_us==9 && sum.sum_high_us==4);
assert(sum.min_us==20 && sum.max_us==70 && sum.count==2);
assert(forward.worst_us[Whole]==850 && forward.worst_us[Frequency]==70);
begin(1,0,0); mark(Frequency,960*64); finish(Success,960*64);
assert(capture.direction[1].stage[Frequency].histogram[15]==1);
begin(1,0,0); mark(Frequency,10000*64); finish(Success,10000*64);
assert(capture.direction[1].stage[Frequency].histogram[15]==2);
assert(capture.direction[1].worst_us[Whole]==10000);
assert(capture.direction[1].worst_us[Frequency]==10000);
}
'''.replace("@GLOBALS@", self.production_globals()))
def test_real_profile_transaction_retains_success_busy_and_rollback(self):
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(method(source, f'{kind} RadioLibWrapper::{name}(')
for kind, name in names)
methods += '\n' + method(source,
'mesh::RadioParamApplyResult RadioLibWrapper::prepareTransmitProfile(uint8_t profile, bool')
wrapper = (ROOT / 'src/helpers/radiolib/CustomSX1262Wrapper.h').read_text()
methods += '\n' + method(wrapper, 'void setProfileStandbyWarm(').replace(
'void setProfileStandbyWarm(bool enabled) override',
'void RadioLibWrapper::setProfileStandbyWarm(bool enabled)')
prefix = '#include <helpers/radiolib/ProfileSwitchTiming.h>\n' + self.production_globals()
fixture = scan.HARNESS.replace('@METHODS@', prefix + methods)
self.compile_run('#define MESH_HIL_PROFILE_TIMING 1\n' + fixture)
def test_actual_admission_standby_and_post_guard_stages_cover_every_exit(self):
source = (ROOT / "src/helpers/radiolib/RadioLibWrappers.cpp").read_text()
implementation = method(source, "uint8_t RadioLibWrapper::beginReconfigure(")
fixture = r'''
#include <cassert>
#include <cstdint>
#define MESH_HIL_PROFILE_TIMING 1
#define MESH_ENABLE_TX_BEACONS 1
#define RADIOLIB_ERR_NONE 0
#define STATE_IDLE 0
#define STATE_RX 1
#define STATE_TX_WAIT 3
static uint32_t now_us=0;
uint32_t micros() { return now_us; }
#include <helpers/radiolib/ProfileSwitchTiming.h>
@GLOBALS@
struct RadioLibWrapper {
bool _beacon_owned=false,_beacon_restoring=false,_cw_active=false;
bool _profile_repair_required=false,_rx_hold_continuous=true;
bool _rx_ps_armed=false,_profile_refresh_required=false;
bool pending=false,busy=false,receiving=false,post_irq=false;
uint8_t state=STATE_RX;
unsigned standby_calls=0;
int16_t standby_result=0;
struct Chip {
RadioLibWrapper* owner;
int16_t standby() {
++owner->standby_calls;now_us+=20;
if(owner->post_irq) owner->pending=true;
return owner->standby_result;
}
} chip{this};
Chip* _radio=&chip;
bool hasPendingRadioInterrupt() { now_us+=2;return pending; }
bool isChipBusy() { now_us+=3;return busy; }
bool isReceivingPacket() { now_us+=5;return receiving; }
void claimRadioInterrupt() { now_us+=4;pending=false; }
uint8_t beginReconfigure();
};
@METHOD@
int main() {
using namespace mesh::profiletiming;
auto& direction=mesh_hil_profile_timing.direction[0];
// Success, already idle, each admission guard, both standby failures,
// an IRQ arriving in standby, and the physical-repair branch.
for(unsigned scenario=0;scenario<13;++scenario) {
RadioLibWrapper radio;
uint8_t expected_result=2;
unsigned admission=0,standby=0,post=0,calls=0;
switch(scenario) {
case 0: expected_result=1;admission=10;standby=20;post=2;calls=1;break;
case 1: radio.state=STATE_IDLE;expected_result=0;admission=10;post=2;break;
case 2: radio.pending=true;admission=2;break;
case 3: radio.state=STATE_TX_WAIT;admission=2;break;
case 4: radio.busy=true;admission=5;break;
case 5: radio.receiving=true;admission=10;break;
case 6: radio._cw_active=true;break;
case 7: radio.standby_result=-1;expected_result=3;admission=10;standby=20;calls=1;break;
case 8: radio.post_irq=true;admission=10;standby=20;post=2;calls=1;break;
case 9: radio._profile_repair_required=true;expected_result=1;admission=7;standby=20;post=4;calls=1;break;
case 10: radio._profile_repair_required=true;radio.busy=true;admission=3;break;
case 11: radio._profile_repair_required=true;radio.standby_result=-1;
expected_result=3;admission=7;standby=20;calls=1;break;
case 12: radio._beacon_owned=true;break;
}
now_us=0;begin(0,1,now_us);
const uint32_t samples=direction.stage[Admission].count;
assert(radio.beginReconfigure()==expected_result);
assert(radio.standby_calls==calls);
const Result result=expected_result<2 ? Success : expected_result==2 ? Busy : Failure;
finish(result,now_us);
assert(direction.last_mask==((1U<<Admission)|(1U<<Standby)|(1U<<PostStandby)|(1U<<Whole)));
assert(direction.last_us[Admission]==admission);
assert(direction.last_us[Standby]==standby);
assert(direction.last_us[PostStandby]==post);
assert(direction.last_us[Whole]==admission+standby+post);
// Even absent standby has a zero sample on successful transactions;
// partial/failing operations cannot contaminate successful aggregates.
assert(direction.stage[Admission].count==samples+unsigned(result==Success));
assert(direction.stage[Standby].count==direction.stage[Admission].count);
assert(direction.stage[PostStandby].count==direction.stage[Admission].count);
}
assert(direction.successes==3 && direction.busy==8 && direction.failures==2);
assert(direction.stage[Standby].min_us==0 && direction.stage[Standby].max_us==20);
assert(direction.worst_us[Whole]==32 && direction.worst_us[Admission]==10);
}
'''.replace("@GLOBALS@", self.production_globals()).replace("@METHOD@", implementation)
self.compile_run(fixture)
def test_disabled_header_has_no_instrumentation_or_ram_definition(self):
source = '#include <helpers/radiolib/ProfileSwitchTiming.h>\nint main() {}\n'
preprocessed = subprocess.run([os.environ.get('CXX', 'g++'), '-E', '-P',
'-x', 'c++', '-I', str(ROOT / 'src'), '-'], input=source,
capture_output=True, text=True)
self.assertEqual(preprocessed.returncode, 0, preprocessed.stderr)
self.assertEqual(preprocessed.stdout.strip(), 'int main() {}')
if __name__ == '__main__':
unittest.main()