Files
MeshCore/test/test_esp32_power_test_controls.py
T

240 lines
13 KiB
Python

"""Execute RAM-only controls, cross-TU scanner handoff and UART recovery."""
import os
from pathlib import Path
import subprocess
import tempfile
import unittest
from test_esp32_usb_sleep import HARNESS, board_method, board_sleep_methods
import test_profile_idle_esp32 as profile_tests
ROOT = Path(__file__).resolve().parents[1]
def compile_run(sources, defines=()):
with tempfile.TemporaryDirectory(prefix="mesh-esp32-power-controls-") as directory:
folder = Path(directory)
(folder / "Arduino.h").write_text("#pragma once\n#include <cstdint>\n#include <cmath>\n")
(folder / "esp_timer.h").write_text("#pragma once\n#include <stdint.h>\nint64_t esp_timer_get_time();\n")
files=[]
for index, source in enumerate(sources):
path=folder/f"source{index}.cpp"; path.write_text(source); files.append(str(path))
command=[os.environ.get("CXX","c++"),"-std=c++17","-Wall","-Wextra",
"-fsanitize=address,undefined","-fno-sanitize-recover=all","-fno-pie","-no-pie",
"-I",str(folder),"-I",str(ROOT/"src"),*defines,*files,"-o",str(folder/"test")]
built=subprocess.run(command,capture_output=True,text=True)
if built.returncode: raise AssertionError(built.stderr)
ran=subprocess.run([str(folder/"test")],capture_output=True,text=True)
if ran.returncode: raise AssertionError(ran.stdout+ran.stderr)
class PowerTestControlsTest(unittest.TestCase):
def test_bounds_local_authority_summary_and_reset(self):
compile_run([r'''
#include <cassert>
#include <cstring>
#include <helpers/esp32/PowerTestControls.h>
using namespace mesh::esp32;
static char reply[160];
static bool command(const char* text,bool local=true) {
memset(reply,0x5a,sizeof(reply));
bool handled=handlePowerTestCommand(text,reply,157,local);
assert(reply[157]==0x5a&&reply[159]==0x5a);
if(handled) assert(memchr(reply,0,157));
return handled;
}
int main(){
assert(command("get powersaving.test"));
assert(strstr(reply,"sleep=on slice_ms=10 guard_ms=4 min_ms=2 settle_ms=auto"));
assert(strstr(reply,"RAM only"));
assert(!command("get powersaving.other"));
assert(command("set powersaving.test sleep off",false)&&strstr(reply,"local only"));
assert(manualCpuSleepEnabled());
assert(command("get powersaving.test",false)&&strstr(reply,"local only"));
for(const char* invalid:{"slice_ms 1","slice_ms 21","guard_ms 21","guard_ms -1",
"min_ms 0","min_ms 11","settle_ms 0","settle_ms 51","min_ms 2x",
"slice_ms 99999999999999999","uart_quiet 4","uart_quiet 301","sleep maybe"}){
char input[96];snprintf(input,sizeof(input),"set powersaving.test %s",invalid);
assert(command(input)&&strstr(reply,"Error:"));
}
assert(command("set powersaving.test slice_ms 20"));assert(profileSleepSliceMillis()==20);
assert(command("set powersaving.test guard_ms 0"));assert(profileSleepGuardMicros()==0);
assert(command("set powersaving.test min_ms 1"));assert(profileSleepMinimumMicros()==1000);
assert(command("set powersaving.test settle_ms 50"));assert(profileSleepSettleMillis(7)==50);
assert(command("set powersaving.test sleep off"));assert(!manualCpuSleepEnabled());
assert(command("set powersaving.test reset"));
assert(manualCpuSleepEnabled()&&profileSleepSliceMillis()==10&&profileSleepGuardMicros()==4000);
assert(profileSleepMinimumMicros()==2000&&profileSleepSettleMillis(7)==7);
assert(command("set powersaving.test uart_quiet 5")&&strstr(reply,"unsupported"));
}
'''.replace("#include <cassert>","#include <cassert>\n#include <initializer_list>")])
def test_cli_in_another_translation_unit_changes_real_scanner_and_deadline(self):
setter=r'''
#include <helpers/esp32/PowerTestControls.h>
extern "C" bool change(const char* command){
char reply[160]={};return mesh::esp32::handlePowerTestCommand(command,reply,sizeof(reply),true)
&& strncmp(reply,"Error:",6)!=0;
}
'''
scanner=profile_tests.Esp32ProfileIdleTest.fixture()+r'''
#include <helpers/esp32/ProfileIdleSleep.h>
extern "C" bool change(const char*);
struct Mesh {
RadioLibWrapper& radio;
bool getDualRadioIdleDelay(uint32_t& out){return radio.getProfileIdleDelay(out);}
};
struct Board {unsigned calls=0;uint32_t deadline=0;void sleepUntilMicroseconds(uint32_t at){++calls;deadline=at;}};
int main(){
RadioLibWrapper w;ready(w);Mesh m{w};Board b;
auto start=w._esp32_profile_visit_started_us;auto visit=w._esp32_profile_idle_visit_us;
elapsed_us=start+visit-14000;assert(idle(w,10));
assert(change("set powersaving.test guard_ms 9"));assert(idle(w,5));
assert(change("set powersaving.test slice_ms 3"));assert(idle(w,3));
assert(mesh::esp32::tryProfileIdleSleep(m,b,uint32_t(elapsed_us),30));
assert(b.calls==1&&b.deadline==uint32_t(elapsed_us+3000));
assert(change("set powersaving.test min_ms 6"));assert(!idle(w,0));
assert(!mesh::esp32::tryProfileIdleSleep(m,b,uint32_t(elapsed_us),30)&&b.calls==1);
assert(change("set powersaving.test reset"));assert(idle(w,10));
assert(change("set powersaving.test settle_ms 50"));
elapsed_us=start+10000;assert(!idle(w,0));
assert(change("set powersaving.test settle_ms auto"));assert(idle(w,10));
assert(change("set powersaving.test sleep off"));
assert(!mesh::esp32::tryProfileIdleSleep(m,b,uint32_t(elapsed_us),30)&&b.calls==1);
assert(change("set powersaving.test reset"));
assert(mesh::esp32::tryProfileIdleSleep(m,b,uint32_t(elapsed_us),30)&&b.calls==2);
}
'''
compile_run([setter,scanner],["-DESP32_PLATFORM=1"])
def quiet_harness(self):
methods=board_sleep_methods()+"\n"+"\n".join(board_method(signature) for signature in (
"bool isUsbDataConnected() override","bool isUsbHostConnected() override","bool isUsbSleepHeld()",
"bool handleUartQuiet(","void servicePowerTestControls("))
code=HARNESS.split("int main()",1)[0]
code=code.replace(" void end() { driver_initialized = false; }",r'''
unsigned begins=0,ends=0,flushes=0,fail_begins=0;
void begin(unsigned baud){assert(baud==115200);++begins;driver_initialized=fail_begins==0;if(fail_begins)--fail_begins;}
void flush(){++flushes;}
void end(){++ends;driver_initialized=false;}
''')
code=code.replace("static bool logging_enabled = false;","static bool logging_enabled = false;\nstatic bool text_pending=false;\nbool hasPendingUsbTerminalOutput(){return text_pending;}")
code=code.replace(" bool inhibit_sleep = false;"," bool inhibit_sleep = false;\n bool ota=false;bool isOTAUpdateRunning(){return ota;}\n uint32_t uart_quiet_start_at=0;uint16_t uart_quiet_seconds=0;")
return "#include <cassert>\n"+code.replace("@METHODS@",methods)
def test_actual_uart_ack_drain_bounded_quiet_and_sleep_deadline_restore(self):
compile_run([self.quiet_harness()+r'''
int main(){
ESP32Board board;char reply[160]={};using mesh::esp32::UartQuietAction;
mesh::logging_enabled=true;
assert(!board.handleUartQuiet(UartQuietAction::Schedule,5,reply,sizeof(reply)));
mesh::logging_enabled=false;board.ota=true;
assert(!board.handleUartQuiet(UartQuietAction::Schedule,5,reply,sizeof(reply)));
board.ota=false;
assert(board.handleUartQuiet(UartQuietAction::Schedule,10,reply,sizeof(reply)));
assert(strstr(reply,"100ms"));mock_millis=1099;board.servicePowerTestControls();assert(Serial.ends==0);
mock_millis=1100;board.servicePowerTestControls(false);assert(Serial.ends==0);
mock_millis=1150;mesh::text_pending=true;board.servicePowerTestControls();assert(Serial.ends==0);
mock_millis=1200;mesh::text_pending=false;board.servicePowerTestControls();
assert(Serial.ends==1&&Serial.flushes==1&&!Serial&&board.uart_quiet_restore_at==11200);
board.sleepMilliseconds(30000);assert(sleep_calls==1&&timer_us==10000000);
mock_millis=11199;board.sleepUntilMicroseconds(micros()+10000);
assert(sleep_calls==1); // recovery is due before the minimum useful sleep
mock_millis=11200;board.servicePowerTestControls();
assert(Serial&&Serial.begins==1&&!board.uart_quiet_restore_at);
board.sleepMilliseconds(10);assert(sleep_calls==1); // normal UART guard restored
// An acknowledgement which never drains leaves the console available.
assert(board.handleUartQuiet(UartQuietAction::Schedule,5,reply,sizeof(reply)));
mock_millis+=2000;board.servicePowerTestControls(false);
assert(Serial&&!board.uart_quiet_start_at&&Serial.ends==1);
// Reboot is not required to recover: local reset/off restores immediately.
assert(board.handleUartQuiet(UartQuietAction::Schedule,5,reply,sizeof(reply)));
mock_millis+=100;board.servicePowerTestControls();assert(!Serial);
assert(board.handleUartQuiet(UartQuietAction::Cancel,0,reply,sizeof(reply)));assert(Serial);
// Active logging cancels a quiet interval rather than losing its output.
assert(board.handleUartQuiet(UartQuietAction::Schedule,5,reply,sizeof(reply)));
mock_millis+=100;board.servicePowerTestControls();assert(!Serial);
mesh::logging_enabled=true;board.servicePowerTestControls();assert(Serial);
// Wrap-safe recovery, even with manual CPU sleep disabled.
mesh::logging_enabled=false;mock_millis=UINT32_MAX-50;
assert(board.handleUartQuiet(UartQuietAction::Schedule,5,reply,sizeof(reply)));
mock_millis+=100;board.servicePowerTestControls();assert(!Serial);
mesh::esp32::powerTestControls().sleep_enabled=false;
mock_millis+=5000;board.servicePowerTestControls();assert(Serial);
}
'''],["-DESP32_PLATFORM=1","-DARDUINO_USB_CDC_ON_BOOT=0","-DMESH_USB_LOGGING_AVAILABLE=1"])
def test_role_hook_failures_keep_session_and_bounded_recovery(self):
compile_run([self.quiet_harness()+r'''
struct Session {
bool parked=false,refuse_park=true,refuse_resume=false;
unsigned parks=0,resumes=0;
};
int main(){
ESP32Board board;Session session;char reply[160]={};
using mesh::esp32::UartQuietAction;using mesh::esp32::UartQuietSessionAction;
mesh::esp32::registerUartQuietSessionControl(&session,
[](void* owner,UartQuietSessionAction action){
auto& state=*static_cast<Session*>(owner);
assert(Serial); // Park precedes end; Resume follows a ready begin.
if(action==UartQuietSessionAction::Park){
++state.parks;assert(Serial.flushes>0);
if(state.refuse_park)return false;
state.parked=true;
}else{
++state.resumes;
if(state.refuse_resume)return false;
state.parked=false;
}
return true;
});
assert(board.handleUartQuiet(UartQuietAction::Schedule,5,reply,sizeof(reply)));
assert(board.handleUartQuiet(UartQuietAction::Cancel,0,reply,sizeof(reply)));
assert(session.parks==0&&session.resumes==0&&Serial.ends==0);
assert(board.handleUartQuiet(UartQuietAction::Schedule,5,reply,sizeof(reply)));
mock_millis+=100;board.servicePowerTestControls();
assert(session.parks==1&&!session.parked&&Serial&&Serial.ends==0);
mock_millis+=1900;board.servicePowerTestControls();
assert(!board.uart_quiet_start_at&&session.parks==1&&Serial.ends==0);
session.refuse_park=false;
assert(board.handleUartQuiet(UartQuietAction::Schedule,5,reply,sizeof(reply)));
mock_millis+=100;board.servicePowerTestControls(false);assert(session.parks==1);
board.servicePowerTestControls();assert(session.parked&&!Serial&&Serial.ends==1);
Serial.fail_begins=1;
assert(!board.handleUartQuiet(UartQuietAction::Cancel,0,reply,sizeof(reply)));
assert(session.parked&&session.resumes==0&&!Serial&&board.uart_quiet_restore_at);
session.refuse_resume=true;mock_millis+=1000;board.servicePowerTestControls();
assert(session.parked&&Serial&&session.resumes==1&&board.uart_quiet_restore_at);
session.refuse_resume=false;mock_millis+=1000;board.servicePowerTestControls();
assert(!session.parked&&Serial&&session.resumes==2&&!board.uart_quiet_restore_at);
}
'''],["-DESP32_PLATFORM=1","-DARDUINO_USB_CDC_ON_BOOT=0","-DMESH_USB_LOGGING_AVAILABLE=1"])
def test_native_usb_and_extra_uart_quiet_never_park_the_session(self):
source = self.quiet_harness()+r'''
static unsigned session_actions=0;
int main(){
ESP32Board board;char reply[160]={};
mesh::esp32::registerUartQuietSessionControl(nullptr,
[](void*,mesh::esp32::UartQuietSessionAction){++session_actions;return true;});
assert(!board.handleUartQuiet(mesh::esp32::UartQuietAction::Schedule,5,reply,sizeof(reply)));
assert(strstr(reply,"Error:")&&Serial.ends==0);
board.servicePowerTestControls();assert(Serial.ends==0);
mesh::esp32::registerUartQuietControl(&board,
[](void* ptr,mesh::esp32::UartQuietAction action,uint16_t seconds,char* out,size_t size){
return static_cast<ESP32Board*>(ptr)->handleUartQuiet(action,seconds,out,size);
});
mesh::esp32::powerTestControls().guard_ms=15;
assert(mesh::esp32::handlePowerTestCommand("set powersaving.test reset",reply,sizeof(reply),true));
assert(mesh::esp32::profileSleepGuardMicros()==4000&&strstr(reply,"uart=unsupported"));
assert(session_actions==0);
}
'''
for defines in (("ARDUINO_USB_CDC_ON_BOOT=1",), ("SERIAL_RX=1",), ("SERIAL_TX=1",)):
with self.subTest(defines=defines):
compile_run([source],["-DESP32_PLATFORM=1","-DMESH_USB_LOGGING_AVAILABLE=1",
*["-D"+value for value in defines]])
if __name__ == "__main__": unittest.main(verbosity=2)