Files
ARCADIA-Studio/crates/oa-desktop/src/semantic_review/constructor_tests.rs
T

1463 lines
52 KiB
Rust

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<DataValue>) -> 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::<Vec<_>>()
.join("; ")
}
fn reviewed(s: &State) -> BTreeSet<FacetSelector> {
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<_>>(),
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::<Vec<_>>();
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::<Vec<_>>(),
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]
);
}
}