use std::{fs, path::PathBuf};
mod support;
fn fixture_path() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join("m6-single.ts")
}
fn is_null_packet(pkt: &[u8]) -> bool {
if pkt.len() < 3 {
return false;
}
support::extract_pid(pkt) == 0x1FFF
}
fn validate_null_packet(pkt: &[u8]) -> bool {
if pkt.len() != 188 {
return false;
}
if pkt[0] != 0x47 {
return false;
}
if support::extract_pid(pkt) != 0x1FFF {
return false;
}
let afc = (pkt[3] >> 4) & 0x03;
if afc != 0b01 {
return false;
}
pkt[4..].iter().all(|&b| b == 0xFF)
}
#[test]
fn drop_nulls_removes_all_null_packets() {
let input = fs::read(fixture_path()).expect("fixture m6-single.ts not found");
let input_with_nulls = support::add_null_packets(&input, 10);
let null_count_before = input_with_nulls
.chunks(188)
.filter(|pkt| is_null_packet(pkt))
.count();
assert!(
null_count_before > 0,
"test fixture should have null packets inserted"
);
let mut engine = ts_fix::TsFix::builder()
.stuffing(ts_fix::Stuffing::drop_nulls())
.build()
.expect("drop_nulls build should not fail");
let mut output = Vec::with_capacity(input_with_nulls.len());
for chunk in input_with_nulls.chunks(188) {
engine
.push(chunk, |pkt| output.extend_from_slice(pkt))
.expect("valid 188-byte packet");
}
engine.finish(|pkt| output.extend_from_slice(pkt));
let null_count_after = output.chunks(188).filter(|pkt| is_null_packet(pkt)).count();
assert_eq!(
null_count_after, 0,
"output should have zero null packets after drop_nulls, found {null_count_after}"
);
let input_non_null: Vec<&[u8]> = input_with_nulls
.chunks(188)
.filter(|pkt| !is_null_packet(pkt))
.collect();
let output_packets: Vec<&[u8]> = output.chunks(188).collect();
assert_eq!(
input_non_null.len(),
output_packets.len(),
"all non-null input packets should be in output"
);
for (idx, (input_pkt, output_pkt)) in
input_non_null.iter().zip(output_packets.iter()).enumerate()
{
assert_eq!(
input_pkt, output_pkt,
"packet {idx} should be byte-identical after drop_nulls"
);
}
}
#[test]
fn drop_nulls_is_identity_on_stream_without_nulls() {
let input = fs::read(fixture_path()).expect("fixture m6-single.ts not found");
let mut engine = ts_fix::TsFix::builder()
.stuffing(ts_fix::Stuffing::drop_nulls())
.build()
.expect("drop_nulls build should not fail");
let mut output = Vec::with_capacity(input.len());
for chunk in input.chunks(188) {
engine
.push(chunk, |pkt| output.extend_from_slice(pkt))
.expect("valid 188-byte packet");
}
engine.finish(|pkt| output.extend_from_slice(pkt));
assert_eq!(
output.len(),
input.len(),
"output length should match input"
);
assert_eq!(
output, input,
"output should be byte-identical to input when no nulls present"
);
}
#[test]
fn pad_to_2_0_doubles_packet_count() {
let input = fs::read(fixture_path()).expect("fixture m6-single.ts not found");
let input_packet_count = input.len() / 188;
let mut engine = ts_fix::TsFix::builder()
.stuffing(ts_fix::Stuffing::pad_to(2.0))
.build()
.expect("pad_to build should not fail");
let mut output = Vec::new();
for chunk in input.chunks(188) {
engine
.push(chunk, |pkt| output.extend_from_slice(pkt))
.expect("valid 188-byte packet");
}
engine.finish(|pkt| output.extend_from_slice(pkt));
let output_packet_count = output.len() / 188;
assert_eq!(
output_packet_count,
input_packet_count * 2,
"pad_to(2.0) should double packet count: {input_packet_count} input → {output_packet_count} output"
);
assert_eq!(
output.len() % 188,
0,
"output must be a multiple of 188 bytes"
);
}
#[test]
fn pad_to_inserts_valid_null_packets() {
let input = fs::read(fixture_path()).expect("fixture m6-single.ts not found");
let mut engine = ts_fix::TsFix::builder()
.stuffing(ts_fix::Stuffing::pad_to(1.5))
.build()
.expect("pad_to build should not fail");
let mut output = Vec::new();
for chunk in input.chunks(188) {
engine
.push(chunk, |pkt| output.extend_from_slice(pkt))
.expect("valid 188-byte packet");
}
engine.finish(|pkt| output.extend_from_slice(pkt));
let null_packets: Vec<&[u8]> = output
.chunks(188)
.filter(|pkt| is_null_packet(pkt))
.collect();
assert!(
!null_packets.is_empty(),
"pad_to(1.5) should insert null packets"
);
for (idx, pkt) in null_packets.iter().enumerate() {
assert!(
validate_null_packet(pkt),
"null packet {idx} is not properly formed (PID 0x1FFF, AFC=01, 0xFF padding)"
);
}
}
#[test]
fn pad_to_rate_matches_expected_count() {
let input = fs::read(fixture_path()).expect("fixture m6-single.ts not found");
let input_packet_count = input.len() / 188;
for rate in &[1.0, 1.5, 2.0, 3.0] {
let mut engine = ts_fix::TsFix::builder()
.stuffing(ts_fix::Stuffing::pad_to(*rate))
.build()
.expect("pad_to build should not fail");
let mut output = Vec::new();
for chunk in input.chunks(188) {
engine
.push(chunk, |pkt| output.extend_from_slice(pkt))
.expect("valid 188-byte packet");
}
engine.finish(|pkt| output.extend_from_slice(pkt));
let output_packet_count = output.len() / 188;
let expected = (input_packet_count as f64 * rate).round() as usize;
assert_eq!(
output_packet_count, expected,
"pad_to({rate}) should produce {expected} packets, got {output_packet_count}"
);
}
}
#[test]
fn pad_to_preserves_input_packets() {
let input = fs::read(fixture_path()).expect("fixture m6-single.ts not found");
let mut engine = ts_fix::TsFix::builder()
.stuffing(ts_fix::Stuffing::pad_to(2.0))
.build()
.expect("pad_to build should not fail");
let mut output = Vec::new();
for chunk in input.chunks(188) {
engine
.push(chunk, |pkt| output.extend_from_slice(pkt))
.expect("valid 188-byte packet");
}
engine.finish(|pkt| output.extend_from_slice(pkt));
let output_non_null: Vec<&[u8]> = output
.chunks(188)
.filter(|pkt| !is_null_packet(pkt))
.collect();
let input_packets: Vec<&[u8]> = input.chunks(188).collect();
assert_eq!(
output_non_null.len(),
input_packets.len(),
"all input packets should appear in output (null packets are additions only)"
);
for (idx, (input_pkt, output_pkt)) in
input_packets.iter().zip(output_non_null.iter()).enumerate()
{
assert_eq!(
input_pkt, output_pkt,
"packet {idx} should be byte-identical after pad_to"
);
}
}