1463 lines
52 KiB
Rust
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]
|
|
);
|
|
}
|
|
}
|