498 lines
17 KiB
Rust
498 lines
17 KiB
Rust
use super::*;
|
|
|
|
struct Composite {
|
|
f: Fixture,
|
|
child: Id,
|
|
child_node: Id,
|
|
unplaced: Id,
|
|
}
|
|
|
|
fn composite(layer: Layer) -> Composite {
|
|
let mut f = fixture(layer);
|
|
let kind = f.p.elements[&f.w].kind();
|
|
let child =
|
|
f.p.add_element(kind, layer, "Placed child", Some(f.w))
|
|
.unwrap();
|
|
let unplaced =
|
|
f.p.add_element(kind, layer, "Unplaced leaf", Some(f.w))
|
|
.unwrap();
|
|
let child_node = f.p.place(f.d, child, xy(140., 600.));
|
|
validate_candidate(&f.p).unwrap();
|
|
Composite {
|
|
f,
|
|
child,
|
|
child_node,
|
|
unplaced,
|
|
}
|
|
}
|
|
|
|
fn expected_allocation(id: Id, alias: &str, name: &str, performer: Id, work: Id) -> Relation {
|
|
Relation {
|
|
id,
|
|
alias: alias.into(),
|
|
name: name.into(),
|
|
description: String::new(),
|
|
body: RelationBody::Allocation {
|
|
source: performer,
|
|
target: work,
|
|
},
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn group_assignment_checks_original_identity_and_visibility_before_normalization() {
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
let c = composite(layer);
|
|
let valid = BTreeMap::from([(c.f.n, c.f.w), (c.child_node, c.child)]);
|
|
let admitted = assignment::capture(&c.f.p, c.f.d, &valid, c.f.n).unwrap();
|
|
assert_eq!(admitted.selected(), &valid);
|
|
let foreign = c.f.p.diagrams[&c.f.second]
|
|
.nodes
|
|
.values()
|
|
.find(|node| node.element == c.f.w)
|
|
.unwrap()
|
|
.id;
|
|
// Each extra member is semantically covered by the valid selected parent.
|
|
// Normalizing first would incorrectly hide its invalid original identity.
|
|
for invalid in [
|
|
BTreeMap::from([(c.f.n, c.f.w), (c.child_node, c.unplaced)]),
|
|
BTreeMap::from([(c.f.n, c.f.w), (Id::new_v4(), c.child)]),
|
|
BTreeMap::from([(c.f.n, c.f.w), (foreign, c.f.w)]),
|
|
] {
|
|
assert!(matches!(
|
|
assignment::capture(&c.f.p, c.f.d, &invalid, c.f.n),
|
|
Err(Refusal::StaleScope)
|
|
));
|
|
}
|
|
let mut folded = c.f.p.clone();
|
|
folded
|
|
.diagrams
|
|
.get_mut(&c.f.d)
|
|
.unwrap()
|
|
.nodes
|
|
.get_mut(&c.f.n)
|
|
.unwrap()
|
|
.collapsed = true;
|
|
validate_candidate(&folded).unwrap();
|
|
assert!(
|
|
!projection::project_view(&folded, c.f.d)
|
|
.nodes
|
|
.iter()
|
|
.any(|node| node.id == c.child_node)
|
|
);
|
|
// Hidden descendants remain in Q when only the visible parent is selected.
|
|
let parent_only =
|
|
assignment::capture(&folded, c.f.d, &BTreeMap::from([(c.f.n, c.f.w)]), c.f.n).unwrap();
|
|
assert!(parent_only.affected().contains_key(&c.child_node));
|
|
assert!(matches!(
|
|
assignment::capture(&folded, c.f.d, &valid, c.f.n),
|
|
Err(Refusal::StaleScope)
|
|
));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn group_assignment_normalizes_parent_child_and_assigns_unplaced_leaf_once() {
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
let c = composite(layer);
|
|
let selected = BTreeMap::from([(c.f.n, c.f.w), (c.child_node, c.child)]);
|
|
let scope = assignment::capture(&c.f.p, c.f.d, &selected, c.f.n).unwrap();
|
|
assert_eq!(scope.selected(), &selected);
|
|
assert_eq!(
|
|
scope.affected().keys().copied().collect::<BTreeSet<_>>(),
|
|
BTreeSet::from([c.f.n, c.child_node])
|
|
);
|
|
assert_eq!(
|
|
scope.leaves().keys().copied().collect::<BTreeSet<_>>(),
|
|
BTreeSet::from([c.child, c.unplaced])
|
|
);
|
|
let child_allocation = Id::new_v4();
|
|
let unplaced_allocation = Id::new_v4();
|
|
let ids = BTreeMap::from([
|
|
(c.child, child_allocation),
|
|
(c.unplaced, unplaced_allocation),
|
|
]);
|
|
let plan = assignment::plan_semantics(&scope, Destination::Performer(c.f.a), &ids).unwrap();
|
|
assert_eq!(plan.changed_count(), 2);
|
|
let mut expected = c.f.p.clone();
|
|
expected.relations.insert(
|
|
child_allocation,
|
|
expected_allocation(
|
|
child_allocation,
|
|
"a_performs_placed_child",
|
|
"A performs Placed child",
|
|
c.f.a,
|
|
c.child,
|
|
),
|
|
);
|
|
expected.relations.insert(
|
|
unplaced_allocation,
|
|
expected_allocation(
|
|
unplaced_allocation,
|
|
"a_performs_unplaced_leaf",
|
|
"A performs Unplaced leaf",
|
|
c.f.a,
|
|
c.unplaced,
|
|
),
|
|
);
|
|
assert_eq!(plan.project(), &expected);
|
|
let placed = assignment::place(
|
|
&plan,
|
|
xy(140., 200.),
|
|
presentation(&c.f.p, c.f.d, Some(c.f.a)),
|
|
)
|
|
.unwrap();
|
|
let mut clipped = presentation(&c.f.p, c.f.d, Some(c.f.a));
|
|
clipped.viewport.size.height = 450.; // bottom350: parent header fits; selected child ends362.
|
|
for (id, should_fit) in [(c.f.n, true), (c.child_node, false)] {
|
|
let node = placed.view.nodes.iter().find(|node| node.id == id).unwrap();
|
|
let header = Rect {
|
|
position: node.position,
|
|
size: Size {
|
|
width: node.size.width,
|
|
height: 62.,
|
|
},
|
|
};
|
|
assert_eq!(clipped.viewport.contains(header), should_fit);
|
|
}
|
|
assert!(
|
|
matches!(
|
|
assignment::place(&plan, xy(140., 200.), clipped),
|
|
Err(Refusal::LayoutRequired(_))
|
|
),
|
|
"normalized-away selected child still requires its header"
|
|
);
|
|
let d = expected.diagrams.get_mut(&c.f.d).unwrap();
|
|
d.nodes.get_mut(&c.f.n).unwrap().position = xy(140., 200.);
|
|
d.nodes.get_mut(&c.child_node).unwrap().position = xy(180., 300.);
|
|
assert_eq!(placed.delta, xy(40., -300.));
|
|
assert_eq!(
|
|
roundtrip(
|
|
&c.f.p,
|
|
assignment::finish(&plan, &c.f.p, &placed).unwrap().unwrap()
|
|
),
|
|
expected
|
|
);
|
|
assert_eq!(
|
|
expected.diagrams[&c.f.d].nodes.len(),
|
|
c.f.p.diagrams[&c.f.d].nodes.len()
|
|
);
|
|
assert!(
|
|
!expected.diagrams[&c.f.d]
|
|
.nodes
|
|
.values()
|
|
.any(|node| node.element == c.unplaced)
|
|
);
|
|
assert!(
|
|
!expected
|
|
.relations
|
|
.values()
|
|
.any(|relation| relation.kind() == RelationKind::Allocation
|
|
&& relation.target() == c.f.w)
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn group_assignment_distributed_parent_refuses_but_its_independent_leaves_can_reassign() {
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
let mut c = composite(layer);
|
|
let second_node = c.f.p.place(c.f.d, c.unplaced, xy(940., 200.));
|
|
let performer =
|
|
c.f.p
|
|
.add_element(
|
|
if layer == Layer::Oa {
|
|
Kind::Actor
|
|
} else {
|
|
Kind::SystemActor
|
|
},
|
|
layer,
|
|
"C",
|
|
None,
|
|
)
|
|
.unwrap();
|
|
c.f.p.place(c.f.d, performer, xy(1700., 100.));
|
|
let d = c.f.p.diagrams.get_mut(&c.f.d).unwrap();
|
|
d.nodes.get_mut(&c.f.n).unwrap().position = xy(1700., 800.);
|
|
d.nodes.get_mut(&c.child_node).unwrap().position = xy(140., 200.);
|
|
let ax =
|
|
c.f.p
|
|
.relate(RelationKind::Allocation, "A child", c.f.a, c.child);
|
|
let by =
|
|
c.f.p
|
|
.relate(RelationKind::Allocation, "B child", c.f.b, c.unplaced);
|
|
validate_candidate(&c.f.p).unwrap();
|
|
let visible = projection::project_view(&c.f.p, c.f.d);
|
|
for id in [c.f.n, c.child_node, second_node] {
|
|
assert!(visible.nodes.iter().any(|node| node.id == id));
|
|
}
|
|
assert_eq!(scope(&c.f).uniform, Uniform::MixedOrUnresolved);
|
|
assert!(matches!(
|
|
assignment::capture(&c.f.p, c.f.d, &BTreeMap::from([(c.f.n, c.f.w)]), c.f.n,),
|
|
Err(Refusal::MixedLayoutRequired)
|
|
));
|
|
let selected = BTreeMap::from([(c.child_node, c.child), (second_node, c.unplaced)]);
|
|
let captured = assignment::capture(&c.f.p, c.f.d, &selected, c.child_node).unwrap();
|
|
let plan = assignment::plan_semantics(
|
|
&captured,
|
|
Destination::Performer(performer),
|
|
&BTreeMap::new(),
|
|
)
|
|
.unwrap();
|
|
let mut expected = c.f.p.clone();
|
|
expected.relations.get_mut(&ax).unwrap().body = RelationBody::Allocation {
|
|
source: performer,
|
|
target: c.child,
|
|
};
|
|
expected.relations.get_mut(&by).unwrap().body = RelationBody::Allocation {
|
|
source: performer,
|
|
target: c.unplaced,
|
|
};
|
|
assert_eq!(plan.project(), &expected);
|
|
// One delta moves both leaves. The unselected semantic parent remains
|
|
// authored at (1700,800), while its derived enclosure may change.
|
|
let placed = assignment::place(
|
|
&plan,
|
|
xy(1740., 120.),
|
|
presentation(&c.f.p, c.f.d, Some(performer)),
|
|
)
|
|
.unwrap();
|
|
let d = expected.diagrams.get_mut(&c.f.d).unwrap();
|
|
d.nodes.get_mut(&c.child_node).unwrap().position = xy(1740., 120.);
|
|
d.nodes.get_mut(&second_node).unwrap().position = xy(2540., 120.);
|
|
assert_eq!(
|
|
roundtrip(
|
|
&c.f.p,
|
|
assignment::finish(&plan, &c.f.p, &placed).unwrap().unwrap()
|
|
),
|
|
expected
|
|
);
|
|
}
|
|
}
|
|
|
|
fn separate_unassigned(layer: Layer) -> (Fixture, Id, Id) {
|
|
let mut f = fixture(layer);
|
|
let companion =
|
|
f.p.add_element(f.p.elements[&f.w].kind(), layer, "Companion", None)
|
|
.unwrap();
|
|
let companion_node = f.p.place(f.d, companion, xy(100., 1000.));
|
|
validate_candidate(&f.p).unwrap();
|
|
(f, companion, companion_node)
|
|
}
|
|
|
|
#[test]
|
|
fn group_assignment_finish_rejects_stale_project_and_each_tampered_result() {
|
|
let (f, companion, companion_node) = separate_unassigned(Layer::Oa);
|
|
let selected = BTreeMap::from([(f.n, f.w), (companion_node, companion)]);
|
|
let captured = assignment::capture(&f.p, f.d, &selected, f.n).unwrap();
|
|
let plan =
|
|
assignment::plan_semantics(&captured, Destination::OutsidePerformers, &BTreeMap::new())
|
|
.unwrap();
|
|
let context = presentation(&f.p, f.d, None);
|
|
let valid = assignment::place(&plan, xy(500., 500.), context).unwrap();
|
|
let mut expected = f.p.clone();
|
|
expected
|
|
.diagrams
|
|
.get_mut(&f.d)
|
|
.unwrap()
|
|
.nodes
|
|
.get_mut(&f.n)
|
|
.unwrap()
|
|
.position = xy(500., 500.);
|
|
expected
|
|
.diagrams
|
|
.get_mut(&f.d)
|
|
.unwrap()
|
|
.nodes
|
|
.get_mut(&companion_node)
|
|
.unwrap()
|
|
.position = xy(500., 1000.);
|
|
assert_eq!(
|
|
roundtrip(
|
|
&f.p,
|
|
assignment::finish(&plan, &f.p, &valid).unwrap().unwrap()
|
|
),
|
|
expected
|
|
);
|
|
let mut stale = f.p.clone();
|
|
stale.elements.get_mut(&f.b).unwrap().description = "Changed unrelated accepted record".into();
|
|
validate_candidate(&stale).unwrap();
|
|
assert!(matches!(
|
|
assignment::finish(&plan, &stale, &valid),
|
|
Err(Refusal::StaleScope)
|
|
));
|
|
for field in 0..6 {
|
|
let mut altered = assignment::place(&plan, xy(500., 500.), context).unwrap();
|
|
match field {
|
|
0 => altered.diagram.nodes.get_mut(&f.n).unwrap().position.x += 20.,
|
|
1 => {
|
|
altered
|
|
.view
|
|
.nodes
|
|
.iter_mut()
|
|
.find(|node| node.id == f.n)
|
|
.unwrap()
|
|
.position
|
|
.x += 20.
|
|
}
|
|
2 => altered.delta.x += 20.,
|
|
3 => altered.unfolded = Some(f.n),
|
|
4 => altered.commands.clear(),
|
|
5 => altered.requested.x += 10.,
|
|
_ => unreachable!(),
|
|
}
|
|
assert!(
|
|
matches!(
|
|
assignment::finish(&plan, &f.p, &altered),
|
|
Err(Refusal::StaleScope)
|
|
),
|
|
"tampered result field {field}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn group_assignment_requires_non_anchor_selected_header_inside_viewport() {
|
|
let (f, companion, companion_node) = separate_unassigned(Layer::Oa);
|
|
let captured = assignment::capture(
|
|
&f.p,
|
|
f.d,
|
|
&BTreeMap::from([(f.n, f.w), (companion_node, companion)]),
|
|
f.n,
|
|
)
|
|
.unwrap();
|
|
let plan =
|
|
assignment::plan_semantics(&captured, Destination::OutsidePerformers, &BTreeMap::new())
|
|
.unwrap();
|
|
let wide = presentation(&f.p, f.d, None);
|
|
let placed = assignment::place(&plan, xy(500., 500.), wide).unwrap();
|
|
let mut narrow = wide;
|
|
narrow.viewport.size.height = 800.; // world bottom700: anchor header fits, companion does not.
|
|
let header = |id| {
|
|
let node = placed.view.nodes.iter().find(|node| node.id == id).unwrap();
|
|
Rect {
|
|
position: node.position,
|
|
size: Size {
|
|
width: node.size.width,
|
|
height: 62.,
|
|
},
|
|
}
|
|
};
|
|
assert!(narrow.viewport.contains(header(f.n)));
|
|
assert!(!narrow.viewport.contains(header(companion_node)));
|
|
// Only the viewport differs from the already admitted identical geometry.
|
|
assert!(matches!(
|
|
assignment::place(&plan, xy(500., 500.), narrow),
|
|
Err(Refusal::LayoutRequired(_))
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn group_assignment_unresolved_oa_role_leaf_accepts_explicit_target_but_not_outside() {
|
|
for ambiguous in [false, true] {
|
|
let (mut f, companion, companion_node) = separate_unassigned(Layer::Oa);
|
|
let (role_id, role_assignment) = role(&mut f, None);
|
|
if ambiguous {
|
|
f.p.relate(
|
|
RelationKind::RoleRealization,
|
|
"A realizes responsibility",
|
|
f.a,
|
|
role_id,
|
|
);
|
|
f.p.relate(
|
|
RelationKind::RoleRealization,
|
|
"B realizes responsibility",
|
|
f.b,
|
|
role_id,
|
|
);
|
|
}
|
|
validate_candidate(&f.p).unwrap();
|
|
assert_eq!(validation::effective_performer(&f.p, f.w), None);
|
|
let captured = assignment::capture(
|
|
&f.p,
|
|
f.d,
|
|
&BTreeMap::from([(f.n, f.w), (companion_node, companion)]),
|
|
f.n,
|
|
)
|
|
.unwrap();
|
|
let Refusal::RoleExitRequired {
|
|
work,
|
|
roles,
|
|
performers,
|
|
..
|
|
} = assignment::plan_semantics(&captured, Destination::OutsidePerformers, &BTreeMap::new())
|
|
.unwrap_err()
|
|
else {
|
|
panic!("unresolved role must produce its specific outward refusal");
|
|
};
|
|
assert_eq!(work, f.w);
|
|
assert_eq!(roles, vec![role_id]);
|
|
assert_eq!(
|
|
performers.into_iter().collect::<BTreeSet<_>>(),
|
|
if ambiguous {
|
|
BTreeSet::from([f.a, f.b])
|
|
} else {
|
|
BTreeSet::new()
|
|
}
|
|
);
|
|
let work_allocation = Id::new_v4();
|
|
let companion_allocation = Id::new_v4();
|
|
let ids = BTreeMap::from([(f.w, work_allocation), (companion, companion_allocation)]);
|
|
let plan =
|
|
assignment::plan_semantics(&captured, Destination::Performer(f.b), &ids).unwrap();
|
|
assert_eq!(plan.changed_count(), 2);
|
|
assert!(plan.label().contains("role responsibility retained"));
|
|
let mut expected = f.p.clone();
|
|
expected.relations.insert(
|
|
work_allocation,
|
|
expected_allocation(
|
|
work_allocation,
|
|
"b_performs_work",
|
|
"B performs Work",
|
|
f.b,
|
|
f.w,
|
|
),
|
|
);
|
|
expected.relations.insert(
|
|
companion_allocation,
|
|
expected_allocation(
|
|
companion_allocation,
|
|
"b_performs_companion",
|
|
"B performs Companion",
|
|
f.b,
|
|
companion,
|
|
),
|
|
);
|
|
assert_eq!(plan.project(), &expected);
|
|
assert_eq!(
|
|
plan.project().relations[&role_assignment],
|
|
f.p.relations[&role_assignment]
|
|
);
|
|
let placed =
|
|
assignment::place(&plan, xy(940., 200.), presentation(&f.p, f.d, Some(f.b))).unwrap();
|
|
expected
|
|
.diagrams
|
|
.get_mut(&f.d)
|
|
.unwrap()
|
|
.nodes
|
|
.get_mut(&f.n)
|
|
.unwrap()
|
|
.position = xy(940., 200.);
|
|
expected
|
|
.diagrams
|
|
.get_mut(&f.d)
|
|
.unwrap()
|
|
.nodes
|
|
.get_mut(&companion_node)
|
|
.unwrap()
|
|
.position = xy(940., 700.);
|
|
assert_eq!(
|
|
roundtrip(
|
|
&f.p,
|
|
assignment::finish(&plan, &f.p, &placed).unwrap().unwrap()
|
|
),
|
|
expected
|
|
);
|
|
}
|
|
}
|