oxedyne/fe2o3/fe2o3_austenite/tests/oracle/trio.rs
6.2 KiB, 21 runs
created by r1870400018:38849, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! The no-regression trio: three fixed fixtures the oracle harness also checks on every run, each |
| 2 | //! guarding a real fix landed on `main` shortly before this unit -- the inline maths gallery, the |
| 3 | //! display-equation cases (aligned `&=`, a big-operator's under/over limits, `ceil`/`floor`/`binom`), |
| 4 | //! and the compact, searchable PDF writer. None of the three needs the Typst oracle or the corpus: they |
| 5 | //! are smoke-level guards against the engine itself regressing, run from the crate's own public API |
| 6 | //! exactly as `mathgallery` and `austenite` call it. |
| 7 | |
| 8 | use oxedyne_fe2o3_austenite::{ |
| 9 | doc::{ |
| 10 | self, |
| 11 | }, |
| 12 | fonts::FaceResolver, |
| 13 | theme::Theme, |
| 14 | driver::{ |
| 15 | self, |
| 16 | Config, |
| 17 | }, |
| 18 | emit, |
| 19 | font::FontMetrics, |
| 20 | lang, |
| 21 | page::PageGeometry, |
| 22 | }; |
| 23 | |
| 24 | use oxedyne_fe2o3_core::prelude::*; |
| 25 | use oxedyne_fe2o3_font::{ |
| 26 | face::Role, |
| 27 | shape::Dir, |
| 28 | }; |
| 29 | |
| 30 | use std::path::Path; |
| 31 | use std::process::Command; |
| 32 | use std::sync::Arc; |
| 33 | |
| 34 | /// Runs the `mathgallery` binary (the living maths-layout comparison harness, `src/bin/mathgallery.rs`) |
| 35 | /// into `work_dir` and checks it exited cleanly and left at least one non-empty equation PDF -- a smoke |
| 36 | /// guard against the inline maths pipeline (parse, layout, PDF emit) regressing outright, not a |
| 37 | /// pixel check, which is what the gallery itself exists for a person to do by eye. |
| 38 | pub fn inline_maths_gallery_renders(work_dir: &Path) -> Outcome<()> { |
| 39 | let out_dir = work_dir.join("trio-mathgallery"); |
| 40 | let bin = env!("CARGO_BIN_EXE_mathgallery"); |
| 41 | let output = match Command::new(bin).arg(&out_dir).output() { |
| 42 | Ok(o) => o, |
| 43 | Err(e) => return Err(err!(e, "Could not run the built mathgallery binary at {:?}.", bin; IO)), |
| 44 | }; |
| 45 | if !output.status.success() { |
| 46 | return Err(err!("mathgallery exited with {}:\n{}", |
| 47 | output.status, String::from_utf8_lossy(&output.stderr); Invalid, Unexpected)); |
| 48 | } |
| 49 | let entries = match std::fs::read_dir(&out_dir) { |
| 50 | Ok(e) => e, |
| 51 | Err(e) => return Err(err!(e, "Could not list {:?}.", out_dir; File, Read)), |
| 52 | }; |
| 53 | let mut pdf_count = 0usize; |
| 54 | for entry in entries.flatten() { |
| 55 | let p = entry.path(); |
| 56 | if p.extension().and_then(|e| e.to_str()) == Some("pdf") { |
| 57 | let len = res!(std::fs::metadata(&p)).len(); |
| 58 | if len == 0 { |
| 59 | return Err(err!("mathgallery wrote an empty PDF at {:?}.", p; Invalid, Size)); |
| 60 | } |
| 61 | pdf_count += 1; |
| 62 | } |
| 63 | } |
| 64 | if pdf_count == 0 { |
| 65 | return Err(err!("mathgallery wrote no equation PDFs into {:?}.", out_dir; Missing, Invalid)); |
| 66 | } |
| 67 | Ok(()) |
| 68 | } |
| 69 | |
| 70 | /// Authors, lays out and renders one small document carrying the display-equation shapes now on |
| 71 | /// `main` -- a multi-line `&`-aligned block (`87ca15c`/`8893ff6` fixed exactly this being stolen line |
| 72 | /// by line before it reached the maths parser), a big operator's `_`/`^` limits, and `ceil`, `floor` |
| 73 | /// and `binom` (`59c6e3b`) -- through the real reader-to-PDF pipeline, checking only that the pipeline |
| 74 | /// completes and the result is a genuine one-page PDF: the geometry of each shape is `mathgallery`'s |
| 75 | /// and the oracle's job, this fixture's is to trip the moment any of the four stops setting at all. |
| 76 | pub fn display_equation_cases_render() -> Outcome<()> { |
| 77 | let src = r#"= Display Equations Regression |
| 78 | |
| 79 | $ a &= b + c \ |
| 80 | &= d $ <eq_aligned> |
| 81 | |
| 82 | $ sum_(i=1)^n a_i $ <eq_bigop_limits> |
| 83 | |
| 84 | $ binom(n, k) + ceil(x) + floor(y) $ <eq_binom_ceil_floor> |
| 85 | "#; |
| 86 | let blocks = res!(lang::to_blocks(src)); |
| 87 | let fonts = Arc::new(res!(oxedyne_fe2o3_austenite::fonts::libertinus())); |
| 88 | let geom = PageGeometry::a4(); |
| 89 | let style = Theme::default(); |
| 90 | let (document, _heads) = res!(doc::author(fonts.clone(), geom, &style, &FaceResolver::default(), &blocks, None, None)); |
| 91 | let metrics = FontMetrics::new(fonts.clone(), Role::Body, Dir::Ltr, style.text.body_size); |
| 92 | let out = res!(driver::run(&document, &metrics, Config::default())); |
| 93 | if out.pages.is_empty() { |
| 94 | return Err(err!("The display-equation regression doc rendered no pages."; Invalid, Missing)); |
| 95 | } |
| 96 | let pdf = res!(emit::pdf::render_document(&out.pages)); |
| 97 | if pdf.len() < 200 { |
| 98 | return Err(err!( |
| 99 | "The display-equation regression PDF is only {} byte(s) -- too small to hold real content.", |
| 100 | pdf.len(); Invalid, Size)); |
| 101 | } |
| 102 | Ok(()) |
| 103 | } |
| 104 | |
| 105 | /// Renders a one-paragraph prose document and checks the compact/searchable PDF writer's two |
| 106 | /// invariants at once: the page's text comes back out through `pdftotext` (the Type-3-plus-ToUnicode |
| 107 | /// glyph encoding stays searchable), and the file stays well under a generous bound (compaction has not |
| 108 | /// regressed into bloat). The bound is generous on purpose -- this guards against a large regression, |
| 109 | /// not a byte-for-byte size lock a legitimate change would have to keep re-tuning. |
| 110 | pub fn pdf_stays_compact_and_extractable(work_dir: &Path) -> Outcome<()> { |
| 111 | const NEEDLE: &str = "quick brown fox"; |
| 112 | const SIZE_BOUND: u64 = 300_000; // bytes; one page of one paragraph of body text |
| 113 | |
| 114 | let src = fmt!("= Regression Prose\n\nThe {} jumps over the lazy dog while the compact PDF writer \ |
| 115 | keeps its content stream small and its text extractable.\n", NEEDLE); |
| 116 | let blocks = res!(lang::to_blocks(&src)); |
| 117 | let fonts = Arc::new(res!(oxedyne_fe2o3_austenite::fonts::libertinus())); |
| 118 | let geom = PageGeometry::a4(); |
| 119 | let style = Theme::default(); |
| 120 | let (document, _heads) = res!(doc::author(fonts.clone(), geom, &style, &FaceResolver::default(), &blocks, None, None)); |
| 121 | let metrics = FontMetrics::new(fonts.clone(), Role::Body, Dir::Ltr, style.text.body_size); |
| 122 | let out = res!(driver::run(&document, &metrics, Config::default())); |
| 123 | let pdf = res!(emit::pdf::render_document(&out.pages)); |
| 124 | |
| 125 | let pdf_path = work_dir.join("trio-pdf-compact.pdf"); |
| 126 | res!(std::fs::write(&pdf_path, &pdf)); |
| 127 | |
| 128 | let len = pdf.len() as u64; |
| 129 | if len > SIZE_BOUND { |
| 130 | return Err(err!( |
| 131 | "The compact-PDF fixture wrote {} byte(s), over the {} byte bound -- the writer may have \ |
| 132 | regressed out of compaction.", len, SIZE_BOUND; Invalid, TooBig)); |
| 133 | } |
| 134 | |
| 135 | let output = match Command::new("pdftotext").arg(&pdf_path).arg("-").output() { |
| 136 | Ok(o) => o, |
| 137 | Err(e) => return Err(err!(e, "Could not run `pdftotext`."; IO)), |
| 138 | }; |
| 139 | if !output.status.success() { |
| 140 | return Err(err!("pdftotext on {:?} exited with {}.", pdf_path, output.status; Invalid, Unexpected)); |
| 141 | } |
| 142 | let text = String::from_utf8_lossy(&output.stdout); |
| 143 | if !text.contains(NEEDLE) { |
| 144 | return Err(err!( |
| 145 | "pdftotext on {:?} did not recover {:?}; extracted: {:?}", pdf_path, NEEDLE, text; |
| 146 | Invalid, Mismatch)); |
| 147 | } |
| 148 | Ok(()) |
| 149 | } |