use super::*; use crate::document::Document; use oa_core::{ language, source_project::{ProjectLimits, SourceBundle}, validation, }; fn id(p: &Project, alias: &str) -> Id { p.identities() .into_iter() .find(|(_, name)| *name == alias) .unwrap() .0 } fn fixture(lifetime: bool) -> (Document, Id) { let (name, input, owner) = if lifetime { ("lifetime.arcadia", LIFETIME, "lifetime_conversation") } else { ("fixture.arcadia", FIXTURE, "conversation") }; let p = language::parse(input).expect("existing real Arcadia fixture"); let errors: Vec<_> = validation::validate(&p) .into_iter() .filter(|issue| issue.severity == validation::Severity::Error) .collect(); assert!(errors.is_empty(), "{errors:?}"); let bundle = SourceBundle { entry: name.into(), files: BTreeMap::from([(name.into(), input.into())]), }; let document = Document::from_bundle(bundle, ProjectLimits::default()).unwrap(); assert_eq!(document.project(), &p); assert_eq!(language::parse(&language::format(&p)).unwrap(), p); let owner = id(&p, owner); (document, owner) } fn passive(document: &Document, before: &Project, source: &SourceBundle) { assert_eq!(document.project(), before); assert_eq!(document.bundle(), source); assert!(!document.dirty()); assert!(!document.can_undo() && !document.can_redo()); } fn row(scene: &Scene, id: Id) -> f32 { scene.rows[&id].1 } fn line(scene: &Scene, id: Id) -> &Line { assert_eq!(scene.lines.iter().filter(|line| line.id == id).count(), 1); scene.lines.iter().find(|line| line.id == id).unwrap() } fn named_box(scene: &Scene, id: Id) -> &BoxItem { scene .boxes .iter() .find(|item| item.id == id && !item.title.is_empty()) .unwrap() } fn same_scene(a: &Scene, b: &Scene) { assert!( !a.activations.is_empty(), "the complete typed comparison must be exercised" ); assert_eq!(a.activations, b.activations); assert_eq!(a.rows, b.rows); assert_eq!(a.sequence_bounds, b.sequence_bounds); assert_eq!(a.size, b.size); assert_eq!(a.layout_origin, b.layout_origin); assert_eq!(a.lanes, b.lanes); assert_eq!(a.render_lanes, b.render_lanes); assert_eq!(a.lifecycle, b.lifecycle); assert_eq!(a.open_calls, b.open_calls); let boxes = |s: &Scene| { s.boxes .iter() .map(|b| (b.id, b.rect, b.title.clone(), b.detail.clone(), b.marker)) .collect::>() }; let lines = |s: &Scene| { s.lines .iter() .map(|l| { ( l.id, l.points.clone(), l.label_at, l.label.clone(), l.dashed, ) }) .collect::>() }; let slots = |s: &Scene| { s.slots .iter() .map(|slot| { ( slot.slot.parent, slot.slot.before, slot.y, slot.label.clone(), ) }) .collect::>() }; assert_eq!(boxes(a), boxes(b)); assert_eq!(lines(a), lines(b)); assert_eq!(slots(a), slots(b)); } const FIXTURE: &str = r###"arcadia 7; project layout_probe "Behavior layout Scene probe" id "10000000-0000-4000-8000-000000000001" { oa_entity sender "Sender" id "10000000-0000-4000-8000-000000000002" {} oa_entity receiver "Receiver" id "10000000-0000-4000-8000-000000000003" {} oa_activity inspect "Inspect" id "10000000-0000-4000-8000-000000000004" {} allocation inspect_assignment "Receiver performs inspection" id "10000000-0000-4000-8000-000000000005" { source @receiver; target @inspect; } communication_mean channel "Shared channel" id "10000000-0000-4000-8000-000000000006" { source @sender; target @receiver; oriented false; } oa_scenario conversation "Conversation" id "10000000-0000-4000-8000-000000000007" { viewpoint ParticipantInteraction; steps [@first, @second, @fragment, @self_call, @self_reply]; } oa_lifeline left "Sender role" id "10000000-0000-4000-8000-000000000008" { parent @conversation; represented @sender; } oa_lifeline right "Receiver role" id "10000000-0000-4000-8000-000000000009" { parent @conversation; represented @receiver; } oa_occurrence first "First delivery" id "10000000-0000-4000-8000-00000000000a" { parent @conversation; body { Exchange { exchange @channel; sender @left; receiver @right; message_kind Asynchronous; }; }; } oa_occurrence second "Second delivery" id "10000000-0000-4000-8000-00000000000b" { parent @conversation; body { Exchange { exchange @channel; sender @left; receiver @right; message_kind Asynchronous; }; }; } oa_occurrence fragment "Optional inspection" id "10000000-0000-4000-8000-00000000000c" { parent @conversation; body { Fragment { operator Optional; branches [@operand]; }; }; } oa_occurrence operand "When requested" id "10000000-0000-4000-8000-00000000000d" { parent @fragment; body { Branch { guard "requested"; occurrences [@nested]; }; }; } oa_occurrence nested "Inspection occurrence" id "10000000-0000-4000-8000-00000000000e" { parent @operand; body { Action { action @inspect; lifeline @right; }; }; } // Self calls intentionally have no global self-edge: such a relation is // invalid in the current metamodel. Their unassigned exchange is a known // incomplete-model warning, while explicit lifelines make Scene drawable. oa_occurrence self_call "Local call" id "10000000-0000-4000-8000-00000000000f" { parent @conversation; body { Exchange { exchange none; sender @right; receiver @right; message_kind Call; }; }; } oa_occurrence self_reply "Local reply" id "10000000-0000-4000-8000-000000000010" { parent @conversation; body { Exchange { exchange none; sender @right; receiver @right; message_kind Reply; reply_to @self_call; }; }; } } "###; const LIFETIME: &str = r###"arcadia 7; // Separate lifetime probe: preserve this authored note and all stable IDs. project lifetime_probe "Delayed receiver lifetime" id "20000000-0000-4000-8000-000000000001" { oa_activity prepare "Prepare request" id "20000000-0000-4000-8000-000000000002" {} oa_activity serve "Serve request" id "20000000-0000-4000-8000-000000000003" {} interaction service "Service request" id "20000000-0000-4000-8000-000000000004" { source @prepare; target @serve; } oa_scenario lifetime_conversation "Receiver created on demand" id "20000000-0000-4000-8000-000000000005" { viewpoint OperationalActivity; steps [@before_birth, @create_receiver, @service_call, @delete_receiver]; } oa_lifeline requester "Requester" id "20000000-0000-4000-8000-000000000006" { parent @lifetime_conversation; represented @prepare; } oa_lifeline provider "Provider" id "20000000-0000-4000-8000-000000000007" { parent @lifetime_conversation; represented @serve; } oa_occurrence before_birth "Prepare before receiver exists" id "20000000-0000-4000-8000-000000000008" { parent @lifetime_conversation; body { Action { action @prepare; lifeline @requester; }; }; } oa_occurrence create_receiver "Create receiver" id "20000000-0000-4000-8000-000000000009" { parent @lifetime_conversation; body { Exchange { exchange @service; sender @requester; receiver @provider; message_kind Create; }; }; } oa_occurrence service_call "Call receiver" id "20000000-0000-4000-8000-00000000000a" { parent @lifetime_conversation; body { Exchange { exchange @service; sender @requester; receiver @provider; message_kind Call; }; }; } // Death ends the visible activation but deliberately does not invent a Reply. oa_occurrence delete_receiver "Delete receiver" id "20000000-0000-4000-8000-00000000000b" { parent @lifetime_conversation; body { Exchange { exchange @service; sender @requester; receiver @provider; message_kind Delete; }; }; } } "###; fn saved(p: &mut Project, owner: Id, layout: Option) -> Id { let view = Id::new_v4(); p.diagrams.insert( view, oa_core::Diagram { id: view, alias: format!("view_{}", p.diagrams.len()), name: "Saved conversation".into(), description: String::new(), layer: p.elements[&owner].layer(), projection: oa_core::ProjectionKind::Scenario { scenario: owner }, view_options: Default::default(), nodes: BTreeMap::new(), routes: vec![], layout: layout.map(DiagramLayout::Scenario), }, ); view } fn positions(messages: &[(Id, f32)], lanes: &[(Id, f32)]) -> ScenarioLayout { ScenarioLayout { lanes: lanes .iter() .map(|(id, x)| oa_core::ScenarioLanePosition { lifeline: *id, x: *x, }) .collect(), messages: messages .iter() .map(|(id, y)| oa_core::ScenarioMessagePosition { address: oa_core::OccurrenceEndpoint { references: vec![], occurrence: *id, }, send_y: *y, }) .collect(), } } fn stored(p: Project) -> Document { let errors: Vec<_> = validation::validate(&p) .into_iter() .filter(|issue| issue.severity == validation::Severity::Error) .collect(); assert!(errors.is_empty(), "{errors:?}"); let text = format!( "// Preserve source and identities through passive resolution.\n{}", language::format(&p) ); let document = Document::from_bundle( SourceBundle { entry: "saved.arcadia".into(), files: BTreeMap::from([("saved.arcadia".into(), text)]), }, ProjectLimits::default(), ) .unwrap(); assert_eq!(document.project(), &p); document } fn result(p: &Project, view: Id) -> ResolvedScenarioPlan { resolve(p, Representation::Saved { diagram: view }, view) .unwrap_or_else(|issues| panic!("{issues:?}")) } fn failures(p: &Project, view: Id) -> Vec { match resolve(p, Representation::Saved { diagram: view }, view) { Ok(_) => panic!("expected explicit unsupported geometry"), Err(issues) => issues, } } fn contains(outer: Rectangle, inner: Rectangle) -> bool { outer.x <= inner.x && outer.y <= inner.y && outer.x + outer.width >= inner.x + inner.width && outer.y + outer.height >= inner.y + inner.height } #[test] fn absent_empty_and_explicit_baseline_saved_values_preserve_complete_scene_and_source() { for lifetime in [false, true] { let (document, owner) = fixture(lifetime); let mut p = document.project().clone(); let baseline = Scene::sequence(&p, owner, owner); let messages: Vec<_> = baseline .lines .iter() .map(|line| (line.id, line.points[0].y)) .collect(); let lane_values: Vec<_> = baseline.lanes.iter().map(|(id, x)| (*id, *x)).collect(); let views = [ saved(&mut p, owner, None), saved(&mut p, owner, Some(ScenarioLayout::default())), saved(&mut p, owner, Some(positions(&messages, &lane_values))), ]; let document = stored(p); let p = document.project(); let before = p.clone(); let source = document.bundle().clone(); for view in views { let resolved = result(p, view); assert_eq!( resolved.representation, Representation::Saved { diagram: view } ); assert_eq!(resolved.owner, owner); same_scene(&baseline, &resolved.scene); } let derived = resolve(p, Representation::Derived { owner }, owner) .unwrap_or_else(|e| panic!("{e:?}")); same_scene(&baseline, &derived.scene); passive(&document, &before, &source); } } #[test] fn independent_saved_appearances_of_reused_exchange_have_exact_anchors_and_no_ambient_state() { let (document, owner) = fixture(false); let mut p = document.project().clone(); let first = id(&p, "first"); let second = id(&p, "second"); let a = saved(&mut p, owner, Some(positions(&[(second, 200.)], &[]))); let b = saved(&mut p, owner, Some(positions(&[(first, 1000.)], &[]))); let document = stored(p); let p = document.project(); let before = p.clone(); let source = document.bundle().clone(); let a1 = result(p, a); let b1 = result(p, b); let a2 = result(p, a); assert_eq!( (row(&a1.scene, first), row(&a1.scene, second)), (148., 200.) ); assert_eq!( (row(&b1.scene, first), row(&b1.scene, second)), (1000., 240.) ); assert_eq!( line(&a1.scene, second) .points .iter() .map(|p| p.y) .collect::>(), vec![200., 200.] ); assert!( b1.plan .diagnostics .contains(&Diagnostic::SourceOrderCrossing { scope: owner, before: first, after: second }) ); assert!(b1.bounds.y + b1.bounds.height >= 1008.); same_scene(&a1.scene, &a2.scene); same_scene( &Scene::sequence(p, owner, owner), &resolve(p, Representation::Derived { owner }, owner) .unwrap_or_else(|e| panic!("{e:?}")) .scene, ); passive(&document, &before, &source); } #[test] fn authored_lanes_precede_headers_arrows_lifetimes_and_complete_bounds() { let (document, owner) = fixture(true); let mut p = document.project().clone(); let requester = id(&p, "requester"); let provider = id(&p, "provider"); let create = id(&p, "create_receiver"); let call = id(&p, "service_call"); let view = saved( &mut p, owner, Some(positions(&[], &[(requester, 900.), (provider, -500.)])), ); let document = stored(p); let p = document.project(); let before = p.clone(); let source = document.bundle().clone(); let resolved = result(p, view); let scene = &resolved.scene; assert_eq!(named_box(scene, provider).rect.x, -602.); assert_eq!( line(scene, create).points, vec![Point::new(900., 243.), Point::new(-430., 243.)] ); assert_eq!( line(scene, call).points, vec![Point::new(900., 335.), Point::new(-494., 335.)] ); assert_eq!( scene.lifecycle.marker_at(Point::new(-500., 243.)), Some(create) ); assert_eq!(scene.activations[0].rect.x, -506.); for item in &scene.boxes { assert!(contains(resolved.bounds, item.rect)); } for bar in &scene.activations { assert!(contains(resolved.bounds, bar.rect)); } assert_eq!(resolved.scene.layout_origin.x, -660.); assert_eq!(resolved.bounds.x, -660.); assert_eq!(resolved.bounds.x + resolved.bounds.width, 1140.); passive(&document, &before, &source); } #[test] fn same_x_distinct_lifelines_are_not_self_messages_in_either_bridge() { let (document, owner) = fixture(true); let mut p = document.project().clone(); let requester = id(&p, "requester"); let provider = id(&p, "provider"); let lanes = BTreeMap::from([(requester, 160.), (provider, 160.)]); let baseline = Scene::sequence_geometry(&p, owner, owner, Some(&lanes), None); assert_eq!(line(&baseline, id(&p, "create_receiver")).points.len(), 2); assert_eq!(line(&baseline, id(&p, "create_receiver")).points[0].y, 243.); let view = saved( &mut p, owner, Some(positions(&[], &[(requester, 160.), (provider, 160.)])), ); assert!( failures(&p, view) .iter() .any(|e| matches!(e, GeometryIssue::CoincidentDistinctEndpoints(_))) ); } #[test] fn self_call_receive_and_reply_send_drive_the_full_typed_execution() { let (document, owner) = fixture(false); let mut p = document.project().clone(); let call = id(&p, "self_call"); let reply = id(&p, "self_reply"); let receiver = id(&p, "right"); let view = saved(&mut p, owner, Some(positions(&[(call, 610.)], &[]))); let resolved = result(&p, view); let x = resolved.lanes[&receiver]; assert_eq!( line(&resolved.scene, call).points, vec![ Point::new(x, 610.), Point::new(x + 66., 610.), Point::new(x + 66., 634.), Point::new(x + 6., 634.) ] ); assert_eq!( resolved.scene.activations, vec![scenario_activation::Segment { call, receiver, scope: owner, rect: Rectangle::new(Point::new(x - 6., 634.), Size::new(12., 94.)), column: 0, finish: scenario_activation::Finish::Reply(reply), }] ); assert_eq!(line(&resolved.scene, reply).points[0].y, 728.); assert_eq!( resolved.plan.messages[&call], MessageAnchors { send_y: 610., receive_y: 634. } ); } #[test] fn moved_death_clips_actual_bar_and_preserves_unanswered_cue() { let (document, owner) = fixture(true); let mut p = document.project().clone(); let call = id(&p, "service_call"); let delete = id(&p, "delete_receiver"); let provider = id(&p, "provider"); let view = saved(&mut p, owner, Some(positions(&[(delete, 380.)], &[]))); let document = stored(p); let p = document.project(); let before = p.clone(); let source = document.bundle().clone(); let resolved = result(p, view); let x = resolved.lanes[&provider]; assert_eq!( resolved.scene.activations, vec![scenario_activation::Segment { call, receiver: provider, scope: owner, rect: Rectangle::new(Point::new(x - 6., 335.), Size::new(12., 45.)), column: 0, finish: scenario_activation::Finish::Terminated(delete), }] ); assert_eq!(resolved.scene.open_calls, vec![Point::new(x + 10., 373.)]); assert_eq!( resolved .scene .lifecycle .alive_spans(provider, owner, 0., 1000.) .iter() .map(|s| (s.start, s.end)) .collect::>(), vec![(243., 380.)] ); assert_eq!( resolved.scene.lifecycle.marker_at(Point::new(x, 380.)), Some(delete) ); passive(&document, &before, &source); } #[test] fn explicit_root_scope_failures_do_not_clamp_calls_or_lifecycle_but_async_can_be_negative() { let (document, owner) = fixture(true); let mut p = document.project().clone(); let create = id(&p, "create_receiver"); let call = id(&p, "service_call"); for y in [50., 84.] { let view = saved(&mut p, owner, Some(positions(&[(create, y)], &[]))); assert_eq!( failures(&p, view), vec![GeometryIssue::LifecycleAnchorOutsideRenderScope { occurrence: create, scope: owner }] ); } let view = saved(&mut p, owner, Some(positions(&[(create, 85.)], &[]))); assert_eq!(result(&p, view).scene.rows[&create].1, 85.); let view = saved(&mut p, owner, Some(positions(&[(call, -10.)], &[]))); assert_eq!( failures(&p, view), vec![GeometryIssue::TypedActivationOutsideScope { call, scope: owner }] ); let (document, owner) = fixture(false); let mut p = document.project().clone(); let first = id(&p, "first"); let view = saved(&mut p, owner, Some(positions(&[(first, -10.)], &[]))); let resolved = result(&p, view); assert_eq!(line(&resolved.scene, first).points[0].y, -10.); assert!(resolved.bounds.y <= -36.); } #[test] fn reply_crossing_is_a_specific_typed_failure() { let (document, owner) = fixture(false); let mut p = document.project().clone(); let call = id(&p, "self_call"); let reply = id(&p, "self_reply"); let view = saved(&mut p, owner, Some(positions(&[(reply, 600.)], &[]))); assert_eq!( failures(&p, view), vec![GeometryIssue::TypedActivationCrossingNotSupported { call, event: reply }] ); } #[test] fn complete_scope_plan_rejects_escape_preserves_overlap_and_keeps_headers_and_followers() { // Internal complete-plan oracle: observations are automatic in the persisted // first slice, but its dependent scope math must retain the proven boundary. let (document, owner) = fixture(false); let p = document.project(); let baseline = Scene::sequence(p, owner, owner); let nested = id(p, "nested"); let frame = id(p, "fragment"); let operand = id(p, "operand"); assert_eq!( Plan::build(p, owner, &baseline, &Candidate::from([(nested, 353.)])).unwrap_err(), GeometryIssue::OperandScopeOutsideEnclosingInterval { operand, fragment: frame } ); let plan = Plan::build(p, owner, &baseline, &Candidate::from([(nested, 380.)])).unwrap(); let scene = Scene::sequence_geometry(p, owner, owner, None, Some(&plan)); assert_eq!(named_box(&scene, nested).rect.y, 360.); assert_eq!(scene.sequence_bounds[&operand], (338., 530.)); assert_eq!( named_box(&scene, frame).rect, named_box(&baseline, frame).rect ); assert_eq!( scene .slots .iter() .find(|slot| slot.slot.before == Some(frame)) .unwrap() .y, 290. ); assert!(!scene.activations.is_empty()); assert_eq!(scene.activations, baseline.activations); for (id, row) in &baseline.rows { if *id != nested { assert_eq!(scene.rows[id], *row); } } } #[test] fn unfinished_coordinate_is_latent_and_zero_or_signed_zero_never_rewrites_source() { let (document, owner) = fixture(false); let mut p = document.project().clone(); let second = id(&p, "second"); if let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { receiver, .. }, .. }) = p.elements.get_mut(&second).unwrap().shared_mut() { *receiver = None; } let left = id(&p, "left"); let view = saved( &mut p, owner, Some(positions(&[(second, -0.)], &[(left, -0.)])), ); let document = stored(p); let p = document.project(); let before = p.clone(); let source = document.bundle().clone(); let resolved = result(p, view); assert_eq!(resolved.latent_messages, vec![second]); assert!(!resolved.scene.lines.iter().any(|line| line.id == second)); assert_eq!(resolved.scene.rows[&second].1, 240.); assert_eq!(named_box(&resolved.scene, second).rect.y, 220.); passive(&document, &before, &source); } #[test] fn self_lifecycle_saved_send_is_converted_once_to_the_actual_receive_marker() { let (document, owner) = fixture(true); let mut p = document.project().clone(); let create = id(&p, "create_receiver"); let delete = id(&p, "delete_receiver"); let provider = id(&p, "provider"); for event in [create, delete] { if let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { sender, exchange, .. }, .. }) = p.elements.get_mut(&event).unwrap().shared_mut() { *sender = Some(provider); *exchange = None; } } let view = saved( &mut p, owner, Some(positions(&[(create, 200.), (delete, 400.)], &[])), ); let document = stored(p); let p = document.project(); let before = p.clone(); let source = document.bundle().clone(); let resolved = result(p, view); let x = resolved.lanes[&provider]; for (event, send, receive) in [(create, 200., 224.), (delete, 400., 424.)] { let points = &line(&resolved.scene, event).points; assert_eq!(points.len(), 4); assert_eq!(points[0].y, send); assert_eq!(points[3].y, receive); assert_eq!(resolved.scene.rows[&event].1, receive); assert_eq!( resolved.plan.messages[&event], MessageAnchors { send_y: send, receive_y: receive } ); assert_eq!( resolved.scene.lifecycle.marker_at(Point::new(x, receive)), Some(event) ); } assert_eq!( resolved .scene .lifecycle .alive_spans(provider, owner, 0., 1000.) .iter() .map(|s| (s.start, s.end)) .collect::>(), vec![(224., 424.)] ); assert_eq!(resolved.scene.activations.len(), 1); assert_eq!( resolved.scene.activations[0].finish, scenario_activation::Finish::Terminated(delete) ); passive(&document, &before, &source); } #[test] fn timer_uses_implicit_effective_receiver_and_true_self_offset() { let (document, owner) = fixture(false); let mut p = document.project().clone(); let first = id(&p, "first"); if let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { receiver, exchange, message_kind, .. }, .. }) = p.elements.get_mut(&first).unwrap().shared_mut() { *receiver = None; *exchange = None; *message_kind = MessageKind::Timer { expression: "after 10 ms".into(), }; } let view = saved(&mut p, owner, Some(positions(&[(first, 180.)], &[]))); let document = stored(p); let resolved = result(document.project(), view); let points = &line(&resolved.scene, first).points; assert_eq!(points.len(), 4); assert_eq!((points[0].y, points[3].y), (180., 204.)); assert_eq!(points[0].x, points[3].x); assert_eq!( resolved.plan.messages[&first], MessageAnchors { send_y: 180., receive_y: 204. } ); } #[test] fn latent_message_becomes_exact_only_when_its_own_endpoint_is_completed() { let (document, owner) = fixture(false); let mut p = document.project().clone(); let first = id(&p, "first"); let second = id(&p, "second"); let receiver = id(&p, "right"); if let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { receiver, .. }, .. }) = p.elements.get_mut(&second).unwrap().shared_mut() { *receiver = None; } let view = saved(&mut p, owner, Some(positions(&[(second, 200.)], &[]))); let pending = result(&p, view); assert_eq!(pending.latent_messages, vec![second]); let authored = p.diagrams[&view].layout.clone(); if let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { receiver: target, .. }, .. }) = p.elements.get_mut(&second).unwrap().shared_mut() { *target = Some(receiver); } let document = stored(p); let complete = result(document.project(), view); assert!(complete.latent_messages.is_empty()); assert_eq!( line(&complete.scene, second) .points .iter() .map(|point| point.y) .collect::>(), vec![200., 200.] ); assert_eq!(line(&complete.scene, first).points[0].y, 148.); assert_eq!(document.project().diagrams[&view].layout, authored); } #[test] fn wide_gap_bounds_include_all_actual_shapes_and_typed_extensions() { let (document, owner) = fixture(false); let mut p = document.project().clone(); let left = id(&p, "left"); let right = id(&p, "right"); let call = id(&p, "self_call"); let view = saved( &mut p, owner, Some(positions(&[], &[(left, -1_000_000.), (right, 1_000_000.)])), ); let resolved = result(&p, view); assert_eq!(resolved.bounds.x, -1_000_160.); assert_eq!(resolved.bounds.x + resolved.bounds.width, 1_000_240.); let frame = named_box(&resolved.scene, id(&p, "fragment")).rect; assert!(frame.x < resolved.lanes[&left] && frame.x + frame.width > resolved.lanes[&right]); assert_eq!(line(&resolved.scene, call).points[1].x, 1_000_066.); assert!(!resolved.scene.activations.is_empty()); for segment in &resolved.scene.activations { assert!(contains(resolved.bounds, segment.rect)); } let (document, owner) = fixture(false); let mut p = document.project().clone(); // The same owner also admits a separate representation with one shifted lane. let right = id(&p, "right"); let view = saved(&mut p, owner, Some(positions(&[], &[(right, 900.)]))); let resolved = result(&p, view); assert!( resolved.bounds.x + resolved.bounds.width >= named_box(&resolved.scene, right).rect.x + 204. ); } #[test] fn earlier_receiver_death_and_prebirth_call_are_named_typed_failures_not_disappearing_bars() { let (document, owner) = fixture(true); let mut p = document.project().clone(); let call = id(&p, "service_call"); let delete = id(&p, "delete_receiver"); let provider = id(&p, "provider"); let view = saved(&mut p, owner, Some(positions(&[(delete, 300.)], &[]))); assert_eq!( failures(&p, view), vec![GeometryIssue::TypedActivationCrossingNotSupported { call, event: delete }] ); let view = saved(&mut p, owner, Some(positions(&[(call, 200.)], &[]))); assert_eq!( failures(&p, view), vec![GeometryIssue::TypedActivationOutsideLifetime { call, receiver: provider }] ); } #[test] fn unsupported_expansion_duplicate_keys_and_foreign_identity_fail_without_mutating_input() { let (document, owner) = fixture(false); let mut p = document.project().clone(); let first = id(&p, "first"); let mut layout = positions(&[(first, 200.), (first, 300.)], &[]); layout.messages[0].address.references.push(owner); let view = saved(&mut p, owner, Some(layout)); let before = p.clone(); let issues = failures(&p, view); assert!(issues.contains(&GeometryIssue::ExpandedAddressNotSupported(first))); assert!(issues.contains(&GeometryIssue::DuplicateCoordinate(first))); assert_eq!(p, before); assert_eq!( failures(&p, Id::nil()), vec![GeometryIssue::InvalidRepresentation(Id::nil())] ); } #[test] fn saved_direct_nested_message_uses_operand_scopes_without_reparenting_or_moving_followers() { let (document, owner) = fixture(false); let mut p = document.project().clone(); let nested = id(&p, "nested"); let operand = id(&p, "operand"); let frame = id(&p, "fragment"); let first = id(&p, "first"); let body = occurrence(&p, first).unwrap().clone(); if let Some(SharedBody::Occurrence { body: target, .. }) = p.elements.get_mut(&nested).unwrap().shared_mut() { *target = body; } let baseline = Scene::sequence(&p, owner, owner); let view = saved(&mut p, owner, Some(positions(&[(nested, 380.)], &[]))); let document = stored(p); let p = document.project(); let before = p.clone(); let source = document.bundle().clone(); let resolved = result(p, view); assert_eq!( line(&resolved.scene, nested) .points .iter() .map(|point| point.y) .collect::>(), vec![380., 380.] ); assert_eq!(resolved.scene.rows[&nested].0, operand); assert_eq!(resolved.scene.sequence_bounds[&operand].0, 338.); assert_eq!( named_box(&resolved.scene, frame).rect, named_box(&baseline, frame).rect ); for (id, row) in &baseline.rows { if *id != nested { assert_eq!(resolved.scene.rows[id], *row); } } assert!(!baseline.activations.is_empty()); assert_eq!(resolved.scene.activations, baseline.activations); passive(&document, &before, &source); } #[test] fn exact_authored_lane_bits_survive_headers_and_tiny_distinct_lanes_are_not_coincident() { let (document, owner) = fixture(false); let mut p = document.project().clone(); let left = id(&p, "left"); let right = id(&p, "right"); let first = id(&p, "first"); let view = saved( &mut p, owner, Some(positions(&[], &[(left, 0.1), (right, 400.)])), ); let resolved = result(&p, view); assert_eq!(resolved.lanes[&left].to_bits(), 0.1_f32.to_bits()); assert_eq!( line(&resolved.scene, first).points[0].x.to_bits(), 0.1_f32.to_bits() ); let view = saved( &mut p, owner, Some(positions(&[], &[(left, 0.), (right, 0.000001)])), ); let resolved = result(&p, view); assert_eq!(resolved.lanes[&left].to_bits(), 0_f32.to_bits()); assert_eq!(resolved.lanes[&right].to_bits(), 0.000001_f32.to_bits()); assert_ne!(resolved.lanes[&left], resolved.lanes[&right]); let points = &line(&resolved.scene, first).points; assert_eq!(points.len(), 2); assert_eq!(points[1].x.to_bits(), 0.000001_f32.to_bits()); } fn alternatives_with_death() -> (Document, Id, Id, Id, Id, Id) { let (document, owner) = fixture(false); let mut p = document.project().clone(); let frame = id(&p, "fragment"); let branch1 = id(&p, "operand"); let first = id(&p, "first"); let second = id(&p, "second"); let delete = id(&p, "self_reply"); let right = id(&p, "right"); let left = id(&p, "left"); // Reuse admitted occurrence identities with a second actual operand. Delete // occupies the former call's sibling identity; the former reply is the event. let branch2 = id(&p, "self_call"); let channel = id(&p, "channel"); if let Some(SharedBody::Scenario { steps: occurrences, .. }) = p.elements.get_mut(&owner).unwrap().shared_mut() { *occurrences = vec![frame, delete]; } *p.elements.get_mut(&frame).unwrap().shared_mut().unwrap() = SharedBody::Occurrence { body: OccurrenceBody::Fragment { operator: oa_core::FragmentOperator::Alternative, branches: vec![branch1, branch2], }, precondition: String::new(), postcondition: String::new(), }; for (branch, child) in [(branch1, first), (branch2, second)] { let e = p.elements.get_mut(&branch).unwrap(); e.parent = Some(frame); *e.shared_mut().unwrap() = SharedBody::Occurrence { body: OccurrenceBody::Branch { guard: "choice".into(), occurrences: vec![child], }, precondition: String::new(), postcondition: String::new(), }; p.elements.get_mut(&child).unwrap().parent = Some(branch); } p.elements.remove(&id(document.project(), "nested")); let template = occurrence(&p, first).unwrap().clone(); let mut death = template; if let OccurrenceBody::Exchange { exchange, sender, receiver, message_kind, reply_to, .. } = &mut death { *exchange = Some(channel); *sender = Some(left); *receiver = Some(right); *message_kind = MessageKind::Delete; *reply_to = None; } *p.elements.get_mut(&delete).unwrap().shared_mut().unwrap() = SharedBody::Occurrence { body: death, precondition: String::new(), postcondition: String::new(), }; (stored(p), owner, first, second, delete, right) } #[test] fn newly_skipped_sibling_scope_is_rejected_using_real_projector_diagnostics() { let (document, owner, first, second, _, _) = alternatives_with_death(); let mut p = document.project().clone(); let baseline = Scene::sequence(&p, owner, owner); assert_eq!( (baseline.rows[&first].1, baseline.rows[&second].1), (293., 475.) ); assert!(baseline.lifecycle.missing_geometry().is_empty()); let plan = Plan::build(&p, owner, &baseline, &Candidate::from([(first, 500.)])).unwrap(); let unsupported = Scene::sequence_geometry(&p, owner, owner, None, Some(&plan)); let missing = unsupported.lifecycle.missing_geometry(); assert!(!missing.is_empty()); let view = saved(&mut p, owner, Some(positions(&[(first, 500.)], &[]))); let failures = failures(&p, view); for (lifeline, scope) in missing { assert!( failures.contains(&GeometryIssue::LifecycleGeometryNotSupported { lifeline, scope }) ); } } #[test] fn post_fragment_death_cannot_disappear_and_feasible_operand_spacing_retains_its_marker() { let (document, owner, _, second, delete, right) = alternatives_with_death(); let mut p = document.project().clone(); let baseline = Scene::sequence(&p, owner, owner); let operand = p.elements[&second].parent.unwrap(); let fragment = p.elements[&operand].parent.unwrap(); let baseline_frame = baseline .boxes .iter() .find(|item| item.id == fragment) .unwrap() .rect; assert_eq!( (baseline_frame.y, baseline_frame.y + baseline_frame.height), (148., 567.) ); assert_eq!(baseline.sequence_bounds[&operand], (433., 525.)); let x = baseline.render_lanes[&right]; assert_eq!(baseline.rows[&delete].1, 631.); assert_eq!( baseline.lifecycle.marker_at(Point::new(x, 631.)), Some(delete) ); let view = saved(&mut p, owner, Some(positions(&[(second, 700.)], &[]))); assert!( failures(&p, view) .iter() .any(|issue| matches!(issue, GeometryIssue::LifecycleGeometryNotSupported { .. })) ); let view = saved(&mut p, owner, Some(positions(&[(second, 520.)], &[]))); let resolved = result(&p, view); // The message still lies above the old frame bottom, but its padded // operand would end at 570. Retain the automatic 92px frame padding. assert_eq!(resolved.scene.rows[&second].1, 520.); assert_eq!(resolved.plan.scopes[&operand], (433., 570.)); let frame = resolved.plan.frames[&fragment]; assert_eq!((frame.y, frame.y + frame.height), (148., 612.)); assert_eq!(resolved.scene.sequence_bounds[&operand], (433., 570.)); assert_eq!( resolved .scene .boxes .iter() .find(|item| item.id == fragment) .unwrap() .rect, frame ); assert!(resolved.scene.lifecycle.missing_geometry().is_empty()); assert_eq!( resolved.scene.lifecycle.marker_at(Point::new(x, 631.)), Some(delete) ); assert_eq!(resolved.scene.rows[&delete].1, 631.); } #[test] fn authored_send_bits_survive_baseline_delta_and_self_lifecycle_row_conversion() { let (document, owner) = fixture(false); let mut p = document.project().clone(); let first = id(&p, "first"); for send in [0.1_f32, 0.000001_f32] { let view = saved(&mut p, owner, Some(positions(&[(first, send)], &[]))); let resolved = result(&p, view); assert_eq!( resolved.plan.messages[&first].send_y.to_bits(), send.to_bits() ); assert_eq!( line(&resolved.scene, first).points[0].y.to_bits(), send.to_bits() ); assert_eq!( line(&resolved.scene, first).points[1].y.to_bits(), send.to_bits() ); } let (document, owner) = fixture(true); let mut p = document.project().clone(); let create = id(&p, "create_receiver"); let provider = id(&p, "provider"); if let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { sender, exchange, .. }, .. }) = p.elements.get_mut(&create).unwrap().shared_mut() { *sender = Some(provider); *exchange = None; } let send = f32::from_bits(64_f32.to_bits() - 1); assert_ne!(((send + 24.) - 24.).to_bits(), send.to_bits()); let view = saved(&mut p, owner, Some(positions(&[(create, send)], &[]))); let document = stored(p); let p = document.project(); let before = p.clone(); let source = document.bundle().clone(); let resolved = result(p, view); assert_eq!( resolved.plan.messages[&create].send_y.to_bits(), send.to_bits() ); assert_eq!( line(&resolved.scene, create).points[0].y.to_bits(), send.to_bits() ); assert_eq!( line(&resolved.scene, create).points[3].y.to_bits(), (send + 24.).to_bits() ); assert_eq!( resolved.scene.rows[&create].1.to_bits(), (send + 24.).to_bits() ); passive(&document, &before, &source); } #[test] fn decimal_call_anchor_is_admitted_and_exact_in_plan_line_and_typed_activation() { let (document, owner) = fixture(false); let mut p = document.project().clone(); let first = id(&p, "first"); let call = id(&p, "second"); let reply = id(&p, "self_reply"); let left = id(&p, "left"); let right = id(&p, "right"); for alias in ["fragment", "operand", "nested", "self_call"] { p.elements.remove(&id(document.project(), alias)); } *p.elements.get_mut(&owner).unwrap().steps_mut().unwrap() = vec![first, call, reply]; if let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { message_kind, .. }, .. }) = p.elements.get_mut(&call).unwrap().shared_mut() { *message_kind = MessageKind::Call; } let mut response = occurrence(&p, call).unwrap().clone(); if let OccurrenceBody::Exchange { sender, receiver, message_kind, reply_to, .. } = &mut response { *sender = Some(right); *receiver = Some(left); *message_kind = MessageKind::Reply; *reply_to = Some(call); } *p.elements.get_mut(&reply).unwrap().shared_mut().unwrap() = SharedBody::Occurrence { body: response, precondition: String::new(), postcondition: String::new(), }; let baseline = Scene::sequence(&p, owner, owner); assert_eq!( ( baseline.rows[&first].1, baseline.rows[&call].1, baseline.rows[&reply].1 ), (148., 240., 332.) ); let send = 100.1_f32; let view = saved( &mut p, owner, Some(positions(&[(first, 90.), (call, send)], &[])), ); let document = stored(p); let p = document.project(); let before = p.clone(); let source = document.bundle().clone(); let resolved = result(p, view); assert_eq!( resolved.plan.messages[&call].send_y.to_bits(), send.to_bits() ); assert_eq!( resolved.plan.messages[&call].receive_y.to_bits(), send.to_bits() ); assert_eq!( line(&resolved.scene, call).points[0].y.to_bits(), send.to_bits() ); assert_eq!( line(&resolved.scene, call).points[1].y.to_bits(), send.to_bits() ); assert_eq!(resolved.scene.activations.len(), 1); assert_eq!( resolved.scene.activations[0].rect.y.to_bits(), send.to_bits() ); assert_eq!( resolved.scene.activations[0].finish, scenario_activation::Finish::Reply(reply) ); passive(&document, &before, &source); }