Editor T2 W3: the goldens become conformance gate [9/9]
A golden-gate feature on epiphany-testkit: resvg + epiphany-render-svg as optional regular dependencies (dev-dependencies cannot be optional; the MSRV test job builds examples with --all-targets, so the raster stack must stay out of the featureless closure — proven by cargo tree, 0 vs 1). Gate 9 re-derives the three T1a golden states headlessly and compares decoded RGBA against the committed baselines; compare-only, failing loudly with a pointer to the editor-gui diagnostic tests. Without the feature the suite's output is byte-identical to before; with it, 9/9. The conformance CI job's invocation gains the feature; CONFORMANCE.md documents gate [9] and the split. Two mutations killed and coordinator re-verified. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
d27ea23ae1
commit
456792debd
|
|
@ -162,7 +162,7 @@ jobs:
|
|||
# manifest-selection gates, convergence / reduction-determinism /
|
||||
# equivocation against the real epiphany-ops, and the layout round-trip.
|
||||
- name: Run conformance suite
|
||||
run: cargo run --release -p epiphany-testkit --example conformance_suite 1
|
||||
run: cargo run --release -p epiphany-testkit --example conformance_suite --features golden-gate 1
|
||||
|
||||
# The Chapter 10 performance-budget gates (Phase 2 worklist F1), in
|
||||
# release with reduced sampling (EPIPHANY_BENCH_QUICK): the 1K and 10K
|
||||
|
|
|
|||
|
|
@ -128,3 +128,46 @@ substituting a vendor heuristic:
|
|||
continued-fraction with documented iteration/denominator bounds and a
|
||||
typed `TempoIntegration`-class tolerance) are advisory, per the
|
||||
floating-point declaration above.
|
||||
|
||||
## The conformance suite's gate count
|
||||
|
||||
`cargo run -p epiphany-testkit --example conformance_suite` runs the numbered
|
||||
gates described above (and several supplementary lettered stages) end to end,
|
||||
printing `[N/TOTAL] ...` as each gate starts and `[TOTAL/TOTAL] ok: full
|
||||
conformance suite passed` on a clean run; CI's `conformance` job
|
||||
(`.github/workflows/ci.yml:146-176`) runs it on every push and pull request.
|
||||
|
||||
**Gate 9 — visual golden conformance, behind the `golden-gate` feature.**
|
||||
`epiphany-testkit`'s default build (`TOTAL = 8`) covers determinism,
|
||||
round-trip, crash recovery, equivocation, convergence, reduction, layout
|
||||
round-trip, and the cross-implementation/Text-Projection decode vectors — no
|
||||
rendering. Gate 9 (`TOTAL = 9` with the feature enabled) additionally
|
||||
re-derives the T1a visual golden states
|
||||
(`spec/CONTRACT_EDITOR_T1A_GOLDENS.md`) headlessly — `EditorSession::open`
|
||||
over the real engraver and renderer, rasterized with `resvg` exactly as
|
||||
`epiphany-editor-gui`'s demo binary does — and compares the decoded RGBA
|
||||
pixels against the baselines committed at
|
||||
`crates/epiphany-editor-gui/goldens/*.png`, dimensions first. It is
|
||||
compare-only: no bless mechanism, no artifact writing; a mismatch's panic
|
||||
names `cargo test -p epiphany-editor-gui goldens` as the crate that owns
|
||||
that machinery.
|
||||
|
||||
**Why the gate is feature-gated rather than always on.** `epiphany-testkit`
|
||||
is a workspace member the MSRV `check` job (`.github/workflows/ci.yml`, pinned
|
||||
to Rust 1.85) builds with `--all-targets`, `conformance_suite` included — and
|
||||
that job deliberately excludes `epiphany-editor-gui`, because one of its own
|
||||
dependencies (`image`, pulled in via `eframe`) declares a newer MSRV than the
|
||||
workspace floor. `resvg` — the crate gate 9 needs to rasterize SVG the same
|
||||
way the demo GUI does — carries that same raster-stack dependency weight.
|
||||
Making it reachable unconditionally from `epiphany-testkit` would silently
|
||||
re-impose that floor on a crate the MSRV job is meant to keep covering. So
|
||||
`resvg` (and `epiphany-render-svg`, needed to produce the SVG in the first
|
||||
place) are `optional = true` dependencies of `epiphany-testkit`, gated by a
|
||||
`golden-gate` Cargo feature nothing enables by default; only the
|
||||
`conformance` job's suite invocation passes `--features golden-gate`
|
||||
(`.github/workflows/ci.yml:165`). Without the feature, `cargo tree -p
|
||||
epiphany-testkit -e normal` shows no `resvg` node in the dependency graph;
|
||||
with `--features golden-gate`, it shows exactly one. Full rationale, the
|
||||
mutation evidence, and the cross-crate baseline-path coupling are recorded in
|
||||
`crates/epiphany-testkit/DECISIONS.md` (T2 W3) and
|
||||
`spec/CONTRACT_EDITOR_T2_SELECTION.md` §W3.
|
||||
|
|
|
|||
|
|
@ -1285,7 +1285,9 @@ dependencies = [
|
|||
"epiphany-engrave",
|
||||
"epiphany-layout-ir",
|
||||
"epiphany-ops",
|
||||
"epiphany-render-svg",
|
||||
"epiphany-textproj",
|
||||
"resvg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -17,6 +17,23 @@ epiphany-textproj.workspace = true
|
|||
# now drives the real crate instead of an in-tree stub.
|
||||
epiphany-layout-ir.workspace = true
|
||||
|
||||
# T2 W3 — the `[9/9]` golden conformance gate (`golden-gate` feature,
|
||||
# `examples/conformance_suite.rs`): re-renders the T1a golden states headlessly
|
||||
# and rasterizes them exactly as `epiphany-editor-gui`'s demo binary does, to
|
||||
# compare against the baselines committed there
|
||||
# (`epiphany-editor-gui/goldens/*.png`). Both crates are example-only — used
|
||||
# solely by the `#[cfg(feature = "golden-gate")]` code — and `optional = true`
|
||||
# so the default (no-feature) build pulls in neither; dev-dependencies cannot
|
||||
# be optional, so both live here in [dependencies], gated by the
|
||||
# `golden-gate` feature (below) rather than always compiled. This is precisely
|
||||
# what keeps `resvg`'s raster stack out of the MSRV job's dependency closure:
|
||||
# that job runs `cargo test --workspace --all-targets` at the pinned MSRV
|
||||
# (`.github/workflows/ci.yml`, the `check` job), which builds every example in
|
||||
# this crate with default features only. See `DECISIONS.md` (T2 W3) and
|
||||
# `spec/CONTRACT_EDITOR_T2_SELECTION.md` §W3.
|
||||
epiphany-render-svg = { workspace = true, optional = true }
|
||||
resvg = { version = "0.45", optional = true }
|
||||
|
||||
# The Chapter 10 performance benches (worklist F1) run under criterion; the
|
||||
# budget-gate logic itself lives in `src/budget.rs` on plain `std`, so the
|
||||
# library builds without it. The editor-core + engrave pair drives the
|
||||
|
|
@ -29,6 +46,16 @@ criterion.workspace = true
|
|||
epiphany-editor-core.workspace = true
|
||||
epiphany-engrave.workspace = true
|
||||
|
||||
# T2 W3: gate 9's session/session-editing surface (`EditorSession`,
|
||||
# `Engraver::default()`) is exactly the pair the multi-system click test and
|
||||
# the reference-suite harness above already pull in as unconditional
|
||||
# dev-dependencies — reused as-is, not duplicated or made optional, since a
|
||||
# dev-dependency cannot be optional and these two carry no `resvg`-class
|
||||
# MSRV risk of their own (see the `[dependencies]` comment above for the two
|
||||
# that do).
|
||||
[features]
|
||||
golden-gate = ["dep:epiphany-render-svg", "dep:resvg"]
|
||||
|
||||
# Drives the whole suite at scale outside the unit-test timeout — the analogue
|
||||
# of epiphany-determinism's `fuzz_roundtrip` and epiphany-bundle's `fuzz_crash`.
|
||||
[[example]]
|
||||
|
|
|
|||
|
|
@ -352,3 +352,124 @@ mutation-verified two ways: restoring the 0.4.0 `value` production, and strippin
|
|||
the `schema-directed` citations. A grammar that claims to distinguish a struct
|
||||
from a sequence would be lying, so the test asserts the symbol-headed alternative
|
||||
is **absent**.
|
||||
|
||||
## T2 W3 (2026-07-23) — Promoting the T1a visual goldens to gate `[9]`, behind `golden-gate`
|
||||
|
||||
`spec/CONTRACT_EDITOR_T2_SELECTION.md` §W3. T1a
|
||||
(`crates/epiphany-editor-gui/src/goldens.rs`, `spec/CONTRACT_EDITOR_T1A_GOLDENS.md`)
|
||||
landed pixel-level golden tests in `epiphany-editor-gui` but left this crate's
|
||||
`conformance_suite` frozen at 8/8 — the harness existed but nothing counted it.
|
||||
This tranche adds gate 9, re-deriving the three T1a golden states headlessly
|
||||
inside `examples/conformance_suite.rs` and comparing against the same committed
|
||||
baselines, entirely behind a new `golden-gate` feature.
|
||||
|
||||
**Why a feature, not an unconditional addition.** The MSRV `check` job
|
||||
(`.github/workflows/ci.yml`, the `check` job) runs `cargo test --workspace
|
||||
--exclude epiphany-editor-gui --all-targets` at the pinned floor (1.85) —
|
||||
`--all-targets` builds every example in this crate, `conformance_suite`
|
||||
included, with default features. `resvg` (and the transitive raster stack it
|
||||
pulls: `usvg`, `tiny-skia`, font/image decoding crates) is only ever exercised
|
||||
by `epiphany-editor-gui`, which is deliberately excluded from that job because
|
||||
one of its own dependencies (`image`, via `eframe`) declares a newer MSRV than
|
||||
the workspace floor. Making `resvg` reachable from `epiphany-testkit`
|
||||
unconditionally would silently re-impose that same floor on a crate the MSRV
|
||||
job is not supposed to skip. The fix is Cargo's standard shape for "a
|
||||
dependency only some callers need": `resvg` (and `epiphany-render-svg`, not
|
||||
previously a dependency of this crate at all) go into `[dependencies]` with
|
||||
`optional = true` — **dev-dependencies cannot be optional**, which is why
|
||||
example-only code still needs a `[dependencies]` entry — gated by a
|
||||
`golden-gate` feature that nothing enables by default. `cargo tree -p
|
||||
epiphany-testkit -e normal` shows zero `resvg` nodes without the feature and
|
||||
exactly one with it (`--features golden-gate`).
|
||||
|
||||
`epiphany-editor-core` and `epiphany-engrave` needed no such treatment: T1a's
|
||||
`multisystem_click.rs` test and the Reference Suite harness (F5, above) already
|
||||
made both **unconditional dev-dependencies** of this crate, carrying no
|
||||
`resvg`-class MSRV risk of their own, so gate 9's `EditorSession` /
|
||||
`Engraver::default()` calls simply reuse them as-is.
|
||||
|
||||
**Keeping the no-feature output byte-identical was the binding constraint,**
|
||||
stated in the contract as a hard requirement (the MSRV job's `--all-targets`
|
||||
build must see zero difference). The suite's gate-count denominator moved from
|
||||
a literal `8` in every `eprintln!` to a `TOTAL_GATES` const — `8` without
|
||||
`golden-gate`, `9` with it — so every existing print line renders
|
||||
byte-for-byte what it always has when the feature is off, and gate 9's own
|
||||
line plus the renumbered final `[9/9] ok:` line appear only when it is on.
|
||||
Verified directly: the pre-tranche example source, rebuilt and run at `SCALE=0`,
|
||||
produces output diffed byte-for-byte against the post-tranche no-feature build
|
||||
of the same invocation — `diff` reports no difference.
|
||||
|
||||
**Gate 9's substance mirrors T1a's comparator, not its full machinery.** It
|
||||
re-renders `ten_measure_single_staff(0)` as opened, the same fixture after the
|
||||
scripted pencil insert (the click point re-derived by literally copying
|
||||
`goldens.rs::scripted_insert_target`'s algorithm — last system by lowest
|
||||
`bounding_box.origin.y`, rightmost Pitch-sourced notehead in that system's
|
||||
vertical band, +0.5 staff spaces right / +2.0 up — since this crate cannot
|
||||
call a `#[cfg(test)]`-private function in a different crate's binary), and
|
||||
`ten_measure_with_slurs(0)` as opened, rasterizing each exactly as
|
||||
`epiphany-editor-gui/src/main.rs::rasterize_pixmap` does (the algorithm
|
||||
copied for the same cross-crate-privacy reason: usvg parse, dimensions
|
||||
ceiling to whole pixels, opaque white background, `resvg::render`). Unlike
|
||||
`goldens.rs`'s own comparator, gate 9 is **compare-only**: no
|
||||
`EPIPHANY_BLESS_GOLDENS` path, no `actual.png`/`expected.png`/`diff.png`
|
||||
artifact writing. A mismatch's panic message points at `cargo test -p
|
||||
epiphany-editor-gui goldens` — the crate that owns the baselines, the bless
|
||||
mechanism, and the reviewable artifacts — as the place to diagnose, rather
|
||||
than duplicating that machinery here. G3 (undo-equals-G1) is deliberately not
|
||||
re-derived as a fourth comparison: it reuses G1's own baseline file in T1a,
|
||||
so covering G1 already covers what G3 would additionally lock at the pixel
|
||||
level for this gate's purposes.
|
||||
|
||||
**The cross-crate baseline path is a documented coupling, not an accident.**
|
||||
The three committed PNGs live in `crates/epiphany-editor-gui/goldens/`, one
|
||||
level up and over from this crate's own directory. Gate 9 resolves that path
|
||||
from *this* crate's `CARGO_MANIFEST_DIR` (the only directory `env!` can see at
|
||||
compile time) as `{CARGO_MANIFEST_DIR}/../epiphany-editor-gui/goldens/`. A
|
||||
rename or relocation of either crate directory, or of
|
||||
`epiphany-editor-gui/goldens/`, breaks this path — silently succeeding at
|
||||
`cargo check` (it is a runtime path, not a compile-time one) but failing gate
|
||||
9 loudly at run time with a clear "no golden baseline at ..." panic (mutation
|
||||
m6 below), never a silent skip. This crate's own `Cargo.toml` comment on the
|
||||
`epiphany-render-svg`/`resvg` dependencies and the `golden_gate` module doc in
|
||||
`examples/conformance_suite.rs` carry the same note; `epiphany-editor-gui` is
|
||||
outside this packet's blast radius (`spec/CONTRACT_EDITOR_T2_SELECTION.md`
|
||||
§W3), so its own `DECISIONS.md` — which already documents the baseline
|
||||
directory from the T1a side — is left untouched here.
|
||||
|
||||
**Mutation evidence (independently reproduced against gate 9's real run, not
|
||||
just described):**
|
||||
|
||||
- **m5 — tamper one decoded pixel before comparison.** Rendering G1, XOR-ing
|
||||
byte 0 of its pixmap with `0xFF`, then comparing that tampered pixmap
|
||||
against the committed baseline made gate 9 die with (actual panic,
|
||||
captured verbatim): `gate 9: ten_measure_open decoded RGBA differs from
|
||||
the committed baseline at .../epiphany-editor-gui/goldens/ten_measure_open.png
|
||||
(1 of 1119968 bytes differ); diagnose with \`cargo test -p
|
||||
epiphany-editor-gui goldens\``. (The pixel comparison is a manual `if
|
||||
actual.data() != expected.data() { panic!(...) }` reporting a differing-byte
|
||||
count, not a bare `assert_eq!` — the latter was tried first and dumps both
|
||||
full byte slices, ~1.1 MB each, into the panic message on a real score
|
||||
raster; the differing-byte count is the useful signal, and the visual
|
||||
artifacts to inspect already live in `epiphany-editor-gui`'s own comparator.)
|
||||
Restored by reversing the exact same byte-flip line (not `git checkout`);
|
||||
the suite returns to 9/9.
|
||||
- **m6 — point the baselines path at a nonexistent directory.** Appending
|
||||
`-DOES-NOT-EXIST` to `baseline_path`'s directory component made the
|
||||
`fs::read` call fail, and the gate panicked immediately with (captured
|
||||
verbatim): `gate 9: no golden baseline at
|
||||
.../epiphany-editor-gui/goldens-DOES-NOT-EXIST/ten_measure_open.png (No
|
||||
such file or directory (os error 2)) — \`epiphany-editor-gui/goldens/\` must
|
||||
contain the committed T1a baselines for this gate to compare against;
|
||||
diagnose with \`cargo test -p epiphany-editor-gui goldens\`` — a loud,
|
||||
named failure, never a silent skip or pass. Restored by reversing the
|
||||
substitution; the suite returns to 9/9.
|
||||
|
||||
**Baselines pin the raster stack (inherited from T1a, restated here because
|
||||
this crate now also depends on it).** Any `Cargo.lock` movement of
|
||||
`resvg`/`usvg`/`tiny-skia`/`png` is a golden-review event — for both crates
|
||||
now, not just `epiphany-editor-gui` — never a silent re-bless. Gate 9 has no
|
||||
bless mechanism of its own by design (above), so a deliberate raster-stack
|
||||
bump that changes pixels must be reviewed and re-blessed through
|
||||
`epiphany-editor-gui`'s own `EPIPHANY_BLESS_GOLDENS=1` path; gate 9 will then
|
||||
pass again against the newly-blessed baselines with no changes of its own
|
||||
needed.
|
||||
|
|
|
|||
|
|
@ -16,6 +16,17 @@ use epiphany_testkit::{
|
|||
negative, prepass_harness, roundtrip, textproj, Rng,
|
||||
};
|
||||
|
||||
/// The suite's total gate count, printed in every `[N/TOTAL_GATES]` line and
|
||||
/// the final summary. T2 W3 adds gate 9 (`golden-gate` feature); without the
|
||||
/// feature this stays at 8, so every print in this file — the final `ok`
|
||||
/// summary included — is byte-identical to before this tranche
|
||||
/// (`spec/CONTRACT_EDITOR_T2_SELECTION.md` §W3: "Without the feature the
|
||||
/// suite builds and prints exactly today's 8/8").
|
||||
#[cfg(feature = "golden-gate")]
|
||||
const TOTAL_GATES: u32 = 9;
|
||||
#[cfg(not(feature = "golden-gate"))]
|
||||
const TOTAL_GATES: u32 = 8;
|
||||
|
||||
fn main() {
|
||||
let scale: u64 = std::env::args()
|
||||
.nth(1)
|
||||
|
|
@ -30,12 +41,12 @@ fn main() {
|
|||
} else {
|
||||
1_000_000 * scale
|
||||
};
|
||||
eprintln!("[0/8] determinism round-trip gate: {det_iters} iters");
|
||||
eprintln!("[0/{TOTAL_GATES}] determinism round-trip gate: {det_iters} iters");
|
||||
roundtrip::run_determinism_roundtrip_gate(det_iters, 0x0A11_CE5E_EDED_2024);
|
||||
|
||||
// 1. Canonical round-trip (criterion 4): type-level corpus.
|
||||
let iters = (50_000 * scale).max(2_000);
|
||||
eprintln!("[1/8] canonical round-trip corpus: {iters} iters");
|
||||
eprintln!("[1/{TOTAL_GATES}] canonical round-trip corpus: {iters} iters");
|
||||
roundtrip::run_roundtrip_corpus(iters, 0x00C0_FFEE_1234_5678);
|
||||
|
||||
// 1b. Bundle manifest + reducer-bookkeeping serialization + full-Score byte
|
||||
|
|
@ -56,26 +67,26 @@ fn main() {
|
|||
|
||||
// 2. Crash safety (criterion 2): the testkit driver + Agent D's gate.
|
||||
let crash_iters = (10_000 * scale).max(1_000);
|
||||
eprintln!("[2/8] crash recovery: {crash_iters} iters (testkit) + bundle gate");
|
||||
eprintln!("[2/{TOTAL_GATES}] crash recovery: {crash_iters} iters (testkit) + bundle gate");
|
||||
bundle_harness::run_crash_recovery(crash_iters, 0xF00D_BEEF_1234_5678);
|
||||
bundle_harness::bundle_crash_recovery_fuzz(crash_iters, 0x0123_4567_89AB_CDEF);
|
||||
|
||||
// 3. Equivocation (criterion 3): testkit driver + Agent C's gate.
|
||||
eprintln!("[3/8] equivocation order-independence");
|
||||
eprintln!("[3/{TOTAL_GATES}] equivocation order-independence");
|
||||
for seed in 0..n(500) {
|
||||
equivocation::run_equivocation(16, seed.wrapping_mul(0x9E37_79B9).wrapping_add(1));
|
||||
}
|
||||
equivocation::ops_equivocation_fuzz((10_000 * scale).max(1_000), 0x1234_5678);
|
||||
|
||||
// 4. Manifest selection.
|
||||
eprintln!("[4/8] manifest selection");
|
||||
eprintln!("[4/{TOTAL_GATES}] manifest selection");
|
||||
for seed in 0..n(32) {
|
||||
bundle_harness::run_manifest_selection(seed);
|
||||
}
|
||||
|
||||
// 5. Convergence (criterion 1): real-Score convergence through reduce_onto,
|
||||
// plus the reducer-bookkeeping projection convergence.
|
||||
eprintln!("[5/8] convergence across delivery orders (real Score + bookkeeping)");
|
||||
eprintln!("[5/{TOTAL_GATES}] convergence across delivery orders (real Score + bookkeeping)");
|
||||
for seed in 0..n(64) {
|
||||
convergence::run_graph_convergence(6, seed.wrapping_mul(0x9E37_79B9).wrapping_add(11));
|
||||
}
|
||||
|
|
@ -93,7 +104,7 @@ fn main() {
|
|||
// 6. Reduction determinism (criterion 5): a large set reduced many ways, the
|
||||
// testkit's authoritative causal-order gate, + Agent C's own gate.
|
||||
let big = (1_000 * scale).max(1_000) as usize;
|
||||
eprintln!("[6/8] reduction determinism: {big}-envelope set, 10 orders");
|
||||
eprintln!("[6/{TOTAL_GATES}] reduction determinism: {big}-envelope set, 10 orders");
|
||||
{
|
||||
let mut rng = Rng::new(0x5EED_0006_0F0F_0F0F);
|
||||
let envelopes = generators::operation_envelopes(&mut rng, big, 3, 40, 40);
|
||||
|
|
@ -110,7 +121,7 @@ fn main() {
|
|||
convergence::ops_reduction_determinism_fuzz((10_000 * scale).max(1_000), 0x00C0_FFEE);
|
||||
|
||||
// 7. Layout round-trip (criterion 6).
|
||||
eprintln!("[7/8] layout round-trip");
|
||||
eprintln!("[7/{TOTAL_GATES}] layout round-trip");
|
||||
for seed in 0..n(128) {
|
||||
layout_stub::round_trip(&fixtures::ten_measure_single_staff(seed));
|
||||
layout_stub::round_trip(&generators::graph::valid_score_rich(seed));
|
||||
|
|
@ -199,5 +210,253 @@ fn main() {
|
|||
}
|
||||
}
|
||||
|
||||
eprintln!("[8/8] ok: full conformance suite passed (scale {scale})");
|
||||
// 8. T2 W3 — the [9/9] golden conformance gate (`golden-gate` feature
|
||||
// only; absent without it, and the line above prints "8/8" unchanged).
|
||||
// Re-derives the three T1a golden states headlessly and rasterizes
|
||||
// them exactly as `epiphany-editor-gui`'s demo binary does, comparing
|
||||
// decoded RGBA against the baselines committed there. See
|
||||
// `spec/CONTRACT_EDITOR_T2_SELECTION.md` §W3 and this crate's
|
||||
// `DECISIONS.md` (T2 W3).
|
||||
#[cfg(feature = "golden-gate")]
|
||||
{
|
||||
eprintln!("[8/{TOTAL_GATES}] golden conformance: T1a baselines rasterized headlessly");
|
||||
golden_gate::run();
|
||||
}
|
||||
|
||||
eprintln!("[{TOTAL_GATES}/{TOTAL_GATES}] ok: full conformance suite passed (scale {scale})");
|
||||
}
|
||||
|
||||
/// T2 W3: the `[9/9]` golden conformance gate. Promotes the T1a visual
|
||||
/// goldens (`spec/CONTRACT_EDITOR_T1A_GOLDENS.md`,
|
||||
/// `crates/epiphany-editor-gui/src/goldens.rs`) to a numbered gate in this
|
||||
/// suite, entirely behind the `golden-gate` feature so the raster stack
|
||||
/// (`resvg`, pulled in only via the optional dependencies in `Cargo.toml`)
|
||||
/// never enters the MSRV build's dependency closure
|
||||
/// (`spec/CONTRACT_EDITOR_T2_SELECTION.md` §W3).
|
||||
///
|
||||
/// **Compare-only.** Unlike `epiphany-editor-gui`'s own comparator (which can
|
||||
/// bless and writes failure artifacts), this gate only ever compares: no
|
||||
/// `EPIPHANY_BLESS_GOLDENS` path, no artifact writing. A mismatch panics with
|
||||
/// a message pointing at `cargo test -p epiphany-editor-gui goldens` — the
|
||||
/// crate that owns the baselines, the bless mechanism, and the reviewable
|
||||
/// `actual.png`/`expected.png`/`diff.png` artifacts — as the diagnostic
|
||||
/// surface.
|
||||
///
|
||||
/// **Cross-crate baseline coupling.** The three committed PNGs live in
|
||||
/// `epiphany-editor-gui/goldens/`, not in this crate — this crate compares
|
||||
/// against them without owning them, resolving the path from *this* crate's
|
||||
/// `CARGO_MANIFEST_DIR` (the only directory `env!` can see at compile time)
|
||||
/// up to the sibling crate directory. Documented here and in `DECISIONS.md`
|
||||
/// (T2 W3) and `epiphany-editor-gui/DECISIONS.md` (T1a): a `git mv` of either
|
||||
/// crate, or a rename of `epiphany-editor-gui/goldens/`, breaks this path
|
||||
/// silently unless both DECISIONS files are updated together.
|
||||
#[cfg(feature = "golden-gate")]
|
||||
mod golden_gate {
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use epiphany_core::TypedObjectId;
|
||||
use epiphany_editor_core::EditorSession;
|
||||
use epiphany_engrave::Engraver;
|
||||
use epiphany_layout_ir::{HitShape, Point};
|
||||
use epiphany_render_svg::{render, RenderOptions};
|
||||
use epiphany_testkit::fixtures;
|
||||
use resvg::tiny_skia::Pixmap;
|
||||
|
||||
/// Runs the three T1a golden-state comparisons. Panics on the first
|
||||
/// mismatch or missing baseline (never a silent skip — the `fs::read`
|
||||
/// failure branch below panics directly, exactly like
|
||||
/// `epiphany-editor-gui/src/goldens.rs::compare`'s own missing-baseline
|
||||
/// path).
|
||||
pub fn run() {
|
||||
// G1 — as opened: `ten_measure_single_staff(0)`, exactly the demo's
|
||||
// open path (`epiphany-editor-gui/src/main.rs:210`,
|
||||
// `EditorApp::new`).
|
||||
let score = fixtures::ten_measure_single_staff(0);
|
||||
let session = EditorSession::open(score, Box::new(Engraver::default()))
|
||||
.expect("the ten-measure fixture renders under the real engraver");
|
||||
assert_matches_baseline("ten_measure_open", &render_pixmap(&session));
|
||||
|
||||
// G2 — after the scripted pencil insert. The click point is
|
||||
// re-derived through the session's own inverses, replicating
|
||||
// `goldens.rs::scripted_insert_target` faithfully: the last system
|
||||
// by lowest `bounding_box.origin.y`, the rightmost Pitch-sourced
|
||||
// notehead within that system's vertical band, offset +0.5 staff
|
||||
// spaces right / +2.0 staff spaces up from that notehead's
|
||||
// right-center.
|
||||
let score = fixtures::ten_measure_single_staff(0);
|
||||
let mut session = EditorSession::open(score, Box::new(Engraver::default()))
|
||||
.expect("the ten-measure fixture renders under the real engraver");
|
||||
let target = scripted_insert_target(&session);
|
||||
let grid = session
|
||||
.default_grid_at(target)
|
||||
.expect("the target point sits over a metric region");
|
||||
let outcome = session
|
||||
.insert_note_at(target, &grid)
|
||||
.expect("the target is a clean, unoccupied insert slot");
|
||||
assert!(
|
||||
outcome.graph_changed,
|
||||
"gate 9: the scripted insert must change the score graph"
|
||||
);
|
||||
assert_matches_baseline("ten_measure_insert", &render_pixmap(&session));
|
||||
|
||||
// G4 — casting-off: `ten_measure_with_slurs(0)`
|
||||
// (`fixtures.rs:777`), the multi-system layout and cross-system
|
||||
// slur-split path the T1a baseline locks.
|
||||
let score = fixtures::ten_measure_with_slurs(0);
|
||||
let session = EditorSession::open(score, Box::new(Engraver::default()))
|
||||
.expect("the slurred ten-measure fixture renders under the real engraver");
|
||||
assert_matches_baseline("ten_measure_slurs_castoff", &render_pixmap(&session));
|
||||
}
|
||||
|
||||
/// Renders `session`'s resolved layout at `px_per_staff_space: 12.0` (the
|
||||
/// demo's default; `RenderOptions`'s other fields stay at their default,
|
||||
/// `GlyphMode::PathOutline` — no fonts) and rasterizes it, matching
|
||||
/// `render_pixmap` in `epiphany-editor-gui/src/goldens.rs`.
|
||||
fn render_pixmap(session: &EditorSession) -> Pixmap {
|
||||
let options = RenderOptions {
|
||||
px_per_staff_space: 12.0,
|
||||
..Default::default()
|
||||
};
|
||||
let output = render(session.resolved(), &options);
|
||||
rasterize(&output.svg)
|
||||
}
|
||||
|
||||
/// Rasterizes a rendered SVG string to a `tiny_skia` pixmap, replicating
|
||||
/// `epiphany-editor-gui/src/main.rs::rasterize_pixmap` exactly: usvg
|
||||
/// parse, dimensions rounded up to whole pixels, an opaque white
|
||||
/// background (so premultiplied alpha stays fully opaque), then
|
||||
/// `resvg::render` over that background. This crate cannot call the
|
||||
/// original directly (it is private to a different crate's binary), so
|
||||
/// the algorithm is copied rather than shared — see the module doc for
|
||||
/// why that coupling is acceptable and documented rather than factored
|
||||
/// out.
|
||||
fn rasterize(svg: &str) -> Pixmap {
|
||||
let tree = resvg::usvg::Tree::from_str(svg, &resvg::usvg::Options::default())
|
||||
.expect("a rendered score's SVG parses");
|
||||
let size = tree.size();
|
||||
let width = size.width().ceil().max(1.0) as u32;
|
||||
let height = size.height().ceil().max(1.0) as u32;
|
||||
let mut pixmap = Pixmap::new(width, height).expect("nonzero rasterized dimensions");
|
||||
pixmap.fill(resvg::tiny_skia::Color::WHITE);
|
||||
resvg::render(
|
||||
&tree,
|
||||
resvg::tiny_skia::Transform::identity(),
|
||||
&mut pixmap.as_mut(),
|
||||
);
|
||||
pixmap
|
||||
}
|
||||
|
||||
/// The click point G2 scripts — derived from `session`'s own rendered
|
||||
/// geometry, never a magic screen constant. Faithfully replicates
|
||||
/// `epiphany-editor-gui/src/goldens.rs::scripted_insert_target`; see that
|
||||
/// function's doc comment for the full derivation rationale (why the
|
||||
/// *last* system is the one with the lowest `bounding_box.origin.y`, and
|
||||
/// why the target is the rightmost Pitch-sourced notehead within that
|
||||
/// system's vertical band rather than the page-wide rightmost notehead).
|
||||
fn scripted_insert_target(session: &EditorSession) -> Point {
|
||||
let last_system = session
|
||||
.resolved()
|
||||
.pages
|
||||
.iter()
|
||||
.flat_map(|page| &page.systems)
|
||||
.min_by(|a, b| {
|
||||
a.bounding_box
|
||||
.origin
|
||||
.y
|
||||
.0
|
||||
.total_cmp(&b.bounding_box.origin.y.0)
|
||||
})
|
||||
.expect("casting-off produced at least one system");
|
||||
let sys_bottom = last_system.bounding_box.origin.y.0;
|
||||
let sys_top = sys_bottom + last_system.bounding_box.size.height.0;
|
||||
|
||||
let last_notehead = session
|
||||
.hit_test()
|
||||
.regions
|
||||
.iter()
|
||||
.filter_map(|r| match (&r.source, r.shape) {
|
||||
(TypedObjectId::Pitch(_), HitShape::Box(b)) => Some(b),
|
||||
_ => None,
|
||||
})
|
||||
.filter(|b| {
|
||||
let mid_y = (b.bottom.0 + b.top.0) / 2.0;
|
||||
(sys_bottom..=sys_top).contains(&mid_y)
|
||||
})
|
||||
.max_by(|a, b| a.right.0.total_cmp(&b.right.0))
|
||||
.expect("the last system renders at least one notehead");
|
||||
|
||||
Point::new(
|
||||
last_notehead.right.0 + 0.5,
|
||||
(last_notehead.bottom.0 + last_notehead.top.0) / 2.0 + 2.0,
|
||||
)
|
||||
}
|
||||
|
||||
/// Compares `actual` against the committed baseline
|
||||
/// `epiphany-editor-gui/goldens/{name}.png`: decoded RGBA, dimensions
|
||||
/// first (the Ruling-C contract) — never encoded PNG bytes. Compare-only:
|
||||
/// no bless, no artifact writing (see the module doc). A missing or
|
||||
/// undecodable baseline panics directly, the same "loud failure, never a
|
||||
/// silent skip" contract as `goldens.rs::compare`.
|
||||
fn assert_matches_baseline(name: &str, actual: &Pixmap) {
|
||||
let path = baseline_path(name);
|
||||
let bytes = fs::read(&path).unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"gate 9: no golden baseline at {} ({err}) — `epiphany-editor-gui/goldens/` must \
|
||||
contain the committed T1a baselines for this gate to compare against; \
|
||||
diagnose with `cargo test -p epiphany-editor-gui goldens`",
|
||||
path.display()
|
||||
)
|
||||
});
|
||||
let expected = Pixmap::decode_png(&bytes).unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"gate 9: baseline at {} is not a decodable PNG: {err}",
|
||||
path.display()
|
||||
)
|
||||
});
|
||||
assert_eq!(
|
||||
(actual.width(), actual.height()),
|
||||
(expected.width(), expected.height()),
|
||||
"gate 9: {name} dimensions differ from the committed baseline at {} (actual {}x{} \
|
||||
vs expected {}x{}); diagnose with `cargo test -p epiphany-editor-gui goldens`",
|
||||
path.display(),
|
||||
actual.width(),
|
||||
actual.height(),
|
||||
expected.width(),
|
||||
expected.height(),
|
||||
);
|
||||
// A plain `assert_eq!` here would dump both full byte slices (millions
|
||||
// of bytes for a real score raster) into the panic message; a
|
||||
// differing-byte count is the useful signal, and `cargo test -p
|
||||
// epiphany-editor-gui goldens` is where the actual/expected/diff PNG
|
||||
// artifacts to inspect visually already live.
|
||||
if actual.data() != expected.data() {
|
||||
let differing = actual
|
||||
.data()
|
||||
.iter()
|
||||
.zip(expected.data())
|
||||
.filter(|(a, b)| a != b)
|
||||
.count();
|
||||
panic!(
|
||||
"gate 9: {name} decoded RGBA differs from the committed baseline at {} \
|
||||
({differing} of {} bytes differ); diagnose with `cargo test -p \
|
||||
epiphany-editor-gui goldens`",
|
||||
path.display(),
|
||||
actual.data().len(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The committed baseline path for a named golden:
|
||||
/// `{CARGO_MANIFEST_DIR}/../epiphany-editor-gui/goldens/{name}.png` — this
|
||||
/// crate's `CARGO_MANIFEST_DIR` is `crates/epiphany-testkit`, and the
|
||||
/// baselines live in the sibling crate `crates/epiphany-editor-gui`. See
|
||||
/// the module doc's "Cross-crate baseline coupling" note.
|
||||
fn baseline_path(name: &str) -> PathBuf {
|
||||
PathBuf::from(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../epiphany-editor-gui/goldens"
|
||||
))
|
||||
.join(format!("{name}.png"))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue