Integrate typed Scenario activations before saved geometry resolution
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user