use oa_core::{ transitions::*, validation::{self, MessageEndpointResolution as Resolution, Severity}, *, }; use std::collections::BTreeMap; fn add(p: &mut Project, kind: Kind, layer: Layer, name: &str) -> Id { p.add_element(kind, layer, name, None).unwrap() } fn fixture() -> (Project, Id, Id, Id) { let mut p = Project::empty(); let a = add(&mut p, Kind::Entity, Layer::Oa, "Rescue team"); let b = add(&mut p, Kind::Actor, Layer::Oa, "Control centre"); let mean = p.relate(RelationKind::CommunicationMean, "Radio", a, b); (p, a, b, mean) } fn errors(p: &Project) -> Vec { validation::validate(p) .into_iter() .filter(|i| i.severity == Severity::Error) .collect() } fn valid(p: &Project) { assert!(errors(p).is_empty(), "{:?}", errors(p)); } fn oriented(p: &mut Project, id: Id, value: bool) { if let RelationBody::CommunicationMean { oriented, .. } = &mut p.relations.get_mut(&id).unwrap().body { *oriented = value; } } fn items(p: &mut Project, id: Id, values: Vec) { match &mut p.relations.get_mut(&id).unwrap().body { RelationBody::CommunicationMean { items, .. } | RelationBody::Interaction { items, .. } => { *items = values } _ => panic!(), } } fn has(p: &Project, id: Id, code: &str, severity: Severity) -> bool { validation::validate(p) .iter() .any(|i| i.element == Some(id) && i.code == code && i.severity == severity) } fn line(p: &mut Project, scenario: Id, represented: Id) -> Id { if let Some(SharedBody::Scenario { viewpoint, .. }) = p.elements.get_mut(&scenario).unwrap().shared_mut() { *viewpoint = ScenarioViewpoint::ParticipantInteraction; } let id = p .add_element( Kind::Lifeline, Layer::Oa, &format!("line_{represented}"), Some(scenario), ) .unwrap(); *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Lifeline { represented: Some(represented), }; id } fn message( mean: Id, sender: Id, receiver: Id, kind: MessageKind, reply_to: Option, ) -> OccurrenceBody { OccurrenceBody::Exchange { operation: None, exchange: Some(mean), sender: Some(sender), receiver: Some(receiver), message_kind: kind, reply_to, values: vec![], } } fn occurrence(p: &mut Project, scenario: Id, body: OccurrenceBody) -> Id { let id = p .add_element(Kind::Occurrence, Layer::Oa, "Message", Some(scenario)) .unwrap(); *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Occurrence { body, precondition: String::new(), postcondition: String::new(), }; let steps = p.elements.get_mut(&scenario).unwrap().steps_mut().unwrap(); if !steps.contains(&id) { steps.push(id); } id } #[test] fn communication_means_are_distinct_oa_participant_relations_and_strict_source6_records() { let (mut p, a, b, mean) = fixture(); valid(&p); assert!(matches!( p.relations[&mean].body, RelationBody::CommunicationMean { oriented: false, .. } )); p.relate(RelationKind::CommunicationMean, "Telephone", a, b); valid(&p); let text = language::format(&p); assert!(text.contains("communication_mean")); assert!(text.starts_with(&format!("arcadia {SCHEMA_VERSION};"))); assert_eq!(language::parse(&text).unwrap(), p); let bytes = serde_json::to_vec(&p).unwrap(); assert_eq!(persistence::decode(&bytes).unwrap(), p); for version in 1..=5 { let mut old = serde_json::to_value(&p).unwrap(); old["schema_version"] = version.into(); assert!( persistence::decode(&serde_json::to_vec(&old).unwrap()).is_err(), "schema {version}" ); assert!( language::parse(&text.replacen( &format!("arcadia {SCHEMA_VERSION};"), &format!("arcadia {version};"), 1 )) .is_err(), "source {version}" ); } let mut bad = serde_json::to_value(&p).unwrap(); bad["relations"][mean.to_string()]["body"]["CommunicationMean"]["typo"] = true.into(); assert!(persistence::decode(&serde_json::to_vec(&bad).unwrap()).is_err()); for endpoint in [ a, add(&mut p, Kind::Role, Layer::Oa, "Role"), add(&mut p, Kind::System, Layer::Sa, "System"), ] { let mut q = p.clone(); q.relations .get_mut(&mean) .unwrap() .body .set_endpoints(a, endpoint); assert!(!errors(&q).is_empty()); } } #[test] fn communication_schema5_upgrade_preserves_legacy_data_and_exact_source_trivia() { let p = Project::demo(); let mut json = serde_json::to_value(&p).unwrap(); json["schema_version"] = 5.into(); for d in json["diagrams"].as_object_mut().unwrap().values_mut() { d.as_object_mut().unwrap().remove("view_options"); } let result = persistence::decode_with_report(&serde_json::to_vec(&json).unwrap()).unwrap(); assert_eq!(result.project, p); assert_eq!(result.migration.unwrap().from_schema, 5); let mut without_views = p.clone(); without_views.diagrams.clear(); let old = language::format(&without_views).replacen( &format!("arcadia {SCHEMA_VERSION};"), "arcadia // old contract\n5;", 1, ); let bundle = source_project::SourceBundle { entry: "entry.arcadia".into(), files: BTreeMap::from([("entry.arcadia".into(), old.clone())]), }; let compiled = source_project::compile_bundle(&bundle, source_project::ProjectLimits::default()).unwrap(); assert_eq!( compiled.bundle.files["entry.arcadia"], old.replacen("\n5;", &format!("\n{SCHEMA_VERSION};"), 1) ); assert_eq!(compiled.decoded.project, without_views); } #[test] fn communication_allocation_checks_whole_partial_pair_and_does_not_guess_roles() { let (mut p, a, b, mean) = fixture(); let x = add(&mut p, Kind::Activity, Layer::Oa, "Send"); let y = add(&mut p, Kind::Activity, Layer::Oa, "Receive"); let work = p.relate(RelationKind::Interaction, "Report", x, y); let allocation = p.relate( RelationKind::ExchangeAllocation, "Radio carries report", mean, work, ); let role = add(&mut p, Kind::Role, Layer::Oa, "Communicator"); p.relate(RelationKind::RoleRealization, "Role", a, role); p.relate(RelationKind::RoleAssignment, "Responsibility", role, x); valid(&p); assert!(has( &p, allocation, "incomplete_exchange_allocation", Severity::Warning )); p.relate(RelationKind::Allocation, "Actual receiver", a, y); valid(&p); // reverse candidate survives oriented(&mut p, mean, true); assert!(has( &p, allocation, "exchange_allocation_performer", Severity::Error )); oriented(&mut p, mean, false); p.relate(RelationKind::Allocation, "Actual sender", b, x); valid(&p); let mut q = p.clone(); q.relations.get_mut(&mean).unwrap().body.set_endpoints(b, a); valid(&q); let third = add(&mut q, Kind::Entity, Layer::Oa, "Third"); q.relations .get_mut(&mean) .unwrap() .body .set_endpoints(a, third); assert!(has( &q, allocation, "exchange_allocation_performer", Severity::Error )); } #[test] fn communication_allocation_items_use_specialization_and_empty_is_not_wildcard() { let (mut p, a, b, mean) = fixture(); let x = add(&mut p, Kind::Activity, Layer::Oa, "Send"); let y = add(&mut p, Kind::Activity, Layer::Oa, "Receive"); p.relate(RelationKind::Allocation, "Send allocation", a, x); p.relate(RelationKind::Allocation, "Receive allocation", b, y); let work = p.relate(RelationKind::Interaction, "Report", x, y); let allocation = p.relate(RelationKind::ExchangeAllocation, "Carried", mean, work); let base = add(&mut p, Kind::ExchangeItem, Layer::Oa, "Report item"); let specialized = add(&mut p, Kind::ExchangeItem, Layer::Oa, "Distress item"); if let Some(SharedBody::ExchangeItem { supertype, .. }) = p.elements.get_mut(&specialized).unwrap().shared_mut() { *supertype = Some(base); } items(&mut p, work, vec![specialized]); assert!(has( &p, allocation, "exchange_allocation_items", Severity::Error )); items(&mut p, mean, vec![base]); valid(&p); items(&mut p, work, vec![base]); items(&mut p, mean, vec![specialized]); assert!(has( &p, allocation, "exchange_allocation_items", Severity::Error )); } #[test] fn communication_scenario_reverse_call_and_reply_follow_actual_direction() { let (mut p, a, b, mean) = fixture(); let scenario = add(&mut p, Kind::Scenario, Layer::Oa, "Radio conversation"); let la = line(&mut p, scenario, a); let lb = line(&mut p, scenario, b); let call_body = message(mean, lb, la, MessageKind::Call, None); let call = occurrence(&mut p, scenario, call_body.clone()); let reply_body = message(mean, la, lb, MessageKind::Reply, Some(call)); occurrence(&mut p, scenario, reply_body.clone()); assert_eq!( validation::resolve_message_endpoints(&p, &call_body), Resolution::Resolved { source: b, target: a } ); assert_eq!( validation::resolve_message_endpoints(&p, &reply_body), Resolution::Resolved { source: a, target: b } ); valid(&p); let mut edited = p.relations[&mean].clone(); if let RelationBody::CommunicationMean { oriented, .. } = &mut edited.body { *oriented = true; } assert!(prepare_command(&p, &Command::EditRelation(mean, edited)).is_err()); valid(&p); // The established operation-response exception also allows same-means Reply on oriented means. if let Some(SharedBody::Occurrence { body, .. }) = p.elements.get_mut(&call).unwrap().shared_mut() { *body = message(mean, la, lb, MessageKind::Call, None); } oriented(&mut p, mean, true); assert_eq!( validation::resolve_message_endpoints( &p, &message(mean, lb, la, MessageKind::Reply, Some(call)) ), Resolution::Resolved { source: b, target: a } ); } #[test] fn communication_scenario_ambiguous_and_incomplete_bindings_never_guess() { let (mut p, a, b, mean) = fixture(); let scenario = add(&mut p, Kind::Scenario, Layer::Oa, "Roles"); let role = add(&mut p, Kind::Role, Layer::Oa, "Shared role"); p.relate(RelationKind::RoleRealization, "A role", a, role); p.relate(RelationKind::RoleRealization, "B role", b, role); let one = line(&mut p, scenario, role); let two = line(&mut p, scenario, role); let mut body = message(mean, one, two, MessageKind::Asynchronous, None); assert_eq!( validation::resolve_message_endpoints(&p, &body), Resolution::Ambiguous ); assert_eq!(validation::message_endpoints(&p, &body), None); if let OccurrenceBody::Exchange { receiver, .. } = &mut body { *receiver = None; } assert_eq!( validation::resolve_message_endpoints(&p, &body), Resolution::Incomplete ); let third = add(&mut p, Kind::Entity, Layer::Oa, "Other"); let other = line(&mut p, scenario, third); if let OccurrenceBody::Exchange { sender, .. } = &mut body { *sender = Some(other); } assert_eq!( validation::resolve_message_endpoints(&p, &body), Resolution::Conflict ); } fn transition_fixture() -> (Project, Id, Id, Id, TransitionRequest) { let (mut p, a, b, mean) = fixture(); let sa = add(&mut p, Kind::System, Layer::Sa, "System"); let sb = add(&mut p, Kind::SystemActor, Layer::Sa, "External"); let mut r = TransitionRequest::new(TransitionDirection::OaToSa, vec![mean]); r.mappings = vec![ TransitionMapping { source: a, target: Some(sa), target_kind: None, }, TransitionMapping { source: b, target: Some(sb), target_kind: None, }, ]; let port = |name: &str| CommunicationPortChoice::Create { id: Id::new_v4(), alias: name.into(), name: name.into(), description: "Explicit technical choice".into(), kind: ComponentPortKind::Standard, orientation: ComponentPortOrientation::Unset, }; r.communication_endpoints .push(CommunicationEndpointDecision { source: mean, source_port: port("radio_source"), target_port: port("radio_target"), }); (p, mean, sa, sb, r) } #[test] fn communication_transition_requires_explicit_ports_and_preserves_participant_mapping() { let (p, mean, sa, sb, r) = transition_fixture(); let mut incomplete = r.clone(); incomplete.communication_endpoints.clear(); assert!( !plan_transition(&p, &incomplete) .unwrap() .conflicts .is_empty() ); let plan = plan_transition(&p, &r).unwrap(); assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); let q = apply_transition(&p, &plan).unwrap(); valid(&q); let trace = q.traces.values().find(|t| t.oa == mean).unwrap(); assert_eq!(trace.kind, TraceKind::Exchange); let boundary = &q.relations[&trace.sa]; assert_eq!(boundary.kind(), RelationKind::BoundaryExchange); assert_eq!(q.elements[&boundary.source()].parent, Some(sa)); assert_eq!(q.elements[&boundary.target()].parent, Some(sb)); assert_eq!(q.relations[&mean], p.relations[&mean]); assert_eq!(q.elements.len(), p.elements.len() + 2); let mut collision = r.clone(); collision.mappings[1].target = Some(sa); assert!( !plan_transition(&p, &collision) .unwrap() .conflicts .is_empty() ); let mut duplicate = r.clone(); duplicate .communication_endpoints .push(r.communication_endpoints[0].clone()); assert!( !plan_transition(&p, &duplicate) .unwrap() .conflicts .is_empty() ); } #[test] fn communication_reverse_transition_projects_owners_without_oa_ports() { let (p, mean, sa, sb, r) = transition_fixture(); let q = apply_transition(&p, &plan_transition(&p, &r).unwrap()).unwrap(); let boundary = q.traces.values().find(|t| t.oa == mean).unwrap().sa; let mut standalone = q.clone(); standalone.traces.clear(); let mut reverse = TransitionRequest::new(TransitionDirection::SaToOa, vec![boundary]); reverse.mappings = vec![ TransitionMapping { source: sa, target: Some(p.relations[&mean].source()), target_kind: None, }, TransitionMapping { source: sb, target: Some(p.relations[&mean].target()), target_kind: None, }, ]; let plan = plan_transition(&standalone, &reverse).unwrap(); assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); let result = apply_transition(&standalone, &plan).unwrap(); assert_eq!(result.elements, standalone.elements); let counterpart = &result.relations[&result .traces .values() .find(|t| t.sa == boundary) .unwrap() .oa]; assert_eq!(counterpart.kind(), RelationKind::CommunicationMean); assert_eq!( counterpart.body.endpoints(), p.relations[&mean].body.endpoints() ); } #[test] fn communication_semantic_endpoint_take_needs_scoped_decision_and_exact_history() { let (p, mean, _, _, r) = transition_fixture(); let q = apply_transition(&p, &plan_transition(&p, &r).unwrap()).unwrap(); let trace = q.traces.values().find(|t| t.oa == mean).unwrap(); let boundary = trace.sa; let mut transition = TransitionRequest::new(TransitionDirection::OaToSa, vec![mean]); transition.mappings = r.mappings.clone(); transition.mappings.push(TransitionMapping { source: mean, target: Some(boundary), target_kind: None, }); let mut request = SemanticTransitionRequest::new(transition); request.references = r .mappings .iter() .map(|m| (m.source, m.target.unwrap())) .collect(); request.reviews.push(SemanticReviewRequest { source: mean, resolutions: vec![FacetResolution { facet: FacetSelector::Relation(RelationFacet::Source), decision: SemanticDecision::TakeSource, }], reset: vec![], }); assert!( !plan_semantic_transition(&q, request.clone()) .unwrap() .conflicts .is_empty() ); request .transition .communication_endpoints .push(CommunicationEndpointDecision { source: mean, source_port: CommunicationPortChoice::Reuse { port: q.relations[&boundary].source(), }, target_port: CommunicationPortChoice::Reuse { port: q.relations[&boundary].target(), }, }); let plan = plan_semantic_transition(&q, request).unwrap(); assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); let result = apply_semantic_transition(&q, &plan).unwrap(); let baseline = result.traces[&trace.id] .baseline .as_ref() .unwrap() .semantic .as_ref() .unwrap(); assert!(baseline.facets.iter().any(|f|matches!(f,FacetReview::Relation(RelationReview::Source(Reviewed{oa:Observed::Value(a),sa:Observed::Value(b)})) if *a==p.relations[&mean].source()&&*b==q.relations[&boundary].source()))); }