Files
ARCADIA-Studio/crates/oa-desktop/src/allocation_drag/assignment_boundary_tests.rs
T

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