From d94bbc8fe088e7b615d55bdc6a427a0a8b2f2296 Mon Sep 17 00:00:00 2001 From: vangel76 Date: Sat, 7 Nov 2026 16:16:20 +0100 Subject: [PATCH] Layout tests: install the pinned fonts in the layout entry points and font helpers layout, layout_para, layout_equation and mark_text_width call test_fonts() first in test builds, so the shared lay helper and everything that goes through them is covered. Tests and helpers that look up fonts directly (the Times New Roman substitute test, the Symbol bullet test, the Arabic/Myanmar/Thai face probes and the nbsp fixture) call it themselves. Audited by running every layout test in its own process with a temporary guard that panicked on any global font access before the pinned fonts were installed. Co-Authored-By: Claude Sonnet 5.5 --- crates/layout/src/display/lines.rs | 2 ++ crates/layout/src/lib.rs | 11 +++++++++-- crates/layout/src/math.rs | 2 ++ crates/layout/src/para/paragraph.rs | 2 ++ crates/layout/src/tests.rs | 2 ++ crates/layout/src/tests/bidi.rs | 1 + crates/layout/src/tests/complex.rs | 3 +++ crates/layout/src/tests/nbsp.rs | 2 ++ 8 files changed, 23 insertions(+), 2 deletions(-) diff --git a/crates/layout/src/display/lines.rs b/crates/layout/src/display/lines.rs index 0d253f9205..6b1fbd6098 100644 --- a/crates/layout/src/display/lines.rs +++ b/crates/layout/src/display/lines.rs @@ -638,6 +638,8 @@ pub fn section_break_label(start: SectionStart) -> &'static str { /// Width of a [`Draw::MarkText`] label. pub fn mark_text_width(text: &str, size: f32) -> f32 { + #[cfg(test)] + crate::test_fonts(); let face = wordcraft_fonts::word::resolve("Source Sans 3", false, false).face; let k = size / face.upem.max(1.0) as f32; text.chars().map(|c| face.advance(face.glyph_for(c)) as f32 * k).sum() diff --git a/crates/layout/src/lib.rs b/crates/layout/src/lib.rs index 6acab3d28c..504c178fa9 100644 --- a/crates/layout/src/lib.rs +++ b/crates/layout/src/lib.rs @@ -8,8 +8,13 @@ //! - [`hit`]: point ↔ position, caret geometry, line navigation, selection rectangles. #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] -/// Every unit test calls this before any font lookup (including its fixture construction). -/// A process-wide immutable choice keeps font and math caches consistent across test threads. +/// Installs the pinned test fonts as the process-wide font database. The layout entry points +/// (`layout`, `layout_para`, `layout_equation`, `display::mark_text_width`) call it first in test +/// builds, so the shared `lay` helper and every test that goes through them are covered; a test +/// that looks up fonts itself before any such call (fixtures, `word::resolve`, `FontDb::global`) +/// calls it at its start. It is idempotent and never calls back into layout, so it cannot recurse. +/// The choice is process-wide and immutable, which keeps font and math caches consistent across +/// test threads. #[cfg(test)] fn test_fonts() { assert!(wordcraft_fonts::FontDb::init_test_fonts(), "layout tests must initialize fixed fonts before any global font lookup"); @@ -314,6 +319,8 @@ pub struct DocLayout { /// Lay out the whole document. pub fn layout(doc: &Document, cache: &mut LayoutCache, opts: &LayoutOptions) -> DocLayout { + #[cfg(test)] + crate::test_fonts(); let t0 = now_ms(); // Page totals shown in the body (`NUMPAGES`, `SECTIONPAGES`) are known only after // paginating: lay out again with the totals found until they hold (bounded). Footnotes diff --git a/crates/layout/src/math.rs b/crates/layout/src/math.rs index 95070ecd2f..b5ba3d8897 100644 --- a/crates/layout/src/math.rs +++ b/crates/layout/src/math.rs @@ -207,6 +207,8 @@ pub fn layout_equation( props: &MathProps, counter: &mut u32, ) -> MathLayout { + #[cfg(test)] + crate::test_fonts(); let parsed; let nodes: &Arg = if math.nodes.is_empty() && !math.linear { parsed = parse_linear(linear); diff --git a/crates/layout/src/para/paragraph.rs b/crates/layout/src/para/paragraph.rs index 2c0c4e8c1e..1cd053e134 100644 --- a/crates/layout/src/para/paragraph.rs +++ b/crates/layout/src/para/paragraph.rs @@ -10,6 +10,8 @@ use super::*; /// Lay out one paragraph. pub fn layout_para(p: &Paragraph, env: &ParaEnv) -> ParaLayout { + #[cfg(test)] + crate::test_fonts(); let doc = env.doc; let mut rp = doc.styles.resolve_para_in(&p.props, env.table.map(|t| &t.para)); if let Some(spacing) = env.read_line_spacing.filter(|s| s.is_finite() && (1.0..=2.0).contains(s)) { diff --git a/crates/layout/src/tests.rs b/crates/layout/src/tests.rs index 7275046772..2e22d43630 100644 --- a/crates/layout/src/tests.rs +++ b/crates/layout/src/tests.rs @@ -1345,6 +1345,7 @@ fn a_missing_roman_font_is_drawn_as_word_draws_it() { #[test] fn a_missing_roman_font_with_its_own_substitute_keeps_it() { + crate::test_fonts(); // Times New Roman is roman in every font table. Where it isn't installed it keeps its metric // clone (Liberation Serif), not Cambria: Word, which always has it, never draws it missing. let mut d = Document::from_text("Serif"); @@ -1970,6 +1971,7 @@ fn bullet_first_line(label_font: Option<&str>) -> (f32, f32, f32, f32) { #[test] fn a_symbol_bullet_sizes_its_line_by_the_symbol_font() { + crate::test_fonts(); // Measured in Word: a Symbol bullet makes its line taller than the text alone (Calibri 11 pt // at 1.08 spacing: 15.1 pt, not 14.5 pt), the extra ascent added unscaled. Symbol isn't an open // font; without it the bullet is drawn from another face but its line keeps Symbol's height. diff --git a/crates/layout/src/tests/bidi.rs b/crates/layout/src/tests/bidi.rs index 002d0ad89f..5eae020ec5 100644 --- a/crates/layout/src/tests/bidi.rs +++ b/crates/layout/src/tests/bidi.rs @@ -314,6 +314,7 @@ fn hostile_bidi_text_never_panics() { /// A face that has Arabic letters (installed system fonts), or `None` to skip. fn arabic_face() -> Option { + crate::test_fonts(); wordcraft_fonts::FontDb::global().fallback_for('ب', 0).map(|f| wordcraft_fonts::FaceRef::of(&f)).filter(|f| f.covers('پ')) } diff --git a/crates/layout/src/tests/complex.rs b/crates/layout/src/tests/complex.rs index 01551b1a31..206f48875a 100644 --- a/crates/layout/src/tests/complex.rs +++ b/crates/layout/src/tests/complex.rs @@ -68,6 +68,7 @@ fn myanmar_without_spaces_wraps_between_syllables() { /// A face with Myanmar letters (an installed system font), or `None` to skip. fn myanmar_face() -> Option { + crate::test_fonts(); wordcraft_fonts::FontDb::global().fallback_for('\u{1000}', 0).map(|f| wordcraft_fonts::FaceRef::of(&f)).filter(|f| f.has_layout_tables()) } @@ -119,12 +120,14 @@ const THAI_RUN: &str = "[D] น้ำ ค่ำ ปั้น ฝั่ง ปู /// Is a font with Thai letters installed? `false` skips the test. fn thai_font_installed() -> bool { + crate::test_fonts(); wordcraft_fonts::FontDb::global().fallback_for('\u{0E19}', 0).is_some() } /// Is a Thai font with Latin letters and digits installed (Tahoma, Leelawadee UI, Thonburi, the /// TLWG fonts…)? Noto Sans Thai alone has none, so no face covers [`THAI_RUN`]: `false` skips. fn thai_latin_font_installed() -> bool { + crate::test_fonts(); let face = wordcraft_fonts::word::resolve("TH Sarabun New", false, false).face; THAI_RUN.chars().filter(|c| !c.is_whitespace()).all(|c| face.covers(c)) } diff --git a/crates/layout/src/tests/nbsp.rs b/crates/layout/src/tests/nbsp.rs index 36b9ccb9b1..64144585bd 100644 --- a/crates/layout/src/tests/nbsp.rs +++ b/crates/layout/src/tests/nbsp.rs @@ -6,6 +6,7 @@ use crate::{Document, LayoutCache, LayoutOptions, Placed, layout}; /// Inter with its U+00A0 glyph twice as wide as its space, under the family name "Wnbsp". /// Built from the bundled font so the test needs no installed font. fn wide_nbsp_family() -> &'static str { + crate::test_fonts(); const FAMILY: &str = "Wnbsp"; let inter = wordcraft_fonts::word::resolve("Inter", false, false).face; let mut data = inter.data().to_vec(); @@ -54,6 +55,7 @@ fn line_width(text: &str) -> f32 { #[test] fn a_no_break_space_is_as_wide_as_a_space() { + crate::test_fonts(); let face = wordcraft_fonts::word::resolve(wide_nbsp_family(), false, false).face; assert_eq!(face.family, wide_nbsp_family(), "the test font is installed under its own name"); assert!(