481 lines
20 KiB
Rust
481 lines
20 KiB
Rust
// Included in connection_edit::tests: exercise the real application event path.
|
|
use oa_core::projection::project_view;
|
|
|
|
fn allocation_fixture(layer: Layer, projection: ProjectionKind) -> (App, Id, Id, Id) {
|
|
let mut project = Project::empty();
|
|
let (performer_kind, work_kind) = match layer {
|
|
Layer::Oa => (Kind::Entity, Kind::Activity),
|
|
Layer::Sa => (Kind::System, Kind::Function),
|
|
};
|
|
let performer = project.add(performer_kind, "Assigned performer", 20., 20.);
|
|
let work = project.add(work_kind, "Existing leaf work", 420., 20.);
|
|
let diagram = project.ensure_diagram(layer);
|
|
project.diagrams.get_mut(&diagram).unwrap().projection = projection;
|
|
let reference_id = Id::new_v4();
|
|
let mut reference = project.diagrams[&diagram].clone();
|
|
reference.id = reference_id;
|
|
reference.alias = project.fresh_alias("Reference view", "diagram", reference_id);
|
|
reference.name = "Reference view".into();
|
|
reference.nodes.clear();
|
|
project.diagrams.insert(reference_id, reference);
|
|
project.place(reference_id, performer, Position { x: 80., y: 100. });
|
|
project.place(reference_id, work, Position { x: 500., y: 300. });
|
|
let mut app = App::with_project(project);
|
|
let _ = app.update(Message::Diagram(crate::app::Choice {
|
|
id: diagram,
|
|
label: app.project().diagrams[&diagram].name.clone(),
|
|
}));
|
|
(app, performer, work, diagram)
|
|
}
|
|
|
|
fn start_allocation(app: &mut App, performer: Id) -> Context {
|
|
send(app, Event::Choose(Some(Tool::Allocation)));
|
|
let context = app.route_context().unwrap();
|
|
assert!(
|
|
app.connection
|
|
.as_ref()
|
|
.unwrap()
|
|
.candidates
|
|
.contains(&performer)
|
|
);
|
|
send(app, Event::Endpoint(context, performer));
|
|
assert_eq!(app.connection.as_ref().unwrap().source, Some(performer));
|
|
context
|
|
}
|
|
|
|
#[test]
|
|
fn allocation_projection_eligibility_requires_both_endpoint_categories() {
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
for projection in [
|
|
ProjectionKind::Overview,
|
|
ProjectionKind::Decomposition,
|
|
ProjectionKind::Traceability,
|
|
ProjectionKind::Allocation,
|
|
] {
|
|
assert!(Tool::Allocation.available_in(layer, projection));
|
|
}
|
|
for projection in [
|
|
ProjectionKind::FunctionalDataflow,
|
|
ProjectionKind::Capabilities,
|
|
ProjectionKind::StateMachine {
|
|
machine: Id::new_v4(),
|
|
},
|
|
] {
|
|
assert!(!Tool::Allocation.available_in(layer, projection));
|
|
}
|
|
for projection in [
|
|
ProjectionKind::FunctionalDataflow,
|
|
ProjectionKind::Capabilities,
|
|
] {
|
|
let (mut app, _, _, _) = allocation_fixture(layer, projection);
|
|
let before = app.document.bundle().clone();
|
|
send(&mut app, Event::Choose(Some(Tool::Allocation)));
|
|
assert!(app.connection.is_none());
|
|
assert_eq!(app.document.bundle(), &before);
|
|
}
|
|
}
|
|
// The new predicate retains the four established exchange-tool policies.
|
|
assert!(Tool::Interaction.available_in(Layer::Oa, ProjectionKind::FunctionalDataflow));
|
|
assert!(Tool::FunctionalExchange.available_in(Layer::Sa, ProjectionKind::FunctionalDataflow));
|
|
assert!(Tool::CommunicationMean.available_in(Layer::Oa, ProjectionKind::Capabilities));
|
|
assert!(Tool::BoundaryExchange.available_in(Layer::Sa, ProjectionKind::Capabilities));
|
|
assert!(!Tool::FunctionalExchange.available_in(Layer::Oa, ProjectionKind::Overview));
|
|
}
|
|
|
|
#[test]
|
|
fn allocation_canvas_commits_only_one_relation_with_exact_source_undo_redo() {
|
|
use oa_core::source_project::{ProjectLimits, compile_bundle};
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
for projection in [
|
|
ProjectionKind::Overview,
|
|
ProjectionKind::Decomposition,
|
|
ProjectionKind::Traceability,
|
|
ProjectionKind::Allocation,
|
|
] {
|
|
let (mut app, performer, work, diagram) = allocation_fixture(layer, projection);
|
|
let original = app.project().clone();
|
|
let before = app.document.bundle().clone();
|
|
let revision = app.document.revision();
|
|
let context = start_allocation(&mut app, performer);
|
|
assert_eq!(app.document.bundle(), &before);
|
|
send(&mut app, Event::Endpoint(context, work));
|
|
assert!(app.connection.is_none());
|
|
assert_eq!(app.document.revision(), revision + 1);
|
|
let relation = &app.project().relations[&app.selected];
|
|
let name = "Allocation: Assigned performer → Existing leaf work";
|
|
let expected_relation = Relation {
|
|
id: relation.id,
|
|
alias: original.fresh_alias(name, "relation", relation.id),
|
|
name: name.into(),
|
|
description: String::new(),
|
|
body: RelationBody::new(RelationKind::Allocation, performer, work),
|
|
};
|
|
let relation_id = relation.id;
|
|
let mut expected = original.clone();
|
|
expected.relations.insert(relation_id, expected_relation);
|
|
assert_eq!(app.project(), &expected, "Only the new relation may change");
|
|
assert_eq!(app.active_diagram, Some(diagram));
|
|
assert!(
|
|
app.status
|
|
.contains("Allocated Existing leaf work to Assigned performer")
|
|
);
|
|
let projected = project_view(app.project(), diagram);
|
|
if projection == ProjectionKind::Allocation {
|
|
assert!(!projected.edges.iter().any(|edge| edge.id == relation_id));
|
|
let owner = projected
|
|
.nodes
|
|
.iter()
|
|
.find(|node| node.element == performer)
|
|
.unwrap();
|
|
let child = projected
|
|
.nodes
|
|
.iter()
|
|
.find(|node| node.element == work)
|
|
.unwrap();
|
|
assert_eq!(child.visual_parent, Some(owner.id));
|
|
} else {
|
|
assert!(projected.edges.iter().any(|edge| edge.id == relation_id));
|
|
}
|
|
let after = app.document.bundle().clone();
|
|
assert_eq!(
|
|
compile_bundle(&after, ProjectLimits::default())
|
|
.unwrap()
|
|
.decoded
|
|
.project,
|
|
expected
|
|
);
|
|
send(&mut app, Event::Endpoint(context, work));
|
|
assert_eq!(
|
|
app.document.revision(),
|
|
revision + 1,
|
|
"Repeated release is not another transaction"
|
|
);
|
|
let _ = app.update(Message::Undo);
|
|
assert_eq!(app.project(), &original);
|
|
assert_eq!(app.document.bundle(), &before);
|
|
let _ = app.update(Message::Redo);
|
|
assert_eq!(app.project(), &expected);
|
|
assert_eq!(app.document.bundle(), &after);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn allocation_candidates_reject_nonleaf_occupied_cross_layer_port_and_hidden_work() {
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
let (base, performer, work, diagram) =
|
|
allocation_fixture(layer, ProjectionKind::Traceability);
|
|
let mut project = base.project().clone();
|
|
let work_kind = project.elements[&work].kind();
|
|
let composite = project
|
|
.add_element(work_kind, layer, "Composite work", None)
|
|
.unwrap();
|
|
project
|
|
.add_element(work_kind, layer, "Child work", Some(composite))
|
|
.unwrap();
|
|
let occupied = project
|
|
.add_element(work_kind, layer, "Occupied work", None)
|
|
.unwrap();
|
|
let allocation = Tool::Allocation
|
|
.command(&project, performer, occupied)
|
|
.unwrap();
|
|
project = prepare_command(&project, &allocation).unwrap().project;
|
|
let hidden = project
|
|
.add_element(work_kind, layer, "Unplaced work", None)
|
|
.unwrap();
|
|
let other_layer = if layer == Layer::Oa {
|
|
Layer::Sa
|
|
} else {
|
|
Layer::Oa
|
|
};
|
|
let other_kind = if other_layer == Layer::Oa {
|
|
Kind::Activity
|
|
} else {
|
|
Kind::Function
|
|
};
|
|
let foreign = project
|
|
.add_element(other_kind, other_layer, "Other perspective", None)
|
|
.unwrap();
|
|
let foreign_performer_kind = if other_layer == Layer::Oa {
|
|
Kind::Actor
|
|
} else {
|
|
Kind::SystemActor
|
|
};
|
|
let foreign_performer = project
|
|
.add_element(foreign_performer_kind, other_layer, "Other performer", None)
|
|
.unwrap();
|
|
for (index, id) in [composite, occupied, foreign, foreign_performer]
|
|
.into_iter()
|
|
.enumerate()
|
|
{
|
|
project.place(
|
|
diagram,
|
|
id,
|
|
Position {
|
|
x: index as f32 * 300.,
|
|
y: 400.,
|
|
},
|
|
);
|
|
}
|
|
let port_owner = if layer == Layer::Sa { work } else { foreign };
|
|
let port_id = Id::new_v4();
|
|
let port = make_port(
|
|
&project,
|
|
port_id,
|
|
port_owner,
|
|
"Work input",
|
|
PortContract::FunctionInput {
|
|
incoming_items: vec![],
|
|
},
|
|
)
|
|
.unwrap();
|
|
project.elements.insert(port_id, port);
|
|
let mut app = App::with_project(project);
|
|
let _ = app.update(Message::Diagram(crate::app::Choice {
|
|
id: diagram,
|
|
label: app.project().diagrams[&diagram].name.clone(),
|
|
}));
|
|
let before = app.document.bundle().clone();
|
|
let original = app.project().clone();
|
|
let revision = app.document.revision();
|
|
send(&mut app, Event::Choose(Some(Tool::Allocation)));
|
|
assert!(
|
|
app.connection
|
|
.as_ref()
|
|
.unwrap()
|
|
.candidates
|
|
.contains(&foreign_performer)
|
|
);
|
|
let context = app.route_context().unwrap();
|
|
send(&mut app, Event::Endpoint(context, performer));
|
|
assert!(app.connection.as_ref().unwrap().candidates.contains(&work));
|
|
assert!(Tool::Allocation.can_connect(app.project(), performer, hidden));
|
|
for target in [composite, occupied, foreign, port_id, hidden, performer] {
|
|
assert!(
|
|
!app.connection
|
|
.as_ref()
|
|
.unwrap()
|
|
.candidates
|
|
.contains(&target)
|
|
);
|
|
send(&mut app, Event::Endpoint(context, target));
|
|
assert_eq!(app.document.revision(), revision);
|
|
assert_eq!(app.project(), &original);
|
|
assert_eq!(app.document.bundle(), &before);
|
|
}
|
|
for target in [composite, occupied, foreign, port_id, performer] {
|
|
assert!(
|
|
Tool::Allocation
|
|
.command(app.project(), performer, target)
|
|
.is_err()
|
|
);
|
|
}
|
|
send(&mut app, Event::Endpoint(context, work));
|
|
assert_eq!(
|
|
app.document.revision(),
|
|
revision + 1,
|
|
"Invalid clicks do not prevent recovery"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn allocation_creation_preserves_saved_edge_filters() {
|
|
let (base, performer, work, diagram) =
|
|
allocation_fixture(Layer::Oa, ProjectionKind::Decomposition);
|
|
let mut project = base.project().clone();
|
|
project
|
|
.diagrams
|
|
.get_mut(&diagram)
|
|
.unwrap()
|
|
.view_options
|
|
.relation_kinds = oa_core::view_options::RelationKinds::Only(vec![]);
|
|
let diagrams = project.diagrams.clone();
|
|
let mut app = App::with_project(project);
|
|
let _ = app.update(Message::Diagram(crate::app::Choice {
|
|
id: diagram,
|
|
label: app.project().diagrams[&diagram].name.clone(),
|
|
}));
|
|
let context = start_allocation(&mut app, performer);
|
|
send(&mut app, Event::Endpoint(context, work));
|
|
assert_eq!(
|
|
app.project().relations[&app.selected].kind(),
|
|
RelationKind::Allocation
|
|
);
|
|
assert_eq!(app.project().diagrams, diagrams);
|
|
assert!(project_view(app.project(), diagram).edges.is_empty());
|
|
assert!(app.status.contains("view filters are unchanged"));
|
|
}
|
|
|
|
#[test]
|
|
fn allocation_mixed_view_keeps_captured_canvas_for_opposite_layer_pairs() {
|
|
for owner_layer in [Layer::Oa, Layer::Sa] {
|
|
for app_layer in [Layer::Oa, Layer::Sa] {
|
|
let (base, owner_performer, owner_work, diagram) =
|
|
allocation_fixture(owner_layer, ProjectionKind::Traceability);
|
|
let mut project = base.project().clone();
|
|
let layer = if owner_layer == Layer::Oa {
|
|
Layer::Sa
|
|
} else {
|
|
Layer::Oa
|
|
};
|
|
let (performer_kind, work_kind) = if layer == Layer::Oa {
|
|
(Kind::Actor, Kind::Activity)
|
|
} else {
|
|
(Kind::SystemActor, Kind::Function)
|
|
};
|
|
let performer = project.add(performer_kind, "Opposite-layer performer", 40., 60.);
|
|
let work = project.add(work_kind, "Opposite-layer work", 420., 60.);
|
|
project.place(diagram, performer, Position { x: 40., y: 350. });
|
|
project.place(diagram, work, Position { x: 420., y: 350. });
|
|
let mut app = App::with_project(project);
|
|
let _ = app.update(Message::Diagram(crate::app::Choice {
|
|
id: diagram,
|
|
label: app.project().diagrams[&diagram].name.clone(),
|
|
}));
|
|
let _ = app.update(Message::Pan(iced::Vector::new(73., 29.)));
|
|
app.layer = app_layer;
|
|
let camera = (app.pan, app.zoom);
|
|
let before = app.document.bundle().clone();
|
|
let original = app.project().clone();
|
|
let revision = app.document.revision();
|
|
let cancelled = start_allocation(&mut app, performer);
|
|
let _ = app.update(Message::Escape);
|
|
send(&mut app, Event::Endpoint(cancelled, work));
|
|
assert!(app.connection.is_none());
|
|
assert_eq!(app.layer, app_layer);
|
|
assert_eq!(app.active_diagram, Some(diagram));
|
|
assert_eq!((app.pan, app.zoom), camera);
|
|
assert_eq!(app.document.bundle(), &before);
|
|
let context = start_allocation(&mut app, performer);
|
|
assert_eq!(app.active_diagram, Some(diagram));
|
|
assert_eq!(app.layer, app_layer);
|
|
assert_eq!(app.route_context(), Some(context));
|
|
assert_eq!((app.pan, app.zoom), camera);
|
|
assert_eq!(app.selected, performer);
|
|
assert!(app.connection.as_ref().unwrap().candidates.contains(&work));
|
|
for wrong_target in [owner_performer, owner_work] {
|
|
assert!(
|
|
!app.connection
|
|
.as_ref()
|
|
.unwrap()
|
|
.candidates
|
|
.contains(&wrong_target)
|
|
);
|
|
send(&mut app, Event::Endpoint(context, wrong_target));
|
|
assert_eq!(app.project(), &original);
|
|
assert_eq!(app.layer, app_layer);
|
|
assert_eq!(app.active_diagram, Some(diagram));
|
|
assert_eq!((app.pan, app.zoom), camera);
|
|
assert_eq!(app.document.bundle(), &before);
|
|
}
|
|
send(&mut app, Event::Endpoint(context, work));
|
|
assert!(app.connection.is_none());
|
|
assert_eq!(app.document.revision(), revision + 1);
|
|
assert_eq!(app.active_diagram, Some(diagram));
|
|
assert_eq!(app.layer, owner_layer);
|
|
assert_eq!((app.pan, app.zoom), camera);
|
|
let relation = &app.project().relations[&app.selected];
|
|
assert_eq!(
|
|
relation.body,
|
|
RelationBody::new(RelationKind::Allocation, performer, work)
|
|
);
|
|
let mut expected = original.clone();
|
|
expected.relations.insert(relation.id, relation.clone());
|
|
assert_eq!(app.project(), &expected);
|
|
let after = app.document.bundle().clone();
|
|
let _ = app.update(Message::Undo);
|
|
assert_eq!(app.project(), &original);
|
|
assert_eq!(app.document.bundle(), &before);
|
|
assert_eq!(app.active_diagram, Some(diagram));
|
|
assert_eq!((app.pan, app.zoom), camera);
|
|
let _ = app.update(Message::Redo);
|
|
assert_eq!(app.project(), &expected);
|
|
assert_eq!(app.document.bundle(), &after);
|
|
assert_eq!(app.active_diagram, Some(diagram));
|
|
assert_eq!((app.pan, app.zoom), camera);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn allocation_cancel_navigation_stale_context_and_pending_edits_preserve_model() {
|
|
let (mut app, performer, work, diagram) =
|
|
allocation_fixture(Layer::Oa, ProjectionKind::Decomposition);
|
|
let original = app.project().clone();
|
|
let before = app.document.bundle().clone();
|
|
let old = start_allocation(&mut app, performer);
|
|
let _ = app.update(Message::Escape);
|
|
assert!(app.connection.is_none());
|
|
send(&mut app, Event::Endpoint(old, work));
|
|
let current = start_allocation(&mut app, performer);
|
|
send(&mut app, Event::Endpoint(old, work));
|
|
assert_eq!(app.connection.as_ref().unwrap().context, current);
|
|
let _ = app.update(Message::Page(Page::Catalog));
|
|
assert!(app.connection.is_none());
|
|
let _ = app.update(Message::Page(Page::Diagram));
|
|
let _ = app.update(Message::Diagram(crate::app::Choice {
|
|
id: diagram,
|
|
label: app.project().diagrams[&diagram].name.clone(),
|
|
}));
|
|
start_allocation(&mut app, performer);
|
|
send(&mut app, Event::Choose(None));
|
|
assert!(app.connection.is_none());
|
|
start_allocation(&mut app, performer);
|
|
send(&mut app, Event::Choose(Some(Tool::Interaction)));
|
|
assert_eq!(app.connection.as_ref().unwrap().tool, Tool::Interaction);
|
|
assert_eq!(app.project(), &original);
|
|
let current = start_allocation(&mut app, performer);
|
|
for stale in [
|
|
Context {
|
|
document: Id::new_v4(),
|
|
..current
|
|
},
|
|
Context {
|
|
diagram: Id::new_v4(),
|
|
..current
|
|
},
|
|
Context {
|
|
revision: current.revision + 1,
|
|
..current
|
|
},
|
|
] {
|
|
send(&mut app, Event::Endpoint(stale, work));
|
|
assert_eq!(app.connection.as_ref().unwrap().context, current);
|
|
}
|
|
app.delete_pending = true;
|
|
send(&mut app, Event::Endpoint(current, work));
|
|
assert_eq!(app.project(), &original);
|
|
let _ = app.update(Message::CancelDelete);
|
|
let _ = app.update(Message::Escape);
|
|
let _ = app.update(Message::Select(performer));
|
|
let _ = app.update(Message::Field(
|
|
"/name".into(),
|
|
"Unapplied performer name".into(),
|
|
));
|
|
send(&mut app, Event::Choose(Some(Tool::Allocation)));
|
|
assert!(app.connection.is_none());
|
|
assert!(app.draft_dirty());
|
|
assert_eq!(app.project(), &original);
|
|
assert_eq!(app.document.bundle(), &before);
|
|
|
|
let (mut source_app, performer, work, _) =
|
|
allocation_fixture(Layer::Sa, ProjectionKind::Decomposition);
|
|
let before = source_app.document.bundle().clone();
|
|
let context = start_allocation(&mut source_app, performer);
|
|
use iced::widget::text_editor;
|
|
let _ = source_app.update(Message::SourceEdit(text_editor::Action::SelectAll));
|
|
let _ = source_app.update(Message::SourceEdit(text_editor::Action::Edit(
|
|
text_editor::Edit::Paste(std::sync::Arc::new("invalid raw source retained".into())),
|
|
)));
|
|
send(&mut source_app, Event::Endpoint(context, work));
|
|
send(&mut source_app, Event::Choose(Some(Tool::Allocation)));
|
|
assert!(source_app.connection.is_none());
|
|
assert!(source_app.source_dirty());
|
|
assert_eq!(
|
|
source_app.source_editor.text(),
|
|
"invalid raw source retained"
|
|
);
|
|
assert_eq!(source_app.document.bundle(), &before);
|
|
}
|