Render nested call activations with correlation-aware anchors
This commit is contained in:
@@ -0,0 +1,389 @@
|
||||
//! 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,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
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>) {
|
||||
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();
|
||||
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;
|
||||
}
|
||||
}
|
||||
// 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.open_calls = self
|
||||
.activations
|
||||
.iter()
|
||||
.filter(|a| matches!(a.finish, Finish::Open | Finish::Continuation))
|
||||
.map(|a| Point::new(a.rect.x + a.rect.width + 4., a.bottom() - 7.))
|
||||
.collect();
|
||||
self.anchor_activation_messages(p, lanes);
|
||||
}
|
||||
|
||||
fn activation_segment(
|
||||
&mut self,
|
||||
call: Id,
|
||||
receiver: Id,
|
||||
scope: Id,
|
||||
x: f32,
|
||||
span: std::ops::Range<f32>,
|
||||
finish: Finish,
|
||||
) {
|
||||
if span.end > span.start {
|
||||
self.activations.push(Segment {
|
||||
call,
|
||||
receiver,
|
||||
scope,
|
||||
rect: Rectangle::new(
|
||||
Point::new(x - WIDTH / 2., span.start),
|
||||
Size::new(WIDTH, span.end - span.start),
|
||||
),
|
||||
column: 0,
|
||||
finish,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
) -> bool {
|
||||
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 {
|
||||
// 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::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(|open| *open)
|
||||
}
|
||||
FragmentOperator::Parallel => outcomes.iter().all(|open| *open),
|
||||
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.
|
||||
};
|
||||
let finish = if entered_open || split {
|
||||
Finish::Continuation
|
||||
} else {
|
||||
Finish::Open
|
||||
};
|
||||
self.activation_segment(call, receiver, owner, x, top..bottom, finish);
|
||||
}
|
||||
start.is_some()
|
||||
}
|
||||
|
||||
fn anchor_activation_messages(&mut self, p: &Project, lanes: &BTreeMap<Id, f32>) {
|
||||
let segments = &self.activations;
|
||||
let mut correlations: BTreeMap<Id, Correlation> = BTreeMap::new();
|
||||
for segment in segments {
|
||||
let correlation = correlations.entry(segment.call).or_insert(Correlation {
|
||||
start: segment.rect.y,
|
||||
replies: vec![],
|
||||
});
|
||||
correlation.start = correlation.start.min(segment.rect.y);
|
||||
if matches!(segment.finish, Finish::Reply(_)) {
|
||||
correlation.replies.push((segment.scope, segment.bottom()));
|
||||
}
|
||||
}
|
||||
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.finish == Finish::Reply(line.id))
|
||||
})
|
||||
} 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.;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,749 @@
|
||||
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 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));
|
||||
}
|
||||
Reference in New Issue
Block a user