Files
ARCADIA-Studio/crates/oa-core/tests/release_routing.rs
T

722 lines
23 KiB
Rust

use oa_core::{projection, validation, *};
use std::collections::BTreeMap;
struct Fixture {
project: Project,
diagram: Id,
second: Id,
group: Id,
a: Id,
b: Id,
edge: Id,
external: Id,
}
fn fixture() -> Fixture {
let mut p = Project::empty();
let group = p
.add_element(Kind::Function, Layer::Sa, "Manage", None)
.unwrap();
let a = p
.add_element(Kind::Function, Layer::Sa, "Send", Some(group))
.unwrap();
let b = p
.add_element(Kind::Function, Layer::Sa, "Receive", Some(group))
.unwrap();
let c = p
.add_element(Kind::Function, Layer::Sa, "Outside", None)
.unwrap();
let edge = p.relate(RelationKind::FunctionalExchange, "Internal message", a, b);
let external = p.relate(RelationKind::FunctionalExchange, "External message", b, c);
let diagram = p.ensure_diagram(Layer::Sa);
let second = Id::new_v4();
p.diagrams.insert(
second,
Diagram {
id: second,
alias: "alternate_view".into(),
name: "Alternate view".into(),
description: String::new(),
layer: Layer::Sa,
projection: ProjectionKind::Overview,
view_options: Default::default(),
nodes: BTreeMap::new(),
routes: vec![],
},
);
for d in [diagram, second] {
for (element, x, y) in [
(group, 100., 100.),
(a, 140., 180.),
(b, 140., 320.),
(c, 900., 180.),
] {
p.place(d, element, Position { x, y });
}
}
validate_candidate(&p).unwrap();
Fixture {
project: p,
diagram,
second,
group,
a,
b,
edge,
external,
}
}
fn route(edge: Id) -> EdgeRoute {
EdgeRoute {
edge,
waypoints: vec![Position { x: 550., y: 240. }, Position { x: 550., y: 360. }],
label: Some(EdgeLabel {
fraction: 0.4,
offset: Position { x: 3., y: -16. },
}),
}
}
fn node(p: &Project, diagram: Id, element: Id) -> Id {
p.diagrams[&diagram]
.nodes
.values()
.find(|n| n.element == element)
.unwrap()
.id
}
fn routed(mut f: Fixture) -> Fixture {
for d in [f.diagram, f.second] {
f.project.diagrams.get_mut(&d).unwrap().routes = vec![route(f.edge), route(f.external)];
}
f
}
#[test]
fn route_edits_are_one_history_step_and_identical_edits_preserve_redo() {
let f = fixture();
let mut session = Session::new(f.project.clone());
let command = Command::SetEdgeRoute {
diagram: f.diagram,
route: route(f.edge),
};
session.execute(command.clone()).unwrap();
let authored = session.project().clone();
assert_eq!(authored.identities(), f.project.identities());
assert_eq!(authored.elements, f.project.elements);
assert_eq!(authored.relations, f.project.relations);
assert_eq!(authored.diagrams[&f.second], f.project.diagrams[&f.second]);
assert_eq!(session.revision(), 1);
session.execute(command.clone()).unwrap();
assert_eq!(
session.revision(),
1,
"Identical route has no history entry"
);
session.undo();
assert_eq!(session.project(), &f.project);
let revision = session.revision();
session
.execute(Command::RemoveEdgeRoute {
diagram: f.diagram,
edge: f.edge,
})
.unwrap();
assert_eq!(session.revision(), revision);
assert!(session.can_redo(), "No-op reset must retain undone route");
session.redo();
assert_eq!(session.project(), &authored);
let mut moved = route(f.edge);
moved.waypoints[0].x += 50.;
session
.execute(Command::SetEdgeRoute {
diagram: f.diagram,
route: moved.clone(),
})
.unwrap();
assert_eq!(session.project().diagrams[&f.diagram].routes, vec![moved]);
session.undo();
assert_eq!(session.project(), &authored);
session
.execute(Command::RemoveEdgeRoute {
diagram: f.diagram,
edge: f.edge,
})
.unwrap();
assert_eq!(session.project(), &f.project);
session.undo();
assert_eq!(session.project(), &authored);
}
#[test]
fn route_upsert_preserves_other_entries_and_accepts_an_explicit_empty_default() {
let f = routed(fixture());
let mut edited = route(f.edge);
edited.waypoints.clear();
edited.label = None;
let result = execute_batch(
&f.project,
&[Command::SetEdgeRoute {
diagram: f.diagram,
route: edited.clone(),
}],
)
.unwrap();
assert_eq!(
result.diagrams[&f.diagram].routes,
vec![edited, route(f.external)]
);
assert_eq!(
persistence::decode(&serde_json::to_vec(&result).unwrap()).unwrap(),
result
);
assert_eq!(result.diagrams[&f.second], f.project.diagrams[&f.second]);
}
#[test]
fn invalid_routes_reject_atomically_with_a_located_diagnostic() {
let f = fixture();
let mut cases = vec![];
for edge in [
f.a,
f.diagram,
f.project.id,
node(&f.project, f.diagram, f.a),
Id::new_v4(),
] {
cases.push((route(edge), "route_edge"));
}
for value in [f32::NAN, f32::INFINITY, -f32::INFINITY, MAX_COORDINATE + 1.] {
let mut bad = route(f.edge);
bad.waypoints[0].x = value;
cases.push((bad, "invalid_route_bounds"));
let mut bad = route(f.edge);
bad.label.as_mut().unwrap().offset.y = value;
cases.push((bad, "invalid_route_bounds"));
}
for fraction in [f32::NAN, f32::INFINITY, -0.1, 1.1] {
let mut bad = route(f.edge);
bad.label.as_mut().unwrap().fraction = fraction;
cases.push((bad, "invalid_route_fraction"));
}
let mut oversized = route(f.edge);
oversized.waypoints = vec![Position { x: 0., y: 0. }; MAX_ROUTE_POINTS + 1];
cases.push((oversized, "route_point_limit"));
for (bad, expected) in cases {
let mut session = Session::new(f.project.clone());
let mut candidate = f.project.clone();
candidate
.diagrams
.get_mut(&f.diagram)
.unwrap()
.routes
.push(bad.clone());
let issue = validation::validate(&candidate)
.into_iter()
.find(|i| i.code == expected)
.unwrap();
assert_eq!(issue.element, Some(f.diagram));
assert_eq!(issue.field.as_deref(), Some("routes"));
let result = session.execute(Command::Batch(vec![
Command::Rename(f.a, "Must roll back".into()),
Command::SetEdgeRoute {
diagram: f.diagram,
route: bad,
},
]));
assert!(result.is_err(), "{expected}");
assert_eq!(session.project(), &f.project);
assert_eq!(session.revision(), 0);
assert!(!session.can_undo());
}
let mut duplicated = f.project.diagrams[&f.diagram].clone();
duplicated.routes = vec![route(f.edge), route(f.edge)];
let error =
execute_batch(&f.project, &[Command::EditDiagram(f.diagram, duplicated)]).unwrap_err();
assert!(error.to_string().contains("duplicate_edge_route"));
for command in [
Command::SetEdgeRoute {
diagram: Id::new_v4(),
route: route(f.edge),
},
Command::RemoveEdgeRoute {
diagram: Id::new_v4(),
edge: f.edge,
},
Command::RemoveEdgeRoute {
diagram: f.diagram,
edge: f.a,
},
] {
assert!(execute_batch(&f.project, &[command]).is_err());
}
}
#[test]
fn finite_boundary_values_and_temporarily_hidden_edges_remain_editable() {
let f = fixture();
for fraction in [0., 1.] {
let mut accepted = route(f.edge);
accepted.waypoints = vec![
Position {
x: -MAX_COORDINATE,
y: MAX_COORDINATE
};
MAX_ROUTE_POINTS
];
accepted.label = Some(EdgeLabel {
fraction,
offset: Position {
x: MAX_COORDINATE,
y: -MAX_COORDINATE,
},
});
let mut hidden = f.project.diagrams[&f.diagram].clone();
hidden.projection = ProjectionKind::Capabilities;
hidden.nodes.clear();
hidden.routes = vec![accepted.clone()];
let result = execute_batch(&f.project, &[Command::EditDiagram(f.diagram, hidden)]).unwrap();
assert_eq!(result.diagrams[&f.diagram].routes, vec![accepted]);
assert!(
projection::project_view(&result, f.diagram)
.edges
.is_empty()
);
}
}
#[test]
fn group_movement_translates_internal_routes_once_and_keeps_external_pins() {
for collapsed in [false, true] {
let mut f = routed(fixture());
let parent_node = node(&f.project, f.diagram, f.group);
f.project
.diagrams
.get_mut(&f.diagram)
.unwrap()
.nodes
.get_mut(&parent_node)
.unwrap()
.collapsed = collapsed;
let mut session = Session::new(f.project.clone());
session
.execute(Command::MoveNode {
diagram: f.diagram,
node: parent_node,
position: Position { x: 130., y: 145. },
with_descendants: true,
})
.unwrap();
let moved = session.project();
for child in [f.a, f.b] {
let placement = node(moved, f.diagram, child);
let old = f.project.diagrams[&f.diagram].nodes[&placement].position;
assert_eq!(
moved.diagrams[&f.diagram].nodes[&placement].position,
Position {
x: old.x + 30.,
y: old.y + 45.
}
);
}
let internal = &moved.diagrams[&f.diagram].routes[0];
assert_eq!(
internal.waypoints,
vec![Position { x: 580., y: 285. }, Position { x: 580., y: 405. }]
);
assert_eq!(internal.label, route(f.edge).label);
assert_eq!(moved.diagrams[&f.diagram].routes[1], route(f.external));
assert_eq!(moved.diagrams[&f.second], f.project.diagrams[&f.second]);
assert_eq!(moved.elements, f.project.elements);
assert_eq!(moved.relations, f.project.relations);
session.undo();
assert_eq!(session.project(), &f.project);
session.redo();
assert_eq!(
session.project().diagrams[&f.diagram].routes[0].waypoints[0],
Position { x: 580., y: 285. }
);
}
}
#[test]
fn single_endpoint_raw_edits_and_differing_group_deltas_keep_world_pins() {
let f = routed(fixture());
let a_node = node(&f.project, f.diagram, f.a);
let b_node = node(&f.project, f.diagram, f.b);
let parent_node = node(&f.project, f.diagram, f.group);
for (node, with_descendants) in [(a_node, true), (parent_node, false)] {
let moved = execute_batch(
&f.project,
&[Command::MoveNode {
diagram: f.diagram,
node,
position: Position { x: 260., y: 260. },
with_descendants,
}],
)
.unwrap();
assert_eq!(
moved.diagrams[&f.diagram].routes,
f.project.diagrams[&f.diagram].routes
);
}
let mut changed = f.project.diagrams[&f.diagram].nodes[&a_node].clone();
changed.position = Position { x: 270., y: 290. };
let edited = execute_batch(
&f.project,
&[Command::EditNode {
diagram: f.diagram,
original: a_node,
node: changed,
}],
)
.unwrap();
assert_eq!(
edited.diagrams[&f.diagram].routes,
f.project.diagrams[&f.diagram].routes
);
let deltas = BTreeMap::from([
(a_node, Position { x: 10., y: 20. }),
(b_node, Position { x: 10., y: 21. }),
]);
assert!(
projection::translated_routes(&f.project, &f.project.diagrams[&f.diagram], &deltas)
.is_empty()
);
let deltas = BTreeMap::from([
(a_node, Position { x: 0., y: 0. }),
(b_node, Position { x: 0., y: 0. }),
]);
assert!(
projection::translated_routes(&f.project, &f.project.diagrams[&f.diagram], &deltas)
.is_empty()
);
}
#[test]
fn component_ports_use_actual_owners_in_movement_and_folded_lookup() {
let mut p = Project::empty();
let parent = p
.add_element(Kind::System, Layer::Sa, "System", None)
.unwrap();
let outside = p
.add_element(Kind::SystemActor, Layer::Sa, "Partner", None)
.unwrap();
let a = p
.add_element(Kind::Function, Layer::Sa, "Send", None)
.unwrap();
let b = p
.add_element(Kind::Function, Layer::Sa, "Receive", None)
.unwrap();
p.relate(RelationKind::Allocation, "Send owner", parent, a);
p.relate(RelationKind::Allocation, "Receive owner", parent, b);
let mut ports = vec![];
for (owner, name) in [(parent, "First interface"), (outside, "Second interface")] {
let port = make_port(
&p,
Id::new_v4(),
owner,
name,
PortContract::Component {
kind: ComponentPortKind::Standard,
orientation: ComponentPortOrientation::Unset,
},
)
.unwrap();
ports.push(port.id);
p.elements.insert(port.id, port);
}
let edge = p.relate(
RelationKind::BoundaryExchange,
"Local boundary",
ports[0],
ports[1],
);
let fe = p.relate(
RelationKind::FunctionalExchange,
"Local function message",
a,
b,
);
let d = p.ensure_diagram(Layer::Sa);
p.diagrams.get_mut(&d).unwrap().projection = ProjectionKind::Allocation;
let pn = p.place(d, parent, Position { x: 100., y: 100. });
let on = p.place(d, outside, Position { x: 900., y: 100. });
let an = p.place(d, a, Position { x: 130., y: 170. });
let bn = p.place(d, b, Position { x: 130., y: 270. });
p.diagrams.get_mut(&d).unwrap().routes = vec![route(edge), route(fe)];
p.diagrams
.get_mut(&d)
.unwrap()
.nodes
.get_mut(&pn)
.unwrap()
.collapsed = true;
validate_candidate(&p).unwrap();
assert_eq!(
projection::edge_endpoint_placements(&p, &p.diagrams[&d], edge),
Some((pn, on))
);
assert_eq!(
projection::edge_endpoint_placements(&p, &p.diagrams[&d], fe),
Some((an, bn))
);
let moved = execute_batch(
&p,
&[Command::MoveNode {
diagram: d,
node: pn,
position: Position { x: 120., y: 130. },
with_descendants: true,
}],
)
.unwrap();
assert_eq!(moved.diagrams[&d].routes[0], route(edge));
assert_eq!(
moved.diagrams[&d].routes[1].waypoints[0],
Position { x: 570., y: 270. }
);
assert_eq!(
moved.diagrams[&d].routes[1].waypoints[1],
Position { x: 570., y: 390. }
);
let deltas = BTreeMap::from([
(pn, Position { x: 20., y: 30. }),
(on, Position { x: 20., y: 30. }),
]);
let translated = projection::translated_routes(&p, &p.diagrams[&d], &deltas);
assert_eq!(translated.len(), 1);
assert_eq!(translated[0].edge, edge);
assert_eq!(translated[0].waypoints[0], Position { x: 570., y: 270. });
}
#[test]
fn reidentify_and_delete_preserve_or_clean_routes_with_the_semantic_edge() {
let f = routed(fixture());
let renamed = Id::new_v4();
let mut session = Session::new(f.project.clone());
session
.execute(Command::Reidentify(f.edge, renamed))
.unwrap();
for d in [f.diagram, f.second] {
let r = &session.project().diagrams[&d].routes[0];
assert_eq!(r.edge, renamed);
assert_eq!(r.waypoints, route(f.edge).waypoints);
}
session.execute(Command::Delete(renamed)).unwrap();
for d in [f.diagram, f.second] {
assert_eq!(
session.project().diagrams[&d].routes,
vec![route(f.external)]
);
}
session.undo();
session.undo();
assert_eq!(session.project(), &f.project);
session.execute(Command::Delete(f.a)).unwrap();
assert!(!session.project().relations.contains_key(&f.edge));
for d in [f.diagram, f.second] {
assert_eq!(
session.project().diagrams[&d].routes,
vec![route(f.external)]
);
}
session.undo();
assert_eq!(session.project(), &f.project);
session.execute(Command::Delete(f.diagram)).unwrap();
assert_eq!(
session.project().diagrams[&f.second],
f.project.diagrams[&f.second]
);
session.undo();
assert_eq!(session.project(), &f.project);
}
#[test]
fn hiding_or_relocating_a_node_does_not_erase_route_metadata() {
let f = routed(fixture());
let an = node(&f.project, f.diagram, f.a);
let hidden = execute_batch(&f.project, &[Command::Delete(an)]).unwrap();
assert_eq!(
hidden.diagrams[&f.diagram].routes,
f.project.diagrams[&f.diagram].routes
);
assert_eq!(
projection::edge_endpoint_placements(&hidden, &hidden.diagrams[&f.diagram], f.edge),
None
);
let target = Id::new_v4();
let mut empty = f.project.diagrams[&f.second].clone();
empty.id = target;
empty.alias = "empty_view".into();
empty.nodes.clear();
empty.routes.clear();
let relocated = execute_batch(
&f.project,
&[
Command::CreateDiagram(empty),
Command::RelocateNode {
node: an,
diagram: target,
},
],
)
.unwrap();
assert_eq!(
relocated.diagrams[&f.diagram].routes,
f.project.diagrams[&f.diagram].routes
);
assert!(relocated.diagrams[&target].routes.is_empty());
}
#[test]
fn trace_routes_follow_trace_identity_and_cleanup_without_extra_objects() {
let mut f = fixture();
let oa = f
.project
.add_element(Kind::Activity, Layer::Oa, "Operational send", None)
.unwrap();
let id = Id::new_v4();
f.project.traces.insert(
id,
Trace {
id,
alias: "send_trace".into(),
name: "Send refinement".into(),
description: String::new(),
oa,
sa: f.a,
kind: TraceKind::Refinement,
baseline: None,
},
);
f.project.diagrams.get_mut(&f.diagram).unwrap().projection = ProjectionKind::Traceability;
let oa_node = f.project.place(f.diagram, oa, Position { x: 0., y: 0. });
let a_node = node(&f.project, f.diagram, f.a);
let mut session = Session::new(f.project.clone());
session
.execute(Command::SetEdgeRoute {
diagram: f.diagram,
route: route(id),
})
.unwrap();
assert_eq!(
projection::edge_endpoint_placements(
session.project(),
&session.project().diagrams[&f.diagram],
id
),
Some((oa_node, a_node))
);
let original = session.project().clone();
let renamed = Id::new_v4();
session.execute(Command::Reidentify(id, renamed)).unwrap();
assert_eq!(
session.project().diagrams[&f.diagram].routes[0].edge,
renamed
);
session.execute(Command::Delete(oa)).unwrap();
assert!(session.project().diagrams[&f.diagram].routes.is_empty());
session.undo();
session.undo();
assert_eq!(session.project(), &original);
}
#[test]
fn group_translation_beyond_coordinate_limit_rolls_back_nodes_and_routes() {
let mut f = routed(fixture());
f.project.diagrams.get_mut(&f.diagram).unwrap().routes[0].waypoints[0].x = MAX_COORDINATE;
let mut session = Session::new(f.project.clone());
let group_node = node(&f.project, f.diagram, f.group);
let result = session.execute(Command::MoveNode {
diagram: f.diagram,
node: group_node,
position: Position { x: 101., y: 100. },
with_descendants: true,
});
assert!(
result
.unwrap_err()
.to_string()
.contains("invalid_route_bounds")
);
assert_eq!(session.project(), &f.project);
assert!(!session.can_undo());
}
#[test]
fn layout_preserves_authored_route_count_order_labels_and_undo() {
let f = routed(fixture());
let mut session = Session::new(f.project.clone());
session.execute(Command::Layout(f.diagram)).unwrap();
let routes = &session.project().diagrams[&f.diagram].routes;
assert_eq!(routes.len(), 2);
assert_eq!(routes[0].edge, f.edge);
assert_eq!(routes[1].edge, f.external);
assert_eq!(routes[0].waypoints.len(), 2);
assert_eq!(routes[1].waypoints.len(), 2);
assert_eq!(routes[0].label, route(f.edge).label);
assert_eq!(routes[1].label, route(f.external).label);
assert_eq!(
session.project().diagrams[&f.second],
f.project.diagrams[&f.second]
);
session.undo();
assert_eq!(session.project(), &f.project);
}
#[test]
fn old_models_get_empty_routes_and_route_json_is_strict() {
for bytes in [
include_bytes!("fixtures/v2-coastal-rescue.json").as_slice(),
include_bytes!("../../../examples/legacy-v1-coastal-rescue.oa.json").as_slice(),
] {
let p = persistence::decode(bytes).unwrap();
assert!(p.diagrams.values().all(|d| d.routes.is_empty()));
}
let f = fixture();
let mut old = serde_json::to_value(&f.project).unwrap();
for d in old["diagrams"].as_object_mut().unwrap().values_mut() {
d.as_object_mut().unwrap().remove("routes");
}
assert_eq!(
persistence::decode(&serde_json::to_vec(&old).unwrap()).unwrap(),
f.project
);
let minimal = serde_json::json!({"edge":f.edge});
assert_eq!(
serde_json::from_value::<EdgeRoute>(minimal).unwrap(),
EdgeRoute {
edge: f.edge,
waypoints: vec![],
label: None
}
);
assert!(
serde_json::from_value::<EdgeRoute>(serde_json::json!({"edge":f.edge,"bendz":[]})).is_err()
);
assert!(serde_json::from_value::<EdgeLabel>(serde_json::json!({"fraction":0.5})).is_err());
assert!(
serde_json::from_value::<EdgeLabel>(
serde_json::json!({"fraction":0.5,"offset":{"x":0,"y":0},"typo":true})
)
.is_err()
);
for command in [
Command::SetEdgeRoute {
diagram: f.diagram,
route: route(f.edge),
},
Command::RemoveEdgeRoute {
diagram: f.diagram,
edge: f.edge,
},
] {
assert_eq!(
serde_json::from_value::<Command>(serde_json::to_value(&command).unwrap()).unwrap(),
command
);
}
}