480 lines
22 KiB
Rust
480 lines
22 KiB
Rust
//! Drives the whole conformance suite at scale from the command line, so the
|
|
//! heavy gates can run outside the unit-test timeout — the analogue of
|
|
//! `epiphany-determinism`'s `fuzz_roundtrip` and `epiphany-bundle`'s
|
|
//! `fuzz_crash`. Every gate is deterministic, so any failure reproduces from
|
|
//! its seed.
|
|
//!
|
|
//! Usage:
|
|
//! cargo run --release --example conformance_suite [SCALE]
|
|
//!
|
|
//! `SCALE` (default 1) multiplies the iteration counts. `SCALE=10` is a soak
|
|
//! run; `SCALE=0` runs a fast smoke pass. Exits non-zero (via panic) on the
|
|
//! first violation.
|
|
|
|
use epiphany_testkit::{
|
|
bundle_harness, convergence, corpus, editloop, equivocation, fixtures, generators, gminor,
|
|
layout_stub, 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)
|
|
.map(|s| s.parse().expect("SCALE must be an integer"))
|
|
.unwrap_or(1);
|
|
let n = |base: u64| (base * scale).max(if base == 0 { 0 } else { 1 });
|
|
|
|
// 0. Agent A's headline hand-off gate: 1,000,000 round-trip iterations.
|
|
// (Scaled; at SCALE=0 a smaller smoke count still runs.)
|
|
let det_iters = if scale == 0 {
|
|
50_000
|
|
} else {
|
|
1_000_000 * scale
|
|
};
|
|
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/{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
|
|
// round-trip (item 5's whole-score codec, via reduce_onto).
|
|
eprintln!("[1b ] manifest + bookkeeping + full-Score serialization stability");
|
|
for seed in 0..n(64) {
|
|
roundtrip::assert_manifest_roundtrip(&roundtrip::committed_manifest(seed));
|
|
let mut rng = Rng::new(seed.wrapping_mul(0x0100_0193).wrapping_add(17));
|
|
roundtrip::assert_manifest_roundtrip(&generators::rich_manifest(&mut rng));
|
|
let session = generators::operation_envelopes(&mut rng, 40, 3, 6, 6);
|
|
roundtrip::assert_reduction_serialization_stable(&session, seed);
|
|
let (score, frontier) = convergence::materialized_score(seed.wrapping_add(101));
|
|
roundtrip::assert_score_serialization_stable(&score, &frontier, seed);
|
|
let envs = generators::operation_envelopes(&mut rng, 24, 3, 8, 8);
|
|
roundtrip::assert_operation_block_summary_survives_storage(&envs, seed.wrapping_add(202));
|
|
}
|
|
roundtrip::assert_content_mutation_changes_serialization();
|
|
|
|
// 2. Crash safety (criterion 2): the testkit driver + Agent D's gate.
|
|
let crash_iters = (10_000 * scale).max(1_000);
|
|
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/{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/{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/{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));
|
|
}
|
|
for seed in 0..n(500) {
|
|
convergence::run_convergence(24, 8, seed.wrapping_mul(0x9E37_79B9));
|
|
}
|
|
for seed in 0..n(32) {
|
|
convergence::run_two_staff_convergence(8, seed.wrapping_mul(0x9E37_79B9).wrapping_add(7));
|
|
}
|
|
|
|
// 5b. Audit regression guards (every defect the Agent C audit surfaced).
|
|
eprintln!("[5b ] audit defect regression guards");
|
|
negative::run_all();
|
|
|
|
// 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/{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);
|
|
convergence::assert_causal_order_respected(&envelopes);
|
|
convergence::assert_reduction_determinism(&envelopes, 10, &mut rng);
|
|
}
|
|
// Authoritative: causal-order correctness over many conformant sets.
|
|
convergence::run_authoritative_reduction_gate(
|
|
(10_000 * scale).max(1_000) as usize,
|
|
3,
|
|
0x0CA0_05A1,
|
|
);
|
|
// Supplementary: Agent C's own hand-off gate (permutation invariance).
|
|
convergence::ops_reduction_determinism_fuzz((10_000 * scale).max(1_000), 0x00C0_FFEE);
|
|
|
|
// 7. Layout round-trip (criterion 6).
|
|
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));
|
|
}
|
|
|
|
// 7b. Track A — Agent H pre-pass merge gate (spelling + decomposition):
|
|
// the representative-corpus eligibility taxonomy (F3) and the H harness
|
|
// (F4) — determinism, spelling correctness, decomposition reconstruction,
|
|
// RespellPitch precedence, and the non-vacuity tripwire — at scale.
|
|
eprintln!("[7b ] Agent H pre-pass gate: taxonomy coverage + merge gate");
|
|
corpus::run_all();
|
|
prepass_harness::run_all(scale.max(1));
|
|
|
|
// 7c. Track A — the UI seam: the editing-loop vertical slice (hit-test → score
|
|
// object → operation → reduce → re-layout → re-render → re-resolve
|
|
// selection). Every fixture must drive a click→sharpen→re-render cycle whose
|
|
// selection survives the relayout — the contract a GUI's correctness rests
|
|
// on. (The harness `epiphany-editor-core` packages as a callable API.)
|
|
eprintln!("[7c ] UI-seam gate: editing loop over the corpus");
|
|
for seed in 0..n(48) {
|
|
for score in [
|
|
fixtures::ten_measure_single_staff(seed),
|
|
generators::graph::valid_score_rich(seed),
|
|
] {
|
|
let report = editloop::run_edit_loop(&score).unwrap_or_else(|| {
|
|
panic!("seed {seed}: no clickable notehead to drive the editing loop")
|
|
});
|
|
assert!(
|
|
report.graph_changed,
|
|
"edit-loop seed {seed}: graph unchanged"
|
|
);
|
|
assert!(
|
|
report.selection_preserved,
|
|
"edit-loop seed {seed}: selection lost across relayout"
|
|
);
|
|
assert!(
|
|
report.render_changed,
|
|
"edit-loop seed {seed}: edit not visible"
|
|
);
|
|
}
|
|
}
|
|
|
|
eprintln!("[7d ] cross-implementation decode conformance vectors");
|
|
{
|
|
use epiphany_testkit::vectors;
|
|
assert_eq!(
|
|
vectors::COMMITTED,
|
|
vectors::render(),
|
|
"{} is stale; regenerate with `cargo run -q -p epiphany-testkit \
|
|
--example generate_vectors`",
|
|
vectors::PATH
|
|
);
|
|
match vectors::verify(vectors::COMMITTED) {
|
|
Ok(n) => eprintln!(" {n} vectors, every verdict agreed"),
|
|
Err(failures) => panic!(
|
|
"{} decode-vector disagreement(s):\n{}",
|
|
failures.len(),
|
|
failures.join("\n")
|
|
),
|
|
}
|
|
}
|
|
|
|
// 7e. Whole-document Text Projection vectors. The strong equation is over
|
|
// text bytes, never bundle identity; the generated-operation pass also
|
|
// checks the weaker semantic equation by reducing both sides.
|
|
eprintln!("[7e ] Text Projection whole-document conformance vectors");
|
|
for seed in 0..n(16) {
|
|
textproj::assert_semantics_preserved(seed);
|
|
}
|
|
{
|
|
use epiphany_textproj::vectors;
|
|
assert_eq!(
|
|
vectors::COMMITTED,
|
|
vectors::render(),
|
|
"{} is stale; regenerate with `cargo run -q -p epiphany-testkit \
|
|
--example generate_vectors`",
|
|
vectors::PATH
|
|
);
|
|
match vectors::verify(vectors::COMMITTED) {
|
|
Ok(n) => eprintln!(" {n} vectors, every verdict agreed"),
|
|
Err(failures) => panic!(
|
|
"{} Text Projection vector disagreement(s):\n{}",
|
|
failures.len(),
|
|
failures.join("\n")
|
|
),
|
|
}
|
|
}
|
|
|
|
// 7f. G-minor (spec/PLAN_GMINOR_SCHEMA_MINOR.md §4, pin 11): the
|
|
// independent oracle over the manifest's carried schema minor, built
|
|
// from known, decodable, in-tree fixtures — never routed through
|
|
// `TOTAL_GATES`, the same convention 7b-7e already use (this is a
|
|
// sub-gate of gate 7, not a new top-level numbered gate). It is NOT
|
|
// evidence that `epiphany-textproj` validates arbitrary hand edits
|
|
// (pin 11.4): that layer stays a preserving producer by ruled design
|
|
// (a), carrying whatever SchemaVersion a document declares verbatim.
|
|
eprintln!("[7f ] G-minor: manifest schema-minor oracle over in-tree fixtures");
|
|
{
|
|
let (checked, not_checkable) = gminor::run_gate();
|
|
eprintln!(
|
|
" {checked} fixture(s) checked (equality oracle), {not_checkable} \
|
|
reported not-checkable (never counted as a pass)"
|
|
);
|
|
}
|
|
|
|
// 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"))
|
|
}
|
|
}
|