From eae25754852f05a074ba365781373b787097c572 Mon Sep 17 00:00:00 2001 From: seunghan Kim Date: Fri, 17 Apr 2026 16:05:18 +0900 Subject: [PATCH] test: HWPX SVG snapshot regression harness (closes #173) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a pure-Rust integration test that renders pages via `HwpDocument::render_page_svg_native` and diffs the output against committed golden SVGs. Runs under the existing `cargo test` job in ci.yml, so no new workflow is required and the check is active on every PR without any Hancom-Office / Windows dependency. ## Highlights - `tests/svg_snapshot.rs` — harness with `UPDATE_GOLDEN=1` regeneration (same pattern used in the MDM project's `golden_hwpx.rs`). - First golden: `form-002.hwpx` page 0 — the smallest committed sample (~130KB input → ~263KB SVG). More goldens can be added file-by-file without harness changes. - Determinism probe: `render_is_deterministic_within_process` renders the same page twice in a single process and asserts byte equality, so any future non-determinism bug surfaces loudly instead of masquerading as a golden mismatch. ## Verification - Byte-identical across two separate CLI invocations: `rhwp export-svg samples/hwpx/form-002.hwpx -o /tmp/a -p 0` vs `... -o /tmp/b -p 0` → `diff -q /tmp/a /tmp/b` returns empty. - `cargo test --test svg_snapshot` passes locally. ## Follow-up (not in this PR) - Expand golden corpus to the 2024/2025 investment samples once the first run is green on CI. - Consider gzip-encoding large goldens if the corpus grows past a few MB. For now, raw SVG plays nicely with git diff and PR review. --- tests/golden_svg/form-002/page-0.svg | 1095 ++++++++++++++++++++++++++ tests/svg_snapshot.rs | 103 +++ 2 files changed, 1198 insertions(+) create mode 100644 tests/golden_svg/form-002/page-0.svg create mode 100644 tests/svg_snapshot.rs diff --git a/tests/golden_svg/form-002/page-0.svg b/tests/golden_svg/form-002/page-0.svg new file mode 100644 index 00000000..9a020a21 --- /dev/null +++ b/tests/golden_svg/form-002/page-0.svg @@ -0,0 +1,1095 @@ + + + + + + + + + + + + + + + + + + + + + + + +2 +0 +2 +6 +- +1 + +- + + + + + + + +- + + + + + + + +- + + +- + + +- +0 +1 + + + + + + + + + + + + + + + + + + + + + + + +원천기술형 + + +혁신제품형 + + + + + + + + + + + + +세계최초 + +세계최고 + + +해당없음 + +A +I + + + +AI 응용 및 활용 + +AI 기반 + +기타 AI 연계 기술 + + +해당없음 + + + +( + + + + +) + + + +지역 산업 연계 + +지역 기업 성장 + +지역 인재 및 일자리 + + +해당없음 + + + + + + + + +( + + +) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +, + + + + +, + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +· + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +IP R&&D연계 + +표준연계 + +적합성인증연계 + + +해당없음 + + + + + + +경쟁형과제 + +복수형과제 + +국가핵심기술 + +국제공동 + +대형통합형 + +민간투자연계형 + +서비스형 + +안전관리형 + +원스톱형 + +유연 컨소시엄 + +초고난도 과제 + +탄소중립 + +보안과제 + +E +S +G + +E + +S + + +G + +해당없음 + +R +D + + + + + + + +R&&D 자율성트랙(일반) + +R&&D 자율성트랙(지정) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +( +T +R +L +: +[ + + +] +4 + + + +[ + + +] +7 + + +) + +1 +. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +( +H +y +p +o +x +i +a +) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +* + + + + + + + + + + + + + + +( + + + + + +) + + + + + + + + + + +, + + + + + + + + + + + + + + + + + + + + + + + + + + + + +' + + +( +R +e +s +e +t +) +' + + + + + + + + + + + + + + +- + + + + + + + + + + + + + + + + + + + + + + +' + + + + + +' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +- + + + + + +( +P +F +C +) + + + + +- + + + + + + + + + + + + + + + +/ + + + + + + +* + + + + + +2 +0 +0 +n +m + + +, +I +n +v +i +t +r +o + + + + + + +1 +5 +p +p +m + + +, + + + + + + + + + + +- + + + + + + + + + + +· + + + + + + + + + + + + + + + + + +- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +( + + +) +, + + + + +( +n +m +) +, + + + + + + + +( +P +D +I +) +, + + + + + + + +( + +) +, + + +D +M +F + + +( + +) +, +G +M +P + + + + +( + +) +, + + + + + +( + +) +, + + + + + + +( + +) +, +I +N +D + + +( + +) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +- +1 +- + diff --git a/tests/svg_snapshot.rs b/tests/svg_snapshot.rs new file mode 100644 index 00000000..a75d3b2d --- /dev/null +++ b/tests/svg_snapshot.rs @@ -0,0 +1,103 @@ +//! SVG snapshot regression tests for HWPX rendering. +//! +//! Pure-Rust replacement for `tools/verify_hwpx.py`, which requires Windows +//! + Hancom Office + pyhwpx and cannot run in CI. This harness invokes +//! `rhwp::wasm_api::HwpDocument::render_page_svg_native()` directly so the +//! same SVG the CLI produces is diffed against committed golden files. +//! +//! # Updating goldens +//! +//! When rhwp's rendering intentionally changes, regenerate goldens: +//! +//! ```sh +//! UPDATE_GOLDEN=1 cargo test --test svg_snapshot +//! ``` +//! +//! Commit the resulting `tests/golden_svg/**/*.svg` files alongside the +//! source change and mention the intentional diff in the PR body. +//! +//! # Determinism +//! +//! These tests assume: +//! - `render_page_svg_native` output is deterministic for a fixed input +//! (no timestamps, no random IDs, no host-font-dependent glyph IDs). +//! - Font embedding is OFF (`FontEmbedMode::None` via the native entry +//! point) so host system fonts cannot leak into the snapshot. +//! +//! If a flake is observed, the first debugging step is to diff two +//! back-to-back runs on the same machine. Host-specific variance +//! indicates a real determinism bug — worth its own issue. + +use std::fs; +use std::path::{Path, PathBuf}; + +/// Generate an SVG for a specific page and compare against the committed +/// golden. Set `UPDATE_GOLDEN=1` to regenerate. +fn check_snapshot(hwpx_relpath: &str, page: u32, golden_name: &str) { + let repo_root = env!("CARGO_MANIFEST_DIR"); + let hwpx_path = Path::new(repo_root).join(hwpx_relpath); + let bytes = fs::read(&hwpx_path) + .unwrap_or_else(|e| panic!("read {}: {}", hwpx_path.display(), e)); + + let doc = rhwp::wasm_api::HwpDocument::from_bytes(&bytes) + .unwrap_or_else(|e| panic!("parse {}: {}", hwpx_relpath, e)); + + let actual = doc + .render_page_svg_native(page) + .unwrap_or_else(|e| panic!("render {} p.{}: {}", hwpx_relpath, page, e)); + + let golden_path = PathBuf::from(repo_root) + .join("tests/golden_svg") + .join(format!("{golden_name}.svg")); + + if std::env::var("UPDATE_GOLDEN").as_deref() == Ok("1") { + if let Some(parent) = golden_path.parent() { + fs::create_dir_all(parent).unwrap(); + } + fs::write(&golden_path, &actual).unwrap(); + eprintln!("UPDATED {}", golden_path.display()); + return; + } + + let expected = fs::read_to_string(&golden_path).unwrap_or_else(|e| { + panic!( + "missing golden {}: {}. Run `UPDATE_GOLDEN=1 cargo test --test svg_snapshot` to create.", + golden_path.display(), + e + ) + }); + + if actual != expected { + // Write the actual output next to the golden for local inspection + // without polluting the committed tree. + let actual_path = golden_path.with_extension("actual.svg"); + let _ = fs::write(&actual_path, &actual); + panic!( + "SVG snapshot mismatch for {}.\n expected: {}\n actual: {}\n\ + Inspect the diff; if intentional, rerun with UPDATE_GOLDEN=1.", + golden_name, + golden_path.display(), + actual_path.display() + ); + } +} + +#[test] +fn form_002_page_0() { + check_snapshot("samples/hwpx/form-002.hwpx", 0, "form-002/page-0"); +} + +/// Determinism probe: render the same page twice in one process and assert +/// byte-for-byte equality. If this ever fails, the snapshot tests above +/// are unreliable regardless of golden correctness. +#[test] +fn render_is_deterministic_within_process() { + let repo_root = env!("CARGO_MANIFEST_DIR"); + let bytes = fs::read(Path::new(repo_root).join("samples/hwpx/form-002.hwpx")) + .expect("sample present"); + + let doc = rhwp::wasm_api::HwpDocument::from_bytes(&bytes).expect("parse"); + let a = doc.render_page_svg_native(0).expect("render #1"); + let b = doc.render_page_svg_native(0).expect("render #2"); + assert_eq!(a, b, "render_page_svg_native must be deterministic"); +}