//! Media identification (extensions/media-identify.md): the committed corpus `identify-conformance/` //! regenerates byte for byte with every result, the extension's tables are the reference's, and //! `wai identify` gives what the library gives. #![cfg(feature = "identify")] use std::collections::BTreeMap; use std::path::PathBuf; use std::process::Command; use wai::identify::{corpus, embedded_table, identify_bytes, signature_table}; fn wai_dir() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..") } fn committed() -> BTreeMap> { let root = wai_dir().join("identify-conformance"); let mut out = BTreeMap::new(); for sub in ["ext", "gen"] { for e in std::fs::read_dir(root.join(sub)).expect("a corpus directory").flatten() { let name = e.file_name().to_string_lossy().to_string(); out.insert(format!("{sub}/{name}"), std::fs::read(e.path()).unwrap()); } } out.insert("vectors.json".into(), std::fs::read(root.join("vectors.json")).expect("vectors.json")); out } #[test] fn the_corpus_regenerates_and_every_result_holds() { let files = committed(); assert!(files.keys().filter(|k| k.starts_with("ext/")).count() >= 40, "the external vectors"); let fails = corpus::verify(&files); assert!(fails.is_empty(), "{}: run `wai_identify_vectors gen`", fails.join("\n")); } fn spec() -> String { std::fs::read_to_string(wai_dir().join("extensions/media-identify.md")).expect("the extension") } fn section(text: &str, heading: &str) -> String { let at = text.find(&format!("\n{heading}")).unwrap_or_else(|| panic!("{heading}")); let end = text[at + 1..].find("\n## ").map_or(text.len(), |e| at + 1 + e); text[at..end].to_string() } /// The table rows of `text`, their cells without their backticks. fn rows(text: &str) -> Vec> { text.lines() .filter(|l| l.starts_with("| ") && !l.starts_with("| id ") && !l.starts_with("|-")) .map(|l| l.trim_matches('|').split('|').map(|c| c.trim().trim_matches('`').to_string()).collect()) .collect() } #[test] fn section_6_lists_the_references_signatures() { let text = spec(); let listed = rows(§ion(&text, "## 6.")); let table = signature_table(); assert_eq!(listed.len(), table.len()); for (row, (id, kind, parts, caps)) in listed.iter().zip(table) { assert_eq!(row[0], id); assert_eq!(row[1], kind); let bytes: Vec = parts.iter().map(|(at, b)| format!("{at}:{}", b.iter().map(|x| format!("{x:02x}")).collect::())).collect(); assert_eq!(row[2], bytes.join(" "), "{id}"); assert_eq!(row[3], caps.join(" "), "{id}"); } } #[test] fn section_7_lists_the_references_embedded_signatures() { let text = spec(); let listed = rows(§ion(&text, "## 7.")); let table = embedded_table(); assert_eq!(listed.len(), table.len()); for (row, (id, bytes, back)) in listed.iter().zip(table) { assert_eq!(row[0], id); assert_eq!(row[1], bytes.iter().map(|x| format!("{x:02x}")).collect::()); assert_eq!(row[2], back.to_string()); assert!(bytes.len() >= 8, "{id}: ยง7's floor"); } } #[test] fn every_reason_and_verdict_is_stated_and_reached() { let text = spec(); let doc: serde_json::Value = serde_json::from_slice(&committed()["vectors.json"]).unwrap(); let results: Vec<&serde_json::Value> = doc["vectors"].as_array().unwrap().iter().map(|v| &v["result"]).collect(); for why in ["overrun", "invalid", "crc", "limit", "unknown_size"] { assert!(section(&text, "## 3.").contains(&format!("| `{why}` |")), "{why}"); assert!( results.iter().any(|r| r["candidates"].as_array().unwrap().iter().any(|c| c["stopped"]["why"] == why)), "no vector reaches {why}" ); } for verdict in ["identified", "ambiguous", "signature_only", "unknown"] { assert!(section(&text, "## 4.").contains(&format!("`{verdict}`")), "{verdict}"); assert!(results.iter().any(|r| r["verdict"] == verdict), "no vector reaches {verdict}"); } for strength in ["signature", "structure", "parameters"] { assert!(results.iter().any(|r| r["candidates"].as_array().unwrap().iter().any(|c| c["strength"] == strength)), "{strength}"); } } fn wai() -> Command { Command::new(env!("CARGO_BIN_EXE_wai")) } #[test] fn wai_identify_gives_what_the_library_gives() { let p = wai_dir().join("identify-conformance/ext/avc-aac.mp4"); let o = wai().args(["identify", p.to_str().unwrap(), "--json"]).output().unwrap(); assert!(o.status.success(), "{}", String::from_utf8_lossy(&o.stderr)); let v: serde_json::Value = serde_json::from_slice(&o.stdout).unwrap(); assert_eq!(v["op"], "media.inspect"); assert_eq!(v["data"], identify_bytes(&std::fs::read(&p).unwrap())); let o = wai().args(["identify", p.to_str().unwrap()]).output().unwrap(); let text = String::from_utf8_lossy(&o.stdout); assert!(text.contains("isobmff (container, structure)"), "{text}"); assert!(text.contains("video avc1 (avc) 32x32"), "{text}"); assert!(text.contains("wai.video.avc"), "{text}"); } #[test] fn a_directory_and_a_missing_file_are_errors_not_results() { let d = wai_dir(); let o = wai().args(["identify", d.to_str().unwrap()]).output().unwrap(); assert!(!o.status.success()); assert!(String::from_utf8_lossy(&o.stderr).contains("a directory")); let o = wai().args(["identify", "/no/such/file"]).output().unwrap(); assert!(!o.status.success()); let o = wai().args(["identify"]).output().unwrap(); assert_eq!(o.status.code(), Some(2)); }