Integrate typed Scenario activations before saved geometry resolution
This commit is contained in:
+100
-197
@@ -20,6 +20,7 @@ mod contextual_source_tests;
|
|||||||
pub(crate) mod creation;
|
pub(crate) mod creation;
|
||||||
mod form_session;
|
mod form_session;
|
||||||
pub(crate) mod scenario;
|
pub(crate) mod scenario;
|
||||||
|
mod scenario_activation;
|
||||||
mod scenario_lifecycle;
|
mod scenario_lifecycle;
|
||||||
pub(crate) mod source_draft;
|
pub(crate) mod source_draft;
|
||||||
pub(crate) mod state_canvas;
|
pub(crate) mod state_canvas;
|
||||||
@@ -1179,9 +1180,10 @@ struct Scene {
|
|||||||
lanes: BTreeMap<Id, f32>,
|
lanes: BTreeMap<Id, f32>,
|
||||||
slots: Vec<SequenceSlot>,
|
slots: Vec<SequenceSlot>,
|
||||||
rows: BTreeMap<Id, (Id, f32)>,
|
rows: BTreeMap<Id, (Id, f32)>,
|
||||||
|
activations: Vec<scenario_activation::Segment>,
|
||||||
|
lifecycle: scenario_lifecycle::render::Rendering,
|
||||||
open_calls: Vec<Point>,
|
open_calls: Vec<Point>,
|
||||||
sequence_bounds: BTreeMap<Id, (f32, f32)>,
|
sequence_bounds: BTreeMap<Id, (f32, f32)>,
|
||||||
lifecycle: scenario_lifecycle::render::Rendering,
|
|
||||||
}
|
}
|
||||||
struct SequenceSlot {
|
struct SequenceSlot {
|
||||||
slot: scenario::Slot,
|
slot: scenario::Slot,
|
||||||
@@ -1238,9 +1240,10 @@ impl Scene {
|
|||||||
lanes: BTreeMap::new(),
|
lanes: BTreeMap::new(),
|
||||||
slots: vec![],
|
slots: vec![],
|
||||||
rows: BTreeMap::new(),
|
rows: BTreeMap::new(),
|
||||||
|
activations: vec![],
|
||||||
|
lifecycle: scenario_lifecycle::render::Rendering::default(),
|
||||||
open_calls: vec![],
|
open_calls: vec![],
|
||||||
sequence_bounds: BTreeMap::new(),
|
sequence_bounds: BTreeMap::new(),
|
||||||
lifecycle: scenario_lifecycle::render::Rendering::default(),
|
|
||||||
};
|
};
|
||||||
let mut lanes = p
|
let mut lanes = p
|
||||||
.children(owner)
|
.children(owner)
|
||||||
@@ -1338,29 +1341,7 @@ impl Scene {
|
|||||||
scene.size.height,
|
scene.size.height,
|
||||||
);
|
);
|
||||||
scene.lifecycle.align_messages(&mut scene.lines);
|
scene.lifecycle.align_messages(&mut scene.lines);
|
||||||
let calls: Vec<_> = scene
|
scene.layout_activations(p, &lane_x);
|
||||||
.rows
|
|
||||||
.keys()
|
|
||||||
.filter_map(|&id| {
|
|
||||||
if let Some(SharedBody::Occurrence {
|
|
||||||
body:
|
|
||||||
OccurrenceBody::Exchange {
|
|
||||||
message_kind: MessageKind::Call,
|
|
||||||
receiver: Some(receiver),
|
|
||||||
..
|
|
||||||
},
|
|
||||||
..
|
|
||||||
}) = p.elements[&id].shared()
|
|
||||||
{
|
|
||||||
Some((id, *lane_x.get(receiver)?))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
for (call, x) in calls {
|
|
||||||
scene.activation_sequence(p, p.elements[&call].parent.unwrap(), call, x, None);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for (id, name, detail) in lanes {
|
for (id, name, detail) in lanes {
|
||||||
let x = lane_x[&id];
|
let x = lane_x[&id];
|
||||||
@@ -1427,14 +1408,16 @@ impl Scene {
|
|||||||
};
|
};
|
||||||
if scenario
|
if scenario
|
||||||
&& matches!(body, OccurrenceBody::Exchange {
|
&& matches!(body, OccurrenceBody::Exchange {
|
||||||
sender: Some(sender), receiver: Some(receiver),
|
sender: Some(sender), receiver: Some(receiver),
|
||||||
message_kind: MessageKind::Create | MessageKind::Delete, ..
|
message_kind: MessageKind::Create | MessageKind::Delete, ..
|
||||||
} if sender == receiver)
|
} if sender == receiver)
|
||||||
{
|
{
|
||||||
// Self lifecycle elbows and their caption need clearance from
|
// Record the lifecycle receive row only after making room for
|
||||||
// the preceding action/state/reference box, before recording rows.
|
// the self-message elbow and caption above its marker.
|
||||||
*y += 16.;
|
*y += 16.;
|
||||||
}
|
}
|
||||||
|
#[cfg(test)]
|
||||||
|
scenario_lifecycle::render::apply_test_row_offset(*id, y);
|
||||||
if ordered {
|
if ordered {
|
||||||
let slot = scenario::Slot {
|
let slot = scenario::Slot {
|
||||||
parent: owner,
|
parent: owner,
|
||||||
@@ -1654,148 +1637,6 @@ impl Scene {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn activation_segment(
|
|
||||||
&mut self,
|
|
||||||
call: Id,
|
|
||||||
scope: Id,
|
|
||||||
receiver: Id,
|
|
||||||
x: f32,
|
|
||||||
top: f32,
|
|
||||||
bottom: f32,
|
|
||||||
) {
|
|
||||||
for span in self.lifecycle.alive_spans(receiver, scope, top, bottom) {
|
|
||||||
self.boxes.push(BoxItem {
|
|
||||||
id: call,
|
|
||||||
rect: Rectangle::new(
|
|
||||||
Point::new(x - 4., span.start),
|
|
||||||
Size::new(8., span.end - span.start),
|
|
||||||
),
|
|
||||||
title: String::new(),
|
|
||||||
detail: String::new(),
|
|
||||||
marker: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Follow a single correlation through the ordered tree. Branches are drawn
|
|
||||||
/// as independent segments, never as one chronological stretch through ALT
|
|
||||||
/// operands. The result says whether a path can leave this sequence open.
|
|
||||||
fn activation_sequence(
|
|
||||||
&mut self,
|
|
||||||
p: &Project,
|
|
||||||
owner: Id,
|
|
||||||
call: Id,
|
|
||||||
x: f32,
|
|
||||||
mut start: Option<f32>,
|
|
||||||
) -> bool {
|
|
||||||
let Some(SharedBody::Occurrence {
|
|
||||||
body:
|
|
||||||
OccurrenceBody::Exchange {
|
|
||||||
receiver: Some(receiver),
|
|
||||||
..
|
|
||||||
},
|
|
||||||
..
|
|
||||||
}) = p.elements[&call].shared()
|
|
||||||
else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
let receiver = *receiver;
|
|
||||||
let entered_open = start.is_some();
|
|
||||||
let mut split = false;
|
|
||||||
for id in p.elements[&owner].steps().into_iter().flatten() {
|
|
||||||
let Some(&(_, y)) = self.rows.get(id) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if *id == call {
|
|
||||||
start = Some(y);
|
|
||||||
}
|
|
||||||
let Some(top) = start else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
match p.elements[id].shared() {
|
|
||||||
Some(SharedBody::Occurrence {
|
|
||||||
body:
|
|
||||||
OccurrenceBody::Exchange {
|
|
||||||
message_kind: MessageKind::Delete,
|
|
||||||
receiver: Some(deleted),
|
|
||||||
..
|
|
||||||
},
|
|
||||||
..
|
|
||||||
}) if *deleted == receiver => {
|
|
||||||
// This path cannot show activation beyond its local death.
|
|
||||||
// Keep the unresolved-call cue; a Delete is not a Reply.
|
|
||||||
self.activation_segment(call, owner, receiver, x, top, y);
|
|
||||||
self.open_calls.push(Point::new(x + 12., y - 16.));
|
|
||||||
start = None;
|
|
||||||
}
|
|
||||||
Some(SharedBody::Occurrence {
|
|
||||||
body:
|
|
||||||
OccurrenceBody::Exchange {
|
|
||||||
message_kind: MessageKind::Reply,
|
|
||||||
reply_to: Some(correlation),
|
|
||||||
..
|
|
||||||
},
|
|
||||||
..
|
|
||||||
}) if *correlation == call => {
|
|
||||||
self.activation_segment(call, owner, receiver, x, top, y);
|
|
||||||
for span in self.lifecycle.alive_spans(receiver, owner, y - 1., y + 1.) {
|
|
||||||
self.boxes.push(BoxItem {
|
|
||||||
id: call,
|
|
||||||
rect: Rectangle::new(
|
|
||||||
Point::new(x - 7., span.start),
|
|
||||||
Size::new(14., span.end - span.start),
|
|
||||||
),
|
|
||||||
title: String::new(),
|
|
||||||
detail: String::new(),
|
|
||||||
marker: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
start = None;
|
|
||||||
}
|
|
||||||
Some(SharedBody::Occurrence {
|
|
||||||
body: OccurrenceBody::Fragment { operator, branches },
|
|
||||||
..
|
|
||||||
}) => {
|
|
||||||
split = true;
|
|
||||||
self.activation_segment(call, owner, receiver, x, top, y);
|
|
||||||
let bottom = self
|
|
||||||
.boxes
|
|
||||||
.iter()
|
|
||||||
.find(|b| b.id == *id && !b.title.is_empty())
|
|
||||||
.map(|b| b.rect.y + b.rect.height)
|
|
||||||
.unwrap_or(y);
|
|
||||||
let outcomes: Vec<_> = branches
|
|
||||||
.iter()
|
|
||||||
.filter_map(|branch| {
|
|
||||||
let &(top, _) = self.sequence_bounds.get(branch)?;
|
|
||||||
Some(self.activation_sequence(p, *branch, call, x, Some(top)))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let open = match operator {
|
|
||||||
FragmentOperator::Alternative => {
|
|
||||||
outcomes.is_empty() || outcomes.iter().any(|open| *open)
|
|
||||||
}
|
|
||||||
FragmentOperator::Parallel => outcomes.iter().all(|open| *open),
|
|
||||||
// Optional/repeated bodies do not guarantee execution.
|
|
||||||
FragmentOperator::Optional | FragmentOperator::Loop => true,
|
|
||||||
};
|
|
||||||
start = open.then_some(bottom);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(top) = start {
|
|
||||||
let bottom = if entered_open || split {
|
|
||||||
self.sequence_bounds
|
|
||||||
.get(&owner)
|
|
||||||
.map_or(top + 26., |(_, bottom)| (*bottom).max(top + 26.))
|
|
||||||
} else {
|
|
||||||
top + 26.
|
|
||||||
};
|
|
||||||
self.activation_segment(call, owner, receiver, x, top, bottom);
|
|
||||||
self.open_calls.push(Point::new(x + 8., bottom - 7.));
|
|
||||||
}
|
|
||||||
start.is_some()
|
|
||||||
}
|
|
||||||
fn slot_at(&self, point: Point, parent: Option<Id>) -> Option<&SequenceSlot> {
|
fn slot_at(&self, point: Point, parent: Option<Id>) -> Option<&SequenceSlot> {
|
||||||
self.slots
|
self.slots
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1842,17 +1683,40 @@ impl Scene {
|
|||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
self.lines
|
self.lines
|
||||||
.iter()
|
.iter()
|
||||||
.find(|line| !line.label.is_empty() && line.label_bounds().contains(point))
|
.find(|line| {
|
||||||
|
!line.label.is_empty()
|
||||||
|
&& line.points.windows(2).any(|p| {
|
||||||
|
crate::diagram::distance_to_segment(point, p[0], p[1]) < 1.5
|
||||||
|
})
|
||||||
|
})
|
||||||
.map(|line| line.id)
|
.map(|line| line.id)
|
||||||
})
|
})
|
||||||
.or_else(|| self.near_sequence_item(point))
|
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
self.boxes
|
self.activations
|
||||||
.iter()
|
.iter()
|
||||||
.rev()
|
.rev()
|
||||||
.find(|b| b.rect.contains(point) && b.title.is_empty())
|
.find(|b| b.rect.contains(point))
|
||||||
.map(|b| b.id)
|
.map(|b| b.call)
|
||||||
})
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
self.lines
|
||||||
|
.iter()
|
||||||
|
.find(|line| {
|
||||||
|
!line.label.is_empty()
|
||||||
|
&& Rectangle::new(
|
||||||
|
line.label_at,
|
||||||
|
Size::new(
|
||||||
|
(line.label.chars().count() as f32 * 6.8 + 8.).min(225.),
|
||||||
|
25.,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.contains(point)
|
||||||
|
})
|
||||||
|
.map(|line| line.id)
|
||||||
|
})
|
||||||
|
// The generous message tolerance must not steal the exposed
|
||||||
|
// shoulder of another call's execution when the view is zoomed out.
|
||||||
|
.or_else(|| self.near_sequence_item(point))
|
||||||
.or_else(|| self.lifecycle.lane_at(point, self.scale))
|
.or_else(|| self.lifecycle.lane_at(point, self.scale))
|
||||||
.or_else(|| self.lane_at(point))
|
.or_else(|| self.lane_at(point))
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
@@ -1885,9 +1749,8 @@ impl Scene {
|
|||||||
.fold(f32::INFINITY, f32::min);
|
.fold(f32::INFINITY, f32::min);
|
||||||
(line.id, distance)
|
(line.id, distance)
|
||||||
});
|
});
|
||||||
// One distance comparison for both kinds of screen-sized hit tolerance.
|
// Compare broad message and lifecycle-marker tolerance together after
|
||||||
// Dense minimum-Fit rows must not give the first message or last marker
|
// exact shapes, so dense minimum-Fit rows choose the nearer item.
|
||||||
// a larger hit priority than the visibly nearer item. UUID breaks ties.
|
|
||||||
self.lifecycle
|
self.lifecycle
|
||||||
.nearest_marker(point)
|
.nearest_marker(point)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -2158,6 +2021,55 @@ impl canvas::Program<Message> for Scene {
|
|||||||
f.translate(self.camera_pan);
|
f.translate(self.camera_pan);
|
||||||
f.scale(self.scale);
|
f.scale(self.scale);
|
||||||
self.lifecycle.draw_lanes(&mut f, self.selected);
|
self.lifecycle.draw_lanes(&mut f, self.selected);
|
||||||
|
// Guides belong behind execution fills and cards. In particular, a
|
||||||
|
// lifeline must not cut through the middle of its activation rectangle.
|
||||||
|
for guide in self.lines.iter().filter(|line| line.label.is_empty()) {
|
||||||
|
f.stroke(
|
||||||
|
&Path::new(|path| {
|
||||||
|
path.move_to(guide.points[0]);
|
||||||
|
for point in &guide.points[1..] {
|
||||||
|
path.line_to(*point);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Stroke::default()
|
||||||
|
.with_color(if guide.id == self.selected {
|
||||||
|
style::TEAL
|
||||||
|
} else {
|
||||||
|
style::MUTED
|
||||||
|
})
|
||||||
|
.with_width(if guide.id == self.selected { 2.5 } else { 1.2 }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Label backplates clear guides, but must not erase execution borders
|
||||||
|
// or message geometry. Their text remains in the foreground below.
|
||||||
|
for line in self.lines.iter().filter(|line| !line.label.is_empty()) {
|
||||||
|
f.fill_rectangle(
|
||||||
|
line.label_at,
|
||||||
|
Size::new(
|
||||||
|
(line.label.chars().count() as f32 * 6.8 + 8.).min(225.),
|
||||||
|
22.,
|
||||||
|
),
|
||||||
|
Color::WHITE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for activation in &self.activations {
|
||||||
|
let path = Path::rectangle(activation.rect.position(), activation.rect.size());
|
||||||
|
f.fill(&path, Color::from_rgb8(243, 248, 248));
|
||||||
|
f.stroke(
|
||||||
|
&path,
|
||||||
|
Stroke::default()
|
||||||
|
.with_color(if activation.call == self.selected {
|
||||||
|
style::TEAL
|
||||||
|
} else {
|
||||||
|
style::LINE
|
||||||
|
})
|
||||||
|
.with_width(if activation.call == self.selected {
|
||||||
|
2.5
|
||||||
|
} else {
|
||||||
|
1.
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
for b in &self.boxes {
|
for b in &self.boxes {
|
||||||
let outline = matches!(
|
let outline = matches!(
|
||||||
b.marker,
|
b.marker,
|
||||||
@@ -2184,7 +2096,7 @@ impl canvas::Program<Message> for Scene {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for l in &self.lines {
|
for l in self.lines.iter().filter(|line| !line.label.is_empty()) {
|
||||||
let color = if l.id == self.selected {
|
let color = if l.id == self.selected {
|
||||||
style::TEAL
|
style::TEAL
|
||||||
} else {
|
} else {
|
||||||
@@ -2221,11 +2133,6 @@ impl canvas::Program<Message> for Scene {
|
|||||||
}),
|
}),
|
||||||
Stroke::default().with_color(color),
|
Stroke::default().with_color(color),
|
||||||
);
|
);
|
||||||
f.fill_rectangle(
|
|
||||||
l.label_at,
|
|
||||||
Size::new((l.label.chars().count() as f32 * 6.8 + 8.).min(225.), 22.),
|
|
||||||
Color::WHITE,
|
|
||||||
);
|
|
||||||
f.fill_text(Text {
|
f.fill_text(Text {
|
||||||
content: short(&l.label, 32),
|
content: short(&l.label, 32),
|
||||||
position: l.label_at,
|
position: l.label_at,
|
||||||
@@ -2237,8 +2144,8 @@ impl canvas::Program<Message> for Scene {
|
|||||||
}
|
}
|
||||||
for b in &self.boxes {
|
for b in &self.boxes {
|
||||||
if let Some(clip) = box_text_clip(self.camera_pan, b.rect, self.scale, f.size()) {
|
if let Some(clip) = box_text_clip(self.camera_pan, b.rect, self.scale, f.size()) {
|
||||||
// Clipped child frames start with an identity transform. Match
|
// WGPU child clips start with an identity transform, as in
|
||||||
// the card's pan and scale, as for lifecycle Create labels.
|
// the lifecycle capsule label painter.
|
||||||
f.with_clip(clip, |label| {
|
f.with_clip(clip, |label| {
|
||||||
label.translate(self.camera_pan);
|
label.translate(self.camera_pan);
|
||||||
label.scale(self.scale);
|
label.scale(self.scale);
|
||||||
@@ -3025,9 +2932,9 @@ mod direct_canvas_tests {
|
|||||||
p = execute_batch(&p, &commands).unwrap();
|
p = execute_batch(&p, &commands).unwrap();
|
||||||
let scene = Scene::sequence(&p, f.owner, f.call);
|
let scene = Scene::sequence(&p, f.owner, f.call);
|
||||||
let bars: Vec<_> = scene
|
let bars: Vec<_> = scene
|
||||||
.boxes
|
.activations
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|b| b.id == f.call && b.rect.width == 8.)
|
.filter(|b| b.call == f.call)
|
||||||
.map(|b| b.rect)
|
.map(|b| b.rect)
|
||||||
.collect();
|
.collect();
|
||||||
for reply in &replies {
|
for reply in &replies {
|
||||||
@@ -3065,11 +2972,7 @@ mod direct_canvas_tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn direct_execution_span_ends_at_its_correlated_reply_and_marks_an_open_call() {
|
fn direct_execution_span_ends_at_its_correlated_reply_and_marks_an_open_call() {
|
||||||
let (f, scene) = scene();
|
let (f, scene) = scene();
|
||||||
let bar = scene
|
let bar = scene.activations.iter().find(|b| b.call == f.call).unwrap();
|
||||||
.boxes
|
|
||||||
.iter()
|
|
||||||
.find(|b| b.id == f.call && b.rect.width == 8.)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(bar.rect.y + bar.rect.height, scene.rows[&f.reply].1);
|
assert_eq!(bar.rect.y + bar.rect.height, scene.rows[&f.reply].1);
|
||||||
assert!(scene.open_calls.is_empty());
|
assert!(scene.open_calls.is_empty());
|
||||||
let p = execute_batch(&f.project, &[Command::Delete(f.reply)]).unwrap();
|
let p = execute_batch(&f.project, &[Command::Delete(f.reply)]).unwrap();
|
||||||
@@ -3077,9 +2980,9 @@ mod direct_canvas_tests {
|
|||||||
assert_eq!(scene.open_calls.len(), 1);
|
assert_eq!(scene.open_calls.len(), 1);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
scene
|
scene
|
||||||
.boxes
|
.activations
|
||||||
.iter()
|
.iter()
|
||||||
.find(|b| b.id == f.call && b.rect.width == 8.)
|
.find(|b| b.call == f.call)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.rect
|
.rect
|
||||||
.height,
|
.height,
|
||||||
|
|||||||
@@ -0,0 +1,492 @@
|
|||||||
|
//! Derived call geometry. Correlation and branch ownership come from the model;
|
||||||
|
//! columns separate overlapping drawings and do not assert an execution thread.
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const WIDTH: f32 = 12.;
|
||||||
|
const OFFSET: f32 = 6.;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub(super) enum Finish {
|
||||||
|
Reply(Id),
|
||||||
|
Boundary,
|
||||||
|
/// Still open at this scope boundary; the visible span is not an isolated stub.
|
||||||
|
Continuation,
|
||||||
|
Open,
|
||||||
|
/// The receiver was deleted without answering this call.
|
||||||
|
Terminated(Id),
|
||||||
|
/// Lifetime clipping ended this visible piece before its logical terminal.
|
||||||
|
Clipped,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub(super) struct Segment {
|
||||||
|
pub call: Id,
|
||||||
|
pub receiver: Id,
|
||||||
|
pub scope: Id,
|
||||||
|
pub rect: Rectangle,
|
||||||
|
pub column: usize,
|
||||||
|
pub finish: Finish,
|
||||||
|
}
|
||||||
|
impl Segment {
|
||||||
|
fn bottom(&self) -> f32 {
|
||||||
|
self.rect.y + self.rect.height
|
||||||
|
}
|
||||||
|
fn overlaps(&self, other: &Self) -> bool {
|
||||||
|
self.receiver == other.receiver
|
||||||
|
&& self.scope == other.scope
|
||||||
|
&& self.rect.y < other.bottom()
|
||||||
|
&& other.rect.y < self.bottom()
|
||||||
|
}
|
||||||
|
fn edge(&self, right: bool) -> f32 {
|
||||||
|
self.rect.x + if right { self.rect.width } else { 0. }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Scene {
|
||||||
|
pub(super) fn layout_activations(&mut self, p: &Project, lanes: &BTreeMap<Id, f32>) {
|
||||||
|
// Logical endpoints precede clipping and remain available even when a
|
||||||
|
// lifetime hides a whole execution or terminates it before its Reply.
|
||||||
|
let correlations = logical_correlations(p, &self.rows, &self.lines);
|
||||||
|
let mut calls: Vec<_> = self
|
||||||
|
.rows
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(&call, &(scope, y))| {
|
||||||
|
let Some(SharedBody::Occurrence {
|
||||||
|
body:
|
||||||
|
OccurrenceBody::Exchange {
|
||||||
|
message_kind: MessageKind::Call,
|
||||||
|
receiver: Some(receiver),
|
||||||
|
..
|
||||||
|
},
|
||||||
|
..
|
||||||
|
}) = p.elements.get(&call).and_then(|e| e.shared())
|
||||||
|
else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some((call, *receiver, scope, y, *lanes.get(receiver)?))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
// Rows are laid out in semantic order. UUID order must not choose which
|
||||||
|
// rectangle obscures another (especially after source reidentification).
|
||||||
|
calls.sort_by(|a, b| a.3.total_cmp(&b.3).then_with(|| a.0.cmp(&b.0)));
|
||||||
|
for &(call, receiver, scope, _, x) in &calls {
|
||||||
|
let first = self.activations.len();
|
||||||
|
let first_cue = self.open_calls.len();
|
||||||
|
self.activation_sequence(p, scope, call, receiver, x, None);
|
||||||
|
let column = self.activations[first..]
|
||||||
|
.iter()
|
||||||
|
.flat_map(|segment| {
|
||||||
|
self.activations[..first]
|
||||||
|
.iter()
|
||||||
|
.filter(move |other| segment.overlaps(other))
|
||||||
|
})
|
||||||
|
.map(|other| other.column + 1)
|
||||||
|
.max()
|
||||||
|
.unwrap_or(0);
|
||||||
|
for segment in &mut self.activations[first..] {
|
||||||
|
segment.column = column;
|
||||||
|
segment.rect.x += column as f32 * OFFSET;
|
||||||
|
}
|
||||||
|
for cue in &mut self.open_calls[first_cue..] {
|
||||||
|
cue.x += column as f32 * OFFSET;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Shallow executions paint first; columns cannot collide within a scope.
|
||||||
|
self.activations.sort_by(|a, b| {
|
||||||
|
a.column
|
||||||
|
.cmp(&b.column)
|
||||||
|
.then_with(|| a.rect.y.total_cmp(&b.rect.y))
|
||||||
|
.then_with(|| a.call.cmp(&b.call))
|
||||||
|
});
|
||||||
|
self.anchor_activation_messages(p, lanes, &correlations);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn activation_segment(
|
||||||
|
&mut self,
|
||||||
|
call: Id,
|
||||||
|
receiver: Id,
|
||||||
|
scope: Id,
|
||||||
|
x: f32,
|
||||||
|
span: std::ops::Range<f32>,
|
||||||
|
finish: Finish,
|
||||||
|
) {
|
||||||
|
// Cues describe unanswered logical paths, including wholly invisible
|
||||||
|
// ones. Never derive them only from the surviving paint rectangles.
|
||||||
|
if matches!(
|
||||||
|
finish,
|
||||||
|
Finish::Open | Finish::Continuation | Finish::Terminated(_)
|
||||||
|
) {
|
||||||
|
self.open_calls
|
||||||
|
.push(Point::new(x + WIDTH / 2. + 4., span.end - 7.));
|
||||||
|
}
|
||||||
|
for alive in self
|
||||||
|
.lifecycle
|
||||||
|
.alive_spans(receiver, scope, span.start, span.end)
|
||||||
|
{
|
||||||
|
self.activations.push(Segment {
|
||||||
|
call,
|
||||||
|
receiver,
|
||||||
|
scope,
|
||||||
|
rect: Rectangle::new(
|
||||||
|
Point::new(x - WIDTH / 2., alive.start),
|
||||||
|
Size::new(WIDTH, alive.end - alive.start),
|
||||||
|
),
|
||||||
|
column: 0,
|
||||||
|
finish: if alive.end == span.end {
|
||||||
|
finish
|
||||||
|
} else {
|
||||||
|
Finish::Clipped
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Follow one exact correlation. A segment belongs to one ordered scope;
|
||||||
|
/// vertically stacked operands must not become a single chronological span.
|
||||||
|
fn activation_sequence(
|
||||||
|
&mut self,
|
||||||
|
p: &Project,
|
||||||
|
owner: Id,
|
||||||
|
call: Id,
|
||||||
|
receiver: Id,
|
||||||
|
x: f32,
|
||||||
|
mut start: Option<f32>,
|
||||||
|
) -> Outcome {
|
||||||
|
let entered_open = start.is_some();
|
||||||
|
let mut split = false;
|
||||||
|
let mut may_be_terminated = false;
|
||||||
|
for id in p.elements[&owner].steps().into_iter().flatten() {
|
||||||
|
let Some(&(_, y)) = self.rows.get(id) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if *id == call {
|
||||||
|
// A reflective call is received at the lower end of its loop.
|
||||||
|
start = Some(
|
||||||
|
self.lines
|
||||||
|
.iter()
|
||||||
|
.find(|line| line.id == call)
|
||||||
|
.and_then(|line| line.points.last())
|
||||||
|
.map_or(y, |point| point.y),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let Some(top) = start else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
match p.elements[id].shared() {
|
||||||
|
Some(SharedBody::Occurrence {
|
||||||
|
body:
|
||||||
|
OccurrenceBody::Exchange {
|
||||||
|
message_kind: MessageKind::Delete,
|
||||||
|
receiver: Some(deleted),
|
||||||
|
..
|
||||||
|
},
|
||||||
|
..
|
||||||
|
}) if *deleted == receiver => {
|
||||||
|
self.activation_segment(
|
||||||
|
call,
|
||||||
|
receiver,
|
||||||
|
owner,
|
||||||
|
x,
|
||||||
|
top..y,
|
||||||
|
Finish::Terminated(*id),
|
||||||
|
);
|
||||||
|
start = None;
|
||||||
|
may_be_terminated = true;
|
||||||
|
}
|
||||||
|
Some(SharedBody::Occurrence {
|
||||||
|
body:
|
||||||
|
OccurrenceBody::Exchange {
|
||||||
|
message_kind: MessageKind::Reply,
|
||||||
|
reply_to: Some(correlation),
|
||||||
|
..
|
||||||
|
},
|
||||||
|
..
|
||||||
|
}) if *correlation == call => {
|
||||||
|
// Execution ends when its reply is sent, including a self reply.
|
||||||
|
self.activation_segment(call, receiver, owner, x, top..y, Finish::Reply(*id));
|
||||||
|
start = None;
|
||||||
|
}
|
||||||
|
Some(SharedBody::Occurrence {
|
||||||
|
body: OccurrenceBody::Fragment { operator, branches },
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
split = true;
|
||||||
|
self.activation_segment(call, receiver, owner, x, top..y, Finish::Boundary);
|
||||||
|
let bottom = self
|
||||||
|
.boxes
|
||||||
|
.iter()
|
||||||
|
.find(|b| b.id == *id && !b.title.is_empty())
|
||||||
|
.map(|b| b.rect.y + b.rect.height)
|
||||||
|
.unwrap_or(y);
|
||||||
|
let outcomes: Vec<_> = branches
|
||||||
|
.iter()
|
||||||
|
.filter_map(|branch| {
|
||||||
|
let &(top, _) = self.sequence_bounds.get(branch)?;
|
||||||
|
Some(self.activation_sequence(p, *branch, call, receiver, x, Some(top)))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let open = match operator {
|
||||||
|
FragmentOperator::Alternative => {
|
||||||
|
outcomes.is_empty() || outcomes.iter().any(|outcome| outcome.open)
|
||||||
|
}
|
||||||
|
FragmentOperator::Parallel => outcomes.iter().all(|outcome| outcome.open),
|
||||||
|
FragmentOperator::Optional | FragmentOperator::Loop => true,
|
||||||
|
};
|
||||||
|
if outcomes.iter().any(|outcome| outcome.may_be_terminated) {
|
||||||
|
// Sibling bars retain their own entry paths, but a
|
||||||
|
// conditional death cannot become a definite parent
|
||||||
|
// execution again after a later Create.
|
||||||
|
self.open_calls
|
||||||
|
.push(Point::new(x + WIDTH / 2. + 4., bottom - 7.));
|
||||||
|
may_be_terminated = true;
|
||||||
|
start = None;
|
||||||
|
} else {
|
||||||
|
start = open.then_some(bottom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(top) = start {
|
||||||
|
let bottom = if entered_open || split {
|
||||||
|
self.sequence_bounds
|
||||||
|
.get(&owner)
|
||||||
|
.map_or(top + 26., |(_, bottom)| (*bottom).max(top + 26.))
|
||||||
|
} else {
|
||||||
|
top + 26.
|
||||||
|
};
|
||||||
|
let finish = if entered_open || split {
|
||||||
|
Finish::Continuation
|
||||||
|
} else {
|
||||||
|
Finish::Open
|
||||||
|
};
|
||||||
|
self.activation_segment(call, receiver, owner, x, top..bottom, finish);
|
||||||
|
}
|
||||||
|
Outcome {
|
||||||
|
open: start.is_some(),
|
||||||
|
may_be_terminated,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn anchor_activation_messages(
|
||||||
|
&mut self,
|
||||||
|
p: &Project,
|
||||||
|
lanes: &BTreeMap<Id, f32>,
|
||||||
|
correlations: &BTreeMap<Id, Correlation>,
|
||||||
|
) {
|
||||||
|
let segments = &self.activations;
|
||||||
|
for line in &mut self.lines {
|
||||||
|
let Some(SharedBody::Occurrence {
|
||||||
|
body:
|
||||||
|
OccurrenceBody::Exchange {
|
||||||
|
sender: Some(sender),
|
||||||
|
receiver: Some(receiver),
|
||||||
|
message_kind,
|
||||||
|
reply_to,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
..
|
||||||
|
}) = p.elements.get(&line.id).and_then(|e| e.shared())
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !matches!(
|
||||||
|
message_kind,
|
||||||
|
MessageKind::Call | MessageKind::Reply | MessageKind::Asynchronous
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (Some(&from), Some(&to), Some(&(scope, _))) = (
|
||||||
|
lanes.get(sender),
|
||||||
|
lanes.get(receiver),
|
||||||
|
self.rows.get(&line.id),
|
||||||
|
) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let send_y = line.points[0].y;
|
||||||
|
let receive_y = line.points.last().unwrap().y;
|
||||||
|
let reflective = sender == receiver;
|
||||||
|
let source = if *message_kind == MessageKind::Reply {
|
||||||
|
reply_to.and_then(|call| {
|
||||||
|
segments.iter().find(|a| {
|
||||||
|
a.call == call
|
||||||
|
&& a.scope == scope
|
||||||
|
&& a.finish == Finish::Reply(line.id)
|
||||||
|
&& a.bottom() == send_y
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
active_segment(
|
||||||
|
p,
|
||||||
|
segments,
|
||||||
|
correlations,
|
||||||
|
*sender,
|
||||||
|
scope,
|
||||||
|
send_y,
|
||||||
|
Some(line.id),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let target = if *message_kind == MessageKind::Call {
|
||||||
|
segments
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.call == line.id && a.scope == scope && a.rect.y == receive_y)
|
||||||
|
} else {
|
||||||
|
active_segment(
|
||||||
|
p,
|
||||||
|
segments,
|
||||||
|
correlations,
|
||||||
|
*receiver,
|
||||||
|
scope,
|
||||||
|
receive_y,
|
||||||
|
*reply_to,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let start = source.map_or(from, |a| a.edge(reflective || to > from));
|
||||||
|
let end = target.map_or(to, |a| a.edge(reflective || from > to));
|
||||||
|
if reflective {
|
||||||
|
let outside = start.max(end) + 60.;
|
||||||
|
line.points = vec![
|
||||||
|
Point::new(start, send_y),
|
||||||
|
Point::new(outside, send_y),
|
||||||
|
Point::new(outside, receive_y),
|
||||||
|
Point::new(end, receive_y),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
line.points = vec![Point::new(start, send_y), Point::new(end, receive_y)];
|
||||||
|
}
|
||||||
|
line.label_at.x = start.min(end) + 12.;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Outcome {
|
||||||
|
open: bool,
|
||||||
|
may_be_terminated: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn logical_correlations(
|
||||||
|
p: &Project,
|
||||||
|
rows: &BTreeMap<Id, (Id, f32)>,
|
||||||
|
lines: &[Line],
|
||||||
|
) -> BTreeMap<Id, Correlation> {
|
||||||
|
let mut result = BTreeMap::new();
|
||||||
|
let points: BTreeMap<_, _> = lines.iter().map(|line| (line.id, &line.points)).collect();
|
||||||
|
for (&id, &(_, y)) in rows {
|
||||||
|
if matches!(
|
||||||
|
p.elements.get(&id).and_then(|e| e.shared()),
|
||||||
|
Some(SharedBody::Occurrence {
|
||||||
|
body: OccurrenceBody::Exchange {
|
||||||
|
message_kind: MessageKind::Call,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
..
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
result.insert(
|
||||||
|
id,
|
||||||
|
Correlation {
|
||||||
|
start: points.get(&id).and_then(|p| p.last()).map_or(y, |p| p.y),
|
||||||
|
replies: vec![],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (&id, &(scope, y)) in rows {
|
||||||
|
if let Some(SharedBody::Occurrence {
|
||||||
|
body:
|
||||||
|
OccurrenceBody::Exchange {
|
||||||
|
message_kind: MessageKind::Reply,
|
||||||
|
reply_to: Some(call),
|
||||||
|
..
|
||||||
|
},
|
||||||
|
..
|
||||||
|
}) = p.elements.get(&id).and_then(|e| e.shared())
|
||||||
|
&& let Some(correlation) = result.get_mut(call)
|
||||||
|
{
|
||||||
|
let sent = points.get(&id).and_then(|p| p.first()).map_or(y, |p| p.y);
|
||||||
|
correlation.replies.push((scope, sent));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A caller has no implicit execution just because it sent a message. Only an
|
||||||
|
/// actually drawn, known-duration execution can supply its anchor. Crossed
|
||||||
|
/// intervals provide no unambiguous innermost execution, so fall back to the lane.
|
||||||
|
fn active_segment<'a>(
|
||||||
|
p: &Project,
|
||||||
|
segments: &'a [Segment],
|
||||||
|
correlations: &BTreeMap<Id, Correlation>,
|
||||||
|
receiver: Id,
|
||||||
|
scope: Id,
|
||||||
|
y: f32,
|
||||||
|
exclude: Option<Id>,
|
||||||
|
) -> Option<&'a Segment> {
|
||||||
|
let candidates: Vec<_> = segments
|
||||||
|
.iter()
|
||||||
|
.filter(|a| {
|
||||||
|
a.receiver == receiver
|
||||||
|
&& a.scope == scope
|
||||||
|
&& Some(a.call) != exclude
|
||||||
|
&& a.rect.y <= y
|
||||||
|
&& y < a.bottom()
|
||||||
|
&& a.finish != Finish::Open
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let inner = candidates.iter().max_by_key(|a| a.column).copied()?;
|
||||||
|
candidates
|
||||||
|
.iter()
|
||||||
|
.all(|a| {
|
||||||
|
a.call == inner.call
|
||||||
|
|| correlations[&a.call].contains(p, scope, &correlations[&inner.call])
|
||||||
|
})
|
||||||
|
.then_some(inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Correlation {
|
||||||
|
start: f32,
|
||||||
|
replies: Vec<(Id, f32)>,
|
||||||
|
}
|
||||||
|
impl Correlation {
|
||||||
|
fn contains(&self, p: &Project, scope: Id, inner: &Self) -> bool {
|
||||||
|
// Segment clipping at a fragment can make crossed calls appear nested.
|
||||||
|
// Compare actual correlated replies on compatible scope paths instead.
|
||||||
|
let relevant = |&(reply_scope, _): &(Id, f32)| {
|
||||||
|
ancestor(p, scope, reply_scope) || ancestor(p, reply_scope, scope)
|
||||||
|
};
|
||||||
|
let mut compared = false;
|
||||||
|
for &(inner_scope, inner_end) in inner.replies.iter().filter(|reply| relevant(reply)) {
|
||||||
|
let mut matched = false;
|
||||||
|
for &(outer_scope, outer_end) in self.replies.iter().filter(|reply| relevant(reply)) {
|
||||||
|
if ancestor(p, inner_scope, outer_scope) || ancestor(p, outer_scope, inner_scope) {
|
||||||
|
// Alternative operands are not chronological. Compare only
|
||||||
|
// returns that can belong to the same enclosing branch path.
|
||||||
|
if outer_end < inner_end {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
matched = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !matched {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
compared = true;
|
||||||
|
}
|
||||||
|
self.start <= inner.start && compared
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn ancestor(p: &Project, expected: Id, mut scope: Id) -> bool {
|
||||||
|
for _ in 0..=p.elements.len() {
|
||||||
|
if scope == expected {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let Some(parent) = p.elements.get(&scope).and_then(|e| e.parent) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
scope = parent;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
@@ -0,0 +1,842 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn id(n: u128) -> Id {
|
||||||
|
Id::from_u128(0x21ca1100000040008000000000000000 + n)
|
||||||
|
}
|
||||||
|
struct Fixture {
|
||||||
|
project: Project,
|
||||||
|
owner: Id,
|
||||||
|
caller: Id,
|
||||||
|
callee: Id,
|
||||||
|
call: Id,
|
||||||
|
inner: Id,
|
||||||
|
reply: Id,
|
||||||
|
inner_reply: Id,
|
||||||
|
}
|
||||||
|
fn seed(p: &mut Project, layer: Layer, n: u128, kind: Kind, parent: Option<Id>) -> Id {
|
||||||
|
let mut e = p
|
||||||
|
.make_element(kind, layer, &format!("Element {n}"), parent)
|
||||||
|
.unwrap();
|
||||||
|
e.id = id(n);
|
||||||
|
e.alias = format!("element_{n}");
|
||||||
|
p.elements.insert(e.id, e);
|
||||||
|
id(n)
|
||||||
|
}
|
||||||
|
fn message(
|
||||||
|
p: &mut Project,
|
||||||
|
layer: Layer,
|
||||||
|
n: u128,
|
||||||
|
parent: Id,
|
||||||
|
endpoints: (Id, Id),
|
||||||
|
reply: Option<Id>,
|
||||||
|
) -> Id {
|
||||||
|
let e = seed(p, layer, n, Kind::Occurrence, Some(parent));
|
||||||
|
*p.elements.get_mut(&e).unwrap().shared_mut().unwrap() = SharedBody::Occurrence {
|
||||||
|
body: OccurrenceBody::Exchange {
|
||||||
|
operation: None,
|
||||||
|
exchange: None,
|
||||||
|
sender: Some(endpoints.0),
|
||||||
|
receiver: Some(endpoints.1),
|
||||||
|
message_kind: if reply.is_some() {
|
||||||
|
MessageKind::Reply
|
||||||
|
} else {
|
||||||
|
MessageKind::Call
|
||||||
|
},
|
||||||
|
reply_to: reply,
|
||||||
|
values: vec![],
|
||||||
|
},
|
||||||
|
precondition: String::new(),
|
||||||
|
postcondition: String::new(),
|
||||||
|
};
|
||||||
|
e
|
||||||
|
}
|
||||||
|
fn fixture(layer: Layer, reverse_uuid_order: bool) -> Fixture {
|
||||||
|
let mut p = Project::empty();
|
||||||
|
let work = if layer == Layer::Oa {
|
||||||
|
Kind::Activity
|
||||||
|
} else {
|
||||||
|
Kind::Function
|
||||||
|
};
|
||||||
|
let a = seed(&mut p, layer, 1, work, None);
|
||||||
|
let b = seed(&mut p, layer, 2, work, None);
|
||||||
|
let owner = seed(&mut p, layer, 3, Kind::Scenario, None);
|
||||||
|
if let SharedBody::Scenario { viewpoint, .. } =
|
||||||
|
p.elements.get_mut(&owner).unwrap().shared_mut().unwrap()
|
||||||
|
{
|
||||||
|
*viewpoint = if layer == Layer::Oa {
|
||||||
|
ScenarioViewpoint::OperationalActivity
|
||||||
|
} else {
|
||||||
|
ScenarioViewpoint::Functional
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let caller = seed(&mut p, layer, 4, Kind::Lifeline, Some(owner));
|
||||||
|
let callee = seed(&mut p, layer, 5, Kind::Lifeline, Some(owner));
|
||||||
|
for (line, represented) in [(caller, a), (callee, b)] {
|
||||||
|
*p.elements.get_mut(&line).unwrap().shared_mut().unwrap() = SharedBody::Lifeline {
|
||||||
|
represented: Some(represented),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let (first, second) = if reverse_uuid_order {
|
||||||
|
(40, 30)
|
||||||
|
} else {
|
||||||
|
(30, 40)
|
||||||
|
};
|
||||||
|
let call = message(&mut p, layer, first, owner, (caller, callee), None);
|
||||||
|
let inner = message(&mut p, layer, second, owner, (caller, callee), None);
|
||||||
|
let reply = message(&mut p, layer, 50, owner, (callee, caller), Some(call));
|
||||||
|
let inner_reply = message(&mut p, layer, 60, owner, (callee, caller), Some(inner));
|
||||||
|
*p.elements.get_mut(&owner).unwrap().steps_mut().unwrap() =
|
||||||
|
vec![call, inner, inner_reply, reply];
|
||||||
|
Fixture {
|
||||||
|
project: p,
|
||||||
|
owner,
|
||||||
|
caller,
|
||||||
|
callee,
|
||||||
|
call,
|
||||||
|
inner,
|
||||||
|
reply,
|
||||||
|
inner_reply,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn scene(f: &Fixture) -> Scene {
|
||||||
|
let errors: Vec<_> = validation::validate(&f.project)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|i| i.severity == validation::Severity::Error)
|
||||||
|
.collect();
|
||||||
|
assert!(errors.is_empty(), "Actual core fixture: {errors:#?}");
|
||||||
|
let source = language::format(&f.project);
|
||||||
|
let reparsed = language::parse(&source).unwrap();
|
||||||
|
assert_eq!(language::format(&reparsed), source);
|
||||||
|
let result = Scene::sequence(&reparsed, f.owner, f.owner);
|
||||||
|
assert_eq!(
|
||||||
|
language::format(&reparsed),
|
||||||
|
source,
|
||||||
|
"Projection must not write geometry into the model"
|
||||||
|
);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
fn bar(scene: &Scene, call: Id) -> &Segment {
|
||||||
|
scene.activations.iter().find(|a| a.call == call).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn long_labels(f: &mut Fixture) {
|
||||||
|
for occurrence in [f.call, f.inner, f.inner_reply, f.reply] {
|
||||||
|
f.project.elements.get_mut(&occurrence).unwrap().name =
|
||||||
|
"Pressure verification with a descriptive operation label".into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn activation_shoulders_under_label_backplates_select_their_call() {
|
||||||
|
for layer in [Layer::Oa, Layer::Sa] {
|
||||||
|
let mut f = fixture(layer, true);
|
||||||
|
long_labels(&mut f);
|
||||||
|
let scene = scene(&f);
|
||||||
|
let outer = bar(&scene, f.call);
|
||||||
|
for occurrence in [f.inner, f.inner_reply, f.reply] {
|
||||||
|
let message = line(&scene, occurrence);
|
||||||
|
let shoulder = Point::new(outer.rect.x + 1., message.label_at.y + 11.);
|
||||||
|
assert!(outer.rect.contains(shoulder));
|
||||||
|
assert!(Rectangle::new(message.label_at, Size::new(225., 22.)).contains(shoulder));
|
||||||
|
assert!(message.label.chars().count() > 32);
|
||||||
|
assert!(message.points.windows(2).all(|p| {
|
||||||
|
crate::diagram::distance_to_segment(shoulder, p[0], p[1]) > 8. / scene.scale
|
||||||
|
}));
|
||||||
|
assert_eq!(scene.hit(shoulder), Some(f.call));
|
||||||
|
assert_eq!(
|
||||||
|
scene.hit(message.label_at + iced::Vector::new(4., 4.)),
|
||||||
|
Some(occurrence),
|
||||||
|
"Label text outside activations remains selectable"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for occurrence in [f.call, f.inner, f.inner_reply, f.reply] {
|
||||||
|
let message = line(&scene, occurrence);
|
||||||
|
for endpoint in [message.points[0], *message.points.last().unwrap()] {
|
||||||
|
assert_eq!(scene.hit(endpoint), Some(occurrence));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn long_message_backplates_do_not_erase_selected_activation_border_pixels() {
|
||||||
|
use iced::advanced::renderer::Headless;
|
||||||
|
use iced_runtime::user_interface::{Cache, UserInterface};
|
||||||
|
|
||||||
|
let mut mounted = crate::behavior::canvas_workspace::tests::Mounted::unshown().await;
|
||||||
|
let mut f = fixture(Layer::Oa, true);
|
||||||
|
long_labels(&mut f);
|
||||||
|
let mut scene = scene(&f);
|
||||||
|
scene.selected = f.call;
|
||||||
|
let border_x = bar(&scene, f.call).rect.x;
|
||||||
|
let label_spans: Vec<_> = [f.inner, f.inner_reply, f.reply]
|
||||||
|
.into_iter()
|
||||||
|
.map(|id| {
|
||||||
|
let message = line(&scene, id);
|
||||||
|
assert!(
|
||||||
|
Rectangle::new(message.label_at, Size::new(225., 22.))
|
||||||
|
.contains(Point::new(border_x, message.label_at.y + 11.))
|
||||||
|
);
|
||||||
|
(message.label_at.y + 3.) as usize..(message.label_at.y + 20.) as usize
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let dimensions = Size::new(800., 700.);
|
||||||
|
let content: Element<'_, Message> = canvas::Canvas::new(scene)
|
||||||
|
.width(dimensions.width)
|
||||||
|
.height(dimensions.height)
|
||||||
|
.into();
|
||||||
|
let mut ui = UserInterface::build(content, dimensions, Cache::new(), &mut mounted.renderer);
|
||||||
|
ui.draw(
|
||||||
|
&mut mounted.renderer,
|
||||||
|
&style::theme(),
|
||||||
|
&iced::advanced::renderer::Style {
|
||||||
|
text_color: style::INK,
|
||||||
|
},
|
||||||
|
mouse::Cursor::Unavailable,
|
||||||
|
);
|
||||||
|
let pixels = Headless::screenshot(&mut mounted.renderer, Size::new(800, 700), 1., Color::WHITE);
|
||||||
|
let expected = [style::TEAL.r, style::TEAL.g, style::TEAL.b].map(|v| v * 255.);
|
||||||
|
for span in label_spans {
|
||||||
|
for y in span {
|
||||||
|
assert!(
|
||||||
|
((border_x as usize - 2)..=(border_x as usize + 2)).any(|x| {
|
||||||
|
let offset = (y * 800 + x) * 4;
|
||||||
|
pixels[offset..offset + 3]
|
||||||
|
.iter()
|
||||||
|
.zip(expected)
|
||||||
|
.all(|(actual, target)| (*actual as f32 - target).abs() < 30.)
|
||||||
|
}),
|
||||||
|
"Selected activation border was erased at y={y}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn line(scene: &Scene, occurrence: Id) -> &super::super::Line {
|
||||||
|
scene.lines.iter().find(|a| a.id == occurrence).unwrap()
|
||||||
|
}
|
||||||
|
fn endpoints(p: &mut Project, occurrence: Id, from: Id, to: Id) {
|
||||||
|
if let SharedBody::Occurrence {
|
||||||
|
body: OccurrenceBody::Exchange {
|
||||||
|
sender, receiver, ..
|
||||||
|
},
|
||||||
|
..
|
||||||
|
} = p
|
||||||
|
.elements
|
||||||
|
.get_mut(&occurrence)
|
||||||
|
.unwrap()
|
||||||
|
.shared_mut()
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
*sender = Some(from);
|
||||||
|
*receiver = Some(to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nested_calls_expose_both_bars_and_end_at_their_exact_replies_in_both_directions() {
|
||||||
|
for layer in [Layer::Oa, Layer::Sa] {
|
||||||
|
for reverse in [false, true] {
|
||||||
|
let mut f = fixture(layer, true);
|
||||||
|
if reverse {
|
||||||
|
for occurrence in [f.call, f.inner] {
|
||||||
|
endpoints(&mut f.project, occurrence, f.callee, f.caller);
|
||||||
|
}
|
||||||
|
for occurrence in [f.reply, f.inner_reply] {
|
||||||
|
endpoints(&mut f.project, occurrence, f.caller, f.callee);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let scene = scene(&f);
|
||||||
|
let outer = bar(&scene, f.call);
|
||||||
|
let inner = bar(&scene, f.inner);
|
||||||
|
assert!(outer.rect.y < inner.rect.y && outer.bottom() > inner.bottom());
|
||||||
|
assert_eq!(outer.column, 0);
|
||||||
|
assert_eq!(inner.column, 1);
|
||||||
|
assert!(outer.rect.x < inner.rect.x && outer.edge(true) < inner.edge(true));
|
||||||
|
for (call, reply) in [(f.call, f.reply), (f.inner, f.inner_reply)] {
|
||||||
|
let execution = bar(&scene, call);
|
||||||
|
let received = *line(&scene, call).points.last().unwrap();
|
||||||
|
let returned = line(&scene, reply).points[0];
|
||||||
|
assert_eq!(execution.rect.y, received.y);
|
||||||
|
assert_eq!(execution.bottom(), returned.y);
|
||||||
|
assert_eq!(received.x, execution.edge(reverse));
|
||||||
|
assert_eq!(returned.x, execution.edge(reverse));
|
||||||
|
assert_eq!(execution.finish, Finish::Reply(reply));
|
||||||
|
}
|
||||||
|
assert!(scene.open_calls.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn call_uuid_order_does_not_choose_activation_columns_or_message_anchors() {
|
||||||
|
let f = fixture(Layer::Oa, false);
|
||||||
|
let g = fixture(Layer::Oa, true);
|
||||||
|
let a = scene(&f);
|
||||||
|
let b = scene(&g);
|
||||||
|
for (x, y) in [
|
||||||
|
(f.call, g.call),
|
||||||
|
(f.inner, g.inner),
|
||||||
|
(f.reply, g.reply),
|
||||||
|
(f.inner_reply, g.inner_reply),
|
||||||
|
] {
|
||||||
|
assert_eq!(line(&a, x).points, line(&b, y).points);
|
||||||
|
}
|
||||||
|
for (x, y) in [(f.call, g.call), (f.inner, g.inner)] {
|
||||||
|
assert_eq!(bar(&a, x).rect, bar(&b, y).rect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sequential_calls_reuse_column_and_crossed_replies_keep_their_correlations() {
|
||||||
|
for layer in [Layer::Oa, Layer::Sa] {
|
||||||
|
let mut f = fixture(layer, true);
|
||||||
|
*f.project
|
||||||
|
.elements
|
||||||
|
.get_mut(&f.owner)
|
||||||
|
.unwrap()
|
||||||
|
.steps_mut()
|
||||||
|
.unwrap() = vec![f.call, f.reply, f.inner, f.inner_reply];
|
||||||
|
let sequential = scene(&f);
|
||||||
|
assert_eq!(
|
||||||
|
bar(&sequential, f.call).rect.x,
|
||||||
|
bar(&sequential, f.inner).rect.x
|
||||||
|
);
|
||||||
|
*f.project
|
||||||
|
.elements
|
||||||
|
.get_mut(&f.owner)
|
||||||
|
.unwrap()
|
||||||
|
.steps_mut()
|
||||||
|
.unwrap() = vec![f.call, f.inner, f.reply, f.inner_reply];
|
||||||
|
let crossed = scene(&f);
|
||||||
|
assert!(bar(&crossed, f.call).bottom() < bar(&crossed, f.inner).bottom());
|
||||||
|
assert!(bar(&crossed, f.call).rect.x < bar(&crossed, f.inner).rect.x);
|
||||||
|
assert_eq!(
|
||||||
|
line(&crossed, f.reply).points[0].x,
|
||||||
|
bar(&crossed, f.call).edge(false)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
line(&crossed, f.inner_reply).points[0].x,
|
||||||
|
bar(&crossed, f.inner).edge(false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn self_call_receives_at_lower_loop_end_and_self_reply_returns_to_the_parent() {
|
||||||
|
for layer in [Layer::Oa, Layer::Sa] {
|
||||||
|
let mut f = fixture(layer, true);
|
||||||
|
endpoints(&mut f.project, f.inner, f.callee, f.callee);
|
||||||
|
endpoints(&mut f.project, f.inner_reply, f.callee, f.callee);
|
||||||
|
let scene = scene(&f);
|
||||||
|
let parent = bar(&scene, f.call);
|
||||||
|
let child = bar(&scene, f.inner);
|
||||||
|
let call = line(&scene, f.inner);
|
||||||
|
let reply = line(&scene, f.inner_reply);
|
||||||
|
assert_eq!(call.points[0].x, parent.edge(true));
|
||||||
|
assert_eq!(call.points[3], Point::new(child.edge(true), child.rect.y));
|
||||||
|
assert_eq!(child.rect.y, scene.rows[&f.inner].1 + 24.);
|
||||||
|
assert_eq!(
|
||||||
|
reply.points[0],
|
||||||
|
Point::new(child.edge(true), child.bottom())
|
||||||
|
);
|
||||||
|
assert_eq!(reply.points[3].x, parent.edge(true));
|
||||||
|
assert!(call.points[1].x > child.edge(true));
|
||||||
|
assert!(reply.points[1].x > child.edge(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_inactive_self_call_has_no_fabricated_parent_and_three_nested_calls_expose_three_columns() {
|
||||||
|
let mut f = fixture(Layer::Oa, true);
|
||||||
|
endpoints(&mut f.project, f.inner, f.callee, f.callee);
|
||||||
|
endpoints(&mut f.project, f.inner_reply, f.callee, f.callee);
|
||||||
|
f.project.elements.remove(&f.call);
|
||||||
|
f.project.elements.remove(&f.reply);
|
||||||
|
*f.project
|
||||||
|
.elements
|
||||||
|
.get_mut(&f.owner)
|
||||||
|
.unwrap()
|
||||||
|
.steps_mut()
|
||||||
|
.unwrap() = vec![f.inner, f.inner_reply];
|
||||||
|
let isolated = scene(&f);
|
||||||
|
assert_eq!(isolated.activations.len(), 1);
|
||||||
|
assert_eq!(bar(&isolated, f.inner).column, 0);
|
||||||
|
assert_eq!(
|
||||||
|
line(&isolated, f.inner).points[0].x,
|
||||||
|
isolated.lanes[&f.callee]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
line(&isolated, f.inner_reply).points[3].x,
|
||||||
|
isolated.lanes[&f.callee]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
bar(&isolated, f.inner).rect.y,
|
||||||
|
line(&isolated, f.inner).points[3].y
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut f = fixture(Layer::Oa, true);
|
||||||
|
let third = message(
|
||||||
|
&mut f.project,
|
||||||
|
Layer::Oa,
|
||||||
|
20,
|
||||||
|
f.owner,
|
||||||
|
(f.caller, f.callee),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let third_reply = message(
|
||||||
|
&mut f.project,
|
||||||
|
Layer::Oa,
|
||||||
|
21,
|
||||||
|
f.owner,
|
||||||
|
(f.callee, f.caller),
|
||||||
|
Some(third),
|
||||||
|
);
|
||||||
|
*f.project
|
||||||
|
.elements
|
||||||
|
.get_mut(&f.owner)
|
||||||
|
.unwrap()
|
||||||
|
.steps_mut()
|
||||||
|
.unwrap() = vec![f.call, f.inner, third, third_reply, f.inner_reply, f.reply];
|
||||||
|
let scene = scene(&f);
|
||||||
|
let calls: Vec<_> = scene.activations.iter().map(|a| a.call).collect();
|
||||||
|
assert_eq!(
|
||||||
|
calls,
|
||||||
|
vec![f.call, f.inner, third],
|
||||||
|
"Paint order is shallow to deep"
|
||||||
|
);
|
||||||
|
for (column, call) in [f.call, f.inner, third].into_iter().enumerate() {
|
||||||
|
let execution = bar(&scene, call);
|
||||||
|
assert_eq!(execution.column, column);
|
||||||
|
let y = (bar(&scene, third).rect.y + bar(&scene, third).bottom()) / 2.;
|
||||||
|
assert_eq!(
|
||||||
|
scene.hit(Point::new(execution.rect.x + OFFSET / 2., y)),
|
||||||
|
Some(call)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reentrant_call_uses_the_other_receiver_without_inventing_a_caller_execution() {
|
||||||
|
let mut f = fixture(Layer::Oa, true);
|
||||||
|
endpoints(&mut f.project, f.inner, f.callee, f.caller);
|
||||||
|
endpoints(&mut f.project, f.inner_reply, f.caller, f.callee);
|
||||||
|
let scene = scene(&f);
|
||||||
|
assert_eq!(bar(&scene, f.call).column, 0);
|
||||||
|
assert_eq!(bar(&scene, f.inner).column, 0);
|
||||||
|
assert_eq!(bar(&scene, f.inner).receiver, f.caller);
|
||||||
|
assert_eq!(line(&scene, f.call).points[0].x, scene.lanes[&f.caller]);
|
||||||
|
assert_eq!(
|
||||||
|
line(&scene, f.inner).points[0].x,
|
||||||
|
bar(&scene, f.call).edge(false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn actual_canvas_pointer_selects_each_exposed_shoulder_under_zoom_and_pan() {
|
||||||
|
let f = fixture(Layer::Oa, true);
|
||||||
|
let mut scene = scene(&f);
|
||||||
|
let context = scenario::direct_tests::context(f.owner);
|
||||||
|
scene.interaction = Some((context, scenario::CanvasTool::Select, true));
|
||||||
|
let y = (bar(&scene, f.inner).rect.y + bar(&scene, f.inner).bottom()) / 2.;
|
||||||
|
let outer = Point::new(bar(&scene, f.call).rect.x + OFFSET / 2., y);
|
||||||
|
let inner = Point::new(bar(&scene, f.inner).edge(true) - OFFSET / 2., y);
|
||||||
|
for scale in [1., 0.25, 0.05] {
|
||||||
|
scene.scale = scale;
|
||||||
|
scene.camera_pan = iced::Vector::new(50., 25.);
|
||||||
|
let bounds = Rectangle::new(Point::new(30., 40.), Size::new(1000., 700.));
|
||||||
|
for (point, call) in [(outer, f.call), (inner, f.inner)] {
|
||||||
|
assert_eq!(scene.hit(point), Some(call));
|
||||||
|
let cursor = bounds.position()
|
||||||
|
+ scene.camera_pan
|
||||||
|
+ iced::Vector::new(point.x * scale, point.y * scale);
|
||||||
|
let mut state = SequenceInput::default();
|
||||||
|
let _ = canvas::Program::update(
|
||||||
|
&scene,
|
||||||
|
&mut state,
|
||||||
|
&canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
|
||||||
|
bounds,
|
||||||
|
mouse::Cursor::Available(cursor),
|
||||||
|
);
|
||||||
|
let action = canvas::Program::update(
|
||||||
|
&scene,
|
||||||
|
&mut state,
|
||||||
|
&canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
|
||||||
|
bounds,
|
||||||
|
mouse::Cursor::Available(cursor),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(matches!(action.into_inner().0,
|
||||||
|
Some(Message::ScenarioCanvas(scenario::CanvasEvent::Select(_, selected))) if selected == call));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_reply_is_a_short_explicit_open_span_and_an_uncorrelated_reply_closes_nothing() {
|
||||||
|
let mut f = fixture(Layer::Oa, true);
|
||||||
|
if let SharedBody::Occurrence {
|
||||||
|
body: OccurrenceBody::Exchange { reply_to, .. },
|
||||||
|
..
|
||||||
|
} = f
|
||||||
|
.project
|
||||||
|
.elements
|
||||||
|
.get_mut(&f.reply)
|
||||||
|
.unwrap()
|
||||||
|
.shared_mut()
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
*reply_to = None;
|
||||||
|
}
|
||||||
|
let scene = scene(&f);
|
||||||
|
assert_eq!(bar(&scene, f.call).finish, Finish::Open);
|
||||||
|
assert_eq!(bar(&scene, f.call).rect.height, 26.);
|
||||||
|
assert_eq!(scene.open_calls.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
bar(&scene, f.inner).column,
|
||||||
|
0,
|
||||||
|
"Unknown duration does not prove later nesting"
|
||||||
|
);
|
||||||
|
assert_eq!(bar(&scene, f.inner).finish, Finish::Reply(f.inner_reply));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn alternative_and_parallel_operand_local_calls_do_not_nest_from_screen_order() {
|
||||||
|
for layer in [Layer::Oa, Layer::Sa] {
|
||||||
|
for operator in [FragmentOperator::Alternative, FragmentOperator::Parallel] {
|
||||||
|
let mut f = fixture(layer, true);
|
||||||
|
let fragment = seed(&mut f.project, layer, 70, Kind::Occurrence, Some(f.owner));
|
||||||
|
let branches = [id(71), id(72)];
|
||||||
|
*f.project
|
||||||
|
.elements
|
||||||
|
.get_mut(&fragment)
|
||||||
|
.unwrap()
|
||||||
|
.shared_mut()
|
||||||
|
.unwrap() = SharedBody::Occurrence {
|
||||||
|
body: OccurrenceBody::Fragment {
|
||||||
|
operator,
|
||||||
|
branches: branches.to_vec(),
|
||||||
|
},
|
||||||
|
precondition: String::new(),
|
||||||
|
postcondition: String::new(),
|
||||||
|
};
|
||||||
|
for (index, occurrences) in [vec![f.call, f.reply], vec![f.inner, f.inner_reply]]
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
let branch = seed(
|
||||||
|
&mut f.project,
|
||||||
|
layer,
|
||||||
|
71 + index as u128,
|
||||||
|
Kind::Occurrence,
|
||||||
|
Some(fragment),
|
||||||
|
);
|
||||||
|
for occurrence in &occurrences {
|
||||||
|
f.project.elements.get_mut(occurrence).unwrap().parent = Some(branch);
|
||||||
|
}
|
||||||
|
*f.project
|
||||||
|
.elements
|
||||||
|
.get_mut(&branch)
|
||||||
|
.unwrap()
|
||||||
|
.shared_mut()
|
||||||
|
.unwrap() = SharedBody::Occurrence {
|
||||||
|
body: OccurrenceBody::Branch {
|
||||||
|
guard: format!("condition_{index}"),
|
||||||
|
occurrences,
|
||||||
|
},
|
||||||
|
precondition: String::new(),
|
||||||
|
postcondition: String::new(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
*f.project
|
||||||
|
.elements
|
||||||
|
.get_mut(&f.owner)
|
||||||
|
.unwrap()
|
||||||
|
.steps_mut()
|
||||||
|
.unwrap() = vec![fragment];
|
||||||
|
let scene = scene(&f);
|
||||||
|
assert_eq!(bar(&scene, f.call).column, 0);
|
||||||
|
assert_eq!(bar(&scene, f.inner).column, 0);
|
||||||
|
assert_ne!(bar(&scene, f.call).scope, bar(&scene, f.inner).scope);
|
||||||
|
assert!(scene.open_calls.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn inherited_execution_anchors_a_self_call_inside_an_operand_before_returning_after_the_fragment() {
|
||||||
|
let mut f = fixture(Layer::Oa, true);
|
||||||
|
endpoints(&mut f.project, f.inner, f.callee, f.callee);
|
||||||
|
endpoints(&mut f.project, f.inner_reply, f.callee, f.callee);
|
||||||
|
let fragment = seed(
|
||||||
|
&mut f.project,
|
||||||
|
Layer::Oa,
|
||||||
|
70,
|
||||||
|
Kind::Occurrence,
|
||||||
|
Some(f.owner),
|
||||||
|
);
|
||||||
|
*f.project
|
||||||
|
.elements
|
||||||
|
.get_mut(&fragment)
|
||||||
|
.unwrap()
|
||||||
|
.shared_mut()
|
||||||
|
.unwrap() = SharedBody::Occurrence {
|
||||||
|
body: OccurrenceBody::Fragment {
|
||||||
|
operator: FragmentOperator::Alternative,
|
||||||
|
branches: vec![id(71), id(72)],
|
||||||
|
},
|
||||||
|
precondition: String::new(),
|
||||||
|
postcondition: String::new(),
|
||||||
|
};
|
||||||
|
for (index, occurrences) in [vec![f.inner, f.inner_reply], vec![]]
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
let branch = seed(
|
||||||
|
&mut f.project,
|
||||||
|
Layer::Oa,
|
||||||
|
71 + index as u128,
|
||||||
|
Kind::Occurrence,
|
||||||
|
Some(fragment),
|
||||||
|
);
|
||||||
|
for occurrence in &occurrences {
|
||||||
|
f.project.elements.get_mut(occurrence).unwrap().parent = Some(branch);
|
||||||
|
}
|
||||||
|
*f.project
|
||||||
|
.elements
|
||||||
|
.get_mut(&branch)
|
||||||
|
.unwrap()
|
||||||
|
.shared_mut()
|
||||||
|
.unwrap() = SharedBody::Occurrence {
|
||||||
|
body: OccurrenceBody::Branch {
|
||||||
|
guard: format!("condition_{index}"),
|
||||||
|
occurrences,
|
||||||
|
},
|
||||||
|
precondition: String::new(),
|
||||||
|
postcondition: String::new(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
*f.project
|
||||||
|
.elements
|
||||||
|
.get_mut(&f.owner)
|
||||||
|
.unwrap()
|
||||||
|
.steps_mut()
|
||||||
|
.unwrap() = vec![f.call, fragment, f.reply];
|
||||||
|
let scene = scene(&f);
|
||||||
|
let parent = scene
|
||||||
|
.activations
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.call == f.call && a.scope == id(71))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(parent.finish, Finish::Continuation);
|
||||||
|
assert_eq!(bar(&scene, f.inner).column, parent.column + 1);
|
||||||
|
assert_eq!(line(&scene, f.inner).points[0].x, parent.edge(true));
|
||||||
|
assert_eq!(line(&scene, f.inner_reply).points[3].x, parent.edge(true));
|
||||||
|
assert!(
|
||||||
|
scene
|
||||||
|
.activations
|
||||||
|
.iter()
|
||||||
|
.any(|a| a.call == f.call && a.finish == Finish::Reply(f.reply))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_fragment_cannot_make_crossed_calls_an_unambiguous_caller_execution() {
|
||||||
|
for placement in ["plain", "before_fragment", "in_operand"] {
|
||||||
|
let mut f = fixture(Layer::Oa, true);
|
||||||
|
let outgoing = message(
|
||||||
|
&mut f.project,
|
||||||
|
Layer::Oa,
|
||||||
|
80,
|
||||||
|
f.owner,
|
||||||
|
(f.callee, f.caller),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
if let SharedBody::Occurrence {
|
||||||
|
body: OccurrenceBody::Exchange { message_kind, .. },
|
||||||
|
..
|
||||||
|
} = f
|
||||||
|
.project
|
||||||
|
.elements
|
||||||
|
.get_mut(&outgoing)
|
||||||
|
.unwrap()
|
||||||
|
.shared_mut()
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
*message_kind = MessageKind::Asynchronous;
|
||||||
|
}
|
||||||
|
let mut ordered = vec![f.call, f.inner];
|
||||||
|
if placement != "in_operand" {
|
||||||
|
ordered.push(outgoing);
|
||||||
|
}
|
||||||
|
if placement != "plain" {
|
||||||
|
let fragment = seed(
|
||||||
|
&mut f.project,
|
||||||
|
Layer::Oa,
|
||||||
|
70,
|
||||||
|
Kind::Occurrence,
|
||||||
|
Some(f.owner),
|
||||||
|
);
|
||||||
|
*f.project
|
||||||
|
.elements
|
||||||
|
.get_mut(&fragment)
|
||||||
|
.unwrap()
|
||||||
|
.shared_mut()
|
||||||
|
.unwrap() = SharedBody::Occurrence {
|
||||||
|
body: OccurrenceBody::Fragment {
|
||||||
|
operator: FragmentOperator::Alternative,
|
||||||
|
branches: vec![id(71), id(72)],
|
||||||
|
},
|
||||||
|
precondition: String::new(),
|
||||||
|
postcondition: String::new(),
|
||||||
|
};
|
||||||
|
for index in 0..2u128 {
|
||||||
|
let branch = seed(
|
||||||
|
&mut f.project,
|
||||||
|
Layer::Oa,
|
||||||
|
71 + index,
|
||||||
|
Kind::Occurrence,
|
||||||
|
Some(fragment),
|
||||||
|
);
|
||||||
|
let occurrences = if index == 0 && placement == "in_operand" {
|
||||||
|
f.project.elements.get_mut(&outgoing).unwrap().parent = Some(branch);
|
||||||
|
vec![outgoing]
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
*f.project
|
||||||
|
.elements
|
||||||
|
.get_mut(&branch)
|
||||||
|
.unwrap()
|
||||||
|
.shared_mut()
|
||||||
|
.unwrap() = SharedBody::Occurrence {
|
||||||
|
body: OccurrenceBody::Branch {
|
||||||
|
guard: format!("condition_{index}"),
|
||||||
|
occurrences,
|
||||||
|
},
|
||||||
|
precondition: String::new(),
|
||||||
|
postcondition: String::new(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
ordered.push(fragment);
|
||||||
|
}
|
||||||
|
ordered.extend([f.reply, f.inner_reply]);
|
||||||
|
*f.project
|
||||||
|
.elements
|
||||||
|
.get_mut(&f.owner)
|
||||||
|
.unwrap()
|
||||||
|
.steps_mut()
|
||||||
|
.unwrap() = ordered;
|
||||||
|
let scene = scene(&f);
|
||||||
|
assert_eq!(
|
||||||
|
line(&scene, outgoing).points[0].x,
|
||||||
|
scene.lanes[&f.callee],
|
||||||
|
"{placement}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nested_correlations_returning_in_alternative_operands_remain_nested_before_the_fragment() {
|
||||||
|
let mut f = fixture(Layer::Oa, true);
|
||||||
|
let outgoing = message(
|
||||||
|
&mut f.project,
|
||||||
|
Layer::Oa,
|
||||||
|
80,
|
||||||
|
f.owner,
|
||||||
|
(f.callee, f.caller),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
if let SharedBody::Occurrence {
|
||||||
|
body: OccurrenceBody::Exchange { message_kind, .. },
|
||||||
|
..
|
||||||
|
} = f
|
||||||
|
.project
|
||||||
|
.elements
|
||||||
|
.get_mut(&outgoing)
|
||||||
|
.unwrap()
|
||||||
|
.shared_mut()
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
*message_kind = MessageKind::Asynchronous;
|
||||||
|
}
|
||||||
|
let fragment = seed(
|
||||||
|
&mut f.project,
|
||||||
|
Layer::Oa,
|
||||||
|
70,
|
||||||
|
Kind::Occurrence,
|
||||||
|
Some(f.owner),
|
||||||
|
);
|
||||||
|
*f.project
|
||||||
|
.elements
|
||||||
|
.get_mut(&fragment)
|
||||||
|
.unwrap()
|
||||||
|
.shared_mut()
|
||||||
|
.unwrap() = SharedBody::Occurrence {
|
||||||
|
body: OccurrenceBody::Fragment {
|
||||||
|
operator: FragmentOperator::Alternative,
|
||||||
|
branches: vec![id(71), id(72)],
|
||||||
|
},
|
||||||
|
precondition: String::new(),
|
||||||
|
postcondition: String::new(),
|
||||||
|
};
|
||||||
|
for index in 0..2u128 {
|
||||||
|
let branch = seed(
|
||||||
|
&mut f.project,
|
||||||
|
Layer::Oa,
|
||||||
|
71 + index,
|
||||||
|
Kind::Occurrence,
|
||||||
|
Some(fragment),
|
||||||
|
);
|
||||||
|
let occurrences = if index == 0 {
|
||||||
|
for reply in [f.inner_reply, f.reply] {
|
||||||
|
f.project.elements.get_mut(&reply).unwrap().parent = Some(branch);
|
||||||
|
}
|
||||||
|
vec![f.inner_reply, f.reply]
|
||||||
|
} else {
|
||||||
|
vec![
|
||||||
|
message(
|
||||||
|
&mut f.project,
|
||||||
|
Layer::Oa,
|
||||||
|
90,
|
||||||
|
branch,
|
||||||
|
(f.callee, f.caller),
|
||||||
|
Some(f.inner),
|
||||||
|
),
|
||||||
|
message(
|
||||||
|
&mut f.project,
|
||||||
|
Layer::Oa,
|
||||||
|
91,
|
||||||
|
branch,
|
||||||
|
(f.callee, f.caller),
|
||||||
|
Some(f.call),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
};
|
||||||
|
*f.project
|
||||||
|
.elements
|
||||||
|
.get_mut(&branch)
|
||||||
|
.unwrap()
|
||||||
|
.shared_mut()
|
||||||
|
.unwrap() = SharedBody::Occurrence {
|
||||||
|
body: OccurrenceBody::Branch {
|
||||||
|
guard: format!("condition_{index}"),
|
||||||
|
occurrences,
|
||||||
|
},
|
||||||
|
precondition: String::new(),
|
||||||
|
postcondition: String::new(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
*f.project
|
||||||
|
.elements
|
||||||
|
.get_mut(&f.owner)
|
||||||
|
.unwrap()
|
||||||
|
.steps_mut()
|
||||||
|
.unwrap() = vec![f.call, f.inner, outgoing, fragment];
|
||||||
|
let scene = scene(&f);
|
||||||
|
let inner = scene
|
||||||
|
.activations
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.call == f.inner && a.scope == f.owner)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(line(&scene, outgoing).points[0].x, inner.edge(false));
|
||||||
|
}
|
||||||
@@ -8,6 +8,21 @@ use iced::{
|
|||||||
use oa_core::{Id, OccurrenceBody, Project, SharedBody};
|
use oa_core::{Id, OccurrenceBody, Project, SharedBody};
|
||||||
use std::collections::BTreeMap;
|
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)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
struct EventMarker {
|
struct EventMarker {
|
||||||
event: Marker,
|
event: Marker,
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ use crate::{behavior::Scene, document::Document};
|
|||||||
use iced::Size;
|
use iced::Size;
|
||||||
use oa_core::*;
|
use oa_core::*;
|
||||||
|
|
||||||
|
#[path = "union_tests.rs"]
|
||||||
|
mod union_tests;
|
||||||
|
|
||||||
struct Fixture {
|
struct Fixture {
|
||||||
project: Project,
|
project: Project,
|
||||||
owner: Id,
|
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 {
|
fn fragment(&mut self, parent: Id, operator: FragmentOperator) -> Id {
|
||||||
self.occurrence(
|
self.occurrence(
|
||||||
parent,
|
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 (left_start, left_end) = scene.sequence_bounds[&left];
|
||||||
let (right_start, _) = scene.sequence_bounds[&right];
|
let (right_start, _) = scene.sequence_bounds[&right];
|
||||||
let bars: Vec<_> = scene
|
let bars: Vec<_> = scene
|
||||||
.boxes
|
.activations
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|b| b.id == call && b.rect.width == 8.)
|
.filter(|b| b.call == call && b.receiver == f.receiver)
|
||||||
.map(|b| b.rect)
|
.map(|b| b.rect)
|
||||||
.collect();
|
.collect();
|
||||||
assert!(
|
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