Bootstrap DD3 Rust port workspace with host-first compatibility tests
This commit is contained in:
347
crates/dd3_protocol/tests/protocol_tests.rs
Normal file
347
crates/dd3_protocol/tests/protocol_tests.rs
Normal file
@@ -0,0 +1,347 @@
|
||||
use dd3_protocol::{
|
||||
crc16_ccitt, decode_ack_down_payload, decode_batch_v3, decode_frame, encode_ack_down_payload,
|
||||
encode_batch_v3, encode_frame, push_chunk, AckDownPayload, BatchInputV3, FrameDecodeError, MsgKind,
|
||||
ReassemblyState, ReassemblyStatus,
|
||||
};
|
||||
|
||||
const FIX_FRAME_OK: &[u8] = include_bytes!("../../../fixtures/protocol/frames/batchup_f19c_payload_0102a5.bin");
|
||||
const FIX_FRAME_BAD_CRC: &[u8] =
|
||||
include_bytes!("../../../fixtures/protocol/frames/batchup_f19c_payload_0102a5_bad_crc.bin");
|
||||
|
||||
const FIX_CHUNK_OK: &[u8] = include_bytes!("../../../fixtures/protocol/chunks/in_order_ok.bin");
|
||||
const FIX_CHUNK_MISSING: &[u8] = include_bytes!("../../../fixtures/protocol/chunks/missing_chunk.bin");
|
||||
const FIX_CHUNK_OUT_OF_ORDER: &[u8] =
|
||||
include_bytes!("../../../fixtures/protocol/chunks/out_of_order_start.bin");
|
||||
const FIX_CHUNK_WRONG_LEN: &[u8] =
|
||||
include_bytes!("../../../fixtures/protocol/chunks/wrong_total_len.bin");
|
||||
|
||||
const FIX_SYNC_EMPTY: &[u8] = include_bytes!("../../../fixtures/protocol/payload_v3/sync_empty.bin");
|
||||
const FIX_SPARSE_5: &[u8] = include_bytes!("../../../fixtures/protocol/payload_v3/sparse_5.bin");
|
||||
const FIX_FULL_30: &[u8] = include_bytes!("../../../fixtures/protocol/payload_v3/full_30.bin");
|
||||
|
||||
fn parse_chunk_records(bytes: &[u8]) -> Vec<(u16, u8, u8, u16, Vec<u8>)> {
|
||||
let mut out = Vec::new();
|
||||
let mut pos = 0usize;
|
||||
while pos < bytes.len() {
|
||||
let batch_id = u16::from_le_bytes([bytes[pos], bytes[pos + 1]]);
|
||||
pos += 2;
|
||||
let idx = bytes[pos];
|
||||
pos += 1;
|
||||
let count = bytes[pos];
|
||||
pos += 1;
|
||||
let total_len = u16::from_le_bytes([bytes[pos], bytes[pos + 1]]);
|
||||
pos += 2;
|
||||
let chunk_len = bytes[pos] as usize;
|
||||
pos += 1;
|
||||
let chunk = bytes[pos..pos + chunk_len].to_vec();
|
||||
pos += chunk_len;
|
||||
out.push((batch_id, idx, count, total_len, chunk));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn fill_sparse_batch() -> BatchInputV3 {
|
||||
let mut input = BatchInputV3::default();
|
||||
input.sender_id = 1;
|
||||
input.batch_id = 42;
|
||||
input.t_last = 1_700_000_000;
|
||||
input.present_mask = (1u32 << 0) | (1u32 << 2) | (1u32 << 3) | (1u32 << 10) | (1u32 << 29);
|
||||
input.n = 5;
|
||||
input.battery_mv = 3750;
|
||||
input.err_m = 2;
|
||||
input.err_d = 1;
|
||||
input.err_tx = 3;
|
||||
input.err_last = 2;
|
||||
input.err_rx_reject = 1;
|
||||
|
||||
input.energy_wh[0] = 100_000;
|
||||
input.energy_wh[1] = 100_001;
|
||||
input.energy_wh[2] = 100_050;
|
||||
input.energy_wh[3] = 100_050;
|
||||
input.energy_wh[4] = 100_200;
|
||||
|
||||
input.p1_w[0] = -120;
|
||||
input.p1_w[1] = -90;
|
||||
input.p1_w[2] = 1910;
|
||||
input.p1_w[3] = -90;
|
||||
input.p1_w[4] = 500;
|
||||
|
||||
input.p2_w[0] = 50;
|
||||
input.p2_w[1] = -1950;
|
||||
input.p2_w[2] = 60;
|
||||
input.p2_w[3] = 2060;
|
||||
input.p2_w[4] = -10;
|
||||
|
||||
input.p3_w[0] = 0;
|
||||
input.p3_w[1] = 10;
|
||||
input.p3_w[2] = -1990;
|
||||
input.p3_w[3] = 10;
|
||||
input.p3_w[4] = 20;
|
||||
input
|
||||
}
|
||||
|
||||
fn fill_full_batch() -> BatchInputV3 {
|
||||
let mut input = BatchInputV3::default();
|
||||
input.sender_id = 1;
|
||||
input.batch_id = 0xBEEF;
|
||||
input.t_last = 1_769_904_999;
|
||||
input.present_mask = 0x3FFF_FFFF;
|
||||
input.n = 30;
|
||||
input.battery_mv = 4095;
|
||||
input.err_m = 10;
|
||||
input.err_d = 20;
|
||||
input.err_tx = 30;
|
||||
input.err_last = 3;
|
||||
input.err_rx_reject = 6;
|
||||
|
||||
for i in 0..30usize {
|
||||
input.energy_wh[i] = 500_000 + (i as u32 * i as u32 * 3);
|
||||
input.p1_w[i] = -1000 + (i as i16 * 25);
|
||||
input.p2_w[i] = 500 - (i as i16 * 30);
|
||||
input.p3_w[i] = if i % 2 == 0 {
|
||||
100 + (i as i16 * 5)
|
||||
} else {
|
||||
-100 + (i as i16 * 5)
|
||||
};
|
||||
}
|
||||
|
||||
input
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crc16_known_vectors() {
|
||||
assert_eq!(0x29B1, crc16_ccitt(b"123456789"));
|
||||
assert_eq!(0x1C0F, crc16_ccitt(&[0x00, 0x01, 0x02, 0x03, 0x04]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_encode_decode_and_crc_reject() {
|
||||
let encoded = encode_frame(MsgKind::BatchUp, 0xF19C, &[0x01, 0x02, 0xA5]);
|
||||
assert_eq!(FIX_FRAME_OK, encoded.as_slice());
|
||||
|
||||
let frame = decode_frame(FIX_FRAME_OK, MsgKind::AckDown as u8).unwrap();
|
||||
assert_eq!(MsgKind::BatchUp, frame.msg_kind);
|
||||
assert_eq!(0xF19C, frame.short_id);
|
||||
assert_eq!(&[0x01, 0x02, 0xA5], frame.payload.as_slice());
|
||||
|
||||
let err = decode_frame(FIX_FRAME_BAD_CRC, MsgKind::AckDown as u8).unwrap_err();
|
||||
assert_eq!(FrameDecodeError::CrcFail, err);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_rejects_invalid_msg_kind_and_short_length() {
|
||||
let mut frame = encode_frame(MsgKind::BatchUp, 0xF19C, &[0x42]);
|
||||
frame[0] = 2;
|
||||
let crc = crc16_ccitt(&frame[..frame.len() - 2]);
|
||||
let n = frame.len();
|
||||
frame[n - 2] = (crc >> 8) as u8;
|
||||
frame[n - 1] = (crc & 0xFF) as u8;
|
||||
|
||||
let bad_kind = decode_frame(&frame, MsgKind::AckDown as u8).unwrap_err();
|
||||
assert_eq!(FrameDecodeError::InvalidMsgKind, bad_kind);
|
||||
|
||||
let short_len = decode_frame(&frame[..4], MsgKind::AckDown as u8).unwrap_err();
|
||||
assert_eq!(FrameDecodeError::LengthMismatch, short_len);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ack_payload_fixed_7byte_contract() {
|
||||
let payload = AckDownPayload {
|
||||
time_valid: true,
|
||||
batch_id: 0x1234,
|
||||
epoch_utc: 1_769_904_000,
|
||||
};
|
||||
let encoded = encode_ack_down_payload(payload);
|
||||
assert_eq!(7, encoded.len());
|
||||
assert_eq!(1, encoded[0]);
|
||||
assert_eq!([0x12, 0x34], [encoded[1], encoded[2]]);
|
||||
|
||||
let decoded = decode_ack_down_payload(&encoded).unwrap();
|
||||
assert_eq!(payload, decoded);
|
||||
|
||||
assert!(decode_ack_down_payload(&encoded[..6]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chunk_reassembly_in_order_success() {
|
||||
let records = parse_chunk_records(FIX_CHUNK_OK);
|
||||
let mut state = ReassemblyState::default();
|
||||
let mut buffer = [0u8; 32];
|
||||
|
||||
let mut status = ReassemblyStatus::InProgress;
|
||||
for (i, rec) in records.iter().enumerate() {
|
||||
status = push_chunk(
|
||||
&mut state,
|
||||
rec.0,
|
||||
rec.1,
|
||||
rec.2,
|
||||
rec.3,
|
||||
&rec.4,
|
||||
1000 + (i as u32 * 100),
|
||||
5000,
|
||||
32,
|
||||
&mut buffer,
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(ReassemblyStatus::Complete { complete_len: 7 }, status);
|
||||
assert_eq!(&[1, 2, 3, 4, 5, 6, 7], &buffer[..7]);
|
||||
assert!(!state.active);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chunk_reassembly_missing_or_out_of_order_fails_deterministically() {
|
||||
let records = parse_chunk_records(FIX_CHUNK_MISSING);
|
||||
let mut state = ReassemblyState::default();
|
||||
let mut buffer = [0u8; 32];
|
||||
|
||||
let s0 = push_chunk(
|
||||
&mut state,
|
||||
records[0].0,
|
||||
records[0].1,
|
||||
records[0].2,
|
||||
records[0].3,
|
||||
&records[0].4,
|
||||
1000,
|
||||
5000,
|
||||
32,
|
||||
&mut buffer,
|
||||
);
|
||||
assert_eq!(ReassemblyStatus::InProgress, s0);
|
||||
|
||||
let s1 = push_chunk(
|
||||
&mut state,
|
||||
records[1].0,
|
||||
records[1].1,
|
||||
records[1].2,
|
||||
records[1].3,
|
||||
&records[1].4,
|
||||
1100,
|
||||
5000,
|
||||
32,
|
||||
&mut buffer,
|
||||
);
|
||||
assert_eq!(ReassemblyStatus::ErrorReset, s1);
|
||||
assert!(!state.active);
|
||||
|
||||
let out_records = parse_chunk_records(FIX_CHUNK_OUT_OF_ORDER);
|
||||
let s2 = push_chunk(
|
||||
&mut state,
|
||||
out_records[0].0,
|
||||
out_records[0].1,
|
||||
out_records[0].2,
|
||||
out_records[0].3,
|
||||
&out_records[0].4,
|
||||
1200,
|
||||
5000,
|
||||
32,
|
||||
&mut buffer,
|
||||
);
|
||||
assert_eq!(ReassemblyStatus::ErrorReset, s2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chunk_reassembly_wrong_total_length_fails() {
|
||||
let records = parse_chunk_records(FIX_CHUNK_WRONG_LEN);
|
||||
let mut state = ReassemblyState::default();
|
||||
let mut buffer = [0u8; 8];
|
||||
|
||||
let s0 = push_chunk(
|
||||
&mut state,
|
||||
records[0].0,
|
||||
records[0].1,
|
||||
records[0].2,
|
||||
records[0].3,
|
||||
&records[0].4,
|
||||
1000,
|
||||
5000,
|
||||
8,
|
||||
&mut buffer,
|
||||
);
|
||||
assert_eq!(ReassemblyStatus::InProgress, s0);
|
||||
|
||||
let s1 = push_chunk(
|
||||
&mut state,
|
||||
records[1].0,
|
||||
records[1].1,
|
||||
records[1].2,
|
||||
records[1].3,
|
||||
&records[1].4,
|
||||
1100,
|
||||
5000,
|
||||
8,
|
||||
&mut buffer,
|
||||
);
|
||||
assert_eq!(ReassemblyStatus::ErrorReset, s1);
|
||||
assert!(!state.active);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn payload_golden_vectors_roundtrip() {
|
||||
let decoded_sync = decode_batch_v3(FIX_SYNC_EMPTY).unwrap();
|
||||
assert_eq!(1, decoded_sync.sender_id);
|
||||
assert_eq!(0x1234, decoded_sync.batch_id);
|
||||
assert_eq!(1_769_904_100, decoded_sync.t_last);
|
||||
assert_eq!(0, decoded_sync.present_mask);
|
||||
assert_eq!(0, decoded_sync.n);
|
||||
assert_eq!(3750, decoded_sync.battery_mv);
|
||||
|
||||
let decoded_sparse = decode_batch_v3(FIX_SPARSE_5).unwrap();
|
||||
assert_eq!(fill_sparse_batch(), decoded_sparse);
|
||||
|
||||
let decoded_full = decode_batch_v3(FIX_FULL_30).unwrap();
|
||||
assert_eq!(fill_full_batch(), decoded_full);
|
||||
|
||||
assert_eq!(FIX_SYNC_EMPTY, encode_batch_v3(&decoded_sync).unwrap().as_slice());
|
||||
assert_eq!(FIX_SPARSE_5, encode_batch_v3(&decoded_sparse).unwrap().as_slice());
|
||||
assert_eq!(FIX_FULL_30, encode_batch_v3(&decoded_full).unwrap().as_slice());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn payload_decode_rejects_bad_magic_schema_flags() {
|
||||
let mut bad_magic = FIX_SPARSE_5.to_vec();
|
||||
bad_magic[0] = 0x00;
|
||||
assert!(decode_batch_v3(&bad_magic).is_err());
|
||||
|
||||
let mut bad_schema = FIX_SPARSE_5.to_vec();
|
||||
bad_schema[2] = 0x02;
|
||||
assert!(decode_batch_v3(&bad_schema).is_err());
|
||||
|
||||
let mut bad_flags = FIX_SPARSE_5.to_vec();
|
||||
bad_flags[3] = 0x00;
|
||||
assert!(decode_batch_v3(&bad_flags).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn payload_decode_rejects_truncated_and_trailing() {
|
||||
assert!(decode_batch_v3(&FIX_SPARSE_5[..FIX_SPARSE_5.len() - 1]).is_err());
|
||||
assert!(decode_batch_v3(&FIX_SPARSE_5[..12]).is_err());
|
||||
|
||||
let mut with_tail = FIX_SPARSE_5.to_vec();
|
||||
with_tail.push(0xAA);
|
||||
assert!(decode_batch_v3(&with_tail).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn payload_encode_decode_reject_invalid_mask_and_n() {
|
||||
let mut input = fill_sparse_batch();
|
||||
input.present_mask = 0x4000_0000;
|
||||
assert!(encode_batch_v3(&input).is_err());
|
||||
|
||||
let mut input2 = fill_sparse_batch();
|
||||
input2.n = 31;
|
||||
assert!(encode_batch_v3(&input2).is_err());
|
||||
|
||||
let mut input3 = fill_sparse_batch();
|
||||
input3.n = 0;
|
||||
input3.present_mask = 1;
|
||||
assert!(encode_batch_v3(&input3).is_err());
|
||||
|
||||
let mut invalid_bits = FIX_SPARSE_5.to_vec();
|
||||
invalid_bits[15] |= 0x40;
|
||||
assert!(decode_batch_v3(&invalid_bits).is_err());
|
||||
|
||||
let mut bitcount_mismatch = FIX_SPARSE_5.to_vec();
|
||||
bitcount_mismatch[16] = 0x01;
|
||||
assert!(decode_batch_v3(&bitcount_mismatch).is_err());
|
||||
}
|
||||
Reference in New Issue
Block a user