use msgtrans::packet::{CompressionType, Packet, PacketType, ReservedFlags};
use serde::Deserialize;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Deserialize, Debug)]
struct FixtureJson {
version: u8,
compression: u8,
packet_type: u8,
biz_type: u8,
message_id: u32,
ext_header_hex: String,
payload_hex: String,
reserved: u16,
}
fn hex_decode(s: &str) -> Vec<u8> {
assert!(
s.len() % 2 == 0,
"hex string length must be even, got {}: {:?}",
s.len(),
s
);
let mut out = Vec::with_capacity(s.len() / 2);
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
let hi = decode_nibble(bytes[i]);
let lo = decode_nibble(bytes[i + 1]);
out.push((hi << 4) | lo);
i += 2;
}
out
}
fn decode_nibble(b: u8) -> u8 {
match b {
b'0'..=b'9' => b - b'0',
b'a'..=b'f' => b - b'a' + 10,
b'A'..=b'F' => b - b'A' + 10,
_ => panic!("invalid hex nibble: {:?}", b as char),
}
}
fn fixtures_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
}
fn list_bin_fixtures() -> Vec<PathBuf> {
let dir = fixtures_dir();
let mut out: Vec<PathBuf> = fs::read_dir(&dir)
.unwrap_or_else(|e| panic!("cannot read fixtures dir {}: {}", dir.display(), e))
.filter_map(Result::ok)
.map(|e| e.path())
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("bin"))
.collect();
out.sort();
out
}
fn build_packet_from_spec(spec: &FixtureJson) -> Packet {
let mut p = Packet::new(PacketType::from(spec.packet_type), spec.message_id);
p.set_biz_type(spec.biz_type);
p.set_compression(CompressionType::from(spec.compression));
p.set_ext_header(hex_decode(&spec.ext_header_hex));
p.set_payload(hex_decode(&spec.payload_hex));
p.header.reserved = ReservedFlags::from_raw(spec.reserved);
p.header.version = spec.version;
p
}
#[test]
fn fixed_header_is_16_bytes() {
let empty = Packet::one_way(0, Vec::<u8>::new());
assert_eq!(
empty.to_bytes().len(),
16,
"fixed header must serialize to exactly 16 bytes"
);
}
#[test]
fn fixtures_directory_has_batch_one() {
let bins = list_bin_fixtures();
assert!(
bins.len() >= 8,
"expected at least 8 batch-1 fixtures in {}, found {}. \
Run `cargo run --example regenerate_fixtures` to generate them.",
fixtures_dir().display(),
bins.len()
);
let names: Vec<String> = bins
.iter()
.filter_map(|p| p.file_stem().and_then(|s| s.to_str()).map(String::from))
.collect();
for n in 1..=8u32 {
let prefix = format!("{:02}_", n);
assert!(
names.iter().any(|name| name.starts_with(&prefix)),
"missing fixture with prefix {:?} in {:?}",
prefix,
names
);
}
}
#[test]
fn fixtures_round_trip() {
let bins = list_bin_fixtures();
assert!(!bins.is_empty(), "no fixtures found");
for bin_path in bins {
let json_path = bin_path.with_extension("json");
let label = bin_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("<unknown>")
.to_string();
let bin_bytes = fs::read(&bin_path)
.unwrap_or_else(|e| panic!("[{}] cannot read {}: {}", label, bin_path.display(), e));
let json_str = fs::read_to_string(&json_path)
.unwrap_or_else(|e| panic!("[{}] cannot read {}: {}", label, json_path.display(), e));
let spec: FixtureJson = serde_json::from_str(&json_str)
.unwrap_or_else(|e| panic!("[{}] invalid JSON: {}", label, e));
let parsed = Packet::from_bytes(&bin_bytes)
.unwrap_or_else(|e| panic!("[{}] from_bytes failed: {:?}", label, e));
assert_eq!(parsed.header.version, spec.version, "[{}] version", label);
assert_eq!(
u8::from(parsed.header.compression),
spec.compression,
"[{}] compression",
label
);
assert_eq!(
u8::from(parsed.header.packet_type),
spec.packet_type,
"[{}] packet_type",
label
);
assert_eq!(
parsed.header.biz_type, spec.biz_type,
"[{}] biz_type",
label
);
assert_eq!(
parsed.header.message_id, spec.message_id,
"[{}] message_id",
label
);
assert_eq!(
parsed.header.reserved.raw(),
spec.reserved,
"[{}] reserved",
label
);
let expected_ext = hex_decode(&spec.ext_header_hex);
let expected_payload = hex_decode(&spec.payload_hex);
assert_eq!(parsed.ext_header, expected_ext, "[{}] ext_header", label);
assert_eq!(parsed.payload, expected_payload, "[{}] payload", label);
assert_eq!(
parsed.header.ext_header_len as usize,
expected_ext.len(),
"[{}] ext_header_len header field",
label
);
assert_eq!(
parsed.header.payload_len as usize,
expected_payload.len(),
"[{}] payload_len header field",
label
);
let rebuilt = build_packet_from_spec(&spec);
let rebuilt_bytes = rebuilt.to_bytes();
assert_eq!(
rebuilt_bytes.as_ref(),
bin_bytes.as_slice(),
"[{}] byte-for-byte mismatch (rebuilt {} bytes, on-disk {} bytes)",
label,
rebuilt_bytes.len(),
bin_bytes.len()
);
assert_eq!(
bin_bytes.len(),
16 + expected_ext.len() + expected_payload.len(),
"[{}] total length mismatch",
label
);
}
}