Integrate typed Scenario activations before saved geometry resolution

This commit is contained in:
2026-09-12 20:52:51 -07:00
parent e4e87951d7
commit 695685a2b1
6 changed files with 1958 additions and 199 deletions
@@ -8,6 +8,21 @@ use iced::{
use oa_core::{Id, OccurrenceBody, Project, SharedBody};
use std::collections::BTreeMap;
// Test-only geometry input, deliberately upstream of rows, scopes, lifecycle
// and activations. This is not an authored placement or drag policy.
#[cfg(test)]
thread_local! {
static TEST_ROW_OFFSETS: std::cell::RefCell<BTreeMap<Id, f32>> = Default::default();
}
#[cfg(test)]
pub(in crate::behavior) fn apply_test_row_offset(id: Id, y: &mut f32) {
TEST_ROW_OFFSETS.with(|offsets| {
if let Some(offset) = offsets.borrow().get(&id) {
*y += offset;
}
});
}
#[derive(Debug, Clone, PartialEq)]
struct EventMarker {
event: Marker,
@@ -3,6 +3,9 @@ use crate::{behavior::Scene, document::Document};
use iced::Size;
use oa_core::*;
#[path = "union_tests.rs"]
mod union_tests;
struct Fixture {
project: Project,
owner: Id,
@@ -100,6 +103,21 @@ impl Fixture {
},
)
}
fn reply(&mut self, parent: Id, call: Id) -> Id {
self.occurrence(
parent,
"Reply",
OccurrenceBody::Exchange {
exchange: Some(self.exchange),
sender: Some(self.receiver),
receiver: Some(self.sender),
message_kind: MessageKind::Reply,
operation: None,
reply_to: Some(call),
values: vec![],
},
)
}
fn fragment(&mut self, parent: Id, operator: FragmentOperator) -> Id {
self.occurrence(
parent,
@@ -488,9 +506,9 @@ fn real_scene_branch_delete_clips_only_its_activation_and_keeps_unanswered_call_
let (left_start, left_end) = scene.sequence_bounds[&left];
let (right_start, _) = scene.sequence_bounds[&right];
let bars: Vec<_> = scene
.boxes
.activations
.iter()
.filter(|b| b.id == call && b.rect.width == 8.)
.filter(|b| b.call == call && b.receiver == f.receiver)
.map(|b| b.rect)
.collect();
assert!(
@@ -0,0 +1,489 @@
use super::*;
use crate::behavior::scenario_activation::{Finish, Segment};
fn line(scene: &Scene, id: Id) -> &crate::behavior::Line {
scene.lines.iter().find(|line| line.id == id).unwrap()
}
fn segments(scene: &Scene, call: Id) -> Vec<&Segment> {
scene
.activations
.iter()
.filter(|a| a.call == call)
.collect()
}
fn assert_visible_lifetime(scene: &Scene) {
for a in &scene.activations {
assert_eq!(
scene
.lifecycle
.alive_spans(a.receiver, a.scope, a.rect.y, a.rect.y + a.rect.height),
vec![Span {
start: a.rect.y,
end: a.rect.y + a.rect.height
}]
);
if let Finish::Reply(reply) = a.finish {
assert_eq!(a.rect.y + a.rect.height, line(scene, reply).points[0].y);
}
}
}
#[test]
fn admitted_delayed_create_nested_calls_replies_delete_share_one_geometry_pipeline() {
for layer in [Layer::Oa, Layer::Sa] {
let mut f = Fixture::new(layer);
let birth = f.message(f.owner, MessageKind::Create);
let c1 = f.message(f.owner, MessageKind::Call);
let c2 = f.message(f.owner, MessageKind::Call);
let r2 = f.reply(f.owner, c2);
let r1 = f.reply(f.owner, c1);
let death = f.message(f.owner, MessageKind::Delete);
let scene = f.scene();
assert_eq!(scene.activations.len(), 2);
for (call, reply, column) in [(c1, r1, 0), (c2, r2, 1)] {
let a = segments(&scene, call)[0];
assert_eq!(
(a.receiver, a.scope, a.column, a.finish),
(f.receiver, f.owner, column, Finish::Reply(reply))
);
let received = *line(&scene, call).points.last().unwrap();
let sent = line(&scene, reply).points[0];
assert_eq!((a.rect.y, a.rect.y + a.rect.height), (received.y, sent.y));
assert_eq!(received.x, sent.x);
assert!(
a.rect.y > scene.rows[&birth].1 && a.rect.y + a.rect.height < scene.rows[&death].1
);
}
assert_visible_lifetime(&scene);
assert!(scene.open_calls.is_empty());
for event in [birth, death] {
assert_eq!(
scene.hit(Point::new(scene.lanes[&f.receiver], scene.rows[&event].1)),
Some(event)
);
}
}
}
#[test]
fn admitted_death_and_incomplete_later_reply_do_not_fabricate_an_answered_execution() {
for layer in [Layer::Oa, Layer::Sa] {
let mut f = Fixture::new(layer);
let call = f.message(f.owner, MessageKind::Call);
let death = f.message(f.owner, MessageKind::Delete);
let reply = f.reply(f.owner, call);
if let SharedBody::Occurrence {
body: OccurrenceBody::Exchange {
sender, exchange, ..
},
..
} = f
.project
.elements
.get_mut(&reply)
.unwrap()
.shared_mut()
.unwrap()
{
*sender = None;
*exchange = None;
}
let scene = f.scene();
let a = segments(&scene, call)[0];
assert_eq!(a.finish, Finish::Terminated(death));
assert_eq!(a.rect.y + a.rect.height, scene.rows[&death].1);
assert!(
scene.boxes.iter().any(|b| b.id == reply),
"The incomplete reply remains a repairable card"
);
assert_eq!(scene.open_calls.len(), 1);
assert_visible_lifetime(&scene);
}
}
#[test]
fn admitted_self_lifecycle_and_self_call_keep_distinct_send_receive_rows() {
for layer in [Layer::Oa, Layer::Sa] {
let mut f = Fixture::new(layer);
let action = f.occurrence(
f.owner,
"Sender preparation",
OccurrenceBody::Action {
action: None,
lifeline: Some(f.sender),
},
);
let birth = f.message(f.owner, MessageKind::Create);
let call = f.message(f.owner, MessageKind::Call);
let reply = f.reply(f.owner, call);
let death = f.message(f.owner, MessageKind::Delete);
for event in [birth, call, reply, death] {
if let SharedBody::Occurrence {
body:
OccurrenceBody::Exchange {
sender,
receiver,
exchange,
..
},
..
} = f
.project
.elements
.get_mut(&event)
.unwrap()
.shared_mut()
.unwrap()
{
*sender = Some(f.receiver);
*receiver = Some(f.receiver);
*exchange = None;
}
}
let scene = f.scene();
let preparation = scene.boxes.iter().find(|b| b.id == action).unwrap();
assert!(line(&scene, birth).label_at.y > preparation.rect.y + preparation.rect.height);
for event in [birth, death] {
let message = line(&scene, event);
assert_eq!(message.points[0].y, scene.rows[&event].1 - 24.);
assert_eq!(message.points.last().unwrap().y, scene.rows[&event].1);
assert_eq!(
scene.hit(Point::new(scene.lanes[&f.receiver], scene.rows[&event].1)),
Some(event)
);
}
let activation = segments(&scene, call)[0];
assert_eq!(activation.rect.y, scene.rows[&call].1 + 24.);
assert_eq!(
activation.rect.y,
line(&scene, call).points.last().unwrap().y
);
assert_eq!(
activation.rect.y + activation.rect.height,
line(&scene, reply).points[0].y
);
assert_eq!(activation.finish, Finish::Reply(reply));
assert_visible_lifetime(&scene);
}
}
#[test]
fn admitted_unresolved_operand_keeps_unanswered_cue_without_any_visible_segment() {
for layer in [Layer::Oa, Layer::Sa] {
let mut f = Fixture::new(layer);
let fragment = f.fragment(f.owner, FragmentOperator::Alternative);
let left = f.branch(fragment);
f.message(left, MessageKind::Create);
let right = f.branch(fragment);
let call = f.message(right, MessageKind::Call);
let scene = f.scene();
assert!(segments(&scene, call).is_empty());
assert!(
intervals(&scene, f.receiver, right)
.iter()
.all(|i| i.2 == Life::Unresolved)
);
assert_eq!(scene.open_calls.len(), 1);
assert!(scene.open_calls[0].y > scene.rows[&call].1);
assert_eq!(
line(&scene, call).points.last().unwrap().x,
scene.lanes[&f.receiver]
);
}
}
#[test]
fn admitted_calls_without_sender_polylines_still_have_logical_correlation_metadata() {
for layer in [Layer::Oa, Layer::Sa] {
let mut f = Fixture::new(layer);
let c1 = f.message(f.owner, MessageKind::Call);
let c2 = f.message(f.owner, MessageKind::Call);
for call in [c1, c2] {
if let SharedBody::Occurrence {
body:
OccurrenceBody::Exchange {
sender, exchange, ..
},
..
} = f
.project
.elements
.get_mut(&call)
.unwrap()
.shared_mut()
.unwrap()
{
*sender = None;
*exchange = None;
}
}
let outgoing = f.occurrence(
f.owner,
"Outgoing",
OccurrenceBody::Exchange {
exchange: None,
sender: Some(f.receiver),
receiver: Some(f.sender),
message_kind: MessageKind::Asynchronous,
operation: None,
reply_to: None,
values: vec![],
},
);
f.reply(f.owner, c2);
f.reply(f.owner, c1);
let scene = f.scene();
assert!(scene.boxes.iter().any(|b| b.id == c1));
assert!(scene.boxes.iter().any(|b| b.id == c2));
let inner = segments(&scene, c2)[0];
let expected = if scene.lanes[&f.sender] > scene.lanes[&f.receiver] {
inner.rect.x + inner.rect.width
} else {
inner.rect.x
};
assert_eq!(line(&scene, outgoing).points[0].x, expected);
}
}
fn lifetime_rejected(f: &Fixture) {
assert!(validate_candidate(&f.project).is_err());
assert!(
validation::validate(&f.project)
.iter()
.any(|issue| issue.severity == validation::Severity::Error
&& issue.code == "lifeline_lifetime")
);
}
#[test]
fn invalid_prebirth_call_and_postdeath_reply_remain_rejected_with_defensive_scene_clipping() {
for layer in [Layer::Oa, Layer::Sa] {
for later_birth in [false, true] {
let mut f = Fixture::new(layer);
let call = f.message(f.owner, MessageKind::Call);
let death = f.message(f.owner, MessageKind::Delete);
if later_birth {
f.message(f.owner, MessageKind::Create);
}
let reply = f.reply(f.owner, call);
lifetime_rejected(&f);
// Deliberately bypass Document only for defensive renderer coverage
// of rejected data. This is not an admitted editing workflow.
let scene = Scene::sequence(&f.project, f.owner, f.owner);
assert!(
scene
.activations
.iter()
.all(|a| a.rect.y + a.rect.height <= scene.rows[&death].1)
);
assert!(
scene
.activations
.iter()
.all(|a| a.finish != Finish::Reply(reply))
);
assert_eq!(line(&scene, reply).points[0].x, scene.lanes[&f.receiver]);
assert!(!scene.open_calls.is_empty());
assert_visible_lifetime(&scene);
}
let mut f = Fixture::new(layer);
let call = f.message(f.owner, MessageKind::Call);
let birth = f.message(f.owner, MessageKind::Create);
let reply = f.reply(f.owner, call);
lifetime_rejected(&f);
let scene = Scene::sequence(&f.project, f.owner, f.owner);
assert_eq!(segments(&scene, call)[0].rect.y, scene.rows[&birth].1);
assert_eq!(segments(&scene, call)[0].finish, Finish::Reply(reply));
assert_eq!(
line(&scene, call).points.last().unwrap().x,
scene.lanes[&f.receiver]
);
assert_visible_lifetime(&scene);
}
}
#[test]
fn conditional_death_then_recreate_never_revives_call_but_keeps_open_sibling() {
for layer in [Layer::Oa, Layer::Sa] {
let mut f = Fixture::new(layer);
f.message(f.owner, MessageKind::Create);
let call = f.message(f.owner, MessageKind::Call);
let fragment = f.fragment(f.owner, FragmentOperator::Alternative);
let left = f.branch(fragment);
let death = f.message(left, MessageKind::Delete);
let right = f.branch(fragment);
let rebirth = f.message(f.owner, MessageKind::Create);
let reply = f.reply(f.owner, call);
lifetime_rejected(&f);
let scene = Scene::sequence(&f.project, f.owner, f.owner);
let bars = segments(&scene, call);
assert!(
bars.iter()
.any(|a| a.scope == left && a.finish == Finish::Terminated(death))
);
assert!(
bars.iter().any(|a| a.scope == right
&& a.rect.height == 26.
&& a.finish == Finish::Continuation)
);
assert!(
bars.iter()
.all(|a| a.rect.y + a.rect.height < scene.rows[&rebirth].1)
);
assert!(!bars.iter().any(|a| a.finish == Finish::Reply(reply)));
assert_eq!(line(&scene, reply).points[0].x, scene.lanes[&f.receiver]);
assert_eq!(
scene.open_calls.len(),
3,
"Terminated, open sibling and uncertain parent cues survive"
);
assert_visible_lifetime(&scene);
}
}
#[test]
fn clipped_operand_preserves_original_nested_or_crossed_correlation_distinction() {
for layer in [Layer::Oa, Layer::Sa] {
for nested in [false, true] {
let mut f = Fixture::new(layer);
f.message(f.owner, MessageKind::Create);
let c1 = f.message(f.owner, MessageKind::Call);
let c2 = f.message(f.owner, MessageKind::Call);
let fragment = f.fragment(f.owner, FragmentOperator::Alternative);
let left = f.branch(fragment);
f.message(left, MessageKind::Delete);
let right = f.branch(fragment);
let outgoing = f.occurrence(
right,
"Outgoing",
OccurrenceBody::Exchange {
exchange: None,
sender: Some(f.receiver),
receiver: Some(f.sender),
message_kind: MessageKind::Asynchronous,
operation: None,
reply_to: None,
values: vec![],
},
);
let r1 = f.reply(f.owner, if nested { c2 } else { c1 });
let r2 = f.reply(f.owner, if nested { c1 } else { c2 });
lifetime_rejected(&f);
let scene = Scene::sequence(&f.project, f.owner, f.owner);
let send = line(&scene, outgoing).points[0];
let expected = if nested {
let inner = segments(&scene, c2)
.into_iter()
.find(|a| a.scope == right)
.unwrap();
if scene.lanes[&f.sender] > scene.lanes[&f.receiver] {
inner.rect.x + inner.rect.width
} else {
inner.rect.x
}
} else {
scene.lanes[&f.receiver]
};
assert_eq!(send.x, expected);
for call in [c1, c2] {
assert!(segments(&scene, call).iter().any(|a| a.scope == right
&& a.rect.y <= send.y
&& send.y < a.rect.y + a.rect.height));
}
assert!(scene.rows[&r1].1 < scene.rows[&r2].1);
assert_visible_lifetime(&scene);
}
}
}
fn with_offset<T>(id: Id, offset: f32, f: impl FnOnce() -> T) -> T {
struct Restore(BTreeMap<Id, f32>);
impl Drop for Restore {
fn drop(&mut self) {
TEST_ROW_OFFSETS.with(|current| *current.borrow_mut() = std::mem::take(&mut self.0));
}
}
let _restore =
Restore(TEST_ROW_OFFSETS.with(|current| current.replace(BTreeMap::from([(id, offset)]))));
f()
}
fn snapshot(scene: &Scene) -> String {
format!(
"{:?}",
(
&scene.lifecycle,
&scene.activations,
&scene.open_calls,
&scene.rows,
&scene.sequence_bounds,
scene
.lines
.iter()
.map(|l| (l.id, &l.points, &l.label, l.label_at, l.dashed))
.collect::<Vec<_>>(),
scene
.boxes
.iter()
.map(|b| (b.id, b.rect, &b.title, &b.detail, b.marker))
.collect::<Vec<_>>(),
scene
.slots
.iter()
.map(|s| (s.slot, s.y, &s.label))
.collect::<Vec<_>>(),
scene.size,
)
)
}
#[test]
fn test_only_upstream_row_offsets_update_lifetime_segments_and_endpoints_without_authoring() {
for layer in [Layer::Oa, Layer::Sa] {
let mut f = Fixture::new(layer);
let birth = f.message(f.owner, MessageKind::Create);
let c1 = f.message(f.owner, MessageKind::Call);
let c2 = f.message(f.owner, MessageKind::Call);
f.reply(f.owner, c2);
f.reply(f.owner, c1);
let death = f.message(f.owner, MessageKind::Delete);
let document = f.document();
let before = document.snapshot();
let baseline = Scene::sequence(document.project(), f.owner, c1);
let moved = with_offset(birth, 60., || {
Scene::sequence(document.project(), f.owner, c1)
});
assert_eq!(moved.rows[&birth].1, baseline.rows[&birth].1 + 60.);
assert_eq!(moved.rows[&death].1, baseline.rows[&death].1 + 60.);
for (a, b) in baseline.activations.iter().zip(&moved.activations) {
assert_eq!(
(a.call, a.receiver, a.scope, a.column, a.finish),
(b.call, b.receiver, b.scope, b.column, b.finish)
);
assert_eq!(
b.rect,
Rectangle::new(
a.rect.position() + iced::Vector::new(0., 60.),
a.rect.size()
)
);
}
assert_eq!(baseline.activations.len(), moved.activations.len());
assert_visible_lifetime(&moved);
let reparsed = language::parse(&language::format(document.project())).unwrap();
assert_eq!(reparsed, *document.project());
assert_eq!(
snapshot(&moved),
with_offset(birth, 60., || snapshot(&Scene::sequence(
&reparsed, f.owner, c1
)))
);
assert_eq!(
snapshot(&baseline),
snapshot(&Scene::sequence(document.project(), f.owner, c1))
);
assert_eq!(document.project(), &before.decoded.project);
assert_eq!(document.bundle(), &before.bundle);
assert_eq!(document.revision(), 0);
assert!(!document.dirty() && !document.can_undo() && !document.can_redo());
}
}