Plan whole saved-view model deletion from exact visible groups

This commit is contained in:
2026-09-24 21:24:43 -07:00
parent afcea12cd9
commit 7750ecd751
6 changed files with 1490 additions and 0 deletions
+2
View File
@@ -43,6 +43,8 @@ pub use port_labels::PortLabels;
#[path = "diagram/focus.rs"]
pub(crate) mod focus;
pub(crate) mod group_gesture;
#[cfg(test)]
pub(crate) mod group_removal;
mod multiselect;
#[cfg(test)]
mod multiselect_tests;
@@ -0,0 +1,152 @@
//! Pure, whole-selection Delete from model planning for saved architecture and
//! State views. UI lifetime, drafts and confirmation remain the caller's concern.
use crate::diagram_usage::Target;
use oa_core::{
Command, Id, Kind, Project, execute_batch, projection, references, validate_candidate,
};
use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug)]
pub(crate) struct Plan {
pub(crate) command: Command,
/// Semantic identities resolved from every admitted exact target.
pub(crate) selected: BTreeSet<Id>,
/// Actual identity and surviving-record differences after the complete batch.
pub(crate) removed: BTreeSet<Id>,
pub(crate) changed: BTreeSet<Id>,
}
pub(crate) fn prepare(project: &Project, diagram: Id, targets: &[Target]) -> Result<Plan, String> {
let saved = project
.diagrams
.get(&diagram)
.ok_or("The saved view no longer exists.")?;
if !saved.projection.is_architecture() && saved.projection.state_machine().is_none() {
return Err("Delete from model supports saved Architecture and State views here.".into());
}
if targets.is_empty() {
return Err("Select at least one visible model object to delete.".into());
}
validate_candidate(project).map_err(|error| error.to_string())?;
let view = projection::project_diagram(project, saved);
let mut selected = BTreeSet::new();
// Resolve every original typed identity BEFORE removing any cascade overlap.
// A stale/hidden/wrong-kind target cannot become admissible because another
// selection member would happen to delete its semantic identity.
for (index, target) in targets.iter().enumerate() {
if targets[..index].contains(target) {
return Err("Every selected exact target must be unique.".into());
}
let id = match target {
Target::Placement(id) => saved.nodes.get(id)
.filter(|node| view.nodes.iter().any(|visible| visible.id == *id && visible.element == node.element))
.map(|node| node.element)
.ok_or("A selected placement is no longer visible in this saved view.")?,
Target::Port { port, placement } => {
let owner = saved.nodes.get(placement)
.ok_or("A selected port's exact owner placement no longer exists in this view.")?;
if !view.ports.iter().any(|visible| visible.element == *port && visible.owner == *placement)
|| !project.elements.get(port).is_some_and(|element| element.kind() == Kind::Port && element.parent == Some(owner.element))
{
return Err("A selected port is no longer visible on its exact owner placement.".into());
}
*port
}
Target::Edge(id) => {
if !view.edges.iter().any(|edge| edge.id == *id)
|| !(project.relations.contains_key(id) || project.traces.contains_key(id))
{
return Err("Every selected edge must be a visible stored relation or trace; inferred edges are read-only.".into());
}
*id
}
Target::StateRow { placement, relation } => {
if !crate::behavior::state_canvas::visible_row_target(project, diagram, &view, *placement, *relation) {
return Err("A selected State row is no longer visible on its exact placement.".into());
}
*relation
}
_ => return Err("The complete selection must contain only visible placements, ports, stored edges or State rows; grouping and read-only targets cannot be deleted here.".into()),
};
selected.insert(id);
}
// Prefer cascades covering the most remaining selected subjects. Selecting a
// parent together with its child/port/incident relation must not issue Delete
// for an identity already removed by that parent. Stable ID order breaks ties.
// Core defers conditional exchange-allocation cleanup until batch completion;
// admission and the final result are therefore never singleton-validated.
let impacts: BTreeMap<_, _> = selected
.iter()
.map(|&id| (id, references::deletion_impact(project, id)))
.collect();
let mut pending = selected.clone();
let mut commands = Vec::new();
while !pending.is_empty() {
let id = *pending
.iter()
.max_by_key(|&&id| {
(
impacts[&id].intersection(&pending).count(),
std::cmp::Reverse(id),
)
})
.unwrap();
commands.push(Command::Delete(id));
pending.retain(|remaining| !impacts[&id].contains(remaining));
}
let command = Command::Batch(commands);
let candidate = execute_batch(project, std::slice::from_ref(&command))
.map_err(|error| error.to_string())?;
let original: BTreeSet<_> = project.identities().into_iter().map(|(id, _)| id).collect();
let retained: BTreeSet<_> = candidate
.identities()
.into_iter()
.map(|(id, _)| id)
.collect();
if !retained.is_subset(&original) {
return Err("Removal unexpectedly created model identities; no change was applied.".into());
}
let removed: BTreeSet<_> = original.difference(&retained).copied().collect();
if !selected.is_subset(&removed) {
return Err("The complete selected set could not be removed.".into());
}
let mut changed = BTreeSet::new();
changed_records(&project.elements, &candidate.elements, &mut changed);
changed_records(&project.relations, &candidate.relations, &mut changed);
changed_records(&project.diagrams, &candidate.diagrams, &mut changed);
changed_records(&project.traces, &candidate.traces, &mut changed);
changed_records(
&project.artifact_links,
&candidate.artifact_links,
&mut changed,
);
for (&id, diagram) in &project.diagrams {
if let Some(after) = candidate.diagrams.get(&id) {
changed_records(&diagram.nodes, &after.nodes, &mut changed);
}
}
Ok(Plan {
command,
selected,
removed,
changed,
})
}
fn changed_records<T: PartialEq>(
before: &BTreeMap<Id, T>,
after: &BTreeMap<Id, T>,
changed: &mut BTreeSet<Id>,
) {
changed.extend(
before
.iter()
.filter_map(|(&id, record)| after.get(&id).filter(|next| *next != record).map(|_| id)),
);
}
#[cfg(test)]
mod conditional_tests;
#[cfg(test)]
mod tests;
@@ -0,0 +1,302 @@
//! Independent conditional-cascade checks for overlapping selected endpoint allocations.
use super::prepare;
use crate::diagram_usage::Target;
use oa_core::*;
use std::collections::BTreeSet;
struct Fixture {
project: Project,
diagram: Id,
selected_allocations: [Id; 2],
exchange_allocation: Id,
functional_exchange: Id,
}
fn relation(project: &mut Project, number: u128, alias: &str, body: RelationBody) -> Id {
let id = Id::from_u128(number);
assert!(
project
.relations
.insert(
id,
Relation {
id,
alias: alias.into(),
name: alias.into(),
description: String::new(),
body,
}
)
.is_none()
);
id
}
fn fixture(reverse_ids: bool, invalid_dual_mapping: bool) -> Fixture {
let mut project = Project::empty();
let performer = project
.add_element(Kind::System, Layer::Sa, "Send performer", None)
.unwrap();
let receiver = if invalid_dual_mapping {
performer
} else {
project
.add_element(Kind::SystemActor, Layer::Sa, "Receive performer", None)
.unwrap()
};
let source = project
.add_element(Kind::Function, Layer::Sa, "Send", None)
.unwrap();
let target = project
.add_element(Kind::Function, Layer::Sa, "Receive", None)
.unwrap();
let retained = project.relate(RelationKind::Allocation, "Performs send", performer, source);
project.relate(
RelationKind::Allocation,
"Performs receive",
receiver,
target,
);
let functional_exchange = project.relate(
RelationKind::FunctionalExchange,
"Information",
source,
target,
);
let (output, input) = match project.relations[&functional_exchange].body {
RelationBody::FunctionalExchange {
source_port,
target_port,
..
} => (source_port, target_port),
_ => unreachable!(),
};
let mut component_ports = vec![];
for (owner, name) in [
(performer, "First component port"),
(receiver, "Second component port"),
] {
let id = Id::new_v4();
let port = make_port(
&project,
id,
owner,
name,
PortContract::Component {
kind: ComponentPortKind::Flow,
orientation: ComponentPortOrientation::InOut,
},
)
.unwrap();
project.elements.insert(id, port);
component_ports.push(id);
}
let [a, b] = [component_ports[0], component_ports[1]];
let boundary = relation(
&mut project,
300,
"boundary",
RelationBody::BoundaryExchange {
source: a,
target: b,
oriented: false,
items: vec![],
},
);
// Either endpoint allocation is required. Their overlapping conditional
// cascade must remove the exchange allocation once, after one whole batch.
let numbers = if reverse_ids { [200, 100] } else { [100, 200] };
let first = relation(
&mut project,
numbers[0],
"first_output",
RelationBody::PortAllocation {
source: a,
target: output,
},
);
let second = relation(
&mut project,
numbers[1],
"second_input",
RelationBody::PortAllocation {
source: b,
target: input,
},
);
if invalid_dual_mapping {
// Two simultaneous direct/reverse mappings would need the same actual
// performer at both ends, violating the different-boundary-owners rule.
relation(
&mut project,
400,
"first_input",
RelationBody::PortAllocation {
source: a,
target: input,
},
);
relation(
&mut project,
500,
"second_output",
RelationBody::PortAllocation {
source: b,
target: output,
},
);
}
let exchange_allocation = relation(
&mut project,
150,
"realizes_exchange",
RelationBody::ExchangeAllocation {
source: boundary,
target: functional_exchange,
},
);
let diagram = project.ensure_diagram(Layer::Sa);
project.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Overview;
for (element, x) in [
(performer, 40.),
(receiver, 1360.),
(source, 480.),
(target, 920.),
] {
project.place(diagram, element, Position { x, y: 200. });
}
project.diagrams.get_mut(&diagram).unwrap().routes = [first, retained]
.into_iter()
.map(|edge| EdgeRoute {
edge,
source_attachment: None,
target_attachment: None,
waypoints: vec![Position { x: 360., y: 350. }],
label: None,
})
.collect();
Fixture {
project,
diagram,
selected_allocations: [first, second],
exchange_allocation,
functional_exchange,
}
}
#[test]
fn overlapping_conditional_allocation_loss_is_reviewed_from_final_candidate() {
for reverse_ids in [false, true] {
let f = fixture(reverse_ids, false);
validate_candidate(&f.project)
.expect("Separate actual performers and required endpoint mappings are valid");
for selected in f.selected_allocations {
assert!(
references::deletion_impact(&f.project, selected).contains(&f.exchange_allocation),
"Both selected allocations overlap on the same conditional dependent"
);
let singleton = execute_batch(&f.project, &[Command::Delete(selected)]).unwrap();
assert!(!singleton.relations.contains_key(&f.exchange_allocation));
}
let targets = f.selected_allocations.map(Target::Edge);
let plan = prepare(&f.project, f.diagram, &targets).unwrap();
let reversed = prepare(
&f.project,
f.diagram,
&[targets[1].clone(), targets[0].clone()],
)
.unwrap();
assert_eq!(
serde_json::to_value(&plan.command).unwrap(),
serde_json::to_value(&reversed.command).unwrap(),
"Click order must not change normalization"
);
let removed = BTreeSet::from([
f.selected_allocations[0],
f.selected_allocations[1],
f.exchange_allocation,
]);
assert_eq!(plan.selected, BTreeSet::from(f.selected_allocations));
assert_eq!(plan.removed, removed);
assert_eq!(plan.changed, BTreeSet::from([f.diagram]));
// Literal record oracle, independent of deletion_impact / planner output.
let mut expected = f.project.clone();
for id in removed {
expected.relations.remove(&id);
}
expected
.diagrams
.get_mut(&f.diagram)
.unwrap()
.routes
.remove(0);
let actual = execute_batch(&f.project, &[plan.command]).unwrap();
assert_eq!(
actual, expected,
"Only two port allocations, their conditional exchange allocation and the deleted edge's authored route change"
);
}
}
#[test]
fn direct_exchange_cascade_and_conditional_support_loss_share_one_batch() {
for reverse_ids in [false, true] {
let f = fixture(reverse_ids, false);
validate_candidate(&f.project)
.expect("Separate actual performers and required endpoint mappings are valid");
let targets = [
Target::Edge(f.selected_allocations[0]),
Target::Edge(f.functional_exchange),
Target::Edge(f.selected_allocations[1]),
];
let plan = prepare(&f.project, f.diagram, &targets).unwrap();
let removed = BTreeSet::from([
f.selected_allocations[0],
f.selected_allocations[1],
f.functional_exchange,
f.exchange_allocation,
]);
assert_eq!(
plan.selected,
BTreeSet::from([
f.selected_allocations[0],
f.selected_allocations[1],
f.functional_exchange
])
);
assert_eq!(plan.removed, removed);
assert_eq!(plan.changed, BTreeSet::from([f.diagram]));
let mut expected = f.project.clone();
for id in removed {
expected.relations.remove(&id);
}
expected
.diagrams
.get_mut(&f.diagram)
.unwrap()
.routes
.remove(0);
assert_eq!(
execute_batch(&f.project, &[plan.command]).unwrap(),
expected
);
}
}
#[test]
fn alternative_pairing_counterexample_is_not_a_valid_saved_model() {
let f = fixture(false, true);
assert!(validation::validate(&f.project).iter().any(
|issue| issue.code == "boundary_owner" && issue.severity == validation::Severity::Error
));
let before = f.project.clone();
assert!(
prepare(
&f.project,
f.diagram,
&f.selected_allocations.map(Target::Edge)
)
.is_err()
);
assert_eq!(f.project, before);
}
@@ -0,0 +1,896 @@
use super::*;
use oa_core::*;
fn pos(x: f32, y: f32) -> Position {
Position { x, y }
}
fn diagram(project: &mut Project, layer: Layer, projection: ProjectionKind) -> Id {
let id = Id::new_v4();
project.diagrams.insert(
id,
Diagram {
id,
alias: format!("view_{}", id.simple()),
name: "Saved view".into(),
description: String::new(),
layer,
projection,
view_options: Default::default(),
nodes: BTreeMap::new(),
routes: vec![],
layout: None,
},
);
id
}
fn artifact(project: &mut Project, owner: Id) -> Id {
let id = Id::new_v4();
project.artifact_links.insert(
id,
ArtifactLink {
id,
alias: format!("evidence_{}", id.simple()),
name: "Evidence".into(),
description: String::new(),
owner,
relation: ArtifactRelation::References,
uri: "docs/evidence.md".into(),
revision: Some("r1".into()),
fragment: None,
},
);
id
}
fn route(edge: Id) -> EdgeRoute {
EdgeRoute {
edge,
source_attachment: None,
target_attachment: None,
waypoints: vec![pos(450., 150.)],
label: None,
}
}
/// Every success compares the whole resulting Project with a separately authored
/// expected value; the planner's own impact calculation is never the oracle.
fn check(
project: &Project,
view: Id,
targets: &[Target],
expected: &Project,
selected: &[Id],
removed: &[Id],
changed: &[Id],
) -> Plan {
validate_candidate(project).unwrap();
validate_candidate(expected).unwrap();
let original = project.clone();
let plan = prepare(project, view, targets).unwrap();
assert_eq!(project, &original, "planning must be pure");
assert_eq!(plan.selected, selected.iter().copied().collect());
assert_eq!(plan.removed, removed.iter().copied().collect());
assert_eq!(plan.changed, changed.iter().copied().collect());
assert_eq!(
execute_batch(project, std::slice::from_ref(&plan.command)).unwrap(),
*expected
);
assert!(
matches!(&plan.command, Command::Batch(commands) if commands.iter().all(|command| matches!(command, Command::Delete(_))))
);
plan
}
#[test]
fn architecture_parent_child_and_incident_edge_collapse_to_one_delete_across_views() {
for layer in [Layer::Oa, Layer::Sa] {
let mut project = Project::empty();
let kind = if layer == Layer::Oa {
Kind::Activity
} else {
Kind::Function
};
let parent = project.add_element(kind, layer, "Parent", None).unwrap();
let child = project
.add_element(kind, layer, "Child", Some(parent))
.unwrap();
let survivor = project.add_element(kind, layer, "Survivor", None).unwrap();
let edge_kind = if layer == Layer::Oa {
RelationKind::Interaction
} else {
RelationKind::FunctionalExchange
};
let edge = project.relate(edge_kind, "Incident", child, survivor);
let ports = match project.relations[&edge].body {
RelationBody::FunctionalExchange {
source_port,
target_port,
..
} => Some((source_port, target_port)),
_ => None,
};
let evidence = artifact(&mut project, child);
let view = diagram(&mut project, layer, ProjectionKind::Overview);
let parent_node = project.place(view, parent, pos(0., 0.));
let child_node = project.place(view, child, pos(30., 80.));
project.place(view, survivor, pos(800., 100.));
project
.diagrams
.get_mut(&view)
.unwrap()
.routes
.push(route(edge));
let other_view = diagram(&mut project, layer, ProjectionKind::Overview);
let other_child = project.place(other_view, child, pos(100., 100.));
project.place(other_view, survivor, pos(800., 100.));
project
.diagrams
.get_mut(&other_view)
.unwrap()
.view_options
.hidden_edges
.push(edge);
let mut expected = project.clone();
expected.elements.remove(&parent);
expected.elements.remove(&child);
expected.relations.remove(&edge);
expected.artifact_links.remove(&evidence);
let mut removed = vec![
parent,
child,
edge,
evidence,
parent_node,
child_node,
other_child,
];
if let Some((source_port, _)) = ports {
expected.elements.remove(&source_port);
removed.push(source_port);
}
let current = expected.diagrams.get_mut(&view).unwrap();
current.nodes.remove(&parent_node);
current.nodes.remove(&child_node);
current.routes.clear();
let other = expected.diagrams.get_mut(&other_view).unwrap();
other.nodes.remove(&other_child);
other.view_options.hidden_edges.clear();
for targets in [
vec![
Target::Placement(parent_node),
Target::Placement(child_node),
Target::Edge(edge),
],
vec![
Target::Edge(edge),
Target::Placement(child_node),
Target::Placement(parent_node),
],
] {
let plan = check(
&project,
view,
&targets,
&expected,
&[parent, child, edge],
&removed,
&[view, other_view],
);
assert!(
matches!(plan.command, Command::Batch(commands) if matches!(commands.as_slice(), [Command::Delete(id)] if *id == parent))
);
}
}
}
#[test]
fn port_owner_port_and_exchange_admission_preserve_other_endpoint_and_nested_anchor_diff() {
let mut project = Project::empty();
let a = project
.add_element(Kind::Function, Layer::Sa, "Send", None)
.unwrap();
let b = project
.add_element(Kind::Function, Layer::Sa, "Receive", None)
.unwrap();
let edge = project.relate(RelationKind::FunctionalExchange, "Payload", a, b);
let RelationBody::FunctionalExchange {
source_port,
target_port,
..
} = project.relations[&edge].body
else {
unreachable!()
};
let view = diagram(&mut project, Layer::Sa, ProjectionKind::FunctionalDataflow);
let na = project.place(view, a, pos(0., 0.));
let nb = project.place(view, b, pos(700., 0.));
project
.diagrams
.get_mut(&view)
.unwrap()
.nodes
.get_mut(&na)
.unwrap()
.port_anchors
.push(PortAnchor {
port: source_port,
side: PortAnchorSide::Top,
offset: 0.3,
});
project
.diagrams
.get_mut(&view)
.unwrap()
.routes
.push(route(edge));
let mut port_expected = project.clone();
port_expected.elements.remove(&source_port);
port_expected.relations.remove(&edge);
let d = port_expected.diagrams.get_mut(&view).unwrap();
d.routes.clear();
d.nodes.get_mut(&na).unwrap().port_anchors.clear();
check(
&project,
view,
&[
Target::Port {
port: source_port,
placement: na,
},
Target::Edge(edge),
],
&port_expected,
&[source_port, edge],
&[source_port, edge],
&[view, na],
);
let mut owner_expected = port_expected;
owner_expected.elements.remove(&a);
owner_expected
.diagrams
.get_mut(&view)
.unwrap()
.nodes
.remove(&na);
let plan = check(
&project,
view,
&[
Target::Port {
port: source_port,
placement: na,
},
Target::Placement(na),
Target::Edge(edge),
],
&owner_expected,
&[a, source_port, edge],
&[a, source_port, edge, na],
&[view],
);
assert!(
owner_expected.elements.contains_key(&b)
&& owner_expected.elements.contains_key(&target_port)
);
assert!(owner_expected.diagrams[&view].nodes.contains_key(&nb));
assert!(matches!(plan.command, Command::Batch(commands) if commands.len() == 1));
// A wrong owner placement is invalid even though the selected owner covers it.
assert!(
prepare(
&project,
view,
&[
Target::Placement(na),
Target::Port {
port: source_port,
placement: nb
}
]
)
.is_err()
);
}
#[test]
fn state_rows_delete_relations_not_referenced_work_and_overlap_with_state_cascade() {
for layer in [Layer::Oa, Layer::Sa] {
let mut project = Project::empty();
let machine = project
.add_element(Kind::StateMachine, layer, "Modes", None)
.unwrap();
let region = project
.add_element(Kind::Region, layer, "Main", Some(machine))
.unwrap();
let state = project
.add_element(Kind::State, layer, "Ready", Some(region))
.unwrap();
let other = project
.add_element(Kind::State, layer, "Other", Some(region))
.unwrap();
let work = project
.add_element(
if layer == Layer::Oa {
Kind::Activity
} else {
Kind::Function
},
layer,
"Work",
None,
)
.unwrap();
let action = project.relate(RelationKind::StateAction, "Entry", state, work);
let internal = project.relate(RelationKind::StateTransition, "Internal", state, state);
if let RelationBody::StateTransition {
transition_kind,
effects,
..
} = &mut project.relations.get_mut(&internal).unwrap().body
{
*transition_kind = TransitionKind::Internal;
*effects = vec![work];
}
let external = project.relate(RelationKind::StateTransition, "External", state, other);
let view = diagram(
&mut project,
layer,
ProjectionKind::StateMachine { machine },
);
let state_node = project.place(view, state, pos(100., 100.));
let other_node = project.place(view, other, pos(700., 100.));
let rows = [
Target::StateRow {
placement: state_node,
relation: action,
},
Target::StateRow {
placement: state_node,
relation: internal,
},
];
let mut expected = project.clone();
expected.relations.remove(&action);
expected.relations.remove(&internal);
check(
&project,
view,
&rows,
&expected,
&[action, internal],
&[action, internal],
&[],
);
expected.relations.remove(&external);
expected.elements.remove(&state);
expected
.diagrams
.get_mut(&view)
.unwrap()
.nodes
.remove(&state_node);
let mut targets = rows.to_vec();
targets.extend([Target::Placement(state_node), Target::Edge(external)]);
check(
&project,
view,
&targets,
&expected,
&[action, internal, state, external],
&[action, internal, state, external, state_node],
&[view],
);
assert_eq!(expected.elements[&work], project.elements[&work]);
assert!(
prepare(
&project,
view,
&[
Target::Placement(state_node),
Target::StateRow {
placement: other_node,
relation: action
}
]
)
.is_err()
);
assert!(prepare(&project, view, &[Target::Edge(internal)]).is_err());
assert!(
prepare(
&project,
view,
&[Target::StateRow {
placement: state_node,
relation: external
}]
)
.is_err()
);
let region_node = project.place(view, region, pos(0., 0.));
project
.diagrams
.get_mut(&view)
.unwrap()
.nodes
.get_mut(&region_node)
.unwrap()
.collapsed = true;
assert!(
prepare(
&project,
view,
&[
Target::Placement(region_node),
Target::StateRow {
placement: state_node,
relation: action
}
]
)
.is_err(),
"hidden State row cannot borrow its selected ancestor's permission"
);
}
}
#[test]
fn deleting_machine_cascades_saved_diagram_and_its_embedded_placements() {
let mut project = Project::empty();
let machine = project
.add_element(Kind::StateMachine, Layer::Sa, "Modes", None)
.unwrap();
let region = project
.add_element(Kind::Region, Layer::Sa, "Main", Some(machine))
.unwrap();
let state = project
.add_element(Kind::State, Layer::Sa, "Ready", Some(region))
.unwrap();
let view = diagram(
&mut project,
Layer::Sa,
ProjectionKind::StateMachine { machine },
);
let machine_node = project.place(view, machine, pos(0., 0.));
let region_node = project.place(view, region, pos(30., 90.));
let state_node = project.place(view, state, pos(60., 180.));
let evidence = artifact(&mut project, view);
let architecture = diagram(&mut project, Layer::Sa, ProjectionKind::Overview);
let architecture_node = project.place(architecture, machine, pos(0., 0.));
let mut expected = project.clone();
for id in [machine, region, state] {
expected.elements.remove(&id);
}
expected.diagrams.remove(&view);
expected.artifact_links.remove(&evidence);
expected
.diagrams
.get_mut(&architecture)
.unwrap()
.nodes
.remove(&architecture_node);
check(
&project,
view,
&[
Target::Placement(machine_node),
Target::Placement(state_node),
],
&expected,
&[machine, state],
&[
machine,
region,
state,
view,
machine_node,
region_node,
state_node,
evidence,
architecture_node,
],
&[architecture],
);
}
#[test]
fn trace_edge_and_endpoint_deletion_preserve_counterpart_unless_explicitly_selected() {
let mut project = Project::empty();
let oa = project
.add_element(Kind::Activity, Layer::Oa, "Operational", None)
.unwrap();
let sa = project
.add_element(Kind::Function, Layer::Sa, "System", None)
.unwrap();
let trace = Id::new_v4();
project.traces.insert(
trace,
Trace {
id: trace,
alias: "refinement".into(),
name: "Refinement".into(),
description: String::new(),
oa,
sa,
kind: TraceKind::Refinement,
baseline: None,
},
);
let evidence = artifact(&mut project, trace);
let view = diagram(&mut project, Layer::Oa, ProjectionKind::Traceability);
let noa = project.place(view, oa, pos(0., 0.));
let nsa = project.place(view, sa, pos(700., 0.));
project
.diagrams
.get_mut(&view)
.unwrap()
.routes
.push(route(trace));
let mut expected = project.clone();
expected.traces.remove(&trace);
expected.artifact_links.remove(&evidence);
expected.diagrams.get_mut(&view).unwrap().routes.clear();
check(
&project,
view,
&[Target::Edge(trace)],
&expected,
&[trace],
&[trace, evidence],
&[view],
);
expected.elements.remove(&oa);
expected.diagrams.get_mut(&view).unwrap().nodes.remove(&noa);
check(
&project,
view,
&[Target::Placement(noa), Target::Edge(trace)],
&expected,
&[oa, trace],
&[oa, noa, trace, evidence],
&[view],
);
assert_eq!(expected.elements[&sa], project.elements[&sa]);
expected.elements.remove(&sa);
expected.diagrams.get_mut(&view).unwrap().nodes.remove(&nsa);
check(
&project,
view,
&[
Target::Placement(noa),
Target::Placement(nsa),
Target::Edge(trace),
],
&expected,
&[oa, sa, trace],
&[oa, noa, sa, nsa, trace, evidence],
&[view],
);
}
#[test]
fn surviving_semantic_records_report_reference_pruning_as_changed() {
let mut project = Project::empty();
let work = project
.add_element(Kind::Function, Layer::Sa, "Work", None)
.unwrap();
let machine = project
.add_element(Kind::StateMachine, Layer::Sa, "Modes", None)
.unwrap();
let region = project
.add_element(Kind::Region, Layer::Sa, "Main", Some(machine))
.unwrap();
let a = project
.add_element(Kind::State, Layer::Sa, "A", Some(region))
.unwrap();
let b = project
.add_element(Kind::State, Layer::Sa, "B", Some(region))
.unwrap();
let transition = project.relate(RelationKind::StateTransition, "Advance", a, b);
if let RelationBody::StateTransition { effects, .. } =
&mut project.relations.get_mut(&transition).unwrap().body
{
*effects = vec![work];
}
let chain = project
.add_element(Kind::FunctionalChain, Layer::Sa, "Chain", None)
.unwrap();
let occurrence = project
.add_element(Kind::Occurrence, Layer::Sa, "Action", Some(chain))
.unwrap();
if let Some(SharedBody::Occurrence { body, .. }) =
project.elements.get_mut(&occurrence).unwrap().shared_mut()
{
*body = OccurrenceBody::Action {
action: Some(work),
lifeline: None,
};
}
project
.elements
.get_mut(&chain)
.unwrap()
.steps_mut()
.unwrap()
.push(occurrence);
let view = diagram(&mut project, Layer::Sa, ProjectionKind::Overview);
let placement = project.place(view, work, pos(100., 100.));
let mut expected = project.clone();
expected.elements.remove(&work);
expected
.diagrams
.get_mut(&view)
.unwrap()
.nodes
.remove(&placement);
if let RelationBody::StateTransition { effects, .. } =
&mut expected.relations.get_mut(&transition).unwrap().body
{
effects.clear();
}
if let Some(SharedBody::Occurrence {
body: OccurrenceBody::Action { action, .. },
..
}) = expected.elements.get_mut(&occurrence).unwrap().shared_mut()
{
*action = None;
}
check(
&project,
view,
&[Target::Placement(placement)],
&expected,
&[work],
&[work, placement],
&[view, occurrence, transition],
);
}
#[test]
fn valid_union_is_not_rejected_by_an_invalid_intermediate_singleton() {
let mut project = Project::empty();
let sender = project
.add_element(Kind::SystemActor, Layer::Sa, "Sender", None)
.unwrap();
let receiver = project
.add_element(Kind::System, Layer::Sa, "Receiver", None)
.unwrap();
let mut ports = vec![];
for owner in [sender, receiver] {
let id = Id::new_v4();
let port = make_port(
&project,
id,
owner,
"Service port",
PortContract::Component {
kind: ComponentPortKind::Standard,
orientation: ComponentPortOrientation::Unset,
},
)
.unwrap();
project.elements.insert(id, port);
ports.push(id);
}
// Ensure the planner's deterministic tie break orders this invalid-alone
// deletion first; a singleton transaction implementation would fail here.
let protocol = Id::from_u128(1);
let mut protocol_element = project
.make_element(Kind::Interface, Layer::Sa, "Payload protocol", None)
.unwrap();
protocol_element.id = protocol;
project.elements.insert(protocol, protocol_element);
let empty_protocol = project
.add_element(Kind::Interface, Layer::Sa, "Other protocol", None)
.unwrap();
let item = project
.add_element(Kind::ExchangeItem, Layer::Sa, "Payload", None)
.unwrap();
let allocation = project.relate(
RelationKind::InterfaceItemAllocation,
"Declares payload",
protocol,
item,
);
let role = project.relate(
RelationKind::InterfaceRequirement,
"Requires payload",
ports[0],
protocol,
);
project.relate(
RelationKind::InterfaceRequirement,
"Requires other",
ports[0],
empty_protocol,
);
let boundary = project.relate(
RelationKind::BoundaryExchange,
"Channel",
ports[0],
ports[1],
);
if let RelationBody::BoundaryExchange { items, .. } =
&mut project.relations.get_mut(&boundary).unwrap().body
{
*items = vec![item];
}
let view = diagram(&mut project, Layer::Sa, ProjectionKind::Overview);
project.place(view, sender, pos(0., 0.));
project.place(view, receiver, pos(800., 0.));
let protocol_node = project.place(view, protocol, pos(400., 300.));
project
.diagrams
.get_mut(&view)
.unwrap()
.routes
.push(route(boundary));
validate_candidate(&project).unwrap();
// The remaining interface declaration becomes an empty constraint on the
// live channel. This is an invalid intermediate state, but deleting the
// selected channel in the SAME batch makes the complete intent valid.
assert!(execute_batch(&project, &[Command::Delete(protocol)]).is_err());
assert!(prepare(&project, view, &[Target::Placement(protocol_node)]).is_err());
let mut expected = project.clone();
expected.elements.remove(&protocol);
for id in [allocation, role, boundary] {
expected.relations.remove(&id);
}
let d = expected.diagrams.get_mut(&view).unwrap();
d.nodes.remove(&protocol_node);
d.routes.clear();
for targets in [
vec![Target::Placement(protocol_node), Target::Edge(boundary)],
vec![Target::Edge(boundary), Target::Placement(protocol_node)],
] {
check(
&project,
view,
&targets,
&expected,
&[protocol, boundary],
&[protocol, protocol_node, allocation, role, boundary],
&[view],
);
}
}
#[test]
fn every_original_target_must_be_visible_local_supported_and_unique_even_when_covered() {
let mut project = Project::empty();
let a = project
.add_element(Kind::Activity, Layer::Oa, "A", None)
.unwrap();
let child = project
.add_element(Kind::Activity, Layer::Oa, "Child", Some(a))
.unwrap();
let b = project
.add_element(Kind::Activity, Layer::Oa, "B", None)
.unwrap();
let edge = project.relate(RelationKind::Interaction, "Flow", child, b);
let view = diagram(&mut project, Layer::Oa, ProjectionKind::Overview);
let na = project.place(view, a, pos(0., 0.));
let nc = project.place(view, child, pos(30., 90.));
let nb = project.place(view, b, pos(700., 0.));
project
.diagrams
.get_mut(&view)
.unwrap()
.nodes
.get_mut(&na)
.unwrap()
.collapsed = true;
project
.diagrams
.get_mut(&view)
.unwrap()
.view_options
.hidden_edges
.push(edge);
let other = diagram(&mut project, Layer::Oa, ProjectionKind::Overview);
let foreign = project.place(other, child, pos(0., 0.));
validate_candidate(&project).unwrap();
let original = project.clone();
for inadmissible in [
Target::Whole,
Target::Placement(nc),
Target::Placement(foreign),
Target::Placement(Id::new_v4()),
Target::Edge(edge),
Target::Edge(Id::new_v4()),
Target::Edge(a),
Target::Placement(a),
Target::Port {
port: child,
placement: na,
},
Target::StateRow {
placement: na,
relation: edge,
},
Target::AllocationGrouping {
relation: edge,
work_placement: nc,
performer_placement: nb,
},
Target::ProcessLink(edge),
Target::ScenarioObject(child),
Target::ProcessOccurrence(OccurrenceEndpoint {
references: vec![],
occurrence: child,
}),
Target::ProcessPreviewLink {
relation: edge,
source: OccurrenceEndpoint {
references: vec![],
occurrence: a,
},
target: OccurrenceEndpoint {
references: vec![],
occurrence: b,
},
},
] {
assert!(
prepare(
&project,
view,
&[Target::Placement(na), inadmissible.clone()]
)
.is_err(),
"accepted {inadmissible:?}"
);
assert_eq!(project, original);
}
assert!(prepare(&project, view, &[]).is_err());
assert!(prepare(&project, Id::new_v4(), &[Target::Placement(na)]).is_err());
assert!(
prepare(
&project,
view,
&[Target::Placement(na), Target::Placement(na)]
)
.is_err()
);
}
#[test]
fn scenario_surface_and_invalid_duplicate_placements_are_rejected_before_planning() {
let mut project = Project::empty();
let activity = project
.add_element(Kind::Activity, Layer::Oa, "Work", None)
.unwrap();
let view = diagram(&mut project, Layer::Oa, ProjectionKind::Overview);
let node = project.place(view, activity, pos(0., 0.));
let scenario = project
.add_element(Kind::Scenario, Layer::Oa, "Scenario", None)
.unwrap();
let scenario_view = diagram(
&mut project,
Layer::Oa,
ProjectionKind::Scenario { scenario },
);
assert!(prepare(&project, scenario_view, &[Target::Placement(node)]).is_err());
let duplicate = Id::new_v4();
let mut placement = project.diagrams[&view].nodes[&node].clone();
placement.id = duplicate;
placement.alias = "duplicate_placement".into();
project
.diagrams
.get_mut(&view)
.unwrap()
.nodes
.insert(duplicate, placement);
assert!(
prepare(
&project,
view,
&[Target::Placement(node), Target::Placement(duplicate)]
)
.unwrap_err()
.contains("duplicate_placement")
);
}