471 lines
15 KiB
Rust
471 lines
15 KiB
Rust
use super::*;
|
|
use oa_core::*;
|
|
|
|
struct Fixture {
|
|
p: Project,
|
|
owner: Id,
|
|
a: Id,
|
|
b: Id,
|
|
exchange: Id,
|
|
rows: BTreeMap<Id, (Id, f32)>,
|
|
scopes: BTreeMap<Id, (f32, f32)>,
|
|
}
|
|
impl Fixture {
|
|
fn new(layer: Layer) -> Self {
|
|
let mut p = Project::empty();
|
|
let kind = if layer == Layer::Oa {
|
|
Kind::Activity
|
|
} else {
|
|
Kind::Function
|
|
};
|
|
let a = p.add_element(kind, layer, "Request", None).unwrap();
|
|
let b = p.add_element(kind, layer, "Provide", None).unwrap();
|
|
let owner = p
|
|
.add_element(Kind::Scenario, layer, "Lifecycle", None)
|
|
.unwrap();
|
|
let exchange = p.relate(
|
|
if layer == Layer::Oa {
|
|
RelationKind::Interaction
|
|
} else {
|
|
RelationKind::FunctionalExchange
|
|
},
|
|
"Service",
|
|
a,
|
|
b,
|
|
);
|
|
let line = |p: &mut Project, represented| {
|
|
let id = p
|
|
.add_element(Kind::Lifeline, layer, "Participant", Some(owner))
|
|
.unwrap();
|
|
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Lifeline {
|
|
represented: Some(represented),
|
|
};
|
|
id
|
|
};
|
|
let a = line(&mut p, a);
|
|
let b = line(&mut p, b);
|
|
Self {
|
|
p,
|
|
owner,
|
|
a,
|
|
b,
|
|
exchange,
|
|
rows: BTreeMap::new(),
|
|
scopes: [(owner, (0., 600.))].into(),
|
|
}
|
|
}
|
|
fn occurrence(&mut self, owner: Id, y: f32, body: OccurrenceBody) -> Id {
|
|
let mut e = self
|
|
.p
|
|
.make_element(
|
|
Kind::Occurrence,
|
|
self.p.elements[&owner].layer(),
|
|
"Occurrence",
|
|
Some(owner),
|
|
)
|
|
.unwrap();
|
|
let id = e.id;
|
|
*e.shared_mut().unwrap() = SharedBody::Occurrence {
|
|
body,
|
|
precondition: String::new(),
|
|
postcondition: String::new(),
|
|
};
|
|
self.p.elements.insert(id, e);
|
|
if let Some(steps) = self.p.elements.get_mut(&owner).unwrap().steps_mut() {
|
|
steps.push(id);
|
|
} else if let Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::Fragment { branches, .. },
|
|
..
|
|
}) = self.p.elements.get_mut(&owner).unwrap().shared_mut()
|
|
{
|
|
branches.push(id);
|
|
}
|
|
self.rows.insert(id, (owner, y));
|
|
id
|
|
}
|
|
fn message(&mut self, owner: Id, y: f32, kind: MessageKind) -> Id {
|
|
self.occurrence(
|
|
owner,
|
|
y,
|
|
OccurrenceBody::Exchange {
|
|
exchange: Some(self.exchange),
|
|
sender: Some(self.a),
|
|
receiver: Some(self.b),
|
|
message_kind: kind,
|
|
operation: None,
|
|
reply_to: None,
|
|
values: vec![],
|
|
},
|
|
)
|
|
}
|
|
fn fragment(&mut self, owner: Id, y: f32, operator: FragmentOperator) -> Id {
|
|
self.occurrence(
|
|
owner,
|
|
y,
|
|
OccurrenceBody::Fragment {
|
|
operator,
|
|
branches: vec![],
|
|
},
|
|
)
|
|
}
|
|
fn branch(&mut self, fragment: Id, start: f32, end: f32) -> Id {
|
|
let id = self.occurrence(
|
|
fragment,
|
|
start,
|
|
OccurrenceBody::Branch {
|
|
guard: "condition".into(),
|
|
occurrences: vec![],
|
|
},
|
|
);
|
|
self.scopes.insert(id, (start, end));
|
|
id
|
|
}
|
|
fn project(&self) -> Vec<LaneLifetime> {
|
|
let errors: Vec<_> = validation::validate(&self.p)
|
|
.into_iter()
|
|
.filter(|i| i.severity == validation::Severity::Error)
|
|
.collect();
|
|
assert!(
|
|
errors.is_empty(),
|
|
"fixture must be ordinarily admitted: {errors:?}"
|
|
);
|
|
let before = self.p.clone();
|
|
let source = source_project::format_bundle(&self.p);
|
|
let geometry = Geometry {
|
|
rows: &self.rows,
|
|
scopes: &self.scopes,
|
|
};
|
|
let result = project_lifetimes(&self.p, self.owner, &geometry);
|
|
assert_eq!(result, project_lifetimes(&self.p, self.owner, &geometry));
|
|
assert_eq!(self.p, before);
|
|
assert_eq!(source_project::format_bundle(&self.p), source);
|
|
assert!(
|
|
result
|
|
.windows(2)
|
|
.all(|pair| pair[0].lifeline < pair[1].lifeline)
|
|
);
|
|
result
|
|
}
|
|
fn lane(&self) -> LaneLifetime {
|
|
self.project()
|
|
.into_iter()
|
|
.find(|lane| lane.lifeline == self.b)
|
|
.unwrap()
|
|
}
|
|
}
|
|
fn intervals(lane: &LaneLifetime, scope: Id) -> Vec<(f32, f32, Life)> {
|
|
lane.intervals
|
|
.iter()
|
|
.filter(|i| i.scope == scope)
|
|
.map(|i| (i.span.start, i.span.end, i.life))
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn root_lifetime_has_exact_birth_death_boundaries_without_source_mutation() {
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
let mut f = Fixture::new(layer);
|
|
let create = f.message(f.owner, 100., MessageKind::Create);
|
|
f.message(f.owner, 200., MessageKind::Asynchronous);
|
|
let delete = f.message(f.owner, 300., MessageKind::Delete);
|
|
let lane = f.lane();
|
|
assert_eq!(
|
|
intervals(&lane, f.owner),
|
|
vec![
|
|
(0., 100., Life::Absent),
|
|
(100., 300., Life::Alive),
|
|
(300., 600., Life::Absent)
|
|
]
|
|
);
|
|
assert_eq!(
|
|
lane.markers,
|
|
vec![
|
|
Marker {
|
|
occurrence: create,
|
|
scope: f.owner,
|
|
y: 100.,
|
|
kind: MarkerKind::Birth
|
|
},
|
|
Marker {
|
|
occurrence: delete,
|
|
scope: f.owner,
|
|
y: 300.,
|
|
kind: MarkerKind::Death
|
|
},
|
|
]
|
|
);
|
|
let sender = f
|
|
.project()
|
|
.into_iter()
|
|
.find(|lane| lane.lifeline == f.a)
|
|
.unwrap();
|
|
assert_eq!(intervals(&sender, f.owner), vec![(0., 600., Life::Alive)]);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn alternative_death_does_not_kill_its_displayed_sibling() {
|
|
let mut f = Fixture::new(Layer::Sa);
|
|
let fragment = f.fragment(f.owner, 100., FragmentOperator::Alternative);
|
|
let left = f.branch(fragment, 120., 250.);
|
|
let right = f.branch(fragment, 300., 450.);
|
|
f.message(left, 200., MessageKind::Delete);
|
|
f.message(right, 350., MessageKind::Asynchronous);
|
|
let lane = f.lane();
|
|
assert_eq!(
|
|
intervals(&lane, left),
|
|
vec![(120., 200., Life::Alive), (200., 250., Life::Absent)]
|
|
);
|
|
assert_eq!(intervals(&lane, right), vec![(300., 450., Life::Alive)]);
|
|
assert_eq!(
|
|
intervals(&lane, f.owner),
|
|
vec![(0., 100., Life::Alive), (450., 600., Life::Unresolved)]
|
|
);
|
|
assert_eq!(
|
|
lane.uncertainties,
|
|
vec![Uncertainty {
|
|
scope: fragment,
|
|
reason: UncertaintyReason::Conditional
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn conditional_birth_does_not_infer_absence_in_siblings_or_ancestors() {
|
|
for operator in [FragmentOperator::Alternative, FragmentOperator::Optional] {
|
|
let mut f = Fixture::new(Layer::Oa);
|
|
let fragment = f.fragment(f.owner, 100., operator);
|
|
let left = f.branch(fragment, 120., 250.);
|
|
let right = f.branch(fragment, 300., 450.);
|
|
f.message(left, 200., MessageKind::Create);
|
|
let lane = f.lane();
|
|
assert_eq!(
|
|
intervals(&lane, left),
|
|
vec![(120., 200., Life::Absent), (200., 250., Life::Alive)]
|
|
);
|
|
assert_eq!(
|
|
intervals(&lane, right),
|
|
vec![(300., 450., Life::Unresolved)]
|
|
);
|
|
assert_eq!(
|
|
intervals(&lane, f.owner),
|
|
vec![(0., 100., Life::Unresolved), (450., 600., Life::Unresolved)]
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn loop_lifetime_is_local_and_warning_bearing_loops_remain_drawable() {
|
|
for balanced in [true, false] {
|
|
let mut f = Fixture::new(Layer::Sa);
|
|
let fragment = f.fragment(f.owner, 100., FragmentOperator::Loop);
|
|
let iteration = f.branch(fragment, 120., 450.);
|
|
let create = f.message(iteration, 160., MessageKind::Create);
|
|
f.message(iteration, 250., MessageKind::Asynchronous);
|
|
if balanced {
|
|
f.message(iteration, 350., MessageKind::Delete);
|
|
}
|
|
let lane = f.lane();
|
|
assert_eq!(lane.markers.len(), if balanced { 2 } else { 1 });
|
|
assert_eq!(lane.markers[0].occurrence, create);
|
|
assert!(lane.uncertainties.contains(&Uncertainty {
|
|
scope: fragment,
|
|
reason: UncertaintyReason::Repeated
|
|
}));
|
|
assert_eq!(
|
|
intervals(&lane, f.owner).last().unwrap().2,
|
|
Life::Unresolved
|
|
);
|
|
assert_eq!(
|
|
validation::validate(&f.p)
|
|
.iter()
|
|
.any(|i| i.code == "lifeline_repetition"),
|
|
!balanced
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn optional_cleanup_inside_loop_does_not_claim_balanced_or_definite_exit() {
|
|
let mut f = Fixture::new(Layer::Sa);
|
|
let repeated = f.fragment(f.owner, 100., FragmentOperator::Loop);
|
|
let iteration = f.branch(repeated, 120., 500.);
|
|
f.message(iteration, 160., MessageKind::Create);
|
|
let optional = f.fragment(iteration, 280., FragmentOperator::Optional);
|
|
let cleanup = f.branch(optional, 320., 400.);
|
|
f.message(cleanup, 350., MessageKind::Delete);
|
|
let lane = f.lane();
|
|
assert_eq!(
|
|
intervals(&lane, cleanup),
|
|
vec![(320., 350., Life::Alive), (350., 400., Life::Absent)]
|
|
);
|
|
assert_eq!(
|
|
intervals(&lane, iteration),
|
|
vec![
|
|
(120., 160., Life::Absent),
|
|
(160., 280., Life::Alive),
|
|
(400., 500., Life::Unresolved)
|
|
]
|
|
);
|
|
assert!(
|
|
validation::validate(&f.p)
|
|
.iter()
|
|
.any(|i| i.code == "lifeline_repetition")
|
|
);
|
|
assert!(lane.uncertainties.contains(&Uncertainty {
|
|
scope: optional,
|
|
reason: UncertaintyReason::Conditional
|
|
}));
|
|
assert!(lane.uncertainties.contains(&Uncertainty {
|
|
scope: repeated,
|
|
reason: UncertaintyReason::Repeated
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn lifecycle_free_fragments_do_not_poison_other_lanes() {
|
|
let mut f = Fixture::new(Layer::Oa);
|
|
let fragment = f.fragment(f.owner, 100., FragmentOperator::Parallel);
|
|
let left = f.branch(fragment, 120., 250.);
|
|
let right = f.branch(fragment, 300., 450.);
|
|
f.message(left, 200., MessageKind::Delete);
|
|
let result = f.project();
|
|
let sender = result.iter().find(|lane| lane.lifeline == f.a).unwrap();
|
|
assert!(sender.intervals.iter().all(|i| i.life == Life::Alive));
|
|
assert!(sender.uncertainties.is_empty());
|
|
let target = result.iter().find(|lane| lane.lifeline == f.b).unwrap();
|
|
assert!(target.uncertainties.contains(&Uncertainty {
|
|
scope: fragment,
|
|
reason: UncertaintyReason::Parallel
|
|
}));
|
|
assert_eq!(intervals(target, right), vec![(300., 450., Life::Alive)]);
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_receiver_is_authoritative_even_with_missing_exchange() {
|
|
let mut f = Fixture::new(Layer::Oa);
|
|
let create = f.message(f.owner, 100., MessageKind::Create);
|
|
if let Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange { exchange, .. },
|
|
..
|
|
}) = f.p.elements.get_mut(&create).unwrap().shared_mut()
|
|
{
|
|
*exchange = None;
|
|
}
|
|
assert_eq!(f.lane().markers[0].occurrence, create);
|
|
if let Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange {
|
|
receiver, exchange, ..
|
|
},
|
|
..
|
|
}) = f.p.elements.get_mut(&create).unwrap().shared_mut()
|
|
{
|
|
*receiver = None;
|
|
*exchange = Some(f.exchange);
|
|
}
|
|
assert!(
|
|
f.lane().markers.is_empty(),
|
|
"exchange endpoints never invent a receiver"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn missing_geometry_is_reported_without_nonfinite_or_guessed_markers() {
|
|
let mut f = Fixture::new(Layer::Oa);
|
|
let create = f.message(f.owner, 100., MessageKind::Create);
|
|
f.rows.insert(create, (f.owner, f32::NAN));
|
|
let lane = f.lane();
|
|
assert!(lane.markers.is_empty());
|
|
assert_eq!(
|
|
lane.uncertainties,
|
|
vec![Uncertainty {
|
|
scope: create,
|
|
reason: UncertaintyReason::MissingGeometry
|
|
}]
|
|
);
|
|
assert_eq!(
|
|
intervals(&lane, f.owner),
|
|
vec![(0., 600., Life::Unresolved)]
|
|
);
|
|
f.scopes.remove(&f.owner);
|
|
let lane = f.lane();
|
|
assert!(lane.intervals.is_empty());
|
|
assert_eq!(lane.uncertainties[0].scope, f.owner);
|
|
}
|
|
|
|
#[test]
|
|
fn scenario_use_is_opaque_and_never_imports_callee_lifecycle_events() {
|
|
let mut f = Fixture::new(Layer::Oa);
|
|
let callee =
|
|
f.p.add_element(Kind::Scenario, Layer::Oa, "Callee", None)
|
|
.unwrap();
|
|
let remote_a =
|
|
f.p.add_element(Kind::Lifeline, Layer::Oa, "Remote A", Some(callee))
|
|
.unwrap();
|
|
let remote_b =
|
|
f.p.add_element(Kind::Lifeline, Layer::Oa, "Remote B", Some(callee))
|
|
.unwrap();
|
|
for (remote, local) in [(remote_a, f.a), (remote_b, f.b)] {
|
|
let body = f.p.elements[&local].shared().unwrap().clone();
|
|
*f.p.elements.get_mut(&remote).unwrap().shared_mut().unwrap() = body;
|
|
}
|
|
f.occurrence(
|
|
callee,
|
|
200.,
|
|
OccurrenceBody::Exchange {
|
|
exchange: Some(f.exchange),
|
|
sender: Some(remote_a),
|
|
receiver: Some(remote_b),
|
|
message_kind: MessageKind::Create,
|
|
operation: None,
|
|
reply_to: None,
|
|
values: vec![],
|
|
},
|
|
);
|
|
f.occurrence(
|
|
f.owner,
|
|
100.,
|
|
OccurrenceBody::ScenarioUse {
|
|
scenario: Some(callee),
|
|
bindings: vec![
|
|
LifelineBinding {
|
|
referenced: remote_a,
|
|
local: f.a,
|
|
},
|
|
LifelineBinding {
|
|
referenced: remote_b,
|
|
local: f.b,
|
|
},
|
|
],
|
|
},
|
|
);
|
|
let lane = f.lane();
|
|
assert_eq!(intervals(&lane, f.owner), vec![(0., 600., Life::Alive)]);
|
|
assert!(lane.markers.is_empty());
|
|
assert!(lane.uncertainties.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn projection_retains_exact_authored_source_and_does_not_enter_history() {
|
|
use crate::document::Document;
|
|
let mut f = Fixture::new(Layer::Oa);
|
|
f.message(f.owner, 100., MessageKind::Create);
|
|
f.message(f.owner, 300., MessageKind::Delete);
|
|
let mut source = source_project::format_bundle(&f.p);
|
|
let text = source.files.get_mut(&source.entry).unwrap();
|
|
*text = format!("// Retained author note — 雨\r\n{text}\n");
|
|
let document =
|
|
Document::from_bundle(source.clone(), source_project::ProjectLimits::default()).unwrap();
|
|
let accepted = document.project().clone();
|
|
let geometry = Geometry {
|
|
rows: &f.rows,
|
|
scopes: &f.scopes,
|
|
};
|
|
let result = project_lifetimes(document.project(), f.owner, &geometry);
|
|
assert_eq!(result, f.project());
|
|
assert_eq!(document.bundle(), &source);
|
|
assert_eq!(document.project(), &accepted);
|
|
assert!(!document.can_undo());
|
|
assert!(!document.can_redo());
|
|
}
|