use super::*; use crate::document::Document; use oa_core::source_project::ProjectLimits; fn fixture() -> Project { persistence::decode(include_bytes!(concat!( env!("CARGO_MANIFEST_DIR"), "/../../harness/fixtures/interface-operation-probe/output/boundary-only.json" ))) .unwrap() } fn id(p: &Project, n: u32) -> Id { let alias = format!("operation_probe_{n}"); p.elements .values() .find(|e| e.alias == alias) .map(|e| e.id) .or_else(|| { p.relations .values() .find(|r| r.alias == alias) .map(|r| r.id) }) .unwrap() } fn choice(p: &Project, n: u32) -> Choice { Choice { id: id(p, n), label: p.named(id(p, n)).to_owned(), } } fn prepare(p: &Project) -> State { let mut state = State::default(); for event in [ Event::Start(id(p, 40), Tool::Message), Event::Style(MessageStyle::CallReply), Event::Exchange(choice(p, 34)), Event::Sender(choice(p, 41)), Event::Receiver(choice(p, 42)), Event::OperationIntent(true), ] { state.update(p, event).unwrap(); } state } #[test] fn operation_gui_selects_two_parameterless_allocations_without_inventing_refinement_and_reply_derives_call() { let p = fixture(); let mut state = prepare(&p); let choices = crate::data_values::operation_choices( &p, &state.draft.as_ref().unwrap().proposed_message(&p), ); assert_eq!( choices.iter().map(|c| c.id).collect::>(), BTreeSet::from([id(&p, 30), id(&p, 31)]) ); for allocation in [30, 31] { state .update(&p, Event::Operation(choice(&p, allocation))) .unwrap(); let command = state.prepare_pending(&p).unwrap(); let expected = execute_batch(&p, std::slice::from_ref(&command)).unwrap(); let added: Vec<_> = expected .elements .values() .filter(|e| !p.contains(e.id)) .collect(); assert_eq!(added.len(), 2); let call = added .iter() .find(|e| { matches!( e.shared(), Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { message_kind: MessageKind::Call, .. }, .. }) ) }) .unwrap(); let reply = added .iter() .find(|e| { matches!( e.shared(), Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { message_kind: MessageKind::Reply, .. }, .. }) ) }) .unwrap(); assert!( matches!(call.shared(),Some(SharedBody::Occurrence {body:OccurrenceBody::Exchange {operation:Some(OperationUse {allocation:Some(selected)}),..},..}) if *selected==id(&p,allocation)) ); assert!( matches!(reply.shared(),Some(SharedBody::Occurrence {body:OccurrenceBody::Exchange {operation:None,reply_to:Some(selected),..},..}) if *selected==call.id) ); let label = crate::data_values::operation_label(&expected, reply).unwrap(); assert!(label.contains("Reply operation from Call")); assert!(label.contains(if allocation == 30 { "Reset" } else { "ReadStatus" })); assert_eq!(expected.relations, p.relations); let mut doc = Document::from_project(p.clone(), ProjectLimits::default()).unwrap(); let before = doc.bundle().clone(); doc.execute(command).unwrap(); let after = doc.bundle().clone(); assert_eq!(doc.project(), &expected); assert_eq!( Document::from_bundle(after.clone(), ProjectLimits::default()) .unwrap() .project(), &expected ); assert!(doc.undo()); assert_eq!(doc.bundle(), &before); assert!(doc.redo()); assert_eq!(doc.bundle(), &after); } } #[test] fn operation_gui_unresolved_and_disabled_selection_recovery_preserves_rejected_raw_values() { let p = fixture(); let mut state = prepare(&p); state.update(&p, Event::Operation(choice(&p, 30))).unwrap(); state.update(&p, Event::OperationIntent(false)).unwrap(); let raw = state.recovery_snapshot().unwrap().unwrap(); let mut state = State::from_recovery(raw.clone()).unwrap(); assert_eq!(state.recovery_snapshot().unwrap(), Some(raw)); state.update(&p, Event::OperationIntent(true)).unwrap(); assert_eq!( state .draft .as_ref() .unwrap() .operation .as_ref() .unwrap() .allocation, Some(id(&p, 30)) ); let key = crate::data_values::FieldPathKey { path: vec![], field: id(&p, 20), }; state .update(&p, Event::Value(key.clone(), " rejected raw && ".into())) .unwrap(); state.update(&p, Event::ClearOperation).unwrap(); assert!(state.draft.as_ref().unwrap().signature(&p).is_err()); let raw = state.recovery_snapshot().unwrap().unwrap(); let mut restored = State::from_recovery(raw.clone()).unwrap(); assert_eq!(restored.recovery_snapshot().unwrap(), Some(raw.clone())); assert!(restored.prepare_pending(&p).is_err()); assert_eq!(restored.recovery_snapshot().unwrap(), Some(raw)); restored .update(&p, Event::Operation(choice(&p, 31))) .unwrap(); assert_eq!( restored.draft.as_ref().unwrap().values[&key], " rejected raw && " ); assert!(restored.prepare_pending(&p).is_err()); restored .update(&p, Event::Value(key, String::new())) .unwrap(); assert!(restored.prepare_pending(&p).is_ok()); } #[test] fn operation_gui_async_picker_uses_request_root_and_nested_directions_without_erasing_rejected_values() { let mut p = fixture(); let allocation = id(&p, 30); if let RelationBody::InterfaceItemAllocation { transmission, .. } = &mut p.relations.get_mut(&allocation).unwrap().body { *transmission = TransmissionProtocol::Asynchronous; } let record = p .add_element( Kind::DataType, Layer::Sa, "Operation parameter 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 leaf = p .add_element(Kind::DataField, Layer::Sa, "Nested value", Some(record)) .unwrap(); let item = id(&p, 20); let mut fields = vec![]; for (name, direction) in [ ("Request input", ParameterDirection::In), ("Response output", ParameterDirection::Out), ("Return result", ParameterDirection::Return), ] { let field = p .add_element(Kind::DataField, Layer::Sa, name, Some(item)) .unwrap(); *p.elements.get_mut(&field).unwrap().shared_mut().unwrap() = SharedBody::DataField { type_ref: TypeRef::Named(record), direction, minimum: 0, maximum: Some(1), unit: String::new(), storage: FieldStorage::Value, default: None, }; fields.push(field); } validate_candidate(&p).unwrap(); let mut state = prepare(&p); state .update(&p, Event::Style(MessageStyle::Asynchronous)) .unwrap(); state.update(&p, Event::Operation(choice(&p, 30))).unwrap(); let signature = state .draft .as_ref() .unwrap() .signature(&p) .unwrap() .unwrap(); let choices = state .draft .as_ref() .unwrap() .browsed_values(&p, &signature) .unwrap(); assert_eq!( choices.iter().map(|c| c.key.field).collect::>(), vec![fields[0]] ); state .update( &p, Event::ValueBrowse(Some(crate::data_values::FieldPathKey { path: vec![], field: fields[0], })), ) .unwrap(); let choices = state .draft .as_ref() .unwrap() .browsed_values(&p, &signature) .unwrap(); assert_eq!(choices.len(), 1); assert_eq!( choices[0].key, crate::data_values::FieldPathKey { path: vec![fields[0]], field: leaf } ); let rejected = crate::data_values::FieldPathKey { path: vec![fields[1]], field: leaf, }; state .update( &p, Event::Value(rejected.clone(), " retained output expression ".into()), ) .unwrap(); assert!(state.prepare_pending(&p).is_err()); let raw = state.recovery_snapshot().unwrap().unwrap(); let mut restored = State::from_recovery(raw.clone()).unwrap(); assert_eq!(restored.recovery_snapshot().unwrap(), Some(raw)); assert_eq!( restored.draft.as_ref().unwrap().values[&rejected], " retained output expression " ); restored.update(&p, Event::OperationIntent(false)).unwrap(); restored.update(&p, Event::ValueBrowse(None)).unwrap(); let d = restored.draft.as_ref().unwrap(); let ordinary = d.signature(&p).unwrap().unwrap(); assert!( d.browsed_values(&p, &ordinary) .unwrap() .iter() .any(|c| c.key.field == fields[1]), "Ordinary asynchronous channel behavior is preserved" ); assert_eq!(d.values[&rejected], " retained output expression "); }