Preserve exchange allocations during atomic replacement and reserve generated aliases
This commit is contained in:
+95
-24
@@ -251,6 +251,20 @@ pub(crate) fn functional_exchange_commands(
|
|||||||
) -> Result<Vec<Command>, ModelError> {
|
) -> Result<Vec<Command>, ModelError> {
|
||||||
vacant(p, request.id)?;
|
vacant(p, request.id)?;
|
||||||
let mut working = p.clone();
|
let mut working = p.clone();
|
||||||
|
let mut relation = Relation {
|
||||||
|
id: request.id,
|
||||||
|
alias: request.alias.clone(),
|
||||||
|
name: request.name.clone(),
|
||||||
|
description: request.description.clone(),
|
||||||
|
body: RelationBody::new(
|
||||||
|
RelationKind::FunctionalExchange,
|
||||||
|
request.source,
|
||||||
|
request.target,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
// Reserve the authored alias as well as identity before naming generated
|
||||||
|
// endpoints. This draft exists only in the planner's private candidate.
|
||||||
|
working.relations.insert(relation.id, relation.clone());
|
||||||
let mut commands = Vec::new();
|
let mut commands = Vec::new();
|
||||||
let label = if request.name.trim().is_empty() {
|
let label = if request.name.trim().is_empty() {
|
||||||
"Exchange"
|
"Exchange"
|
||||||
@@ -289,19 +303,14 @@ pub(crate) fn functional_exchange_commands(
|
|||||||
&request.items,
|
&request.items,
|
||||||
&mut commands,
|
&mut commands,
|
||||||
)?;
|
)?;
|
||||||
commands.push(Command::CreateRelation(Relation {
|
relation.body = RelationBody::FunctionalExchange {
|
||||||
id: request.id,
|
source: request.source,
|
||||||
alias: request.alias.clone(),
|
target: request.target,
|
||||||
name: request.name.clone(),
|
source_port,
|
||||||
description: request.description.clone(),
|
target_port,
|
||||||
body: RelationBody::FunctionalExchange {
|
items: request.items.clone(),
|
||||||
source: request.source,
|
};
|
||||||
target: request.target,
|
commands.push(Command::CreateRelation(relation));
|
||||||
source_port,
|
|
||||||
target_port,
|
|
||||||
items: request.items.clone(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
Ok(commands)
|
Ok(commands)
|
||||||
}
|
}
|
||||||
fn add_allocation(
|
fn add_allocation(
|
||||||
@@ -392,6 +401,14 @@ pub fn plan_component_exchange(
|
|||||||
target_port,
|
target_port,
|
||||||
} => {
|
} => {
|
||||||
vacant(&working, *id)?;
|
vacant(&working, *id)?;
|
||||||
|
let mut relation = Relation {
|
||||||
|
id: *id,
|
||||||
|
alias: alias.clone(),
|
||||||
|
name: name.clone(),
|
||||||
|
description: description.clone(),
|
||||||
|
body: RelationBody::new(RelationKind::BoundaryExchange, Id::nil(), Id::nil()),
|
||||||
|
};
|
||||||
|
working.relations.insert(*id, relation.clone());
|
||||||
let contract = |outgoing| PortContract::Component {
|
let contract = |outgoing| PortContract::Component {
|
||||||
kind: *port_kind,
|
kind: *port_kind,
|
||||||
orientation: if *port_kind == ComponentPortKind::Standard {
|
orientation: if *port_kind == ComponentPortKind::Standard {
|
||||||
@@ -424,17 +441,11 @@ pub fn plan_component_exchange(
|
|||||||
contract(false),
|
contract(false),
|
||||||
)?;
|
)?;
|
||||||
let b = select_port(&mut working, *target_port, input_port, items, &mut commands)?;
|
let b = select_port(&mut working, *target_port, input_port, items, &mut commands)?;
|
||||||
let relation = Relation {
|
relation.body = RelationBody::BoundaryExchange {
|
||||||
id: *id,
|
source: a,
|
||||||
alias: alias.clone(),
|
target: b,
|
||||||
name: name.clone(),
|
oriented: *oriented,
|
||||||
description: description.clone(),
|
items: items.clone(),
|
||||||
body: RelationBody::BoundaryExchange {
|
|
||||||
source: a,
|
|
||||||
target: b,
|
|
||||||
oriented: *oriented,
|
|
||||||
items: items.clone(),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
working.relations.insert(*id, relation.clone());
|
working.relations.insert(*id, relation.clone());
|
||||||
commands.push(Command::CreateRelation(relation));
|
commands.push(Command::CreateRelation(relation));
|
||||||
@@ -646,6 +657,66 @@ pub fn plan_function_port_items(
|
|||||||
finish(p, vec![Command::EditElement(port, e)])
|
finish(p, vec![Command::EditElement(port, e)])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether removing these allocation facts breaks a typed exchange mapping.
|
||||||
|
/// Legacy or malformed endpoints are left to their own validation rules; this
|
||||||
|
/// helper only determines the conditional dependency on PortAllocation facts.
|
||||||
|
pub(crate) fn missing_exchange_port_allocations(
|
||||||
|
p: &Project,
|
||||||
|
relation: &Relation,
|
||||||
|
removed: &BTreeSet<Id>,
|
||||||
|
) -> bool {
|
||||||
|
let Some(Relation {
|
||||||
|
body:
|
||||||
|
RelationBody::BoundaryExchange {
|
||||||
|
source: a,
|
||||||
|
target: b,
|
||||||
|
oriented,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
..
|
||||||
|
}) = p.relations.get(&relation.source())
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Some(Relation {
|
||||||
|
body:
|
||||||
|
RelationBody::FunctionalExchange {
|
||||||
|
source_port,
|
||||||
|
target_port,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
..
|
||||||
|
}) = p.relations.get(&relation.target())
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if ![*a, *b].iter().all(|id| {
|
||||||
|
p.elements
|
||||||
|
.get(id)
|
||||||
|
.is_some_and(|e| matches!(port_contract(e), Some(PortContract::Component { .. })))
|
||||||
|
}) || !p
|
||||||
|
.elements
|
||||||
|
.get(source_port)
|
||||||
|
.is_some_and(|e| matches!(port_contract(e), Some(PortContract::FunctionOutput { .. })))
|
||||||
|
|| !p
|
||||||
|
.elements
|
||||||
|
.get(target_port)
|
||||||
|
.is_some_and(|e| matches!(port_contract(e), Some(PortContract::FunctionInput { .. })))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let allocated = |component, function| {
|
||||||
|
p.relations.values().any(|allocation| {
|
||||||
|
allocation.kind() == RelationKind::PortAllocation
|
||||||
|
&& !removed.contains(&allocation.id)
|
||||||
|
&& allocation.source() == component
|
||||||
|
&& allocation.target() == function
|
||||||
|
})
|
||||||
|
};
|
||||||
|
!((allocated(*a, *source_port) && allocated(*b, *target_port))
|
||||||
|
|| (!*oriented && allocated(*b, *source_port) && allocated(*a, *target_port)))
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn validate_port(p: &Project, e: &Element, out: &mut Vec<Issue>) {
|
pub(crate) fn validate_port(p: &Project, e: &Element, out: &mut Vec<Issue>) {
|
||||||
let Some(SharedBody::Port { contract, legacy }) = e.shared() else {
|
let Some(SharedBody::Port { contract, legacy }) = e.shared() else {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -203,6 +203,9 @@ pub fn remap_references(p: &mut Project, old: Id, new: Id) {
|
|||||||
}
|
}
|
||||||
/// IDs removed by a cascade. Cross-layer traced counterparts are never included.
|
/// IDs removed by a cascade. Cross-layer traced counterparts are never included.
|
||||||
pub fn deletion_impact(p: &Project, id: Id) -> BTreeSet<Id> {
|
pub fn deletion_impact(p: &Project, id: Id) -> BTreeSet<Id> {
|
||||||
|
deletion_impact_inner(p, id, false)
|
||||||
|
}
|
||||||
|
fn deletion_impact_inner(p: &Project, id: Id, defer_exchange_allocations: bool) -> BTreeSet<Id> {
|
||||||
let mut removed = BTreeSet::from([id]);
|
let mut removed = BTreeSet::from([id]);
|
||||||
loop {
|
loop {
|
||||||
let before = removed.len();
|
let before = removed.len();
|
||||||
@@ -254,12 +257,15 @@ pub fn deletion_impact(p: &Project, id: Id) -> BTreeSet<Id> {
|
|||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if p.relations.values().any(|allocation| {
|
if !defer_exchange_allocations
|
||||||
allocation.kind() == RelationKind::PortAllocation
|
&& p.relations.values().any(|allocation| {
|
||||||
&& removed.contains(&allocation.id)
|
allocation.kind() == RelationKind::PortAllocation
|
||||||
&& [boundary.source(), boundary.target()].contains(&allocation.source())
|
&& removed.contains(&allocation.id)
|
||||||
&& [*source_port, *target_port].contains(&allocation.target())
|
&& [boundary.source(), boundary.target()].contains(&allocation.source())
|
||||||
}) {
|
&& [*source_port, *target_port].contains(&allocation.target())
|
||||||
|
})
|
||||||
|
&& crate::ports::missing_exchange_port_allocations(p, r, &removed)
|
||||||
|
{
|
||||||
removed.insert(r.id);
|
removed.insert(r.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -288,6 +294,40 @@ pub fn deletion_impact(p: &Project, id: Id) -> BTreeSet<Id> {
|
|||||||
}
|
}
|
||||||
pub fn delete_cascade(p: &mut Project, id: Id) {
|
pub fn delete_cascade(p: &mut Project, id: Id) {
|
||||||
let removed = deletion_impact(p, id);
|
let removed = deletion_impact(p, id);
|
||||||
|
remove_objects(p, removed);
|
||||||
|
}
|
||||||
|
/// Preserve conditional exchange-allocation dependents until the transaction
|
||||||
|
/// has had the opportunity to supply explicit replacement allocation facts.
|
||||||
|
pub(crate) fn delete_cascade_deferred(p: &mut Project, id: Id, pending: &mut BTreeSet<Id>) {
|
||||||
|
let complete = deletion_impact(p, id);
|
||||||
|
let removed = deletion_impact_inner(p, id, true);
|
||||||
|
// An explicitly removed relation cannot pass its conditional cascade on
|
||||||
|
// to a new relation that later reuses the same identity in this batch.
|
||||||
|
pending.retain(|id| !removed.contains(id));
|
||||||
|
pending.extend(
|
||||||
|
complete
|
||||||
|
.difference(&removed)
|
||||||
|
.filter(|id| {
|
||||||
|
p.relations
|
||||||
|
.get(id)
|
||||||
|
.is_some_and(|r| r.kind() == RelationKind::ExchangeAllocation)
|
||||||
|
})
|
||||||
|
.copied(),
|
||||||
|
);
|
||||||
|
remove_objects(p, removed);
|
||||||
|
}
|
||||||
|
pub(crate) fn finish_deletions(p: &mut Project, pending: &BTreeSet<Id>) {
|
||||||
|
let none = BTreeSet::new();
|
||||||
|
for id in pending {
|
||||||
|
if p.relations.get(id).is_some_and(|r| {
|
||||||
|
r.kind() == RelationKind::ExchangeAllocation
|
||||||
|
&& crate::ports::missing_exchange_port_allocations(p, r, &none)
|
||||||
|
}) {
|
||||||
|
delete_cascade(p, *id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn remove_objects(p: &mut Project, removed: BTreeSet<Id>) {
|
||||||
let removed_names: std::collections::BTreeMap<_, _> = removed
|
let removed_names: std::collections::BTreeMap<_, _> = removed
|
||||||
.iter()
|
.iter()
|
||||||
.map(|id| (*id, p.named(*id).to_string()))
|
.map(|id| (*id, p.named(*id).to_string()))
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::*;
|
use crate::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::BTreeSet;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Error, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Error, Serialize, Deserialize)]
|
||||||
@@ -135,7 +136,9 @@ impl Session {
|
|||||||
}
|
}
|
||||||
pub fn execute(&mut self, command: Command) -> Result<Option<Id>, ModelError> {
|
pub fn execute(&mut self, command: Command) -> Result<Option<Id>, ModelError> {
|
||||||
let mut candidate = self.project.clone();
|
let mut candidate = self.project.clone();
|
||||||
let selected = apply_command(&mut candidate, &command, 0)?;
|
let mut pending_deletions = BTreeSet::new();
|
||||||
|
let selected = apply_command(&mut candidate, &command, 0, &mut pending_deletions)?;
|
||||||
|
crate::references::finish_deletions(&mut candidate, &pending_deletions);
|
||||||
validate_candidate(&candidate)?;
|
validate_candidate(&candidate)?;
|
||||||
if candidate != self.project {
|
if candidate != self.project {
|
||||||
self.past
|
self.past
|
||||||
@@ -161,9 +164,11 @@ pub fn validate_candidate(p: &Project) -> Result<(), ModelError> {
|
|||||||
}
|
}
|
||||||
pub fn execute_batch(project: &Project, commands: &[Command]) -> Result<Project, ModelError> {
|
pub fn execute_batch(project: &Project, commands: &[Command]) -> Result<Project, ModelError> {
|
||||||
let mut candidate = project.clone();
|
let mut candidate = project.clone();
|
||||||
|
let mut pending_deletions = BTreeSet::new();
|
||||||
for command in commands {
|
for command in commands {
|
||||||
apply_command(&mut candidate, command, 0)?;
|
apply_command(&mut candidate, command, 0, &mut pending_deletions)?;
|
||||||
}
|
}
|
||||||
|
crate::references::finish_deletions(&mut candidate, &pending_deletions);
|
||||||
validate_candidate(&candidate)?;
|
validate_candidate(&candidate)?;
|
||||||
Ok(candidate)
|
Ok(candidate)
|
||||||
}
|
}
|
||||||
@@ -224,7 +229,12 @@ fn attach(p: &mut Project, id: Id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn apply_command(p: &mut Project, c: &Command, depth: usize) -> Result<Option<Id>, ModelError> {
|
fn apply_command(
|
||||||
|
p: &mut Project,
|
||||||
|
c: &Command,
|
||||||
|
depth: usize,
|
||||||
|
pending_deletions: &mut BTreeSet<Id>,
|
||||||
|
) -> Result<Option<Id>, ModelError> {
|
||||||
if depth > 64 {
|
if depth > 64 {
|
||||||
return Err(ModelError(
|
return Err(ModelError(
|
||||||
"Command batch nesting exceeds 64 levels.".into(),
|
"Command batch nesting exceeds 64 levels.".into(),
|
||||||
@@ -233,12 +243,13 @@ fn apply_command(p: &mut Project, c: &Command, depth: usize) -> Result<Option<Id
|
|||||||
let selected = match c {
|
let selected = match c {
|
||||||
Command::Replace(value) => {
|
Command::Replace(value) => {
|
||||||
*p = value.clone();
|
*p = value.clone();
|
||||||
|
pending_deletions.clear();
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
Command::Batch(commands) => {
|
Command::Batch(commands) => {
|
||||||
let mut selected = None;
|
let mut selected = None;
|
||||||
for command in commands {
|
for command in commands {
|
||||||
let id = apply_command(p, command, depth + 1)?;
|
let id = apply_command(p, command, depth + 1, pending_deletions)?;
|
||||||
if id.is_some() {
|
if id.is_some() {
|
||||||
selected = id;
|
selected = id;
|
||||||
}
|
}
|
||||||
@@ -318,6 +329,9 @@ fn apply_command(p: &mut Project, c: &Command, depth: usize) -> Result<Option<Id
|
|||||||
v.id = *old;
|
v.id = *old;
|
||||||
p.relations.insert(*old, v);
|
p.relations.insert(*old, v);
|
||||||
reidentify(p, *old, value.id)?;
|
reidentify(p, *old, value.id)?;
|
||||||
|
if pending_deletions.remove(old) {
|
||||||
|
pending_deletions.insert(value.id);
|
||||||
|
}
|
||||||
Some(value.id)
|
Some(value.id)
|
||||||
}
|
}
|
||||||
Command::CreateDiagram(value) => {
|
Command::CreateDiagram(value) => {
|
||||||
@@ -438,6 +452,9 @@ fn apply_command(p: &mut Project, c: &Command, depth: usize) -> Result<Option<Id
|
|||||||
}
|
}
|
||||||
Command::Reidentify(old, new) => {
|
Command::Reidentify(old, new) => {
|
||||||
reidentify(p, *old, *new)?;
|
reidentify(p, *old, *new)?;
|
||||||
|
if pending_deletions.remove(old) {
|
||||||
|
pending_deletions.insert(*new);
|
||||||
|
}
|
||||||
Some(*new)
|
Some(*new)
|
||||||
}
|
}
|
||||||
Command::Rename(id, value) => {
|
Command::Rename(id, value) => {
|
||||||
@@ -469,7 +486,7 @@ fn apply_command(p: &mut Project, c: &Command, depth: usize) -> Result<Option<Id
|
|||||||
items: vec![],
|
items: vec![],
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
return apply_command(p, &Command::Batch(commands), depth + 1);
|
return apply_command(p, &Command::Batch(commands), depth + 1, pending_deletions);
|
||||||
}
|
}
|
||||||
Some(p.relate(*kind, name, *source, *target))
|
Some(p.relate(*kind, name, *source, *target))
|
||||||
}
|
}
|
||||||
@@ -480,7 +497,7 @@ fn apply_command(p: &mut Project, c: &Command, depth: usize) -> Result<Option<Id
|
|||||||
if !p.contains(*id) {
|
if !p.contains(*id) {
|
||||||
return Err(missing());
|
return Err(missing());
|
||||||
}
|
}
|
||||||
crate::references::delete_cascade(p, *id);
|
crate::references::delete_cascade_deferred(p, *id, pending_deletions);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
Command::AddStep(owner, exchange) => {
|
Command::AddStep(owner, exchange) => {
|
||||||
@@ -519,7 +536,7 @@ fn apply_command(p: &mut Project, c: &Command, depth: usize) -> Result<Option<Id
|
|||||||
.and_then(Element::steps)
|
.and_then(Element::steps)
|
||||||
.and_then(|s| s.get(*index))
|
.and_then(|s| s.get(*index))
|
||||||
.ok_or_else(|| ModelError("Step index is out of range.".into()))?;
|
.ok_or_else(|| ModelError("Step index is out of range.".into()))?;
|
||||||
crate::references::delete_cascade(p, id);
|
crate::references::delete_cascade_deferred(p, id, pending_deletions);
|
||||||
Some(*owner)
|
Some(*owner)
|
||||||
}
|
}
|
||||||
Command::MoveStep(owner, from, to) => {
|
Command::MoveStep(owner, from, to) => {
|
||||||
|
|||||||
@@ -137,6 +137,245 @@ fn ambiguous_reuse_creates_a_fresh_port_and_collisions_never_overwrite() {
|
|||||||
assert_eq!(p.elements[&other], occupied);
|
assert_eq!(p.elements[&other], occupied);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generated_ports_respect_authored_exchange_aliases() {
|
||||||
|
let mut p = Project::empty();
|
||||||
|
let a = add(&mut p, Kind::Function, Layer::Sa, "Send");
|
||||||
|
let b = add(&mut p, Kind::Function, Layer::Sa, "Receive");
|
||||||
|
for alias in ["message_output", "message_input"] {
|
||||||
|
let mut req = request(&p, a, b, vec![]);
|
||||||
|
req.alias = alias.into();
|
||||||
|
req.source_port = PortChoice::Create;
|
||||||
|
req.target_port = PortChoice::Create;
|
||||||
|
let commands = plan_functional_exchange(&p, &req).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_value(&commands).unwrap(),
|
||||||
|
serde_json::to_value(plan_functional_exchange(&p, &req).unwrap()).unwrap()
|
||||||
|
);
|
||||||
|
let result = execute_batch(&p, &commands).unwrap();
|
||||||
|
assert_eq!(result.relations[&req.id].alias, alias);
|
||||||
|
let (output, input) = endpoints(&result, req.id);
|
||||||
|
assert_eq!(output, Id::new_v5(&req.id, b"function-source-port"));
|
||||||
|
assert_eq!(input, Id::new_v5(&req.id, b"function-target-port"));
|
||||||
|
assert_ne!(result.elements[&output].alias, alias);
|
||||||
|
assert_ne!(result.elements[&input].alias, alias);
|
||||||
|
assert!(!p.contains(req.id));
|
||||||
|
req.alias = p.elements[&a].alias.clone();
|
||||||
|
assert!(plan_functional_exchange(&p, &req).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
let (mut p, a, b, _, fe) = fixture();
|
||||||
|
allocate(&mut p, a, b);
|
||||||
|
for alias in ["boundary_source_port", "boundary_target_port"] {
|
||||||
|
let mut req = component_request(&p, fe, ComponentPortKind::Flow);
|
||||||
|
let id = boundary_id(&req);
|
||||||
|
if let ComponentExchangeChoice::Create {
|
||||||
|
alias: authored,
|
||||||
|
source_port,
|
||||||
|
target_port,
|
||||||
|
..
|
||||||
|
} = &mut req.boundary
|
||||||
|
{
|
||||||
|
*authored = alias.into();
|
||||||
|
*source_port = PortChoice::Create;
|
||||||
|
*target_port = PortChoice::Create;
|
||||||
|
}
|
||||||
|
let commands = plan_component_exchange(&p, &req).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_value(&commands).unwrap(),
|
||||||
|
serde_json::to_value(plan_component_exchange(&p, &req).unwrap()).unwrap()
|
||||||
|
);
|
||||||
|
let result = execute_batch(&p, &commands).unwrap();
|
||||||
|
assert_eq!(result.relations[&id].alias, alias);
|
||||||
|
let (source, target) = result.relations[&id].body.endpoints();
|
||||||
|
assert_ne!(result.elements[&source].alias, alias);
|
||||||
|
assert_ne!(result.elements[&target].alias, alias);
|
||||||
|
assert!(!p.contains(id));
|
||||||
|
if let ComponentExchangeChoice::Create { alias, .. } = &mut req.boundary {
|
||||||
|
*alias = p.elements[&a].alias.clone();
|
||||||
|
}
|
||||||
|
assert!(plan_component_exchange(&p, &req).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn allocated_fixture() -> (Project, Id, Id, Id) {
|
||||||
|
let (mut p, a, b, _, fe) = fixture();
|
||||||
|
allocate(&mut p, a, b);
|
||||||
|
let req = component_request(&p, fe, ComponentPortKind::Flow);
|
||||||
|
p = execute_batch(&p, &plan_component_exchange(&p, &req).unwrap()).unwrap();
|
||||||
|
let allocation = p
|
||||||
|
.relations
|
||||||
|
.values()
|
||||||
|
.find(|r| r.kind() == RelationKind::PortAllocation)
|
||||||
|
.unwrap()
|
||||||
|
.id;
|
||||||
|
let exchange_allocation = p
|
||||||
|
.relations
|
||||||
|
.values()
|
||||||
|
.find(|r| r.kind() == RelationKind::ExchangeAllocation)
|
||||||
|
.unwrap()
|
||||||
|
.id;
|
||||||
|
p.relations
|
||||||
|
.get_mut(&exchange_allocation)
|
||||||
|
.unwrap()
|
||||||
|
.description = "Retain this authored mapping rationale".into();
|
||||||
|
let artifact = Id::new_v4();
|
||||||
|
p.artifact_links.insert(
|
||||||
|
artifact,
|
||||||
|
ArtifactLink {
|
||||||
|
id: artifact,
|
||||||
|
alias: "mapping_evidence".into(),
|
||||||
|
name: "Allocation evidence".into(),
|
||||||
|
description: "Keep the original evidence".into(),
|
||||||
|
owner: exchange_allocation,
|
||||||
|
relation: ArtifactRelation::References,
|
||||||
|
uri: "docs/port-rationale.md".into(),
|
||||||
|
revision: Some("r1".into()),
|
||||||
|
fragment: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
validate_candidate(&p).unwrap();
|
||||||
|
(p, allocation, exchange_allocation, artifact)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replacement_port_allocation_retains_exchange_mapping_and_evidence_in_either_order() {
|
||||||
|
let (p, old, exchange_allocation, artifact) = allocated_fixture();
|
||||||
|
let mut replacement = p.relations[&old].clone();
|
||||||
|
replacement.id = Id::new_v4();
|
||||||
|
replacement.alias = "replacement_mapping".into();
|
||||||
|
for delete_first in [false, true] {
|
||||||
|
let mut commands = vec![
|
||||||
|
Command::CreateRelation(replacement.clone()),
|
||||||
|
Command::Delete(old),
|
||||||
|
];
|
||||||
|
if delete_first {
|
||||||
|
commands.reverse();
|
||||||
|
}
|
||||||
|
let mut expected = p.clone();
|
||||||
|
expected.relations.remove(&old);
|
||||||
|
expected
|
||||||
|
.relations
|
||||||
|
.insert(replacement.id, replacement.clone());
|
||||||
|
let mut session = Session::new(p.clone());
|
||||||
|
session.execute(Command::Batch(commands)).unwrap();
|
||||||
|
assert_eq!(session.project(), &expected);
|
||||||
|
assert_eq!(
|
||||||
|
session.project().relations[&exchange_allocation],
|
||||||
|
p.relations[&exchange_allocation]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
session.project().artifact_links[&artifact],
|
||||||
|
p.artifact_links[&artifact]
|
||||||
|
);
|
||||||
|
assert_eq!(session.revision(), 1);
|
||||||
|
session.undo();
|
||||||
|
assert_eq!(session.project(), &p);
|
||||||
|
assert!(!session.can_undo());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deferred_deletion_preserves_preview_explicit_deletion_and_reidentification() {
|
||||||
|
let (p, old, exchange_allocation, artifact) = allocated_fixture();
|
||||||
|
let impact = references::deletion_impact(&p, old);
|
||||||
|
assert!(impact.contains(&exchange_allocation));
|
||||||
|
assert!(impact.contains(&artifact));
|
||||||
|
let result = execute_batch(&p, &[Command::Delete(old)]).unwrap();
|
||||||
|
assert!(impact.iter().all(|id| !result.contains(*id)));
|
||||||
|
let mut expected = p.clone();
|
||||||
|
references::delete_cascade(&mut expected, old);
|
||||||
|
assert_eq!(result, expected);
|
||||||
|
|
||||||
|
let mut replacement = p.relations[&old].clone();
|
||||||
|
replacement.id = Id::new_v4();
|
||||||
|
replacement.alias = "replacement_mapping".into();
|
||||||
|
let explicit = execute_batch(
|
||||||
|
&p,
|
||||||
|
&[
|
||||||
|
Command::Delete(old),
|
||||||
|
Command::Delete(exchange_allocation),
|
||||||
|
Command::CreateRelation(replacement.clone()),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(!explicit.contains(exchange_allocation));
|
||||||
|
assert!(!explicit.contains(artifact));
|
||||||
|
assert_eq!(explicit.relations[&replacement.id], replacement);
|
||||||
|
|
||||||
|
let renamed = Id::new_v4();
|
||||||
|
let mut edited = p.relations[&exchange_allocation].clone();
|
||||||
|
edited.id = renamed;
|
||||||
|
for command in [
|
||||||
|
Command::Reidentify(exchange_allocation, renamed),
|
||||||
|
Command::EditRelation(exchange_allocation, edited),
|
||||||
|
] {
|
||||||
|
let result = execute_batch(&p, &[Command::Delete(old), command.clone()]).unwrap();
|
||||||
|
assert_eq!(result, expected);
|
||||||
|
let preserved = execute_batch(
|
||||||
|
&p,
|
||||||
|
&[
|
||||||
|
Command::Delete(old),
|
||||||
|
command,
|
||||||
|
Command::CreateRelation(replacement.clone()),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
preserved.relations[&renamed].description,
|
||||||
|
p.relations[&exchange_allocation].description
|
||||||
|
);
|
||||||
|
assert_eq!(preserved.artifact_links[&artifact].owner, renamed);
|
||||||
|
}
|
||||||
|
|
||||||
|
let recreated = execute_batch(
|
||||||
|
&p,
|
||||||
|
&[
|
||||||
|
Command::Delete(old),
|
||||||
|
Command::Delete(exchange_allocation),
|
||||||
|
Command::CreateRelation(replacement),
|
||||||
|
Command::CreateRelation(p.relations[&exchange_allocation].clone()),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
recreated.relations[&exchange_allocation],
|
||||||
|
p.relations[&exchange_allocation]
|
||||||
|
);
|
||||||
|
assert!(!recreated.contains(artifact));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_replacement_rolls_back_without_repairing_explicit_replacement_projects() {
|
||||||
|
let (p, old, exchange_allocation, _) = allocated_fixture();
|
||||||
|
let mut replacement = p.relations[&old].clone();
|
||||||
|
replacement.id = Id::new_v4();
|
||||||
|
replacement.alias = "invalid_replacement_mapping".into();
|
||||||
|
// The reverse allocation is an invalid role, even if deletion would remove
|
||||||
|
// its dependent exchange allocation before the final candidate check.
|
||||||
|
replacement.body = RelationBody::PortAllocation {
|
||||||
|
source: replacement.target(),
|
||||||
|
target: replacement.source(),
|
||||||
|
};
|
||||||
|
let mut invalid_project = p.clone();
|
||||||
|
invalid_project.relations.remove(&old);
|
||||||
|
for commands in [
|
||||||
|
vec![Command::Delete(old), Command::CreateRelation(replacement)],
|
||||||
|
vec![Command::Delete(old), Command::Replace(invalid_project)],
|
||||||
|
vec![
|
||||||
|
Command::Delete(old),
|
||||||
|
Command::Delete(exchange_allocation),
|
||||||
|
Command::CreateRelation(p.relations[&exchange_allocation].clone()),
|
||||||
|
],
|
||||||
|
] {
|
||||||
|
let mut session = Session::new(p.clone());
|
||||||
|
assert!(session.execute(Command::Batch(commands)).is_err());
|
||||||
|
assert_eq!(session.project(), &p);
|
||||||
|
assert_eq!(session.revision(), 0);
|
||||||
|
assert!(!session.can_undo());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn component_generation_requires_real_allocations_and_reuses_existing_facts() {
|
fn component_generation_requires_real_allocations_and_reuses_existing_facts() {
|
||||||
let (mut p, a, b, _, fe) = fixture();
|
let (mut p, a, b, _, fe) = fixture();
|
||||||
|
|||||||
Reference in New Issue
Block a user