547 lines
20 KiB
Rust
547 lines
20 KiB
Rust
//! The five committed fixture strings (recipe §2) and the precommitted
|
|
//! measured expectations (recipe §4) the generator checks its own shaped
|
|
//! output against.
|
|
//!
|
|
//! Every string below is a Rust string literal with every non-ASCII
|
|
//! codepoint escaped (recipe §2: "so the file is unambiguous under any
|
|
//! editor or normalization"). **Never re-record an expectation to make a
|
|
//! check pass**: [`check_against_recipe`] returning an error is this crate's
|
|
//! signal to stop and report the disagreement, not to edit the literal it
|
|
//! failed against.
|
|
|
|
use epiphany_core::{EventId, TypedObjectId};
|
|
use epiphany_layout_ir::{BoundingBox as RealBoundingBox, Point as RealPoint, Provenance};
|
|
use ttf_parser::GlyphId;
|
|
|
|
use crate::faces::LoadedFace;
|
|
use crate::identity::build_shaping_identity;
|
|
use crate::invariants;
|
|
use crate::shape::shape_text;
|
|
use crate::types::{
|
|
SpikeBoundingBox, SpikeGlyphStyle, SpikePoint, SpikeResolvedText, SpikeTextAlign,
|
|
};
|
|
use crate::{EM_SIZE_STAFF_SPACE, RUN_ORIGIN_STAFF};
|
|
|
|
/// One committed fixture: its id, W3 §5 purpose, and verbatim literal.
|
|
pub struct FixtureDef {
|
|
pub id: &'static str,
|
|
pub purpose: &'static str,
|
|
pub text: &'static str,
|
|
}
|
|
|
|
/// The five fixtures, in the recipe §2 table's order. This exact set, in
|
|
/// this exact order, is what `output::FixtureFile::validate` restates as a
|
|
/// literal roster (mirroring `round1-candidates/harness`'s `ROUND1_ROSTER`
|
|
/// discipline).
|
|
pub const FIXTURES: &[FixtureDef] = &[
|
|
FixtureDef {
|
|
id: "F-A",
|
|
purpose: "check 1 (faithful consumption), check 5 (accessibility)",
|
|
text: "Allegro affettuoso \u{2014} al fine",
|
|
},
|
|
FixtureDef {
|
|
id: "F-B",
|
|
purpose: "check 2 (fallback, forced)",
|
|
text: "Coro \u{05D0}\u{05D1}\u{05D2}",
|
|
},
|
|
FixtureDef {
|
|
id: "F-C",
|
|
purpose: "check 2 (uncovered codepoint)",
|
|
text: "Coro \u{0627}",
|
|
},
|
|
// **Not "check 3 (bidi)".** Ruled 2026-07-29 (recipe §1.2): check 3
|
|
// requires an Arabic/Latin run, no Arabic-capable face exists on this
|
|
// machine, and pin 9 makes an absent required face an environmental
|
|
// `NOT RUN`. F-D is Hebrew/Latin and cannot exercise contextual Arabic
|
|
// joining, so it is scored on its own **Supplementary** row and must never
|
|
// upgrade check 3 to PASS. The purpose string says so because this string
|
|
// is what `fixtures.json`, `FIXTURES_SUMMARY.md` and every generator's
|
|
// console output print — labelling it "check 3" there recreates exactly
|
|
// the scoring ambiguity the ruling forbids, whatever the recipe says
|
|
// elsewhere.
|
|
FixtureDef {
|
|
id: "F-D",
|
|
purpose: "SUPPLEMENTARY bidi evidence (Hebrew/Latin) — check 3 remains NOT RUN \
|
|
(no Arabic-capable face; recipe §1.2)",
|
|
text: "Allegro \u{05D0}\u{05D1}\u{05D2} con brio",
|
|
},
|
|
FixtureDef {
|
|
id: "F-E",
|
|
purpose: "check 4 (hit testing / caret)",
|
|
text: "Cafe\u{301} \u{2014} resume\u{301}",
|
|
},
|
|
];
|
|
|
|
/// Builds one fixture's `SpikeResolvedText`: shapes it against the resolved
|
|
/// faces, computes `bounds`/`reserved_box`/`origin`, and asserts every W3 §5
|
|
/// invariant before returning.
|
|
pub fn build_fixture(
|
|
def: &FixtureDef,
|
|
faces: &[LoadedFace],
|
|
fixture_ordinal: u64,
|
|
) -> SpikeResolvedText {
|
|
let shaped = shape_text(def.text, faces);
|
|
// Recipe §3's nominal `(1.6, 0.0)` is not itself on the `1/1024` grid
|
|
// (see `crate::quantize`'s doc comment) — invariant 5 requires every
|
|
// position this crate records to be, `origin` included, so it is
|
|
// quantized like everything else rather than kept as the raw literal.
|
|
let origin = SpikePoint::new(
|
|
crate::quantize::quantize_component(RUN_ORIGIN_STAFF.0 as f64),
|
|
crate::quantize::quantize_component(RUN_ORIGIN_STAFF.1 as f64),
|
|
);
|
|
let bounds = compute_bounds(&shaped.segments, faces, origin);
|
|
// Reserved-box policy (§3E: "a solver policy over bounds — padding, a
|
|
// minimum allocation"): this spike has no real solver, so the policy is
|
|
// the simplest defensible one — bounds padded by a fixed 0.1 staff space
|
|
// on every side — recorded as a *named* policy, not an unshaped guess.
|
|
const PAD: f64 = 0.1;
|
|
let reserved_box = SpikeBoundingBox {
|
|
left: crate::quantize::quantize_component(bounds.left - PAD),
|
|
bottom: crate::quantize::quantize_component(bounds.bottom - PAD),
|
|
right: crate::quantize::quantize_component(bounds.right + PAD),
|
|
top: crate::quantize::quantize_component(bounds.top + PAD),
|
|
};
|
|
|
|
let identity = build_shaping_identity(faces.iter().map(|f| f.identity.clone()).collect());
|
|
|
|
let source = TypedObjectId::Event(EventId::from_raw(0xF00D_0000 + fixture_ordinal as u128));
|
|
let real_provenance = Provenance::projected(source, Vec::new());
|
|
|
|
let rt = SpikeResolvedText {
|
|
provenance: (&real_provenance).into(),
|
|
text: def.text.to_string(),
|
|
shaping: identity,
|
|
segments: shaped.segments,
|
|
clusters: shaped.clusters,
|
|
bounds,
|
|
reserved_box,
|
|
origin,
|
|
align: SpikeTextAlign::Start,
|
|
style: SpikeGlyphStyle { rgba: 0x0000_00ff },
|
|
layer: 0,
|
|
};
|
|
|
|
let expect_unresolved = def.id == "F-C";
|
|
invariants::assert_all(def.id, &rt, expect_unresolved);
|
|
if def.id == "F-D" {
|
|
invariants::assert_direction_boundary_stops_differ("F-D", &rt, 8)
|
|
.expect("F-D byte 8 direction-boundary stops");
|
|
invariants::assert_direction_boundary_stops_differ("F-D", &rt, 14)
|
|
.expect("F-D byte 14 direction-boundary stops");
|
|
}
|
|
|
|
check_against_recipe(def.id, &rt).unwrap_or_else(|e| {
|
|
panic!(
|
|
"{}: measured shaping disagrees with recipe §4's precommitted expectation — STOPPING \
|
|
per the packet's rule against silent re-recording:\n{e}",
|
|
def.id
|
|
)
|
|
});
|
|
|
|
rt
|
|
}
|
|
|
|
/// Real-type ink bounding box over every positioned glyph across every
|
|
/// segment, computed from each glyph's *own resolving face*'s outline
|
|
/// bounds (`ttf_parser::Face::glyph_bounding_box`) — never estimated from
|
|
/// advances. A glyph with no outline (a space) contributes no extent but
|
|
/// still occupies pen advance, exactly as `PositionedGlyph::offset` already
|
|
/// records.
|
|
fn compute_bounds(
|
|
segments: &[crate::types::SpikeShapedSegment],
|
|
faces: &[LoadedFace],
|
|
origin: SpikePoint,
|
|
) -> SpikeBoundingBox {
|
|
let mut left = f64::INFINITY;
|
|
let mut bottom = f64::INFINITY;
|
|
let mut right = f64::NEG_INFINITY;
|
|
let mut top = f64::NEG_INFINITY;
|
|
|
|
for seg in segments {
|
|
let Some(face_idx) = seg.face else { continue };
|
|
let loaded = &faces[face_idx as usize];
|
|
let rb_face = loaded.face();
|
|
let upem = rb_face.units_per_em() as f64;
|
|
let scale = EM_SIZE_STAFF_SPACE / upem;
|
|
for g in &seg.glyphs {
|
|
let Some(bbox) = rb_face.glyph_bounding_box(GlyphId(g.glyph_id as u16)) else {
|
|
continue;
|
|
};
|
|
let gx = origin.x + g.offset.x;
|
|
let gy = origin.y + g.offset.y;
|
|
left = left.min(gx + bbox.x_min as f64 * scale);
|
|
right = right.max(gx + bbox.x_max as f64 * scale);
|
|
bottom = bottom.min(gy + bbox.y_min as f64 * scale);
|
|
top = top.max(gy + bbox.y_max as f64 * scale);
|
|
}
|
|
}
|
|
|
|
if !left.is_finite() {
|
|
// No glyph produced ink (a degenerate all-unresolved fixture) — an
|
|
// empty box at the origin rather than an infinite one, matching
|
|
// `epiphany_layout_ir::BoundingBox::default()`'s zero convention.
|
|
let real_default: RealBoundingBox = RealBoundingBox::default();
|
|
return SpikeBoundingBox::from(real_default);
|
|
}
|
|
// Round-trip through the real `BoundingBox`/`Point` types (they carry no
|
|
// extra invariant beyond `f32` storage) so this function is honestly
|
|
// computing the *real* type's value, not a shape only this crate defines.
|
|
let _real_point_smoke_test = RealPoint::new(left as f32, bottom as f32);
|
|
SpikeBoundingBox {
|
|
left: crate::quantize::quantize_component(left),
|
|
bottom: crate::quantize::quantize_component(bottom),
|
|
right: crate::quantize::quantize_component(right),
|
|
top: crate::quantize::quantize_component(top),
|
|
}
|
|
}
|
|
|
|
/// Checks the shaped result against recipe §4's precommitted, measured
|
|
/// expectations. **A mismatch is stopped and reported, never silently
|
|
/// re-recorded** — this function's only job is to say which fact disagreed.
|
|
fn check_against_recipe(id: &str, rt: &SpikeResolvedText) -> Result<(), String> {
|
|
match id {
|
|
"F-A" => check_f_a(rt),
|
|
"F-B" => check_f_b(rt),
|
|
"F-C" => check_f_c(rt),
|
|
"F-D" => check_f_d(rt),
|
|
"F-E" => check_f_e(rt),
|
|
other => Err(format!(
|
|
"no recipe §4 check registered for fixture {other:?}"
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn total_glyphs(rt: &SpikeResolvedText) -> usize {
|
|
rt.segments.iter().map(|s| s.glyphs.len()).sum()
|
|
}
|
|
|
|
/// F-A: 28 codepoints, 30 bytes, 26 glyphs, all face 0. `ff` ligature at
|
|
/// byte 9 -> gid 234; `fi` ligature at byte 26 -> gid 97; em dash -> gid 119.
|
|
fn check_f_a(rt: &SpikeResolvedText) -> Result<(), String> {
|
|
expect_eq("codepoints", rt.text.chars().count(), 28)?;
|
|
expect_eq("bytes", rt.text.len(), 30)?;
|
|
expect_eq("glyphs", total_glyphs(rt), 26)?;
|
|
// Recipe §4 does not literally write "1 segment" for F-A — it says "all
|
|
// face 0", from which a single segment follows (unidirectional Latin
|
|
// text, one face throughout, nothing to split on). This is a derived
|
|
// check, not a quoted number; `output::FixtureFile::validate` does not
|
|
// repeat it as a "recipe §4 literal" for exactly that reason.
|
|
expect_eq("segments", rt.segments.len(), 1)?;
|
|
if rt.segments[0].face != Some(0) {
|
|
return Err(format!(
|
|
"F-A segment 0 resolved to face {:?}, recipe says face 0",
|
|
rt.segments[0].face
|
|
));
|
|
}
|
|
expect_cluster_glyph("F-A ff ligature", rt, 9, 11, 234)?;
|
|
expect_cluster_glyph("F-A fi ligature", rt, 26, 28, 97)?;
|
|
// The em dash: U+2014 sits at byte offset 19 (after "Allegro affettuoso "
|
|
// — "Allegro " is 8 bytes, "affettuoso " is 11 bytes, 8+11=19).
|
|
expect_cluster_glyph("F-A em dash", rt, 19, 22, 119)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// F-B: Latin head "Coro " (5 bytes) -> 5 glyphs on face 0; Hebrew tail
|
|
/// (6 bytes) -> 3 glyphs on face 1, RTL, clusters descending 4/2/0 (segment-
|
|
/// relative). Two segments, two faces.
|
|
fn check_f_b(rt: &SpikeResolvedText) -> Result<(), String> {
|
|
expect_eq("segments", rt.segments.len(), 2)?;
|
|
let head = &rt.segments[0];
|
|
let tail = &rt.segments[1];
|
|
expect_eq(
|
|
"F-B head bytes",
|
|
(head.source.end - head.source.start) as usize,
|
|
5,
|
|
)?;
|
|
expect_eq("F-B head glyphs", head.glyphs.len(), 5)?;
|
|
if head.face != Some(0) {
|
|
return Err(format!(
|
|
"F-B head resolved to face {:?}, recipe says face 0",
|
|
head.face
|
|
));
|
|
}
|
|
expect_eq(
|
|
"F-B tail bytes",
|
|
(tail.source.end - tail.source.start) as usize,
|
|
6,
|
|
)?;
|
|
expect_eq("F-B tail glyphs", tail.glyphs.len(), 3)?;
|
|
if tail.face != Some(1) {
|
|
return Err(format!(
|
|
"F-B tail resolved to face {:?}, recipe says face 1",
|
|
tail.face
|
|
));
|
|
}
|
|
if tail.direction != crate::types::SpikeTextDirection::Rtl {
|
|
return Err("F-B tail must be Rtl".to_string());
|
|
}
|
|
// Clusters covering the tail must exist at absolute bytes 5,7,9 (segment-
|
|
// relative 0,2,4), each a single grapheme/glyph, descending in the
|
|
// shaped glyph *array* order (checked via clusters' recorded glyph
|
|
// index order matching descending source offsets is implicit in the
|
|
// shaping; here we assert the three source spans exist).
|
|
for abs in [5u32, 7, 9] {
|
|
let found = rt
|
|
.clusters
|
|
.clusters
|
|
.iter()
|
|
.any(|c| c.source.start == abs && c.segment == 1);
|
|
if !found {
|
|
return Err(format!(
|
|
"F-B: no cluster starts at absolute byte {abs} in the Hebrew segment"
|
|
));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// F-C: U+0627 resolves in neither face — an explicit unresolved cluster.
|
|
fn check_f_c(rt: &SpikeResolvedText) -> Result<(), String> {
|
|
let unresolved_count = rt.clusters.clusters.iter().filter(|c| !c.resolved).count();
|
|
if unresolved_count != 1 {
|
|
return Err(format!(
|
|
"F-C: expected exactly 1 unresolved cluster, found {unresolved_count}"
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// F-D: base level 0; visual runs `0..8` (Latn, face 0), `8..14` (Hebr,
|
|
/// face 1), `14..23` (Latn, face 0). Three segments.
|
|
fn check_f_d(rt: &SpikeResolvedText) -> Result<(), String> {
|
|
expect_eq("segments", rt.segments.len(), 3)?;
|
|
let expected = [
|
|
(0u32, 8u32, Some(0u32)),
|
|
(8, 14, Some(1)),
|
|
(14, 23, Some(0)),
|
|
];
|
|
for (i, (s, e, face)) in expected.into_iter().enumerate() {
|
|
let seg = &rt.segments[i];
|
|
if seg.source.start != s || seg.source.end != e {
|
|
return Err(format!(
|
|
"F-D segment[{i}] source {:?}, recipe says {s}..{e}",
|
|
seg.source
|
|
));
|
|
}
|
|
if seg.face != face {
|
|
return Err(format!(
|
|
"F-D segment[{i}] face {:?}, recipe says {face:?}",
|
|
seg.face
|
|
));
|
|
}
|
|
}
|
|
if rt.segments[1].direction != crate::types::SpikeTextDirection::Rtl {
|
|
return Err("F-D middle segment must be Rtl".to_string());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// F-E: 15 codepoints, 19 bytes, 13 glyphs; `e`+U+0301 composes to gid 198
|
|
/// at byte 3 and again at byte 16; 13 graphemes.
|
|
fn check_f_e(rt: &SpikeResolvedText) -> Result<(), String> {
|
|
expect_eq("codepoints", rt.text.chars().count(), 15)?;
|
|
expect_eq("bytes", rt.text.len(), 19)?;
|
|
expect_eq("glyphs", total_glyphs(rt), 13)?;
|
|
let grapheme_count: u32 = rt.clusters.clusters.iter().map(|c| c.grapheme_count).sum();
|
|
expect_eq("graphemes", grapheme_count as usize, 13)?;
|
|
// e+U+0301 is 3 bytes (1 + 2); the composed cluster covering it should
|
|
// span exactly 3 bytes and carry 1 glyph (gid 198), at byte 3 and byte 16.
|
|
expect_composed_e_acute(rt, 3)?;
|
|
expect_composed_e_acute(rt, 16)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn expect_composed_e_acute(rt: &SpikeResolvedText, start: u32) -> Result<(), String> {
|
|
let c = rt
|
|
.clusters
|
|
.clusters
|
|
.iter()
|
|
.find(|c| c.source.start == start)
|
|
.ok_or_else(|| format!("F-E: no cluster starts at byte {start}"))?;
|
|
if c.source.end - c.source.start != 3 {
|
|
return Err(format!(
|
|
"F-E: cluster at byte {start} spans {} bytes, recipe says 3 (e + U+0301)",
|
|
c.source.end - c.source.start
|
|
));
|
|
}
|
|
if c.glyph_indices.len() != 1 {
|
|
return Err(format!(
|
|
"F-E: cluster at byte {start} carries {} glyphs, recipe says 1 (composed)",
|
|
c.glyph_indices.len()
|
|
));
|
|
}
|
|
let seg = &rt.segments[c.segment];
|
|
let gid = seg.glyphs[c.glyph_indices[0] as usize].glyph_id;
|
|
if gid != 198 {
|
|
return Err(format!(
|
|
"F-E: cluster at byte {start} is gid {gid}, recipe says gid 198"
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Asserts the (single) glyph named by the cluster starting at `start`
|
|
/// (spanning to `end`) is `expected_gid`.
|
|
fn expect_cluster_glyph(
|
|
label: &str,
|
|
rt: &SpikeResolvedText,
|
|
start: u32,
|
|
end: u32,
|
|
expected_gid: u32,
|
|
) -> Result<(), String> {
|
|
let c = rt
|
|
.clusters
|
|
.clusters
|
|
.iter()
|
|
.find(|c| c.source.start == start && c.source.end == end)
|
|
.ok_or_else(|| format!("{label}: no cluster spans {start}..{end}"))?;
|
|
if c.glyph_indices.len() != 1 {
|
|
return Err(format!(
|
|
"{label}: cluster {start}..{end} carries {} glyphs, expected exactly 1",
|
|
c.glyph_indices.len()
|
|
));
|
|
}
|
|
let seg = &rt.segments[c.segment];
|
|
let gid = seg.glyphs[c.glyph_indices[0] as usize].glyph_id;
|
|
if gid != expected_gid {
|
|
return Err(format!(
|
|
"{label}: cluster {start}..{end} is gid {gid}, recipe says gid {expected_gid}"
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn expect_eq(label: &str, actual: usize, expected: usize) -> Result<(), String> {
|
|
if actual != expected {
|
|
return Err(format!(
|
|
"{label}: measured {actual}, recipe §4 records {expected}"
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::faces::{resolve_declared_chain, FaceResolution, LoadedFace};
|
|
|
|
/// Loads the real declared faces, or `None` (pin 14: environment
|
|
/// absence) — mirrors `output::tests::real_valid_file`'s pattern so
|
|
/// these mutation tests exercise the *real* recipe-§4 checks against
|
|
/// genuinely shaped output, not a hand-built stand-in.
|
|
fn real_faces() -> Option<Vec<LoadedFace>> {
|
|
let mut out = Vec::new();
|
|
for r in resolve_declared_chain() {
|
|
match r {
|
|
FaceResolution::Loaded(lf) => out.push(lf),
|
|
FaceResolution::Missing { .. } => return None,
|
|
}
|
|
}
|
|
Some(out)
|
|
}
|
|
|
|
fn require_faces() -> Vec<LoadedFace> {
|
|
real_faces().expect("this test requires the two declared faces to be present")
|
|
}
|
|
|
|
#[test]
|
|
fn check_f_a_kills_a_wrong_ligature_gid() {
|
|
let faces = require_faces();
|
|
let mut rt = build_fixture(&FIXTURES[0], &faces, 0);
|
|
let c = rt
|
|
.clusters
|
|
.clusters
|
|
.iter()
|
|
.find(|c| c.source.start == 9)
|
|
.unwrap()
|
|
.clone();
|
|
let seg = &mut rt.segments[c.segment];
|
|
seg.glyphs[c.glyph_indices[0] as usize].glyph_id = 1; // not gid 234
|
|
let err = check_f_a(&rt).unwrap_err();
|
|
assert!(err.contains("gid 234"), "{err}");
|
|
}
|
|
|
|
#[test]
|
|
fn check_f_a_kills_a_wrong_em_dash_gid() {
|
|
let faces = require_faces();
|
|
let mut rt = build_fixture(&FIXTURES[0], &faces, 0);
|
|
let c = rt
|
|
.clusters
|
|
.clusters
|
|
.iter()
|
|
.find(|c| c.source.start == 19)
|
|
.unwrap()
|
|
.clone();
|
|
let seg = &mut rt.segments[c.segment];
|
|
seg.glyphs[c.glyph_indices[0] as usize].glyph_id = 1;
|
|
let err = check_f_a(&rt).unwrap_err();
|
|
assert!(err.contains("gid 119"), "{err}");
|
|
}
|
|
|
|
#[test]
|
|
fn check_f_b_kills_a_swapped_face_assignment() {
|
|
let faces = require_faces();
|
|
let mut rt = build_fixture(&FIXTURES[1], &faces, 1);
|
|
rt.segments[1].face = Some(0); // Hebrew tail must be face 1, not 0
|
|
let err = check_f_b(&rt).unwrap_err();
|
|
assert!(err.contains("face 1"), "{err}");
|
|
}
|
|
|
|
#[test]
|
|
fn check_f_c_kills_a_dropped_unresolved_cluster() {
|
|
let faces = require_faces();
|
|
let mut rt = build_fixture(&FIXTURES[2], &faces, 2);
|
|
rt.clusters.clusters.retain(|c| c.resolved); // simulate silently dropping it
|
|
let err = check_f_c(&rt).unwrap_err();
|
|
assert!(err.contains("unresolved cluster"), "{err}");
|
|
}
|
|
|
|
#[test]
|
|
fn check_f_d_kills_a_wrong_segment_source_range() {
|
|
let faces = require_faces();
|
|
let mut rt = build_fixture(&FIXTURES[3], &faces, 3);
|
|
rt.segments[1].source = 9..14; // recipe says 8..14
|
|
let err = check_f_d(&rt).unwrap_err();
|
|
assert!(err.contains("recipe says 8..14"), "{err}");
|
|
}
|
|
|
|
#[test]
|
|
fn check_f_e_kills_a_wrong_composed_gid() {
|
|
let faces = require_faces();
|
|
let mut rt = build_fixture(&FIXTURES[4], &faces, 4);
|
|
let c = rt
|
|
.clusters
|
|
.clusters
|
|
.iter()
|
|
.find(|c| c.source.start == 3)
|
|
.unwrap()
|
|
.clone();
|
|
let seg = &mut rt.segments[c.segment];
|
|
seg.glyphs[c.glyph_indices[0] as usize].glyph_id = 1; // not gid 198
|
|
let err = check_f_e(&rt).unwrap_err();
|
|
assert!(err.contains("gid 198"), "{err}");
|
|
}
|
|
|
|
#[test]
|
|
fn fixture_roster_matches_the_recipe_table() {
|
|
let ids: Vec<&str> = FIXTURES.iter().map(|f| f.id).collect();
|
|
assert_eq!(ids, ["F-A", "F-B", "F-C", "F-D", "F-E"]);
|
|
assert_eq!(FIXTURES[0].text, "Allegro affettuoso \u{2014} al fine");
|
|
assert_eq!(FIXTURES[1].text, "Coro \u{05D0}\u{05D1}\u{05D2}");
|
|
assert_eq!(FIXTURES[2].text, "Coro \u{0627}");
|
|
assert_eq!(
|
|
FIXTURES[3].text,
|
|
"Allegro \u{05D0}\u{05D1}\u{05D2} con brio"
|
|
);
|
|
assert_eq!(FIXTURES[4].text, "Cafe\u{301} \u{2014} resume\u{301}");
|
|
}
|
|
|
|
/// Mutation-first: `expect_eq` must actually fail when the numbers
|
|
/// disagree, not just when they happen to agree.
|
|
#[test]
|
|
fn expect_eq_kills_a_disagreement() {
|
|
assert!(expect_eq("x", 5, 6).is_err());
|
|
assert!(expect_eq("x", 5, 5).is_ok());
|
|
}
|
|
}
|