272 lines
15 KiB
Rust
272 lines
15 KiB
Rust
//! Unexecuted evaluator candidates; the module is intentionally not wired yet.
|
|
use super::*;
|
|
use iced::advanced::{graphics::text::Paragraph, text::{Paragraph as _, Text as ParagraphText, Wrapping}};
|
|
|
|
// Independent paragraph construction: no candidate cache, normalization, search,
|
|
// or character-width estimate is used by this measurement oracle.
|
|
fn paragraph_size(content: &str, zoom: f32, style: Style) -> Size {
|
|
let size = Pixels(style.size.0 * zoom);
|
|
let line_height = match style.line_height {
|
|
LineHeight::Relative(factor) => LineHeight::Relative(factor),
|
|
LineHeight::Absolute(pixels) => LineHeight::Absolute(Pixels(pixels.0 * zoom)),
|
|
};
|
|
let paragraph = Paragraph::with_text(ParagraphText { content, bounds: Size::INFINITE,
|
|
size, line_height, font: style.font, align_x: Alignment::Default,
|
|
align_y: iced::alignment::Vertical::Top, shaping: style.shaping, wrapping: Wrapping::default() });
|
|
assert_eq!(paragraph.buffer().layout_runs().count(), 1);
|
|
Size::new(paragraph.min_width() / zoom, paragraph.min_height() / zoom)
|
|
}
|
|
|
|
#[test]
|
|
fn adequate_corridor_reveals_full_realistic_names_and_distinguishing_suffixes() {
|
|
let mut cache = Cache::default();
|
|
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
|
|
let style = Style::default();
|
|
for text in [
|
|
"1. call · Request valve repair · Technician service conversation",
|
|
"2. reply · Return pressure after repair · Technician service conversation",
|
|
"1. call · Request redundant hydraulic pressure calibration channel A",
|
|
"1. call · Request redundant hydraulic pressure calibration channel B",
|
|
] {
|
|
let wide = cache.resolve(&session, text, 1600., 1., style).unwrap();
|
|
assert_eq!(wide.displayed(&session, 1.).unwrap(), text);
|
|
assert_eq!(wide.retained_source(&session, 1.).unwrap(), 0..text.len());
|
|
assert_eq!(wide.bounds.size(), paragraph_size(text, 1., style));
|
|
let narrow = cache.resolve(&session, text, 80., 1., style).unwrap();
|
|
assert_ne!(narrow.displayed, text);
|
|
assert!(narrow.bounds.width <= 80.);
|
|
assert_eq!(narrow.bounds.size(), paragraph_size(&narrow.displayed, 1., style));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn normalized_visible_prefix_maps_to_original_bytes_without_claiming_hidden_operation() {
|
|
let raw = " \t1. call · Pump\r\n · Operation: Need\u{301}le ";
|
|
let expected = "1. call · Pump · Operation: Need\u{301}le";
|
|
let n = Normalized::new(raw);
|
|
assert_eq!(n.text, expected);
|
|
let prefix = "1. call · Pump ";
|
|
assert_eq!(n.source_end(prefix.len()), raw.find("· Operation").unwrap());
|
|
let mut cache = Cache::default();
|
|
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
|
|
let wide = cache.resolve(&session, raw, 1600., 1., Style::default()).unwrap();
|
|
assert_eq!(wide.displayed, expected);
|
|
assert_eq!(wide.retained_source.end, raw.trim_end().len());
|
|
let field = raw.find("Need").unwrap()..raw.trim_end().len();
|
|
assert!(wide.retains_any(&session, 1., field.clone()).unwrap());
|
|
assert!(!wide.retains_any(&session, 1., 0..2).unwrap(), "normalized whitespace is not a visible field");
|
|
let small = cache.resolve(&session, raw, 70., 1., Style::default()).unwrap();
|
|
assert!(!small.retains_any(&session, 1., field).unwrap());
|
|
assert!(small.retained_source.end < raw.find("Operation").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn grapheme_prefixes_and_zoom_match_actual_paragraph_layout() {
|
|
let text = "call · 戻り値 圧力 e\u{301} 👩🔬 safety valve verification";
|
|
let mut cache = Cache::default();
|
|
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
|
|
for zoom in [0.66, 1., 1.75] {
|
|
let label = cache.resolve(&session, text, 105., zoom, Style::default()).unwrap();
|
|
assert!(label.bounds.width <= 105.);
|
|
assert_eq!(label.bounds.size(), paragraph_size(&label.displayed, zoom, Style::default()));
|
|
assert!(text.grapheme_indices(true).any(|(i, _)| i == label.retained_source.end)
|
|
|| label.retained_source.end == text.len());
|
|
let painted = label.canvas_text(&session, zoom, Point::new(400., 90.), Color::BLACK).unwrap();
|
|
assert_eq!(painted.content, label.displayed);
|
|
assert!(painted.max_width.is_infinite());
|
|
assert_eq!(label.bounds_at(&session, zoom, painted.position).unwrap().size(), label.bounds.size());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn empty_and_too_narrow_labels_never_claim_source_visibility() {
|
|
let mut cache = Cache::default();
|
|
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
|
|
for text in ["", " \t\n", "Operation: Pump"] {
|
|
let label = cache.resolve(&session, text, 0., 1., Style::default()).unwrap();
|
|
assert_eq!(label.displayed, "");
|
|
assert_eq!(label.retained_source, 0..0);
|
|
assert_eq!(label.bounds.size(), Size::ZERO);
|
|
assert!(!label.retains_any(&session, 1., 0..text.len()).unwrap());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn redraws_reuse_resolution_but_width_zoom_and_style_are_dependencies() {
|
|
let mut cache = Cache::default();
|
|
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
|
|
let text = "call · Pressure verification with a descriptive operation label";
|
|
let style = Style::default();
|
|
let a = cache.resolve(&session, text, 100., 1., style).unwrap();
|
|
let measured = cache.allocations;
|
|
let b = cache.resolve(&session, text, 100., 1., style).unwrap();
|
|
assert!(Arc::ptr_eq(&a, &b));
|
|
assert_eq!(cache.allocations, measured);
|
|
for (width, zoom, style) in [(120., 1., style), (100., 0.66, style),
|
|
(100., 1., Style { size: Pixels(14.), ..style })] {
|
|
let other = cache.resolve(&session, text, width, zoom, style).unwrap();
|
|
assert!(!Arc::ptr_eq(&a, &other));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn resident_entry_is_remeasured_for_a_fresh_settled_render_session() {
|
|
let mut cache = Cache::default();
|
|
let old_session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
|
|
let text = "call · Pressure verification";
|
|
let a = cache.resolve(&old_session, text, 100., 1., Style::default()).unwrap();
|
|
assert_eq!(cache.entries.len(), 1);
|
|
assert!(Arc::ptr_eq(&cache.entries.front().unwrap().label, &a));
|
|
let measured = cache.allocations;
|
|
assert!(measured > 0);
|
|
assert!(Arc::ptr_eq(&a, &cache.resolve(&old_session, text, 100., 1., Style::default()).unwrap()));
|
|
assert_eq!(cache.allocations, measured);
|
|
// A fresh renderer session invalidates a still-resident entry. This does
|
|
// not mutate global fonts or purport to test dynamic WGPU font loading.
|
|
let new_session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
|
|
let refreshed = cache.resolve(&new_session, text, 100., 1., Style::default()).unwrap();
|
|
assert!(!Arc::ptr_eq(&a, &refreshed));
|
|
assert!(cache.allocations > measured);
|
|
assert_eq!(cache.entries.len(), 1);
|
|
assert!(Arc::ptr_eq(&cache.entries.front().unwrap().label, &refreshed));
|
|
}
|
|
|
|
#[test]
|
|
fn resident_entry_remeasures_after_local_font_epoch_invalidation() {
|
|
let mut cache = Cache::default();
|
|
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
|
|
let text = "call · Resident pressure label";
|
|
let a = cache.resolve(&session, text, 100., 1., Style::default()).unwrap();
|
|
assert_eq!(cache.entries.len(), 1);
|
|
assert!(Arc::ptr_eq(&cache.entries.front().unwrap().label, &a));
|
|
let measured = cache.allocations;
|
|
assert!(measured > 0);
|
|
// Exercise the local memo epoch branch while a is still resident. No
|
|
// intervening width/zoom/style resolution can cause incidental eviction.
|
|
// This is not an actual font-load or renderer-cache acceptance test.
|
|
cache.version = None;
|
|
let refreshed = cache.resolve(&session, text, 100., 1., Style::default()).unwrap();
|
|
assert!(!Arc::ptr_eq(&a, &refreshed));
|
|
assert!(cache.allocations > measured);
|
|
assert_eq!(cache.entries.len(), 1);
|
|
assert!(Arc::ptr_eq(&cache.entries.front().unwrap().label, &refreshed));
|
|
}
|
|
|
|
#[test]
|
|
fn retained_artifact_rejects_other_zoom_and_render_session_for_every_consumer() {
|
|
let mut cache = Cache::default();
|
|
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
|
|
let label = cache.resolve(&session, "Operation: Pressure", 200., 1., Style::default()).unwrap();
|
|
let other = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
|
|
for (context, zoom, expected) in [(&session, 0.66, Error::StaleZoom), (&other, 1., Error::StaleRenderSession)] {
|
|
assert_eq!(label.displayed(context, zoom).unwrap_err(), expected);
|
|
assert_eq!(label.retained_source(context, zoom).unwrap_err(), expected);
|
|
assert_eq!(label.retains_any(context, zoom, 0..9).unwrap_err(), expected);
|
|
assert_eq!(label.bounds_at(context, zoom, Point::ORIGIN).unwrap_err(), expected);
|
|
assert_eq!(label.canvas_text(context, zoom, Point::ORIGIN, Color::BLACK).unwrap_err(), expected);
|
|
}
|
|
assert!(label.canvas_text(&session, 1., Point::ORIGIN, Color::BLACK).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn finite_scaled_metrics_cannot_escape_as_infinite_world_dimensions() {
|
|
let mut cache = Cache::default();
|
|
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
|
|
let style = Style { size: Pixels(2.), line_height: LineHeight::Relative(f32::MAX), ..Style::default() };
|
|
// At zoom 0.5 the scaled size=1 and line height=MAX are finite. The
|
|
// required world line height is not; reject before passing it to shaping.
|
|
let scaled = style.scaled(0.5);
|
|
assert!(scaled.0.is_finite() && scaled.1.is_finite());
|
|
assert_eq!(cache.resolve(&session, "call", 100., 0.5, style).unwrap_err(), Error::InvalidMetrics);
|
|
assert_eq!(cache.allocations, 0);
|
|
assert!(cache.entries.is_empty());
|
|
// Independently exercise validation after division for actual measured
|
|
// dimensions: finite scaled width/height do not imply finite world values.
|
|
assert_eq!(world_size(Size::new(f32::MAX, 1.), 0.5), Err(Error::InvalidMeasurement));
|
|
assert_eq!(world_size(Size::new(1., f32::MAX), 0.5), Err(Error::InvalidMeasurement));
|
|
assert_eq!(world_size(Size::new(20., 8.), 0.5).unwrap(), Size::new(40., 16.));
|
|
}
|
|
|
|
#[test]
|
|
fn malformed_metrics_and_explicit_experiment_budgets_fail_without_a_cached_candidate() {
|
|
let mut cache = Cache::default();
|
|
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
|
|
for (width, zoom) in [(f32::NAN, 1.), (-1., 1.), (100., 0.), (100., f32::INFINITY)] {
|
|
assert_eq!(cache.resolve(&session, "call", width, zoom, Style::default()).unwrap_err(), Error::InvalidMetrics);
|
|
}
|
|
assert_eq!(cache.resolve(&session, &"x".repeat(MAX_SOURCE_BYTES + 1), 100., 1., Style::default()).unwrap_err(), Error::SourceBudget);
|
|
assert_eq!(cache.resolve(&session, &"x".repeat(MAX_GRAPHEMES + 1), 100., 1., Style::default()).unwrap_err(), Error::GraphemeBudget);
|
|
assert!(cache.entries.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn bounds_translation_rejects_nonfinite_world_positions_and_endpoints() {
|
|
let mut cache = Cache::default();
|
|
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
|
|
let label = cache.resolve(&session, "call", 100., 1., Style::default()).unwrap();
|
|
for position in [Point::new(f32::NAN, 0.), Point::new(0., f32::INFINITY)] {
|
|
assert_eq!(label.bounds_at(&session, 1., position).unwrap_err(), Error::InvalidPosition);
|
|
assert_eq!(label.canvas_text(&session, 1., position, Color::BLACK).unwrap_err(), Error::InvalidPosition);
|
|
}
|
|
assert_eq!(translated_bounds(Point::new(f32::MAX, 0.), Size::new(f32::MAX, 1.)), Err(Error::InvalidPosition));
|
|
assert_eq!(translated_bounds(Point::new(0., f32::MAX), Size::new(1., f32::MAX)), Err(Error::InvalidPosition));
|
|
assert_eq!(label.bounds_at(&session, 1., Point::new(-20., -10.)).unwrap().position(), Point::new(-20., -10.));
|
|
}
|
|
|
|
/// Must run as the ONLY selected test in a dedicated, fresh test process.
|
|
/// The ignored marker keeps this global-font mutation out of normal parallel
|
|
/// desktop tests. See the evidence directory's exact executable runner.
|
|
#[test]
|
|
#[ignore = "mutates the global font database; run only this exact test in a fresh process"]
|
|
fn actual_font_load_rejects_retained_label_and_old_session() {
|
|
use std::borrow::Cow;
|
|
use graphics_text::cosmic_text::fontdb;
|
|
|
|
let mut cache = Cache::default();
|
|
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
|
|
let source = "Operation: Pressure";
|
|
let label = cache.resolve(&session, source, 200., 1., Style::default()).unwrap();
|
|
assert_eq!(label.displayed(&session, 1.).unwrap(), source);
|
|
assert!(label.canvas_text(&session, 1., Point::ORIGIN, Color::BLACK).is_ok());
|
|
let measured = cache.allocations;
|
|
assert!(measured > 0);
|
|
assert!(Arc::ptr_eq(&cache.entries.front().unwrap().label, &label));
|
|
|
|
let (version_before, version_after, faces_before, faces_after) = {
|
|
let mut fonts = graphics_text::font_system().write().unwrap();
|
|
let face = fonts.raw().db().faces()
|
|
.find(|face| face.families.iter().any(|(family, _)| family == "Iced-Icons"))
|
|
.expect("Iced 0.14 embeds and loads its known Iced-Icons.ttf face").id;
|
|
let bytes = fonts.raw().db().with_face_data(face, |data, _| data.to_vec())
|
|
.expect("the built-in binary font has accessible bytes");
|
|
assert!(!bytes.is_empty());
|
|
// load_font increments Version even for invalid bytes. Independently
|
|
// establish actual face admission so this cannot pass with invalid data.
|
|
let mut probe = fontdb::Database::new();
|
|
probe.load_font_data(bytes.clone());
|
|
let admitted = probe.faces().filter(|face|
|
|
face.families.iter().any(|(family, _)| family == "Iced-Icons")).count();
|
|
assert!(admitted > 0, "the extracted bytes parse as the expected real font");
|
|
let before = fonts.version();
|
|
assert_eq!(before, session.version);
|
|
let count = fonts.raw().db().faces().count();
|
|
// Owned bytes intentionally bypass Borrowed-address deduplication.
|
|
// This is an actual valid duplicate-face load, not a claim the chosen
|
|
// face or rendered metrics change; the stale-epoch rule is the oracle.
|
|
fonts.load_font(Cow::Owned(bytes));
|
|
(before, fonts.version(), count, fonts.raw().db().faces().count())
|
|
};
|
|
assert_ne!(version_before, version_after);
|
|
assert!(faces_after > faces_before, "actual font admission must accompany the version increment");
|
|
assert_eq!(label.displayed(&session, 1.).unwrap_err(), Error::FontSetChanged);
|
|
assert_eq!(label.retained_source(&session, 1.).unwrap_err(), Error::FontSetChanged);
|
|
assert_eq!(label.retains_any(&session, 1., 0..9).unwrap_err(), Error::FontSetChanged);
|
|
assert_eq!(label.bounds_at(&session, 1., Point::ORIGIN).unwrap_err(), Error::FontSetChanged);
|
|
assert_eq!(label.canvas_text(&session, 1., Point::ORIGIN, Color::BLACK).unwrap_err(), Error::FontSetChanged);
|
|
assert_eq!(cache.resolve(&session, source, 200., 1., Style::default()).unwrap_err(), Error::FontSetChanged);
|
|
assert_eq!(cache.allocations, measured, "an invalid session must not remeasure and imply renderer repair");
|
|
println!("Valid Iced-Icons load: faces {faces_before}->{faces_after}, font Version {version_before:?}->{version_after:?}; old label and session rejected");
|
|
// Do not create a fresh session or claim dynamic WGPU cache recovery here.
|
|
// Process exit is the isolation boundary; no other test may run afterward.
|
|
}
|