346 lines
12 KiB
Rust
346 lines
12 KiB
Rust
//! Isolated test-only adapter. See README.md and wiring.patch before running.
|
|
//! It drives the actual parent Scene; it contains no replacement geometry model.
|
|
use super::*;
|
|
use oa_core::{language, validation};
|
|
use std::cell::RefCell;
|
|
|
|
mod lifetime;
|
|
|
|
type Layout = BTreeMap<Id, f32>;
|
|
|
|
thread_local! {
|
|
static ANCHORS: RefCell<Option<Layout>> = const { RefCell::new(None) };
|
|
}
|
|
|
|
struct AnchorScope(Option<Layout>);
|
|
impl AnchorScope {
|
|
fn install(layout: &Layout) -> Self {
|
|
Self(ANCHORS.with(|slot| slot.replace(Some(layout.clone()))))
|
|
}
|
|
}
|
|
impl Drop for AnchorScope {
|
|
fn drop(&mut self) {
|
|
ANCHORS.with(|slot| {
|
|
slot.replace(self.0.take());
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Called only by the proposed cfg(test) row hook, before Scene records rows.
|
|
/// Retains the CURRENT renderer's next-row floor; this is not general free y.
|
|
pub(super) fn resolve_row_anchor(id: Id, y: &mut f32) {
|
|
ANCHORS.with(|slot| {
|
|
if let Some(desired) = slot.borrow().as_ref().and_then(|map| map.get(&id)) {
|
|
assert!(desired.is_finite());
|
|
*y = (*y).max(*desired);
|
|
}
|
|
});
|
|
}
|
|
|
|
fn render(project: &Project, owner: Id, layout: &Layout) -> Scene {
|
|
let _scope = AnchorScope::install(layout);
|
|
Scene::sequence(project, owner, owner)
|
|
}
|
|
|
|
/// Pointer intent differs from explicit SetDesiredY: zero displacement preserves
|
|
/// the complete original authored candidate even when desired != displayed.
|
|
fn drag(original: &Layout, frame: &Scene, target: Id, delta_y: f32) -> Layout {
|
|
assert!(delta_y.is_finite());
|
|
if delta_y == 0. {
|
|
return original.clone();
|
|
}
|
|
let mut next = original.clone();
|
|
next.insert(target, frame.rows[&target].1 + delta_y);
|
|
next
|
|
}
|
|
|
|
fn id(p: &Project, alias: &str) -> Id {
|
|
p.identities()
|
|
.into_iter()
|
|
.find(|(_, name)| *name == alias)
|
|
.unwrap()
|
|
.0
|
|
}
|
|
|
|
fn fixture() -> (Project, Id) {
|
|
let p = language::parse(include_str!("fixture.arcadia")).expect("real Arcadia fixture parses");
|
|
let errors: Vec<_> = validation::validate(&p)
|
|
.into_iter()
|
|
.filter(|issue| issue.severity == validation::Severity::Error)
|
|
.collect();
|
|
assert!(errors.is_empty(), "fixture semantic errors: {errors:?}");
|
|
let owner = id(&p, "conversation");
|
|
(p, owner)
|
|
}
|
|
|
|
fn row(scene: &Scene, id: Id) -> f32 {
|
|
scene.rows[&id].1
|
|
}
|
|
fn line(scene: &Scene, id: Id) -> &Line {
|
|
assert_eq!(scene.lines.iter().filter(|line| line.id == id).count(), 1);
|
|
scene.lines.iter().find(|line| line.id == id).unwrap()
|
|
}
|
|
fn named_box(scene: &Scene, id: Id) -> &BoxItem {
|
|
scene
|
|
.boxes
|
|
.iter()
|
|
.find(|item| item.id == id && !item.title.is_empty())
|
|
.unwrap()
|
|
}
|
|
fn same_geometry(a: &Scene, b: &Scene) {
|
|
assert_eq!(a.rows, b.rows);
|
|
assert_eq!(a.sequence_bounds, b.sequence_bounds);
|
|
assert_eq!(a.size, b.size);
|
|
assert_eq!(a.lanes, b.lanes);
|
|
assert_eq!(a.lifecycle, b.lifecycle);
|
|
assert_eq!(a.open_calls, b.open_calls);
|
|
let lines = |s: &Scene| {
|
|
s.lines
|
|
.iter()
|
|
.map(|l| {
|
|
(
|
|
l.id,
|
|
l.points.clone(),
|
|
l.label_at,
|
|
l.label.clone(),
|
|
l.dashed,
|
|
)
|
|
})
|
|
.collect::<Vec<_>>()
|
|
};
|
|
let boxes = |s: &Scene| {
|
|
s.boxes
|
|
.iter()
|
|
.map(|b| (b.id, b.rect, b.title.clone(), b.detail.clone(), b.marker))
|
|
.collect::<Vec<_>>()
|
|
};
|
|
assert_eq!(lines(a), lines(b));
|
|
assert_eq!(boxes(a), boxes(b));
|
|
let slots = |s: &Scene| {
|
|
s.slots
|
|
.iter()
|
|
.map(|s| (s.slot.parent, s.slot.before, s.y, s.label.clone()))
|
|
.collect::<Vec<_>>()
|
|
};
|
|
assert_eq!(slots(a), slots(b));
|
|
}
|
|
|
|
#[test]
|
|
fn real_parser_roundtrip_and_baseline_scene_have_independent_expected_anchors() {
|
|
let (p, owner) = fixture();
|
|
let before = p.clone();
|
|
let source = language::format(&p);
|
|
let reparsed = language::parse(&source).unwrap();
|
|
assert_eq!(reparsed, p);
|
|
let baseline = Scene::sequence(&p, owner, owner);
|
|
same_geometry(&baseline, &render(&p, owner, &Layout::new()));
|
|
same_geometry(&baseline, &Scene::sequence(&reparsed, owner, owner));
|
|
let first = id(&p, "first");
|
|
let second = id(&p, "second");
|
|
assert_ne!(first, second);
|
|
for occurrence in [first, second] {
|
|
assert!(matches!(p.elements[&occurrence].shared(),
|
|
Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { exchange: Some(edge), .. }, .. })
|
|
if *edge == id(&p, "channel")));
|
|
}
|
|
// Hand-derived current metrics: start148, each message92, fragment55,
|
|
// operand90, action95, fragment trailing64. These are regression oracles,
|
|
// NOT a claim that those fixed spacings should remain product constraints.
|
|
for (alias, expected) in [
|
|
("first", 148.),
|
|
("second", 240.),
|
|
("fragment", 332.),
|
|
("nested", 477.),
|
|
("self_call", 636.),
|
|
("self_reply", 728.),
|
|
] {
|
|
assert_eq!(row(&baseline, id(&p, alias)), expected, "{alias}");
|
|
}
|
|
assert_eq!(baseline.rows[&id(&p, "nested")].0, id(&p, "operand"));
|
|
assert_eq!(named_box(&baseline, id(&p, "nested")).rect.y, 457.);
|
|
let frame = named_box(&baseline, id(&p, "fragment")).rect;
|
|
assert_eq!((frame.y, frame.height), (332., 240.));
|
|
let left = baseline.lanes[&id(&p, "left")];
|
|
let right = baseline.lanes[&id(&p, "right")];
|
|
assert_eq!(
|
|
line(&baseline, first).points,
|
|
vec![Point::new(left, 148.), Point::new(right, 148.)]
|
|
);
|
|
assert_eq!(
|
|
line(&baseline, second).points,
|
|
vec![Point::new(left, 240.), Point::new(right, 240.)]
|
|
);
|
|
let local = line(&baseline, id(&p, "self_call"));
|
|
assert_eq!(
|
|
local.points,
|
|
vec![
|
|
Point::new(right, 636.),
|
|
Point::new(right + 75., 636.),
|
|
Point::new(right + 75., 660.),
|
|
Point::new(right, 660.)
|
|
]
|
|
);
|
|
assert_eq!(p, before);
|
|
assert_eq!(language::format(&p), source);
|
|
}
|
|
|
|
#[test]
|
|
fn moved_repeat_drives_actual_scene_followers_frames_and_self_message_without_semantic_edits() {
|
|
let (p, owner) = fixture();
|
|
let before = p.clone();
|
|
let source = language::format(&p);
|
|
let baseline = Scene::sequence(&p, owner, owner);
|
|
let second = id(&p, "second");
|
|
let candidate = drag(&Layout::new(), &baseline, second, 40.);
|
|
assert_eq!(candidate, Layout::from([(second, 280.)]));
|
|
let moved = render(&p, owner, &candidate);
|
|
assert_eq!(row(&moved, id(&p, "first")), 148.);
|
|
assert_eq!(row(&moved, second), 280.);
|
|
assert_eq!(row(&moved, id(&p, "nested")), 517.);
|
|
assert_eq!(row(&moved, id(&p, "self_call")), 676.);
|
|
assert_eq!(row(&moved, id(&p, "self_reply")), 768.);
|
|
assert_eq!(
|
|
line(&moved, id(&p, "first")).points,
|
|
line(&baseline, id(&p, "first")).points
|
|
);
|
|
for alias in ["second", "self_call", "self_reply"] {
|
|
assert_eq!(
|
|
line(&moved, id(&p, alias)).points.len(),
|
|
line(&baseline, id(&p, alias)).points.len()
|
|
);
|
|
for (old, new) in line(&baseline, id(&p, alias))
|
|
.points
|
|
.iter()
|
|
.zip(&line(&moved, id(&p, alias)).points)
|
|
{
|
|
assert_eq!(new.x, old.x);
|
|
assert_eq!(new.y - old.y, 40.);
|
|
}
|
|
}
|
|
let frame = named_box(&moved, id(&p, "fragment")).rect;
|
|
assert_eq!((frame.y, frame.height), (372., 240.));
|
|
assert_eq!(named_box(&moved, id(&p, "nested")).rect.y, 497.);
|
|
same_geometry(&moved, &render(&p, owner, &candidate));
|
|
same_geometry(
|
|
&moved,
|
|
&render(&language::parse(&source).unwrap(), owner, &candidate),
|
|
);
|
|
assert_eq!(p, before); // Entire Project, not only the steps vector.
|
|
assert_eq!(language::format(&p), source);
|
|
}
|
|
|
|
#[test]
|
|
fn nested_anchor_grows_actual_fragment_and_self_call_keeps_its_receive_offset() {
|
|
let (p, owner) = fixture();
|
|
let before = p.clone();
|
|
let baseline = Scene::sequence(&p, owner, owner);
|
|
let nested = id(&p, "nested");
|
|
let candidate = drag(&Layout::new(), &baseline, nested, 30.);
|
|
let moved = render(&p, owner, &candidate);
|
|
let frame = named_box(&moved, id(&p, "fragment")).rect;
|
|
assert_eq!((frame.y, frame.height), (332., 270.));
|
|
assert_eq!(named_box(&moved, nested).rect.y, 487.);
|
|
assert_eq!(row(&moved, id(&p, "self_call")), 666.);
|
|
let call = id(&p, "self_call");
|
|
let next = drag(&candidate, &moved, call, 18.);
|
|
let local = render(&p, owner, &next);
|
|
assert_eq!(row(&local, call), 684.);
|
|
assert_eq!(line(&local, call).points.first().unwrap().y, 684.);
|
|
assert_eq!(line(&local, call).points.last().unwrap().y, 708.);
|
|
assert_eq!(row(&local, id(&p, "self_reply")), 776.);
|
|
assert_eq!(
|
|
named_box(&local, nested).rect,
|
|
named_box(&moved, nested).rect
|
|
);
|
|
assert_eq!(p, before);
|
|
}
|
|
|
|
#[test]
|
|
fn zero_pointer_delta_preserves_latent_authored_intent_but_explicit_assignment_can_change_it() {
|
|
let (p, owner) = fixture();
|
|
let baseline = Scene::sequence(&p, owner, owner);
|
|
let first = id(&p, "first");
|
|
let second = id(&p, "second");
|
|
let original = Layout::from([(first, 148.), (second, 240.)]);
|
|
let displaced = drag(&original, &baseline, first, 200.);
|
|
let frame = render(&p, owner, &displaced);
|
|
assert_eq!(row(&frame, second), 440.);
|
|
assert_eq!(displaced[&second], 240.);
|
|
let untouched = drag(&displaced, &frame, second, 0.);
|
|
assert_eq!(untouched, displaced);
|
|
let back = drag(&untouched, &frame, first, -200.);
|
|
assert_eq!(back, original);
|
|
assert_eq!(row(&render(&p, owner, &back), second), 240.);
|
|
let mut explicit = displaced.clone();
|
|
explicit.insert(second, 440.); // Explicit SetDesiredY, not a pointer gesture.
|
|
let explicit_back = drag(&explicit, &frame, first, -200.);
|
|
assert_eq!(row(&render(&p, owner, &explicit_back), second), 440.);
|
|
}
|
|
|
|
#[test]
|
|
fn source_reorder_keeps_override_bound_to_occurrence_identity_and_does_not_write_back_projection() {
|
|
let (p, owner) = fixture();
|
|
let input = include_str!("fixture.arcadia");
|
|
let reordered_source =
|
|
input.replace("[@first, @second, @fragment", "[@second, @first, @fragment");
|
|
assert_ne!(input, reordered_source);
|
|
let reordered = language::parse(&reordered_source).unwrap();
|
|
let before = reordered.clone();
|
|
let second = id(&p, "second");
|
|
let first = id(&p, "first");
|
|
assert_eq!(id(&reordered, "second"), second);
|
|
let scene = render(&reordered, owner, &Layout::from([(second, 280.)]));
|
|
assert_eq!(row(&scene, second), 280.);
|
|
assert_eq!(row(&scene, first), 372.);
|
|
assert_eq!(
|
|
&reordered.elements[&owner].steps().unwrap()[..2],
|
|
&[second, first]
|
|
);
|
|
assert_eq!(reordered, before);
|
|
assert_eq!(
|
|
language::parse(&language::format(&reordered)).unwrap(),
|
|
before
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn current_floor_is_an_explicit_counterexample_to_general_free_positioning() {
|
|
let (p, owner) = fixture();
|
|
let baseline = Scene::sequence(&p, owner, owner);
|
|
let second = id(&p, "second");
|
|
let requested = drag(&Layout::new(), &baseline, second, -40.);
|
|
assert_eq!(requested[&second], 200.);
|
|
assert_eq!(row(&render(&p, owner, &requested), second), 240.);
|
|
// This expected mismatch records a limitation; it is not acceptance of
|
|
// silently clamped product gestures or proof that y200 is semantically bad.
|
|
}
|
|
|
|
#[test]
|
|
fn scoped_hook_is_inactive_by_default_thread_local_and_restored_after_panic() {
|
|
let (p, owner) = fixture();
|
|
let first = id(&p, "first");
|
|
let baseline = Scene::sequence(&p, owner, owner);
|
|
let caught = std::panic::catch_unwind(|| {
|
|
let _scope = AnchorScope::install(&Layout::from([(first, 900.)]));
|
|
assert_eq!(row(&Scene::sequence(&p, owner, owner), first), 900.);
|
|
// Another test thread has no access to this thread's temporary anchors.
|
|
std::thread::spawn(|| {
|
|
let (p, owner) = fixture();
|
|
assert_eq!(
|
|
row(&Scene::sequence(&p, owner, owner), id(&p, "first")),
|
|
148.
|
|
);
|
|
})
|
|
.join()
|
|
.unwrap();
|
|
panic!("exercise scoped restoration");
|
|
});
|
|
let payload = caught.expect_err("the deliberate scope-restoration panic must occur");
|
|
assert_eq!(
|
|
payload.downcast_ref::<&str>().copied(),
|
|
Some("exercise scoped restoration"),
|
|
"an earlier failed assertion must not masquerade as the expected panic"
|
|
);
|
|
same_geometry(&baseline, &Scene::sequence(&p, owner, owner));
|
|
}
|