use super::*; use oa_core::*; struct Fixture { p: Project, owner: Id, a: Id, b: Id, exchange: Id, rows: BTreeMap, scopes: BTreeMap, } 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 { 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()); }