387 lines
13 KiB
Rust
387 lines
13 KiB
Rust
//! 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);
|
|
}
|