Repository navigation
test(core): capture compatibility baselines for refactoring #157
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| //! Contracts at the boundaries between independently maintained layout domains. | ||
| use ratex_layout::{layout, to_display_list, LayoutOptions}; | ||
| use ratex_parser::parse; | ||
| use ratex_types::display_item::DisplayItem; | ||
| use ratex_types::{Color, MathStyle}; | ||
|
|
||
| #[test] | ||
| fn compound_domains_preserve_inherited_color_in_every_style() { | ||
| let color = Color::new(0.2, 0.4, 0.6, 0.75); | ||
| for style in [MathStyle::Display, MathStyle::Text, MathStyle::Script] { | ||
| for formula in [ | ||
| r"\left(\frac{\widehat{x}_i}{\sqrt{y}}\middle|z\right)", | ||
| r"\begin{array}{c|c}\hline x&\overline{y}\\[1em]z&\sqrt{w}\end{array}", | ||
| r"\begin{CD} A @>f>> B \\ @VVgV @AAhA \\ C @= D \end{CD}", | ||
| r"\begin{prooftree}\LeftLabel{L}\AxiomC{P}\UnaryInfC{Q}\end{prooftree}", | ||
| ] { | ||
| let options = LayoutOptions::default().with_style(style).with_color(color); | ||
| let display = to_display_list(&layout(&parse(formula).unwrap(), &options)); | ||
| assert!(!display.items.is_empty(), "{formula}"); | ||
| for item in display.items { | ||
| let actual = match item { | ||
| DisplayItem::GlyphPath { color, .. } | ||
| | DisplayItem::Line { color, .. } | ||
| | DisplayItem::Rect { color, .. } | ||
| | DisplayItem::Path { color, .. } => color, | ||
| }; | ||
| assert_eq!(actual, color, "{formula}: {style:?}"); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn explicit_array_tags_survive_row_gap_layout() { | ||
| let plain = r"\begin{align}x&=y\tag{A}\\z&=w\tag{B}\end{align}"; | ||
| let spaced = r"\begin{align}x&=y\tag{A}\\[1em]z&=w\tag{B}\end{align}"; | ||
| let render = | ||
| |formula| to_display_list(&layout(&parse(formula).unwrap(), &LayoutOptions::default())); | ||
| let before = render(plain); | ||
| let after = render(spaced); | ||
| assert!(after.height + after.depth > before.height + before.depth); | ||
| for character in ['A', 'B'] { | ||
| let count = |items: &[DisplayItem]| { | ||
| items | ||
| .iter() | ||
| .filter(|item| { | ||
| matches!(item, | ||
| DisplayItem::GlyphPath { char_code, .. } if *char_code == character as u32) | ||
| }) | ||
| .count() | ||
| }; | ||
| assert_eq!(count(&before.items), 1); | ||
| assert_eq!(count(&after.items), 1); | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn proof_labels_add_ink_without_changing_the_inference_count() { | ||
| let plain = r"\begin{prooftree}\AxiomC{P}\UnaryInfC{Q}\end{prooftree}"; | ||
| let labeled = | ||
| r"\begin{prooftree}\LeftLabel{L}\RightLabel{R}\AxiomC{P}\UnaryInfC{Q}\end{prooftree}"; | ||
| let render = | ||
| |formula| to_display_list(&layout(&parse(formula).unwrap(), &LayoutOptions::default())); | ||
| let before = render(plain); | ||
| let after = render(labeled); | ||
| let rules = |items: &[DisplayItem]| { | ||
| items | ||
| .iter() | ||
| .filter(|item| matches!(item, DisplayItem::Line { .. })) | ||
| .count() | ||
| }; | ||
| assert_eq!(rules(&before.items), 1); | ||
| assert_eq!(rules(&after.items), 1); | ||
| assert!(after.items.len() > before.items.len()); | ||
| assert!(after.width > before.width); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| //! Same-machine compatibility capture for structural refactors. Artifacts are | ||
| //! deliberately opt-in and never replace the committed golden references. | ||
| use std::fs; | ||
| use std::path::{Path, PathBuf}; | ||
|
|
||
| use ratex_layout::{layout, to_display_list, LayoutOptions}; | ||
| use ratex_parser::parse; | ||
| use ratex_render::{render_to_png, RenderOptions}; | ||
| use ratex_types::{Color, MathStyle}; | ||
| use serde_json::{json, Value}; | ||
|
|
||
| fn capture_suite(root: &Path, output: &Path, name: &str, corpus: &str, font_size: f32, dpr: f32) { | ||
| let directory = output.join(name); | ||
| fs::create_dir(&directory).expect("create new suite directory"); | ||
| let source = fs::read_to_string(root.join("tests/golden").join(corpus)).unwrap(); | ||
| let options = RenderOptions { | ||
| font_dir: root.join("fonts").to_string_lossy().into_owned(), | ||
| font_size, | ||
| device_pixel_ratio: dpr, | ||
| ..RenderOptions::default() | ||
| }; | ||
| let mut records = Vec::new(); | ||
| let mut errors = String::new(); | ||
| for (index, formula) in source | ||
| .lines() | ||
| .map(str::trim) | ||
| .filter(|line| !line.is_empty() && !line.starts_with(['#', '%'])) | ||
| .enumerate() | ||
| { | ||
| let index = index + 1; | ||
| let mut record = json!({"index": index, "formula": formula}); | ||
| match parse(formula) { | ||
| Err(error) => { | ||
| record["status"] = json!("parse_error"); | ||
| record["error"] = json!(error.to_string()); | ||
| errors.push_str(&format!("ERR {index:4} {formula} — parse error: {error}\n")); | ||
| } | ||
| Ok(ast) => { | ||
| record["ast"] = serde_json::to_value(&ast).unwrap(); | ||
| let box_ = layout(&ast, &LayoutOptions::default()); | ||
| let display = to_display_list(&box_); | ||
| record["box"] = json!([box_.width, box_.height, box_.depth]); | ||
| record["display"] = serde_json::to_value(&display).unwrap(); | ||
| // Exercise style/color/size propagation as well as the default | ||
| // path, without multiplying the number of PNG artifacts. | ||
| let inline_options = LayoutOptions::default() | ||
| .with_style(MathStyle::Text) | ||
| .with_color(Color::new(0.2, 0.4, 0.6, 0.75)) | ||
| .with_inter_glyph_kern(0.02); | ||
| record["inline_display"] = | ||
| serde_json::to_value(to_display_list(&layout(&ast, &inline_options))).unwrap(); | ||
| match render_to_png(&display, &options) { | ||
| Ok(png) => { | ||
| fs::write(directory.join(format!("{index:04}.png")), png).unwrap(); | ||
| record["status"] = json!("rendered"); | ||
| } | ||
| Err(error) => { | ||
| record["status"] = json!("render_error"); | ||
| record["error"] = json!(error); | ||
| errors.push_str(&format!("ERR {index:4} {formula} — {error}\n")); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| records.push(record); | ||
| } | ||
| fs::write(directory.join("errors.log"), errors).unwrap(); | ||
| fs::write( | ||
| directory.join("records.json"), | ||
| serde_json::to_vec(&records).unwrap(), | ||
| ) | ||
| .unwrap(); | ||
| println!("captured {}: {} formulas", name, records.len()); | ||
| } | ||
|
|
||
| fn compare_suite(baseline: &Path, output: &Path, name: &str) { | ||
| let before = baseline.join(name); | ||
| let after = output.join(name); | ||
| let old: Vec<Value> = | ||
| serde_json::from_slice(&fs::read(before.join("records.json")).unwrap()).unwrap(); | ||
| let new: Vec<Value> = | ||
| serde_json::from_slice(&fs::read(after.join("records.json")).unwrap()).unwrap(); | ||
| assert_eq!(old.len(), new.len(), "{name}: corpus size changed"); | ||
| for (old, new) in old.iter().zip(&new) { | ||
| let index = new["index"].as_u64().unwrap(); | ||
| assert_eq!( | ||
| old, new, | ||
| "{name}/{index:04}: AST, geometry, display items or status changed" | ||
| ); | ||
| if new["status"] == "rendered" { | ||
| let png = format!("{index:04}.png"); | ||
| // The same encoder/options make byte equality a stricter check | ||
| // than decoded dimensions and pixel equality. | ||
| assert_eq!( | ||
| fs::read(before.join(&png)).unwrap(), | ||
| fs::read(after.join(&png)).unwrap(), | ||
| "{name}/{png}: PNG changed" | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| #[ignore = "set RATEX_COMPAT_OUTPUT; optionally RATEX_COMPAT_BASELINE to compare two captures"] | ||
| fn capture_and_compare_corpus() { | ||
| let root = Path::new(env!("CARGO_MANIFEST_DIR")) | ||
| .join("../..") | ||
| .canonicalize() | ||
| .unwrap(); | ||
| let output = | ||
| PathBuf::from(std::env::var_os("RATEX_COMPAT_OUTPUT").expect("set RATEX_COMPAT_OUTPUT")); | ||
| fs::create_dir_all(output.parent().unwrap()).unwrap(); | ||
| fs::create_dir(&output).expect("output must be a new directory; never overwrite a baseline"); | ||
| for (name, corpus, size, dpr) in [ | ||
| ("main", "test_cases.txt", 40.0, 1.0), | ||
| ("mhchem", "test_case_ce.txt", 40.0, 2.0), | ||
| ("prooftree", "test_cases_prooftree.txt", 36.0, 1.0), | ||
| ] { | ||
| capture_suite(&root, &output, name, corpus, size, dpr); | ||
| if let Some(baseline) = std::env::var_os("RATEX_COMPAT_BASELINE") { | ||
| compare_suite(Path::new(&baseline), &output, name); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| # Core implementation boundaries | ||
|
|
||
| This guide describes private implementation modules. Public Rust import paths, | ||
| AST and layout types, C ABI and DisplayList JSON remain unchanged. | ||
|
|
||
| ## Same-machine refactor acceptance | ||
|
|
||
| The ignored `refactor_compatibility` integration test captures all three golden | ||
| corpora. Each record includes the complete AST, unrounded box dimensions, | ||
| DisplayList, an inline/color/tracking variant and the render/error result. | ||
| Successful cases also produce PNGs using the normal golden size and DPR. | ||
|
|
||
| Before modifying production code: | ||
|
|
||
| ```bash | ||
| RATEX_COMPAT_OUTPUT="$PWD/target/refactor-compat/before" \ | ||
| cargo test --release -p ratex-render --test refactor_compatibility -- --ignored --nocapture | ||
| ``` | ||
|
|
||
| After the refactor, on the same machine with the same fonts and build features: | ||
|
|
||
| ```bash | ||
| RATEX_COMPAT_OUTPUT="$PWD/target/refactor-compat/after" \ | ||
| RATEX_COMPAT_BASELINE="$PWD/target/refactor-compat/before" \ | ||
| cargo test --release -p ratex-render --test refactor_compatibility -- --ignored --nocapture | ||
| ``` | ||
|
|
||
| Output directories must be new; the test refuses to overwrite a baseline. JSON | ||
| records are compared structurally, including array order. Identical PNG bytes | ||
| also guarantee identical dimensions and pixels with the same encoder. Known | ||
| unsupported formulas must preserve their error result, not disappear. | ||
|
|
||
| For authoritative visual scoring, use `tools/golden_compare/compare_golden.py` | ||
| and the workflow in `GOLDEN_BASELINE.md`. Capture directories have `errors.log` | ||
| files accepted by `build_render_manifest.py`; use the original corpus and DPR | ||
| to create each `render-manifest.json`. Compare both captures against the same | ||
| reference images, then pass the before report as `--baseline-report` with | ||
| `--max-case-regression 0`. Capture artifacts do not replace committed reference | ||
| images or `tests/golden/baseline.json`. | ||
|
|
||
| Pass `--ce` for mhchem even when specifying custom output directories. For | ||
| prooftree, pass `--prooftree-tolerant` and an explicit policy file containing | ||
| `{"cases": {}}`; the main suite's indexed exclusions do not apply to that corpus. | ||
| Use `--require-manifests` with the main references. Existing mhchem/prooftree | ||
| reference images may lack generation manifests: the scorer still checks image | ||
| index coverage, and both refactor captures must use those same references. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Expand this loop to include the cramped and scriptscript variants.
MathStylehas eight variants, but this test exercises onlyDisplay,Text, andScript; therefore a refactor that breaks inherited colors specifically for rootScriptScriptsizing or any cramped style will still pass despite the test'sin_every_stylecontract.Useful? React with 👍 / 👎.