#![allow(clippy::unwrap_used, clippy::panic, clippy::bool_assert_comparison)]
use donglora_protocol::{
Command, CommandEncodeError, CommandParseError, DeviceMessage, DeviceMessageParseError,
ErrorCode, FrameDecodeError, FrameDecoder, FrameResult, Info, InfoParseError, LoRaBandwidth,
LoRaCodingRate, LoRaConfig, LoRaHeaderMode, MAX_MCU_UID_LEN, MAX_OK_PAYLOAD, MAX_OTA_PAYLOAD,
MAX_PAYLOAD_FIELD, MAX_PRE_COBS_FRAME, MAX_RADIO_UID_LEN, MAX_SETCONFIG_OK_PAYLOAD,
MAX_WIRE_FRAME, Modulation, OkPayload, Owner, RxOrigin, RxPayload, SetConfigResult,
SetConfigResultCode, TxDonePayload, TxFlags, TxResult, cap, commands, crc::crc16, encode_frame,
events,
};
use heapless::Vec as HVec;
#[test]
fn crc16_check_value_is_0x29b1() {
assert_eq!(crc16(b"123456789"), 0x29B1);
}
#[test]
fn crc16_single_null_byte_not_zero() {
assert_eq!(crc16(&[0x00]), 0xE1F0);
}
#[test]
fn crc16_empty_input_is_initial_value() {
assert_eq!(crc16(&[]), 0xFFFF);
}
#[test]
fn encode_then_decode_flipped_crc_byte_fails() {
let mut wire = [0u8; MAX_WIRE_FRAME];
let n = encode_frame(commands::TYPE_PING, 0x0042, &[], &mut wire).unwrap();
wire[n - 2] ^= 0x01;
let mut decoder = FrameDecoder::new();
let mut saw_err = false;
decoder.feed(&wire[..n], |res| {
if matches!(res, FrameResult::Err(_)) {
saw_err = true;
}
});
assert!(saw_err, "CRC-corrupted frame must produce FrameResult::Err");
}
#[test]
fn decoder_rejects_frame_exactly_4_bytes_post_cobs() {
let wire = [0x05, 0xAA, 0xBB, 0xCC, 0xDD, 0x00];
let mut decoder = FrameDecoder::new();
let mut saw_too_short = false;
decoder.feed(&wire, |res| {
if matches!(res, FrameResult::Err(FrameDecodeError::TooShort)) {
saw_too_short = true;
}
});
assert!(saw_too_short);
}
#[test]
fn decoder_accepts_frame_exactly_5_bytes_post_cobs() {
let mut wire = [0u8; MAX_WIRE_FRAME];
let n = encode_frame(0x01, 0x0001, &[], &mut wire).unwrap();
let mut decoder = FrameDecoder::new();
let mut saw_ok = false;
decoder.feed(&wire[..n], |res| {
if matches!(res, FrameResult::Ok { .. }) {
saw_ok = true;
}
});
assert!(saw_ok);
}
#[test]
fn tx_flags_reserved_bits_each_rejected() {
for bit in 1..8 {
let byte = 1u8 << bit;
assert!(
matches!(
TxFlags::from_byte(byte),
Err(CommandParseError::ReservedBitSet)
),
"bit {bit} must be rejected",
);
}
assert_eq!(TxFlags::from_byte(0b0000_0001).unwrap().skip_cad, true);
}
#[test]
fn tx_parse_rejects_only_flags_byte() {
assert!(matches!(
Command::parse(commands::TYPE_TX, &[0x00]),
Err(CommandParseError::WrongLength)
));
}
#[test]
fn tx_parse_accepts_one_data_byte() {
let got = Command::parse(commands::TYPE_TX, &[0x00, 0x42]).unwrap();
match got {
Command::Tx { flags, data } => {
assert_eq!(flags.skip_cad, false);
assert_eq!(data.as_slice(), &[0x42]);
}
other => panic!("expected Tx, got {:?}", other),
}
}
#[test]
fn tx_parse_rejects_payload_exceeding_max_ota() {
let mut bytes: HVec<u8, { MAX_OTA_PAYLOAD + 2 }> = HVec::new();
bytes.push(0x00).unwrap(); for _ in 0..=MAX_OTA_PAYLOAD {
bytes.push(0x55).unwrap();
}
assert!(matches!(
Command::parse(commands::TYPE_TX, &bytes),
Err(CommandParseError::WrongLength)
));
}
#[test]
fn cobs_254_byte_nonzero_run() {
let mut big: HVec<u8, MAX_OTA_PAYLOAD> = HVec::new();
for i in 1..=253u8 {
big.push(i).unwrap();
}
let mut wire = [0u8; MAX_WIRE_FRAME];
let n = encode_frame(0x04, 0x0101, &big, &mut wire).unwrap();
let mut decoder = FrameDecoder::new();
let mut seen: Option<(u8, u16, HVec<u8, MAX_PAYLOAD_FIELD>)> = None;
decoder.feed(&wire[..n], |res| {
if let FrameResult::Ok {
type_id,
tag,
payload,
} = res
{
let mut p = HVec::new();
p.extend_from_slice(payload).unwrap();
seen = Some((type_id, tag, p));
}
});
let (ty, tag, payload) = seen.unwrap();
assert_eq!(ty, 0x04);
assert_eq!(tag, 0x0101);
assert_eq!(payload.len(), big.len());
assert_eq!(payload.as_slice(), big.as_slice());
}
#[test]
fn decoder_handles_split_feed() {
let mut wire = [0u8; MAX_WIRE_FRAME];
let n = encode_frame(0x01, 0x0001, &[], &mut wire).unwrap();
assert!(n >= 3);
let mut decoder = FrameDecoder::new();
let mut count = 0;
decoder.feed(&wire[..n / 2], |_| {
count += 1;
});
assert_eq!(count, 0, "no 0x00 in first chunk → no frame yet");
decoder.feed(&wire[n / 2..n], |res| {
if matches!(res, FrameResult::Ok { .. }) {
count += 1;
}
});
assert_eq!(count, 1, "second chunk contains the 0x00 sentinel");
}
#[test]
fn tag_byte_order_is_le() {
let mut wire = [0u8; MAX_WIRE_FRAME];
let n = encode_frame(0x01, 0x0102, &[], &mut wire).unwrap();
let mut decoder = FrameDecoder::new();
let mut seen_tag = None;
decoder.feed(&wire[..n], |res| {
if let FrameResult::Ok { tag, .. } = res {
seen_tag = Some(tag);
}
});
assert_eq!(seen_tag, Some(0x0102));
}
#[test]
fn max_pre_cobs_frame_pins_exactly_280() {
assert_eq!(MAX_PRE_COBS_FRAME, 280);
}
#[test]
fn max_wire_frame_pins_exactly_284() {
assert_eq!(MAX_WIRE_FRAME, 284);
}
#[test]
fn max_ok_payload_pins_exactly_85() {
assert_eq!(MAX_OK_PAYLOAD, 85);
}
#[test]
fn max_setconfig_ok_payload_pins_exactly_27() {
assert_eq!(MAX_SETCONFIG_OK_PAYLOAD, 27);
}
fn tx_cmd(data_len: usize) -> Command {
let mut d: HVec<u8, MAX_OTA_PAYLOAD> = HVec::new();
for i in 0..data_len {
d.push(i as u8).unwrap();
}
Command::Tx {
flags: TxFlags::default(),
data: d,
}
}
#[test]
fn tx_encode_accepts_exactly_max_ota_payload() {
let cmd = tx_cmd(MAX_OTA_PAYLOAD);
let mut buf = [0u8; MAX_OTA_PAYLOAD + 8];
assert!(cmd.encode_payload(&mut buf).is_ok());
}
#[test]
fn tx_encode_rejects_max_ota_payload_plus_one() {
let mut bytes: HVec<u8, { MAX_OTA_PAYLOAD + 2 }> = HVec::new();
bytes.push(0x00).unwrap();
for _ in 0..=MAX_OTA_PAYLOAD {
bytes.push(0x42).unwrap();
}
assert!(matches!(
Command::parse(commands::TYPE_TX, &bytes),
Err(CommandParseError::WrongLength)
));
}
#[test]
fn tx_encode_rejects_buffer_exactly_one_byte_too_small() {
let cmd = tx_cmd(5);
let mut exact = [0u8; 6];
assert!(cmd.encode_payload(&mut exact).is_ok());
let mut too_small = [0u8; 5];
assert!(matches!(
cmd.encode_payload(&mut too_small),
Err(CommandEncodeError::BufferTooSmall)
));
}
#[test]
fn tx_parse_body_exactly_max_ota_payload() {
let mut bytes: HVec<u8, { MAX_OTA_PAYLOAD + 2 }> = HVec::new();
bytes.push(0x00).unwrap();
for _ in 0..MAX_OTA_PAYLOAD {
bytes.push(0x42).unwrap();
}
let cmd = Command::parse(commands::TYPE_TX, &bytes).unwrap();
match cmd {
Command::Tx { data, .. } => assert_eq!(data.len(), MAX_OTA_PAYLOAD),
_ => panic!("expected Tx"),
}
}
#[test]
fn set_config_result_code_as_u8_exact() {
assert_eq!(SetConfigResultCode::Applied.as_u8(), 0);
assert_eq!(SetConfigResultCode::AlreadyMatched.as_u8(), 1);
assert_eq!(SetConfigResultCode::LockedMismatch.as_u8(), 2);
}
#[test]
fn set_config_result_code_from_u8_exact() {
assert_eq!(
SetConfigResultCode::from_u8(0),
Some(SetConfigResultCode::Applied),
);
assert_eq!(
SetConfigResultCode::from_u8(1),
Some(SetConfigResultCode::AlreadyMatched),
);
assert_eq!(
SetConfigResultCode::from_u8(2),
Some(SetConfigResultCode::LockedMismatch),
);
assert_eq!(SetConfigResultCode::from_u8(3), None);
}
#[test]
fn owner_as_u8_exact() {
assert_eq!(Owner::None.as_u8(), 0);
assert_eq!(Owner::Mine.as_u8(), 1);
assert_eq!(Owner::Other.as_u8(), 2);
}
#[test]
fn owner_from_u8_exact() {
assert_eq!(Owner::from_u8(0), Some(Owner::None));
assert_eq!(Owner::from_u8(1), Some(Owner::Mine));
assert_eq!(Owner::from_u8(2), Some(Owner::Other));
assert_eq!(Owner::from_u8(3), None);
}
fn sample_lora_for_scr() -> LoRaConfig {
LoRaConfig {
freq_hz: 915_000_000,
sf: 7,
bw: LoRaBandwidth::Khz125,
cr: LoRaCodingRate::Cr4_5,
preamble_len: 8,
sync_word: 0x1424,
tx_power_dbm: 14,
header_mode: LoRaHeaderMode::Explicit,
payload_crc: true,
iq_invert: false,
}
}
#[test]
fn set_config_result_encode_rejects_exactly_one_byte_buf() {
let r = SetConfigResult {
result: SetConfigResultCode::Applied,
owner: Owner::Mine,
current: Modulation::LoRa(sample_lora_for_scr()),
};
let mut one = [0u8; 1];
assert!(r.encode(&mut one).is_err());
let mut enough = [0u8; 32];
assert!(r.encode(&mut enough).is_ok());
}
#[test]
fn set_config_result_decode_rejects_exactly_one_byte_buf() {
assert!(matches!(
SetConfigResult::decode(&[]),
Err(DeviceMessageParseError::TooShort)
));
assert!(matches!(
SetConfigResult::decode(&[0x00]),
Err(DeviceMessageParseError::TooShort)
));
}
#[test]
fn ok_payload_parses_get_info() {
let mut info_buf = [0u8; 128];
let info = sample_info();
let n = info.encode(&mut info_buf).unwrap();
let parsed = OkPayload::parse_for(commands::TYPE_GET_INFO, &info_buf[..n]).unwrap();
assert!(matches!(parsed, OkPayload::Info(_)));
}
#[test]
fn ok_payload_parses_set_config() {
let r = SetConfigResult {
result: SetConfigResultCode::Applied,
owner: Owner::Mine,
current: Modulation::LoRa(sample_lora_for_scr()),
};
let mut buf = [0u8; 64];
let n = r.encode(&mut buf).unwrap();
let parsed = OkPayload::parse_for(commands::TYPE_SET_CONFIG, &buf[..n]).unwrap();
assert!(matches!(parsed, OkPayload::SetConfig(_)));
}
#[test]
fn device_message_parses_err() {
let mut err_buf = [0u8; 2];
events::encode_err_payload(ErrorCode::EBusy, &mut err_buf).unwrap();
let parsed = DeviceMessage::parse(events::TYPE_ERR, &err_buf, None).unwrap();
assert!(matches!(parsed, DeviceMessage::Err(ErrorCode::EBusy)));
}
#[test]
fn device_message_parses_tx_done() {
let td = TxDonePayload {
result: TxResult::Transmitted,
airtime_us: 42,
};
let mut buf = [0u8; 8];
let n = td.encode(&mut buf).unwrap();
let parsed = DeviceMessage::parse(events::TYPE_TX_DONE, &buf[..n], None).unwrap();
assert!(matches!(
parsed,
DeviceMessage::TxDone(TxDonePayload { airtime_us: 42, .. })
));
}
#[test]
fn rx_payload_decode_subtracts_metadata_size() {
let mut meta = [0u8; 20];
meta[0..2].copy_from_slice(&(-100i16).to_le_bytes());
meta[16] = 1; meta[19] = 0; let mut buf: HVec<u8, 64> = HVec::new();
buf.extend_from_slice(&meta).unwrap();
buf.extend_from_slice(&[0x01u8; 20]).unwrap();
let decoded = RxPayload::decode(&buf).unwrap();
assert_eq!(decoded.data.len(), 20);
}
#[test]
fn frame_decoder_reset_clears_accumulator() {
let mut decoder = FrameDecoder::new();
decoder.feed(&[0xAA, 0xBB, 0xCC, 0xDD], |_| {});
decoder.reset();
let mut wire = [0u8; MAX_WIRE_FRAME];
let n = encode_frame(0x01, 0x0001, &[], &mut wire).unwrap();
let mut saw_ok = false;
decoder.feed(&wire[..n], |res| {
if matches!(res, FrameResult::Ok { .. }) {
saw_ok = true;
}
});
assert!(saw_ok, "reset() must clear stale bytes");
}
#[test]
fn frame_decoder_stray_sentinel_does_not_emit() {
let mut decoder = FrameDecoder::new();
let mut saw_any = false;
decoder.feed(&[0x00], |_| {
saw_any = true;
});
assert!(
!saw_any,
"stray 0x00 with empty buffer must not produce any frame"
);
}
#[test]
fn cap_bit_values_pinned() {
assert_eq!(cap::LORA, 1);
assert_eq!(cap::FSK, 2);
assert_eq!(cap::GFSK, 4);
assert_eq!(cap::LR_FHSS, 8);
assert_eq!(cap::FLRC, 16);
assert_eq!(cap::MSK, 32);
assert_eq!(cap::GMSK, 64);
assert_eq!(cap::BLE_COMPATIBLE, 128);
assert_eq!(cap::CAD_BEFORE_TX, 0x0001_0000);
assert_eq!(cap::IQ_INVERSION, 0x0002_0000);
assert_eq!(cap::RANGING, 0x0004_0000);
assert_eq!(cap::GNSS_SCAN, 0x0008_0000);
assert_eq!(cap::WIFI_MAC_SCAN, 0x0010_0000);
assert_eq!(cap::SPECTRAL_SCAN, 0x0020_0000);
assert_eq!(cap::FULL_DUPLEX, 0x0040_0000);
assert_eq!(cap::MULTI_CLIENT, 0x0000_0001_0000_0000);
}
fn sample_info() -> Info {
let mut mcu = [0u8; MAX_MCU_UID_LEN];
mcu[..8].copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x23, 0x45, 0x67]);
Info {
proto_major: 1,
proto_minor: 0,
fw_major: 0,
fw_minor: 1,
fw_patch: 0,
radio_chip_id: 0x0002,
capability_bitmap: cap::LORA | cap::CAD_BEFORE_TX,
supported_sf_bitmap: 0x1FE0,
supported_bw_bitmap: 0x03FF,
max_payload_bytes: 255,
rx_queue_capacity: 32,
tx_queue_capacity: 1,
freq_min_hz: 150_000_000,
freq_max_hz: 960_000_000,
tx_power_min_dbm: -9,
tx_power_max_dbm: 22,
mcu_uid_len: 8,
mcu_uid: mcu,
radio_uid_len: 0,
radio_uid: [0u8; MAX_RADIO_UID_LEN],
}
}
#[test]
fn info_encode_rejects_buffer_exactly_one_byte_too_small() {
let i = sample_info();
let mut exact = [0u8; 45];
assert!(i.encode(&mut exact).is_ok());
let mut too_small = [0u8; 44];
assert!(i.encode(&mut too_small).is_err());
}
#[test]
fn info_decode_rejects_mcu_uid_len_overrun() {
let i = sample_info();
let mut buf = [0u8; 128];
let n = i.encode(&mut buf).unwrap();
buf[35] = 30;
assert!(matches!(
Info::decode(&buf[..n]),
Err(donglora_protocol::InfoParseError::TooShort)
));
}
#[test]
fn info_decode_offsets_are_correct() {
let mut i = sample_info();
let mut radio_uid = [0u8; MAX_RADIO_UID_LEN];
radio_uid[..4].copy_from_slice(&[0xCA, 0xFE, 0xBA, 0xBE]);
i.radio_uid_len = 4;
i.radio_uid = radio_uid;
let mut buf = [0u8; 128];
let n = i.encode(&mut buf).unwrap();
let decoded = Info::decode(&buf[..n]).unwrap();
assert_eq!(
&decoded.mcu_uid[..8],
&[0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x23, 0x45, 0x67]
);
assert_eq!(&decoded.radio_uid[..4], &[0xCA, 0xFE, 0xBA, 0xBE]);
}
#[test]
fn info_min_wire_size_pinned() {
assert_eq!(Info::MIN_WIRE_SIZE, 37);
}
#[test]
fn fsk_encode_rejects_buffer_exactly_one_byte_too_small() {
let mut cfg = donglora_protocol::FskConfig {
freq_hz: 0,
bitrate_bps: 0,
freq_dev_hz: 0,
rx_bw: 0,
preamble_len: 0,
sync_word_len: 4,
sync_word: [0u8; donglora_protocol::MAX_SYNC_WORD_LEN],
};
cfg.sync_word[..4].copy_from_slice(&[1, 2, 3, 4]);
let mut exact = [0u8; 20];
assert!(cfg.encode(&mut exact).is_ok());
let mut too_small = [0u8; 19];
assert!(cfg.encode(&mut too_small).is_err());
}
#[test]
fn encode_frame_rejects_output_buffer_exactly_one_byte_too_small() {
let mut generous = [0u8; MAX_WIRE_FRAME];
let n = encode_frame(0x01, 0x0001, b"test", &mut generous).unwrap();
assert!(n >= 3);
let mut exact = [0u8; MAX_WIRE_FRAME];
assert!(encode_frame(0x01, 0x0001, b"test", &mut exact[..n]).is_ok());
assert!(encode_frame(0x01, 0x0001, b"test", &mut exact[..n - 1]).is_err());
}
#[test]
fn max_wire_frame_minus_one_leaves_room_for_sentinel() {
let big = [0x42u8; MAX_PAYLOAD_FIELD];
let mut wire = [0u8; MAX_WIRE_FRAME];
let n = encode_frame(0xC0, 0x0000, &big, &mut wire).unwrap();
assert!(n <= MAX_WIRE_FRAME);
assert!(n > MAX_PAYLOAD_FIELD);
}
#[test]
fn parse_unknown_type_with_empty_payload() {
assert!(matches!(
Command::parse(0x10, &[]),
Err(CommandParseError::UnknownType)
));
}
#[test]
fn set_config_result_encode_writes_header_before_modulation_fails() {
let r = SetConfigResult {
result: SetConfigResultCode::LockedMismatch,
owner: Owner::Other,
current: Modulation::LoRa(sample_lora_for_scr()),
};
let mut buf = [0xFFu8; 2];
let res = r.encode(&mut buf);
assert!(res.is_err(), "modulation encode needs more than 2 bytes");
assert_eq!(
buf[0],
SetConfigResultCode::LockedMismatch.as_u8(),
"result byte must be written before modulation encode fails",
);
assert_eq!(
buf[1],
Owner::Other.as_u8(),
"owner byte must be written before modulation encode fails",
);
}
#[test]
fn set_config_result_decode_two_byte_buf_is_wrong_length_not_too_short() {
let res = SetConfigResult::decode(&[0x00, 0x01]);
assert!(
matches!(res, Err(DeviceMessageParseError::WrongLength)),
"decode of a 2-byte buffer must surface WrongLength from the inner \
Modulation::decode call, not TooShort from the guard",
);
}
#[test]
fn rx_payload_encode_accepts_exactly_max_ota_payload() {
let mut data: HVec<u8, MAX_OTA_PAYLOAD> = HVec::new();
for i in 0..MAX_OTA_PAYLOAD {
data.push(i as u8).unwrap();
}
let rx = RxPayload {
rssi_tenths_dbm: -735,
snr_tenths_db: 95,
freq_err_hz: 0,
timestamp_us: 0,
crc_valid: true,
packets_dropped: 0,
origin: RxOrigin::Ota,
data,
};
let mut buf = [0u8; 20 + MAX_OTA_PAYLOAD];
let n = rx.encode(&mut buf).unwrap();
assert_eq!(n, 20 + MAX_OTA_PAYLOAD);
}
#[test]
fn rx_payload_encode_accepts_buffer_exactly_equal_to_total() {
let mut data: HVec<u8, MAX_OTA_PAYLOAD> = HVec::new();
data.extend_from_slice(&[0x01, 0x02, 0x03, 0x04]).unwrap();
let rx = RxPayload {
rssi_tenths_dbm: -735,
snr_tenths_db: 95,
freq_err_hz: -125,
timestamp_us: 42_000_000,
crc_valid: true,
packets_dropped: 0,
origin: RxOrigin::Ota,
data,
};
let mut buf = [0u8; 24];
let n = rx.encode(&mut buf).unwrap();
assert_eq!(n, 24);
}
#[test]
fn rx_payload_decode_accepts_exactly_max_ota_payload() {
let mut buf = [0u8; 20 + MAX_OTA_PAYLOAD];
buf[16] = 1; buf[19] = 0; for (i, b) in buf[20..].iter_mut().enumerate() {
*b = i as u8;
}
let decoded = RxPayload::decode(&buf).unwrap();
assert_eq!(decoded.data.len(), MAX_OTA_PAYLOAD);
assert_eq!(decoded.data[0], 0);
assert_eq!(
decoded.data[MAX_OTA_PAYLOAD - 1],
(MAX_OTA_PAYLOAD - 1) as u8
);
}
#[test]
fn rx_payload_decode_accepts_exactly_metadata_size_no_data() {
let mut buf = [0u8; 20];
buf[16] = 1; buf[19] = 0; let decoded = RxPayload::decode(&buf).unwrap();
assert_eq!(decoded.data.len(), 0);
}
#[test]
fn rx_payload_decode_rejects_one_byte_below_metadata_size() {
let buf = [0u8; 19];
assert!(matches!(
RxPayload::decode(&buf),
Err(DeviceMessageParseError::TooShort),
));
}
#[test]
fn rx_payload_decode_n_is_used_for_data_slice() {
let mut buf = [0u8; 40];
buf[16] = 1; buf[19] = 0; for (i, b) in buf[20..40].iter_mut().enumerate() {
*b = i as u8;
}
let decoded = RxPayload::decode(&buf).unwrap();
assert_eq!(decoded.data.len(), 20);
assert_eq!(decoded.data[0], 0);
assert_eq!(decoded.data[19], 19);
}
fn info_sample() -> Info {
let mut mcu = [0u8; MAX_MCU_UID_LEN];
mcu[..8].copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x23, 0x45, 0x67]);
Info {
proto_major: 1,
proto_minor: 0,
fw_major: 0,
fw_minor: 1,
fw_patch: 0,
radio_chip_id: 0x0002,
capability_bitmap: cap::LORA | cap::CAD_BEFORE_TX,
supported_sf_bitmap: 0x1FE0,
supported_bw_bitmap: 0x03FF,
max_payload_bytes: 255,
rx_queue_capacity: 32,
tx_queue_capacity: 1,
freq_min_hz: 150_000_000,
freq_max_hz: 960_000_000,
tx_power_min_dbm: -9,
tx_power_max_dbm: 22,
mcu_uid_len: 8,
mcu_uid: mcu,
radio_uid_len: 0,
radio_uid: [0u8; MAX_RADIO_UID_LEN],
}
}
#[test]
fn info_decode_accepts_exactly_min_wire_size() {
let mut i = info_sample();
i.mcu_uid_len = 0;
i.radio_uid_len = 0;
let mut buf = [0u8; 64];
let n = i.encode(&mut buf).unwrap();
assert_eq!(n, 37);
let decoded = Info::decode(&buf[..n]).unwrap();
assert_eq!(decoded.mcu_uid_len, 0);
assert_eq!(decoded.radio_uid_len, 0);
}
#[test]
fn info_decode_radio_len_idx_plus_one_guard_is_active() {
let mut buf = [0u8; 44];
buf[35] = 8; assert!(matches!(Info::decode(&buf), Err(InfoParseError::TooShort),));
}
#[test]
fn info_decode_expected_total_uses_both_plus_signs() {
let mut i = info_sample();
i.radio_uid_len = 4;
let mut big = [0u8; 128];
let n = i.encode(&mut big).unwrap();
assert_eq!(n, 49);
assert!(matches!(
Info::decode(&big[..45]),
Err(InfoParseError::TooShort),
));
}
#[test]
fn info_decode_tolerates_trailing_bytes() {
let i = info_sample();
let mut buf = [0u8; 64];
let n = i.encode(&mut buf).unwrap();
assert_eq!(n, 45);
for b in &mut buf[45..55] {
*b = 0xAA;
}
let decoded = Info::decode(&buf[..55]).unwrap();
assert_eq!(decoded, i);
}