mod protocol;
use protocol::coverage_matrix::command_coverage;
use spvirit_codec::epics_decode::{PvaPacket, PvaPacketCommand};
use spvirit_codec::spvd_decode::PvdDecoder;
use spvirit_codec::spvirit_encode::encode_header;
fn build_frame(
command: u8,
payload: &[u8],
is_server: bool,
is_be: bool,
is_control: bool,
) -> Vec<u8> {
let mut out = encode_header(
is_server,
is_be,
is_control,
2,
command,
payload.len() as u32,
);
out.extend_from_slice(payload);
out
}
fn minimal_payload_for_command(command: u8) -> Vec<u8> {
match command {
0 => {
let mut payload = vec![0u8; 34];
payload.push(0); payload.push(0); payload
}
1 => vec![0u8; 8],
2 => vec![],
3 => {
let mut payload = vec![0u8; 26];
payload.push(0); payload.extend_from_slice(&0u16.to_le_bytes()); payload
}
4 => {
let mut payload = vec![0u8; 34];
payload.push(0); payload
}
5 => vec![0],
6 => vec![0xFF],
7 => 0u16.to_le_bytes().to_vec(),
8 => vec![0u8; 8],
9 => vec![0xFF],
10 | 11 | 12 | 13 | 14 | 16 | 20 => {
let mut payload = Vec::new();
payload.extend_from_slice(&1u32.to_le_bytes()); payload.extend_from_slice(&7u32.to_le_bytes()); payload.push(0x00); payload
}
15 => 7u32.to_le_bytes().to_vec(),
17 => 1u32.to_le_bytes().to_vec(),
18 => vec![0xFF],
19 => vec![0], 21 => 7u32.to_le_bytes().to_vec(),
22 => vec![0u8; 16],
_ => vec![],
}
}
#[test]
fn decode_surface_covers_all_protocol_commands() {
for entry in command_coverage() {
let payload = minimal_payload_for_command(entry.command);
let frame = build_frame(entry.command, &payload, false, false, false);
let mut packet = PvaPacket::new(&frame);
let decoded = packet.decode_payload();
assert!(
decoded.is_some(),
"decode failed for command {}",
entry.command
);
let decoded = decoded.expect("decoded");
match (entry.command, decoded) {
(0, PvaPacketCommand::Beacon(_))
| (1, PvaPacketCommand::ConnectionValidation(_))
| (3, PvaPacketCommand::Search(_))
| (4, PvaPacketCommand::SearchResponse(_))
| (5, PvaPacketCommand::AuthNZ(_))
| (6, PvaPacketCommand::AclChange(_))
| (7, PvaPacketCommand::CreateChannel(_))
| (8, PvaPacketCommand::DestroyChannel(_))
| (9, PvaPacketCommand::ConnectionValidated(_))
| (10, PvaPacketCommand::Op(_))
| (11, PvaPacketCommand::Op(_))
| (12, PvaPacketCommand::Op(_))
| (13, PvaPacketCommand::Op(_))
| (14, PvaPacketCommand::Op(_))
| (15, PvaPacketCommand::DestroyRequest(_))
| (16, PvaPacketCommand::Op(_))
| (17, PvaPacketCommand::GetField(_))
| (18, PvaPacketCommand::Message(_))
| (19, PvaPacketCommand::MultipleData(_))
| (20, PvaPacketCommand::Op(_))
| (21, PvaPacketCommand::CancelRequest(_))
| (22, PvaPacketCommand::OriginTag(_)) => {}
(2, PvaPacketCommand::Echo(_)) => {}
(id, other) => panic!("unexpected decoded variant for cmd {}: {:?}", id, other),
}
}
}
#[test]
fn malformed_packet_length_is_rejected() {
let mut frame = encode_header(false, false, false, 2, 7, 16);
frame.extend_from_slice(&[0u8; 2]);
let mut packet = PvaPacket::new(&frame);
assert!(packet.decode_payload().is_none());
}
#[test]
fn malformed_reserved_flag_bits_mark_header_invalid() {
let mut raw = vec![0xCA, 0x02, 0x0E, 0x07];
raw.extend_from_slice(&0u32.to_le_bytes());
let packet = PvaPacket::new(&raw);
assert!(!packet.is_valid());
}
#[test]
fn malformed_magic_mark_header_invalid() {
let mut raw = vec![0xCB, 0x02, 0x00, 0x07];
raw.extend_from_slice(&0u32.to_le_bytes());
let packet = PvaPacket::new(&raw);
assert!(!packet.is_valid());
}
#[test]
fn malformed_unknown_pvd_type_decodes_to_none() {
let decoder = PvdDecoder::new(false);
let field_desc = [1u8, b'v', 0x06];
assert!(decoder.parse_field_desc(&field_desc).is_none());
}
#[test]
fn golden_spec_vectors_decode() {
let vectors: &[(&str, &[u8], u8)] = &[
(
"cmd01_connection_validation_le.bin",
include_bytes!("protocol/spec_vectors/cmd01_connection_validation_le.bin"),
1,
),
(
"cmd07_create_channel_req_le.bin",
include_bytes!("protocol/spec_vectors/cmd07_create_channel_req_le.bin"),
7,
),
(
"cmd10_get_init_req_le.bin",
include_bytes!("protocol/spec_vectors/cmd10_get_init_req_le.bin"),
10,
),
(
"cmd11_put_init_req_le.bin",
include_bytes!("protocol/spec_vectors/cmd11_put_init_req_le.bin"),
11,
),
(
"cmd13_monitor_init_req_le.bin",
include_bytes!("protocol/spec_vectors/cmd13_monitor_init_req_le.bin"),
13,
),
(
"cmd18_status_error_message_le.bin",
include_bytes!("protocol/spec_vectors/cmd18_status_error_message_le.bin"),
18,
),
];
for (name, bytes, expected_cmd) in vectors {
let mut packet = PvaPacket::new(bytes);
assert_eq!(packet.header.command, *expected_cmd, "vector {}", name);
let decoded = packet.decode_payload();
assert!(decoded.is_some(), "vector {} failed to decode", name);
}
}
#[test]
fn pv_request_nested_roundtrip_locks_wire_format() {
use spvirit_codec::spvd_encode::{decode_pv_request_fields, encode_pv_request};
let empty = encode_pv_request(&[], false);
assert!(decode_pv_request_fields(&empty, false).is_none());
let flat = encode_pv_request(&["value"], false);
let paths = decode_pv_request_fields(&flat, false).expect("flat paths");
assert_eq!(paths, vec!["value".to_string()]);
let nested = encode_pv_request(&["alarm.severity"], false);
let paths = decode_pv_request_fields(&nested, false).expect("nested paths");
assert_eq!(paths, vec!["alarm.severity".to_string()]);
let mixed = encode_pv_request(&["value", "alarm.severity", "timeStamp"], false);
let paths = decode_pv_request_fields(&mixed, false).expect("mixed paths");
assert_eq!(
paths,
vec![
"value".to_string(),
"alarm.severity".to_string(),
"timeStamp".to_string(),
]
);
}
#[test]
fn encode_nt_payload_filtered_locks_bitset_and_projection() {
use spvirit_codec::spvd_decode::{DecodedValue, PvdDecoder};
use spvirit_codec::spvd_encode::{
encode_nt_payload_filtered, encode_structure_desc, filter_structure_desc, nt_payload_desc,
};
use spvirit_types::{NtPayload, NtScalar, ScalarValue};
let mut nt = NtScalar::from_value(ScalarValue::F64(1.25));
nt.alarm_severity = 2;
let payload = NtPayload::Scalar(nt);
let full_desc = nt_payload_desc(&payload);
let desc = filter_structure_desc(&full_desc, &["alarm.severity".to_string()]);
assert_eq!(desc.fields.len(), 1);
assert_eq!(desc.fields[0].name, "alarm");
let (bitset, values) = encode_nt_payload_filtered(&payload, &desc, false);
assert!(!bitset.is_empty(), "bitset must be present");
assert!(bitset[0] > 0, "bitset size byte non-zero");
let data_byte = bitset[1];
assert_eq!(data_byte & 0x01, 0x01, "bit 0 (whole-struct) must be set");
let desc_bytes = encode_structure_desc(&desc, false);
let mut pvd = Vec::with_capacity(1 + desc_bytes.len() + values.len());
pvd.push(0x80);
pvd.extend_from_slice(&desc_bytes);
pvd.extend_from_slice(&values);
let decoder = PvdDecoder::new(false);
let parsed = decoder.parse_introspection(&pvd).expect("desc parse");
let (decoded, _consumed) = decoder
.decode_structure(&pvd[1 + desc_bytes.len()..], &parsed)
.expect("value decode");
let DecodedValue::Structure(top) = decoded else {
panic!("expected top-level struct");
};
assert_eq!(top.len(), 1);
assert_eq!(top[0].0, "alarm");
let DecodedValue::Structure(ref alarm) = top[0].1 else {
panic!("alarm must be a struct");
};
assert_eq!(alarm.len(), 1);
assert_eq!(alarm[0].0, "severity");
}
#[test]
fn encode_nt_payload_delta_is_none_when_filtered_view_unchanged() {
use spvirit_codec::spvd_encode::{
encode_nt_payload_delta, filter_structure_desc, nt_payload_desc,
};
use spvirit_types::{NtPayload, NtScalar, ScalarValue};
let mut a = NtScalar::from_value(ScalarValue::F64(1.0));
a.alarm_severity = 0;
let mut b = NtScalar::from_value(ScalarValue::F64(2.0)); b.alarm_severity = 0; let pa = NtPayload::Scalar(a);
let pb = NtPayload::Scalar(b);
let full_desc = nt_payload_desc(&pa);
let desc = filter_structure_desc(&full_desc, &["alarm.severity".to_string()]);
let delta = encode_nt_payload_delta(&pa, &pb, &desc, false);
assert!(
delta.is_none(),
"delta must be None when selected fields unchanged"
);
}
#[test]
fn encode_nt_payload_delta_emits_bytes_when_selected_field_changes() {
use spvirit_codec::spvd_encode::{
encode_nt_payload_delta, filter_structure_desc, nt_payload_desc,
};
use spvirit_types::{NtPayload, NtScalar, ScalarValue};
let mut a = NtScalar::from_value(ScalarValue::F64(1.0));
a.alarm_severity = 0;
let mut b = NtScalar::from_value(ScalarValue::F64(1.0));
b.alarm_severity = 2;
let pa = NtPayload::Scalar(a);
let pb = NtPayload::Scalar(b);
let full_desc = nt_payload_desc(&pa);
let desc = filter_structure_desc(&full_desc, &["alarm.severity".to_string()]);
let (bitset, values) =
encode_nt_payload_delta(&pa, &pb, &desc, false).expect("delta expected on change");
assert!(!bitset.is_empty());
assert!(!values.is_empty(), "changed severity must emit bytes");
}