Files
ARCADIA-Studio/crates/oa-desktop/src/behavior/creation/tests.rs
T

600 lines
22 KiB
Rust

use super::*;
use crate::document::Document;
use oa_core::source_project::ProjectLimits;
fn start(p: &Project, layer: Layer, family: Family, name: &str) -> State {
let mut state = State::default();
assert!(state.update(p, Event::Open(layer)).unwrap().is_none());
state.update(p, Event::Choose(family)).unwrap();
state.update(p, Event::Name(name.into())).unwrap();
state
}
#[test]
fn mounted_creation_keeps_actions_visible_and_renders_only_six_owner_candidates() {
use iced::advanced::{Layout, layout, widget};
use iced::{Rectangle, Size, Vector};
struct Geometry {
candidates: Vec<widget::Id>,
rendered: Vec<widget::Id>,
actions: Option<Rectangle>,
fields: Option<Rectangle>,
}
impl widget::Operation for Geometry {
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation)) {
visit(self);
}
fn container(&mut self, id: Option<&widget::Id>, bounds: Rectangle) {
if id == Some(&widget::Id::new("behavior-creation-actions")) {
self.actions = Some(bounds);
}
if let Some(id) = id.filter(|id| self.candidates.contains(id)) {
self.rendered.push(id.clone());
}
}
fn scrollable(
&mut self,
id: Option<&widget::Id>,
bounds: Rectangle,
_: Rectangle,
_: Vector,
_: &mut dyn widget::operation::Scrollable,
) {
if id == Some(&widget::Id::new("behavior-creation-fields")) {
self.fields = Some(bounds);
}
}
}
let mut p = Project::empty();
for index in 0..105 {
p.add_element(
Kind::Entity,
Layer::Oa,
&format!(
"{index:03} Long engineering participant name {}",
"雨 extended ".repeat(20)
),
None,
)
.unwrap();
}
let candidates: Vec<_> = p
.elements
.keys()
.map(|id| widget::Id::from(format!("behavior-creation-candidate-{id}")))
.collect();
for family in [Family::Sequence, Family::Scenario, Family::Machine] {
let state = start(&p, Layer::Oa, family, "Very long exact behavior name 雨");
for size in [
Size::new(500., 570.),
Size::new(1050., 570.),
Size::new(500., 320.),
] {
let mut view: UiElement<'_, Event, iced::Theme, ()> = state.view_with(&p).unwrap();
let mut tree = widget::Tree::new(view.as_widget());
let node =
view.as_widget_mut()
.layout(&mut tree, &(), &layout::Limits::new(Size::ZERO, size));
let mut geometry = Geometry {
candidates: candidates.clone(),
rendered: Vec::new(),
actions: None,
fields: None,
};
view.as_widget_mut()
.operate(&mut tree, Layout::new(&node), &(), &mut geometry);
let actions = geometry.actions.unwrap();
let fields = geometry.fields.unwrap();
assert!(actions.x >= 0. && actions.y >= 0.);
assert!(
actions.x + actions.width <= size.width
&& actions.y + actions.height <= size.height
);
assert!(fields.height >= 100. && fields.y + fields.height <= actions.y);
assert_eq!(
geometry.rendered.len(),
if family == Family::Machine { 6 } else { 0 }
);
}
}
}
fn roundtrip(p: Project, state: &mut State) -> Project {
let command = state.update(&p, Event::Submit).unwrap().unwrap();
assert_eq!(
command,
state.prepare_pending(&p).unwrap(),
"preparation must retain reserved IDs"
);
let api: Command = serde_json::from_value(serde_json::to_value(&command).unwrap()).unwrap();
assert_eq!(api, command);
let expected = execute_batch(&p, &[api]).unwrap();
let target = state.created().unwrap();
let mut document = Document::from_project(p.clone(), ProjectLimits::default()).unwrap();
let before_source = document.bundle().clone();
document.execute(command).unwrap();
assert_eq!(document.project(), &expected);
assert!(
state.has_pending(),
"only successful acknowledgement clears raw fields"
);
state.acknowledge_commit();
assert!(!state.has_pending());
assert!(document.project().elements.contains_key(&target.behavior));
let after_source = document.bundle().clone();
assert!(document.undo());
assert_eq!(document.project(), &p);
assert_eq!(document.bundle(), &before_source);
assert!(!document.can_undo(), "one creation must be one Undo entry");
assert!(document.redo());
assert_eq!(document.project(), &expected);
assert_eq!(document.bundle(), &after_source);
let reopened = Document::from_bundle(after_source, ProjectLimits::default()).unwrap();
assert_eq!(reopened.project(), &expected);
expected
}
#[test]
fn oa_process_and_sa_chain_keep_exact_names_root_ownership_and_one_history_entry() {
for layer in [Layer::Oa, Layer::Sa] {
let p = Project::empty();
let name = " Supply \"water\" — режим ";
let mut state = start(&p, layer, Family::Sequence, name);
let target = state.created().unwrap();
let result = roundtrip(p, &mut state);
let element = &result.elements[&target.behavior];
assert_eq!(element.name, name);
assert_eq!(element.layer(), layer);
assert_eq!(
element.kind(),
if layer == Layer::Oa {
Kind::Process
} else {
Kind::FunctionalChain
}
);
assert_eq!(element.parent, None);
assert!(element.steps().unwrap().is_empty());
assert!(result.diagrams.is_empty());
assert!(result.relations.is_empty());
}
}
#[test]
fn all_four_viewpoints_are_explicit_and_survive_source_history_reopen() {
for layer in [Layer::Oa, Layer::Sa] {
for viewpoint in viewpoints(layer) {
let mut p = Project::empty();
let context = p
.add_element(
if layer == Layer::Oa {
Kind::Capability
} else {
Kind::SystemCapability
},
layer,
"Water supply",
None,
)
.unwrap();
let mut state = start(&p, layer, Family::Scenario, "Nominal supply");
assert!(
state
.prepare_pending(&p)
.unwrap_err()
.contains("explicitly")
);
assert!(
state
.update(&p, Event::Viewpoint(ScenarioViewpoint::MixedLegacy))
.is_err()
);
let wrong = viewpoints(if layer == Layer::Oa {
Layer::Sa
} else {
Layer::Oa
})[0];
assert!(state.update(&p, Event::Viewpoint(wrong)).is_err());
state.update(&p, Event::Viewpoint(viewpoint)).unwrap();
state.update(&p, Event::Context(Some(context))).unwrap();
let id = state.created().unwrap().behavior;
let result = roundtrip(p, &mut state);
assert_eq!(
result.elements[&id].parent, None,
"context is not containment"
);
assert!(
matches!(result.elements[&id].shared(), Some(SharedBody::Scenario { viewpoint: actual, context: Some(actual_context), steps, .. }) if *actual == viewpoint && *actual_context == context && steps.is_empty())
);
}
}
}
#[test]
fn machine_requires_valid_owner_and_first_view_adds_no_region_or_state() {
for layer in [Layer::Oa, Layer::Sa] {
for first_view in [false, true] {
let mut p = Project::empty();
let owner = p
.add_element(
if layer == Layer::Oa {
Kind::Entity
} else {
Kind::System
},
layer,
"Pump",
None,
)
.unwrap();
let wrong_kind = p
.add_element(
if layer == Layer::Oa {
Kind::Activity
} else {
Kind::Function
},
layer,
"Pump water",
None,
)
.unwrap();
let opposite = if layer == Layer::Oa {
Layer::Sa
} else {
Layer::Oa
};
let wrong_layer = p
.add_element(
if opposite == Layer::Oa {
Kind::Entity
} else {
Kind::System
},
opposite,
"Other layer pump",
None,
)
.unwrap();
let mut state = start(&p, layer, Family::Machine, "Pump modes");
assert!(
state
.prepare_pending(&p)
.unwrap_err()
.contains("participant")
);
assert!(state.update(&p, Event::Owner(wrong_kind)).is_err());
assert!(state.update(&p, Event::Owner(wrong_layer)).is_err());
state.update(&p, Event::Owner(owner)).unwrap();
state.update(&p, Event::FirstView(first_view)).unwrap();
let target = state.created().unwrap();
let expected_count = p.elements.len() + 1;
let result = roundtrip(p, &mut state);
assert_eq!(result.elements.len(), expected_count);
assert_eq!(result.elements[&target.behavior].parent, Some(owner));
assert_eq!(result.elements[&target.behavior].kind(), Kind::StateMachine);
assert_eq!(result.diagrams.len(), usize::from(first_view));
if let Some(view) = target.diagram {
let diagram = &result.diagrams[&view];
assert_eq!(
diagram.projection,
ProjectionKind::StateMachine {
machine: target.behavior
}
);
assert_eq!(diagram.nodes.len(), 1);
assert_eq!(
diagram.nodes.values().next().unwrap().element,
target.behavior
);
}
}
}
}
#[test]
fn cancel_and_illegal_family_layer_switches_never_create_model_objects() {
let p = Project::empty();
let mut state = start(&p, Layer::Oa, Family::Scenario, "Raw name");
state
.update(
&p,
Event::Viewpoint(ScenarioViewpoint::ParticipantInteraction),
)
.unwrap();
let snapshot = state.recovery_snapshot().unwrap();
assert!(state.update(&p, Event::Open(Layer::Sa)).is_err());
assert!(state.update(&p, Event::Choose(Family::Sequence)).is_err());
assert_eq!(state.recovery_snapshot().unwrap(), snapshot);
assert!(state.update(&p, Event::Cancel).unwrap().is_none());
assert!(!state.has_pending());
assert!(state.created().is_none());
assert!(state.prepare_pending(&p).is_err());
assert!(p.elements.is_empty());
assert!(p.diagrams.is_empty());
}
#[test]
fn recovery_preserves_exact_raw_inputs_reserved_ids_and_revalidates_stale_owner() {
let mut p = Project::empty();
let owner = p
.add_element(Kind::Entity, Layer::Oa, "Plant", None)
.unwrap();
let mut state = start(&p, Layer::Oa, Family::Machine, " \nRaw rejected name\0 ");
state.update(&p, Event::Owner(owner)).unwrap();
state.update(&p, Event::FirstView(true)).unwrap();
state
.update(&p, Event::Search(Picker::Owner, " Plant ".into()))
.unwrap();
let raw = state.recovery_snapshot().unwrap().unwrap();
let mut restored = State::from_recovery(raw.clone()).unwrap();
assert_eq!(restored.recovery_snapshot().unwrap(), Some(raw.clone()));
assert_eq!(restored.created(), state.created());
assert_eq!(restored.pending_layer(), Some(Layer::Oa));
assert!(restored.prepare_pending(&p).is_err());
restored
.update(&p, Event::Name("Plant modes".into()))
.unwrap();
let command = restored.prepare_pending(&p).unwrap();
let again = State::from_recovery(restored.recovery_snapshot().unwrap().unwrap()).unwrap();
assert_eq!(again.prepare_pending(&p).unwrap(), command);
p.elements.remove(&owner);
let before = restored.recovery_snapshot().unwrap();
assert!(
restored
.prepare_pending(&p)
.unwrap_err()
.contains("participant")
);
assert_eq!(restored.recovery_snapshot().unwrap(), before);
let different = Project::empty();
assert!(
restored
.prepare_pending(&different)
.unwrap_err()
.contains("another document")
);
}
#[test]
fn rejected_source_transaction_keeps_draft_and_every_accepted_source_byte() {
let p = Project::empty();
let initial = Document::from_project(p.clone(), ProjectLimits::default()).unwrap();
let bytes: usize = initial.bundle().files.values().map(String::len).sum();
let limits = ProjectLimits {
model: language::ParseLimits {
max_source_bytes: bytes + 32,
..language::ParseLimits::UNLIMITED
},
..ProjectLimits::default()
};
let mut doc = Document::from_bundle(initial.bundle().clone(), limits).unwrap();
let mut state = start(
&p,
Layer::Oa,
Family::Sequence,
"A long exact name retained after the source budget refuses this new process",
);
let raw = state.recovery_snapshot().unwrap();
let command = state.prepare_pending(&p).unwrap();
assert!(doc.execute(command).is_err());
assert_eq!(doc.project(), &p);
assert_eq!(doc.bundle(), initial.bundle());
assert!(!doc.can_undo());
assert_eq!(state.recovery_snapshot().unwrap(), raw);
state.update(&p, Event::Cancel).unwrap();
}
#[test]
fn owner_search_is_name_ordered_filtered_and_page_sized_not_a_catalog() {
let mut p = Project::empty();
for index in (0..105).rev() {
p.add_element(Kind::Entity, Layer::Oa, &format!("Pump {index:03}"), None)
.unwrap();
}
p.add_element(Kind::System, Layer::Sa, "Pump 999", None)
.unwrap();
p.add_element(Kind::Activity, Layer::Oa, "Pump work", None)
.unwrap();
let mut state = start(&p, Layer::Oa, Family::Machine, "Pump modes");
let all = choices(&p, Layer::Oa, Picker::Owner, "");
assert_eq!(all.len(), 105);
assert_eq!(all[0].name, "Pump 000");
state
.update(&p, Event::Page(Picker::Owner, usize::MAX))
.unwrap();
assert!(
state.view(&p).is_some(),
"stale pages are clamped before slicing"
);
state
.update(&p, Event::Search(Picker::Owner, "Pump 104".into()))
.unwrap();
assert_eq!(state.draft.as_ref().unwrap().browsers[0].page, 0);
let found = choices(&p, Layer::Oa, Picker::Owner, "Pump 104");
assert_eq!(found.len(), 1);
state.update(&p, Event::Owner(found[0].id)).unwrap();
assert_eq!(state.draft.as_ref().unwrap().owner, Some(found[0].id));
}
#[test]
fn repeated_owner_names_remain_distinguishable_by_searchable_parent_context_and_alias() {
let mut p = Project::empty();
let north = p
.add_element(Kind::Entity, Layer::Oa, "North station", None)
.unwrap();
let south = p
.add_element(Kind::Entity, Layer::Oa, "South station", None)
.unwrap();
let first = p
.add_element(Kind::Entity, Layer::Oa, "Pump", Some(north))
.unwrap();
let second = p
.add_element(Kind::Entity, Layer::Oa, "Pump", Some(south))
.unwrap();
let label = reference_label(&p, second);
assert_ne!(reference_label(&p, first), label);
assert!(label.contains("South station") && label.contains("Pump"));
assert!(label.contains(&p.elements[&second].alias));
let found = choices(&p, Layer::Oa, Picker::Owner, "south station");
assert!(found.iter().any(|e| e.id == second));
assert!(!found.iter().any(|e| e.id == first));
let mut state = start(&p, Layer::Oa, Family::Machine, "Pump states");
state.update(&p, Event::Owner(second)).unwrap();
let result = execute_batch(&p, &[state.prepare_pending(&p).unwrap()]).unwrap();
assert_eq!(
result.elements[&state.created().unwrap().behavior].parent,
Some(second)
);
}
#[test]
fn corrupted_recovery_and_reserved_identity_collision_refuse_without_repairing_raw_values() {
let p = Project::empty();
let state = start(&p, Layer::Sa, Family::Sequence, "Supply");
let raw = state.recovery_snapshot().unwrap().unwrap();
for (field, value) in [
("version", serde_json::json!(99)),
("unexpected", serde_json::json!(true)),
] {
let mut bad = raw.clone();
bad[field] = value;
assert!(State::from_recovery(bad).is_err());
}
let mut bad = raw;
bad["element"] = serde_json::json!(p.id);
let restored = State::from_recovery(bad.clone()).unwrap();
assert!(
restored
.prepare_pending(&p)
.unwrap_err()
.contains("identity")
);
assert_eq!(restored.recovery_snapshot().unwrap(), Some(bad));
}
#[test]
fn post_commit_selection_is_correlated_with_exact_created_behavior_and_view() {
let mut p = Project::empty();
let owner = p
.add_element(Kind::Entity, Layer::Oa, "Pump", None)
.unwrap();
let mut state = start(&p, Layer::Oa, Family::Machine, "Pump modes");
state.update(&p, Event::Owner(owner)).unwrap();
state.update(&p, Event::FirstView(true)).unwrap();
let command = state.prepare_pending(&p).unwrap();
assert_eq!(state.created_by(&p, &command), state.created());
assert_eq!(
state.created_by(&p, &Command::Rename(owner, "Unrelated change".into())),
None
);
for edit in 0..4 {
let mut changed = command.clone();
let Command::Batch(ref mut commands) = changed else {
panic!("creation batch");
};
if edit < 3 {
let Command::CreateElement(element) = &mut commands[0] else {
panic!("new behavior");
};
match edit {
0 => element.id = Id::new_v4(),
1 => element.name = "A different authored draft".into(),
_ => element.parent = None,
}
} else {
let Command::CreateDiagram(diagram) = &mut commands[1] else {
panic!("new view");
};
diagram.projection = ProjectionKind::StateMachine { machine: owner };
}
assert_eq!(state.created_by(&p, &changed), None);
}
let Command::Batch(commands) = command else {
panic!("creation batch");
};
assert_eq!(
state.created_by(&p, &commands[0]),
None,
"an opt-in view must be part of the actual committed batch"
);
}
#[test]
fn optional_context_collapses_without_losing_authored_reference_or_raw_search() {
let mut p = Project::empty();
let context = p
.add_element(Kind::Capability, Layer::Oa, "Water supply", None)
.unwrap();
let mut state = start(&p, Layer::Oa, Family::Scenario, "Supply nominal");
assert!(!state.draft.as_ref().unwrap().context_open);
state
.update(&p, Event::Viewpoint(ScenarioViewpoint::OperationalActivity))
.unwrap();
state.update(&p, Event::ContextOptions(true)).unwrap();
state
.update(&p, Event::Search(Picker::Context, " Water ".into()))
.unwrap();
state.update(&p, Event::Context(Some(context))).unwrap();
let command = state.prepare_pending(&p).unwrap();
state.update(&p, Event::ContextOptions(false)).unwrap();
let restored = State::from_recovery(state.recovery_snapshot().unwrap().unwrap()).unwrap();
assert!(!restored.draft.as_ref().unwrap().context_open);
assert_eq!(
restored.draft.as_ref().unwrap().browsers[1].query,
" Water "
);
assert_eq!(restored.prepare_pending(&p).unwrap(), command);
assert_eq!(restored.created_by(&p, &command), restored.created());
}
#[test]
fn creation_selection_rejects_changed_scenario_body_and_post_creation_operations() {
let mut p = Project::empty();
let capability = p
.add_element(Kind::SystemCapability, Layer::Sa, "Supply", None)
.unwrap();
let mut state = start(&p, Layer::Sa, Family::Scenario, "Nominal functions");
state
.update(&p, Event::Viewpoint(ScenarioViewpoint::Functional))
.unwrap();
let command = state.prepare_pending(&p).unwrap();
assert_eq!(state.created_by(&p, &command), state.created());
for edit in 0..4 {
let mut altered = command.clone();
let Command::Batch(commands) = &mut altered else {
panic!("creation batch");
};
match edit {
0 => {
let Command::CreateElement(element) = &mut commands[0] else {
panic!("scenario");
};
let Some(SharedBody::Scenario { viewpoint, .. }) = element.shared_mut() else {
panic!("scenario body");
};
*viewpoint = ScenarioViewpoint::BoundaryInterface;
}
1 => {
let Command::CreateElement(element) = &mut commands[0] else {
panic!("scenario");
};
let Some(SharedBody::Scenario { context, .. }) = element.shared_mut() else {
panic!("scenario body");
};
*context = Some(capability);
}
2 => commands.push(Command::Rename(
state.created().unwrap().behavior,
"Changed afterward".into(),
)),
_ => commands.push(Command::Delete(state.created().unwrap().behavior)),
}
assert!(
execute_batch(&p, std::slice::from_ref(&altered)).is_ok(),
"the alternate command is valid, but is not this authored creation"
);
assert_eq!(state.created_by(&p, &altered), None);
}
}