use super::{tests::*, *}; use crate::{app::App, document::Document}; use serde_json::json; fn pair(oa: SharedBody, sa: SharedBody) -> (Project, Id, Id, Id) { let mut p = Project::empty(); let mut ids = vec![]; for (layer, body) in [(Layer::Oa, oa), (Layer::Sa, sa)] { let kind = match body { SharedBody::DataType { .. } => Kind::DataType, SharedBody::Port { .. } => Kind::Port, SharedBody::Occurrence { .. } => Kind::Occurrence, SharedBody::Event { .. } => Kind::Event, _ => unreachable!(), }; let owner = if kind == Kind::Occurrence { Some( p.add_element(Kind::Scenario, layer, "Scenario", None) .unwrap(), ) } else if kind == Kind::Port { Some( p.add_element( if layer == Layer::Oa { Kind::Activity } else { Kind::Function }, layer, "Owner", None, ) .unwrap(), ) } else { None }; let id = if kind == Kind::Port { let id = Id::new_v4(); let port = ports::make_port( &p, id, owner.unwrap(), "Reviewed port", PortContract::Legacy, ) .unwrap(); p.elements.insert(id, port); id } else { p.add_element(kind, layer, "Reviewed object", owner) .unwrap() }; *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = body; if kind == Kind::Occurrence { p.elements .get_mut(&owner.unwrap()) .unwrap() .steps_mut() .unwrap() .push(id); } ids.push(id); } let trace = add_trace(&mut p, ids[0], ids[1], "Constructor review"); (p, ids[0], ids[1], trace) } fn data(definition: DataTypeDefinition, default: Option) -> SharedBody { SharedBody::DataType { definition, default, unit: "retain unit".into(), } } fn occurrence(body: OccurrenceBody) -> SharedBody { SharedBody::Occurrence { body, precondition: "retain precondition".into(), postcondition: "retain postcondition".into(), } } fn event(body: EventBody) -> SharedBody { SharedBody::Event { body } } fn failure(s: &State) -> String { s.error .iter() .chain(s.plan.iter().flat_map(|p| &p.conflicts)) .cloned() .collect::>() .join("; ") } fn reviewed(s: &State) -> BTreeSet { s.plan.as_ref().unwrap().reviews[0] .acknowledged .iter() .cloned() .collect() } fn history(p: &mut Project, trace: Id) { let t = &p.traces[&trace]; let baseline = TraceBaseline { oa: p.properties(t.oa).unwrap(), sa: p.properties(t.sa).unwrap(), semantic: Some(SemanticBaseline { facets: semantic_facets(p, t, &BTreeMap::new()) .unwrap() .into_iter() .map(|f| f.current) .collect(), }), }; p.traces.get_mut(&trace).unwrap().baseline = Some(baseline); } #[test] fn data_group_candidates_cover_every_definition_and_companion_default_both_directions() { let definitions = [ (DataTypeDefinition::Boolean, Some(DataValue::Boolean(true))), ( DataTypeDefinition::Integer { minimum: Some(-3), maximum: Some(15), }, Some(DataValue::Integer(7)), ), ( DataTypeDefinition::Real { minimum: Some(-0.5), maximum: Some(12.5), }, Some(DataValue::Real(2.25)), ), ( DataTypeDefinition::String { minimum_length: 1, maximum_length: Some(12), }, Some(DataValue::String("raw text".into())), ), ( DataTypeDefinition::Enumeration { literals: vec!["green".into(), "red".into()], }, Some(DataValue::Enumeration("green".into())), ), (DataTypeDefinition::Record { supertype: None }, None), ]; for reverse in [false, true] { for (definition, default) in &definitions { let initial = if *definition == DataTypeDefinition::Boolean { DataTypeDefinition::Integer { minimum: None, maximum: None, } } else { DataTypeDefinition::Boolean }; let (p, oa, sa, _) = pair(data(initial.clone(), None), data(initial, None)); let (source, target) = if reverse { (sa, oa) } else { (oa, sa) }; let group = FacetSelector::Data(DataFacet::Constructor); let mut app = App::with_project(p); start(&mut app, source); decide(&mut app, group.clone(), SemanticDecision::Candidate); let paths = constructors::roots(app.project(), target, &group).unwrap(); send( &mut app, Event::Variant(paths[0].clone(), json!(definition)), ); send(&mut app, Event::Shape(paths[1].clone(), json!(default))); assert!( state(&app).can_apply(), "{definition:?}: {}", failure(state(&app)) ); assert_eq!( state(&app).proposed.as_ref().unwrap().elements[&target].shared(), Some(&data(definition.clone(), default.clone())) ); let _ = state(&app).view(app.project()); assert!(reviewed(state(&app)).contains(&FacetSelector::Data(DataFacet::Default))); assert!(!reviewed(state(&app)).contains(&FacetSelector::Data(DataFacet::Unit))); apply(&mut app); assert_eq!( app.project().elements[&target].shared(), Some(&data(definition.clone(), default.clone())) ); } } } #[test] fn group_take_reference_payload_and_scalar_candidate_match_core_and_one_document_batch() { for reverse in [false, true] { for family in ["Data", "Event", "Occurrence"] { let source_body = match family { "Data" => data(DataTypeDefinition::Record { supertype: None }, None), "Event" => event(EventBody::Signal { items: vec![] }), _ => occurrence(OccurrenceBody::State { state: None, lifeline: None, }), }; let target_body = match family { "Data" => data(DataTypeDefinition::Boolean, Some(DataValue::Boolean(false))), "Event" => event(EventBody::Change { expression: "old".into(), }), _ => occurrence(OccurrenceBody::Action { action: None, lifeline: None, }), }; let (mut p, oa, sa, trace) = if reverse { pair(target_body, source_body) } else { pair(source_body, target_body) }; let kind = match family { "Data" => Kind::DataType, "Event" => Kind::ExchangeItem, _ => Kind::State, }; let mut refs = vec![]; for layer in [Layer::Oa, Layer::Sa] { let parent = if kind == Kind::State { let owner = p .add_element( if layer == Layer::Oa { Kind::Entity } else { Kind::System }, layer, "State owner", None, ) .unwrap(); let machine = p .add_element(Kind::StateMachine, layer, "Machine", Some(owner)) .unwrap(); Some( p.add_element(Kind::Region, layer, "Region", Some(machine)) .unwrap(), ) } else { None }; let id = p .add_element(kind, layer, "Referenced definition", parent) .unwrap(); if family == "Data" { *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = data(DataTypeDefinition::Record { supertype: None }, None); } refs.push(id); } let (source, target, source_ref, target_ref) = if reverse { (sa, oa, refs[1], refs[0]) } else { (oa, sa, refs[0], refs[1]) }; match p.elements.get_mut(&source).unwrap().shared_mut().unwrap() { SharedBody::DataType { definition: DataTypeDefinition::Record { supertype }, .. } => *supertype = Some(source_ref), SharedBody::Event { body: EventBody::Signal { items }, } => items.push(source_ref), SharedBody::Occurrence { body: OccurrenceBody::State { state, .. }, .. } => *state = Some(source_ref), _ => unreachable!(), } let group = match family { "Data" => FacetSelector::Data(DataFacet::Constructor), "Event" => FacetSelector::Event(EventFacet::Constructor), _ => FacetSelector::Occurrence(OccurrenceFacet::Constructor), }; let mut app = App::with_project(p); let mut bundle = app.document.bundle().clone(); bundle .files .get_mut(&bundle.entry) .unwrap() .insert_str(0, "// Preserve the constructor review rationale.\n"); app.document.apply_bundle(bundle).unwrap(); let before = app.document.snapshot(); start(&mut app, source); decide(&mut app, group.clone(), SemanticDecision::TakeSource); assert!(!state(&app).can_apply()); let oa_choice = view::choice(app.project(), refs[0]); let sa_choice = view::choice(app.project(), refs[1]); send(&mut app, Event::ReferenceOa(oa_choice)); send(&mut app, Event::ReferenceSa(sa_choice)); send(&mut app, Event::AddReference); assert!( state(&app).can_apply(), "{family} reverse={reverse}: {}", failure(state(&app)) ); let core_group = state(&app).proposed.as_ref().unwrap().elements[&target].clone(); decide( &mut app, FacetSelector::Header(HeaderFacet::Description), SemanticDecision::Candidate, ); send( &mut app, Event::Input("/description".into(), source_ref.to_string()), ); assert!( state(&app).can_apply(), "{family} reverse={reverse}: {}", failure(state(&app)) ); assert_eq!( state(&app).proposed.as_ref().unwrap().elements[&target].shared(), core_group.shared() ); assert!(oa_core::references::element_references(&core_group).contains(&target_ref)); assert_eq!(app.document.bundle(), &before.bundle); let expected = state(&app).proposed.clone().unwrap(); apply(&mut app); assert_eq!(app.project(), &expected); assert_eq!( app.project().elements[&source], before.decoded.project.elements[&source] ); assert!( app.document.bundle().files[&app.document.bundle().entry] .starts_with("// Preserve the constructor review rationale.") ); assert_eq!( Document::from_bundle( app.document.bundle().clone(), source_project::ProjectLimits::default() ) .unwrap() .snapshot() .decoded .project, expected ); let _ = app.update(Message::Undo); assert_eq!(app.document.bundle(), &before.bundle); let _ = app.update(Message::Redo); assert_eq!(app.project(), &expected); assert!( app.project().traces[&trace] .baseline .as_ref() .unwrap() .semantic .is_some() ); } } } #[test] fn data_group_retains_invalid_sibling_drafts_companion_default_and_variant_editor_buffers() { let integer = DataTypeDefinition::Integer { minimum: Some(1), maximum: Some(10), }; let (p, oa, sa, _) = pair( data(integer.clone(), None), data(integer, Some(DataValue::Integer(5))), ); let group = FacetSelector::Data(DataFacet::Constructor); let minimum = FacetSelector::Data(DataFacet::IntegerMinimum); let mut app = App::with_project(p); start(&mut app, oa); let roots = constructors::roots(app.project(), sa, &group).unwrap(); let min_path = format!("{}/Integer/minimum", roots[0]); decide(&mut app, minimum.clone(), SemanticDecision::Candidate); send(&mut app, Event::Input(min_path.clone(), "-".into())); decide(&mut app, minimum.clone(), SemanticDecision::KeepTarget); decide(&mut app, group.clone(), SemanticDecision::Candidate); send(&mut app, Event::Input(min_path.clone(), "-draft".into())); let cursor = state(&app).group_drafts[&group].inputs[&min_path] .editor .cursor(); send(&mut app, Event::Variant(roots[0].clone(), json!("Boolean"))); assert_eq!( state(&app).group_drafts[&group].value.pointer(&roots[1]), Some(&json!({"Integer":5})) ); assert!(!state(&app).can_apply()); // overlap and explicitly incompatible default retained send( &mut app, Event::Variant( roots[0].clone(), json!({"Integer":{"minimum":null,"maximum":null}}), ), ); assert_eq!( state(&app).group_drafts[&group].inputs[&min_path].raw, "-draft" ); assert_eq!( state(&app).group_drafts[&group].inputs[&min_path] .editor .cursor(), cursor ); assert_eq!( state(&app).draft.as_ref().unwrap().inputs[&min_path].raw, "-" ); send(&mut app, Event::Shape(min_path.clone(), Value::Null)); send(&mut app, Event::Variant(roots[0].clone(), json!("Boolean"))); send( &mut app, Event::Shape(roots[1].clone(), json!({"Boolean":true})), ); send(&mut app, Event::Decide(minimum, None)); assert!(state(&app).can_apply(), "{}", failure(state(&app))); for protected in ["/id", "/alias", "/body", "/parent", "/name"] { send(&mut app, Event::Shape(protected.into(), Value::Null)); assert!( state(&app) .error .as_ref() .unwrap() .contains("Only the selected") ); } assert_eq!( state(&app).draft.as_ref().unwrap().inputs[&min_path].raw, "-" ); send(&mut app, Event::Preview); assert!(state(&app).can_apply()); } #[test] fn occurrence_and_event_candidate_variants_cover_closed_families_without_touching_headers() { let occurrences = [ OccurrenceBody::Action { action: None, lifeline: None, }, OccurrenceBody::Control { kind: ControlKind::Choice, }, OccurrenceBody::SequenceReference { sequence: None }, OccurrenceBody::ScenarioUse { scenario: None, bindings: vec![], }, OccurrenceBody::State { state: None, lifeline: None, }, OccurrenceBody::Exchange { exchange: None, sender: None, receiver: None, message_kind: MessageKind::Timer { expression: "after 5s".into(), }, reply_to: None, values: vec![], }, OccurrenceBody::Fragment { operator: FragmentOperator::Alternative, branches: vec![], }, OccurrenceBody::Branch { guard: "condition".into(), occurrences: vec![], }, ]; let events = [ EventBody::Signal { items: vec![] }, EventBody::Time { kind: TimeEventKind::At, expression: "08:00".into(), }, EventBody::Change { expression: "changed".into(), }, ]; for reverse in [false, true] { for body in occurrences .iter() .cloned() .map(occurrence) .chain(events.iter().cloned().map(event)) { let (group, initial) = if matches!(body, SharedBody::Occurrence { .. }) { ( FacetSelector::Occurrence(OccurrenceFacet::Constructor), occurrence(OccurrenceBody::Action { action: None, lifeline: None, }), ) } else { ( FacetSelector::Event(EventFacet::Constructor), event(EventBody::Change { expression: "original".into(), }), ) }; let initial = match &body { SharedBody::Occurrence { body: OccurrenceBody::Action { .. }, .. } => occurrence(OccurrenceBody::Exchange { exchange: None, sender: None, receiver: None, message_kind: MessageKind::Asynchronous, reply_to: None, values: vec![], }), SharedBody::Event { body: EventBody::Change { .. }, } => event(EventBody::Signal { items: vec![] }), _ => initial, }; let (mut p, oa, sa, _) = pair(initial.clone(), initial); let (source, target) = if reverse { (sa, oa) } else { (oa, sa) }; match &body { SharedBody::Occurrence { body: OccurrenceBody::Control { .. } | OccurrenceBody::SequenceReference { .. }, .. } => { for id in [oa, sa] { let old = p.elements[&id].parent.unwrap(); p.elements .get_mut(&old) .unwrap() .steps_mut() .unwrap() .clear(); let layer = p.elements[&id].layer(); let owner = p .add_element( if layer == Layer::Oa { Kind::Process } else { Kind::FunctionalChain }, layer, "Sequence", None, ) .unwrap(); p.elements .get_mut(&owner) .unwrap() .steps_mut() .unwrap() .push(id); p.elements.get_mut(&id).unwrap().parent = Some(owner); } } SharedBody::Occurrence { body: OccurrenceBody::Branch { .. }, .. } => { let owner = p.elements[&target].parent.unwrap(); let layer = p.elements[&target].layer(); let fragment = p .add_element(Kind::Occurrence, layer, "Fragment", Some(owner)) .unwrap(); *p.elements.get_mut(&fragment).unwrap().shared_mut().unwrap() = occurrence(OccurrenceBody::Fragment { operator: FragmentOperator::Alternative, branches: vec![target], }); *p.elements.get_mut(&owner).unwrap().steps_mut().unwrap() = vec![fragment]; p.elements.get_mut(&target).unwrap().parent = Some(fragment); *p.elements.get_mut(&target).unwrap().shared_mut().unwrap() = occurrence(OccurrenceBody::Branch { guard: "old".into(), occurrences: vec![], }); } _ => {} } let mut app = App::with_project(p); start(&mut app, source); decide(&mut app, group.clone(), SemanticDecision::Candidate); let root = scalar::binding(app.project(), target, &group).unwrap(); let payload = match &body { SharedBody::Occurrence { body, .. } => json!(body), SharedBody::Event { body } => json!(body), _ => unreachable!(), }; send(&mut app, Event::Shape(root, payload)); assert!( state(&app).can_apply(), "{body:?}: {}", failure(state(&app)) ); assert_eq!( state(&app).proposed.as_ref().unwrap().elements[&target].shared(), Some(&body) ); let _ = state(&app).view(app.project()); } } } #[test] fn port_group_companions_typed_owner_context_and_direction_restrictions_are_explicit() { let legacy = || SharedBody::Port { contract: PortContract::Legacy, legacy: Some(LegacyPortContract { direction: PortDirection::InOut, interface: None, }), }; let group = FacetSelector::Port(PortFacet::Constructor); let (mut p, oa, sa, _) = pair(legacy(), legacy()); let item = p .add_element(Kind::ExchangeItem, Layer::Sa, "Item", None) .unwrap(); let interface = p .add_element(Kind::Interface, Layer::Sa, "Interface", None) .unwrap(); let component = p .add_element(Kind::System, Layer::Sa, "Component", None) .unwrap(); let mut app = App::with_project(p); start(&mut app, oa); decide(&mut app, group.clone(), SemanticDecision::Candidate); let roots = constructors::roots(app.project(), sa, &group).unwrap(); send( &mut app, Event::Shape(format!("{}/interface", roots[1]), json!(interface)), ); send( &mut app, Event::Variant( roots[0].clone(), json!({"FunctionInput":{"incoming_items":[]}}), ), ); send( &mut app, Event::Append( format!("{}/FunctionInput/incoming_items", roots[0]), json!(item), ), ); assert!(state(&app).can_apply(), "{}", failure(state(&app))); send( &mut app, Event::Variant( roots[0].clone(), json!({"FunctionOutput":{"outgoing_items":[]}}), ), ); assert_eq!( state(&app).group_drafts[&group] .value .pointer(&format!("{}/FunctionOutput/outgoing_items", roots[0])), Some(&json!([item])) ); assert_eq!( state(&app).group_drafts[&group] .value .pointer(&format!("{}/interface", roots[1])), Some(&json!(interface)) ); assert!(state(&app).can_apply(), "{}", failure(state(&app))); decide( &mut app, FacetSelector::Header(HeaderFacet::Parent), SemanticDecision::Candidate, ); send(&mut app, Event::Shape("/parent".into(), json!(component))); send(&mut app, Event::Select(group.clone())); send( &mut app, Event::Variant( roots[0].clone(), json!({"Component":{"kind":"Flow","orientation":"InOut"}}), ), ); assert!(state(&app).can_apply(), "{}", failure(state(&app))); let _ = state(&app).view(app.project()); assert_eq!( state(&app).group_drafts[&group].context_value()["parent"], json!(component) ); send(&mut app, Event::Shape(roots[1].clone(), Value::Null)); assert!(state(&app).can_apply()); apply(&mut app); assert!(matches!( app.project().elements[&sa].shared(), Some(SharedBody::Port { contract: PortContract::Component { .. }, legacy: None }) )); start(&mut app, sa); decide(&mut app, group.clone(), SemanticDecision::TakeSource); assert!( !state(&app).can_apply(), "Typed SA ports must not be copied to OA." ); let _ = state(&app).view(app.project()); assert_eq!(app.project().elements[&oa].shared(), Some(&legacy())); } #[test] fn outer_and_nested_constructor_candidates_require_explicit_resolution_and_retain_both_drafts() { let original = occurrence(OccurrenceBody::Exchange { exchange: None, sender: None, receiver: None, message_kind: MessageKind::Timer { expression: "initial".into(), }, reply_to: None, values: vec![], }); let (p, oa, sa, _) = pair(original.clone(), original); let outer = FacetSelector::Occurrence(OccurrenceFacet::Constructor); let nested = message_kind::selector(); let mut app = App::with_project(p); start(&mut app, oa); let path = scalar::binding(app.project(), sa, &outer).unwrap(); let kind_path = scalar::binding(app.project(), sa, &nested).unwrap(); let expression = format!("{kind_path}/Timer/expression"); decide(&mut app, nested.clone(), SemanticDecision::Candidate); send( &mut app, Event::Input(expression.clone(), "nested draft".into()), ); decide(&mut app, outer.clone(), SemanticDecision::Candidate); send( &mut app, Event::Input(expression.clone(), "outer draft".into()), ); assert!(!state(&app).can_apply()); assert!(failure(state(&app)).contains("different final values")); send( &mut app, Event::Variant( path.clone(), json!({"Action":{"action":null,"lifeline":null}}), ), ); assert!(!state(&app).can_apply()); assert_eq!(state(&app).decisions.len(), 2); send(&mut app, Event::Decide(nested.clone(), None)); assert!(state(&app).can_apply(), "{}", failure(state(&app))); send( &mut app, Event::Variant( path.clone(), json!({"Exchange":{"exchange":null,"sender":null,"receiver":null,"message_kind":"Asynchronous","reply_to":null,"values":[]}}), ), ); assert_eq!( state(&app).group_drafts[&outer].inputs[&expression].raw, "outer draft" ); assert_eq!( state(&app).group_drafts[&nested].inputs[&expression].raw, "nested draft" ); assert!( !state(&app).can_apply(), "Unchanged kind cannot silently acknowledge edited payload." ); send( &mut app, Event::Variant(path, json!({"Action":{"action":null,"lifeline":null}})), ); assert!(state(&app).can_apply()); let scope = reviewed(state(&app)); assert!(scope.contains(&message_kind::selector())); assert!(scope.contains(&message_kind::timer_selector())); assert!(!scope.contains(&FacetSelector::Occurrence(OccurrenceFacet::Precondition))); assert!(!scope.contains(&FacetSelector::Occurrence(OccurrenceFacet::Postcondition))); } #[test] fn historical_event_keep_retires_only_obsolete_branch_and_unchanged_kind_acknowledges_no_payload() { let time = || { event(EventBody::Time { kind: TimeEventKind::After, expression: "5 seconds".into(), }) }; let (mut p, oa, sa, trace) = pair(time(), time()); history(&mut p, trace); for id in [oa, sa] { *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = event(EventBody::Change { expression: "changed".into(), }); } let group = FacetSelector::Event(EventFacet::Constructor); let mut app = App::with_project(p); start(&mut app, oa); decide(&mut app, group.clone(), SemanticDecision::KeepTarget); assert!(state(&app).can_apply(), "{}", failure(state(&app))); let preview = &state(&app).plan.as_ref().unwrap().reviews[0]; assert_eq!( preview.retired.iter().cloned().collect::>(), BTreeSet::from([ FacetSelector::Event(EventFacet::TimeKind), FacetSelector::Event(EventFacet::TimeExpression) ]) ); assert_eq!( reviewed(state(&app)), BTreeSet::from([ group.clone(), FacetSelector::Event(EventFacet::ChangeExpression) ]) ); apply(&mut app); start(&mut app, oa); decide(&mut app, group.clone(), SemanticDecision::KeepTarget); assert!(!state(&app).can_apply()); let (p, oa, sa, _) = pair( time(), event(EventBody::Time { kind: TimeEventKind::At, expression: "9:00".into(), }), ); let mut app = App::with_project(p); start(&mut app, oa); decide(&mut app, group.clone(), SemanticDecision::TakeSource); decide( &mut app, FacetSelector::Header(HeaderFacet::Name), SemanticDecision::Candidate, ); send(&mut app, Event::Input("/name".into(), "Kind only".into())); assert!(state(&app).can_apply(), "{}", failure(state(&app))); assert_eq!( reviewed(state(&app)), BTreeSet::from([group, FacetSelector::Header(HeaderFacet::Name)]) ); assert_eq!( state(&app).proposed.as_ref().unwrap().elements[&sa].shared(), app.project().elements[&sa].shared() ); } #[test] fn existing_fragment_and_branch_group_candidates_edit_complete_owned_order_without_deletion() { for reverse in [false, true] { for fragment in [false, true] { let source_body = occurrence(OccurrenceBody::Action { action: None, lifeline: None, }); let target_body = occurrence(OccurrenceBody::Fragment { operator: FragmentOperator::Alternative, branches: vec![], }); let (mut p, oa, sa, _) = if reverse { pair(target_body, source_body) } else { pair(source_body, target_body) }; let (source, mut target) = if reverse { (sa, oa) } else { (oa, sa) }; if !fragment { let branch = p .add_element( Kind::Occurrence, p.elements[&target].layer(), "Branch", Some(target), ) .unwrap(); *p.elements.get_mut(&branch).unwrap().shared_mut().unwrap() = occurrence(OccurrenceBody::Branch { guard: "old guard".into(), occurrences: vec![], }); *p.elements.get_mut(&target).unwrap().shared_mut().unwrap() = occurrence(OccurrenceBody::Fragment { operator: FragmentOperator::Alternative, branches: vec![branch], }); let t = p.traces.values_mut().next().unwrap(); if reverse { t.oa = branch; } else { t.sa = branch; } target = branch; } let mut children = vec![]; for name in ["First", "Second"] { let child = p .add_element( Kind::Occurrence, p.elements[&target].layer(), name, Some(target), ) .unwrap(); *p.elements.get_mut(&child).unwrap().shared_mut().unwrap() = if fragment { occurrence(OccurrenceBody::Branch { guard: name.into(), occurrences: vec![], }) } else { occurrence(OccurrenceBody::Action { action: None, lifeline: None, }) }; children.push(child); } let (payload, suffix) = if fragment { ( occurrence(OccurrenceBody::Fragment { operator: FragmentOperator::Alternative, branches: children.clone(), }), "/Fragment/branches", ) } else { ( occurrence(OccurrenceBody::Branch { guard: "old guard".into(), occurrences: children.clone(), }), "/Branch/occurrences", ) }; *p.elements.get_mut(&target).unwrap().shared_mut().unwrap() = payload; let original_children = children .iter() .map(|id| p.elements[id].clone()) .collect::>(); let mut app = App::with_project(p); start(&mut app, source); let group = FacetSelector::Occurrence(OccurrenceFacet::Constructor); let root = scalar::binding(app.project(), target, &group).unwrap(); decide(&mut app, group, SemanticDecision::Candidate); send( &mut app, Event::Array(format!("{root}{suffix}"), Some(1), -1), ); assert!(state(&app).can_apply(), "{}", failure(state(&app))); let result = state(&app).proposed.as_ref().unwrap(); let value = json!(result.elements[&target]); assert_eq!( value.pointer(&format!("{root}{suffix}")), Some(&json!([children[1], children[0]])) ); let expected_order = FacetSelector::Order(if fragment { OrderedCollection::FragmentBranches } else { OrderedCollection::Steps }); assert!(reviewed(state(&app)).contains(&expected_order)); assert!(state(&app).plan.as_ref().unwrap().impacts.is_empty()); let _ = state(&app).view(app.project()); apply(&mut app); assert_eq!( children .iter() .map(|id| app.project().elements[id].clone()) .collect::>(), original_children ); } } } #[test] fn scenario_use_group_takes_typed_binding_references_and_preserves_source_text_both_directions() { for reverse in [false, true] { let use_body = occurrence(OccurrenceBody::ScenarioUse { scenario: None, bindings: vec![], }); let action = occurrence(OccurrenceBody::Action { action: None, lifeline: None, }); let (mut p, oa, sa, _) = if reverse { pair(action, use_body) } else { pair(use_body, action) }; let mut refs = vec![]; for (layer, id) in [(Layer::Oa, oa), (Layer::Sa, sa)] { let scenario = p .add_element(Kind::Scenario, layer, "Reused scenario", None) .unwrap(); let referenced = p .add_element(Kind::Lifeline, layer, "Referenced lifeline", Some(scenario)) .unwrap(); let local = p .add_element( Kind::Lifeline, layer, "Local lifeline", p.elements[&id].parent, ) .unwrap(); refs.push([scenario, referenced, local]); } let (source, target, side) = if reverse { (sa, oa, 1) } else { (oa, sa, 0) }; *p.elements.get_mut(&source).unwrap().shared_mut().unwrap() = occurrence(OccurrenceBody::ScenarioUse { scenario: Some(refs[side][0]), bindings: vec![LifelineBinding { referenced: refs[side][1], local: refs[side][2], }], }); let mut s = State::new(&p, source).unwrap(); for (oa, sa) in refs[0].iter().zip(&refs[1]) { s.references.insert(*oa, *sa); } s.update( &p, Event::Decide( FacetSelector::Occurrence(OccurrenceFacet::Constructor), Some(SemanticDecision::TakeSource), ), ); assert!(s.can_apply(), "{}", failure(&s)); let expected = s.proposed.as_ref().unwrap().elements[&target] .shared() .unwrap() .clone(); s.update( &p, Event::Decide( FacetSelector::Header(HeaderFacet::Description), Some(SemanticDecision::Candidate), ), ); s.update( &p, Event::Select(FacetSelector::Header(HeaderFacet::Description)), ); s.update( &p, Event::Input("/description".into(), refs[side][1].to_string()), ); assert!(s.can_apply(), "{}", failure(&s)); assert_eq!( s.proposed.as_ref().unwrap().elements[&target].shared(), Some(&expected) ); assert_eq!( expected, occurrence(OccurrenceBody::ScenarioUse { scenario: Some(refs[1 - side][0]), bindings: vec![LifelineBinding { referenced: refs[1 - side][1], local: refs[1 - side][2] }] }) ); } } #[test] fn reverse_existing_typed_port_reviews_keep_or_candidate_legacy_without_permitting_typed_oa_write() { for decision in [SemanticDecision::KeepTarget, SemanticDecision::Candidate] { let legacy = SharedBody::Port { contract: PortContract::Legacy, legacy: Some(LegacyPortContract { direction: PortDirection::Out, interface: None, }), }; let typed = SharedBody::Port { contract: PortContract::FunctionOutput { outgoing_items: vec![], }, legacy: None, }; let (p, oa, sa, _) = pair(legacy.clone(), typed); let mut app = App::with_project(p); let before = app.document.snapshot(); start(&mut app, sa); let group = FacetSelector::Port(PortFacet::Constructor); decide(&mut app, group.clone(), SemanticDecision::TakeSource); assert!(!state(&app).can_apply()); assert_eq!(app.document.bundle(), &before.bundle); decide(&mut app, group, decision); let mut expected = legacy.clone(); if decision == SemanticDecision::Candidate { send( &mut app, Event::Shape("/body/Oa/Shared/Port/legacy/direction".into(), json!("In")), ); if let SharedBody::Port { legacy: Some(association), .. } = &mut expected { association.direction = PortDirection::In; } } assert!( state(&app).can_apply(), "{decision:?}: {}", failure(state(&app)) ); assert_eq!( state(&app).proposed.as_ref().unwrap().elements[&oa].shared(), Some(&expected) ); assert!(reviewed(state(&app)).contains(&FacetSelector::Port(PortFacet::LegacyDirection))); apply(&mut app); assert_eq!(app.project().elements[&oa].shared(), Some(&expected)); assert_eq!( app.project().elements[&sa], before.decoded.project.elements[&sa] ); let _ = app.update(Message::Undo); assert_eq!(app.document.bundle(), &before.bundle); } } #[test] fn historical_same_kind_take_preserves_target_only_literals_when_composed_with_candidate() { for reverse in [false, true] { let source = data(DataTypeDefinition::Boolean, None); let target = data( DataTypeDefinition::Enumeration { literals: vec!["local".into()], }, Some(DataValue::Enumeration("local".into())), ); let (mut p, oa, sa, trace) = if reverse { pair(target, source) } else { pair(source, target) }; history(&mut p, trace); let (source, target) = if reverse { (sa, oa) } else { (oa, sa) }; *p.elements.get_mut(&source).unwrap().shared_mut().unwrap() = data( DataTypeDefinition::Enumeration { literals: vec!["incoming".into()], }, Some(DataValue::Enumeration("incoming".into())), ); let mut app = App::with_project(p); start(&mut app, source); decide( &mut app, FacetSelector::Data(DataFacet::Constructor), SemanticDecision::TakeSource, ); assert!(state(&app).can_apply(), "{}", failure(state(&app))); let core_payload = state(&app).proposed.as_ref().unwrap().elements[&target] .shared() .unwrap() .clone(); decide( &mut app, FacetSelector::Data(DataFacet::Unit), SemanticDecision::Candidate, ); let unit = scalar::binding(app.project(), target, &FacetSelector::Data(DataFacet::Unit)).unwrap(); send(&mut app, Event::Input(unit, "final unit".into())); assert!(state(&app).can_apply(), "{}", failure(state(&app))); let SharedBody::DataType { definition: DataTypeDefinition::Enumeration { literals }, .. } = core_payload else { panic!() }; assert_eq!(literals, ["incoming", "local"]); apply(&mut app); assert!( matches!(app.project().elements[&target].shared(), Some(SharedBody::DataType { definition: DataTypeDefinition::Enumeration { literals }, unit, .. }) if literals == &["incoming", "local"] && unit == "final unit") ); } } #[test] fn closed_data_and_event_take_composition_matches_core_across_every_kind_pair_and_direction() { let data_bodies = vec![ data(DataTypeDefinition::Boolean, Some(DataValue::Boolean(true))), data( DataTypeDefinition::Integer { minimum: Some(-5), maximum: Some(20), }, Some(DataValue::Integer(3)), ), data( DataTypeDefinition::Real { minimum: None, maximum: Some(4.5), }, Some(DataValue::Real(1.25)), ), data( DataTypeDefinition::String { minimum_length: 0, maximum_length: Some(20), }, Some(DataValue::String("source text".into())), ), data( DataTypeDefinition::Enumeration { literals: vec!["a".into(), "b".into()], }, Some(DataValue::Enumeration("a".into())), ), data(DataTypeDefinition::Record { supertype: None }, None), ]; let event_bodies = vec![ event(EventBody::Signal { items: vec![] }), event(EventBody::Time { kind: TimeEventKind::After, expression: "after 12 s".into(), }), event(EventBody::Change { expression: "mode changed".into(), }), ]; for (group, bodies) in [ (FacetSelector::Data(DataFacet::Constructor), data_bodies), (FacetSelector::Event(EventFacet::Constructor), event_bodies), ] { for reverse in [false, true] { for source_body in &bodies { for target_body in &bodies { let (p, oa, sa, _) = if reverse { pair(target_body.clone(), source_body.clone()) } else { pair(source_body.clone(), target_body.clone()) }; let (source, target) = if reverse { (sa, oa) } else { (oa, sa) }; let mut s = State::new(&p, source).unwrap(); s.update( &p, Event::Decide(group.clone(), Some(SemanticDecision::TakeSource)), ); assert!( s.can_apply(), "{source_body:?} -> {target_body:?}, reverse={reverse}: {}", failure(&s) ); let expected = s.proposed.as_ref().unwrap().elements[&target] .shared() .unwrap() .clone(); let expected_review = s.plan.as_ref().unwrap().reviews[0].clone(); s.update( &p, Event::Decide( FacetSelector::Header(HeaderFacet::Description), Some(SemanticDecision::Candidate), ), ); s.update( &p, Event::Select(FacetSelector::Header(HeaderFacet::Description)), ); s.update( &p, Event::Input("/description".into(), "composed candidate".into()), ); assert!( s.can_apply(), "{source_body:?} -> {target_body:?}, reverse={reverse}: {}", failure(&s) ); assert_eq!( s.proposed.as_ref().unwrap().elements[&target].shared(), Some(&expected) ); let reviewed = reviewed(&s); assert!( expected_review .acknowledged .iter() .all(|f| reviewed.contains(f)) ); assert_eq!( s.plan.as_ref().unwrap().reviews[0].retired, expected_review.retired ); } } } } } #[test] fn exchange_constructor_takes_qualified_annotation_addresses_without_remapping_expression_text() { for reverse in [false, true] { let message = occurrence(OccurrenceBody::Exchange { exchange: None, sender: None, receiver: None, message_kind: MessageKind::Asynchronous, reply_to: None, values: vec![], }); let action = occurrence(OccurrenceBody::Action { action: None, lifeline: None, }); let (mut p, oa, sa, _) = if reverse { pair(action, message) } else { pair(message, action) }; let mut refs = vec![]; for layer in [Layer::Oa, Layer::Sa] { let work = if layer == Layer::Oa { Kind::Activity } else { Kind::Function }; let from = p.add_element(work, layer, "From", None).unwrap(); let to = p.add_element(work, layer, "To", None).unwrap(); let item = p .add_element(Kind::ExchangeItem, layer, "Carried item", None) .unwrap(); let record = p .add_element(Kind::DataType, layer, "Record", None) .unwrap(); *p.elements.get_mut(&record).unwrap().shared_mut().unwrap() = data(DataTypeDefinition::Record { supertype: None }, None); let field = p .add_element(Kind::DataField, layer, "Record-valued field", Some(item)) .unwrap(); if let Some(SharedBody::DataField { type_ref, .. }) = p.elements.get_mut(&field).unwrap().shared_mut() { *type_ref = TypeRef::Named(record); } let leaf = p .add_element(Kind::DataField, layer, "Qualified leaf", Some(record)) .unwrap(); let exchange = p.relate( if layer == Layer::Oa { RelationKind::Interaction } else { RelationKind::FunctionalExchange }, "Carrier", from, to, ); let ports = match &mut p.relations.get_mut(&exchange).unwrap().body { RelationBody::Interaction { items, .. } => { items.push(item); None } RelationBody::FunctionalExchange { items, source_port, target_port, .. } => { items.push(item); Some((*source_port, *target_port)) } _ => unreachable!(), }; if let Some((output, input)) = ports { if let Some(SharedBody::Port { contract: PortContract::FunctionOutput { outgoing_items }, .. }) = p.elements.get_mut(&output).unwrap().shared_mut() { outgoing_items.push(item); } if let Some(SharedBody::Port { contract: PortContract::FunctionInput { incoming_items }, .. }) = p.elements.get_mut(&input).unwrap().shared_mut() { incoming_items.push(item); } } refs.push([exchange, field, leaf]); } let (source, target, side) = if reverse { (sa, oa, 1) } else { (oa, sa, 0) }; let expression = format!(" {}\n exact authored expression ", refs[side][1]); if let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { exchange, values, .. }, .. }) = p.elements.get_mut(&source).unwrap().shared_mut() { *exchange = Some(refs[side][0]); values.push(MessageValue { path: vec![refs[side][1]], field: refs[side][2], expression: expression.clone(), }); } let mut s = State::new(&p, source).unwrap(); for (oa, sa) in refs[0].iter().zip(&refs[1]) { s.references.insert(*oa, *sa); } s.update( &p, Event::Decide( FacetSelector::Occurrence(OccurrenceFacet::Constructor), Some(SemanticDecision::TakeSource), ), ); assert!(s.can_apply(), "reverse={reverse}: {}", failure(&s)); s.update( &p, Event::Decide( FacetSelector::Header(HeaderFacet::Description), Some(SemanticDecision::Candidate), ), ); s.update( &p, Event::Select(FacetSelector::Header(HeaderFacet::Description)), ); s.update( &p, Event::Input("/description".into(), "annotation reviewed".into()), ); assert!(s.can_apply(), "reverse={reverse}: {}", failure(&s)); let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { values, .. }, .. }) = s.proposed.as_ref().unwrap().elements[&target].shared() else { panic!() }; assert_eq!( values, &[MessageValue { path: vec![refs[1 - side][1]], field: refs[1 - side][2], expression }] ); assert_eq!( s.proposed.as_ref().unwrap().elements[&source], p.elements[&source] ); } }