Files
MeshCore/test/test_radio_interrupt_recovery.py
mikecarper 0be4c7488c Add bounded Morse receive and negotiated RS232 autosleep
Add native narrow-band CW capture, ACK-first remote sessions, retained LoRa-readable pages, checked restoration and transport-safe progress. Add stock-compatible negotiated heartbeat/autosleep and recovery. Include host regressions and identity-bound HIL runners; keep unverified faster tuning HIL-only and document outstanding hardware validation.
2026-10-13 01:24:39 -07:00

432 lines
21 KiB
Python

"""Execute real ISR handoff, wrapper lifecycle and LR2021 receive failure paths."""
from pathlib import Path
import os
import subprocess
import tempfile
import unittest
from test_radio_receive_contract import method
ROOT = Path(__file__).resolve().parents[1]
def compile_run(code, defines=()):
with tempfile.TemporaryDirectory(prefix="meshcore-radio-irq-") as tmp:
cpp, exe = Path(tmp) / "test.cpp", Path(tmp) / "test"
cpp.write_text(code)
command = [os.environ.get("CXX", "g++"), "-std=c++17", "-O1", "-Wall", "-Wextra",
"-fsanitize=address,undefined", "-fno-omit-frame-pointer", "-fno-pie", "-no-pie",
"-pthread", "-I", str(ROOT / "src"), *defines, str(cpp), "-o", str(exe)]
result = subprocess.run(command, capture_output=True, text=True)
if result.returncode:
raise AssertionError(result.stderr)
result = subprocess.run([str(exe)], capture_output=True, text=True)
if result.returncode:
raise AssertionError(result.stderr)
PRELUDE = r'''
#include <cassert>
#include <cstring>
#include <cstdint>
#include <initializer_list>
#include <helpers/radiolib/RadioInterruptEvents.h>
#define STATE_IDLE 0
#define STATE_RX 1
#define STATE_TX_WAIT 3
#define RADIOLIB_ERR_NONE 0
#define RADIOLIB_ERR_INVALID_FREQUENCY -2
#define RADIOLIB_ERR_UNKNOWN -16
#define RADIOLIB_ERR_UNSUPPORTED -2
#define RADIOLIB_ERR_PACKET_TOO_SHORT -5
#define RADIOLIB_ERR_SPI_CMD_TIMEOUT -77
#define NF_CALIB_SETTLE_MS 7
#define RADIOLIB_LR2021_RX_TIMEOUT_INF UINT32_MAX
#define RADIOLIB_IRQ_RX_DEFAULT_FLAGS 0
#define RADIOLIB_IRQ_RX_DEFAULT_MASK 0
#define RADIOLIB_IRQ_PREAMBLE_DETECTED 5
#define RADIOLIB_IRQ_RX_DONE 1
#define RADIOLIB_IRQ_TX_DONE 2
#define RADIOLIB_LR2021_IRQ_RX_DONE 1
#define RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED 32
#define RADIOLIB_LR2021_IRQ_LORA_HEADER_VALID 64
#define RADIOLIB_LR2021_IRQ_LORA_HDR_CRC_ERROR 512
#define MESH_DEBUG_PRINTLN(...) ((void)0)
static uint32_t now_ms=100;
uint32_t millis(){return now_ms;}
struct LR2021 {
bool stopped=false, inject_read_irq=false;
int rx_status=1, tx_status=1, fifo_status=0, clear_status=0, base_status=0, standby_status=0;
uint32_t irq=0;
uint16_t fifo=4;
int packet_length=4;
unsigned reads=0, clears=0, standbys=0, arms=0, irq_clears=0, tx_starts=0;
void (*callback)()=nullptr;
void clearPacketReceivedAction(){callback=nullptr;}
void setPacketReceivedAction(void (*fn)()){callback=fn;}
int checkIrq(int type){return type==RADIOLIB_IRQ_RX_DONE?rx_status:tx_status;}
int standby(){++standbys;if(standby_status)return standby_status;stopped=true;return 0;}
int clearIrqFlags(uint32_t){assert(stopped);++irq_clears;rx_status=0;return 0;}
int getPacketLength(){return packet_length;}
float getSNR(){return 7;}
float getRSSI(){return -90;}
int getRxFifoLevel(uint16_t* output){*output=fifo;return fifo_status;}
int clearRxFifo(){++clears;if(clear_status)return clear_status;fifo=0;return 0;}
virtual int16_t readData(uint8_t* output,size_t len){
++reads;
#if defined(USE_LR2021)
// Real RadioLib clears FIFO and all IRQs. A concurrent packet must not be
// able to modify the FIFO while those destructive operations run.
assert(stopped);
#endif
memset(output,0xa5,len);fifo=0;
if(inject_read_irq && !stopped && callback) callback();
return base_status;
}
virtual int16_t startReceive(){++arms;stopped=false;return 0;}
int16_t startReceive(uint32_t,uint32_t,uint32_t,int){assert(stopped);++arms;stopped=false;return 0;}
int startTransmit(uint8_t*,int){++tx_starts;return 0;}
int finishTransmit(){stopped=true;return 0;}
uint32_t getIrqStatus(){return irq;}
};
struct Chip : LR2021 {
uint32_t _activityAt=0, _preambleMillis=66, _maxPayloadMillis=3934;
bool _headerSeen=false;
bool isChipBusy(){return false;}
@LR_METHODS@
};
struct Board {
unsigned before=0,after=0;
void (*before_hook)()=nullptr;
void onBeforeTransmit(){++before;if(before_hook)before_hook();}
void onAfterTransmit(){++after;}
};
struct RadioLibWrapper {
uint8_t state=STATE_RX, _interrupt_slot=0xff, _irq_probe_failures=0;
bool _loop_event_pending=false, _cw_active=false;
bool _rx_ps_armed=false, _rx_hold_continuous=false, _rx_ps_enabled=false;
bool _rx_ps_continuous_fallback=false, _nf_calib_active=false;
float _last_snr=0, _last_rssi=0;
unsigned n_recv=0,n_recv_errors=0,n_sent=0;
uint32_t last_recv_millis=0,last_radio_interrupt_millis=0,_nf_sample_from=0;
Board board;Board* _board=&board;
Chip chip;Chip* _radio=&chip;
protected:
~RadioLibWrapper()=default;
public:
bool registerInterruptAction();void unregisterInterruptAction();
bool hasPendingRadioInterrupt()const;bool claimRadioInterrupt();
void requestRestartRecv();bool isPacketReady();
bool startSendRaw(const uint8_t*,int);bool isSendComplete();
int recvRaw(uint8_t*,int);
void onSendFinished();
void idle(){state=STATE_IDLE;}
bool recoverRadio(bool){return !hasPendingRadioInterrupt();}
void stopReceiveDutyCycle(){_rx_ps_armed=false;chip.standby();}
void startRecv(){if(chip.startReceive()==0)state=STATE_RX;}
};
@CALLBACKS@
@METHODS@
@CONCRETE_CLASSES@
'''
class RadioInterruptRecoveryTests(unittest.TestCase):
def run_dispatcher_retry(self, soft_only=False, remove_guard=False):
with tempfile.TemporaryDirectory(prefix="meshcore-radio-retry-") as tmp:
dispatcher = ROOT / "src/Dispatcher.cpp"
if remove_guard:
original = dispatcher.read_text()
guard = (" if (outbound->isRouteFlood() && outbound->flood_retry_policy "
"== FLOOD_RETRY_POLICY_DENY) return false;\n")
self.assertEqual(original.count(guard), 1)
dispatcher = Path(tmp) / "Dispatcher.cpp"
dispatcher.write_text(original.replace(guard, ""))
exe = Path(tmp) / "retry"
command = [os.environ.get("CXX", "g++"), "-std=c++17", "-O1", "-Wall", "-Wextra",
"-fsanitize=address,undefined", "-fno-omit-frame-pointer", "-fno-pie", "-no-pie",
"-I", str(ROOT / "test/mocks"), "-I", str(ROOT / "src")]
if soft_only:
command.append("-DRADIO_LIVENESS_SOFT_ONLY")
command += [str(dispatcher), str(ROOT / "src/Packet.cpp"),
str(ROOT / "src/helpers/StaticPoolPacketManager.cpp"),
str(ROOT / "test/fixtures/dispatcher/radio_retry_test.cpp"), "-o", str(exe)]
result = subprocess.run(command, capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
env = os.environ.copy()
# Firmware-lifetime production pools do not have destructors.
env["ASAN_OPTIONS"] = env.get("ASAN_OPTIONS", "") + ":detect_leaks=0"
result = subprocess.run([str(exe)], capture_output=True, text=True, env=env)
if remove_guard:
self.assertNotEqual(result.returncode, 0)
self.assertIn("a denied flood must never be retransmitted", result.stderr)
else:
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
def test_actual_dispatcher_lost_tx_done_respects_flood_retry_policy(self):
for soft_only in (False, True):
with self.subTest(soft_only=soft_only):
self.run_dispatcher_retry(soft_only=soft_only)
def test_removed_flood_retry_guard_repeats_actual_timed_out_transmission(self):
self.run_dispatcher_retry(remove_guard=True)
def test_actual_wrapper_header_uses_actual_radio_base(self):
# Keep Mesh/Dispatcher and the wrapper header real: the common test
# Mesh.h gives Radio a virtual destructor that production does not.
with tempfile.TemporaryDirectory(prefix="meshcore-radio-header-") as tmp:
folder = Path(tmp)
(folder / "RadioLib.h").write_text(r'''
#pragma once
#include <cstdint>
#define RADIOLIB_ERR_NONE 0
#define RADIOLIB_ERR_INVALID_FREQUENCY -2
#define RADIOLIB_ERR_UNKNOWN -16
#define RADIOLIB_NC 0xffffffff
class PhysicalLayer {
public:
int setOutputPower(int8_t){return 0;}
int setFrequency(float){return 0;}
int standby(){return 0;}
int startReceive(){return 0;}
int transmitDirect(){return 0;}
int setPreambleLength(uint16_t){return 0;}
uint8_t randomByte(){return 0;}
};
''')
(folder / "header.cpp").write_text(r'''
long random(long,long);
#include <helpers/radiolib/RadioLibWrappers.h>
#include <type_traits>
static_assert(!std::has_virtual_destructor<mesh::Radio>::value, "real Radio ABI");
static_assert(!std::has_virtual_destructor<RadioLibWrapper>::value, "concrete wrapper ownership ABI");
static_assert(!std::is_destructible<RadioLibWrapper>::value, "borrowed base cannot own destruction");
''')
result = subprocess.run([
os.environ.get("CXX", "g++"), "-std=c++17", "-fsyntax-only",
"-DLORA_SF=7", "-DLORA_BW=125", "-I", str(folder),
"-I", str(ROOT / "src"), "-I", str(ROOT / "test/mocks"),
str(folder / "header.cpp"),
], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
def test_atomic_generation_latch_and_counter_wrap(self):
compile_run(r'''
#include <helpers/radiolib/RadioInterruptEvents.h>
#include <atomic>
#include <cassert>
#include <cstring>
#include <thread>
int main(){
RadioInterruptEvents a,b;
assert(!a.claim());a.record();assert(a.pending()&&!b.pending());
assert(a.claim()&&!a.pending());a.record();assert(a.claim());
uint32_t wrapped[2]={UINT32_MAX,UINT32_MAX};
static_assert(sizeof(a)==sizeof(wrapped),"bounded ISR storage");
memcpy(&a,wrapped,sizeof(a));a.record();assert(a.pending()&&a.claim()&&!a.pending());
std::atomic<bool> done{false};
std::thread producer([&]{for(unsigned i=0;i<1000000;++i)a.record();done.store(true);});
while(!done.load())a.claim();producer.join();a.claim();assert(!a.pending());
b.record();assert(!a.pending()&&b.pending());
}
''')
def source(self):
wrapper = (ROOT / "src/helpers/radiolib/RadioLibWrappers.cpp").read_text()
custom = (ROOT / "src/helpers/radiolib/CustomLR2021.h").read_text()
callbacks = wrapper[wrapper.index("#define RADIO_INTERRUPT_INTERNAL_"):
wrapper.index("// The Adafruit nRF52")]
callbacks += "\n" + wrapper[wrapper.index("static\n#if defined(ESP8266)"):
wrapper.index("bool RadioLibWrapper::registerInterruptAction()")]
names = [("bool", "registerInterruptAction"), ("void", "unregisterInterruptAction"),
("bool", "hasPendingRadioInterrupt"), ("bool", "claimRadioInterrupt"),
("void", "requestRestartRecv"), ("bool", "isPacketReady"),
("bool", "startSendRaw"), ("bool", "isSendComplete"), ("int", "recvRaw"),
("void", "onSendFinished")]
methods = "\n".join(method(wrapper, f"{kind} RadioLibWrapper::{name}(")
for kind, name in names)
lr_methods = "\n".join(method(custom, signature) for signature in
("int16_t readData(", "bool isReceiving()", "int16_t startReceive()"))
concrete = []
for name in ("CustomLLCC68Wrapper", "CustomLR1110Wrapper", "CustomLR1121Wrapper",
"CustomLR2021Wrapper", "CustomSTM32WLxWrapper", "CustomSX1262Wrapper",
"CustomSX1268Wrapper", "CustomSX1276Wrapper"):
header = (ROOT / f"src/helpers/radiolib/{name}.h").read_text()
concrete.append(f"struct {name} : RadioLibWrapper {{" + method(header, f"~{name}()") + "};")
# The two further-derived production families inherit concrete cleanup.
for file in (ROOT / "src/helpers/radiolib/DacPaSX1276Wrapper.h",
ROOT / "variants/sensecap_indicator-espnow/IndicatorSX1262Wrapper.h"):
import re
declaration = re.search(r"class (\w+) : public (\w+)", file.read_text())
concrete.append(f"struct {declaration[1]} : {declaration[2]} {{}};")
return (PRELUDE.replace("@LR_METHODS@", lr_methods).replace("@CALLBACKS@", callbacks)
.replace("@METHODS@", methods).replace("@CONCRETE_CLASSES@", "\n".join(concrete)))
def test_mode_changes_lifecycle_late_callbacks_and_tx_status_retry(self):
compile_run(self.source() + r'''
int main(){
uint8_t bytes[16]={};
auto* a=new CustomSX1262Wrapper;auto* b=new CustomSX1262Wrapper;
assert(a->registerInterruptAction()&&b->registerInterruptAction());
assert(a->chip.callback!=b->chip.callback);
auto* c=new CustomSX1262Wrapper;assert(!c->registerInterruptAction());
a->chip.callback();assert(a->hasPendingRadioInterrupt()&&!b->hasPendingRadioInterrupt());
a->requestRestartRecv();assert(a->state==STATE_IDLE&&a->hasPendingRadioInterrupt());
assert(!a->startSendRaw(bytes,4));assert(a->chip.tx_starts==0);
assert(a->recvRaw(bytes,sizeof(bytes))==4&&a->n_recv==1);
b->chip.callback();assert(b->hasPendingRadioInterrupt()&&!a->hasPendingRadioInterrupt());
assert(b->registerInterruptAction()&&!b->hasPendingRadioInterrupt());
void(*late)()=a->chip.callback;delete a;late(); // static storage, no UAF
assert(c->registerInterruptAction()&&!c->hasPendingRadioInterrupt());
late();c->chip.rx_status=0;assert(c->recvRaw(bytes,sizeof(bytes))==0); // no ghost frame
assert(c->recvRaw(bytes,sizeof(bytes))==0&&c->recvRaw(bytes,sizeof(bytes))==0);
assert(!c->hasPendingRadioInterrupt());
c->chip.rx_status=1;
c->board.before_hook=c->chip.callback;
assert(!c->startSendRaw(bytes,4)&&c->board.before==1&&c->board.after==1);
assert(c->hasPendingRadioInterrupt()&&c->chip.tx_starts==0);
c->claimRadioInterrupt();c->board.before_hook=nullptr;
assert(c->startSendRaw(bytes,4)&&c->state==STATE_TX_WAIT);
c->chip.callback();c->chip.tx_status=0;
assert(!c->isSendComplete()&&c->hasPendingRadioInterrupt());
c->chip.tx_status=-99;assert(!c->isSendComplete()&&c->hasPendingRadioInterrupt());
c->chip.tx_status=1;assert(c->isSendComplete()&&c->n_sent==1);
assert(!c->isSendComplete()&&c->n_sent==1);
c->chip.tx_status=0;assert(c->startSendRaw(bytes,4));c->chip.callback();
assert(!c->isSendComplete()&&c->hasPendingRadioInterrupt());
c->onSendFinished();assert(!c->hasPendingRadioInterrupt()&&c->recoverRadio(true));
// A receive status read temporarily returning zero retains the event.
c->chip.callback();c->chip.rx_status=0;assert(c->recvRaw(bytes,sizeof(bytes))==0);
c->chip.rx_status=1;c->chip.fifo=4;
assert(c->recvRaw(bytes,sizeof(bytes))==4&&c->n_recv==1);
delete b;delete c;
}
''')
def test_all_concrete_families_release_slot_on_destruction(self):
names = ("CustomLLCC68Wrapper", "CustomLR1110Wrapper", "CustomLR1121Wrapper",
"CustomLR2021Wrapper", "CustomSTM32WLxWrapper", "CustomSX1262Wrapper",
"CustomSX1268Wrapper", "CustomSX1276Wrapper", "DacPaSX1276Wrapper",
"IndicatorSX1262Wrapper")
checks = ["int main(){"]
for name in names:
checks.append(f"""
+ {{
+ auto* original=new {name};assert(original->registerInterruptAction());
+ void(*late)()=original->chip.callback;delete original;
+ assert(!radio_interrupt_owners[0]&&!radio_interrupt_owners[1]);
+ late(); // no object access through an in-flight hardware callback
+ auto* replacement=new {name};assert(replacement->registerInterruptAction());
+ assert(!replacement->hasPendingRadioInterrupt());delete replacement;
+ }}
+""".replace("\n+", "\n"))
checks.append("}")
compile_run(self.source() + "\n".join(checks))
def test_internal_stm32_slot_capacity_and_external_radio_capacity(self):
checks = self.source() + r'''
int main(){
CustomSTM32WLxWrapper first,second,third;
assert(first.registerInterruptAction());
second._interrupt_slot=0; // stale slot must never steal another owner
#if RADIO_INTERRUPT_SLOT_COUNT == 1
static_assert(sizeof(radio_interrupt_owners)==sizeof(RadioLibWrapper*),"internal radio has one slot");
assert(!second.registerInterruptAction()&&second._interrupt_slot==0xff);
assert(!second.chip.callback&&radio_interrupt_owners[0]==&first);
#else
static_assert(sizeof(radio_interrupt_owners)==2*sizeof(RadioLibWrapper*),"external radio retains two slots");
assert(second.registerInterruptAction()&&second._interrupt_slot==1);
assert(second.chip.callback!=first.chip.callback);
assert(!third.registerInterruptAction()&&third._interrupt_slot==0xff);
second.unregisterInterruptAction();
#endif
first.chip.callback();assert(first.hasPendingRadioInterrupt());
assert(first.registerInterruptAction()&&!first.hasPendingRadioInterrupt());
void(*late)()=first.chip.callback;first.unregisterInterruptAction();late();
assert(second.registerInterruptAction()&&!second.hasPendingRadioInterrupt());
assert(!first.hasPendingRadioInterrupt());
second.chip.callback();assert(second.hasPendingRadioInterrupt());
second.unregisterInterruptAction();
for(auto* owner:radio_interrupt_owners)assert(!owner);
}
'''
for defines in ([], ["-DRADIO_CLASS=CustomSTM32WLx"],
["-DSTM32_PLATFORM=1", "-DRADIO_CLASS=CustomSX1262"]):
compile_run(checks, defines)
def test_shared_phy_rejected_registration_preserves_owner_callback(self):
checks = self.source() + r'''
int main(){
auto* owner=new CustomSTM32WLxWrapper;
auto* filler=new CustomSTM32WLxWrapper;
assert(owner->registerInterruptAction());
#if RADIO_INTERRUPT_SLOT_COUNT > 1
assert(filler->registerInterruptAction());
#endif
auto* denied=new CustomSTM32WLxWrapper;
denied->_radio=owner->_radio; // same integrated PhysicalLayer, distinct wrapper
denied->_interrupt_slot=0; // stale slot is not ownership
void(*active)()=owner->chip.callback;
assert(!denied->registerInterruptAction()&&denied->_interrupt_slot==0xff);
assert(owner->chip.callback==active&&radio_interrupt_owners[0]==owner);
active();assert(owner->hasPendingRadioInterrupt());
delete denied; // denied concrete destruction must not detach the owner's PHY
assert(owner->chip.callback==active&&radio_interrupt_owners[0]==owner);
assert(owner->claimRadioInterrupt());active();assert(owner->hasPendingRadioInterrupt());
delete filler;delete owner;
for(auto* slot_owner:radio_interrupt_owners)assert(!slot_owner);
}
'''
for defines in ([], ["-DRADIO_CLASS=CustomSTM32WLx"],
["-DSTM32_PLATFORM=1", "-DRADIO_CLASS=CustomSX1262"]):
compile_run(checks, defines)
def test_lr2021_fifo_guards_stopped_snapshot_cleanup_and_recovery(self):
compile_run(self.source() + r'''
int main(){
CustomLR2021Wrapper w;assert(w.registerInterruptAction());uint8_t bytes[16]={};
for(int fifo:{0,3,5}){
w.chip.rx_status=1;w.chip.fifo=fifo;w.chip.callback();
const unsigned prior=w.chip.reads;
assert(w.recvRaw(bytes,sizeof(bytes))==0&&w.chip.reads==prior);
assert(w.chip.fifo==0&&!w.chip.stopped&&w.state==STATE_RX);
}
w.chip.rx_status=1;w.chip.fifo=4;w.chip.fifo_status=-99;w.chip.callback();
assert(w.recvRaw(bytes,sizeof(bytes))==0&&w.chip.irq_clears==4&&w.chip.fifo==0);
w.chip.fifo_status=0;w.chip.rx_status=1;w.chip.fifo=4;
w.chip.inject_read_irq=true;w.chip.callback();
assert(w.recvRaw(bytes,sizeof(bytes))==4&&w.n_recv==1&&!w.hasPendingRadioInterrupt());
assert(w.chip.arms==5&&w.chip.irq_clears==5&&!w.chip.stopped);
w.chip.rx_status=1;w.chip.fifo=4;w.chip.base_status=-88;w.chip.callback();
assert(w.recvRaw(bytes,sizeof(bytes))==0&&w.n_recv_errors==5);
w.chip.base_status=0;w.chip.rx_status=1;w.chip.fifo=4;w.chip.callback();
assert(w.recvRaw(bytes,sizeof(bytes))==4&&w.n_recv==2);
// Zero-length metadata (including a silently failed status read) discards
// its suspect FIFO before the next receive window.
w.chip.packet_length=0;w.chip.fifo=8;w.chip.rx_status=1;w.chip.callback();
assert(w.recvRaw(bytes,sizeof(bytes))==0&&w.chip.fifo==0&&!w.chip.stopped);
w.chip.packet_length=4;
// A failed standby never clears FIFO/IRQ or rearms as though stopped.
w.chip.standby_status=-55;w.chip.fifo=4;w.chip.rx_status=1;w.chip.callback();
const unsigned armed=w.chip.arms,cleared=w.chip.irq_clears;
assert(w.recvRaw(bytes,sizeof(bytes))==0&&w.chip.fifo==4);
assert(w.chip.arms==armed&&w.chip.irq_clears==cleared);
assert(w.recvRaw(bytes,sizeof(bytes))==0&&w.chip.arms==armed);
w.chip.standby_status=0;w.chip.clear_status=-44;
assert(w.recvRaw(bytes,sizeof(bytes))==0&&w.chip.arms==armed); // discard failure also blocks Set_RX
w.chip.clear_status=0;w.recvRaw(bytes,sizeof(bytes));assert(w.chip.fifo==0&&w.chip.arms==armed+1);
w.chip.rx_status=1;w.chip.fifo=4;w.chip.callback();
assert(w.recvRaw(bytes,sizeof(bytes))==4&&w.n_recv==3);
// A stale header CRC flag must not hide a newly valid header/preamble.
w.chip.irq=RADIOLIB_LR2021_IRQ_LORA_HEADER_VALID|RADIOLIB_LR2021_IRQ_LORA_HDR_CRC_ERROR;
assert(w.chip.isReceiving());
w.chip.irq=RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED;
assert(w.chip.isReceiving());
w.chip.irq=0;assert(!w.chip.isReceiving());
}
''', ["-DUSE_LR2021=1"])
if __name__ == "__main__":
unittest.main()