use super::*; fn add_occurrence(p: &mut Project, owner: Id, name: &str, body: OccurrenceBody) -> Id { let layer = p.elements[&owner].layer(); let id = p .add_element(Kind::Occurrence, layer, name, Some(owner)) .unwrap(); *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Occurrence { body, precondition: String::new(), postcondition: String::new(), }; if let Some(steps) = p.elements.get_mut(&owner).unwrap().steps_mut() && !steps.contains(&id) { steps.push(id); } id } fn targets(state: &State, p: &Project, owner: Id, subject: Id) -> BTreeSet { state .diagram_usages(p, owner, subject) .into_iter() .filter_map(|u| match u.target { Target::ScenarioObject(id) => Some(id), _ => None, }) .collect() } #[test] fn scenario_usage_counts_nested_occurrences_and_lifelines_but_keeps_reuse_opaque() { for (layer, work_kind, participant_kind) in [ (Layer::Oa, Kind::Activity, Kind::Entity), (Layer::Sa, Kind::Function, Kind::SystemActor), ] { let mut p = Project::empty(); let owner = p .add_element(Kind::Scenario, layer, "Caller", None) .unwrap(); let other = p .add_element(Kind::Scenario, layer, "Definition", None) .unwrap(); let work = p.add_element(work_kind, layer, "Respond", None).unwrap(); let unused = p.add_element(work_kind, layer, "Respond", None).unwrap(); let participant = p .add_element(participant_kind, layer, "Operator", None) .unwrap(); let lanes = ["First role", "Second role"].map(|name| { let id = p .add_element(Kind::Lifeline, layer, name, Some(owner)) .unwrap(); *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Lifeline { represented: Some(participant), }; id }); let first = add_occurrence( &mut p, owner, "First action", OccurrenceBody::Action { action: Some(work), lifeline: Some(lanes[0]), }, ); let fragment = add_occurrence( &mut p, owner, "Decision", OccurrenceBody::Fragment { operator: FragmentOperator::Alternative, branches: vec![], }, ); let branch = add_occurrence( &mut p, fragment, "When ready", OccurrenceBody::Branch { guard: "ready".into(), occurrences: vec![], }, ); if let Some(SharedBody::Occurrence { body: OccurrenceBody::Fragment { branches, .. }, .. }) = p.elements.get_mut(&fragment).unwrap().shared_mut() { branches.push(branch); } let nested = add_occurrence( &mut p, branch, "Again", OccurrenceBody::Action { action: Some(work), lifeline: Some(lanes[1]), }, ); let inside = add_occurrence( &mut p, other, "Hidden inside definition", OccurrenceBody::Action { action: Some(unused), lifeline: None, }, ); let use_a = add_occurrence( &mut p, owner, "Use A", OccurrenceBody::ScenarioUse { scenario: Some(other), bindings: vec![], }, ); let use_b = add_occurrence( &mut p, owner, "Use B", OccurrenceBody::ScenarioUse { scenario: Some(other), bindings: vec![], }, ); let before = p.clone(); let state = State::default(); assert_eq!( targets(&state, &p, owner, work), BTreeSet::from([first, nested]) ); assert_eq!( targets(&state, &p, owner, participant), BTreeSet::from(lanes) ); assert_eq!( targets(&state, &p, owner, other), BTreeSet::from([use_a, use_b]) ); assert!(targets(&state, &p, owner, unused).is_empty()); assert!(targets(&state, &p, owner, inside).is_empty()); assert_eq!(targets(&state, &p, owner, branch), BTreeSet::from([branch])); assert_eq!( state.diagram_usages(&p, owner, owner)[0].target, Target::Whole ); assert_eq!(p, before); } } #[test] fn scenario_usage_keeps_repeated_messages_and_reveal_is_read_only_and_revalidated() { let f = super::super::direct_tests::fixture(); let mut p = f.project; let mut state = State::default(); assert_eq!( targets(&state, &p, f.owner, f.exchange), BTreeSet::from([f.call, f.reply]) ); let before = p.clone(); let target = Target::ScenarioObject(f.reply); state.viewport(Size::new(900., 500.)); assert!(state.reveal_usage(&p, Id::new_v4(), f.owner, &target)); let usage = state .diagram_usages(&p, f.owner, f.reply) .into_iter() .find(|u| u.target == target) .unwrap(); let zoom = state .presentation .scale(Scene::sequence(&p, f.owner, f.reply).size); let centered = Point::new( usage.bounds.center_x() * zoom, usage.bounds.center_y() * zoom, ) + state.presentation.pan; assert!((centered.x - 450.).abs() < 0.01 && (centered.y - 250.).abs() < 0.01); assert!(zoom >= 0.5); assert_eq!(p, before); // Keeping an orphan semantic occurrence does not keep its painted target. p.elements .get_mut(&f.owner) .unwrap() .steps_mut() .unwrap() .retain(|id| *id != f.reply); let camera = state.camera_boundary(); assert!(!state.reveal_usage(&p, Id::new_v4(), f.owner, &target)); assert!(camera == state.camera_boundary()); assert!(!state.reveal_usage(&p, Id::new_v4(), f.owner, &Target::Edge(f.exchange))); // A message whose endpoints are incomplete paints only its repair box. if let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { receiver, .. }, .. }) = p.elements.get_mut(&f.call).unwrap().shared_mut() { *receiver = None; } assert!(targets(&state, &p, f.owner, f.exchange).is_empty()); assert_eq!( targets(&state, &p, f.owner, f.call), BTreeSet::from([f.call]) ); } #[test] fn scenario_operation_label_membership_excludes_ellipsis_only_and_normalizes_whitespace() { assert!(label_field_visible( "1. call · X · Operation: I / O", "1. call · X · Operation: ", "I" )); assert!(!label_field_visible( "1234567890123456789012345678901Ixyz", "1234567890123456789012345678901", "Ixyz" )); assert!(!label_field_visible( "A very long occurrence label · Operation: I", "A very long occurrence label · Operation: ", "I" )); assert!(label_field_visible( "call\n\t· Operation: 界", "call\n\t· Operation: ", "界" )); assert!(!label_field_visible("Operation: ", "Operation: ", "")); } #[test] fn scenario_usage_only_reports_the_resolved_operation_fields_actually_in_the_label() { let mut p = persistence::decode(include_bytes!(concat!( env!("CARGO_MANIFEST_DIR"), "/../../harness/fixtures/interface-operation-probe/output/boundary-only.json" ))) .unwrap(); let id = |n| Id::parse_str(&format!("52060906-0000-4000-8000-{n:012x}")).unwrap(); let (owner, call, exchange, allocation) = (id(40), id(43), id(34), id(30)); let (interface, item) = ( p.relations[&allocation].source(), p.relations[&allocation].target(), ); for (id, name) in [(call, "x"), (interface, "I"), (item, "O")] { p.elements.get_mut(&id).unwrap().name = name.into(); } p.relations.get_mut(&exchange).unwrap().name = "x".into(); p.relations.get_mut(&allocation).unwrap().name = "A".into(); if let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { operation, .. }, .. }) = p.elements.get_mut(&call).unwrap().shared_mut() { *operation = Some(OperationUse { allocation: Some(allocation), }); } assert!(matches!( operations::resolve(&p, call), Ok(operations::OperationResolution::Selected(_)) )); let state = State::default(); let before = p.clone(); assert_eq!( targets(&state, &p, owner, interface), BTreeSet::from([call]) ); assert_eq!(targets(&state, &p, owner, item), BTreeSet::from([call])); assert!(targets(&state, &p, owner, allocation).is_empty()); assert_eq!(p, before); p.elements.get_mut(&call).unwrap().name = "This occurrence has a deliberately long name".into(); assert!(targets(&state, &p, owner, interface).is_empty()); assert!(targets(&state, &p, owner, item).is_empty()); }