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 { 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 { 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)); }