Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Preserve current core, source, REST, desktop, recovery, documentation and validation work, including AGENTS.md and the development summary. This is a work-in-progress checkpoint: the new compact palette has outstanding compilation errors and native acceptance; earlier passing gates retain their documented scope.
305 lines
9.9 KiB
Rust
305 lines
9.9 KiB
Rust
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<_>>(),
|
|
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<_>>(),
|
|
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 ");
|
|
}
|