labels: apply_labels integration tests + class_reader robustness fuzz tests
Closes the final two audit items from this session. labels::apply_labels: factored out of apply() so the matching logic is unit-testable without needing a SectorReader / UdfFs. 11 new tests in apply_tests cover: - codec_hint + variant flow through to AudioStream.label - purpose set on audio with no label English text - name fallback only when purpose=Normal (CLI owns purpose i18n) - subtitle SDH qualifier set; forced flag flipped on Forced - per-type 1-based indexing (audio #2 maps to 2nd audio stream, not 2nd stream overall) - labels for nonexistent streams are no-ops - empty labels list leaves streams untouched - fill_defaults generates audio + video labels; preserves existing class_reader: robustness smoke tests. ClassFile::parse must NEVER panic on adversarial input — only return Err. 9 new tests: - empty input - short magic (0..4 bytes) - wrong magic - truncated after magic - bad CP tag - truncated UTF-8 in CP - 200 random byte buffers (deterministic xorshift) - 100 magic + random tail (most adversarial — magic check passes, everything else garbage) - instructions iter on random code (200 buffers) - instruction_size on every opcode 0..255 with varied tail buffers - modified_utf8 on random byte buffers (500) The xorshift PRNG keeps the tests deterministic (no rand dep) and reproducible — failures will be the same buffer every time. This is the lightweight alternative to a cargo-fuzz setup; if/when we adopt cargo-fuzz, these tests stay as regression cases. All 451 tests passing on cargo +1.86 fmt + clippy + test.
This commit is contained in:
@@ -1132,4 +1132,168 @@ mod tests {
|
||||
};
|
||||
assert_eq!(i.cp_index(), None);
|
||||
}
|
||||
|
||||
// ── Robustness smoke tests ──────────────────────────────────────────────
|
||||
//
|
||||
// ClassFile::parse must NEVER panic on adversarial input, only
|
||||
// return Err. These tests feed a battery of malformed byte
|
||||
// sequences and assert Err results — they're the lightweight
|
||||
// alternative to a full cargo-fuzz target (which would need
|
||||
// nightly + separate crate). If we adopt cargo-fuzz later, these
|
||||
// tests stay as deterministic regression cases.
|
||||
|
||||
/// Tiny pseudo-random byte generator — deterministic + reproducible
|
||||
/// without needing a `rand` dep. xorshift64*; good enough for
|
||||
/// generating adversarial byte payloads.
|
||||
fn xorshift(state: &mut u64) -> u64 {
|
||||
let mut x = *state;
|
||||
x ^= x << 13;
|
||||
x ^= x >> 7;
|
||||
x ^= x << 17;
|
||||
*state = x;
|
||||
x
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_rejects_empty_input() {
|
||||
assert!(ClassFile::parse(&[]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_rejects_short_magic() {
|
||||
for n in 0..4 {
|
||||
let buf = vec![0u8; n];
|
||||
assert!(ClassFile::parse(&buf).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_rejects_wrong_magic() {
|
||||
let buf = vec![0xDE, 0xAD, 0xBE, 0xEF, 0, 0, 0, 0];
|
||||
match ClassFile::parse(&buf) {
|
||||
Err(Error::BadMagic(0xDEADBEEF)) => {}
|
||||
Err(other) => panic!("expected BadMagic, got {:?}", other),
|
||||
Ok(_) => panic!("expected BadMagic error, got Ok"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_rejects_truncated_after_magic() {
|
||||
// CAFEBABE + 1 byte = not enough for minor_version (u16).
|
||||
let buf = vec![0xCA, 0xFE, 0xBA, 0xBE, 0x00];
|
||||
assert!(ClassFile::parse(&buf).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_rejects_bad_cp_tag() {
|
||||
// CAFEBABE + minor/major(0,0,0,52) + cp_count=2 + tag=99 (unknown).
|
||||
let buf = vec![
|
||||
0xCA, 0xFE, 0xBA, 0xBE, // magic
|
||||
0x00, 0x00, // minor
|
||||
0x00, 0x34, // major
|
||||
0x00, 0x02, // cp_count = 2 (one entry)
|
||||
99, // unknown tag
|
||||
];
|
||||
match ClassFile::parse(&buf) {
|
||||
Err(_) => {} // BadCpTag, BadMagic, or any other malformed-input err
|
||||
Ok(_) => panic!("expected error on unknown CP tag"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_rejects_truncated_utf8() {
|
||||
// CAFEBABE + minor/major + cp_count=2 + tag=1 (Utf8) + length=10 + 3 bytes (< 10).
|
||||
let buf = vec![
|
||||
0xCA, 0xFE, 0xBA, 0xBE, 0x00, 0x00, 0x00, 0x34, 0x00, 0x02, // cp_count=2
|
||||
1, // Utf8 tag
|
||||
0x00, 10, // length=10
|
||||
b'h', b'i', b'!', // only 3 bytes (truncated)
|
||||
];
|
||||
assert!(ClassFile::parse(&buf).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_does_not_panic_on_random_bytes() {
|
||||
// 200 deterministic-pseudo-random byte buffers of varying
|
||||
// lengths. The contract: never panic, only return Err (or in
|
||||
// the vanishingly unlikely case of a coincidentally-valid
|
||||
// buffer, Ok — we don't assert one or the other).
|
||||
let mut state: u64 = 0xDEADBEEF_DEADBEEF;
|
||||
for _ in 0..200 {
|
||||
let len = (xorshift(&mut state) % 256) as usize;
|
||||
let mut buf = Vec::with_capacity(len);
|
||||
for _ in 0..len {
|
||||
buf.push((xorshift(&mut state) & 0xFF) as u8);
|
||||
}
|
||||
// No panic. Result doesn't matter — Err is expected for
|
||||
// 99%+ of inputs.
|
||||
let _ = ClassFile::parse(&buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_does_not_panic_on_valid_magic_random_tail() {
|
||||
// 100 buffers that start with the magic + plausible
|
||||
// minor/major but have garbage afterwards. These are the
|
||||
// most adversarial — they pass the magic check and then
|
||||
// exercise every other parser path.
|
||||
let mut state: u64 = 0xCAFEBABE_DEADBEEF;
|
||||
for _ in 0..100 {
|
||||
let mut buf = vec![0xCA, 0xFE, 0xBA, 0xBE, 0x00, 0x00, 0x00, 0x34];
|
||||
let tail_len = (xorshift(&mut state) % 512) as usize;
|
||||
for _ in 0..tail_len {
|
||||
buf.push((xorshift(&mut state) & 0xFF) as u8);
|
||||
}
|
||||
let _ = ClassFile::parse(&buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instructions_never_panic_on_random_code() {
|
||||
// Bytecode iterator must not panic on any byte sequence.
|
||||
let mut state: u64 = 0x12345678_87654321;
|
||||
for _ in 0..200 {
|
||||
let len = (xorshift(&mut state) % 256) as usize;
|
||||
let mut code = Vec::with_capacity(len);
|
||||
for _ in 0..len {
|
||||
code.push((xorshift(&mut state) & 0xFF) as u8);
|
||||
}
|
||||
let attr = CodeAttribute {
|
||||
max_stack: 0,
|
||||
max_locals: 0,
|
||||
code: &code,
|
||||
};
|
||||
// Bounded — iterator stops on truncated/unknown opcodes.
|
||||
let _: Vec<_> = attr.instructions().collect();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instruction_size_never_panics() {
|
||||
// Cover every opcode byte 0..=255 with various code-buffer
|
||||
// shapes. instruction_size returns Option but must not panic.
|
||||
for op in 0u8..=255 {
|
||||
for tail_len in [0usize, 1, 2, 3, 7, 16, 32] {
|
||||
let mut buf = vec![op];
|
||||
for i in 0..tail_len {
|
||||
buf.push((i as u8).wrapping_mul(31));
|
||||
}
|
||||
let _ = instruction_size(&buf, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modified_utf8_never_panics_on_random_bytes() {
|
||||
let mut state: u64 = 0xABCDEF12_34567890;
|
||||
for _ in 0..500 {
|
||||
let len = (xorshift(&mut state) % 64) as usize;
|
||||
let mut buf = Vec::with_capacity(len);
|
||||
for _ in 0..len {
|
||||
buf.push((xorshift(&mut state) & 0xFF) as u8);
|
||||
}
|
||||
// Either Ok or Err; never a panic.
|
||||
let _ = decode_modified_utf8(&buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +148,18 @@ pub fn apply(reader: &mut dyn SectorReader, udf: &UdfFs, titles: &mut [DiscTitle
|
||||
if labels.is_empty() {
|
||||
return;
|
||||
}
|
||||
apply_labels(&labels, titles);
|
||||
}
|
||||
|
||||
/// Apply a pre-extracted set of labels to titles' streams. Match
|
||||
/// labels to streams by (stream_type, 1-based stream_number per type).
|
||||
/// Audio streams update `purpose` + `label` (codec/variant info; never
|
||||
/// English purpose text). Subtitle streams update `qualifier` and the
|
||||
/// `forced` flag.
|
||||
///
|
||||
/// Extracted from `apply()` so the matching logic is unit-testable
|
||||
/// without needing a SectorReader / UdfFs.
|
||||
pub(crate) fn apply_labels(labels: &[StreamLabel], titles: &mut [DiscTitle]) {
|
||||
for title in titles.iter_mut() {
|
||||
let mut audio_idx: u16 = 0;
|
||||
let mut sub_idx: u16 = 0;
|
||||
@@ -559,3 +570,361 @@ mod registry_tests {
|
||||
assert!(!PARSERS.is_empty(), "PARSERS array must not be empty");
|
||||
}
|
||||
}
|
||||
|
||||
// ── apply() integration tests ──────────────────────────────────────────────
|
||||
//
|
||||
// End-to-end coverage for the apply_labels + fill_defaults pipeline
|
||||
// without needing a SectorReader / UdfFs. Synthetic DiscTitle +
|
||||
// StreamLabel inputs, assert on the resulting Stream field values.
|
||||
|
||||
#[cfg(test)]
|
||||
mod apply_tests {
|
||||
use super::*;
|
||||
use crate::disc::{
|
||||
AudioChannels, AudioStream, Codec, ColorSpace, FrameRate, HdrFormat, Resolution,
|
||||
SampleRate, SubtitleStream, VideoStream,
|
||||
};
|
||||
|
||||
fn audio(pid: u16, codec: Codec, channels: AudioChannels, language: &str) -> Stream {
|
||||
Stream::Audio(AudioStream {
|
||||
pid,
|
||||
codec,
|
||||
channels,
|
||||
language: language.into(),
|
||||
sample_rate: SampleRate::S48,
|
||||
secondary: false,
|
||||
purpose: LabelPurpose::Normal,
|
||||
label: String::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn subtitle(pid: u16, language: &str) -> Stream {
|
||||
Stream::Subtitle(SubtitleStream {
|
||||
pid,
|
||||
codec: Codec::Pgs,
|
||||
language: language.into(),
|
||||
forced: false,
|
||||
qualifier: LabelQualifier::None,
|
||||
codec_data: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn video() -> Stream {
|
||||
Stream::Video(VideoStream {
|
||||
pid: 0x1011,
|
||||
codec: Codec::Hevc,
|
||||
resolution: Resolution::R2160p,
|
||||
frame_rate: FrameRate::F23_976,
|
||||
hdr: HdrFormat::Hdr10,
|
||||
color_space: ColorSpace::Bt2020,
|
||||
secondary: false,
|
||||
label: String::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn title_with(streams: Vec<Stream>) -> DiscTitle {
|
||||
DiscTitle {
|
||||
playlist: "00800.mpls".into(),
|
||||
playlist_id: 800,
|
||||
duration_secs: 7200.0,
|
||||
size_bytes: 0,
|
||||
clips: Vec::new(),
|
||||
streams,
|
||||
chapters: Vec::new(),
|
||||
extents: Vec::new(),
|
||||
content_format: crate::disc::ContentFormat::BdTs,
|
||||
codec_privates: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn audio_label(num: u16, lang: &str, codec_hint: &str, variant: &str) -> StreamLabel {
|
||||
StreamLabel {
|
||||
stream_number: num,
|
||||
stream_type: StreamLabelType::Audio,
|
||||
language: lang.into(),
|
||||
name: String::new(),
|
||||
purpose: LabelPurpose::Normal,
|
||||
qualifier: LabelQualifier::None,
|
||||
codec_hint: codec_hint.into(),
|
||||
variant: variant.into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_attaches_codec_hint_and_variant_to_audio() {
|
||||
let mut titles = vec![title_with(vec![
|
||||
video(),
|
||||
audio(0x1100, Codec::TrueHd, AudioChannels::Surround51, "eng"),
|
||||
])];
|
||||
let labels = vec![audio_label(1, "eng", "Dolby Atmos", "")];
|
||||
apply_labels(&labels, &mut titles);
|
||||
|
||||
if let Stream::Audio(a) = &titles[0].streams[1] {
|
||||
assert_eq!(a.label, "Dolby Atmos");
|
||||
} else {
|
||||
panic!("expected audio stream");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_combines_variant_and_codec_hint() {
|
||||
let mut titles = vec![title_with(vec![audio(
|
||||
0x1100,
|
||||
Codec::TrueHd,
|
||||
AudioChannels::Surround51,
|
||||
"por",
|
||||
)])];
|
||||
let labels = vec![audio_label(1, "por", "Dolby Atmos", "Brazilian")];
|
||||
apply_labels(&labels, &mut titles);
|
||||
|
||||
if let Stream::Audio(a) = &titles[0].streams[0] {
|
||||
assert_eq!(a.label, "(Brazilian) Dolby Atmos");
|
||||
} else {
|
||||
panic!("expected audio stream");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_sets_purpose_on_audio_commentary() {
|
||||
let mut titles = vec![title_with(vec![audio(
|
||||
0x1100,
|
||||
Codec::Ac3,
|
||||
AudioChannels::Stereo,
|
||||
"eng",
|
||||
)])];
|
||||
let labels = vec![StreamLabel {
|
||||
stream_number: 1,
|
||||
stream_type: StreamLabelType::Audio,
|
||||
language: "eng".into(),
|
||||
name: String::new(),
|
||||
purpose: LabelPurpose::Commentary,
|
||||
qualifier: LabelQualifier::None,
|
||||
codec_hint: String::new(),
|
||||
variant: String::new(),
|
||||
}];
|
||||
apply_labels(&labels, &mut titles);
|
||||
|
||||
if let Stream::Audio(a) = &titles[0].streams[0] {
|
||||
assert_eq!(a.purpose, LabelPurpose::Commentary);
|
||||
// Label stays empty: no codec/variant; purpose is conveyed
|
||||
// structurally, NOT as English text.
|
||||
assert_eq!(a.label, "");
|
||||
} else {
|
||||
panic!("expected audio stream");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_uses_name_fallback_only_for_normal_purpose() {
|
||||
// Name fallback fires when purpose=Normal and codec/variant are empty.
|
||||
let mut titles = vec![title_with(vec![audio(
|
||||
0x1100,
|
||||
Codec::TrueHd,
|
||||
AudioChannels::Surround71,
|
||||
"eng",
|
||||
)])];
|
||||
let labels = vec![StreamLabel {
|
||||
stream_number: 1,
|
||||
stream_type: StreamLabelType::Audio,
|
||||
language: "eng".into(),
|
||||
name: "Director's Cut Edition".into(),
|
||||
purpose: LabelPurpose::Normal,
|
||||
qualifier: LabelQualifier::None,
|
||||
codec_hint: String::new(),
|
||||
variant: String::new(),
|
||||
}];
|
||||
apply_labels(&labels, &mut titles);
|
||||
|
||||
if let Stream::Audio(a) = &titles[0].streams[0] {
|
||||
assert_eq!(a.label, "Director's Cut Edition");
|
||||
} else {
|
||||
panic!("expected audio stream");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_name_fallback_suppressed_for_non_normal_purpose() {
|
||||
// Name fallback must NOT fire when purpose != Normal — the
|
||||
// CLI is responsible for rendering purpose text.
|
||||
let mut titles = vec![title_with(vec![audio(
|
||||
0x1100,
|
||||
Codec::Ac3,
|
||||
AudioChannels::Stereo,
|
||||
"eng",
|
||||
)])];
|
||||
let labels = vec![StreamLabel {
|
||||
stream_number: 1,
|
||||
stream_type: StreamLabelType::Audio,
|
||||
language: "eng".into(),
|
||||
name: "Commentary by Director".into(),
|
||||
purpose: LabelPurpose::Commentary,
|
||||
qualifier: LabelQualifier::None,
|
||||
codec_hint: String::new(),
|
||||
variant: String::new(),
|
||||
}];
|
||||
apply_labels(&labels, &mut titles);
|
||||
if let Stream::Audio(a) = &titles[0].streams[0] {
|
||||
assert_eq!(a.label, "", "label must not contain English purpose text");
|
||||
assert_eq!(a.purpose, LabelPurpose::Commentary);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_sets_qualifier_on_subtitle_sdh() {
|
||||
let mut titles = vec![title_with(vec![subtitle(0x1200, "eng")])];
|
||||
let labels = vec![StreamLabel {
|
||||
stream_number: 1,
|
||||
stream_type: StreamLabelType::Subtitle,
|
||||
language: "eng".into(),
|
||||
name: String::new(),
|
||||
purpose: LabelPurpose::Normal,
|
||||
qualifier: LabelQualifier::Sdh,
|
||||
codec_hint: String::new(),
|
||||
variant: String::new(),
|
||||
}];
|
||||
apply_labels(&labels, &mut titles);
|
||||
|
||||
if let Stream::Subtitle(s) = &titles[0].streams[0] {
|
||||
assert_eq!(s.qualifier, LabelQualifier::Sdh);
|
||||
// SDH doesn't flip the `forced` flag.
|
||||
assert!(!s.forced);
|
||||
} else {
|
||||
panic!("expected subtitle");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_flips_forced_flag_on_subtitle_forced_qualifier() {
|
||||
let mut titles = vec![title_with(vec![subtitle(0x1200, "eng")])];
|
||||
let labels = vec![StreamLabel {
|
||||
stream_number: 1,
|
||||
stream_type: StreamLabelType::Subtitle,
|
||||
language: "eng".into(),
|
||||
name: String::new(),
|
||||
purpose: LabelPurpose::Normal,
|
||||
qualifier: LabelQualifier::Forced,
|
||||
codec_hint: String::new(),
|
||||
variant: String::new(),
|
||||
}];
|
||||
apply_labels(&labels, &mut titles);
|
||||
if let Stream::Subtitle(s) = &titles[0].streams[0] {
|
||||
assert_eq!(s.qualifier, LabelQualifier::Forced);
|
||||
assert!(s.forced);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_indexes_streams_by_type_separately() {
|
||||
// Audio and subtitle each have their own 1-based index; an
|
||||
// Audio #2 label maps to the 2nd audio stream, not the 2nd
|
||||
// stream overall (which could be a subtitle).
|
||||
let mut titles = vec![title_with(vec![
|
||||
video(),
|
||||
audio(0x1100, Codec::TrueHd, AudioChannels::Surround51, "eng"),
|
||||
subtitle(0x1200, "eng"),
|
||||
audio(0x1101, Codec::Ac3, AudioChannels::Stereo, "fra"),
|
||||
])];
|
||||
let labels = vec![
|
||||
audio_label(1, "eng", "Dolby Atmos", ""),
|
||||
audio_label(2, "fra", "Dolby Digital", ""),
|
||||
StreamLabel {
|
||||
stream_number: 1,
|
||||
stream_type: StreamLabelType::Subtitle,
|
||||
language: "eng".into(),
|
||||
name: String::new(),
|
||||
purpose: LabelPurpose::Normal,
|
||||
qualifier: LabelQualifier::Sdh,
|
||||
codec_hint: String::new(),
|
||||
variant: String::new(),
|
||||
},
|
||||
];
|
||||
apply_labels(&labels, &mut titles);
|
||||
|
||||
// Audio #1
|
||||
if let Stream::Audio(a) = &titles[0].streams[1] {
|
||||
assert_eq!(a.label, "Dolby Atmos");
|
||||
}
|
||||
// Audio #2 (4th stream overall)
|
||||
if let Stream::Audio(a) = &titles[0].streams[3] {
|
||||
assert_eq!(a.label, "Dolby Digital");
|
||||
}
|
||||
// Subtitle #1
|
||||
if let Stream::Subtitle(s) = &titles[0].streams[2] {
|
||||
assert_eq!(s.qualifier, LabelQualifier::Sdh);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_ignores_labels_for_nonexistent_streams() {
|
||||
// A label for stream #99 with no matching stream is a no-op.
|
||||
let mut titles = vec![title_with(vec![audio(
|
||||
0x1100,
|
||||
Codec::TrueHd,
|
||||
AudioChannels::Surround51,
|
||||
"eng",
|
||||
)])];
|
||||
let labels = vec![audio_label(99, "fra", "Dolby Digital", "")];
|
||||
apply_labels(&labels, &mut titles);
|
||||
if let Stream::Audio(a) = &titles[0].streams[0] {
|
||||
assert_eq!(a.label, "", "label must be untouched");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_empty_labels_does_not_touch_streams() {
|
||||
let mut titles = vec![title_with(vec![audio(
|
||||
0x1100,
|
||||
Codec::TrueHd,
|
||||
AudioChannels::Surround51,
|
||||
"eng",
|
||||
)])];
|
||||
apply_labels(&[], &mut titles);
|
||||
if let Stream::Audio(a) = &titles[0].streams[0] {
|
||||
assert_eq!(a.label, "");
|
||||
}
|
||||
}
|
||||
|
||||
// ── fill_defaults() tests ───────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn fill_defaults_generates_audio_label_when_empty() {
|
||||
let mut titles = vec![title_with(vec![audio(
|
||||
0x1100,
|
||||
Codec::TrueHd,
|
||||
AudioChannels::Surround71,
|
||||
"eng",
|
||||
)])];
|
||||
fill_defaults(&mut titles);
|
||||
if let Stream::Audio(a) = &titles[0].streams[0] {
|
||||
assert_eq!(a.label, "Dolby TrueHD 7.1");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fill_defaults_preserves_existing_audio_label() {
|
||||
let mut titles = vec![title_with(vec![Stream::Audio(AudioStream {
|
||||
pid: 0x1100,
|
||||
codec: Codec::TrueHd,
|
||||
channels: AudioChannels::Surround71,
|
||||
language: "eng".into(),
|
||||
sample_rate: SampleRate::S48,
|
||||
secondary: false,
|
||||
purpose: LabelPurpose::Normal,
|
||||
label: "Pre-set Atmos".into(),
|
||||
})])];
|
||||
fill_defaults(&mut titles);
|
||||
if let Stream::Audio(a) = &titles[0].streams[0] {
|
||||
assert_eq!(a.label, "Pre-set Atmos");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fill_defaults_generates_video_label_with_hdr() {
|
||||
let mut titles = vec![title_with(vec![video()])];
|
||||
fill_defaults(&mut titles);
|
||||
if let Stream::Video(v) = &titles[0].streams[0] {
|
||||
assert!(v.label.contains("4K"), "expected 4K, got {}", v.label);
|
||||
assert!(v.label.contains("HDR10"), "expected HDR10, got {}", v.label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user