use oa_core::{ references, transitions::*, validation::{self, Severity}, *, }; fn add_occurrence(p: &mut Project, owner: Id, name: &str, body: OccurrenceBody) -> Id { let mut e = p .make_element( Kind::Occurrence, p.elements[&owner].layer(), name, Some(owner), ) .unwrap(); *e.shared_mut().unwrap() = SharedBody::Occurrence { body, precondition: String::new(), postcondition: String::new(), }; let id = e.id; p.elements.insert(id, e); p.elements .get_mut(&owner) .unwrap() .steps_mut() .unwrap() .push(id); id } fn line(p: &mut Project, scenario: Id, represented: Id, name: &str) -> Id { let id = p .add_element( Kind::Lifeline, p.elements[&scenario].layer(), name, Some(scenario), ) .unwrap(); *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Lifeline { represented: Some(represented), }; id } fn message( exchange: Option, sender: Id, receiver: Option, kind: MessageKind, call: Option, ) -> OccurrenceBody { OccurrenceBody::Exchange { operation: None, exchange, sender: Some(sender), receiver, message_kind: kind, reply_to: call, values: vec![], } } struct Fixture { p: Project, scenario: Id, a: Id, b: Id, call: Id, reply: Id, exchange: Id, field: Id, } fn fixture(layer: Layer) -> Fixture { let mut p = Project::empty(); let work_kind = if layer == Layer::Oa { Kind::Activity } else { Kind::Function }; let client = p .add_element(work_kind, layer, "Request service", None) .unwrap(); let server = p .add_element(work_kind, layer, "Provide service", None) .unwrap(); let scenario = p .add_element(Kind::Scenario, layer, "Service conversation", None) .unwrap(); let a = line(&mut p, scenario, client, "Client"); let b = line(&mut p, scenario, server, "Provider"); let item = p .add_element(Kind::ExchangeItem, layer, "Service request", None) .unwrap(); let record = p .add_element(Kind::DataType, layer, "Request record", None) .unwrap(); *p.elements.get_mut(&record).unwrap().shared_mut().unwrap() = SharedBody::DataType { definition: DataTypeDefinition::Record { supertype: None }, default: None, unit: String::new(), }; let field = p .add_element(Kind::DataField, layer, "Requested level", Some(record)) .unwrap(); let payload = p .add_element(Kind::DataField, layer, "Payload", Some(item)) .unwrap(); if let Some(SharedBody::DataField { type_ref, .. }) = p.elements.get_mut(&payload).unwrap().shared_mut() { *type_ref = TypeRef::Named(record); } let exchange = p.relate( if layer == Layer::Oa { RelationKind::Interaction } else { RelationKind::FunctionalExchange }, "Service invocation", client, server, ); match &mut p.relations.get_mut(&exchange).unwrap().body { RelationBody::Interaction { items, .. } | RelationBody::FunctionalExchange { items, .. } => items.push(item), _ => unreachable!(), } if let RelationBody::FunctionalExchange { source_port, target_port, .. } = p.relations[&exchange].body { for port in [source_port, target_port] { match p.elements.get_mut(&port).unwrap().shared_mut() { Some(SharedBody::Port { contract: PortContract::FunctionInput { incoming_items }, .. }) => incoming_items.push(item), Some(SharedBody::Port { contract: PortContract::FunctionOutput { outgoing_items }, .. }) => outgoing_items.push(item), _ => unreachable!(), } } } let call = add_occurrence( &mut p, scenario, "Request service", message(Some(exchange), a, Some(b), MessageKind::Call, None), ); let reply = add_occurrence( &mut p, scenario, "Return result", message(Some(exchange), b, Some(a), MessageKind::Reply, Some(call)), ); if let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { values, .. }, .. }) = p.elements.get_mut(&call).unwrap().shared_mut() { values.push(MessageValue { path: vec![payload], field, expression: "requested_level".into(), }); } Fixture { p, scenario, a, b, call, reply, exchange, field, } } 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 edit_message(p: &Project, id: Id, edit: impl FnOnce(&mut OccurrenceBody)) -> Element { let mut element = p.elements[&id].clone(); if let Some(SharedBody::Occurrence { body, .. }) = element.shared_mut() { edit(body); } element } #[test] fn calls_and_replies_have_real_direction_correlation_and_order() { for layer in [Layer::Oa, Layer::Sa] { let Fixture { p, scenario, a, b, call, reply, exchange, .. } = fixture(layer); valid(&p); let mut session = Session::new(p.clone()); assert!( session .execute(Command::MoveStep(scenario, 1, 0)) .unwrap_err() .0 .contains("reply_order") ); assert_eq!(session.project(), &p); for edit in [ edit_message(&p, reply, |body| { if let OccurrenceBody::Exchange { sender, receiver, .. } = body { *sender = Some(a); *receiver = Some(b); } }), edit_message(&p, reply, |body| { if let OccurrenceBody::Exchange { reply_to, .. } = body { *reply_to = Some(reply); } }), ] { assert!(session.execute(Command::EditElement(reply, edit)).is_err()); assert_eq!(session.project(), &p); } let mut duplicate = p.clone(); add_occurrence( &mut duplicate, scenario, "Second return", message(Some(exchange), b, Some(a), MessageKind::Reply, Some(call)), ); assert!( validate_candidate(&duplicate) .unwrap_err() .0 .contains("duplicate_reply") ); let mut reverse = p.clone(); let original = &p.relations[&exchange]; let reverse_exchange = reverse.relate( original.kind(), "Explicit return contract", original.target(), original.source(), ); let edited = edit_message(&reverse, reply, |body| { if let OccurrenceBody::Exchange { exchange, .. } = body { *exchange = Some(reverse_exchange); } }); reverse.elements.insert(reply, edited); valid(&reverse); assert_eq!(reverse.relations[&exchange], p.relations[&exchange]); } } #[test] fn reused_scenarios_bind_existing_roles_and_transition_the_reference_closure() { for direction in [TransitionDirection::OaToSa, TransitionDirection::SaToOa] { let Fixture { mut p, scenario, a, b, .. } = fixture(direction.source_layer()); let represented = |p: &Project, id| match p.elements[&id].shared().unwrap() { SharedBody::Lifeline { represented: Some(id), } => *id, _ => unreachable!(), }; let client = represented(&p, a); let provider = represented(&p, b); let mut callers = vec![]; for name in ["Nominal service", "Degraded service"] { let caller = p .add_element(Kind::Scenario, direction.source_layer(), name, None) .unwrap(); let local_a = line(&mut p, caller, client, "Caller client"); let local_b = line(&mut p, caller, provider, "Caller provider"); let use_id = add_occurrence( &mut p, caller, "Reuse service conversation", OccurrenceBody::ScenarioUse { scenario: Some(scenario), bindings: vec![ LifelineBinding { referenced: a, local: local_a, }, LifelineBinding { referenced: b, local: local_b, }, ], }, ); callers.push((caller, use_id)); } valid(&p); let request = { let mut r = TransitionRequest::new(direction, callers.iter().map(|(id, _)| *id).collect()); r.scope = DependencyScope::Required; r }; let plan = plan_transition(&p, &request).unwrap(); assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); let mapped = |id| plan.changes.iter().find(|c| c.source == id).unwrap().target; let target = apply_transition(&p, &plan).unwrap(); valid(&target); for (caller, use_id) in &callers { assert!( references::element_references(&target.elements[&mapped(*use_id)]) .contains(&mapped(scenario)) ); assert_eq!( target.elements[&mapped(*caller)].layer(), direction.target_layer() ); assert!( matches!(target.elements[&mapped(*use_id)].shared(),Some(SharedBody::Occurrence{body:OccurrenceBody::ScenarioUse{scenario:Some(id),..},..})if *id==mapped(scenario)) ); } let mut session = Session::new(p.clone()); session .execute(Command::Rename( scenario, "Reviewed shared conversation".into(), )) .unwrap(); for (_, use_id) in callers { assert_eq!(session.project().elements[&use_id], p.elements[&use_id]); } session.undo(); assert_eq!(session.project(), &p); let reverse = { let mut r = TransitionRequest::new( if direction == TransitionDirection::OaToSa { TransitionDirection::SaToOa } else { TransitionDirection::OaToSa }, vec![mapped(scenario)], ); r.scope = DependencyScope::Required; r }; let repeat = plan_transition(&target, &reverse).unwrap(); assert!(repeat.conflicts.is_empty()); assert!(repeat.commands.is_empty()); } } #[test] fn reuse_rejects_wrong_bindings_recursive_calls_and_wrong_viewpoints() { let Fixture { mut p, scenario, a, b, .. } = fixture(Layer::Sa); let caller = p .add_element(Kind::Scenario, Layer::Sa, "Caller", None) .unwrap(); let wrong_work = p .add_element(Kind::Function, Layer::Sa, "Different function", None) .unwrap(); let wrong = line(&mut p, caller, wrong_work, "Wrong role"); let use_id = add_occurrence( &mut p, caller, "Bad role binding", OccurrenceBody::ScenarioUse { scenario: Some(scenario), bindings: vec![LifelineBinding { referenced: a, local: wrong, }], }, ); assert!( validate_candidate(&p) .unwrap_err() .0 .contains("scenario_binding_identity") ); let correct = match p.elements[&a].shared().unwrap() { SharedBody::Lifeline { represented: Some(id), } => *id, _ => unreachable!(), }; *p.elements.get_mut(&wrong).unwrap().shared_mut().unwrap() = SharedBody::Lifeline { represented: Some(correct), }; valid(&p); assert!( validation::validate(&p) .iter() .any(|i| i.element == Some(use_id) && i.code == "missing_lifeline_binding") ); let mut cycle = p.clone(); add_occurrence( &mut cycle, scenario, "Recursive caller", OccurrenceBody::ScenarioUse { scenario: Some(caller), bindings: vec![], }, ); assert!( validation::validate(&cycle) .iter() .any(|i| i.code == "recursive_scenario_use" && i.severity == Severity::Error) ); let mut wrong_context = p.clone(); let edited = edit_message(&wrong_context, use_id, |body| { if let OccurrenceBody::ScenarioUse { bindings, .. } = body { bindings[0].local = b; } }); wrong_context.elements.insert(use_id, edited); assert!( validation::validate(&wrong_context) .iter() .any(|i| i.code == "scenario_binding_context") ); if let Some(SharedBody::Scenario { viewpoint, .. }) = p.elements.get_mut(&caller).unwrap().shared_mut() { *viewpoint = ScenarioViewpoint::BoundaryInterface; } assert!( validation::validate(&p) .iter() .any(|i| i.code == "scenario_use_viewpoint") ); } #[test] fn create_delete_and_local_timer_messages_enforce_lifetime_rules() { let Fixture { mut p, scenario, a, b, exchange, .. } = fixture(Layer::Sa); let create = add_occurrence( &mut p, scenario, "Create provider", message(Some(exchange), a, Some(b), MessageKind::Create, None), ); let steps = p.elements.get_mut(&scenario).unwrap().steps_mut().unwrap(); steps.pop(); steps.insert(0, create); let timer = add_occurrence( &mut p, scenario, "Wait for completion", message( None, b, None, MessageKind::Timer { expression: "after 5 s".into(), }, None, ), ); let delete = add_occurrence( &mut p, scenario, "Delete provider", message(Some(exchange), a, Some(b), MessageKind::Delete, None), ); valid(&p); let mut session = Session::new(p.clone()); for command in [ Command::MoveStep(scenario, 0, 2), Command::MoveStep(scenario, 4, 1), ] { assert!( session .execute(command) .unwrap_err() .0 .contains("lifeline_lifetime") ); assert_eq!(session.project(), &p); } let bad = edit_message(&p, timer, |body| { if let OccurrenceBody::Exchange { receiver, .. } = body { *receiver = Some(a); } }); assert!( session .execute(Command::EditElement(timer, bad)) .unwrap_err() .0 .contains("timer_contract") ); let incomplete = edit_message(&p, timer, |body| { if let OccurrenceBody::Exchange { message_kind, .. } = body { *message_kind = MessageKind::Timer { expression: String::new(), }; } }); session .execute(Command::EditElement(timer, incomplete)) .unwrap(); assert!( validation::validate(session.project()) .iter() .any(|i| i.element == Some(timer) && i.code == "missing_timer_expression") ); assert_eq!(session.project().elements[&delete], p.elements[&delete]); } #[test] fn value_annotations_are_bound_to_carried_fields_and_all_references_follow_identity_changes() { let Fixture { p, call, field, .. } = fixture(Layer::Oa); valid(&p); let mut duplicate = edit_message(&p, call, |body| { if let OccurrenceBody::Exchange { values, .. } = body { values.push(values[0].clone()); } }); let mut session = Session::new(p.clone()); assert!( session .execute(Command::EditElement(call, duplicate.clone())) .unwrap_err() .0 .contains("duplicate_message_value") ); let mut unrelated = p.clone(); let item = unrelated .add_element(Kind::ExchangeItem, Layer::Oa, "Unrelated item", None) .unwrap(); let other = unrelated .add_element(Kind::DataField, Layer::Oa, "Unrelated field", Some(item)) .unwrap(); if let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { values, .. }, .. }) = duplicate.shared_mut() { values.truncate(1); values[0].field = other; } assert!( execute_batch(&unrelated, &[Command::EditElement(call, duplicate)]) .unwrap_err() .0 .contains("message_value_field") ); for id in p.elements.keys().chain(p.relations.keys()).copied() { let mut session = Session::new(p.clone()); let replacement = Id::new_v4(); session .execute(Command::Reidentify(id, replacement)) .unwrap(); let renamed = session.project().clone(); assert!( renamed .elements .values() .all(|e| !references::element_references(e).contains(&id)) ); session.execute(Command::Delete(replacement)).unwrap(); valid(session.project()); session.undo(); assert_eq!(session.project(), &renamed); session.undo(); assert_eq!(session.project(), &p); } session.execute(Command::Delete(field)).unwrap(); assert!( matches!(session.project().elements[&call].shared(),Some(SharedBody::Occurrence{body:OccurrenceBody::Exchange{values,..},..})if values.is_empty()) ); } #[test] fn loop_fragment_repetition_is_distinct_from_recursive_scenario_use() { let Fixture { mut p, scenario, a, b, call, reply, .. } = fixture(Layer::Oa); let mut fragment = p .make_element(Kind::Occurrence, Layer::Oa, "Retry service", Some(scenario)) .unwrap(); let fragment_id = fragment.id; *fragment.shared_mut().unwrap() = SharedBody::Occurrence { body: OccurrenceBody::Fragment { operator: FragmentOperator::Loop, branches: vec![], }, precondition: String::new(), postcondition: String::new(), }; let mut branch = p .make_element( Kind::Occurrence, Layer::Oa, "While needed", Some(fragment_id), ) .unwrap(); let branch_id = branch.id; *branch.shared_mut().unwrap() = SharedBody::Occurrence { body: OccurrenceBody::Branch { guard: "retry && count < 3".into(), occurrences: vec![call, reply], }, precondition: String::new(), postcondition: String::new(), }; if let Some(SharedBody::Occurrence { body: OccurrenceBody::Fragment { branches, .. }, .. }) = fragment.shared_mut() { branches.push(branch_id); } p.elements.get_mut(&call).unwrap().parent = Some(branch_id); p.elements.get_mut(&reply).unwrap().parent = Some(branch_id); *p.elements.get_mut(&scenario).unwrap().steps_mut().unwrap() = vec![fragment_id]; p.elements.insert(fragment_id, fragment); p.elements.insert(branch_id, branch); valid(&p); let exchange = p_exchange(&p, call); let outside = add_occurrence( &mut p, scenario, "Reply outside optional loop", message(Some(exchange), b, Some(a), MessageKind::Reply, Some(call)), ); assert!( validation::validate(&p) .iter() .any(|i| i.element == Some(outside) && i.code == "reply_order") ); } fn p_exchange(p: &Project, id: Id) -> Id { match p.elements[&id].shared().unwrap() { SharedBody::Occurrence { body: OccurrenceBody::Exchange { exchange: Some(id), .. }, .. } => *id, _ => unreachable!(), } } #[test] fn deleting_reused_scenarios_or_bound_lifelines_preserves_callers_and_undo() { let Fixture { mut p, scenario, a, b, .. } = fixture(Layer::Sa); let caller = p .add_element(Kind::Scenario, Layer::Sa, "Caller", None) .unwrap(); let client = match p.elements[&a].shared().unwrap() { SharedBody::Lifeline { represented: Some(id), } => *id, _ => unreachable!(), }; let provider = match p.elements[&b].shared().unwrap() { SharedBody::Lifeline { represented: Some(id), } => *id, _ => unreachable!(), }; let local_a = line(&mut p, caller, client, "Local client"); let local_b = line(&mut p, caller, provider, "Local provider"); let use_id = add_occurrence( &mut p, caller, "Shared conversation", OccurrenceBody::ScenarioUse { scenario: Some(scenario), bindings: vec![ LifelineBinding { referenced: a, local: local_a, }, LifelineBinding { referenced: b, local: local_b, }, ], }, ); valid(&p); for id in [scenario, a, b, local_a, local_b, use_id] { let mut session = Session::new(p.clone()); let replacement = Id::new_v4(); session .execute(Command::Reidentify(id, replacement)) .unwrap(); assert!( session .project() .elements .values() .all(|e| !references::element_references(e).contains(&id)) ); let renamed = session.project().clone(); session.execute(Command::Delete(replacement)).unwrap(); valid(session.project()); assert!(session.project().elements.contains_key(&caller)); if id == scenario { assert!( matches!(session.project().elements[&use_id].shared(),Some(SharedBody::Occurrence{body:OccurrenceBody::ScenarioUse{scenario:None,bindings},..})if bindings.is_empty()) ); assert!(session.project().elements.contains_key(&local_a)); } session.undo(); assert_eq!(session.project(), &renamed); session.undo(); assert_eq!(session.project(), &p); } } fn scenario_branch(p: &mut Project, owner: Id, operator: FragmentOperator) -> (Id, Id) { let fragment = add_occurrence( p, owner, "Conditional fragment", OccurrenceBody::Fragment { operator, branches: vec![], }, ); let branch = additional_branch(p, fragment); (fragment, branch) } fn additional_branch(p: &mut Project, fragment: Id) -> Id { let mut branch = p .make_element( Kind::Occurrence, p.elements[&fragment].layer(), "Operand", Some(fragment), ) .unwrap(); let id = branch.id; *branch.shared_mut().unwrap() = SharedBody::Occurrence { body: OccurrenceBody::Branch { guard: "condition".into(), occurrences: vec![], }, precondition: String::new(), postcondition: String::new(), }; p.elements.insert(id, branch); if let Some(SharedBody::Occurrence { body: OccurrenceBody::Fragment { branches, .. }, .. }) = p.elements.get_mut(&fragment).unwrap().shared_mut() { branches.push(id); } id } #[test] fn conditional_and_parallel_replies_cannot_answer_one_call_twice_but_alternatives_can() { for operator in [FragmentOperator::Optional, FragmentOperator::Alternative] { let Fixture { mut p, scenario, a, b, call, reply, exchange, .. } = fixture(Layer::Sa); p.elements.remove(&reply); p.elements .get_mut(&scenario) .unwrap() .steps_mut() .unwrap() .retain(|id| *id != reply); let (_, operand) = scenario_branch(&mut p, scenario, operator); add_occurrence( &mut p, operand, "Conditional return", message(Some(exchange), b, Some(a), MessageKind::Reply, Some(call)), ); valid(&p); let before = p.clone(); add_occurrence( &mut p, scenario, "Later return", message(Some(exchange), b, Some(a), MessageKind::Reply, Some(call)), ); let mut session = Session::new(before.clone()); assert!( session .execute(Command::Replace(p)) .unwrap_err() .0 .contains("duplicate_reply") ); assert_eq!(session.project(), &before); } for operator in [FragmentOperator::Alternative, FragmentOperator::Parallel] { let Fixture { mut p, scenario, a, b, call, reply, exchange, .. } = fixture(Layer::Sa); p.elements.remove(&reply); p.elements .get_mut(&scenario) .unwrap() .steps_mut() .unwrap() .retain(|id| *id != reply); let (fragment, left) = scenario_branch(&mut p, scenario, operator); let right = additional_branch(&mut p, fragment); for operand in [left, right] { add_occurrence( &mut p, operand, "Branch return", message(Some(exchange), b, Some(a), MessageKind::Reply, Some(call)), ); } if operator == FragmentOperator::Alternative { valid(&p); } else { assert!( validate_candidate(&p) .unwrap_err() .0 .contains("duplicate_reply") ); } } } #[test] fn conditional_lifetime_changes_conflict_with_later_or_parallel_use_not_exclusive_use() { for operator in [FragmentOperator::Optional, FragmentOperator::Alternative] { let Fixture { mut p, scenario, a, b, exchange, .. } = fixture(Layer::Sa); let (_, operand) = scenario_branch(&mut p, scenario, operator); add_occurrence( &mut p, operand, "Conditional deletion", message(Some(exchange), a, Some(b), MessageKind::Delete, None), ); valid(&p); add_occurrence( &mut p, scenario, "Use after fragment", message(Some(exchange), a, Some(b), MessageKind::Asynchronous, None), ); assert!( validate_candidate(&p) .unwrap_err() .0 .contains("lifeline_lifetime") ); } for operator in [FragmentOperator::Alternative, FragmentOperator::Parallel] { let Fixture { mut p, scenario, a, b, exchange, .. } = fixture(Layer::Sa); let (fragment, left) = scenario_branch(&mut p, scenario, operator); let right = additional_branch(&mut p, fragment); add_occurrence( &mut p, left, "Delete in one operand", message(Some(exchange), a, Some(b), MessageKind::Delete, None), ); add_occurrence( &mut p, right, "Use in another operand", message(Some(exchange), a, Some(b), MessageKind::Asynchronous, None), ); if operator == FragmentOperator::Alternative { valid(&p); } else { assert!( validate_candidate(&p) .unwrap_err() .0 .contains("lifeline_lifetime") ); } } } #[test] fn role_lifelines_require_explicit_work_assignment_not_just_a_shared_realizing_entity() { let Fixture { mut p, scenario, a, b, exchange, .. } = fixture(Layer::Oa); let work = p.relations[&exchange].source(); let target = p.relations[&exchange].target(); let entity = p .add_element(Kind::Entity, Layer::Oa, "Controller", None) .unwrap(); let other = p .add_element(Kind::Entity, Layer::Oa, "Recipient", None) .unwrap(); let role = p .add_element(Kind::Role, Layer::Oa, "Coordinator", None) .unwrap(); let wrong_role = p .add_element(Kind::Role, Layer::Oa, "Unassigned role", None) .unwrap(); let allocation = p.relate( RelationKind::Allocation, "Controller performs", entity, work, ); p.relate( RelationKind::Allocation, "Recipient performs", other, target, ); p.relate( RelationKind::RoleRealization, "Controller realizes coordinator", entity, role, ); p.relate( RelationKind::RoleRealization, "Controller also realizes unassigned role", entity, wrong_role, ); p.relate( RelationKind::RoleAssignment, "Coordinator assignment", role, work, ); if let Some(SharedBody::Scenario { viewpoint, .. }) = p.elements.get_mut(&scenario).unwrap().shared_mut() { *viewpoint = ScenarioViewpoint::ParticipantInteraction; } *p.elements.get_mut(&a).unwrap().shared_mut().unwrap() = SharedBody::Lifeline { represented: Some(role), }; *p.elements.get_mut(&b).unwrap().shared_mut().unwrap() = SharedBody::Lifeline { represented: Some(other), }; valid(&p); assert_eq!( validation::lifeline_matches_endpoint(&p, a, work), Some(true) ); assert_eq!( validation::semantic_representation_compatible(&p, wrong_role, work), Some(false) ); assert_eq!( validation::semantic_representation_compatible(&p, work, work), Some(true) ); assert_eq!( validation::semantic_representation_compatible(&p, target, work), Some(false) ); let port = Id::new_v4(); p.elements.insert( port, Element { id: port, alias: "legacy_controller_port".into(), name: "Controller port".into(), description: String::new(), parent: Some(entity), body: ElementBody::shared_in( Layer::Oa, SharedBody::Port { contract: PortContract::Legacy, legacy: Some(LegacyPortContract { direction: PortDirection::InOut, interface: None, }), }, ), }, ); assert_eq!( validation::semantic_representation_compatible(&p, entity, port), Some(true) ); assert_eq!( validation::semantic_representation_compatible(&p, role, port), Some(true) ); assert_eq!( validation::semantic_representation_compatible(&p, other, port), Some(false) ); let mut wrong = p.clone(); *wrong.elements.get_mut(&a).unwrap().shared_mut().unwrap() = SharedBody::Lifeline { represented: Some(wrong_role), }; assert!( validation::validate(&wrong) .iter() .any(|i| i.code == "scenario_endpoint" && i.severity == Severity::Error) ); p.relations.remove(&allocation); let additional = p .add_element(Kind::Entity, Layer::Oa, "Alternative coordinator", None) .unwrap(); p.relate( RelationKind::RoleRealization, "Another coordinator realization", additional, role, ); assert_eq!(validation::effective_performer(&p, work), None); assert_eq!( validation::lifeline_matches_endpoint(&p, a, work), Some(true) ); assert_eq!( validation::semantic_representation_compatible(&p, entity, work), None ); valid(&p); } #[test] fn effective_message_endpoints_are_shared_for_forward_and_reverse_reply_choices() { let Fixture { mut p, a, b, call, reply, exchange, .. } = fixture(Layer::Sa); let body = |p: &Project, id| match p.elements[&id].shared().unwrap() { SharedBody::Occurrence { body, .. } => body.clone(), _ => unreachable!(), }; let (source, target) = p.relations[&exchange].body.endpoints(); assert_eq!( validation::message_endpoints(&p, &body(&p, call)), Some((source, target)) ); let reversed = validation::message_endpoints(&p, &body(&p, reply)).unwrap(); assert_eq!(reversed, (target, source)); assert_eq!( validation::lifeline_matches_endpoint(&p, b, reversed.0), Some(true) ); assert_eq!( validation::lifeline_matches_endpoint(&p, a, reversed.0), Some(false) ); let reverse = p.relate( RelationKind::FunctionalExchange, "Explicit reply exchange", target, source, ); let explicit = message(Some(reverse), b, Some(a), MessageKind::Reply, Some(call)); assert_eq!( validation::message_endpoints(&p, &explicit), Some((target, source)) ); assert_eq!( validation::message_endpoints( &p, &message(Some(exchange), b, Some(a), MessageKind::Reply, None) ), None ); assert_eq!( validation::message_endpoints( &p, &message( None, b, None, MessageKind::Timer { expression: "5 s".into() }, None ) ), None ); } fn move_into_operand(p: &mut Project, id: Id, operand: Id) { let old = p.elements[&id].parent.unwrap(); p.elements .get_mut(&old) .unwrap() .steps_mut() .unwrap() .retain(|step| *step != id); p.elements.get_mut(&id).unwrap().parent = Some(operand); p.elements .get_mut(&operand) .unwrap() .steps_mut() .unwrap() .push(id); } #[test] fn replies_repeated_without_their_call_warn_in_each_extra_loop_scope() { let Fixture { mut p, scenario, call, reply, .. } = fixture(Layer::Sa); let (_, outer) = scenario_branch(&mut p, scenario, FragmentOperator::Loop); move_into_operand(&mut p, reply, outer); valid(&p); assert!( validation::validate(&p) .iter() .any(|i| i.element == Some(reply) && i.code == "reply_repetition" && i.severity == Severity::Warning) ); // Moving both into the same loop gives each iteration its own call and reply. move_into_operand(&mut p, call, outer); p.elements .get_mut(&outer) .unwrap() .steps_mut() .unwrap() .swap(0, 1); valid(&p); assert!( !validation::validate(&p) .iter() .any(|i| i.code == "reply_repetition") ); // An inner loop can still repeat the reply more often than the outer call. let (_, inner) = scenario_branch(&mut p, outer, FragmentOperator::Loop); move_into_operand(&mut p, reply, inner); valid(&p); assert_eq!( validation::validate(&p) .iter() .filter(|i| i.code == "reply_repetition") .count(), 1 ); move_into_operand(&mut p, call, inner); p.elements .get_mut(&inner) .unwrap() .steps_mut() .unwrap() .swap(0, 1); valid(&p); assert!( !validation::validate(&p) .iter() .any(|i| i.code == "reply_repetition") ); } #[test] fn repeated_lifecycle_requires_per_iteration_balance_without_prohibiting_balanced_loops() { for starts_with_creation in [false, true] { let Fixture { mut p, scenario, a, b, call, reply, exchange, .. } = fixture(Layer::Sa); for id in [call, reply] { p.elements.remove(&id); } p.elements .get_mut(&scenario) .unwrap() .steps_mut() .unwrap() .clear(); let (_, operand) = scenario_branch(&mut p, scenario, FragmentOperator::Loop); let lifecycle = if starts_with_creation { add_occurrence( &mut p, operand, "Create each iteration", message(Some(exchange), a, Some(b), MessageKind::Create, None), ) } else { Id::nil() }; add_occurrence( &mut p, operand, "Use provider", message(Some(exchange), a, Some(b), MessageKind::Asynchronous, None), ); let lifecycle = if starts_with_creation { lifecycle } else { add_occurrence( &mut p, operand, "Delete each iteration", message(Some(exchange), a, Some(b), MessageKind::Delete, None), ) }; valid(&p); assert!( validation::validate(&p) .iter() .any(|i| i.element == Some(lifecycle) && i.code == "lifeline_repetition" && i.severity == Severity::Warning) ); if starts_with_creation { add_occurrence( &mut p, operand, "Complete lifetime", message(Some(exchange), a, Some(b), MessageKind::Delete, None), ); } else { add_occurrence( &mut p, operand, "Start lifetime", message(Some(exchange), a, Some(b), MessageKind::Create, None), ); p.elements .get_mut(&operand) .unwrap() .steps_mut() .unwrap() .rotate_right(1); } valid(&p); assert!( !validation::validate(&p) .iter() .any(|i| i.code == "lifeline_repetition") ); } } #[test] fn optional_cleanup_does_not_claim_a_repeated_lifetime_is_balanced() { let Fixture { mut p, scenario, a, b, call, reply, exchange, .. } = fixture(Layer::Sa); for id in [call, reply] { p.elements.remove(&id); } p.elements .get_mut(&scenario) .unwrap() .steps_mut() .unwrap() .clear(); let (_, iteration) = scenario_branch(&mut p, scenario, FragmentOperator::Loop); let create = add_occurrence( &mut p, iteration, "Create provider", message(Some(exchange), a, Some(b), MessageKind::Create, None), ); add_occurrence( &mut p, iteration, "Use provider", message(Some(exchange), a, Some(b), MessageKind::Asynchronous, None), ); let (_, cleanup) = scenario_branch(&mut p, iteration, FragmentOperator::Optional); add_occurrence( &mut p, cleanup, "Optional deletion", message(Some(exchange), a, Some(b), MessageKind::Delete, None), ); valid(&p); assert!( validation::validate(&p) .iter() .any(|i| i.element == Some(create) && i.code == "lifeline_repetition") ); }