//! The text layer (`graphics-vector.md` §2.11, §5): the text digests, each block's //! active set, and the projection and the transcript as JSON. //! //! Each test reads committed payloads of `vector-conformance/` and holds what §5 says //! of them, written out here by hand: the JSON is never taken from the code it checks. //! The worked example is read from the extension's own text. //! //! Every leg runs these: natively, and under WASI with the `wai` directory preopened. use std::path::PathBuf; use wai_vector::json::hex; use wai_vector::refuse::Code; use wai_vector::sha256::sha256; use wai_vector::text::{self, TextLayer}; use wai_vector::wvg1::{self, Loaded, SetStore}; fn wai() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..") } fn read(rel: &str) -> Vec { std::fs::read(wai().join(rel)).unwrap_or_else(|e| panic!("{rel}: {e}")) } /// A resolver holding every set under `sets/`. fn store() -> SetStore { let mut s = SetStore::new(); let mut sets: Vec = std::fs::read_dir(wai().join("vector-conformance/sets")).unwrap().flatten().map(|e| e.path()).collect(); sets.sort(); for p in sets { s.add(std::fs::read(p).unwrap()); } s } fn payload(group: &str, name: &str) -> Vec { read(&format!("vector-conformance/vectors/{group}/{name}.wvg1")) } fn load(group: &str, name: &str) -> Loaded { wvg1::load(&payload(group, name), &mut store()).unwrap_or_else(|e| panic!("{group}/{name}: {}", e.code.name())) } /// The committed text record of an `a11y` vector, by line. fn committed(name: &str) -> Vec { let text = String::from_utf8(read(&format!("vector-conformance/expected/a11y/{name}.a11y.jsonl"))).unwrap(); assert!(text.ends_with('\n'), "{name}: every line of a record ends in U+000A"); text.lines().map(String::from).collect() } /// A node as §5.4 writes it: `active` (a transcript's) first when given, then `dir`, /// `group`, `index`, `lang`, `mode`, `purpose`, `ruby` when given, `text`. #[allow(clippy::too_many_arguments)] fn node(active: Option<&str>, dir: u8, group: u16, index: usize, lang: &str, mode: u8, purpose: u8, ruby: Option<&str>, text: &str) -> String { let active = active.map(|a| format!("\"active\":{a},")).unwrap_or_default(); let ruby = ruby.map(|r| format!("\"ruby\":{r},")).unwrap_or_default(); format!("{{{active}\"dir\":{dir},\"group\":{group},\"index\":{index},\"lang\":\"{lang}\",\"mode\":{mode},\"purpose\":{purpose},{ruby}\"text\":\"{text}\"}}") } /// A plain English label: left to right, ungrouped, horizontal, purpose 7. fn label(active: Option<&str>, index: usize, text: &str) -> String { node(active, 0, 0, index, "en", 0, 7, None, text) } fn array(nodes: &[String]) -> String { format!("[{}]", nodes.join(",")) } /// A record's line for one frame. fn frame(num: u32, den: u32, g: u64, nodes: &[String]) -> String { format!("{{\"den\":{den},\"g\":{g},\"num\":{num},\"projection\":{}}}", array(nodes)) } /// The fenced block that follows `marker` in the extension's §5.4, by line. fn fenced_after(marker: &str) -> Vec { let doc = String::from_utf8(read("extensions/graphics-vector.md")).unwrap(); let at = doc.find(marker).unwrap_or_else(|| panic!("the extension has no {marker}")); let rest = &doc[at..]; let open = rest.find("```").expect("a fenced block"); let body = &rest[open..]; let body = &body[body.find('\n').unwrap() + 1..]; body[..body.find("```").expect("the fence closes")].lines().map(String::from).collect() } #[test] fn the_worked_example_of_section_5_4_byte_for_byte() { let example = fenced_after("**A worked example.**"); assert_eq!(example.len(), 2, "the example is a transcript and one frame's line"); let l = load("a11y", "worked-example"); let layer = TextLayer::new(&l.doc); // The transcript, and frame 30 at 25 fps: tick 144000, after the annotation's window. assert_eq!(layer.transcript(), example[0], "the transcript"); assert_eq!(l.doc.rates[0].ticks_per_frame(l.doc.header.tb), 4800); assert_eq!(format!("{{\"den\":1,\"g\":30,\"num\":25,\"projection\":{}}}", layer.projection(0, 30).unwrap()), example[1], "frame 30"); // The committed record holds both lines: the transcript first, then frames 0 to 74. let lines = committed("worked-example"); assert_eq!(lines.len(), 1 + 75); assert_eq!(lines[0], example[0]); assert_eq!(lines[1 + 30], example[1]); assert_eq!(text::record(&l.doc), lines.iter().map(|l| format!("{l}\n")).collect::()); // What the example says of itself, written out again here. // The Arabic block's words: common nouns, "the door of the house". let words = "\u{0628}\u{0627}\u{0628} \u{0627}\u{0644}\u{0628}\u{064A}\u{062A}"; let both = "[[24000,264000]]"; let want = array(&[ node(Some(both), 1, 1, 0, "ar", 0, 3, None, words), node(Some(both), 0, 1, 1, "en", 0, 3, None, "Director of photography"), node(Some("[[24000,120000]]"), 0, 1, 2, "en", 0, 7, Some("{\"base\":1,\"len\":8,\"start\":0}"), "DOP"), ]); assert_eq!(example[0], want); assert_eq!(example[1], frame(25, 1, 30, &[node(None, 1, 1, 0, "ar", 0, 3, None, words), node(None, 0, 1, 1, "en", 0, 3, None, "Director of photography")])); // Before the windows open nothing is projected; inside the annotation's, all three. assert_eq!(lines[1 + 4], frame(25, 1, 4, &[])); assert!(lines[1 + 5].contains("\"text\":\"DOP\"") && lines[1 + 24].contains("\"text\":\"DOP\"") && !lines[1 + 25].contains("DOP")); assert_eq!(lines[1 + 55], frame(25, 1, 55, &[]), "tick 264000 is past both windows"); } #[test] fn purpose_200_is_written_raw() { let l = load("a11y", "purposes"); let names = ["title", "credit", "lower_third", "caption", "subtitle", "forced_narrative", "label", "legal", "alert", "logo", "graphic", "data", "decorative", "p14", "p200", "p255"]; let purposes: Vec = (1..=13).chain([14, 200, 255]).collect(); assert_eq!(l.doc.texts.iter().map(|t| t.purpose).collect::>(), purposes, "the TEXT bytes"); // Every node carries the TEXT byte itself; block 12, decorative, is in neither. let nodes = |active: Option<&str>| -> Vec { (0..16).filter(|&i| i != 12).map(|i| node(active, 0, 0, i, "en", 0, purposes[i], None, names[i])).collect() }; let layer = TextLayer::new(&l.doc); assert_eq!(layer.transcript(), array(&nodes(Some("[[0,24000]]")))); assert_eq!(layer.projection(0, 0).unwrap(), array(&nodes(None))); let t = layer.transcript(); assert!(t.contains("\"index\":14,\"lang\":\"en\",\"mode\":0,\"purpose\":200,\"text\":\"p200\""), "200 is written as 200"); assert!(t.contains("\"purpose\":14,") && t.contains("\"purpose\":255,")); assert_eq!(t.matches("\"purpose\":7,").count(), 1, "only the label block reads 7"); assert_eq!(committed("purposes")[0], t); } #[test] fn touching_intervals_merge() { let l = load("a11y", "intervals"); let want = array(&[ label(Some("[[0,48000]]"), 0, "touching windows"), label(Some("[[0,60000]]"), 1, "overlapping windows"), label(Some("[[0,24000],[24001,48000]]"), 2, "apart windows"), label(Some("[[0,96000]]"), 3, "nested windows"), label(Some("[[0,48000],[72000,96000]]"), 4, "later then earlier"), ]); assert_eq!(text::transcript(&l.doc), want); assert_eq!(committed("intervals")[0], want); // The windows as carried: [0, 24000) and [24000, 48000) for block 0. let wins: Vec<(u32, u32)> = l.doc.items.iter().filter(|it| it.text == 0).map(|it| (it.t_on, it.t_off)).collect(); assert_eq!(wins, [(0, 24_000), (24_000, 48_000)]); assert_eq!(text::active_sets(&l.doc)[0], [(0, 48_000)]); // Tick 24000 is in neither interval of block 2, which stays two intervals. assert_eq!(text::active_sets(&l.doc)[2], [(0, 24_000), (24_001, 48_000)]); } #[test] fn windows_are_clipped_to_the_document() { let l = load("a11y", "clipped"); assert_eq!((l.doc.header.t0, l.doc.header.dur), (50_000, 94_000), "the document's window is [50000, 144000)"); let want = array(&[ label(Some("[[50000,72000]]"), 0, "starts before"), label(Some("[[120000,144000]]"), 1, "ends after"), label(Some("[[50000,144000]]"), 2, "spans it"), label(Some("[]"), 3, "over by then"), label(Some("[]"), 4, "not yet"), label(Some("[[50000,144000]]"), 5, "two parts"), ]); assert_eq!(text::transcript(&l.doc), want); // The windows as carried run past the document on both sides. assert_eq!((l.doc.items[0].t_on, l.doc.items[0].t_off), (0, 72_000)); assert_eq!((l.doc.items[2].t_on, l.doc.items[2].t_off), (0, 1 << 31)); // Frames 11 to 29: the first at tick 52800, the last at 139200. let lines = committed("clipped"); assert_eq!(lines.len(), 1 + 19); assert_eq!(lines[0], want); assert_eq!(lines[1], frame(25, 1, 11, &[label(None, 0, "starts before"), label(None, 2, "spans it"), label(None, 5, "two parts")])); assert_eq!(lines[19], frame(25, 1, 29, &[label(None, 1, "ends after"), label(None, 2, "spans it"), label(None, 5, "two parts")])); } #[test] fn the_ruby_object_is_base_len_start() { let l = load("a11y", "ruby"); let base = "\u{0939}\u{093F}\u{0902}\u{0926}\u{0940}"; assert_eq!(base.len(), 15); // TEXT carries r = 1 for block 0; the JSON carries the block's index, r − 1 = 0. assert_eq!(l.doc.texts.iter().map(|t| (t.ruby, t.ruby_start, t.ruby_len)).collect::>(), [(0, 0, 0), (1, 0, 9), (1, 9, 6)]); let first = "{\"base\":0,\"len\":9,\"start\":0}"; let second = "{\"base\":0,\"len\":6,\"start\":9}"; let want = array(&[ node(Some("[[0,96000]]"), 0, 0, 0, "hi", 0, 7, None, base), node(Some("[[0,48000]]"), 0, 0, 1, "hi-Latn", 0, 7, Some(first), "hin"), node(Some("[[48000,120000]]"), 0, 0, 2, "hi-Latn", 0, 7, Some(second), "di"), ]); let layer = TextLayer::new(&l.doc); assert_eq!(layer.transcript(), want); // Frame 21, tick 100800: the base's window has ended, and the annotation keeps its // ruby object. assert_eq!(layer.projection(0, 21).unwrap(), array(&[node(None, 0, 0, 2, "hi-Latn", 0, 7, Some(second), "di")])); assert_eq!(committed("ruby")[1 + 21], frame(25, 1, 21, &[node(None, 0, 0, 2, "hi-Latn", 0, 7, Some(second), "di")])); // The range rebuilds the annotated part of the base exactly. assert_eq!(&base[0..9], "\u{0939}\u{093F}\u{0902}"); assert_eq!(&base[9..15], "\u{0926}\u{0940}"); } #[test] fn mode_is_in_the_block_identity() { let l = load("a11y", "modes"); let d = &l.doc; assert_eq!(d.texts.iter().map(|t| t.mode).collect::>(), [1, 2, 0]); // The three blocks are equal in every other field. for t in 1..3 { let (a, b) = (d.texts[0], d.texts[t]); assert_eq!((a.purpose, a.dir, a.lang_len, a.group, a.ruby, a.str_len), (b.purpose, b.dir, b.lang_len, b.group, b.ruby, b.str_len)); assert_eq!((d.lang(0), d.string(0)), (d.lang(t), d.string(t))); } // "wai:text-block", 0x01, purpose 7, dir 0, the mode, lang_len 2, "en", str_len 3 as // u32 little-endian, "WAI": built here byte by byte. let identity = |mode: u8| { let mut m = b"wai:text-block".to_vec(); m.extend_from_slice(&[0x01, 7, 0, mode, 2, b'e', b'n', 3, 0, 0, 0, b'W', b'A', b'I']); sha256(&m) }; for (t, mode) in [(0usize, 1u8), (1, 2), (2, 0)] { assert_eq!(text::block_identity(d, t), identity(mode), "block {t}, mode {mode}"); } assert!(identity(0) != identity(1) && identity(1) != identity(2) && identity(0) != identity(2)); // The same digests from another SHA-256 (Python's hashlib), over the same bytes. assert_eq!(hex(&text::block_identity(d, 2)), IDENTITY_MODE_0); assert_eq!(hex(&text::block_identity(d, 0)), IDENTITY_MODE_1); assert_eq!(hex(&text::block_identity(d, 1)), IDENTITY_MODE_2); // The expected file states them in block order, and each node carries its mode. let expected = String::from_utf8(read("vector-conformance/expected/a11y/modes.json")).unwrap(); assert!(expected.contains(&format!("\"blocks\":[\"{IDENTITY_MODE_1}\",\"{IDENTITY_MODE_2}\",\"{IDENTITY_MODE_0}\"]"))); let nodes: Vec = [1u8, 2, 0].iter().enumerate().map(|(i, &m)| node(Some("[[0,24000]]"), 0, 0, i, "en", m, 7, None, "WAI")).collect(); assert_eq!(text::transcript(d), array(&nodes)); } // hashlib.sha256(b"wai:text-block\x01\x07\x00" + bytes([mode]) + b"\x02en\x03\x00\x00\x00WAI") const IDENTITY_MODE_0: &str = "7717339a5b738a00c3f6191d2f43cc78156c65651b8c55d4ec234efa06efc968"; const IDENTITY_MODE_1: &str = "47d74c9ccb8456e3f1171904dda15a84bf74897c9bcc12d9f4fef166730e8c1a"; const IDENTITY_MODE_2: &str = "916bd4458608698f35b350172b1827ab26e53094e6d9fd784a62311f4270c7bc"; #[test] fn group_contiguity_1_2_1_is_refused() { let groups = |name: &str| -> Vec { wvg1::split(&payload("canonical", name)).unwrap().texts.iter().map(|t| t.group).collect() }; let refusal = |name: &str| wvg1::load(&payload("canonical", name), &mut store()).err().unwrap_or_else(|| panic!("canonical/{name} was accepted")); // [1, 2, 1]: group 1 resumes after group 2. assert_eq!(groups("group-not-contiguous"), [1, 2, 1]); let r = refusal("group-not-contiguous"); assert_eq!((r.code, r.step, r.index), (Code::PayloadMalformed, "5g", 2), "refused at block 2, by the canonical rules"); assert!(r.rule.contains("contiguous"), "{}", r.rule); // [1, 0, 1]: an ungrouped block splits it too. assert_eq!(groups("group-split-by-ungrouped"), [1, 0, 1]); let r = refusal("group-split-by-ungrouped"); assert_eq!((r.code, r.step), (Code::PayloadMalformed, "5g")); assert!(r.rule.contains("contiguous"), "{}", r.rule); // Contiguous groups load, with or without an ungrouped block between them. for (name, want) in [("groups-in-order", [1, 1, 2]), ("groups-with-ungrouped", [1, 0, 2])] { assert_eq!(groups(name), want); assert!(wvg1::load(&payload("canonical", name), &mut store()).is_ok(), "{name}"); } } #[test] fn text_hash_is_texts_then_strings() { for (group, name) in [("a11y", "worked-example"), ("a11y", "strings"), ("canonical", "every-table")] { let bytes = payload(group, name); let l = wvg1::load(&bytes, &mut store()).unwrap(); let h = &l.doc.header; // The two areas cut out of the payload by the header's counts (§2.2, §2.3): // STRINGS is the last table before INLINE, and TEXTS ends where ITEMS, // GLYPHS, CLUSTERS, STRINGS and INLINE begin. let strings_end = bytes.len() - h.inline_len as usize; let strings = &bytes[strings_end - h.strings_len as usize..strings_end]; let texts_end = strings_end - h.strings_len as usize - 12 * h.n_clusters as usize - 12 * h.n_glyphs as usize - 48 * h.n_items as usize; let texts = &bytes[texts_end - 20 * h.n_texts as usize..texts_end]; assert!(h.n_texts >= 3 && !strings.is_empty()); let want = sha256(&[texts, strings].concat()); assert_eq!(text::text_hash(&l.doc), want, "{group}/{name}: TEXTS, then STRINGS"); assert_ne!(sha256(&[strings, texts].concat()), want); let expected = String::from_utf8(read(&format!("vector-conformance/expected/{group}/{name}.json"))).unwrap(); assert!(expected.contains(&format!("\"text_hash\":\"{}\"", hex(&want))), "{group}/{name}: the expected file states it"); } // The worked example's, from another SHA-256 (Python's hashlib) over the same bytes. assert_eq!(hex(&text::text_hash(&load("a11y", "worked-example").doc)), TEXT_HASH_WORKED_EXAMPLE); } // hashlib.sha256(TEXTS + STRINGS) of a11y/worked-example.wvg1, the areas cut by offset. const TEXT_HASH_WORKED_EXAMPLE: &str = "70d5ce883f06a8aedd33bfa1fbc638e69909a380c19007a252584a7d38e0d432"; #[test] fn decorative_blocks_are_excluded() { let l = load("a11y", "decorative"); assert_eq!(l.doc.texts.iter().map(|t| t.purpose).collect::>(), [7, 13, 12]); // Block 1 is drawn for the whole document, like the others. assert_eq!(text::active_sets(&l.doc), [[(0, 120_000)], [(0, 120_000)], [(0, 120_000)]]); let whole = Some("[[0,120000]]"); let lines = committed("decorative"); assert_eq!(lines.len(), 1 + 25); assert_eq!(lines[0], array(&[label(whole, 0, "Scores"), node(whole, 0, 0, 2, "en", 0, 12, None, "3 - 1")])); for g in 0..25u64 { assert_eq!(lines[1 + g as usize], frame(25, 1, g, &[label(None, 0, "Scores"), node(None, 0, 0, 2, "en", 0, 12, None, "3 - 1")])); } let layer = TextLayer::new(&l.doc); assert_eq!(layer.transcript(), lines[0]); assert_eq!(layer.blocks_at(0), [0, 2]); assert!(!text::record(&l.doc).contains("* * *"), "the ornament's string is nowhere in the record"); } #[test] fn windows_are_cut_by_enclosing_groups() { let l = load("a11y", "nested-groups"); let want = array(&[ label(Some("[[24000,96000]]"), 0, "in the outer group"), label(Some("[[48000,60000]]"), 1, "in both groups"), label(Some("[[72000,96000]]"), 2, "in both, later"), label(Some("[]"), 3, "in a group that never opens"), label(Some("[[100000,130000]]"), 4, "in no group"), ]); assert_eq!(text::transcript(&l.doc), want); assert_eq!(committed("nested-groups")[0], want); // The runs as carried are active far longer than their groups. let runs: Vec<(u32, u32)> = l.doc.items.iter().filter(|it| it.kind == wvg1::KIND_RUN).map(|it| (it.t_on, it.t_off)).collect(); assert_eq!(runs, [(0, 240_000), (0, 60_000), (72_000, 240_000), (0, 240_000), (100_000, 130_000)]); // What a frame draws agrees: at tick 96000 the outer group has closed. let layer = TextLayer::new(&l.doc); assert_eq!(layer.blocks_at(95_999), [0, 2]); assert_eq!(layer.blocks_at(96_000), Vec::::new()); } #[test] fn effective_windows_are_the_costs() { // §5.4 uses §3.4(b)'s effective windows. Over every accepted payload of the corpus, // the text layer's are the ones the cost was computed from. let mut n = 0; let root = wai().join("vector-conformance/vectors"); let mut groups: Vec = std::fs::read_dir(&root).unwrap().flatten().map(|e| e.path()).collect(); groups.sort(); for g in groups { let mut files: Vec = std::fs::read_dir(&g).unwrap().flatten().map(|e| e.path()).filter(|p| p.extension().is_some_and(|x| x == "wvg1")).collect(); files.sort(); for f in files { let Ok(d) = wvg1::parse(&std::fs::read(&f).unwrap()) else { continue }; let Ok(sets) = wvg1::load_sets(&d, &mut store()) else { continue }; let cost: Vec> = wai_vector::place::Scene::new(&d, &sets).item_win; let mine: Vec> = text::effective_windows(&d).into_iter().map(|w| w.map(|(a, b)| (i64::from(a), i64::from(b)))).collect(); assert_eq!(mine, cost, "{}", f.display()); n += 1; } } assert!(n > 60, "{n} payloads"); } #[test] fn strings_take_three_escapes_and_no_others() { let l = load("a11y", "strings"); let whole = Some("[[0,24000]]"); // The string of block 0 holds a quotation mark, a backslash and U+000A; in JSON // those three are escaped, and `<`, `&`, `>` and `/` are not. assert_eq!(l.doc.string(0), b"He said \"A&B\" \\\n2/2"); let want = array(&[ label(whole, 0, "He said \\\"A&B\\\" \\\\\\n2/2"), label(whole, 1, "OK \u{1F600}"), node(whole, 1, 0, 2, "he", 0, 7, None, "\u{05E9}\u{05DC}\u{05D5}\u{05DD}"), node(whole, 0, 0, 3, "hi", 0, 7, None, "\u{0939}\u{093F}\u{0928}\u{094D}\u{0926}\u{0940}"), node(whole, 0, 0, 4, "sr-Latn-RS", 0, 7, None, "Zdravo"), label(whole, 5, "soft\u{00AD}hyphen"), ]); let t = text::transcript(&l.doc); assert_eq!(t, want); assert_eq!(committed("strings")[0], want); // Nothing is written as \u, and no byte below U+0020 is in a line. assert!(!t.contains("\\u") && !t.contains("\\/")); assert!(t.bytes().all(|b| b >= 0x20)); assert!(t.contains("\u{1F600}") && t.as_bytes().windows(2).any(|w| w == [0xC2, 0xAD])); } #[test] fn a_window_holds_its_first_tick_and_not_its_last() { // One block active over [24000, 50000), at four rates in RATE order. let l = load("a11y", "rates"); let n = node(None, 0, 0, 0, "en", 0, 3, None, "Four rates"); let mut want = vec![array(&[node(Some("[[24000,50000]]"), 0, 0, 0, "en", 0, 3, None, "Four rates")])]; // (num, den, frames, the frames whose tick is in the window) for (num, den, count, inside) in [(24_000u32, 1_001u32, 12u64, 5..=9u64), (25, 1, 13, 5..=10), (30_000, 1_001, 15, 6..=12), (60, 1, 30, 12..=24)] { for g in 0..count { want.push(frame(num, den, g, if inside.contains(&g) { std::slice::from_ref(&n) } else { &[] })); } } assert_eq!(committed("rates"), want); assert_eq!(text::record(&l.doc).lines().map(String::from).collect::>(), want); // At 60 fps a frame is 2,000 ticks: frame 12 sits on the first tick, frame 25 on the // tick the window ends at. let layer = TextLayer::new(&l.doc); assert_eq!(layer.projection_request(60, 1, 12).unwrap(), array(std::slice::from_ref(&n))); assert_eq!(layer.projection_request(60, 1, 25).unwrap(), "[]"); // A title that fades to nothing stays in the projection until its window ends. let w = committed("windows"); let title = node(None, 0, 0, 0, "en", 0, 1, None, "Opening title"); let credit = node(None, 0, 0, 1, "en", 0, 2, None, "Written here"); assert_eq!(w.len(), 1 + 50); for (g, nodes) in [(4u64, vec![]), (5, vec![title.clone()]), (18, vec![title.clone()]), (19, vec![title]), (20, vec![]), (24, vec![]), (25, vec![credit.clone()]), (44, vec![credit]), (45, vec![]), (49, vec![])] { assert_eq!(w[1 + g as usize], frame(25, 1, g, &nodes), "frame {g}"); } } #[test] fn a_paths_alternative_is_active_over_the_paths_window() { let l = load("a11y", "alternatives"); let want = array(&[ node(Some("[[0,72000]]"), 0, 0, 0, "en", 0, 10, None, "WAI"), node(Some("[[48000,120000]]"), 0, 0, 1, "en", 0, 11, None, "A red square"), label(Some("[[0,24000],[96000,120000]]"), 2, "Both"), ]); assert_eq!(text::transcript(&l.doc), want); assert_eq!(committed("alternatives")[0], want); // Blocks 0 and 1 are drawn by paths alone. for t in 0..2u16 { assert!(l.doc.items.iter().filter(|it| it.text == t).all(|it| it.kind == wvg1::KIND_PATH)); } // And the groups vector: each node carries its group number, in block order. let g = [ node(Some("[[0,48000]]"), 0, 1, 0, "en", 0, 3, None, "Camera"), node(Some("[[0,48000]]"), 0, 1, 1, "en", 0, 3, None, "Second unit"), label(Some("[[48000,72000]]"), 2, "Interval"), node(Some("[[72000,120000]]"), 0, 2, 3, "en", 0, 2, None, "Editing"), node(Some("[[72000,120000]]"), 0, 2, 4, "en", 0, 2, None, "Sound"), node(Some("[[96000,120000]]"), 0, 2, 5, "en", 0, 2, None, "Music"), ]; assert_eq!(committed("groups")[0], array(&g)); } #[test] fn a_frame_outside_the_document_is_a_request_error() { let l = load("a11y", "clipped"); let layer = TextLayer::new(&l.doc); // The document's frames at 25 fps are 11 to 29. assert!(layer.projection(0, 11).is_ok() && layer.projection(0, 29).is_ok()); for g in [0u64, 10, 30, 1 << 40, u64::MAX] { assert_eq!(layer.projection(0, g).map_err(|e| e.code), Err(Code::FrameRange), "frame {g}"); } assert_eq!(layer.projection_request(30, 1, 11).map_err(|e| e.code), Err(Code::RateUnlisted)); assert_eq!(layer.projection(1, 11).map_err(|e| e.code), Err(Code::RateUnlisted)); assert_eq!(text::projection(&l.doc, 0, 11).unwrap(), layer.projection_request(25, 1, 11).unwrap()); // A payload with no text: the empty array, at every frame. let none = committed("no-text"); assert_eq!(none[0], "[]"); assert_eq!(none[1..], (0..5).map(|g| frame(25, 1, g, &[])).collect::>()); } #[test] fn a_verifier_finds_bytes_no_cluster_covers() { // §5.3's encoder rule holds for every block a run draws in the text and a11y // groups: each byte in exactly one cluster, but for U+000A and U+00AD. let mut n = 0; for group in ["text", "a11y"] { let dir = wai().join("vector-conformance/vectors").join(group); let mut files: Vec = std::fs::read_dir(&dir).unwrap().flatten().map(|e| e.path()).filter(|p| p.extension().is_some_and(|x| x == "wvg1")).collect(); files.sort(); for f in files { let d = wvg1::parse(&std::fs::read(&f).unwrap()).unwrap(); for t in 0..d.texts.len() { if d.items.iter().any(|it| it.kind == wvg1::KIND_RUN && it.text as usize == t) { let c = text::coverage(&d, t); assert!(c.complete(), "{} block {t}: {c:?}", f.display()); n += 1; } } } } assert!(n > 50, "{n} blocks"); // The line separator and the soft hyphen of a11y/strings are in no cluster. let d = load("a11y", "strings").doc; let covered = |t: usize, at: usize| d.items.iter().enumerate().filter(|(_, it)| it.kind == wvg1::KIND_RUN && it.text as usize == t).any(|(i, _)| d.run_clusters(i).iter().any(|c| (c.byte_first as usize..(c.byte_first + c.byte_len) as usize).contains(&at))); assert_eq!(d.string(0)[22], 0x0A); assert!(!covered(0, 22) && covered(0, 21) && covered(0, 23)); assert_eq!(&d.string(5)[4..6], [0xC2, 0xAD]); assert!(!covered(5, 4) && !covered(5, 5)); // A payload that breaks the rule still loads, and a verifier reports where: with // one cluster dropped its bytes are uncovered, and with one repeated they are // covered twice. let mut d = load("a11y", "decorative").doc; let first = d.ix.item_cluster0[0] as usize; d.clusters[first + 1].byte_first = 0; assert_eq!(text::coverage(&d, 0), text::Coverage { uncovered: vec![(1, 2)], repeated: vec![(0, 1)] }); assert!(!text::coverage(&d, 0).complete()); assert!(wvg1::parse(&d.write()).is_ok(), "a sink does not check the rule"); }