mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
labels: log caught parser panics, pin label tables and selection rules with tests
This commit is contained in:
+373
-21
@@ -302,8 +302,16 @@ pub fn apply(
|
||||
titles: &mut [DiscTitle],
|
||||
) -> Option<FeaturePlaylistHint> {
|
||||
let (labels, winner_hint) =
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| extract(reader, udf)))
|
||||
.unwrap_or_default();
|
||||
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| extract(reader, udf))) {
|
||||
Ok(r) => r,
|
||||
Err(payload) => {
|
||||
tracing::warn!(
|
||||
panic = %panic_message(payload.as_ref()),
|
||||
"label extraction panicked; streams left unlabelled"
|
||||
);
|
||||
Default::default()
|
||||
}
|
||||
};
|
||||
if !labels.is_empty() {
|
||||
apply_labels(&labels, titles);
|
||||
}
|
||||
@@ -313,11 +321,26 @@ pub fn apply(
|
||||
let hint = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
resolve_feature_hint(reader, udf, winner_hint, titles)
|
||||
}))
|
||||
.ok()
|
||||
.flatten();
|
||||
.unwrap_or_else(|payload| {
|
||||
tracing::warn!(
|
||||
panic = %panic_message(payload.as_ref()),
|
||||
"feature-hint resolution panicked; no hint"
|
||||
);
|
||||
None
|
||||
});
|
||||
hint.filter(|h| !h.is_empty())
|
||||
}
|
||||
|
||||
// A caught panic's message, control characters escaped (it can carry disc-derived text).
|
||||
fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
|
||||
let msg = payload
|
||||
.downcast_ref::<&str>()
|
||||
.copied()
|
||||
.or_else(|| payload.downcast_ref::<String>().map(String::as_str))
|
||||
.unwrap_or("non-string panic payload");
|
||||
msg.escape_debug().to_string()
|
||||
}
|
||||
|
||||
// The disc's feature-playlist hint: the winning parser's manifest hint when it
|
||||
// carries one, else the generic BD-J menu-walk (`bdj_feature::resolve`). Kept
|
||||
// separate from extraction so a hint survives an empty/absent label set.
|
||||
@@ -1172,7 +1195,8 @@ pub struct LabelAnalysis {
|
||||
/// MPLS itself was the chosen parser. Diagnostic for the labels-analyze
|
||||
/// tool.
|
||||
pub gap_fill_added: usize,
|
||||
/// Per-playlist chapter summary: `(playlist_filename, chapter_count, duration_secs)`.
|
||||
/// Per-playlist chapter summary rows ([`ChapterSummary`]): playlist filename,
|
||||
/// chapter count, duration in seconds.
|
||||
/// Sourced from MPLS PlaylistMark entries with `mark_type == 1`
|
||||
/// (chapter entries). Ordered by playlist filename. Empty if no
|
||||
/// MPLS files have parseable marks, or the disc isn't Blu-ray.
|
||||
@@ -1465,22 +1489,6 @@ mod registry_tests {
|
||||
let none: Vec<(&'static str, ParseResult)> = Vec::new();
|
||||
assert!(select_result(&none).is_none());
|
||||
}
|
||||
|
||||
/// Per-parser sanity: every parser has both detect and parse
|
||||
/// hooked up. Catches accidental nullification (e.g. someone
|
||||
/// stubbing `parse` to always-None during a refactor).
|
||||
#[test]
|
||||
fn parsers_registry_all_entries_populated() {
|
||||
for (name, detect, parse) in PARSERS {
|
||||
// Function pointers can't be null in safe Rust, so this just fails to
|
||||
// compile if the tuple shape changes (e.g. a 4th field is added)
|
||||
// without updating callers; it also marks "these parsers exist."
|
||||
let _ = (name, detect, parse);
|
||||
}
|
||||
// Non-empty invariant is covered separately by
|
||||
// `parsers_registry_order_locked`, whose assert_eq! on the expected
|
||||
// order fails if PARSERS is empty.
|
||||
}
|
||||
}
|
||||
|
||||
// ── MPLS floor merge tests ─────────────────────────────────────────────────
|
||||
@@ -3409,3 +3417,347 @@ mod feature_hint_pass_tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Label text tables, ordering, and extraction ───────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod label_table_tests {
|
||||
use super::*;
|
||||
use crate::disc::{AudioChannels, Codec, HdrFormat, Stream, SubtitleStream};
|
||||
use crate::udf::fixture::*;
|
||||
|
||||
fn vendor(t: StreamLabelType, n: u16) -> StreamLabel {
|
||||
StreamLabel {
|
||||
stream_id: None,
|
||||
stream_number: n,
|
||||
stream_type: t,
|
||||
language: "eng".into(),
|
||||
name: String::new(),
|
||||
purpose: LabelPurpose::Normal,
|
||||
qualifier: LabelQualifier::None,
|
||||
codec_hint: String::new(),
|
||||
variant: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn derived(clip: &str, pid: u16, n: u16) -> StreamLabel {
|
||||
StreamLabel {
|
||||
stream_id: Some(StreamId {
|
||||
clip_id: clip.into(),
|
||||
pid,
|
||||
}),
|
||||
..vendor(StreamLabelType::Audio, n)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn panic_message_escapes_control_characters() {
|
||||
let p = std::panic::catch_unwind(|| panic!("bad \x1b[31m disc")).unwrap_err();
|
||||
assert_eq!(panic_message(p.as_ref()), "bad \\u{1b}[31m disc");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iso639_b_codes_map_to_their_t_codes() {
|
||||
for (b, t) in [
|
||||
("alb", "sqi"),
|
||||
("arm", "hye"),
|
||||
("baq", "eus"),
|
||||
("bur", "mya"),
|
||||
("chi", "zho"),
|
||||
("cze", "ces"),
|
||||
("dut", "nld"),
|
||||
("fre", "fra"),
|
||||
("geo", "kat"),
|
||||
("ger", "deu"),
|
||||
("gre", "ell"),
|
||||
("ice", "isl"),
|
||||
("mac", "mkd"),
|
||||
("mao", "mri"),
|
||||
("may", "msa"),
|
||||
("per", "fas"),
|
||||
("rum", "ron"),
|
||||
("slo", "slk"),
|
||||
("tib", "bod"),
|
||||
("wel", "cym"),
|
||||
] {
|
||||
assert_eq!(to_iso639_t(b), t, "{b}");
|
||||
assert_eq!(to_iso639_t(&b.to_ascii_uppercase()), t, "{b}");
|
||||
assert!(same_language(b, t), "{b}");
|
||||
}
|
||||
assert_eq!(to_iso639_t("eng"), "eng");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_channel_layouts_have_their_own_text() {
|
||||
for (ch, want) in [
|
||||
(AudioChannels::Mono, "1.0"),
|
||||
(AudioChannels::Stereo, "2.0"),
|
||||
(AudioChannels::Stereo21, "2.1"),
|
||||
(AudioChannels::Surround30, "3.0"),
|
||||
(AudioChannels::Surround31, "3.1"),
|
||||
(AudioChannels::Quad, "4.0"),
|
||||
(AudioChannels::Surround41, "4.1"),
|
||||
(AudioChannels::Surround50, "5.0"),
|
||||
(AudioChannels::Surround51, "5.1"),
|
||||
(AudioChannels::Surround60, "6.0"),
|
||||
(AudioChannels::Surround61, "6.1"),
|
||||
(AudioChannels::Surround70, "7.0"),
|
||||
(AudioChannels::Surround71, "7.1"),
|
||||
] {
|
||||
assert_eq!(
|
||||
generate_audio_label(&Codec::Ac3, &ch, false),
|
||||
format!("Dolby Digital {want}")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn video_resolution_token_boundaries() {
|
||||
let tok = |w, h, i| generate_video_label(&Codec::Hevc, (w, h), i, &HdrFormat::Sdr, false);
|
||||
let name = Codec::Hevc.name();
|
||||
for (w, h, interlaced, want) in [
|
||||
(7680, 4320, false, "8K"),
|
||||
(7679, 4320, false, "4K"),
|
||||
(3840, 2160, false, "4K"),
|
||||
(1920, 1080, false, "1080p"),
|
||||
(1920, 1080, true, "1080i"),
|
||||
(1280, 720, false, "720p"),
|
||||
(720, 576, false, "576p"),
|
||||
(720, 576, true, "576i"),
|
||||
(720, 480, false, "480p"),
|
||||
(720, 480, true, "480i"),
|
||||
(640, 479, false, ""),
|
||||
] {
|
||||
let want = if want.is_empty() {
|
||||
name.to_string()
|
||||
} else {
|
||||
format!("{name} {want}")
|
||||
};
|
||||
assert_eq!(tok(w, h, interlaced), want, "{w}x{h} i={interlaced}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn atmos_marker_only_rides_a_dolby_carrier() {
|
||||
let l = |c| generate_audio_label_atmos(&c, &AudioChannels::Surround71, false);
|
||||
assert_eq!(l(Codec::DtsHdMa), "DTS-HD Master Audio 7.1");
|
||||
assert_eq!(l(Codec::Dts), "DTS 7.1");
|
||||
assert_eq!(l(Codec::Ac3), "Dolby Digital 7.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn atmos_hint_is_consistent_only_with_a_dolby_carrier() {
|
||||
assert!(codec_hint_consistent("Atmos", &Codec::TrueHd));
|
||||
assert!(codec_hint_consistent("Atmos", &Codec::Ac3Plus));
|
||||
assert!(!codec_hint_consistent("Atmos", &Codec::DtsHdMa));
|
||||
assert!(!codec_hint_consistent("Atmos", &Codec::Dts));
|
||||
assert!(codec_hint_consistent("High Resolution", &Codec::DtsHdHr));
|
||||
assert!(codec_hint_consistent("DTS-HD HR", &Codec::DtsHdHr));
|
||||
assert!(!codec_hint_consistent("Master Audio", &Codec::DtsHdHr));
|
||||
assert!(!codec_hint_consistent("High Resolution", &Codec::DtsHdMa));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merged_labels_order_by_slot_then_stream() {
|
||||
let mut labels = vec![
|
||||
vendor(StreamLabelType::Audio, 3),
|
||||
vendor(StreamLabelType::Audio, 1),
|
||||
derived("00002", 0x1100, 1),
|
||||
derived("00001", 0x1101, 1),
|
||||
derived("00001", 0x1100, 1),
|
||||
derived("00001", 0x1102, 0),
|
||||
];
|
||||
sort_labels(&mut labels);
|
||||
let order: Vec<(Option<(&str, u16)>, u16)> = labels
|
||||
.iter()
|
||||
.map(|l| {
|
||||
(
|
||||
l.stream_id.as_ref().map(|i| (i.clip_id.as_str(), i.pid)),
|
||||
l.stream_number,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
order,
|
||||
[
|
||||
(None, 1),
|
||||
(None, 3),
|
||||
(Some(("00001", 0x1102)), 0),
|
||||
(Some(("00001", 0x1100)), 1),
|
||||
(Some(("00001", 0x1101)), 1),
|
||||
(Some(("00002", 0x1100)), 1),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Off the authoritative path, a label binds by bare ordinal only when the stream
|
||||
// STATES the same language; an unknown stream language is not agreement.
|
||||
#[test]
|
||||
fn ordinal_binding_needs_a_stated_subtitle_language() {
|
||||
let mut title = DiscTitle {
|
||||
playlist: "00800.mpls".into(),
|
||||
playlist_id: 800,
|
||||
duration_secs: 7200.0,
|
||||
size_bytes: 0,
|
||||
clips: Vec::new(),
|
||||
streams: vec![Stream::Subtitle(SubtitleStream {
|
||||
pid: 0x12A0,
|
||||
codec: Codec::Pgs,
|
||||
language: String::new(),
|
||||
forced: false,
|
||||
qualifier: LabelQualifier::None,
|
||||
codec_data: None,
|
||||
})],
|
||||
chapters: Vec::new(),
|
||||
extents: Vec::new(),
|
||||
content_format: crate::disc::ContentFormat::BdTs,
|
||||
codec_privates: Vec::new(),
|
||||
};
|
||||
let labels = vec![StreamLabel {
|
||||
qualifier: LabelQualifier::Forced,
|
||||
..vendor(StreamLabelType::Subtitle, 1)
|
||||
}];
|
||||
apply_labels(&labels, std::slice::from_mut(&mut title));
|
||||
let Stream::Subtitle(s) = &title.streams[0] else {
|
||||
unreachable!()
|
||||
};
|
||||
assert!(!s.forced);
|
||||
assert_eq!(s.qualifier, LabelQualifier::None);
|
||||
}
|
||||
|
||||
// Minimal MPLS: one play item on `clip` whose STN lists one primary audio stream `pid`.
|
||||
fn mpls_bytes(clip: &[u8; 5], pid: u16) -> Vec<u8> {
|
||||
let mut item = Vec::new();
|
||||
item.extend_from_slice(clip);
|
||||
item.extend_from_slice(b"M2TS");
|
||||
item.extend_from_slice(&[0u8; 3]);
|
||||
item.extend_from_slice(&0u32.to_be_bytes());
|
||||
item.extend_from_slice(&(7000u32 * 45000).to_be_bytes());
|
||||
item.extend_from_slice(&[0u8; 12]); // UO mask, misc, still
|
||||
item.extend_from_slice(&[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
item.extend_from_slice(&[3, 0x01]);
|
||||
item.extend_from_slice(&pid.to_be_bytes());
|
||||
item.extend_from_slice(&[5, 0x81, 0x61]);
|
||||
item.extend_from_slice(b"eng");
|
||||
let mut pl = vec![0u8; 6];
|
||||
pl.extend_from_slice(&1u16.to_be_bytes());
|
||||
pl.extend_from_slice(&[0u8; 2]);
|
||||
pl.extend_from_slice(&(item.len() as u16).to_be_bytes());
|
||||
pl.extend_from_slice(&item);
|
||||
let pl_len = (pl.len() - 4) as u32;
|
||||
pl[0..4].copy_from_slice(&pl_len.to_be_bytes());
|
||||
let mut buf = b"MPLS0200".to_vec();
|
||||
buf.extend_from_slice(&40u32.to_be_bytes());
|
||||
buf.extend_from_slice(&[0u8; 28]);
|
||||
buf.extend_from_slice(&pl);
|
||||
buf
|
||||
}
|
||||
|
||||
fn dir(name: &str, icb: u32, files: Vec<FileSpec>, subdirs: Vec<DirSpec>) -> DirSpec {
|
||||
DirSpec {
|
||||
name: name.to_string(),
|
||||
icb_lba: icb,
|
||||
dir_data_lba: icb + 1,
|
||||
files,
|
||||
subdirs,
|
||||
}
|
||||
}
|
||||
|
||||
fn open(root: DirSpec) -> (MemDisc, UdfFs) {
|
||||
let mut disc = MemDisc::new();
|
||||
build_udf_skeleton(&mut disc, 10);
|
||||
lay_dir(&mut disc, &root);
|
||||
let udf = crate::udf::read_filesystem(&mut disc).expect("fs");
|
||||
(disc, udf)
|
||||
}
|
||||
|
||||
// A framework winner keeps its own labels and hint, and the MPLS floor adds the
|
||||
// streams it named no label for.
|
||||
#[test]
|
||||
fn extract_merges_the_mpls_floor_into_a_framework_winner() {
|
||||
let dcx = br#"<dcx><disc>
|
||||
<playlist id="00800" name="feature" durs="7000"><audio id="01" lang="eng"/></playlist>
|
||||
</disc></dcx>"#;
|
||||
let (mut disc, udf) = open(dir(
|
||||
"",
|
||||
10,
|
||||
vec![],
|
||||
vec![dir(
|
||||
"BDMV",
|
||||
20,
|
||||
vec![],
|
||||
vec![
|
||||
dir(
|
||||
"JAR",
|
||||
30,
|
||||
vec![],
|
||||
vec![dir(
|
||||
"00000",
|
||||
40,
|
||||
vec![file_with("dcx.xml", 100, 2000, dcx.to_vec(), true)],
|
||||
vec![],
|
||||
)],
|
||||
),
|
||||
dir(
|
||||
"PLAYLIST",
|
||||
50,
|
||||
vec![file_with(
|
||||
"00800.mpls",
|
||||
101,
|
||||
3000,
|
||||
mpls_bytes(b"00001", 0x1100),
|
||||
true,
|
||||
)],
|
||||
vec![],
|
||||
),
|
||||
],
|
||||
)],
|
||||
));
|
||||
let (labels, hint) = extract(&mut disc, &udf);
|
||||
assert_eq!(labels.iter().filter(|l| l.stream_id.is_none()).count(), 1);
|
||||
assert_eq!(labels.iter().filter(|l| l.stream_id.is_some()).count(), 1);
|
||||
assert_eq!(hint.and_then(|h| h.playlist_id), Some(800));
|
||||
}
|
||||
|
||||
// The first copy of a manifest that reads non-empty wins; names match without case.
|
||||
#[test]
|
||||
fn read_jar_file_skips_empty_copies_and_matches_names_without_case() {
|
||||
let (mut disc, udf) = open(dir(
|
||||
"",
|
||||
10,
|
||||
vec![],
|
||||
vec![dir(
|
||||
"BDMV",
|
||||
20,
|
||||
vec![],
|
||||
vec![dir(
|
||||
"JAR",
|
||||
30,
|
||||
vec![],
|
||||
vec![
|
||||
dir(
|
||||
"00000",
|
||||
40,
|
||||
vec![file_with("playlists.xml", 100, 2000, Vec::new(), true)],
|
||||
vec![],
|
||||
),
|
||||
dir(
|
||||
"00001",
|
||||
50,
|
||||
vec![file_with(
|
||||
"PLAYLISTS.XML",
|
||||
101,
|
||||
3000,
|
||||
b"<p/>".to_vec(),
|
||||
true,
|
||||
)],
|
||||
vec![],
|
||||
),
|
||||
],
|
||||
)],
|
||||
)],
|
||||
));
|
||||
let got = read_jar_file(&mut disc, &udf, "playlists.xml");
|
||||
assert_eq!(got.as_deref(), Some(&b"<p/>"[..]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,6 +342,60 @@ fn pick<'a>(cands: &[Candidate<'a>], key: Key, require_audio: bool) -> Option<&'
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn hint_id(doc: &str) -> Option<u16> {
|
||||
feature_hint(doc).and_then(|h| h.playlist_id)
|
||||
}
|
||||
|
||||
// Tier 1 ranks exact `Feature` names by duration, not document order.
|
||||
#[test]
|
||||
fn exact_feature_tier_prefers_the_longest_duration() {
|
||||
let doc = r#"
|
||||
<playlist name="Feature" id="00001" duration="600" aud="eng"/>
|
||||
<playlist name="Feature" id="00002" duration="7000" aud="eng"/>
|
||||
"#;
|
||||
assert_eq!(hint_id(doc), Some(2));
|
||||
}
|
||||
|
||||
// A zero-audio exact `Feature` still wins over an audio-bearing extra: its id feeds the hint.
|
||||
#[test]
|
||||
fn zero_audio_exact_feature_is_still_selected() {
|
||||
let doc = r#"
|
||||
<playlist name="Feature" id="00800"/>
|
||||
<playlist name="Bonus" id="00900" aud="eng,fra"/>
|
||||
"#;
|
||||
assert_eq!(hint_id(doc), Some(800));
|
||||
}
|
||||
|
||||
// With equal audio counts, tier 3 breaks the tie by the longer duration.
|
||||
#[test]
|
||||
fn most_audio_tier_breaks_a_tie_by_longer_duration() {
|
||||
let doc = r#"
|
||||
<playlist name="Menu" id="00001" duration="600" aud="eng,fra"/>
|
||||
<playlist name="Other" id="00002" duration="7000" aud="eng,fra"/>
|
||||
"#;
|
||||
assert_eq!(hint_id(doc), Some(2));
|
||||
}
|
||||
|
||||
// Exactly the minimum feature length is a feature; one second less is a decoy.
|
||||
#[test]
|
||||
fn minimum_feature_length_is_inclusive() {
|
||||
let doc = |secs: u64| format!(r#"<playlist name="Feature" id="00800" duration="{secs}"/>"#);
|
||||
assert_eq!(hint_id(&doc(60)), Some(800));
|
||||
assert_eq!(hint_id(&doc(59)), None);
|
||||
}
|
||||
|
||||
// Every duration attribute spelling arms the sub-minute decoy guard.
|
||||
#[test]
|
||||
fn every_duration_attribute_spelling_is_honoured() {
|
||||
for key in ["duration", "durs", "dur", "runtime", "length", "len"] {
|
||||
let doc = format!(
|
||||
r#"<playlist name="Feature" id="00001" {key}="10"/>
|
||||
<playlist name="Feature" id="00800" {key}="7000"/>"#
|
||||
);
|
||||
assert_eq!(hint_id(&doc), Some(800), "{key}");
|
||||
}
|
||||
}
|
||||
|
||||
// Immunity pin, section-boundary half.
|
||||
#[test]
|
||||
fn a_playlists_stream_list_cannot_run_into_the_next_playlist() {
|
||||
|
||||
@@ -1186,4 +1186,53 @@ mod tests {
|
||||
assert!(!is_stream_token("Video Stream 1"));
|
||||
assert!(!is_stream_token("entry-markEa9"));
|
||||
}
|
||||
|
||||
// A notice-clip name (`deu_Warning`) has the token shape: it is recorded as an
|
||||
// uncatalogued component, yields no label, and still occupies its stream slot.
|
||||
#[test]
|
||||
fn notice_clip_token_is_recorded_unlabelled_and_takes_a_slot() {
|
||||
let mut acc = UnknownParts::default();
|
||||
assert!(parse_token_inner("deu_Warning", Some(&mut acc)).is_none());
|
||||
assert_eq!(acc.seen.iter().collect::<Vec<_>>(), ["WARNING"]);
|
||||
|
||||
let mut acc = UnknownParts::default();
|
||||
let labels = assign_labels(
|
||||
&strs(&["FPL_Main", "Audio Stream 1", "deu_Warning", "eng_MLP_"]),
|
||||
&mut acc,
|
||||
);
|
||||
assert_eq!(labels.len(), 1);
|
||||
assert_eq!(labels[0].stream_number, 3);
|
||||
assert_eq!(acc.total, 1);
|
||||
}
|
||||
|
||||
// Lay `bluray_project.bin` into /BDMV/JAR/00000 and run the real `parse`.
|
||||
fn parse_blob(blob: &[u8]) -> Option<ParseResult> {
|
||||
use crate::udf::fixture::*;
|
||||
let dir = |name: &str, icb, data, files, subdirs| DirSpec {
|
||||
name: name.to_string(),
|
||||
icb_lba: icb,
|
||||
dir_data_lba: data,
|
||||
files,
|
||||
subdirs,
|
||||
};
|
||||
let file = file_with("bluray_project.bin", 100, 2000, blob.to_vec(), true);
|
||||
let sub = dir("00000", 54, 55, vec![file], vec![]);
|
||||
let jar = dir("JAR", 52, 53, vec![], vec![sub]);
|
||||
let bdmv = dir("BDMV", 12, 13, vec![], vec![jar]);
|
||||
let root = dir("", 10, 11, vec![], vec![bdmv]);
|
||||
let mut disc = MemDisc::new();
|
||||
build_udf_skeleton(&mut disc, 10);
|
||||
lay_dir(&mut disc, &root);
|
||||
let udf = crate::udf::read_filesystem(&mut disc).expect("fs");
|
||||
parse(&mut disc, &udf)
|
||||
}
|
||||
|
||||
// Confidence is High only when every token component was catalogued.
|
||||
#[test]
|
||||
fn parse_confidence_reflects_uncatalogued_components() {
|
||||
let known = parse_blob(b"FPL_Main\0Audio Stream 1\0eng_MLP_\0").expect("labels");
|
||||
assert_eq!(known.confidence, Confidence::High);
|
||||
let unknown = parse_blob(b"FPL_Main\0Audio Stream 1\0eng_MLP_NEWFLAG_\0").expect("labels");
|
||||
assert_eq!(unknown.confidence, Confidence::Medium);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,6 +495,72 @@ fn has_word(haystack: &str, needle: &str) -> bool {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// ISO 639-1 -> 639-2/T, checked against an independently written list so a mistyped or
|
||||
// swapped row cannot pass on the table's own shape.
|
||||
#[test]
|
||||
fn iso639_1_values_match_the_standard() {
|
||||
let want = [
|
||||
"aa:aar", "ab:abk", "ae:ave", "af:afr", "ak:aka", "am:amh", "an:arg", "ar:ara",
|
||||
"as:asm", "av:ava", "ay:aym", "az:aze", "ba:bak", "be:bel", "bg:bul", "bh:bih",
|
||||
"bi:bis", "bm:bam", "bn:ben", "bo:bod", "br:bre", "bs:bos", "ca:cat", "ce:che",
|
||||
"ch:cha", "co:cos", "cr:cre", "cs:ces", "cu:chu", "cv:chv", "cy:cym", "da:dan",
|
||||
"de:deu", "dv:div", "dz:dzo", "ee:ewe", "el:ell", "en:eng", "eo:epo", "es:spa",
|
||||
"et:est", "eu:eus", "fa:fas", "ff:ful", "fi:fin", "fj:fij", "fo:fao", "fr:fra",
|
||||
"fy:fry", "ga:gle", "gd:gla", "gl:glg", "gn:grn", "gu:guj", "gv:glv", "ha:hau",
|
||||
"he:heb", "hi:hin", "ho:hmo", "hr:hrv", "ht:hat", "hu:hun", "hy:hye", "hz:her",
|
||||
"ia:ina", "id:ind", "ie:ile", "ig:ibo", "ii:iii", "ik:ipk", "io:ido", "is:isl",
|
||||
"it:ita", "iu:iku", "ja:jpn", "jv:jav", "ka:kat", "kg:kon", "ki:kik", "kj:kua",
|
||||
"kk:kaz", "kl:kal", "km:khm", "kn:kan", "ko:kor", "kr:kau", "ks:kas", "ku:kur",
|
||||
"kv:kom", "kw:cor", "ky:kir", "la:lat", "lb:ltz", "lg:lug", "li:lim", "ln:lin",
|
||||
"lo:lao", "lt:lit", "lu:lub", "lv:lav", "mg:mlg", "mh:mah", "mi:mri", "mk:mkd",
|
||||
"ml:mal", "mn:mon", "mr:mar", "ms:msa", "mt:mlt", "my:mya", "na:nau", "nb:nob",
|
||||
"nd:nde", "ne:nep", "ng:ndo", "nl:nld", "nn:nno", "no:nor", "nr:nbl", "nv:nav",
|
||||
"ny:nya", "oc:oci", "oj:oji", "om:orm", "or:ori", "os:oss", "pa:pan", "pi:pli",
|
||||
"pl:pol", "ps:pus", "pt:por", "qu:que", "rm:roh", "rn:run", "ro:ron", "ru:rus",
|
||||
"rw:kin", "sa:san", "sc:srd", "sd:snd", "se:sme", "sg:sag", "si:sin", "sk:slk",
|
||||
"sl:slv", "sm:smo", "sn:sna", "so:som", "sq:sqi", "sr:srp", "ss:ssw", "st:sot",
|
||||
"su:sun", "sv:swe", "sw:swa", "ta:tam", "te:tel", "tg:tgk", "th:tha", "ti:tir",
|
||||
"tk:tuk", "tl:tgl", "tn:tsn", "to:ton", "tr:tur", "ts:tso", "tt:tat", "tw:twi",
|
||||
"ty:tah", "ug:uig", "uk:ukr", "ur:urd", "uz:uzb", "ve:ven", "vi:vie", "vo:vol",
|
||||
"wa:wln", "wo:wol", "xh:xho", "yi:yid", "yo:yor", "za:zha", "zh:zho", "zu:zul",
|
||||
];
|
||||
assert_eq!(want.len(), 184);
|
||||
for pair in want {
|
||||
let (two, three) = pair.split_once(':').unwrap();
|
||||
assert_eq!(iso639_1_to_iso639_2(two), Some(three), "{two}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iso639_1_withdrawn_dvd_codes_resolve_to_their_replacements() {
|
||||
for (old, three) in [
|
||||
("iw", "heb"),
|
||||
("in", "ind"),
|
||||
("ji", "yid"),
|
||||
("jw", "jav"),
|
||||
("mo", "ron"),
|
||||
] {
|
||||
assert_eq!(iso639_1_to_iso639_2(old), Some(three), "{old}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mandarin_chinese_compound_keeps_its_variant() {
|
||||
assert_eq!(
|
||||
lang("Mandarin Chinese"),
|
||||
Some(LangInfo {
|
||||
code: "zho",
|
||||
variant: "Mandarin"
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn description_word_marks_descriptive_purpose() {
|
||||
assert_eq!(purpose("English Description"), LabelPurpose::Descriptive);
|
||||
assert_eq!(purpose("Descriptions"), LabelPurpose::Normal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iso639_1_accepts_jw_and_mo_dvd_aliases() {
|
||||
assert_eq!(iso639_1_to_iso639_2("jw"), Some("jav"));
|
||||
|
||||
Reference in New Issue
Block a user