test: constrain MP4 composition timing, MLP substream directory, and codec-private absence
Mutation testing over src/mux/. No production change — 49 survivors killed, all proven red before green. The MP4 composition-time chain was entirely unconstrained: VideoTiming::ctts, build_ctts and parse_ctts could each return a constant and the suite stayed green. Confirmed on HEAD: build_ctts -> vec![] passes all 1,220 mux tests. A demuxed B-frame title presenting in decode order would have shipped. The cause is a test whose name asserts coverage its body does not deliver — stts_and_ctts_expand builds an stts box and never touches ctts, and write_then_read_round_trip asserts sample sizes and keyframe flags but not one PTS. Same shape as the set_speed forwarding finding, different disguise. mlp_num_substreams / mlp_substr_header_size: every TrueHD fixture in the crate uses one substream and no extraword, so both could return a constant and agree with all of them. These position mlp_parity_ok's window over the AU header, so a constant mis-windows the parity check on exactly the multi-substream AUs that carry 7.1 and Atmos. CodecPrivate absent vs empty: mkv.rs writes Some(bytes) verbatim and omits the element on None (RFC 9559 5.1.4.1.24), so a zero-length Some emits a track header asserting the config IS empty. Four parsers could return Some(vec![]) before any frame. Also: mandatory ISO/IEC 14496-12 boxes (tkhd, vmhd, smhd, dinf, mdhd) could each build empty; HEVC num_extra_slice_header_bits (H.265 7.3.2.3) was never non-zero in any fixture, so the slice-type offset skip was unexercised; chapter names from the disc go straight into <ChapterString> and the & escape must run first; a stray 0x47 in a payload must not latch a TS resync. Documented as equivalent rather than killed: CodecParser::flush and the three parser flush bodies that differ from the mutant only by a tracing call, and DropTally::log_summary.
This commit is contained in:
@@ -190,6 +190,33 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// A dropped ADTS frame is dropped BECAUSE its header failed validation, so
|
||||
/// the very fields a duration would come from (sampling_frequency_index, and
|
||||
/// the 1024-samples-per-AAC-frame constant applied to it) are the ones known
|
||||
/// to be untrustworthy. This gate therefore reports the drop's duration as
|
||||
/// zero rather than deriving a number from a header it has just rejected —
|
||||
/// the honest answer, and the one the count alongside it must be read with.
|
||||
/// A nonzero constant here would report silence that was never measured.
|
||||
#[test]
|
||||
fn dropped_frames_are_counted_but_their_duration_is_not_invented() {
|
||||
let mut parser = AdtsParser::new();
|
||||
// Three frames whose sampling_frequency_index is a reserved value (13),
|
||||
// so `adts_verdict` rejects each one.
|
||||
let mut bad = adts_frame(32);
|
||||
bad[2] = (bad[2] & 0b1100_0011) | (13 << 2);
|
||||
for i in 0..3 {
|
||||
let out = parser.parse(&make_pes(bad.clone(), Some(i * 90_000)));
|
||||
assert!(out.is_empty(), "an invalid ADTS frame is not emitted");
|
||||
}
|
||||
assert_eq!(parser.dropped_frames(), 3, "every drop is counted");
|
||||
assert_eq!(
|
||||
parser.dropped_duration_ns(),
|
||||
0,
|
||||
"the duration comes from the header that just failed validation, so \
|
||||
it is reported as unmeasured rather than guessed"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reserved_sample_rate_index_is_dropped() {
|
||||
// sr_index = 13 (reserved). byte2 bits5..2 = 1101 → 0x34.
|
||||
|
||||
@@ -200,6 +200,31 @@ mod tests {
|
||||
assert!(!t.is_poisoned());
|
||||
}
|
||||
|
||||
/// The poison verdict is a RATIO — verified drops against every AU seen — so
|
||||
/// the kept count is half of it. `does_not_poison_a_mostly_good_track` above
|
||||
/// records its keeps AFTER the single drop, and `maybe_poison` only runs
|
||||
/// inside `record_drop`, so the keeps are never in the denominator when the
|
||||
/// verdict is actually computed: that test passes even with the kept count
|
||||
/// never incremented. Interleaving them puts the kept count on the critical
|
||||
/// path, where losing it turns the ratio into "verified drops vs verified
|
||||
/// drops" — always >50% — and silently discards a healthy track.
|
||||
#[test]
|
||||
fn interleaved_keeps_are_in_the_poison_denominator() {
|
||||
let mut t = DropTally::new("test");
|
||||
// 2 kept per 1 dropped, well past the minimum-AU gate: a third of the
|
||||
// track is undecodable, which is bad but nowhere near the >50% threshold.
|
||||
for _ in 0..(TRACK_VERDICT_MIN_AUS * 3) {
|
||||
t.record_kept();
|
||||
t.record_kept();
|
||||
t.record_drop(0, 1000, 512, "bad");
|
||||
assert!(
|
||||
!t.is_poisoned(),
|
||||
"33% dropped must never poison, at any point in the run"
|
||||
);
|
||||
}
|
||||
assert_eq!(t.dropped_frames(), TRACK_VERDICT_MIN_AUS * 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collateral_drops_never_poison_the_track() {
|
||||
// A TrueHD resync-forward run collaterally drops a long burst of AUs, but
|
||||
|
||||
@@ -1447,6 +1447,81 @@ mod tests {
|
||||
assert_eq!(h.max_display_mastering_luminance, 10_000_000);
|
||||
}
|
||||
|
||||
/// `num_extra_slice_header_bits` (H.265 §7.3.2.3) is a PPS field, and the
|
||||
/// `slice_reserved_flag[i]` bits it counts sit BETWEEN
|
||||
/// `slice_pic_parameter_set_id` and `slice_type` in the slice segment header
|
||||
/// (§7.3.6.1). Every existing fixture used a PPS with the field == 0, so the
|
||||
/// skip was never exercised: a parser that ignored the field entirely agreed
|
||||
/// with all of them, and would then read `slice_type` from the wrong bit
|
||||
/// offset on any real stream that sets it — mislabelling every picture's
|
||||
/// coding type.
|
||||
#[test]
|
||||
fn nonzero_num_extra_slice_header_bits_shifts_the_slice_type_offset() {
|
||||
use super::super::coding::CodingType;
|
||||
|
||||
// PPS body bits: pps_id ue=0 ('1'), sps_id ue=0 ('1'),
|
||||
// dependent_slice_segments_enabled_flag 0, output_flag_present_flag 0,
|
||||
// num_extra_slice_header_bits u(3).
|
||||
let pps_body = |num_extra: u8| 0b1100_0000u8 | (num_extra << 1);
|
||||
assert_eq!(pps_body(0), 0xC0, "matches the existing zero-extra fixture");
|
||||
assert_eq!(pps_body(3), 0xC6);
|
||||
|
||||
let pps_nal = |num_extra: u8| {
|
||||
let mut v = hevc_nal_header(NAL_PPS).to_vec();
|
||||
v.push(pps_body(num_extra));
|
||||
v
|
||||
};
|
||||
for n in 0..8u8 {
|
||||
assert_eq!(
|
||||
hevc_num_extra_slice_header_bits(&pps_nal(n)),
|
||||
Some(n as u32),
|
||||
"PPS must yield the value it encodes, for every u(3) code point"
|
||||
);
|
||||
}
|
||||
// A PPS truncated to just its 2-byte NAL header carries no field to read,
|
||||
// so the answer is absent — never a defaulted zero.
|
||||
assert_eq!(
|
||||
hevc_num_extra_slice_header_bits(&hevc_nal_header(NAL_PPS)),
|
||||
None
|
||||
);
|
||||
|
||||
// Slice segment header for a non-IRAP VCL NAL (TRAIL_R, type 1):
|
||||
// first_slice_segment_in_pic_flag 1, slice_pic_parameter_set_id ue=0
|
||||
// ('1'), then THREE reserved bits set to 101 (deliberately not zero, so a
|
||||
// parser that reads them instead of skipping them cannot agree), then
|
||||
// slice_type ue(v) = '011' → 2 → I.
|
||||
let slice_body = 0b1_1_101_011u8;
|
||||
assert_eq!(slice_body, 0xEB);
|
||||
|
||||
let nal = |t: u8, body: u8| {
|
||||
let mut v = vec![0x00, 0x00, 0x01];
|
||||
v.extend_from_slice(&hevc_nal_header(t));
|
||||
v.push(body);
|
||||
v
|
||||
};
|
||||
let mut data = nal(NAL_PPS, pps_body(3));
|
||||
data.extend_from_slice(&nal(1, slice_body));
|
||||
let frames = HevcParser::new().parse(&make_pes(data, Some(0)));
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert_eq!(
|
||||
frames[0].coding.expect("PictureInfo").coding_type(),
|
||||
CodingType::I,
|
||||
"with num_extra=3 the reserved bits are skipped and slice_type reads 2 (I)"
|
||||
);
|
||||
|
||||
// Control: the SAME slice bytes under a PPS declaring num_extra=0 land on
|
||||
// a different slice_type — proving the PPS field, not the slice bytes,
|
||||
// decides the offset.
|
||||
let mut data0 = nal(NAL_PPS, pps_body(0));
|
||||
data0.extend_from_slice(&nal(1, slice_body));
|
||||
let frames0 = HevcParser::new().parse(&make_pes(data0, Some(0)));
|
||||
assert_eq!(
|
||||
frames0[0].coding.expect("PictureInfo").coding_type(),
|
||||
CodingType::B,
|
||||
"num_extra=0 reads slice_type from bit 2 instead → 0 (B)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hevc_populates_measured_coding_type_and_source() {
|
||||
use super::super::coding::CodingType;
|
||||
|
||||
@@ -284,6 +284,63 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// A codec parser's `codec_private()` feeds `DiscStream::codec_private`,
|
||||
/// which the MKV muxer turns directly into the track's `CodecPrivate`
|
||||
/// element (RFC 9559 §5.1.4.1.24): `Some(bytes)` writes an element holding
|
||||
/// exactly those bytes, `None` omits the element entirely. The two are NOT
|
||||
/// interchangeable — a zero-length `CodecPrivate` asserts that the codec's
|
||||
/// initialisation data IS empty, which is not true of any codec, and a
|
||||
/// one-byte one asserts a config no decoder can parse.
|
||||
///
|
||||
/// The gating parsers (ADTS, MPEG audio, FLAC) and the passthrough parser
|
||||
/// extract no configuration at all: they validate and forward frames whose
|
||||
/// configuration is carried in band (ADTS headers, MPEG-1 audio frame
|
||||
/// headers, FLAC frame headers) or supplied by the source container. Having
|
||||
/// derived nothing, the only truthful answer they can give is "absent" —
|
||||
/// any `Some` would be a value they invented. Nothing else in the suite
|
||||
/// distinguished the two, so each of these impls could have returned a
|
||||
/// fabricated `Some` and produced a malformed track header unnoticed.
|
||||
#[test]
|
||||
fn parsers_that_derive_no_config_report_absent_never_an_empty_codec_private() {
|
||||
// Codecs whose parsers do no configuration extraction, paired with a
|
||||
// payload that is a REAL frame of that codec so the gate takes its
|
||||
// keep-path (a rejected frame proves nothing about the config answer).
|
||||
let cases: [(Codec, Vec<u8>); 5] = [
|
||||
// ADTS: syncword FFF1, MPEG-4 AAC-LC, 44.1 kHz, stereo, 7-byte frame.
|
||||
(Codec::Aac, vec![0xFF, 0xF1, 0x50, 0x80, 0x00, 0xBF, 0xFC]),
|
||||
// MPEG-1 Layer II, 44.1 kHz, 128 kbit/s, stereo.
|
||||
(Codec::Mp2, vec![0xFF, 0xFD, 0x70, 0x00, 0x00, 0x00]),
|
||||
// MPEG-1 Layer III, 44.1 kHz, 128 kbit/s, stereo.
|
||||
(Codec::Mp3, vec![0xFF, 0xFB, 0x90, 0x00, 0x00, 0x00]),
|
||||
// Not a FLAC frame sync, so the gate passes it through unvalidated —
|
||||
// still the keep path, and still no configuration derived.
|
||||
(Codec::Flac, vec![0x01, 0x02, 0x03, 0x04]),
|
||||
// Opus rides the all-keyframe passthrough parser.
|
||||
(Codec::Opus, vec![0x78, 0x01, 0x02, 0x03]),
|
||||
];
|
||||
|
||||
for (codec, payload) in cases {
|
||||
let mut parser = parser_for_codec(codec, None, false);
|
||||
assert_eq!(
|
||||
parser.codec_private(),
|
||||
None,
|
||||
"{codec:?}: no config before any frame"
|
||||
);
|
||||
parser.parse(&pes(Some(0), payload.clone()));
|
||||
parser.parse(&pes(Some(90_000), payload));
|
||||
assert_eq!(
|
||||
parser.codec_private(),
|
||||
None,
|
||||
"{codec:?}: this parser derives no config, so it must report the \
|
||||
CodecPrivate as ABSENT — an empty or invented Some would be \
|
||||
written into the track header as if it were real"
|
||||
);
|
||||
// Flushing at end of stream must not conjure one either.
|
||||
parser.flush();
|
||||
assert_eq!(parser.codec_private(), None, "{codec:?}: after flush");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_codecs_emit_keyframe_frames() {
|
||||
// PES = frame audio: every frame is independently decodable → keyframe.
|
||||
|
||||
@@ -183,6 +183,30 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Same contract as the ADTS gate: an MPEG-audio frame is dropped because
|
||||
/// its header is invalid, and the header is exactly where a frame duration
|
||||
/// (samples-per-frame for the layer, divided by the sampling rate) would
|
||||
/// have to come from — see `mpa_verdict`'s doc comment. So the drop's
|
||||
/// duration is reported as zero, not derived from rejected fields.
|
||||
#[test]
|
||||
fn dropped_frames_are_counted_but_their_duration_is_not_invented() {
|
||||
let mut parser = MpegAudioParser::new();
|
||||
// Reserved layer field (00) — rejected per ISO/IEC 11172-3.
|
||||
let mut bad = mp3_frame(32);
|
||||
bad[1] &= !0b0000_0110;
|
||||
for i in 0..3 {
|
||||
let out = parser.parse(&make_pes(bad.clone(), Some(i * 90_000)));
|
||||
assert!(out.is_empty(), "an invalid MPEG-audio frame is not emitted");
|
||||
}
|
||||
assert_eq!(parser.dropped_frames(), 3, "every drop is counted");
|
||||
assert_eq!(
|
||||
parser.dropped_duration_ns(),
|
||||
0,
|
||||
"the duration comes from the header that just failed validation, so \
|
||||
it is reported as unmeasured rather than guessed"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reserved_version_field_is_dropped() {
|
||||
// version field = 01 (reserved) → rejected. byte1 = 111_01_01_1 = 0xEB
|
||||
|
||||
@@ -341,6 +341,40 @@ mod tests {
|
||||
assert_eq!(display_set_is_forced(&[]), None);
|
||||
}
|
||||
|
||||
/// `observed()` is the probe's "did I actually see any PGS content?" signal.
|
||||
/// When it is false the track's forced state is UNKNOWN, and the probe leaves
|
||||
/// whatever flag the disc's own metadata supplied alone; when it is true the
|
||||
/// probe overwrites that flag with its own verdict. A tracker that always
|
||||
/// claims to have observed something therefore lets an unread or undecrypted
|
||||
/// subtitle track — where `is_forced()` is vacuously false — overwrite a
|
||||
/// correct vendor "forced" flag with "not forced".
|
||||
#[test]
|
||||
fn observed_stays_false_until_a_real_display_set_is_seen() {
|
||||
let mut t = ForcedTracker::new();
|
||||
assert!(!t.observed(), "a fresh tracker has seen nothing");
|
||||
assert!(!t.is_forced(), "and has no verdict to give");
|
||||
|
||||
// Blocks that carry no display set must not count as observation: a clear
|
||||
// PCS (zero composition objects), a non-PCS segment, a truncated PCS, and
|
||||
// an empty frame.
|
||||
let mut clear = pcs_display(false);
|
||||
clear[PCS_NUM_OBJECTS_OFFSET] = 0;
|
||||
let mut ods = pcs_display(true);
|
||||
ods[0] = 0x15;
|
||||
for block in [clear, ods, pcs_display(true)[..15].to_vec(), Vec::new()] {
|
||||
t.observe(&block);
|
||||
assert!(
|
||||
!t.observed(),
|
||||
"a block with no display set leaves the verdict unknown"
|
||||
);
|
||||
}
|
||||
|
||||
// The first real display set is what flips it.
|
||||
t.observe(&pcs_display(true));
|
||||
assert!(t.observed());
|
||||
assert!(t.is_forced(), "the only display set seen was forced");
|
||||
}
|
||||
|
||||
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
|
||||
PesPacket {
|
||||
source: None,
|
||||
|
||||
@@ -752,6 +752,70 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The substream count and the substream-directory size are the two numbers
|
||||
/// that position `mlp_parity_ok`'s check window over the AU header. Every
|
||||
/// other fixture in this module uses the degenerate shape — one substream,
|
||||
/// no extraword — so neither function's real behaviour was ever exercised:
|
||||
/// a constant answer agreed with all of them, and would then mis-window the
|
||||
/// parity check on the multi-substream AUs that carry 7.1 and Atmos, judging
|
||||
/// clean audio corrupt (or corrupt audio clean).
|
||||
#[test]
|
||||
fn mlp_num_substreams_is_the_top_nibble_of_major_sync_byte_16() {
|
||||
// Byte 16 of the major sync: top nibble is num_substreams, bottom nibble
|
||||
// is a different field, so it must not leak into the answer.
|
||||
for n in 0..16u8 {
|
||||
let mut ms = vec![0u8; 17];
|
||||
ms[16] = (n << 4) | 0x0F;
|
||||
assert_eq!(
|
||||
mlp_num_substreams(&ms),
|
||||
Some(n),
|
||||
"num_substreams is the high nibble only"
|
||||
);
|
||||
}
|
||||
// Real counts: 1 substream for 2.0/5.1 core-only, 4 for the 7.1/Atmos
|
||||
// layouts this crate has to mux.
|
||||
let mut ms = vec![0u8; 20];
|
||||
ms[16] = 0x40;
|
||||
assert_eq!(mlp_num_substreams(&ms), Some(4));
|
||||
|
||||
// A major sync too short to contain byte 16 yields no answer — never a
|
||||
// defaulted count, which would arm the parity check against garbage.
|
||||
assert_eq!(mlp_num_substreams(&[0u8; 16]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mlp_substr_header_size_counts_the_extraword_entries() {
|
||||
// Directory entry: 2 bytes, plus 2 more when the entry's top bit
|
||||
// (extraword) is set. Build a 4-substream directory that mixes both.
|
||||
const HDR: usize = 4;
|
||||
let mut au = vec![0xAAu8; HDR];
|
||||
au.extend_from_slice(&[0x00, 0x11]); // plain → 2
|
||||
au.extend_from_slice(&[0x80, 0x22, 0x01, 0x02]); // extraword → 4
|
||||
au.extend_from_slice(&[0x00, 0x33]); // plain → 2
|
||||
au.extend_from_slice(&[0x80, 0x44, 0x03, 0x04]); // extraword → 4
|
||||
au.extend_from_slice(&[0xFFu8; 8]); // payload past the directory
|
||||
assert_eq!(
|
||||
mlp_substr_header_size(&au, HDR, 4),
|
||||
Some(12),
|
||||
"2 + 4 + 2 + 4"
|
||||
);
|
||||
|
||||
// Same AU, fewer declared substreams → only that many entries counted.
|
||||
assert_eq!(mlp_substr_header_size(&au, HDR, 1), Some(2));
|
||||
assert_eq!(mlp_substr_header_size(&au, HDR, 2), Some(6));
|
||||
assert_eq!(mlp_substr_header_size(&au, HDR, 0), Some(0));
|
||||
|
||||
// An all-plain directory is 2 bytes per substream.
|
||||
let plain = vec![0x00u8; HDR + 8];
|
||||
assert_eq!(mlp_substr_header_size(&plain, HDR, 4), Some(8));
|
||||
|
||||
// A directory that runs past the AU has no answer: the parity window
|
||||
// would otherwise be placed over bytes that are not there.
|
||||
let truncated = &au[..HDR + 9];
|
||||
assert_eq!(mlp_substr_header_size(truncated, HDR, 4), None);
|
||||
assert_eq!(mlp_substr_header_size(&plain, HDR, 5), None);
|
||||
}
|
||||
|
||||
fn make_truehd_unit(size_bytes: usize) -> Vec<u8> {
|
||||
let words = size_bytes / 2;
|
||||
let mut data = vec![0u8; size_bytes];
|
||||
@@ -994,6 +1058,15 @@ mod tests {
|
||||
frames.extend(parser.flush());
|
||||
assert_eq!(frames.len(), 2, "the parity-broken AU is dropped");
|
||||
assert_eq!(parser.dropped_frames(), 1);
|
||||
// The drop is a SILENCE GAP, and its length is what the CLI reports as
|
||||
// lost audio. One TrueHD access unit is 40 samples; at the 48 kHz base
|
||||
// rate that is 40/48000 s = 833_333 ns. A count without a duration (or a
|
||||
// duration that ignores the AU rate) understates or invents the loss.
|
||||
assert_eq!(
|
||||
parser.dropped_duration_ns(),
|
||||
833_333,
|
||||
"one dropped AU = 40 samples at 48 kHz"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user