//! Independent allocation membership oracles. Expected targets and coordinates //! come from handwritten fixtures, never from the usage predicate under test. use super::{Surface, Target, Usage, saved_usages, saved_usages_for}; use iced::Rectangle; use oa_core::{ Id, Kind, Layer, Position, Project, ProjectionKind, RelationKind, Size, projection, validation, }; struct AllocationFixture { project: Project, diagram: Id, performer: Id, work: Id, relation: Id, performer_node: Id, work_node: Id, } fn assert_valid(project: &Project) { let errors: Vec<_> = validation::validate(project) .into_iter() .filter(|issue| issue.severity == validation::Severity::Error) .collect(); assert!( errors.is_empty(), "fixture must be structurally valid: {errors:?}" ); } fn fixture() -> AllocationFixture { let mut project = Project::empty(); let performer = project .add_element(Kind::Entity, Layer::Oa, "Rescue team", None) .unwrap(); let work = project .add_element(Kind::Activity, Layer::Oa, "Dispatch", None) .unwrap(); let relation = project.relate(RelationKind::Allocation, "Performs", performer, work); let diagram = project.ensure_diagram(Layer::Oa); project.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Allocation; let performer_node = project.place(diagram, performer, Position { x: 20., y: 40. }); let work_node = project.place(diagram, work, Position { x: 240., y: 320. }); project .diagrams .get_mut(&diagram) .unwrap() .nodes .get_mut(&work_node) .unwrap() .size = Size { width: 320., height: 110., }; assert_valid(&project); AllocationFixture { project, diagram, performer, work, relation, performer_node, work_node, } } fn expected(f: &AllocationFixture) -> Usage { Usage { subject: f.relation, surface: Surface::Saved(f.diagram), target: Target::AllocationGrouping { relation: f.relation, work_placement: f.work_node, performer_placement: f.performer_node, }, role: "allocation grouping".into(), bounds: Rectangle { x: 240., y: 320., width: 320., height: 64., }, } } /// Two semantic decomposition levels, both summaries of the one allocated leaf. /// No Allocation relation targets either summary. fn nest_under_summaries(f: &mut AllocationFixture) -> (Id, Id, Id, Id) { let top = f .project .add_element(Kind::Activity, Layer::Oa, "Respond", None) .unwrap(); let middle = f .project .add_element(Kind::Activity, Layer::Oa, "Coordinate", Some(top)) .unwrap(); f.project.elements.get_mut(&f.work).unwrap().parent = Some(middle); let top_node = f .project .place(f.diagram, top, Position { x: 70., y: 120. }); let middle_node = f .project .place(f.diagram, middle, Position { x: 130., y: 210. }); assert_valid(&f.project); (top, middle, top_node, middle_node) } #[test] fn allocation_grouping_has_exact_relation_placements_and_readable_work_geometry() { let f = fixture(); let before = f.project.clone(); let view = projection::project_view(&f.project, f.diagram); let work = view .nodes .iter() .find(|node| node.id == f.work_node) .unwrap(); assert_eq!(work.visual_parent, Some(f.performer_node)); assert_eq!(work.position, Position { x: 240., y: 320. }); assert_eq!( work.size, Size { width: 320., height: 110. } ); assert!(!view.edges.iter().any(|edge| edge.id == f.relation)); assert_eq!(saved_usages(&f.project, f.relation), vec![expected(&f)]); assert_eq!( saved_usages_for(&f.project, f.relation, Some(f.diagram)), vec![expected(&f)] ); assert!(saved_usages_for(&f.project, f.relation, Some(Id::new_v4())).is_empty()); // The semantic work and its placement remain ordinary node selections. for subject in [f.work, f.work_node] { let usages = saved_usages(&f.project, subject); assert_eq!(usages.len(), 1); assert_eq!(usages[0].target, Target::Placement(f.work_node)); } assert_eq!(f.project, before); } #[test] fn allocation_grouping_walks_two_visible_summary_levels_without_inventing_allocations() { let mut f = fixture(); let (top, middle, top_node, middle_node) = nest_under_summaries(&mut f); let before = f.project.clone(); let view = projection::project_view(&f.project, f.diagram); for (node_id, parent, summary) in [ (f.work_node, Some(middle_node), false), (middle_node, Some(top_node), true), (top_node, Some(f.performer_node), true), ] { let node = view.nodes.iter().find(|node| node.id == node_id).unwrap(); assert_eq!(node.visual_parent, parent); assert_eq!(node.allocation_summary, summary); } assert_eq!(f.project.relations.len(), 1); assert_eq!(saved_usages(&f.project, f.relation), vec![expected(&f)]); for (element, node) in [(top, top_node), (middle, middle_node)] { let usages = saved_usages(&f.project, element); assert_eq!(usages.len(), 1); assert_eq!(usages[0].target, Target::Placement(node)); } assert_eq!(f.project, before); } #[test] fn folded_summary_or_performer_does_not_substitute_a_visible_ancestor_for_the_leaf() { let mut f = fixture(); let (_, _, top_node, middle_node) = nest_under_summaries(&mut f); for folded in [f.performer_node, top_node, middle_node] { let mut project = f.project.clone(); project .diagrams .get_mut(&f.diagram) .unwrap() .nodes .get_mut(&folded) .unwrap() .collapsed = true; assert_valid(&project); let before = project.clone(); let view = projection::project_view(&project, f.diagram); assert!(view.nodes.iter().any(|node| node.id == folded)); assert!(!view.nodes.iter().any(|node| node.id == f.work_node)); assert!(saved_usages(&project, f.relation).is_empty()); assert_eq!(project, before); } } #[test] fn absent_endpoint_placements_and_hidden_performer_do_not_create_grouping_targets() { let mut f = fixture(); let (_, _, top_node, _) = nest_under_summaries(&mut f); for absent in [f.performer_node, f.work_node] { let mut project = f.project.clone(); project .diagrams .get_mut(&f.diagram) .unwrap() .nodes .remove(&absent); assert_valid(&project); let before = project.clone(); let view = projection::project_view(&project, f.diagram); // The aggregate card remains visible even when the actual leaf or its // actual performer has no placement. It cannot stand in for that end. assert!( view.nodes .iter() .any(|node| node.id == top_node && node.allocation_summary) ); assert!(saved_usages(&project, f.relation).is_empty()); assert_eq!(project, before); } let outer = f .project .add_element(Kind::Entity, Layer::Oa, "Organization", None) .unwrap(); f.project.elements.get_mut(&f.performer).unwrap().parent = Some(outer); let outer_node = f .project .place(f.diagram, outer, Position { x: -40., y: -60. }); f.project .diagrams .get_mut(&f.diagram) .unwrap() .nodes .get_mut(&outer_node) .unwrap() .collapsed = true; assert_valid(&f.project); let before = f.project.clone(); let view = projection::project_view(&f.project, f.diagram); assert!(view.nodes.iter().any(|node| node.id == outer_node)); assert!(!view.nodes.iter().any(|node| node.id == f.performer_node)); assert!(saved_usages(&f.project, f.relation).is_empty()); assert_eq!(f.project, before); } #[test] fn collapsed_leaf_and_edge_filter_still_leave_the_actual_nesting_visible() { let mut f = fixture(); let diagram = f.project.diagrams.get_mut(&f.diagram).unwrap(); diagram.nodes.get_mut(&f.work_node).unwrap().collapsed = true; // hidden_edges controls arrows. Allocation is drawn as nesting, which is // still visible; do not mistake the filter for a hidden node/grouping. diagram.view_options.hidden_edges.push(f.relation); assert_valid(&f.project); let before = f.project.clone(); let view = projection::project_view(&f.project, f.diagram); let work = view .nodes .iter() .find(|node| node.id == f.work_node) .unwrap(); assert!(work.collapsed); assert_eq!(work.visual_parent, Some(f.performer_node)); assert_eq!(saved_usages(&f.project, f.relation), vec![expected(&f)]); assert_eq!(f.project, before); } #[test] fn other_projections_do_not_report_allocation_nesting_when_only_an_arrow_or_nothing_is_drawn() { let mut f = fixture(); f.project.diagrams.get_mut(&f.diagram).unwrap().projection = ProjectionKind::Overview; assert_valid(&f.project); let before = f.project.clone(); let view = projection::project_view(&f.project, f.diagram); assert!(view.edges.iter().any(|edge| edge.id == f.relation)); assert!(view.nodes.iter().all(|node| node.visual_parent.is_none())); let usages = saved_usages(&f.project, f.relation); assert_eq!(usages.len(), 1); assert_eq!(usages[0].subject, f.relation); assert_eq!(usages[0].surface, Surface::Saved(f.diagram)); assert_eq!(usages[0].target, Target::Edge(f.relation)); assert_eq!(f.project, before); f.project.diagrams.get_mut(&f.diagram).unwrap().projection = ProjectionKind::FunctionalDataflow; assert_valid(&f.project); let before = f.project.clone(); let view = projection::project_view(&f.project, f.diagram); assert!(view.nodes.iter().any(|node| node.id == f.work_node)); assert!(!view.nodes.iter().any(|node| node.id == f.performer_node)); assert!(saved_usages(&f.project, f.relation).is_empty()); assert_eq!(f.project, before); } #[test] fn repeated_saved_views_keep_allocation_placement_identity_and_geometry_independent() { let mut f = fixture(); // The first diagram is Allocation, so this creates a different Overview. let second = f.project.ensure_diagram(Layer::Oa); assert_ne!(second, f.diagram); f.project.diagrams.get_mut(&second).unwrap().projection = ProjectionKind::Allocation; let second_performer = f .project .place(second, f.performer, Position { x: 900., y: 600. }); let second_work = f .project .place(second, f.work, Position { x: 1000., y: 700. }); f.project .diagrams .get_mut(&second) .unwrap() .nodes .get_mut(&second_work) .unwrap() .size = Size { width: 520., height: 130., }; assert_ne!(second_performer, f.performer_node); assert_ne!(second_work, f.work_node); assert_valid(&f.project); let before = f.project.clone(); let other = Usage { subject: f.relation, surface: Surface::Saved(second), target: Target::AllocationGrouping { relation: f.relation, work_placement: second_work, performer_placement: second_performer, }, role: "allocation grouping".into(), bounds: Rectangle { x: 1000., y: 700., width: 420., height: 64., }, }; let usages = saved_usages(&f.project, f.relation); assert_eq!(usages.len(), 2); assert!(usages.contains(&expected(&f))); assert!(usages.contains(&other)); assert_eq!( saved_usages_for(&f.project, f.relation, Some(second)), vec![other] ); assert_eq!(f.project, before); } #[test] fn role_derived_nesting_without_a_direct_allocation_does_not_invent_one() { let mut f = fixture(); f.project.relations.remove(&f.relation); let role = f .project .add_element(Kind::Role, Layer::Oa, "Dispatcher", None) .unwrap(); let realization = f.project.relate( RelationKind::RoleRealization, "Realizes dispatcher", f.performer, role, ); let assignment = f .project .relate(RelationKind::RoleAssignment, "Dispatches", role, f.work); assert_valid(&f.project); let before = f.project.clone(); let view = projection::project_view(&f.project, f.diagram); let work = view .nodes .iter() .find(|node| node.id == f.work_node) .unwrap(); assert_eq!(work.visual_parent, Some(f.performer_node)); assert!(work.allocation_summary); assert!( f.project .relations .values() .all(|relation| relation.kind() != RelationKind::Allocation) ); for subject in [f.relation, realization, assignment] { assert!(saved_usages(&f.project, subject).is_empty()); } let usages = saved_usages(&f.project, f.work); assert_eq!(usages.len(), 1); assert_eq!(usages[0].target, Target::Placement(f.work_node)); assert_eq!(f.project, before); }