use oa_core::{ data::*, references, transitions::*, validation::{self, Severity}, *, }; fn add(p: &mut Project, kind: Kind, name: &str, parent: Option) -> Id { p.add_element(kind, Layer::Oa, name, parent).unwrap() } fn body(p: &mut Project, id: Id, body: SharedBody) { *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = body; } fn make_record(p: &mut Project, name: &str, supertype: Option) -> Id { let id = add(p, Kind::DataType, name, None); body( p, id, SharedBody::DataType { definition: DataTypeDefinition::Record { supertype }, default: None, unit: String::new(), }, ); id } fn field( p: &mut Project, owner: Id, name: &str, type_ref: TypeRef, direction: ParameterDirection, ) -> Id { let id = add(p, Kind::DataField, name, Some(owner)); body( p, id, SharedBody::DataField { type_ref, minimum: 1, maximum: Some(1), unit: String::new(), direction, storage: FieldStorage::Value, default: None, }, ); id } fn edited(p: &Project, id: Id, change: impl FnOnce(&mut SharedBody)) -> Command { let mut e = p.elements[&id].clone(); change(e.shared_mut().unwrap()); Command::EditElement(id, e) } fn valid(p: &Project) { let errors: Vec<_> = validation::validate(p) .into_iter() .filter(|i| i.severity == Severity::Error) .collect(); assert!(errors.is_empty(), "{errors:?}"); } fn code(p: &Project, code: &str, severity: Severity) -> bool { validation::validate(p) .iter() .any(|i| i.code == code && i.severity == severity) } fn values(p: &Project, id: Id) -> &Vec { let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { values, .. }, .. }) = p.elements[&id].shared() else { panic!() }; values } #[allow(clippy::too_many_arguments)] fn message( p: &mut Project, scenario: Id, exchange: Id, kind: MessageKind, reply_to: Option, name: &str, values: Vec, ) -> Id { let id = add(p, Kind::Occurrence, name, Some(scenario)); body( p, id, SharedBody::Occurrence { body: OccurrenceBody::Exchange { exchange: Some(exchange), sender: None, receiver: None, message_kind: kind, reply_to, values, }, precondition: String::new(), postcondition: String::new(), }, ); p.elements .get_mut(&scenario) .unwrap() .steps_mut() .unwrap() .push(id); id } struct Payload { p: Project, item: Id, record: Id, leaf: Id, origin: Id, destination: Id, exchange: Id, call: Id, reply: Id, } fn payload() -> Payload { let mut p = Project::empty(); let item = add(&mut p, Kind::ExchangeItem, "Move", None); body( &mut p, item, SharedBody::ExchangeItem { kind: ExchangeItemKind::Information, mechanism: ExchangeMechanism::Operation, supertype: None, }, ); let record = make_record(&mut p, "Coordinate", None); let leaf = field( &mut p, record, "latitude", TypeRef::Real, ParameterDirection::Unspecified, ); let origin = field( &mut p, item, "origin", TypeRef::Named(record), ParameterDirection::In, ); let destination = field( &mut p, item, "destination", TypeRef::Named(record), ParameterDirection::Out, ); let a = add(&mut p, Kind::Activity, "Request movement", None); let b = add(&mut p, Kind::Activity, "Perform movement", None); let exchange = p.relate(RelationKind::Interaction, "Movement invocation", a, b); if let RelationBody::Interaction { items, .. } = &mut p.relations.get_mut(&exchange).unwrap().body { items.push(item); } let scenario = add(&mut p, Kind::Scenario, "Movement conversation", None); let call = message( &mut p, scenario, exchange, MessageKind::Call, None, "Call", vec![MessageValue { path: vec![origin], field: leaf, expression: "45.0".into(), }], ); let reply = message( &mut p, scenario, exchange, MessageKind::Reply, Some(call), "Reply", vec![MessageValue { path: vec![destination], field: leaf, expression: "46.0".into(), }], ); valid(&p); Payload { p, item, record, leaf, origin, destination, exchange, call, reply, } } #[test] fn scalar_bounds_defaults_unicode_units_and_failed_batches_are_exact() { let mut p = Project::empty(); let integer = add(&mut p, Kind::DataType, "Full precision integer", None); body( &mut p, integer, SharedBody::DataType { definition: DataTypeDefinition::Integer { minimum: Some(i64::MAX - 1), maximum: Some(i64::MAX), }, default: Some(DataValue::Integer(i64::MAX)), unit: "count".into(), }, ); let text = add(&mut p, Kind::DataType, "Two scalars", None); body( &mut p, text, SharedBody::DataType { definition: DataTypeDefinition::String { minimum_length: 2, maximum_length: Some(2), }, default: Some(DataValue::String("é🙂".into())), unit: String::new(), }, ); let real = add(&mut p, Kind::DataType, "Mass flow", None); body( &mut p, real, SharedBody::DataType { definition: DataTypeDefinition::Real { minimum: Some(0.0), maximum: Some(25.0), }, default: Some(DataValue::Real(1.0)), unit: "kg/s".into(), }, ); let item = add(&mut p, Kind::ExchangeItem, "Fuel", None); body( &mut p, item, SharedBody::ExchangeItem { kind: ExchangeItemKind::Material, mechanism: ExchangeMechanism::Flow, supertype: None, }, ); let amount = field( &mut p, item, "amount", TypeRef::Named(real), ParameterDirection::Unspecified, ); assert_eq!(effective_field_unit(&p, &p.elements[&amount]), Some("kg/s")); assert_eq!( effective_element_default(&p, &p.elements[&amount]), Some(&DataValue::Real(1.0)) ); valid(&p); assert!( validate_data_value( &p, &TypeRef::Named(integer), &DataValue::Integer(i64::MAX - 2) ) .is_err() ); assert!(validate_data_value(&p, &TypeRef::Named(real), &DataValue::Integer(1)).is_err()); assert!(validate_data_value(&p, &TypeRef::Real, &DataValue::Real(f64::INFINITY)).is_err()); assert!( validate_data_value(&p, &TypeRef::Named(text), &DataValue::String("é".into())).is_err() ); let mut session = Session::new(p.clone()); let invalid = edited(&p, integer, |s| { if let SharedBody::DataType { default, .. } = s { *default = Some(DataValue::Integer(i64::MIN)); } }); assert!( session .execute(Command::Batch(vec![ Command::Rename(item, "Unsaved name".into()), invalid ])) .is_err() ); assert_eq!(session.project(), &p); assert_eq!(session.revision(), 0); assert!(!session.can_undo()); let invalid = edited(&p, real, |s| { if let SharedBody::DataType { definition, .. } = s { *definition = DataTypeDefinition::Real { minimum: Some(f64::NAN), maximum: None, }; } }); assert!(session.execute(invalid).is_err()); let empty = edited(&p, amount, |s| { if let SharedBody::DataField { minimum, default, .. } = s { *minimum = 0; *default = Some(vec![]); } }); session.execute(empty).unwrap(); valid(session.project()); session.undo(); assert_eq!(session.project(), &p); } #[test] fn enum_literal_edits_validate_type_and_field_defaults_in_one_candidate() { let mut p = Project::empty(); let enumeration = add(&mut p, Kind::DataType, "Mode", None); body( &mut p, enumeration, SharedBody::DataType { definition: DataTypeDefinition::Enumeration { literals: vec!["Ready".into(), "Busy".into()], }, default: Some(DataValue::Enumeration("Ready".into())), unit: String::new(), }, ); let item = add(&mut p, Kind::ExchangeItem, "Status", None); let mode = field( &mut p, item, "mode", TypeRef::Named(enumeration), ParameterDirection::Unspecified, ); if let Some(SharedBody::DataField { default, .. }) = p.elements.get_mut(&mode).unwrap().shared_mut() { *default = Some(vec![DataValue::Enumeration("Ready".into())]); } let mut session = Session::new(p.clone()); let change_type = edited(&p, enumeration, |s| { if let SharedBody::DataType { definition: DataTypeDefinition::Enumeration { literals }, default, .. } = s { literals[0] = "Idle".into(); *default = Some(DataValue::Enumeration("Idle".into())); } }); assert!(session.execute(change_type.clone()).is_err()); assert_eq!(session.project(), &p); let change_field = edited(&p, mode, |s| { if let SharedBody::DataField { default, .. } = s { *default = Some(vec![DataValue::Enumeration("Idle".into())]); } }); session .execute(Command::Batch(vec![change_type, change_field])) .unwrap(); valid(session.project()); session.undo(); assert_eq!(session.project(), &p); } #[test] fn an_empty_enumeration_is_a_located_incomplete_draft() { let mut p = Project::empty(); let enumeration = add(&mut p, Kind::DataType, "Pending values", None); body( &mut p, enumeration, SharedBody::DataType { definition: DataTypeDefinition::Enumeration { literals: vec![] }, default: None, unit: String::new(), }, ); valid(&p); assert!( validation::validate(&p) .iter() .any(|issue| issue.code == "empty_enumeration" && issue.severity == Severity::Warning && issue.element == Some(enumeration) && issue.field.as_deref() == Some("literals")) ); Session::try_new(p).unwrap(); } #[test] fn inherited_interfaces_and_item_specializations_satisfy_typed_port_contracts() { let mut p = Project::empty(); let a = p .add_element(Kind::Function, Layer::Sa, "Send", None) .unwrap(); let b = p .add_element(Kind::Function, Layer::Sa, "Receive", None) .unwrap(); let ca = p .add_element(Kind::System, Layer::Sa, "Client", None) .unwrap(); let cb = p .add_element(Kind::SystemActor, Layer::Sa, "Provider", None) .unwrap(); p.relate(RelationKind::Allocation, "Client work", ca, a); p.relate(RelationKind::Allocation, "Provider work", cb, b); let item = p .add_element(Kind::ExchangeItem, Layer::Sa, "Base item", None) .unwrap(); let specialized = p .add_element(Kind::ExchangeItem, Layer::Sa, "Specialized item", None) .unwrap(); body( &mut p, specialized, SharedBody::ExchangeItem { kind: ExchangeItemKind::Information, mechanism: ExchangeMechanism::Unspecified, supertype: Some(item), }, ); let fe = Id::new_v4(); let request = FunctionalExchangeRequest { id: fe, alias: "specialized_exchange".into(), name: "Specialized exchange".into(), description: String::new(), source: a, target: b, source_port: PortChoice::Create, target_port: PortChoice::Create, items: vec![item], }; p = execute_batch(&p, &plan_functional_exchange(&p, &request).unwrap()).unwrap(); let mut changed = p.relations[&fe].clone(); if let RelationBody::FunctionalExchange { items, .. } = &mut changed.body { *items = vec![specialized]; } p = execute_batch(&p, &[Command::EditRelation(fe, changed)]).unwrap(); let boundary = Id::new_v4(); let request = ComponentExchangeRequest { functional_exchange: fe, boundary: ComponentExchangeChoice::Create { id: boundary, alias: "service_boundary".into(), name: "Service boundary".into(), description: String::new(), port_kind: ComponentPortKind::Standard, oriented: false, source_port: PortChoice::Create, target_port: PortChoice::Create, }, }; p = execute_batch(&p, &plan_component_exchange(&p, &request).unwrap()).unwrap(); let interface = p .add_element(Kind::Interface, Layer::Sa, "Service", None) .unwrap(); let extended = p .add_element(Kind::Interface, Layer::Sa, "Extended service", None) .unwrap(); body( &mut p, extended, SharedBody::Interface { supertype: Some(interface), }, ); p.relate( RelationKind::InterfaceItemAllocation, "Service item", interface, item, ); let (source, target) = p.relations[&boundary].body.endpoints(); p.relate( RelationKind::InterfaceRequirement, "Required service", source, interface, ); let provider = p.relate( RelationKind::InterfaceProvision, "Provided service", target, extended, ); valid(&p); assert!(!code( &p, "unsatisfied_interface_requirement", Severity::Warning )); let unrelated = p .add_element(Kind::Interface, Layer::Sa, "Unrelated service", None) .unwrap(); p.relate( RelationKind::InterfaceItemAllocation, "Other item contract", unrelated, item, ); p.relations .get_mut(&provider) .unwrap() .body .set_endpoints(target, unrelated); valid(&p); assert!(code( &p, "unsatisfied_interface_requirement", Severity::Warning )); } #[test] fn repeated_record_paths_enforce_call_reply_phase_and_survive_identity_changes() { let Payload { p, item, record, leaf, origin, destination, exchange, call, reply, } = payload(); let paths = carried_field_paths(&p, &p.relations[&exchange]); assert_eq!(paths.iter().filter(|f| f.field == leaf).count(), 2); let mut session = Session::new(p.clone()); let wrong = edited(&p, call, |s| { if let SharedBody::Occurrence { body: OccurrenceBody::Exchange { values, .. }, .. } = s { values[0].path = vec![destination]; } }); assert!( session .execute(wrong) .unwrap_err() .0 .contains("message_parameter_phase") ); assert_eq!(session.project(), &p); let new_origin = Id::new_v4(); let new_leaf = Id::new_v4(); session .execute(Command::Batch(vec![ Command::Reidentify(origin, new_origin), Command::Reidentify(leaf, new_leaf), ])) .unwrap(); assert_eq!(values(session.project(), call)[0].path, vec![new_origin]); assert_eq!(values(session.project(), call)[0].field, new_leaf); assert_eq!(values(session.project(), reply)[0].field, new_leaf); session.undo(); assert_eq!(session.project(), &p); session.execute(Command::Delete(origin)).unwrap(); assert!(values(session.project(), call).is_empty()); assert_eq!(values(session.project(), reply).len(), 1); assert!(session.project().elements.contains_key(&record)); assert!(session.project().elements.contains_key(&leaf)); session.undo(); assert_eq!(session.project(), &p); let reference = edited(&p, origin, |s| { if let SharedBody::DataField { storage, .. } = s { *storage = FieldStorage::Reference; } }); assert!( session .execute(reference) .unwrap_err() .0 .contains("message_value_field") ); let direction = edited(&p, origin, |s| { if let SharedBody::DataField { direction, .. } = s { *direction = ParameterDirection::Unspecified; } }); let generic = edited(&p, item, |s| { if let SharedBody::ExchangeItem { mechanism, .. } = s { *mechanism = ExchangeMechanism::Flow; } }); assert!( session .execute(Command::Batch(vec![direction, generic])) .is_err() ); } #[test] fn ambiguous_legacy_annotations_warn_and_reference_fields_stop_payload_descent() { let Payload { mut p, origin, destination, call, reply, leaf, exchange, .. } = payload(); for message in [call, reply] { if let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { values, .. }, .. }) = p.elements.get_mut(&message).unwrap().shared_mut() { values[0].path.clear(); } } valid(&p); assert!(code(&p, "message_value_ambiguous_path", Severity::Warning)); if let Some(SharedBody::DataField { storage, .. }) = p.elements.get_mut(&origin).unwrap().shared_mut() { *storage = FieldStorage::Reference; } let paths = carried_field_paths(&p, &p.relations[&exchange]); assert!(paths.iter().any(|f| f.field == origin && f.path.is_empty())); assert_eq!( paths .iter() .filter(|f| f.field == leaf) .map(|f| &f.path) .collect::>(), vec![&vec![destination]] ); } #[test] fn identified_allocations_keep_protocols_order_artifacts_and_inherited_identity() { let mut p = Project::empty(); let item = add(&mut p, Kind::ExchangeItem, "Telemetry", None); let other_item = add(&mut p, Kind::ExchangeItem, "Command", None); let first = add(&mut p, Kind::Interface, "Primary", None); let second = add(&mut p, Kind::Interface, "Secondary", None); let derived = add(&mut p, Kind::Interface, "Specialized primary", None); body( &mut p, derived, SharedBody::Interface { supertype: Some(first), }, ); let a = p.relate( RelationKind::InterfaceItemAllocation, "Primary telemetry", first, item, ); let b = p.relate( RelationKind::InterfaceItemAllocation, "Secondary telemetry", second, item, ); if let RelationBody::InterfaceItemAllocation { transmission, acquisition, .. } = &mut p.relations.get_mut(&a).unwrap().body { *transmission = TransmissionProtocol::Synchronous; *acquisition = AcquisitionProtocol::Read; } if let RelationBody::InterfaceItemAllocation { transmission, order, .. } = &mut p.relations.get_mut(&b).unwrap().body { *transmission = TransmissionProtocol::Broadcast; *order = 4; } let local = p.relate( RelationKind::InterfaceItemAllocation, "Local command", derived, other_item, ); assert_eq!( effective_interface_allocations(&p, derived) .iter() .map(|r| r.id) .collect::>(), vec![a, local] ); assert!(interface_satisfies(&p, derived, first)); assert!(!interface_satisfies(&p, first, derived)); let artifact_id = Id::new_v4(); p.artifact_links.insert( artifact_id, ArtifactLink { id: artifact_id, alias: "allocation_spec".into(), name: "Protocol specification".into(), description: String::new(), owner: a, relation: ArtifactRelation::References, uri: "https://example.org/protocol".into(), revision: None, fragment: None, }, ); valid(&p); let mut session = Session::new(p.clone()); let duplicate = Relation { id: Id::new_v4(), alias: "duplicate_allocation".into(), name: "Duplicate inherited item".into(), description: String::new(), body: RelationBody::new(RelationKind::InterfaceItemAllocation, derived, item), }; assert!(session.execute(Command::CreateRelation(duplicate)).is_err()); let impact = references::deletion_impact(&p, a); assert!(impact.contains(&artifact_id)); assert!(!impact.contains(&item)); assert!(!impact.contains(&b)); session.execute(Command::Delete(a)).unwrap(); assert_eq!( interface_items(session.project(), derived), vec![other_item] ); assert_eq!(session.project().relations[&b], p.relations[&b]); assert!(session.project().elements.contains_key(&item)); session.undo(); assert_eq!(session.project(), &p); let new_item = Id::new_v4(); session .execute(Command::Reidentify(item, new_item)) .unwrap(); assert_eq!(session.project().relations[&a].target(), new_item); assert_eq!(session.project().relations[&b].target(), new_item); } #[test] fn specialization_preserves_fields_rejects_cycles_and_prunes_removed_inherited_paths() { let Payload { mut p, item, record, leaf, origin, call, reply, .. } = payload(); let specialized = make_record(&mut p, "Refined coordinate", Some(record)); if let Some(SharedBody::DataField { type_ref, .. }) = p.elements.get_mut(&origin).unwrap().shared_mut() { *type_ref = TypeRef::Named(specialized); } valid(&p); assert_eq!(effective_fields(&p, specialized)[0].id, leaf); assert_eq!(p.elements[&leaf].parent, Some(record)); let mut session = Session::new(p.clone()); let unbase = edited(&p, specialized, |s| { if let SharedBody::DataType { definition: DataTypeDefinition::Record { supertype }, .. } = s { *supertype = None; } }); let mut explicit = p.clone(); if let Command::EditElement(id, e) = &unbase { explicit.elements.insert(*id, e.clone()); } assert!( session .execute(Command::Replace(explicit)) .unwrap_err() .0 .contains("message_value_field") ); assert_eq!(session.project(), &p); session.execute(unbase).unwrap(); assert!(values(session.project(), call).is_empty()); assert_eq!(values(session.project(), reply).len(), 1); session.undo(); assert_eq!(session.project(), &p); let cycle = edited(&p, record, |s| { if let SharedBody::DataType { definition: DataTypeDefinition::Record { supertype }, .. } = s { *supertype = Some(specialized); } }); assert!( session .execute(cycle) .unwrap_err() .0 .contains("data_inheritance_cycle") ); let shadow = field( &mut p, specialized, "latitude", TypeRef::Real, ParameterDirection::Unspecified, ); assert!(code(&p, "inherited_field_shadow", Severity::Error)); p.elements.remove(&shadow); let derived_item = add(&mut p, Kind::ExchangeItem, "Precise move", None); body( &mut p, derived_item, SharedBody::ExchangeItem { kind: ExchangeItemKind::Information, mechanism: ExchangeMechanism::Operation, supertype: Some(item), }, ); assert!(oa_core::item_satisfies(&p, derived_item, item)); assert!(!oa_core::item_satisfies(&p, item, derived_item)); let result = field( &mut p, item, "result", TypeRef::Boolean, ParameterDirection::Return, ); field( &mut p, derived_item, "second result", TypeRef::Boolean, ParameterDirection::Return, ); assert!(code(&p, "multiple_return_parameters", Severity::Error)); assert_eq!(p.elements[&result].parent, Some(item)); } #[test] fn removing_inheritance_prunes_legacy_annotations_only_when_every_reachable_path_disappears() { let Payload { mut p, record, origin, destination, call, reply, .. } = payload(); let derived = make_record(&mut p, "Inherited arguments", Some(record)); for id in [origin, destination] { if let Some(SharedBody::DataField { type_ref, .. }) = p.elements.get_mut(&id).unwrap().shared_mut() { *type_ref = TypeRef::Named(derived); } } for id in [call, reply] { if let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { values, .. }, .. }) = p.elements.get_mut(&id).unwrap().shared_mut() { values[0].path.clear(); } } valid(&p); assert!(code(&p, "message_value_ambiguous_path", Severity::Warning)); let remove = edited(&p, derived, |s| { if let SharedBody::DataType { definition: DataTypeDefinition::Record { supertype }, .. } = s { *supertype = None; } }); let mut session = Session::new(p.clone()); session.execute(remove).unwrap(); assert!(values(session.project(), call).is_empty()); assert!(values(session.project(), reply).is_empty()); assert!(session.project().elements.contains_key(&record)); session.undo(); assert_eq!(session.project(), &p); } #[test] fn recursive_value_types_warn_while_optional_reference_cycles_and_associations_are_distinct() { let mut p = Project::empty(); let a = make_record(&mut p, "Node", None); let b = make_record(&mut p, "Envelope", None); let next = field( &mut p, a, "next", TypeRef::Named(a), ParameterDirection::Unspecified, ); field( &mut p, b, "root", TypeRef::Named(a), ParameterDirection::Unspecified, ); valid(&p); assert!(code(&p, "record_no_finite_value", Severity::Warning)); if let Some(SharedBody::DataField { minimum, .. }) = p.elements.get_mut(&next).unwrap().shared_mut() { *minimum = 0; } assert!(!code(&p, "record_no_finite_value", Severity::Warning)); if let Some(SharedBody::DataField { minimum, storage, .. }) = p.elements.get_mut(&next).unwrap().shared_mut() { *minimum = 1; *storage = FieldStorage::Reference; } assert!(!code(&p, "record_no_finite_value", Severity::Warning)); let association = p.relate(RelationKind::DataAssociation, "Parent to children", a, a); if let RelationBody::DataAssociation { source_role, target_role, source_navigable, target_navigable, .. } = &mut p.relations.get_mut(&association).unwrap().body { *source_role = "parent".into(); *target_role = "children".into(); *source_navigable = true; *target_navigable = true; } valid(&p); let mut duplicate = p.relations[&association].clone(); duplicate.id = Id::new_v4(); duplicate.alias = "second_role_contract".into(); p.relations.insert(duplicate.id, duplicate); valid(&p); assert!(code(&p, "duplicate_association_roles", Severity::Warning)); let mut session = Session::new(p.clone()); let mut invalid = p.relations[&association].clone(); if let RelationBody::DataAssociation { target_role, .. } = &mut invalid.body { *target_role = "parent".into(); } assert!( session .execute(Command::EditRelation(association, invalid)) .is_err() ); session.execute(Command::Delete(association)).unwrap(); assert!(session.project().elements.contains_key(&a)); assert!(session.project().elements.contains_key(&next)); } #[test] fn a_self_association_is_visible_with_neutral_connector_and_real_role_contracts() { let mut p = Project::empty(); let record = make_record(&mut p, "Tree node", None); let relation = p.relate( RelationKind::DataAssociation, "Parent-child reference", record, record, ); if let RelationBody::DataAssociation { source_role, target_role, source_maximum, target_navigable, .. } = &mut p.relations.get_mut(&relation).unwrap().body { *source_role = "parent".into(); *target_role = "children".into(); *source_maximum = Some(1); *target_navigable = true; } let diagram = p.ensure_diagram(Layer::Oa); p.place(diagram, record, Position { x: 100., y: 100. }); let item = add( &mut p, Kind::ExchangeItem, "Nested allocation payload", None, ); let interface = add(&mut p, Kind::Interface, "Nested allocation owner", None); let allocation = p.relate( RelationKind::InterfaceItemAllocation, "Nested contract row", interface, item, ); p.place(diagram, item, Position { x: 600., y: 100. }); p.place(diagram, interface, Position { x: 600., y: 300. }); let view = oa_core::projection::project_view(&p, diagram); assert!(!view.edges.iter().any(|e| e.id == allocation)); let projected = view.edges.iter().find(|e| e.id == relation).unwrap(); assert_eq!(projected.source, projected.target); assert!(!projected.oriented); let routed = oa_core::routing::route_diagram(&p, &p.diagrams[&diagram], &view); let edge = &routed.edges[&relation]; assert!(edge.points.len() > 3); assert_ne!(edge.points.first(), edge.points.last()); assert_eq!(edge.label.text, "Parent-child reference"); assert_eq!( association_end_label(&p.relations[&relation], true).unwrap(), "parent [0..1]" ); assert_eq!( association_end_label(&p.relations[&relation], false).unwrap(), "children [0..*]" ); let ends = oa_core::routing::association_end_geometry(&p.relations[&relation], edge); assert_eq!(ends.len(), 2); assert!(ends[0].source_end); assert!(!ends[0].navigable); assert!(ends[1].navigable); assert_eq!(ends[0].tip, *edge.points.first().unwrap()); assert_eq!(ends[1].tip, *edge.points.last().unwrap()); assert_ne!(ends[0].label.center, ends[1].label.center); for end in &ends { let b = end.label.bounds; assert!( b.position.x >= routed.bounds.position.x && b.position.y >= routed.bounds.position.y ); assert!( b.position.x + b.size.width <= routed.bounds.position.x + routed.bounds.size.width + 0.01 ); assert!( b.position.y + b.size.height <= routed.bounds.position.y + routed.bounds.size.height + 0.01 ); } let mut changed = p.relations[&relation].clone(); if let RelationBody::DataAssociation { source_navigable, target_navigable, .. } = &mut changed.body { *source_navigable = true; *target_navigable = false; } let changed_ends = oa_core::routing::association_end_geometry(&changed, edge); assert!(changed_ends[0].navigable); assert!(!changed_ends[1].navigable); assert_eq!(changed_ends[0].tip, ends[0].tip); assert_eq!(changed_ends[1].tip, ends[1].tip); for points in [ vec![Position { x: 0., y: 0. }, Position { x: 12., y: 0. }], vec![ Position { x: 0., y: 0. }, Position { x: 18., y: 0. }, Position { x: 18., y: 5. }, Position { x: 0., y: 5. }, ], ] { let mut short = edge.clone(); short.points = points; let labels = oa_core::routing::association_end_geometry(&changed, &short); let a = labels[0].label.bounds; let b = labels[1].label.bounds; assert!( a.position.x + a.size.width <= b.position.x || b.position.x + b.size.width <= a.position.x || a.position.y + a.size.height <= b.position.y || b.position.y + b.size.height <= a.position.y, "End labels overlap on a short or self route: {labels:?}" ); } } #[test] fn empty_record_collections_are_supported_but_record_literals_and_reference_defaults_are_not() { let mut p = Project::empty(); let record = make_record(&mut p, "Collection member", None); let item = add(&mut p, Kind::ExchangeItem, "Collection", None); let collection = field( &mut p, item, "members", TypeRef::Named(record), ParameterDirection::Unspecified, ); if let Some(SharedBody::DataField { minimum, maximum, default, .. }) = p.elements.get_mut(&collection).unwrap().shared_mut() { *minimum = 0; *maximum = None; *default = Some(vec![]); } valid(&p); let mut session = Session::new(p.clone()); let reference = edited(&p, collection, |s| { if let SharedBody::DataField { storage, .. } = s { *storage = FieldStorage::Reference; } }); assert!( session .execute(reference) .unwrap_err() .0 .contains("reference_field_contract") ); let literal = edited(&p, collection, |s| { if let SharedBody::DataField { default, .. } = s { *default = Some(vec![DataValue::String("{}".into())]); } }); assert!(session.execute(literal).is_err()); assert_eq!(session.project(), &p); } #[test] fn exponential_payload_expansion_is_reported_and_recursive_finite_paths_remain_valid() { let mut p = Project::empty(); let item = add(&mut p, Kind::ExchangeItem, "Bounded payload", None); let mut owner = item; for depth in 0..15 { let next = make_record(&mut p, &format!("Level {depth}"), None); field( &mut p, owner, "left", TypeRef::Named(next), ParameterDirection::Unspecified, ); field( &mut p, owner, "right", TypeRef::Named(next), ParameterDirection::Unspecified, ); owner = next; } let a = add(&mut p, Kind::Activity, "A", None); let b = add(&mut p, Kind::Activity, "B", None); let exchange = p.relate(RelationKind::Interaction, "Expanded exchange", a, b); if let RelationBody::Interaction { items, .. } = &mut p.relations.get_mut(&exchange).unwrap().body { items.push(item); } assert!(try_carried_field_paths(&p, &p.relations[&exchange]).is_err()); assert!(code(&p, "data_path_expansion_limit", Severity::Error)); let mut p = Project::empty(); let item = add(&mut p, Kind::ExchangeItem, "Recursive payload", None); let record = make_record(&mut p, "Node", None); let root = field( &mut p, item, "root", TypeRef::Named(record), ParameterDirection::Unspecified, ); let next = field( &mut p, record, "next", TypeRef::Named(record), ParameterDirection::Unspecified, ); if let Some(SharedBody::DataField { minimum, .. }) = p.elements.get_mut(&next).unwrap().shared_mut() { *minimum = 0; } let a = add(&mut p, Kind::Activity, "A", None); let b = add(&mut p, Kind::Activity, "B", None); let exchange = p.relate(RelationKind::Interaction, "Recursive exchange", a, b); if let RelationBody::Interaction { items, .. } = &mut p.relations.get_mut(&exchange).unwrap().body { items.push(item); } valid(&p); assert!(try_carried_field_paths(&p, &p.relations[&exchange]).is_ok()); assert!(carried_value_path(&p, &p.relations[&exchange], &[root, next, next], next).is_some()); assert_eq!( resolve_legacy_field_path(&p, &p.relations[&exchange], next).unwrap(), LegacyFieldPathResolution::Ambiguous ); let scenario = add(&mut p, Kind::Scenario, "Recursive annotation", None); message( &mut p, scenario, exchange, MessageKind::Asynchronous, None, "Legacy recursive value", vec![MessageValue { path: vec![], field: next, expression: "descriptive annotation".into(), }], ); valid(&p); assert!(code(&p, "message_value_ambiguous_path", Severity::Warning)); } #[test] fn initial_transition_copies_qualified_paths_inheritance_allocations_and_associations() { let Payload { mut p, item, record, origin, leaf, call, .. } = payload(); let base = make_record(&mut p, "Reference base", None); if let Some(SharedBody::DataType { definition: DataTypeDefinition::Record { supertype }, .. }) = p.elements.get_mut(&record).unwrap().shared_mut() { *supertype = Some(base); } let interface = add(&mut p, Kind::Interface, "Movement service", None); let allocation = p.relate( RelationKind::InterfaceItemAllocation, "Service operation", interface, item, ); let association = p.relate( RelationKind::DataAssociation, "Coordinate relation", record, base, ); let scenario = p.elements[&call].parent.unwrap(); let mut request = TransitionRequest::new(TransitionDirection::OaToSa, vec![scenario, interface]); request.scope = DependencyScope::Required; let plan = plan_transition(&p, &request).unwrap(); assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); let candidate = apply_transition(&p, &plan).unwrap(); valid(&candidate); let mapped = |id| plan.changes.iter().find(|c| c.source == id).unwrap().target; assert_eq!( values(&candidate, mapped(call))[0].path, vec![mapped(origin)] ); assert_eq!(values(&candidate, mapped(call))[0].field, mapped(leaf)); assert_eq!( candidate.relations[&mapped(allocation)].source(), mapped(interface) ); assert_eq!( candidate.relations[&mapped(association)].target(), mapped(base) ); assert_eq!( supertype(&candidate.elements[&mapped(record)]), Some(mapped(base)) ); assert_eq!(p.elements[&leaf].parent, Some(record)); } #[test] fn required_dictionary_closure_does_not_expand_through_incident_associations() { let mut p = Project::empty(); let a = make_record(&mut p, "A", None); let b = make_record(&mut p, "B", None); let c = make_record(&mut p, "C", None); let ab = p.relate(RelationKind::DataAssociation, "A to B", a, b); let bc = p.relate(RelationKind::DataAssociation, "B to C", b, c); for (sources, expected, omitted) in [ (vec![a], vec![a], vec![b, c, ab, bc]), (vec![a, b], vec![a, b, ab], vec![c, bc]), (vec![ab], vec![a, b, ab], vec![c, bc]), ] { let mut request = TransitionRequest::new(TransitionDirection::OaToSa, sources); request.scope = DependencyScope::Required; let plan = plan_transition(&p, &request).unwrap(); assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); let mapped: std::collections::BTreeSet<_> = plan.changes.iter().map(|change| change.source).collect(); assert_eq!(mapped, expected.into_iter().collect()); for id in omitted { assert!(!mapped.contains(&id)); } valid(&apply_transition(&p, &plan).unwrap()); } } #[test] #[ignore = "explicit inheritance scaling probe; run with --ignored --nocapture"] fn inheritance_chain_validation_scaling_probe() { for count in [250, 500, 1000] { let mut p = Project::empty(); let mut base = None; for index in 0..count { base = Some(make_record(&mut p, &format!("Record {index}"), base)); } let started = std::time::Instant::now(); valid(&p); eprintln!( "R6 inheritance validation {count} records: {:.3} ms", started.elapsed().as_secs_f64() * 1000.0 ); } }