Verify OA and SA Arrange policy boundaries with focused production modules
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
//! Focused test harness for the production Allocation planner and Document.
|
||||||
|
//! This does not exercise App input, painting, or native gestures.
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(dead_code)] // App-facing entry points are intentionally absent here.
|
||||||
|
#[path = "../src/allocation_drag.rs"]
|
||||||
|
mod allocation_drag;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(dead_code, unused_imports)] // App-facing API and reexports are absent here.
|
||||||
|
#[path = "../src/document.rs"]
|
||||||
|
mod document;
|
||||||
|
|
||||||
|
fn main() {}
|
||||||
@@ -833,4 +833,5 @@ fn exact_project(expected: &Project, actual: &Project) -> Result<(), Refusal> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
#[path = "allocation_drag/tests.rs"]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
//! Unaccepted mixed-Arrange experiment. No production admission is changed.
|
//! Reviewed policy experiment using the production projection and translator.
|
||||||
|
//! No production admission or atomic-refusal behavior is tested here.
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
struct Mixed {
|
struct Mixed {
|
||||||
@@ -7,52 +8,98 @@ struct Mixed {
|
|||||||
xn: Id,
|
xn: Id,
|
||||||
yn: Id,
|
yn: Id,
|
||||||
internal: Id,
|
internal: Id,
|
||||||
|
include_performer: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mixed(collapsed: bool, obstacle: bool) -> Mixed {
|
fn mixed(layer: Layer, collapsed: bool, obstacle: bool, include_performer: bool) -> Mixed {
|
||||||
let mut f = fixture(Layer::Oa);
|
let mut f = fixture(layer);
|
||||||
let x = f.p.add_element(Kind::Activity, Layer::Oa, "X", Some(f.w)).unwrap();
|
let work = f.p.elements[&f.w].kind();
|
||||||
let y = f.p.add_element(Kind::Activity, Layer::Oa, "Y", Some(f.w)).unwrap();
|
let exchange = match layer {
|
||||||
let outside = f.p.add_element(Kind::Activity, Layer::Oa, "Outside", None).unwrap();
|
Layer::Oa => RelationKind::Interaction,
|
||||||
|
Layer::Sa => RelationKind::FunctionalExchange,
|
||||||
|
};
|
||||||
|
let x = f.p.add_element(work, layer, "X", Some(f.w)).unwrap();
|
||||||
|
let y = f.p.add_element(work, layer, "Y", Some(f.w)).unwrap();
|
||||||
|
let outside = f.p.add_element(work, layer, "Outside", None).unwrap();
|
||||||
let xn = f.p.place(f.d, x, xy(140., 200.));
|
let xn = f.p.place(f.d, x, xy(140., 200.));
|
||||||
let yn = f.p.place(f.d, y, xy(940., 200.));
|
let yn = f.p.place(f.d, y, xy(940., 200.));
|
||||||
f.p.place(f.d, outside, xy(1800., 800.));
|
f.p.place(f.d, outside, xy(1800., 800.));
|
||||||
f.p.relate(RelationKind::Allocation, "A owns X", f.a, x);
|
f.p.relate(RelationKind::Allocation, "A owns X", f.a, x);
|
||||||
f.p.relate(RelationKind::Allocation, "B owns Y", f.b, y);
|
f.p.relate(RelationKind::Allocation, "B owns Y", f.b, y);
|
||||||
let internal = f.p.relate(RelationKind::Interaction, "X to Y", x, y);
|
// FunctionalExchange's actual builder creates valid function input/output
|
||||||
let external = f.p.relate(RelationKind::Interaction, "Y to Outside", y, outside);
|
// ports; do not manufacture Interaction records between SA Functions.
|
||||||
let an = f.p.diagrams[&f.d].nodes.values().find(|n| n.element == f.a).unwrap().id;
|
let internal = f.p.relate(exchange, "X to Y", x, y);
|
||||||
|
let external = f.p.relate(exchange, "Y to Outside", y, outside);
|
||||||
|
let an = f.p.diagrams[&f.d]
|
||||||
|
.nodes
|
||||||
|
.values()
|
||||||
|
.find(|n| n.element == f.a)
|
||||||
|
.unwrap()
|
||||||
|
.id;
|
||||||
let d = f.p.diagrams.get_mut(&f.d).unwrap();
|
let d = f.p.diagrams.get_mut(&f.d).unwrap();
|
||||||
d.nodes.get_mut(&f.n).unwrap().position = xy(100., 800.);
|
d.nodes.get_mut(&f.n).unwrap().position = xy(100., 800.);
|
||||||
d.nodes.get_mut(&an).unwrap().collapsed = collapsed;
|
d.nodes.get_mut(&an).unwrap().collapsed = collapsed;
|
||||||
d.routes = vec![route(internal), route(external)];
|
d.routes = vec![route(internal), route(external)];
|
||||||
if obstacle {
|
if obstacle {
|
||||||
let id = f.p.add_element(Kind::Activity, Layer::Oa, "Stationary obstacle", None).unwrap();
|
let id =
|
||||||
|
f.p.add_element(work, layer, "Stationary obstacle", None)
|
||||||
|
.unwrap();
|
||||||
f.p.place(f.d, id, xy(550., 230.));
|
f.p.place(f.d, id, xy(550., 230.));
|
||||||
}
|
}
|
||||||
validate_candidate(&f.p).unwrap();
|
validate_candidate(&f.p).unwrap();
|
||||||
let captured = scope(&f);
|
let captured = scope(&f);
|
||||||
assert_eq!(captured.uniform, Uniform::MixedOrUnresolved);
|
assert_eq!(captured.uniform, Uniform::MixedOrUnresolved);
|
||||||
assert_eq!(captured.placements.keys().copied().collect::<BTreeSet<_>>(), BTreeSet::from([f.n, xn, yn]));
|
assert_eq!(
|
||||||
|
captured.placements.keys().copied().collect::<BTreeSet<_>>(),
|
||||||
|
BTreeSet::from([f.n, xn, yn])
|
||||||
|
);
|
||||||
// Explicit control: the real current API still refuses Arrange for this root.
|
// Explicit control: the real current API still refuses Arrange for this root.
|
||||||
assert!(matches!(plan_semantics(&f.p, &captured, Destination::LayoutOnly, &BTreeMap::new()), Err(Refusal::MixedLayoutRequired)));
|
assert!(matches!(
|
||||||
Mixed { f, an, xn, yn, internal }
|
plan_semantics(&f.p, &captured, Destination::LayoutOnly, &BTreeMap::new()),
|
||||||
|
Err(Refusal::MixedLayoutRequired)
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
plan_semantics(
|
||||||
|
&f.p,
|
||||||
|
&captured,
|
||||||
|
Destination::Performer(f.a),
|
||||||
|
&BTreeMap::new()
|
||||||
|
),
|
||||||
|
Err(Refusal::MixedLayoutRequired)
|
||||||
|
));
|
||||||
|
Mixed {
|
||||||
|
f,
|
||||||
|
an,
|
||||||
|
xn,
|
||||||
|
yn,
|
||||||
|
internal,
|
||||||
|
include_performer,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn affected(m: &Mixed) -> BTreeSet<Id> {
|
fn affected(m: &Mixed) -> BTreeSet<Id> {
|
||||||
let mut ids: BTreeSet<_> = scope(&m.f).placements.keys().copied().collect();
|
let mut ids: BTreeSet<_> = scope(&m.f).placements.keys().copied().collect();
|
||||||
ids.insert(m.an);
|
let mut expected = BTreeSet::from([m.f.n, m.xn, m.yn]);
|
||||||
ids.extend(projection::descendant_nodes(&m.f.p, m.f.d, m.an));
|
if m.include_performer {
|
||||||
assert_eq!(ids, BTreeSet::from([m.an, m.f.n, m.xn, m.yn]));
|
ids.insert(m.an);
|
||||||
|
ids.extend(projection::descendant_nodes(&m.f.p, m.f.d, m.an));
|
||||||
|
expected.insert(m.an);
|
||||||
|
}
|
||||||
|
assert_eq!(ids, expected);
|
||||||
ids
|
ids
|
||||||
}
|
}
|
||||||
|
|
||||||
// Diagnostic expansion changes only a temporary Diagram. Include affected
|
// Diagnostic expansion changes only a temporary Diagram. Include affected
|
||||||
// descendants and ancestors even when the normal projection hides them.
|
// descendants and ancestors even when the normal projection hides them.
|
||||||
|
// Unrelated folded interiors are intentionally OUTSIDE this diagnostic domain.
|
||||||
fn diagnostic(p: &Project, diagram: Id, ids: &BTreeSet<Id>) -> ProjectedView {
|
fn diagnostic(p: &Project, diagram: Id, ids: &BTreeSet<Id>) -> ProjectedView {
|
||||||
let mut d = p.diagrams[&diagram].clone();
|
let mut d = p.diagrams[&diagram].clone();
|
||||||
for (id, node) in &mut d.nodes {
|
for (id, node) in &mut d.nodes {
|
||||||
if ids.contains(id) || projection::descendant_nodes(p, diagram, *id).iter().any(|n| ids.contains(n)) {
|
if ids.contains(id)
|
||||||
|
|| projection::descendant_nodes(p, diagram, *id)
|
||||||
|
.iter()
|
||||||
|
.any(|n| ids.contains(n))
|
||||||
|
{
|
||||||
node.collapsed = false;
|
node.collapsed = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,67 +121,330 @@ fn rigid(before: &ProjectedView, after: &ProjectedView, ids: &BTreeSet<Id>) {
|
|||||||
// falsely enlarged Scope. The combined scene uses the real overlap predicate.
|
// falsely enlarged Scope. The combined scene uses the real overlap predicate.
|
||||||
fn candidate(m: &Mixed) -> (Command, Project) {
|
fn candidate(m: &Mixed) -> (Command, Project) {
|
||||||
let ids = affected(m);
|
let ids = affected(m);
|
||||||
let command = oa_core::diagram_edit::translate_placements(&m.f.p, m.f.d, &ids, xy(400., 0.)).unwrap();
|
let command =
|
||||||
|
oa_core::diagram_edit::translate_placements(&m.f.p, m.f.d, &ids, xy(400., 0.)).unwrap();
|
||||||
let actual = prepare_command(&m.f.p, &command).unwrap().project;
|
let actual = prepare_command(&m.f.p, &command).unwrap().project;
|
||||||
validate_candidate(&actual).unwrap();
|
validate_candidate(&actual).unwrap();
|
||||||
// Independent literal expected Project: no planner or route helper oracle.
|
// Independent literal expected Project: no planner or route helper oracle.
|
||||||
let mut expected = m.f.p.clone();
|
let mut expected = m.f.p.clone();
|
||||||
let d = expected.diagrams.get_mut(&m.f.d).unwrap();
|
let d = expected.diagrams.get_mut(&m.f.d).unwrap();
|
||||||
for (id, position) in [(m.an, xy(500., 100.)), (m.f.n, xy(500., 800.)), (m.xn, xy(540., 200.)), (m.yn, xy(1340., 200.))] {
|
for (id, position) in [
|
||||||
|
(m.f.n, xy(500., 800.)),
|
||||||
|
(m.xn, xy(540., 200.)),
|
||||||
|
(m.yn, xy(1340., 200.)),
|
||||||
|
] {
|
||||||
d.nodes.get_mut(&id).unwrap().position = position;
|
d.nodes.get_mut(&id).unwrap().position = position;
|
||||||
}
|
}
|
||||||
|
if m.include_performer {
|
||||||
|
d.nodes.get_mut(&m.an).unwrap().position = xy(500., 100.);
|
||||||
|
}
|
||||||
let internal = d.routes.iter_mut().find(|r| r.edge == m.internal).unwrap();
|
let internal = d.routes.iter_mut().find(|r| r.edge == m.internal).unwrap();
|
||||||
internal.waypoints = vec![xy(820., 400.), xy(820., 550.)];
|
internal.waypoints = vec![xy(820., 400.), xy(820., 550.)];
|
||||||
assert_eq!(actual, expected, "complete records, including folds, routes, other view and relations");
|
assert_eq!(
|
||||||
|
actual, expected,
|
||||||
|
"complete records, including folds, routes, other view and relations"
|
||||||
|
);
|
||||||
(command, actual)
|
(command, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mixed_arrange_experiment_distributed_root_and_performer_can_translate_rigidly() {
|
fn mixed_arrange_experiment_distributed_root_and_performer_can_translate_rigidly() {
|
||||||
for collapsed in [false, true] {
|
for layer in [Layer::Oa, Layer::Sa] {
|
||||||
let m = mixed(collapsed, false);
|
let enclosure_height = match layer {
|
||||||
|
Layer::Oa => 218.,
|
||||||
|
Layer::Sa => 256.,
|
||||||
|
};
|
||||||
|
for include_performer in [false, true] {
|
||||||
|
for collapsed in [false, true] {
|
||||||
|
let m = mixed(layer, collapsed, false, include_performer);
|
||||||
|
let accepted = m.f.p.clone();
|
||||||
|
let (command, actual) = candidate(&m);
|
||||||
|
let ids = affected(&m);
|
||||||
|
let before = projection::project_view(&accepted, m.f.d);
|
||||||
|
let after = projection::project_view(&actual, m.f.d);
|
||||||
|
let before_expanded = diagnostic(&accepted, m.f.d, &ids);
|
||||||
|
let after_expanded = diagnostic(&actual, m.f.d, &ids);
|
||||||
|
assert!(
|
||||||
|
!overlaps(&before, &after),
|
||||||
|
"visible scene must admit the combined move"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!overlaps(&before_expanded, &after_expanded),
|
||||||
|
"expanded scene must admit the combined move"
|
||||||
|
);
|
||||||
|
rigid(&before_expanded, &after_expanded, &ids);
|
||||||
|
for node in after.nodes.iter().chain(&after_expanded.nodes) {
|
||||||
|
assert!(
|
||||||
|
(Rect {
|
||||||
|
position: node.position,
|
||||||
|
size: node.size
|
||||||
|
})
|
||||||
|
.valid()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Explicit selected headers remain visible. Native clipping/readability
|
||||||
|
// and complete group gesture admission are intentionally not claimed.
|
||||||
|
assert!(after.nodes.iter().any(|n| n.id == m.f.n));
|
||||||
|
if include_performer {
|
||||||
|
assert!(after.nodes.iter().any(|n| n.id == m.an));
|
||||||
|
} else {
|
||||||
|
assert_eq!(
|
||||||
|
actual.diagrams[&m.f.d].nodes[&m.an],
|
||||||
|
accepted.diagrams[&m.f.d].nodes[&m.an]
|
||||||
|
);
|
||||||
|
assert_eq!(node(&before_expanded, m.an).position, xy(100., 100.));
|
||||||
|
assert_eq!(
|
||||||
|
node(&before_expanded, m.an).size,
|
||||||
|
Size {
|
||||||
|
width: 288.,
|
||||||
|
height: enclosure_height
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(node(&after_expanded, m.an).position, xy(100., 100.));
|
||||||
|
assert_eq!(
|
||||||
|
node(&after_expanded, m.an).size,
|
||||||
|
Size {
|
||||||
|
width: 688.,
|
||||||
|
height: enclosure_height
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert_eq!(roundtrip(&accepted, command), actual);
|
||||||
|
assert_eq!(m.f.p, accepted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mixed_arrange_experiment_detects_moved_folded_child_collision() {
|
||||||
|
for layer in [Layer::Oa, Layer::Sa] {
|
||||||
|
let expected_overlap = match layer {
|
||||||
|
Layer::Oa => 13696.,
|
||||||
|
Layer::Sa => 20116.,
|
||||||
|
};
|
||||||
|
let m = mixed(layer, true, true, true);
|
||||||
let accepted = m.f.p.clone();
|
let accepted = m.f.p.clone();
|
||||||
let (command, actual) = candidate(&m);
|
let (_, actual) = candidate(&m);
|
||||||
let ids = affected(&m);
|
let ids = affected(&m);
|
||||||
let before = projection::project_view(&accepted, m.f.d);
|
let before = projection::project_view(&accepted, m.f.d);
|
||||||
let after = projection::project_view(&actual, m.f.d);
|
let after = projection::project_view(&actual, m.f.d);
|
||||||
|
assert!(!before.nodes.iter().any(|n| n.id == m.xn));
|
||||||
|
assert!(!after.nodes.iter().any(|n| n.id == m.xn));
|
||||||
|
assert!(
|
||||||
|
!overlaps(&before, &after),
|
||||||
|
"must distinguish hidden collision from ordinary visible refusal"
|
||||||
|
);
|
||||||
let before_expanded = diagnostic(&accepted, m.f.d, &ids);
|
let before_expanded = diagnostic(&accepted, m.f.d, &ids);
|
||||||
let after_expanded = diagnostic(&actual, m.f.d, &ids);
|
let after_expanded = diagnostic(&actual, m.f.d, &ids);
|
||||||
assert!(!overlaps(&before, &after), "visible scene must admit the combined move");
|
|
||||||
assert!(!overlaps(&before_expanded, &after_expanded), "expanded scene must admit the combined move");
|
|
||||||
rigid(&before_expanded, &after_expanded, &ids);
|
rigid(&before_expanded, &after_expanded, &ids);
|
||||||
for node in after.nodes.iter().chain(&after_expanded.nodes) {
|
let obstacle = before_expanded
|
||||||
assert!((Rect { position: node.position, size: node.size }).valid());
|
.nodes
|
||||||
|
.iter()
|
||||||
|
.find(|n| n.position == xy(550., 230.))
|
||||||
|
.unwrap()
|
||||||
|
.id;
|
||||||
|
assert_eq!(
|
||||||
|
overlap(
|
||||||
|
node(&before_expanded, m.xn),
|
||||||
|
node(&before_expanded, obstacle)
|
||||||
|
),
|
||||||
|
0.
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
overlap(node(&after_expanded, m.xn), node(&after_expanded, obstacle)),
|
||||||
|
expected_overlap
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
overlaps(&before_expanded, &after_expanded),
|
||||||
|
"combined diagnostic scene must refuse"
|
||||||
|
);
|
||||||
|
// Detection only: there is no production group admission/commit API here.
|
||||||
|
assert_eq!(m.f.p, accepted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn node(view: &ProjectedView, id: Id) -> &ProjectedNode {
|
||||||
|
view.nodes.iter().find(|n| n.id == id).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mixed_arrange_experiment_detects_stationary_enclosure_growth_collision() {
|
||||||
|
for layer in [Layer::Oa, Layer::Sa] {
|
||||||
|
let enclosure_height = match layer {
|
||||||
|
Layer::Oa => 218.,
|
||||||
|
Layer::Sa => 256.,
|
||||||
|
};
|
||||||
|
let mut m = mixed(layer, false, false, true);
|
||||||
|
let kind = m.f.p.elements[&m.f.w].kind();
|
||||||
|
let z =
|
||||||
|
m.f.p
|
||||||
|
.add_element(kind, layer, "Stationary Z", None)
|
||||||
|
.unwrap();
|
||||||
|
let zn = m.f.p.place(m.f.d, z, xy(1200., 100.));
|
||||||
|
validate_candidate(&m.f.p).unwrap();
|
||||||
|
let accepted = m.f.p.clone();
|
||||||
|
let (_, actual) = candidate(&m);
|
||||||
|
let bn = accepted.diagrams[&m.f.d]
|
||||||
|
.nodes
|
||||||
|
.values()
|
||||||
|
.find(|n| n.element == m.f.b)
|
||||||
|
.unwrap()
|
||||||
|
.id;
|
||||||
|
let before = projection::project_view(&accepted, m.f.d);
|
||||||
|
let after = projection::project_view(&actual, m.f.d);
|
||||||
|
assert_eq!(node(&before, bn).position, xy(900., 100.));
|
||||||
|
assert_eq!(
|
||||||
|
node(&before, bn).size,
|
||||||
|
Size {
|
||||||
|
width: 288.,
|
||||||
|
height: enclosure_height
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(node(&after, bn).position, xy(900., 100.));
|
||||||
|
assert_eq!(
|
||||||
|
node(&after, bn).size,
|
||||||
|
Size {
|
||||||
|
width: 688.,
|
||||||
|
height: enclosure_height
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(node(&after, zn).position, xy(1200., 100.));
|
||||||
|
assert_eq!(
|
||||||
|
node(&after, zn).size,
|
||||||
|
Size {
|
||||||
|
width: 224.,
|
||||||
|
height: 94.
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(overlap(node(&before, bn), node(&before, zn)), 0.);
|
||||||
|
assert_eq!(overlap(node(&after, bn), node(&after, zn)), 21056.);
|
||||||
|
assert_eq!(node(&after, m.yn).position, xy(1340., 200.));
|
||||||
|
assert_eq!(
|
||||||
|
node(&after, m.yn).position.y
|
||||||
|
- (node(&after, zn).position.y + node(&after, zn).size.height),
|
||||||
|
6.
|
||||||
|
);
|
||||||
|
for id in affected(&m) {
|
||||||
|
assert_eq!(overlap(node(&before, id), node(&before, zn)), 0.);
|
||||||
|
assert_eq!(overlap(node(&after, id), node(&after, zn)), 0.);
|
||||||
}
|
}
|
||||||
// Explicit selected headers remain visible. Native clipping/readability
|
assert_eq!(
|
||||||
// and complete group gesture admission are intentionally not claimed.
|
actual.diagrams[&m.f.d].nodes[&bn],
|
||||||
for id in [m.an, m.f.n] { assert!(after.nodes.iter().any(|n| n.id == id)); }
|
accepted.diagrams[&m.f.d].nodes[&bn]
|
||||||
assert_eq!(roundtrip(&accepted, command), actual);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
actual.diagrams[&m.f.d].nodes[&zn],
|
||||||
|
accepted.diagrams[&m.f.d].nodes[&zn]
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
overlaps(&before, &after),
|
||||||
|
"complete scene must detect stationary enclosure growth"
|
||||||
|
);
|
||||||
|
let ids = affected(&m);
|
||||||
|
assert!(overlaps(
|
||||||
|
&diagnostic(&accepted, m.f.d, &ids),
|
||||||
|
&diagnostic(&actual, m.f.d, &ids)
|
||||||
|
));
|
||||||
assert_eq!(m.f.p, accepted);
|
assert_eq!(m.f.p, accepted);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mixed_arrange_experiment_folded_child_collision_refuses_before_commit() {
|
fn mixed_arrange_experiment_unrelated_folded_interiors_are_outside_diagnostic_domain() {
|
||||||
let m = mixed(true, true);
|
for layer in [Layer::Oa, Layer::Sa] {
|
||||||
let accepted = m.f.p.clone();
|
// Independent source-derived literals: SA's auto-created function ports
|
||||||
let (_, actual) = candidate(&m);
|
// raise projected X/Y height to 76 + 28 * (1 + 1), while Z is port-free.
|
||||||
let ids = affected(&m);
|
let (work_height, expected_overlap) = match layer {
|
||||||
let before = projection::project_view(&accepted, m.f.d);
|
Layer::Oa => (94., 13696.),
|
||||||
let after = projection::project_view(&actual, m.f.d);
|
Layer::Sa => (132., 20116.),
|
||||||
assert!(!before.nodes.iter().any(|n| n.id == m.xn));
|
};
|
||||||
assert!(!after.nodes.iter().any(|n| n.id == m.xn));
|
let mut m = mixed(layer, true, false, true);
|
||||||
assert!(!overlaps(&before, &after), "must distinguish hidden collision from ordinary visible refusal");
|
let performer = match layer {
|
||||||
let before_expanded = diagnostic(&accepted, m.f.d, &ids);
|
Layer::Oa => Kind::Actor,
|
||||||
let after_expanded = diagnostic(&actual, m.f.d, &ids);
|
Layer::Sa => Kind::SystemActor,
|
||||||
rigid(&before_expanded, &after_expanded, &ids);
|
};
|
||||||
assert!(overlaps(&before_expanded, &after_expanded), "combined diagnostic scene must refuse");
|
let kind = m.f.p.elements[&m.f.w].kind();
|
||||||
// No commit on refusal; candidate preparation never mutates accepted source.
|
let d =
|
||||||
let document = Document::from_project(accepted.clone(), ProjectLimits::default()).unwrap();
|
m.f.p
|
||||||
let source = document.bundle().clone();
|
.add_element(performer, layer, "Unrelated D", None)
|
||||||
assert_eq!(document.project(), &accepted);
|
.unwrap();
|
||||||
assert_eq!(document.bundle(), &source);
|
let z =
|
||||||
assert_eq!(document.revision(), 0);
|
m.f.p
|
||||||
assert!(!document.can_undo());
|
.add_element(kind, layer, "Unrelated hidden Z", None)
|
||||||
assert_eq!(m.f.p, accepted);
|
.unwrap();
|
||||||
|
m.f.p.relate(RelationKind::Allocation, "D owns Z", d, z);
|
||||||
|
let dn = m.f.p.place(m.f.d, d, xy(2500., 100.));
|
||||||
|
let zn = m.f.p.place(m.f.d, z, xy(550., 230.));
|
||||||
|
m.f.p
|
||||||
|
.diagrams
|
||||||
|
.get_mut(&m.f.d)
|
||||||
|
.unwrap()
|
||||||
|
.nodes
|
||||||
|
.get_mut(&dn)
|
||||||
|
.unwrap()
|
||||||
|
.collapsed = true;
|
||||||
|
validate_candidate(&m.f.p).unwrap();
|
||||||
|
let accepted = m.f.p.clone();
|
||||||
|
let (_, actual) = candidate(&m);
|
||||||
|
let ids = affected(&m);
|
||||||
|
let before = diagnostic(&accepted, m.f.d, &ids);
|
||||||
|
let after = diagnostic(&actual, m.f.d, &ids);
|
||||||
|
assert!(!before.nodes.iter().any(|n| n.id == zn));
|
||||||
|
assert!(!after.nodes.iter().any(|n| n.id == zn));
|
||||||
|
assert!(
|
||||||
|
!overlaps(&before, &after),
|
||||||
|
"chosen diagnostic domain excludes unrelated folded contents"
|
||||||
|
);
|
||||||
|
assert!(!overlaps(
|
||||||
|
&projection::project_view(&accepted, m.f.d),
|
||||||
|
&projection::project_view(&actual, m.f.d)
|
||||||
|
));
|
||||||
|
let all_unfolded = |p: &Project| {
|
||||||
|
let mut diagram = p.diagrams[&m.f.d].clone();
|
||||||
|
for n in diagram.nodes.values_mut() {
|
||||||
|
n.collapsed = false;
|
||||||
|
}
|
||||||
|
projection::project_diagram(p, &diagram)
|
||||||
|
};
|
||||||
|
let broad_before = all_unfolded(&accepted);
|
||||||
|
let broad_after = all_unfolded(&actual);
|
||||||
|
assert_eq!(node(&broad_before, m.xn).position, xy(140., 200.));
|
||||||
|
assert_eq!(node(&broad_after, m.xn).position, xy(540., 200.));
|
||||||
|
assert_eq!(
|
||||||
|
node(&broad_after, m.xn).size,
|
||||||
|
Size {
|
||||||
|
width: 224.,
|
||||||
|
height: work_height
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(node(&broad_before, zn).position, xy(550., 230.));
|
||||||
|
assert_eq!(node(&broad_after, zn).position, xy(550., 230.));
|
||||||
|
assert_eq!(
|
||||||
|
node(&broad_after, zn).size,
|
||||||
|
Size {
|
||||||
|
width: 224.,
|
||||||
|
height: 94.
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
overlap(node(&broad_before, m.xn), node(&broad_before, zn)),
|
||||||
|
0.
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
overlap(node(&broad_after, m.xn), node(&broad_after, zn)),
|
||||||
|
expected_overlap
|
||||||
|
);
|
||||||
|
// Other newly exposed enclosure pairs may also overlap. X/Z is a
|
||||||
|
// concrete discriminator, not a claim that it is the only cause.
|
||||||
|
assert!(overlaps(&broad_before, &broad_after));
|
||||||
|
for id in [dn, zn] {
|
||||||
|
assert_eq!(
|
||||||
|
actual.diagrams[&m.f.d].nodes[&id],
|
||||||
|
accepted.diagrams[&m.f.d].nodes[&id]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert!(actual.diagrams[&m.f.d].nodes[&dn].collapsed);
|
||||||
|
assert_eq!(m.f.p, accepted);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user