2312 lines
73 KiB
Rust
2312 lines
73 KiB
Rust
use oa_core::{transitions::*, *};
|
|
use std::collections::BTreeMap;
|
|
|
|
fn add(p: &mut Project, kind: Kind, layer: Layer, name: &str) -> Id {
|
|
p.add_element(kind, layer, name, None).unwrap()
|
|
}
|
|
fn trace(p: &mut Project, oa: Id, sa: Id) -> Id {
|
|
let id = Id::new_v4();
|
|
p.traces.insert(
|
|
id,
|
|
Trace {
|
|
id,
|
|
alias: format!("trace_{}", id.simple()),
|
|
name: "Review".into(),
|
|
description: String::new(),
|
|
oa,
|
|
sa,
|
|
kind: TraceKind::Manual,
|
|
baseline: Some(TraceBaseline {
|
|
oa: p.properties(oa).unwrap(),
|
|
sa: p.properties(sa).unwrap(),
|
|
semantic: None,
|
|
}),
|
|
},
|
|
);
|
|
id
|
|
}
|
|
fn request(
|
|
oa: Id,
|
|
sa: Id,
|
|
facets: Vec<(FacetSelector, SemanticDecision)>,
|
|
) -> SemanticTransitionRequest {
|
|
let mut transition = TransitionRequest::new(TransitionDirection::OaToSa, vec![oa]);
|
|
transition.mappings.push(TransitionMapping {
|
|
source: oa,
|
|
target: Some(sa),
|
|
target_kind: None,
|
|
});
|
|
let mut request = SemanticTransitionRequest::new(transition);
|
|
request.reviews.push(SemanticReviewRequest {
|
|
source: oa,
|
|
resolutions: facets
|
|
.into_iter()
|
|
.map(|(facet, decision)| FacetResolution { facet, decision })
|
|
.collect(),
|
|
reset: vec![],
|
|
});
|
|
request
|
|
}
|
|
fn applied(p: &Project, r: SemanticTransitionRequest) -> Project {
|
|
let plan = plan_semantic_transition(p, r).unwrap();
|
|
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
|
apply_semantic_transition(p, &plan).unwrap()
|
|
}
|
|
fn data(p: &mut Project, id: Id, min: i64, max: i64, unit: &str) {
|
|
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::DataType {
|
|
definition: DataTypeDefinition::Integer {
|
|
minimum: Some(min),
|
|
maximum: Some(max),
|
|
},
|
|
default: None,
|
|
unit: unit.into(),
|
|
};
|
|
}
|
|
#[test]
|
|
fn independent_bounds_keep_take_and_stale_prepare() {
|
|
let mut p = Project::empty();
|
|
let oa = add(&mut p, Kind::DataType, Layer::Oa, "bounded");
|
|
let sa = add(&mut p, Kind::DataType, Layer::Sa, "bounded");
|
|
data(&mut p, oa, 0, 10, "m");
|
|
data(&mut p, sa, 0, 10, "m");
|
|
let t = trace(&mut p, oa, sa);
|
|
let min = FacetSelector::Data(DataFacet::IntegerMinimum);
|
|
let max = FacetSelector::Data(DataFacet::IntegerMaximum);
|
|
p = applied(
|
|
&p,
|
|
request(
|
|
oa,
|
|
sa,
|
|
vec![
|
|
(min.clone(), SemanticDecision::KeepTarget),
|
|
(max.clone(), SemanticDecision::KeepTarget),
|
|
],
|
|
),
|
|
);
|
|
data(&mut p, oa, 2, 12, "m");
|
|
data(&mut p, sa, 0, 10, "cm");
|
|
let plan = plan_semantic_transition(
|
|
&p,
|
|
request(oa, sa, vec![(min.clone(), SemanticDecision::KeepTarget)]),
|
|
)
|
|
.unwrap();
|
|
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
|
let q = apply_semantic_transition(&p, &plan).unwrap();
|
|
let facts = semantic_facets(&q, &q.traces[&t], &BTreeMap::new()).unwrap();
|
|
assert_eq!(
|
|
facts.iter().find(|f| f.selector == min).unwrap().drift,
|
|
Drift::Unchanged
|
|
);
|
|
assert_eq!(
|
|
facts.iter().find(|f| f.selector == max).unwrap().drift,
|
|
Drift::OaChanged
|
|
);
|
|
assert_eq!(
|
|
facts
|
|
.iter()
|
|
.find(|f| f.selector == FacetSelector::Data(DataFacet::Unit))
|
|
.unwrap()
|
|
.drift,
|
|
Drift::Unreviewed
|
|
);
|
|
let taken = applied(
|
|
&q,
|
|
request(oa, sa, vec![(max.clone(), SemanticDecision::TakeSource)]),
|
|
);
|
|
assert!(
|
|
matches!(taken.elements[&sa].shared(),Some(SharedBody::DataType{definition:DataTypeDefinition::Integer{minimum:Some(0),maximum:Some(12)},unit,..}) if unit=="cm")
|
|
);
|
|
let mut stale = p.clone();
|
|
stale.name.push('x');
|
|
assert!(prepare_semantic_transition(&stale, &plan).is_err());
|
|
assert_eq!(apply_semantic_transition(&p, &plan).unwrap(), q);
|
|
}
|
|
#[test]
|
|
fn constructors_expand_into_independent_facts_without_outer_prose() {
|
|
let mut p = Project::empty();
|
|
let oa = add(&mut p, Kind::DataType, Layer::Oa, "definition");
|
|
let sa = add(&mut p, Kind::DataType, Layer::Sa, "definition");
|
|
data(&mut p, oa, 1, 9, "m");
|
|
*p.elements.get_mut(&sa).unwrap().shared_mut().unwrap() = SharedBody::DataType {
|
|
definition: DataTypeDefinition::Real {
|
|
minimum: None,
|
|
maximum: Some(20.),
|
|
},
|
|
default: Some(DataValue::Real(1.)),
|
|
unit: "cm".into(),
|
|
};
|
|
let t = trace(&mut p, oa, sa);
|
|
let group = FacetSelector::Data(DataFacet::Constructor);
|
|
p = applied(
|
|
&p,
|
|
request(oa, sa, vec![(group.clone(), SemanticDecision::TakeSource)]),
|
|
);
|
|
let base = p.traces[&t]
|
|
.baseline
|
|
.as_ref()
|
|
.unwrap()
|
|
.semantic
|
|
.as_ref()
|
|
.unwrap();
|
|
assert!(
|
|
base.facets
|
|
.iter()
|
|
.any(|f| f.selector() == FacetSelector::Data(DataFacet::IntegerMaximum))
|
|
);
|
|
assert!(
|
|
base.facets
|
|
.iter()
|
|
.any(|f| f.selector() == FacetSelector::Data(DataFacet::Default))
|
|
);
|
|
assert!(
|
|
!base
|
|
.facets
|
|
.iter()
|
|
.any(|f| f.selector() == FacetSelector::Data(DataFacet::Unit))
|
|
);
|
|
assert!(matches!(p.elements[&sa].shared(),Some(SharedBody::DataType{unit,..}) if unit=="cm"));
|
|
let bad = plan_semantic_transition(
|
|
&p,
|
|
request(oa, sa, vec![(group, SemanticDecision::KeepTarget)]),
|
|
)
|
|
.unwrap();
|
|
assert!(!bad.conflicts.is_empty());
|
|
}
|
|
#[test]
|
|
fn enum_members_preserve_target_only_and_no_whole_list_overwrite() {
|
|
let mut p = Project::empty();
|
|
let oa = add(&mut p, Kind::DataType, Layer::Oa, "statuses");
|
|
let sa = add(&mut p, Kind::DataType, Layer::Sa, "statuses");
|
|
for (id, literals) in [
|
|
(oa, vec!["Ready", "Paused"]),
|
|
(sa, vec!["Ready", "Maintenance"]),
|
|
] {
|
|
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::DataType {
|
|
definition: DataTypeDefinition::Enumeration {
|
|
literals: literals.into_iter().map(str::to_string).collect(),
|
|
},
|
|
default: None,
|
|
unit: String::new(),
|
|
};
|
|
}
|
|
trace(&mut p, oa, sa);
|
|
let r = request(
|
|
oa,
|
|
sa,
|
|
vec![
|
|
(
|
|
FacetSelector::Literal(KeyPair {
|
|
oa: Some("Paused".into()),
|
|
sa: Some("Paused".into()),
|
|
}),
|
|
SemanticDecision::TakeSource,
|
|
),
|
|
(
|
|
FacetSelector::Literal(KeyPair {
|
|
oa: None,
|
|
sa: Some("Maintenance".into()),
|
|
}),
|
|
SemanticDecision::TakeSource,
|
|
),
|
|
],
|
|
);
|
|
let q = applied(&p, r);
|
|
assert!(
|
|
matches!(q.elements[&sa].shared(),Some(SharedBody::DataType{definition:DataTypeDefinition::Enumeration{literals},..}) if literals==&vec!["Ready".to_string(),"Maintenance".into(),"Paused".into()])
|
|
);
|
|
}
|
|
#[test]
|
|
fn wrong_family_both_na_and_duplicate_resolutions_rejected_atomically() {
|
|
let mut p = Project::empty();
|
|
let oa = add(&mut p, Kind::DataType, Layer::Oa, "boolean");
|
|
let sa = add(&mut p, Kind::DataType, Layer::Sa, "boolean");
|
|
trace(&mut p, oa, sa);
|
|
for facet in [
|
|
FacetSelector::State(StateFacet::Entry),
|
|
FacetSelector::Data(DataFacet::IntegerMinimum),
|
|
] {
|
|
let r = request(oa, sa, vec![(facet, SemanticDecision::KeepTarget)]);
|
|
let plan = plan_semantic_transition(&p, r).unwrap();
|
|
assert!(!plan.conflicts.is_empty());
|
|
assert!(apply_semantic_transition(&p, &plan).is_err());
|
|
}
|
|
let facet = FacetSelector::Data(DataFacet::Unit);
|
|
let plan = plan_semantic_transition(
|
|
&p,
|
|
request(
|
|
oa,
|
|
sa,
|
|
vec![
|
|
(facet.clone(), SemanticDecision::KeepTarget),
|
|
(facet, SemanticDecision::TakeSource),
|
|
],
|
|
),
|
|
)
|
|
.unwrap();
|
|
assert!(!plan.conflicts.is_empty());
|
|
}
|
|
#[test]
|
|
fn historical_reference_remap_preserves_text_and_deleted_reference_observations() {
|
|
let old = Id::new_v4();
|
|
let new = Id::new_v4();
|
|
let mut base = SemanticBaseline {
|
|
facets: vec![
|
|
FacetReview::Field(FieldReview::Type(Reviewed {
|
|
oa: Observed::Value(TypeRef::Named(old)),
|
|
sa: Observed::Value(TypeRef::Boolean),
|
|
})),
|
|
FacetReview::Data(DataReview::Unit(Reviewed {
|
|
oa: Observed::Value(old.to_string()),
|
|
sa: Observed::Value("m".into()),
|
|
})),
|
|
],
|
|
};
|
|
assert!(validate_semantic_baseline(&base).is_ok());
|
|
visit_semantic_references(&mut base, |id| {
|
|
if *id == old {
|
|
*id = new;
|
|
}
|
|
});
|
|
assert_eq!(
|
|
base.facets[0],
|
|
FacetReview::Field(FieldReview::Type(Reviewed {
|
|
oa: Observed::Value(TypeRef::Named(new)),
|
|
sa: Observed::Value(TypeRef::Boolean)
|
|
}))
|
|
);
|
|
assert_eq!(
|
|
base.facets[1],
|
|
FacetReview::Data(DataReview::Unit(Reviewed {
|
|
oa: Observed::Value(old.to_string()),
|
|
sa: Observed::Value("m".into())
|
|
}))
|
|
);
|
|
}
|
|
#[test]
|
|
fn same_target_member_keys_cannot_last_write_win() {
|
|
let a = Id::new_v4();
|
|
let b = Id::new_v4();
|
|
let c = Id::new_v4();
|
|
let review = Reviewed {
|
|
oa: Observed::Value(true),
|
|
sa: Observed::Value(false),
|
|
};
|
|
let base = SemanticBaseline {
|
|
facets: vec![
|
|
FacetReview::Member {
|
|
collection: ReferenceCollection::TransitionTriggers,
|
|
keys: KeyPair {
|
|
oa: Some(a),
|
|
sa: Some(c),
|
|
},
|
|
review: review.clone(),
|
|
},
|
|
FacetReview::Member {
|
|
collection: ReferenceCollection::TransitionTriggers,
|
|
keys: KeyPair {
|
|
oa: Some(b),
|
|
sa: Some(c),
|
|
},
|
|
review,
|
|
},
|
|
],
|
|
};
|
|
assert!(validate_semantic_baseline(&base).is_err());
|
|
}
|
|
#[test]
|
|
fn source_and_target_semantic_headers_can_be_reviewed_in_reverse() {
|
|
let mut p = Project::empty();
|
|
let oa = add(&mut p, Kind::Activity, Layer::Oa, "OA");
|
|
let sa = add(&mut p, Kind::Function, Layer::Sa, "SA");
|
|
trace(&mut p, oa, sa);
|
|
let mut r = request(
|
|
oa,
|
|
sa,
|
|
vec![(
|
|
FacetSelector::Header(HeaderFacet::Name),
|
|
SemanticDecision::TakeSource,
|
|
)],
|
|
);
|
|
r.transition.direction = TransitionDirection::SaToOa;
|
|
r.transition.sources = vec![sa];
|
|
r.transition.mappings = vec![TransitionMapping {
|
|
source: sa,
|
|
target: Some(oa),
|
|
target_kind: None,
|
|
}];
|
|
r.reviews[0].source = sa;
|
|
let q = applied(&p, r);
|
|
assert_eq!(q.elements[&oa].name, "SA");
|
|
}
|
|
|
|
fn owned(p: &mut Project, kind: Kind, layer: Layer, name: &str, parent: Option<Id>) -> Id {
|
|
p.add_element(kind, layer, name, parent).unwrap()
|
|
}
|
|
fn set(p: &mut Project, id: Id, body: SharedBody) {
|
|
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = body;
|
|
}
|
|
|
|
fn field(p: &mut Project, layer: Layer, owner: Id, name: &str, type_ref: TypeRef, max: u32) -> Id {
|
|
let id = owned(p, Kind::DataField, layer, name, Some(owner));
|
|
set(
|
|
p,
|
|
id,
|
|
SharedBody::DataField {
|
|
type_ref,
|
|
minimum: 0,
|
|
maximum: Some(max),
|
|
unit: String::new(),
|
|
direction: ParameterDirection::Unspecified,
|
|
storage: FieldStorage::Value,
|
|
default: None,
|
|
},
|
|
);
|
|
id
|
|
}
|
|
fn record(p: &mut Project, layer: Layer, name: &str, supertype: Option<Id>) -> Id {
|
|
let id = owned(p, Kind::DataType, layer, name, None);
|
|
set(
|
|
p,
|
|
id,
|
|
SharedBody::DataType {
|
|
definition: DataTypeDefinition::Record { supertype },
|
|
default: None,
|
|
unit: String::new(),
|
|
},
|
|
);
|
|
id
|
|
}
|
|
|
|
fn occurrence(p: &mut Project, layer: Layer, owner: Id, name: &str, body: OccurrenceBody) -> Id {
|
|
let id = owned(p, Kind::Occurrence, layer, name, Some(owner));
|
|
set(
|
|
p,
|
|
id,
|
|
SharedBody::Occurrence {
|
|
body,
|
|
precondition: String::new(),
|
|
postcondition: String::new(),
|
|
},
|
|
);
|
|
p.elements
|
|
.get_mut(&owner)
|
|
.unwrap()
|
|
.steps_mut()
|
|
.unwrap()
|
|
.push(id);
|
|
id
|
|
}
|
|
|
|
struct Payload {
|
|
message: Id,
|
|
record: Id,
|
|
leaf: Id,
|
|
first: AnnotationKey,
|
|
second: AnnotationKey,
|
|
}
|
|
fn payload(p: &mut Project, layer: Layer, prefix: &str) -> Payload {
|
|
let coord = record(p, layer, &format!("{prefix} Coordinate"), None);
|
|
let leaf = field(
|
|
p,
|
|
layer,
|
|
coord,
|
|
&format!("{prefix} latitude"),
|
|
TypeRef::Real,
|
|
1,
|
|
);
|
|
let item = owned(
|
|
p,
|
|
Kind::ExchangeItem,
|
|
layer,
|
|
&format!("{prefix} Item"),
|
|
None,
|
|
);
|
|
let origin = field(
|
|
p,
|
|
layer,
|
|
item,
|
|
&format!("{prefix} origin"),
|
|
TypeRef::Named(coord),
|
|
1,
|
|
);
|
|
let destination = field(
|
|
p,
|
|
layer,
|
|
item,
|
|
&format!("{prefix} destination"),
|
|
TypeRef::Named(coord),
|
|
1,
|
|
);
|
|
let a = owned(
|
|
p,
|
|
if layer == Layer::Oa {
|
|
Kind::Activity
|
|
} else {
|
|
Kind::Function
|
|
},
|
|
layer,
|
|
&format!("{prefix} A"),
|
|
None,
|
|
);
|
|
let b = owned(
|
|
p,
|
|
if layer == Layer::Oa {
|
|
Kind::Activity
|
|
} else {
|
|
Kind::Function
|
|
},
|
|
layer,
|
|
&format!("{prefix} B"),
|
|
None,
|
|
);
|
|
let exchange = p.relate(
|
|
if layer == Layer::Oa {
|
|
RelationKind::Interaction
|
|
} else {
|
|
RelationKind::FunctionalExchange
|
|
},
|
|
&format!("{prefix} exchange"),
|
|
a,
|
|
b,
|
|
);
|
|
match &mut p.relations.get_mut(&exchange).unwrap().body {
|
|
RelationBody::Interaction { items, .. }
|
|
| RelationBody::FunctionalExchange { items, .. } => items.push(item),
|
|
_ => unreachable!(),
|
|
}
|
|
if let RelationBody::FunctionalExchange {
|
|
source_port,
|
|
target_port,
|
|
..
|
|
} = p.relations[&exchange].body
|
|
{
|
|
for id in [source_port, target_port] {
|
|
match p.elements.get_mut(&id).unwrap().shared_mut().unwrap() {
|
|
SharedBody::Port {
|
|
contract: PortContract::FunctionInput { incoming_items },
|
|
..
|
|
} => incoming_items.push(item),
|
|
SharedBody::Port {
|
|
contract: PortContract::FunctionOutput { outgoing_items },
|
|
..
|
|
} => outgoing_items.push(item),
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
}
|
|
let scenario = owned(
|
|
p,
|
|
Kind::Scenario,
|
|
layer,
|
|
&format!("{prefix} scenario"),
|
|
None,
|
|
);
|
|
let first = AnnotationKey {
|
|
path: vec![origin],
|
|
field: leaf,
|
|
};
|
|
let second = AnnotationKey {
|
|
path: vec![destination],
|
|
field: leaf,
|
|
};
|
|
let message = occurrence(
|
|
p,
|
|
layer,
|
|
scenario,
|
|
&format!("{prefix} message"),
|
|
OccurrenceBody::Exchange {
|
|
exchange: Some(exchange),
|
|
sender: None,
|
|
receiver: None,
|
|
message_kind: MessageKind::Asynchronous,
|
|
reply_to: None,
|
|
values: vec![
|
|
MessageValue {
|
|
path: first.path.clone(),
|
|
field: leaf,
|
|
expression: "first old".into(),
|
|
},
|
|
MessageValue {
|
|
path: second.path.clone(),
|
|
field: leaf,
|
|
expression: "second independent".into(),
|
|
},
|
|
],
|
|
},
|
|
);
|
|
Payload {
|
|
message,
|
|
record: coord,
|
|
leaf,
|
|
first,
|
|
second,
|
|
}
|
|
}
|
|
fn values_mut(p: &mut Project, id: Id) -> &mut Vec<MessageValue> {
|
|
let Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange { values, .. },
|
|
..
|
|
}) = p.elements.get_mut(&id).unwrap().shared_mut()
|
|
else {
|
|
panic!()
|
|
};
|
|
values
|
|
}
|
|
|
|
struct Use {
|
|
use_id: Id,
|
|
referenced: Id,
|
|
alternate: Id,
|
|
extra_referenced: Id,
|
|
}
|
|
fn scenario_use(p: &mut Project, layer: Layer, prefix: &str) -> Use {
|
|
let caller = owned(p, Kind::Scenario, layer, &format!("{prefix} caller"), None);
|
|
let callee = owned(p, Kind::Scenario, layer, &format!("{prefix} callee"), None);
|
|
let mut line = |owner, name: &str| {
|
|
owned(
|
|
p,
|
|
Kind::Lifeline,
|
|
layer,
|
|
&format!("{prefix} {name}"),
|
|
Some(owner),
|
|
)
|
|
};
|
|
let referenced = line(callee, "referenced");
|
|
let extra_referenced = line(callee, "extra referenced");
|
|
let old_local = line(caller, "local");
|
|
let alternate = line(caller, "alternate");
|
|
let extra_local = line(caller, "extra local");
|
|
let use_id = occurrence(
|
|
p,
|
|
layer,
|
|
caller,
|
|
&format!("{prefix} use"),
|
|
OccurrenceBody::ScenarioUse {
|
|
scenario: Some(callee),
|
|
bindings: vec![
|
|
LifelineBinding {
|
|
referenced,
|
|
local: old_local,
|
|
},
|
|
LifelineBinding {
|
|
referenced: extra_referenced,
|
|
local: extra_local,
|
|
},
|
|
],
|
|
},
|
|
);
|
|
Use {
|
|
use_id,
|
|
referenced,
|
|
alternate,
|
|
extra_referenced,
|
|
}
|
|
}
|
|
fn bindings_mut(p: &mut Project, id: Id) -> &mut Vec<LifelineBinding> {
|
|
let Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::ScenarioUse { bindings, .. },
|
|
..
|
|
}) = p.elements.get_mut(&id).unwrap().shared_mut()
|
|
else {
|
|
panic!()
|
|
};
|
|
bindings
|
|
}
|
|
|
|
#[test]
|
|
fn atomic_annotation_full_path_updates_only_selected_row() {
|
|
let mut p = Project::empty();
|
|
let a = payload(&mut p, Layer::Oa, "oa");
|
|
let b = payload(&mut p, Layer::Sa, "sa");
|
|
trace(&mut p, a.message, b.message);
|
|
values_mut(&mut p, a.message)[0].expression = "new first".into();
|
|
let retained = values_mut(&mut p, b.message)[1].clone();
|
|
let selector = FacetSelector::Annotation(KeyPair {
|
|
oa: Some(a.first.clone()),
|
|
sa: Some(b.first.clone()),
|
|
});
|
|
let mut r = request(
|
|
a.message,
|
|
b.message,
|
|
vec![(selector, SemanticDecision::TakeSource)],
|
|
);
|
|
r.references.extend(
|
|
a.first
|
|
.path
|
|
.iter()
|
|
.copied()
|
|
.zip(b.first.path.iter().copied()),
|
|
);
|
|
r.references.insert(a.leaf, b.leaf);
|
|
let mut q = applied(&p, r);
|
|
let rows = values_mut(&mut q, b.message);
|
|
assert_eq!(rows.len(), 2);
|
|
assert!(rows.contains(&retained));
|
|
assert!(
|
|
rows.iter()
|
|
.any(|v| AnnotationKey::from(v) == b.first && v.expression == "new first")
|
|
);
|
|
assert!(
|
|
rows.iter()
|
|
.any(|v| AnnotationKey::from(v) == b.second && v.expression == retained.expression)
|
|
);
|
|
}
|
|
#[test]
|
|
fn atomic_binding_updates_local_and_rejects_pair_collision_and_remapping_without_reset() {
|
|
let mut p = Project::empty();
|
|
let a = scenario_use(&mut p, Layer::Oa, "oa");
|
|
let b = scenario_use(&mut p, Layer::Sa, "sa");
|
|
trace(&mut p, a.use_id, b.use_id);
|
|
bindings_mut(&mut p, a.use_id)[0].local = a.alternate;
|
|
let retained = bindings_mut(&mut p, b.use_id)[1].clone();
|
|
let selector = FacetSelector::Binding(KeyPair {
|
|
oa: Some(a.referenced),
|
|
sa: Some(b.referenced),
|
|
});
|
|
let mut r = request(
|
|
a.use_id,
|
|
b.use_id,
|
|
vec![(selector.clone(), SemanticDecision::TakeSource)],
|
|
);
|
|
r.references = BTreeMap::from([(a.referenced, b.referenced), (a.alternate, b.alternate)]);
|
|
let mut q = applied(&p, r.clone());
|
|
assert!(bindings_mut(&mut q, b.use_id).contains(&retained));
|
|
assert!(
|
|
bindings_mut(&mut q, b.use_id)
|
|
.iter()
|
|
.any(|v| v.referenced == b.referenced && v.local == b.alternate)
|
|
);
|
|
let collided = FacetSelector::Binding(KeyPair {
|
|
oa: Some(a.extra_referenced),
|
|
sa: Some(b.referenced),
|
|
});
|
|
let mut bad = r.clone();
|
|
bad.references.insert(a.extra_referenced, b.referenced);
|
|
bad.reviews[0].resolutions.push(FacetResolution {
|
|
facet: collided,
|
|
decision: SemanticDecision::KeepTarget,
|
|
});
|
|
assert!(
|
|
!plan_semantic_transition(&p, bad)
|
|
.unwrap()
|
|
.conflicts
|
|
.is_empty()
|
|
);
|
|
let mapped = FacetSelector::Binding(KeyPair {
|
|
oa: Some(a.referenced),
|
|
sa: Some(b.extra_referenced),
|
|
});
|
|
r.references.insert(a.referenced, b.extra_referenced);
|
|
r.reviews[0].resolutions = vec![FacetResolution {
|
|
facet: mapped.clone(),
|
|
decision: SemanticDecision::KeepTarget,
|
|
}];
|
|
assert!(
|
|
!plan_semantic_transition(&q, r.clone())
|
|
.unwrap()
|
|
.conflicts
|
|
.is_empty()
|
|
);
|
|
r.reviews[0].reset = vec![selector];
|
|
q = applied(&q, r);
|
|
let t = q.traces.values().find(|t| t.oa == a.use_id).unwrap();
|
|
let base = t.baseline.as_ref().unwrap().semantic.as_ref().unwrap();
|
|
assert_eq!(base.facets.len(), 1);
|
|
assert_eq!(base.facets[0].selector(), mapped);
|
|
}
|
|
#[test]
|
|
fn ordered_member_requires_anchor_or_explicit_order_and_preserves_target_only() {
|
|
let mut p = Project::empty();
|
|
let oa = add(&mut p, Kind::Process, Layer::Oa, "process");
|
|
let sa = add(&mut p, Kind::FunctionalChain, Layer::Sa, "chain");
|
|
let a = occurrence(
|
|
&mut p,
|
|
Layer::Oa,
|
|
oa,
|
|
"a",
|
|
OccurrenceBody::Control {
|
|
kind: ControlKind::Choice,
|
|
},
|
|
);
|
|
let c = occurrence(
|
|
&mut p,
|
|
Layer::Oa,
|
|
oa,
|
|
"c",
|
|
OccurrenceBody::Control {
|
|
kind: ControlKind::Choice,
|
|
},
|
|
);
|
|
let b = occurrence(
|
|
&mut p,
|
|
Layer::Oa,
|
|
oa,
|
|
"b",
|
|
OccurrenceBody::Control {
|
|
kind: ControlKind::Choice,
|
|
},
|
|
);
|
|
let ta = occurrence(
|
|
&mut p,
|
|
Layer::Sa,
|
|
sa,
|
|
"a",
|
|
OccurrenceBody::Control {
|
|
kind: ControlKind::Choice,
|
|
},
|
|
);
|
|
let tc = occurrence(
|
|
&mut p,
|
|
Layer::Sa,
|
|
sa,
|
|
"c",
|
|
OccurrenceBody::Control {
|
|
kind: ControlKind::Choice,
|
|
},
|
|
);
|
|
let tb = occurrence(
|
|
&mut p,
|
|
Layer::Sa,
|
|
sa,
|
|
"b",
|
|
OccurrenceBody::Control {
|
|
kind: ControlKind::Choice,
|
|
},
|
|
);
|
|
let extra = occurrence(
|
|
&mut p,
|
|
Layer::Sa,
|
|
sa,
|
|
"local",
|
|
OccurrenceBody::Control {
|
|
kind: ControlKind::Choice,
|
|
},
|
|
);
|
|
*p.elements.get_mut(&oa).unwrap().steps_mut().unwrap() = vec![a, c, b];
|
|
*p.elements.get_mut(&sa).unwrap().steps_mut().unwrap() = vec![ta, extra, tb];
|
|
let t = trace(&mut p, oa, sa);
|
|
let member = FacetSelector::Member {
|
|
collection: ReferenceCollection::Steps,
|
|
keys: KeyPair {
|
|
oa: Some(c),
|
|
sa: Some(tc),
|
|
},
|
|
};
|
|
let mut r = request(oa, sa, vec![(member.clone(), SemanticDecision::TakeSource)]);
|
|
r.references = BTreeMap::from([(a, ta), (b, tb), (c, tc)]);
|
|
assert!(
|
|
!plan_semantic_transition(&p, r.clone())
|
|
.unwrap()
|
|
.conflicts
|
|
.is_empty()
|
|
);
|
|
r.reviews[0].resolutions.push(FacetResolution {
|
|
facet: FacetSelector::Order(OrderedCollection::Steps),
|
|
decision: SemanticDecision::Candidate,
|
|
});
|
|
let mut candidate = p.elements[&sa].clone();
|
|
*candidate.steps_mut().unwrap() = vec![ta, extra, tc, tb];
|
|
r.candidates.push(SemanticRecord::Element(candidate));
|
|
let q = applied(&p, r);
|
|
assert_eq!(q.elements[&sa].steps().unwrap(), &vec![ta, extra, tc, tb]);
|
|
let facts = q.traces[&t]
|
|
.baseline
|
|
.as_ref()
|
|
.unwrap()
|
|
.semantic
|
|
.as_ref()
|
|
.unwrap();
|
|
assert_eq!(facts.facets.len(), 2);
|
|
}
|
|
#[test]
|
|
fn prepared_inherited_annotation_cleanup_must_be_explicitly_accepted() {
|
|
let mut p = Project::empty();
|
|
let a = payload(&mut p, Layer::Oa, "oa");
|
|
let b = payload(&mut p, Layer::Sa, "sa");
|
|
for (layer, part) in [(Layer::Oa, &a), (Layer::Sa, &b)] {
|
|
let parent = record(&mut p, layer, "base", None);
|
|
p.elements.get_mut(&part.leaf).unwrap().parent = Some(parent);
|
|
set(
|
|
&mut p,
|
|
part.record,
|
|
SharedBody::DataType {
|
|
definition: DataTypeDefinition::Record {
|
|
supertype: Some(parent),
|
|
},
|
|
default: None,
|
|
unit: String::new(),
|
|
},
|
|
);
|
|
}
|
|
trace(&mut p, a.record, b.record);
|
|
let mut changed = p.elements[&a.record].clone();
|
|
set_record_supertype(&mut changed, None);
|
|
p = prepare_command(&p, &Command::EditElement(a.record, changed))
|
|
.unwrap()
|
|
.project;
|
|
assert!(values_mut(&mut p, a.message).is_empty());
|
|
assert_eq!(values_mut(&mut p, b.message).len(), 2);
|
|
let mut r = request(
|
|
a.record,
|
|
b.record,
|
|
vec![(
|
|
FacetSelector::Data(DataFacet::RecordSupertype),
|
|
SemanticDecision::TakeSource,
|
|
)],
|
|
);
|
|
let plan = plan_semantic_transition(&p, r.clone()).unwrap();
|
|
assert!(!plan.conflicts.is_empty());
|
|
assert_eq!(plan.impacts.len(), 1);
|
|
assert_eq!(plan.impacts[0].object, b.message);
|
|
assert!(apply_semantic_transition(&p, &plan).is_err());
|
|
r.accepted_impacts = plan.impacts;
|
|
let mut q = applied(&p, r);
|
|
assert!(values_mut(&mut q, b.message).is_empty());
|
|
}
|
|
fn set_record_supertype(e: &mut Element, supertype: Option<Id>) {
|
|
let Some(SharedBody::DataType {
|
|
definition: DataTypeDefinition::Record { supertype: s },
|
|
..
|
|
}) = e.shared_mut()
|
|
else {
|
|
panic!()
|
|
};
|
|
*s = supertype;
|
|
}
|
|
|
|
#[test]
|
|
fn repeated_keep_of_unequal_constructor_pair_cannot_swallow_bound_drift() {
|
|
let mut p = Project::empty();
|
|
let oa = add(&mut p, Kind::DataType, Layer::Oa, "integer");
|
|
let sa = add(&mut p, Kind::DataType, Layer::Sa, "real");
|
|
data(&mut p, oa, 0, 10, "m");
|
|
set(
|
|
&mut p,
|
|
sa,
|
|
SharedBody::DataType {
|
|
definition: DataTypeDefinition::Real {
|
|
minimum: Some(0.),
|
|
maximum: Some(20.),
|
|
},
|
|
default: None,
|
|
unit: "m".into(),
|
|
},
|
|
);
|
|
let t = trace(&mut p, oa, sa);
|
|
let selector = FacetSelector::Data(DataFacet::Constructor);
|
|
p = applied(
|
|
&p,
|
|
request(
|
|
oa,
|
|
sa,
|
|
vec![(selector.clone(), SemanticDecision::KeepTarget)],
|
|
),
|
|
);
|
|
data(&mut p, oa, 0, 12, "m");
|
|
let plan = plan_semantic_transition(
|
|
&p,
|
|
request(
|
|
oa,
|
|
sa,
|
|
vec![(selector.clone(), SemanticDecision::KeepTarget)],
|
|
),
|
|
)
|
|
.unwrap();
|
|
assert!(
|
|
plan.conflicts
|
|
.iter()
|
|
.any(|c| c.contains("No constructor switch"))
|
|
);
|
|
let facts = semantic_facets(&p, &p.traces[&t], &BTreeMap::new()).unwrap();
|
|
assert_eq!(
|
|
facts
|
|
.iter()
|
|
.find(|f| f.selector == FacetSelector::Data(DataFacet::IntegerMaximum))
|
|
.unwrap()
|
|
.drift,
|
|
Drift::OaChanged
|
|
);
|
|
let converted = applied(
|
|
&p,
|
|
request(oa, sa, vec![(selector, SemanticDecision::TakeSource)]),
|
|
);
|
|
assert!(matches!(
|
|
converted.elements[&sa].shared(),
|
|
Some(SharedBody::DataType {
|
|
definition: DataTypeDefinition::Integer {
|
|
maximum: Some(12),
|
|
..
|
|
},
|
|
..
|
|
})
|
|
));
|
|
}
|
|
|
|
fn state_pair(p: &mut Project) -> (Id, Id) {
|
|
let mut pair = vec![];
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
let machine = add(p, Kind::StateMachine, layer, "machine");
|
|
let region = owned(p, Kind::Region, layer, "region", Some(machine));
|
|
pair.push(owned(p, Kind::State, layer, "ready", Some(region)));
|
|
}
|
|
(pair[0], pair[1])
|
|
}
|
|
#[test]
|
|
fn changed_state_kind_remains_reviewable_in_both_directions() {
|
|
let mut p = Project::empty();
|
|
let (oa, sa) = state_pair(&mut p);
|
|
trace(&mut p, oa, sa);
|
|
let Some(SharedBody::State { kind, .. }) = p.elements.get_mut(&oa).unwrap().shared_mut() else {
|
|
panic!()
|
|
};
|
|
*kind = StateKind::Final;
|
|
let selector = FacetSelector::State(StateFacet::Kind);
|
|
let kept = applied(
|
|
&p,
|
|
request(
|
|
oa,
|
|
sa,
|
|
vec![(selector.clone(), SemanticDecision::KeepTarget)],
|
|
),
|
|
);
|
|
assert_eq!(kept.elements[&sa].kind(), Kind::State);
|
|
let taken = applied(
|
|
&p,
|
|
request(
|
|
oa,
|
|
sa,
|
|
vec![(selector.clone(), SemanticDecision::TakeSource)],
|
|
),
|
|
);
|
|
assert_eq!(taken.elements[&sa].kind(), Kind::Final);
|
|
let mut reverse = request(oa, sa, vec![(selector, SemanticDecision::TakeSource)]);
|
|
reverse.transition.direction = TransitionDirection::SaToOa;
|
|
reverse.transition.sources = vec![sa];
|
|
reverse.transition.mappings = vec![TransitionMapping {
|
|
source: sa,
|
|
target: Some(oa),
|
|
target_kind: None,
|
|
}];
|
|
reverse.reviews[0].source = sa;
|
|
let back = applied(&p, reverse);
|
|
assert_eq!(back.elements[&oa].kind(), Kind::State);
|
|
}
|
|
|
|
#[test]
|
|
fn constructor_retirement_cannot_override_other_sources_kept_bound_in_either_order() {
|
|
let mut p = Project::empty();
|
|
let a = add(&mut p, Kind::DataType, Layer::Oa, "real");
|
|
let b = add(&mut p, Kind::DataType, Layer::Oa, "integer");
|
|
let target = add(&mut p, Kind::DataType, Layer::Sa, "real");
|
|
for id in [a, target] {
|
|
set(
|
|
&mut p,
|
|
id,
|
|
SharedBody::DataType {
|
|
definition: DataTypeDefinition::Real {
|
|
minimum: Some(0.),
|
|
maximum: Some(20.),
|
|
},
|
|
default: None,
|
|
unit: String::new(),
|
|
},
|
|
);
|
|
}
|
|
data(&mut p, b, 0, 10, "");
|
|
trace(&mut p, a, target);
|
|
trace(&mut p, b, target);
|
|
let mut r = request(
|
|
a,
|
|
target,
|
|
vec![(
|
|
FacetSelector::Data(DataFacet::RealMaximum),
|
|
SemanticDecision::KeepTarget,
|
|
)],
|
|
);
|
|
r.transition.sources.push(b);
|
|
r.transition.mappings.push(TransitionMapping {
|
|
source: b,
|
|
target: Some(target),
|
|
target_kind: None,
|
|
});
|
|
r.reviews.push(SemanticReviewRequest {
|
|
source: b,
|
|
resolutions: vec![FacetResolution {
|
|
facet: FacetSelector::Data(DataFacet::Constructor),
|
|
decision: SemanticDecision::TakeSource,
|
|
}],
|
|
reset: vec![],
|
|
});
|
|
for reverse in [false, true] {
|
|
let mut r = r.clone();
|
|
if reverse {
|
|
r.reviews.reverse();
|
|
}
|
|
let plan = plan_semantic_transition(&p, r).unwrap();
|
|
assert!(
|
|
plan.conflicts.iter().any(|c| c.contains("Contradictory")),
|
|
"{:?}",
|
|
plan.conflicts
|
|
);
|
|
assert!(apply_semantic_transition(&p, &plan).is_err());
|
|
}
|
|
}
|
|
#[test]
|
|
fn semantic_rollup_does_not_claim_unchanged_when_only_unit_is_reviewed() {
|
|
let mut p = Project::empty();
|
|
let oa = add(&mut p, Kind::DataType, Layer::Oa, "integer");
|
|
let sa = add(&mut p, Kind::DataType, Layer::Sa, "integer");
|
|
data(&mut p, oa, 0, 10, "m");
|
|
data(&mut p, sa, 0, 10, "m");
|
|
let t = trace(&mut p, oa, sa);
|
|
p = applied(
|
|
&p,
|
|
request(
|
|
oa,
|
|
sa,
|
|
vec![(
|
|
FacetSelector::Data(DataFacet::Unit),
|
|
SemanticDecision::KeepTarget,
|
|
)],
|
|
),
|
|
);
|
|
data(&mut p, oa, 0, 12, "m");
|
|
let status = trace_semantic_drift(&p, &p.traces[&t], &BTreeMap::new()).unwrap();
|
|
assert_eq!(status.own, Drift::Unreviewed);
|
|
assert!(status.own_unreviewed);
|
|
}
|
|
fn artifact(p: &mut Project, owner: Id, name: &str) -> Id {
|
|
let id = Id::new_v4();
|
|
p.artifact_links.insert(
|
|
id,
|
|
ArtifactLink {
|
|
id,
|
|
alias: format!("artifact_{}", id.simple()),
|
|
name: name.into(),
|
|
description: String::new(),
|
|
owner,
|
|
relation: ArtifactRelation::References,
|
|
uri: "https://example.test/requirement".into(),
|
|
revision: None,
|
|
fragment: None,
|
|
},
|
|
);
|
|
id
|
|
}
|
|
#[test]
|
|
fn artifact_metadata_is_attached_and_cannot_last_write_through_two_owner_traces() {
|
|
let mut p = Project::empty();
|
|
let a = add(&mut p, Kind::Activity, Layer::Oa, "a");
|
|
let b = add(&mut p, Kind::Activity, Layer::Oa, "b");
|
|
let ta = add(&mut p, Kind::Function, Layer::Sa, "a");
|
|
let tb = add(&mut p, Kind::Function, Layer::Sa, "b");
|
|
trace(&mut p, a, ta);
|
|
trace(&mut p, b, tb);
|
|
let aa = artifact(&mut p, a, "A");
|
|
let ab = artifact(&mut p, b, "B");
|
|
let at = artifact(&mut p, ta, "retained");
|
|
let mut r = request(
|
|
a,
|
|
ta,
|
|
vec![(
|
|
FacetSelector::Artifact {
|
|
keys: KeyPair {
|
|
oa: Some(aa),
|
|
sa: Some(at),
|
|
},
|
|
facet: ArtifactFacet::Name,
|
|
},
|
|
SemanticDecision::TakeSource,
|
|
)],
|
|
);
|
|
r.transition.sources.push(b);
|
|
r.transition.mappings.push(TransitionMapping {
|
|
source: b,
|
|
target: Some(tb),
|
|
target_kind: None,
|
|
});
|
|
r.references = BTreeMap::from([(aa, at), (ab, at)]);
|
|
r.reviews.push(SemanticReviewRequest {
|
|
source: b,
|
|
resolutions: vec![FacetResolution {
|
|
facet: FacetSelector::Artifact {
|
|
keys: KeyPair {
|
|
oa: Some(ab),
|
|
sa: Some(at),
|
|
},
|
|
facet: ArtifactFacet::Name,
|
|
},
|
|
decision: SemanticDecision::TakeSource,
|
|
}],
|
|
reset: vec![],
|
|
});
|
|
for reverse in [false, true] {
|
|
let mut r = r.clone();
|
|
if reverse {
|
|
r.reviews.reverse();
|
|
}
|
|
let plan = plan_semantic_transition(&p, r).unwrap();
|
|
assert!(!plan.conflicts.is_empty());
|
|
assert!(apply_semantic_transition(&p, &plan).is_err());
|
|
}
|
|
let mut same = request(
|
|
a,
|
|
ta,
|
|
vec![(
|
|
FacetSelector::Artifact {
|
|
keys: KeyPair {
|
|
oa: Some(aa),
|
|
sa: Some(at),
|
|
},
|
|
facet: ArtifactFacet::Name,
|
|
},
|
|
SemanticDecision::TakeSource,
|
|
)],
|
|
);
|
|
same.references.insert(aa, at);
|
|
let q = applied(&p, same);
|
|
assert_eq!(q.artifact_links[&at].name, "A");
|
|
}
|
|
|
|
#[test]
|
|
fn child_membership_and_parent_resolutions_agree_by_actual_child_slot() {
|
|
let mut p = Project::empty();
|
|
let owner = add(&mut p, Kind::Activity, Layer::Oa, "owner");
|
|
let child = owned(&mut p, Kind::Activity, Layer::Oa, "child", Some(owner));
|
|
let target = add(&mut p, Kind::Function, Layer::Sa, "owner");
|
|
let target_child = add(&mut p, Kind::Function, Layer::Sa, "child");
|
|
let other = add(&mut p, Kind::Function, Layer::Sa, "other");
|
|
trace(&mut p, owner, target);
|
|
trace(&mut p, child, target_child);
|
|
let mut r = request(
|
|
owner,
|
|
target,
|
|
vec![(
|
|
FacetSelector::Child(KeyPair {
|
|
oa: Some(child),
|
|
sa: Some(target_child),
|
|
}),
|
|
SemanticDecision::TakeSource,
|
|
)],
|
|
);
|
|
r.transition.sources.push(child);
|
|
r.transition.mappings.push(TransitionMapping {
|
|
source: child,
|
|
target: Some(target_child),
|
|
target_kind: None,
|
|
});
|
|
r.reviews.push(SemanticReviewRequest {
|
|
source: child,
|
|
resolutions: vec![FacetResolution {
|
|
facet: FacetSelector::Header(HeaderFacet::Parent),
|
|
decision: SemanticDecision::TakeSource,
|
|
}],
|
|
reset: vec![],
|
|
});
|
|
let q = applied(&p, r.clone());
|
|
assert_eq!(q.elements[&target_child].parent, Some(target));
|
|
r.reviews[1].resolutions[0].decision = SemanticDecision::Candidate;
|
|
let mut candidate = p.elements[&target_child].clone();
|
|
candidate.parent = Some(other);
|
|
r.candidates.push(SemanticRecord::Element(candidate));
|
|
for reverse in [false, true] {
|
|
let mut r = r.clone();
|
|
if reverse {
|
|
r.reviews.reverse();
|
|
}
|
|
let plan = plan_semantic_transition(&p, r).unwrap();
|
|
assert!(
|
|
plan.conflicts.iter().any(|c| c.contains("Contradictory")),
|
|
"{:?}",
|
|
plan.conflicts
|
|
);
|
|
}
|
|
}
|
|
#[test]
|
|
fn state_guard_and_deleted_trigger_membership_preserve_target_only_trigger() {
|
|
let mut p = Project::empty();
|
|
let (oa, sa) = state_pair(&mut p);
|
|
let a = p.relate(RelationKind::StateTransition, "self", oa, oa);
|
|
let b = p.relate(RelationKind::StateTransition, "self", sa, sa);
|
|
let old = add(&mut p, Kind::Event, Layer::Oa, "old");
|
|
let retained = add(&mut p, Kind::Event, Layer::Sa, "old");
|
|
let local = add(&mut p, Kind::Event, Layer::Sa, "local");
|
|
for (id, triggers) in [(a, vec![old]), (b, vec![retained, local])] {
|
|
let RelationBody::StateTransition {
|
|
triggers: t, guard, ..
|
|
} = &mut p.relations.get_mut(&id).unwrap().body
|
|
else {
|
|
panic!()
|
|
};
|
|
*t = triggers;
|
|
*guard = "old".into();
|
|
}
|
|
let t = trace(&mut p, a, b);
|
|
let selector = FacetSelector::Member {
|
|
collection: ReferenceCollection::TransitionTriggers,
|
|
keys: KeyPair {
|
|
oa: Some(old),
|
|
sa: Some(retained),
|
|
},
|
|
};
|
|
let mut r = request(a, b, vec![(selector.clone(), SemanticDecision::KeepTarget)]);
|
|
r.references.insert(old, retained);
|
|
p = applied(&p, r.clone());
|
|
p = prepare_command(&p, &Command::Delete(old)).unwrap().project;
|
|
let RelationBody::StateTransition { guard, .. } = &mut p.relations.get_mut(&a).unwrap().body
|
|
else {
|
|
panic!()
|
|
};
|
|
*guard = "ready".into();
|
|
r.reviews[0].resolutions = vec![
|
|
FacetResolution {
|
|
facet: selector,
|
|
decision: SemanticDecision::TakeSource,
|
|
},
|
|
FacetResolution {
|
|
facet: FacetSelector::Relation(RelationFacet::Guard),
|
|
decision: SemanticDecision::TakeSource,
|
|
},
|
|
];
|
|
let q = applied(&p, r);
|
|
assert!(
|
|
matches!(&q.relations[&b].body,RelationBody::StateTransition{guard,triggers,..} if guard=="ready"&&triggers==&vec![local])
|
|
);
|
|
assert!(q.elements.contains_key(&retained));
|
|
assert!(q.traces[&t].baseline.as_ref().unwrap().semantic.is_some());
|
|
}
|
|
#[test]
|
|
fn artifact_creation_requires_reviewed_metadata_and_deletion_retires_exact_facts() {
|
|
let mut p = Project::empty();
|
|
let oa = add(&mut p, Kind::Activity, Layer::Oa, "oa");
|
|
let sa = add(&mut p, Kind::Function, Layer::Sa, "sa");
|
|
let t = trace(&mut p, oa, sa);
|
|
let a = artifact(&mut p, oa, "requirement");
|
|
let b = Id::new_v4();
|
|
let keys = KeyPair {
|
|
oa: Some(a),
|
|
sa: Some(b),
|
|
};
|
|
let mut candidate = p.artifact_links[&a].clone();
|
|
candidate.id = b;
|
|
candidate.alias = format!("artifact_{}", b.simple());
|
|
candidate.owner = sa;
|
|
let mut r = request(
|
|
oa,
|
|
sa,
|
|
vec![(
|
|
FacetSelector::Artifact {
|
|
keys: keys.clone(),
|
|
facet: ArtifactFacet::Presence,
|
|
},
|
|
SemanticDecision::TakeSource,
|
|
)],
|
|
);
|
|
r.references.insert(a, b);
|
|
r.candidates
|
|
.push(SemanticRecord::Artifact(candidate.clone()));
|
|
assert!(
|
|
!plan_semantic_transition(&p, r.clone())
|
|
.unwrap()
|
|
.conflicts
|
|
.is_empty()
|
|
);
|
|
for facet in [
|
|
ArtifactFacet::Name,
|
|
ArtifactFacet::Description,
|
|
ArtifactFacet::Owner,
|
|
ArtifactFacet::Relation,
|
|
ArtifactFacet::Uri,
|
|
ArtifactFacet::Revision,
|
|
ArtifactFacet::Fragment,
|
|
] {
|
|
r.reviews[0].resolutions.push(FacetResolution {
|
|
facet: FacetSelector::Artifact {
|
|
keys: keys.clone(),
|
|
facet,
|
|
},
|
|
decision: SemanticDecision::Candidate,
|
|
});
|
|
}
|
|
p = applied(&p, r);
|
|
assert_eq!(p.artifact_links[&b], candidate);
|
|
assert_eq!(
|
|
p.traces[&t]
|
|
.baseline
|
|
.as_ref()
|
|
.unwrap()
|
|
.semantic
|
|
.as_ref()
|
|
.unwrap()
|
|
.facets
|
|
.len(),
|
|
8
|
|
);
|
|
p = prepare_command(&p, &Command::Delete(a)).unwrap().project;
|
|
let mut removal = request(
|
|
oa,
|
|
sa,
|
|
vec![(
|
|
FacetSelector::Artifact {
|
|
keys,
|
|
facet: ArtifactFacet::Presence,
|
|
},
|
|
SemanticDecision::TakeSource,
|
|
)],
|
|
);
|
|
removal.references.insert(a, b);
|
|
let plan = plan_semantic_transition(&p, removal).unwrap();
|
|
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
|
assert_eq!(plan.reviews[0].retired.len(), 7);
|
|
let q = apply_semantic_transition(&p, &plan).unwrap();
|
|
assert!(!q.artifact_links.contains_key(&b));
|
|
assert_eq!(
|
|
q.traces[&t]
|
|
.baseline
|
|
.as_ref()
|
|
.unwrap()
|
|
.semantic
|
|
.as_ref()
|
|
.unwrap()
|
|
.facets
|
|
.len(),
|
|
1
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn semantic_batch_is_one_undo_and_replanning_acknowledged_values_is_noop() {
|
|
let mut p = Project::empty();
|
|
let oa = add(&mut p, Kind::DataType, Layer::Oa, "source");
|
|
let sa = add(&mut p, Kind::DataType, Layer::Sa, "target");
|
|
data(&mut p, oa, 0, 12, "m");
|
|
data(&mut p, sa, 0, 10, "ft");
|
|
trace(&mut p, oa, sa);
|
|
let r = request(
|
|
oa,
|
|
sa,
|
|
vec![
|
|
(
|
|
FacetSelector::Header(HeaderFacet::Name),
|
|
SemanticDecision::TakeSource,
|
|
),
|
|
(
|
|
FacetSelector::Data(DataFacet::IntegerMaximum),
|
|
SemanticDecision::TakeSource,
|
|
),
|
|
(
|
|
FacetSelector::Data(DataFacet::Unit),
|
|
SemanticDecision::KeepTarget,
|
|
),
|
|
],
|
|
);
|
|
let plan = plan_semantic_transition(&p, r.clone()).unwrap();
|
|
assert!(plan.conflicts.is_empty());
|
|
let mut session = Session::new(p.clone());
|
|
session.execute(Command::Batch(plan.commands)).unwrap();
|
|
let accepted = session.project().clone();
|
|
assert_eq!(session.revision(), 1);
|
|
session.undo();
|
|
assert_eq!(session.project(), &p);
|
|
session.redo();
|
|
assert_eq!(session.project(), &accepted);
|
|
let repeated = plan_semantic_transition(&accepted, r).unwrap();
|
|
assert!(repeated.conflicts.is_empty());
|
|
assert_eq!(
|
|
apply_semantic_transition(&accepted, &repeated).unwrap(),
|
|
accepted
|
|
);
|
|
let mut invalid = request(
|
|
oa,
|
|
sa,
|
|
vec![(
|
|
FacetSelector::Data(DataFacet::IntegerMinimum),
|
|
SemanticDecision::Candidate,
|
|
)],
|
|
);
|
|
let mut candidate = accepted.elements[&sa].clone();
|
|
let Some(SharedBody::DataType {
|
|
definition: DataTypeDefinition::Integer { minimum, .. },
|
|
..
|
|
}) = candidate.shared_mut()
|
|
else {
|
|
panic!()
|
|
};
|
|
*minimum = Some(100);
|
|
invalid.candidates.push(SemanticRecord::Element(candidate));
|
|
let rejected = plan_semantic_transition(&accepted, invalid).unwrap();
|
|
assert!(!rejected.conflicts.is_empty());
|
|
assert!(apply_semantic_transition(&accepted, &rejected).is_err());
|
|
assert_eq!(session.project(), &accepted);
|
|
}
|
|
#[test]
|
|
fn explicit_trace_selection_keeps_independent_trace_baselines() {
|
|
let mut p = Project::empty();
|
|
let oa = add(&mut p, Kind::Activity, Layer::Oa, "source");
|
|
let sa = add(&mut p, Kind::Function, Layer::Sa, "target");
|
|
let first = trace(&mut p, oa, sa);
|
|
let second = Id::new_v4();
|
|
let mut extra = p.traces[&first].clone();
|
|
extra.id = second;
|
|
extra.alias = format!("trace_{}", second.simple());
|
|
extra.kind = TraceKind::Refinement;
|
|
extra.baseline = None;
|
|
p.traces.insert(second, extra);
|
|
let mut r = request(
|
|
oa,
|
|
sa,
|
|
vec![(
|
|
FacetSelector::Header(HeaderFacet::Name),
|
|
SemanticDecision::KeepTarget,
|
|
)],
|
|
);
|
|
assert!(
|
|
!plan_semantic_transition(&p, r.clone())
|
|
.unwrap()
|
|
.conflicts
|
|
.is_empty()
|
|
);
|
|
r.traces.insert(oa, second);
|
|
let q = applied(&p, r);
|
|
assert!(
|
|
q.traces[&first]
|
|
.baseline
|
|
.as_ref()
|
|
.unwrap()
|
|
.semantic
|
|
.is_none()
|
|
);
|
|
assert_eq!(
|
|
q.traces[&second]
|
|
.baseline
|
|
.as_ref()
|
|
.unwrap()
|
|
.semantic
|
|
.as_ref()
|
|
.unwrap()
|
|
.facets
|
|
.len(),
|
|
1
|
|
);
|
|
}
|
|
|
|
fn enumeration(p: &mut Project, id: Id, literals: &[&str]) {
|
|
set(
|
|
p,
|
|
id,
|
|
SharedBody::DataType {
|
|
definition: DataTypeDefinition::Enumeration {
|
|
literals: literals.iter().map(|s| s.to_string()).collect(),
|
|
},
|
|
default: None,
|
|
unit: String::new(),
|
|
},
|
|
);
|
|
}
|
|
#[test]
|
|
fn constructor_union_conflicts_with_another_sources_new_member_in_either_order() {
|
|
let mut p = Project::empty();
|
|
let a = add(&mut p, Kind::DataType, Layer::Oa, "enumeration");
|
|
let b = add(&mut p, Kind::DataType, Layer::Oa, "boolean");
|
|
let target = add(&mut p, Kind::DataType, Layer::Sa, "enumeration");
|
|
enumeration(&mut p, a, &["New"]);
|
|
enumeration(&mut p, target, &[]);
|
|
trace(&mut p, a, target);
|
|
trace(&mut p, b, target);
|
|
let mut r = request(
|
|
a,
|
|
target,
|
|
vec![(
|
|
FacetSelector::Literal(KeyPair {
|
|
oa: Some("New".into()),
|
|
sa: Some("New".into()),
|
|
}),
|
|
SemanticDecision::TakeSource,
|
|
)],
|
|
);
|
|
r.transition.sources.push(b);
|
|
r.transition.mappings.push(TransitionMapping {
|
|
source: b,
|
|
target: Some(target),
|
|
target_kind: None,
|
|
});
|
|
r.reviews.push(SemanticReviewRequest {
|
|
source: b,
|
|
resolutions: vec![FacetResolution {
|
|
facet: FacetSelector::Data(DataFacet::Constructor),
|
|
decision: SemanticDecision::TakeSource,
|
|
}],
|
|
reset: vec![],
|
|
});
|
|
for reverse in [false, true] {
|
|
let mut r = r.clone();
|
|
if reverse {
|
|
r.reviews.reverse();
|
|
}
|
|
let plan = plan_semantic_transition(&p, r).unwrap();
|
|
assert!(
|
|
plan.conflicts.iter().any(|c| c.contains("Contradictory")),
|
|
"{:?}",
|
|
plan.conflicts
|
|
);
|
|
}
|
|
}
|
|
#[test]
|
|
fn same_target_constructor_preserves_local_members_and_reset_precedes_expansion() {
|
|
for ready in [false, true] {
|
|
let mut p = Project::empty();
|
|
let oa = add(&mut p, Kind::DataType, Layer::Oa, "integer");
|
|
let sa = add(&mut p, Kind::DataType, Layer::Sa, "enumeration");
|
|
data(&mut p, oa, 0, 10, "");
|
|
enumeration(
|
|
&mut p,
|
|
sa,
|
|
if ready {
|
|
&["Ready", "Local"]
|
|
} else {
|
|
&["Local"]
|
|
},
|
|
);
|
|
let t = trace(&mut p, oa, sa);
|
|
let constructor = FacetSelector::Data(DataFacet::Constructor);
|
|
p = applied(
|
|
&p,
|
|
request(
|
|
oa,
|
|
sa,
|
|
vec![(constructor.clone(), SemanticDecision::KeepTarget)],
|
|
),
|
|
);
|
|
enumeration(&mut p, oa, if ready { &["Ready"] } else { &["New"] });
|
|
let mut r = request(oa, sa, vec![(constructor, SemanticDecision::TakeSource)]);
|
|
if ready {
|
|
r.reviews[0].reset.push(FacetSelector::Literal(KeyPair {
|
|
oa: None,
|
|
sa: Some("Ready".into()),
|
|
}));
|
|
}
|
|
let q = applied(&p, r);
|
|
assert!(
|
|
matches!(q.elements[&sa].shared(),Some(SharedBody::DataType{definition:DataTypeDefinition::Enumeration{literals},..}) if literals.contains(&"Local".to_string()))
|
|
);
|
|
if ready {
|
|
let base = q.traces[&t]
|
|
.baseline
|
|
.as_ref()
|
|
.unwrap()
|
|
.semantic
|
|
.as_ref()
|
|
.unwrap();
|
|
assert_eq!(base.facets.iter().filter(|f|matches!(f.selector(),FacetSelector::Literal(KeyPair{sa:Some(ref value),..})if value=="Ready")).count(),1);
|
|
}
|
|
}
|
|
}
|
|
#[test]
|
|
fn artifact_owner_candidate_moves_link_and_conflicts_with_kept_attachment() {
|
|
let mut p = Project::empty();
|
|
let oa = add(&mut p, Kind::Activity, Layer::Oa, "oa");
|
|
let sa = add(&mut p, Kind::Function, Layer::Sa, "sa");
|
|
let other = add(&mut p, Kind::Function, Layer::Sa, "other");
|
|
trace(&mut p, oa, sa);
|
|
let a = artifact(&mut p, oa, "source");
|
|
let b = artifact(&mut p, sa, "target");
|
|
let keys = KeyPair {
|
|
oa: Some(a),
|
|
sa: Some(b),
|
|
};
|
|
let mut r = request(
|
|
oa,
|
|
sa,
|
|
vec![(
|
|
FacetSelector::Artifact {
|
|
keys: keys.clone(),
|
|
facet: ArtifactFacet::Owner,
|
|
},
|
|
SemanticDecision::Candidate,
|
|
)],
|
|
);
|
|
r.references.insert(a, b);
|
|
let mut candidate = p.artifact_links[&b].clone();
|
|
candidate.owner = other;
|
|
r.candidates.push(SemanticRecord::Artifact(candidate));
|
|
let q = applied(&p, r.clone());
|
|
assert_eq!(q.artifact_links[&b].owner, other);
|
|
r.reviews[0].resolutions.push(FacetResolution {
|
|
facet: FacetSelector::Artifact {
|
|
keys,
|
|
facet: ArtifactFacet::Presence,
|
|
},
|
|
decision: SemanticDecision::KeepTarget,
|
|
});
|
|
let plan = plan_semantic_transition(&p, r).unwrap();
|
|
assert!(
|
|
plan.conflicts.iter().any(|c| c.contains("Contradictory")),
|
|
"{:?}",
|
|
plan.conflicts
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn shared_definition_drift_is_linked_without_copying_definition_into_field_baseline() {
|
|
let mut p = Project::empty();
|
|
let a = add(&mut p, Kind::DataType, Layer::Oa, "type");
|
|
let b = add(&mut p, Kind::DataType, Layer::Sa, "type");
|
|
data(&mut p, a, 0, 10, "m");
|
|
data(&mut p, b, 0, 10, "m");
|
|
trace(&mut p, a, b);
|
|
let oa_owner = record(&mut p, Layer::Oa, "record", None);
|
|
let sa_owner = record(&mut p, Layer::Sa, "record", None);
|
|
let oa = field(&mut p, Layer::Oa, oa_owner, "value", TypeRef::Named(a), 1);
|
|
let sa = field(&mut p, Layer::Sa, sa_owner, "value", TypeRef::Named(b), 1);
|
|
let t = trace(&mut p, oa, sa);
|
|
p = applied(
|
|
&p,
|
|
request(
|
|
a,
|
|
b,
|
|
vec![(
|
|
FacetSelector::Data(DataFacet::IntegerMaximum),
|
|
SemanticDecision::KeepTarget,
|
|
)],
|
|
),
|
|
);
|
|
let mut r = request(
|
|
oa,
|
|
sa,
|
|
vec![(
|
|
FacetSelector::Field(FieldFacet::Type),
|
|
SemanticDecision::KeepTarget,
|
|
)],
|
|
);
|
|
r.references.insert(a, b);
|
|
p = applied(&p, r);
|
|
data(&mut p, a, 0, 12, "m");
|
|
let impacts = linked_semantic_impacts(&p, &p.traces[&t], &BTreeMap::from([(a, b)])).unwrap();
|
|
assert!(impacts.iter().any(|impact| impact.objects
|
|
== KeyPair {
|
|
oa: Some(a),
|
|
sa: Some(b)
|
|
}
|
|
&& impact.drift == Drift::OaChanged));
|
|
let base = p.traces[&t]
|
|
.baseline
|
|
.as_ref()
|
|
.unwrap()
|
|
.semantic
|
|
.as_ref()
|
|
.unwrap();
|
|
assert_eq!(base.facets.len(), 1);
|
|
assert_eq!(
|
|
base.facets[0].selector(),
|
|
FacetSelector::Field(FieldFacet::Type)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn combined_ordered_membership_commutes_or_requires_agreed_anchors() {
|
|
for insertion in [false, true] {
|
|
let mut p = Project::empty();
|
|
let a = add(&mut p, Kind::Process, Layer::Oa, "first");
|
|
let b = add(&mut p, Kind::Process, Layer::Oa, "second");
|
|
let target = add(&mut p, Kind::FunctionalChain, Layer::Sa, "target");
|
|
let mut rows = Vec::new();
|
|
for (owner, layer) in [(a, Layer::Oa), (b, Layer::Oa), (target, Layer::Sa)] {
|
|
rows.push(["left", "right", "middle"].map(|name| {
|
|
occurrence(
|
|
&mut p,
|
|
layer,
|
|
owner,
|
|
name,
|
|
OccurrenceBody::Control {
|
|
kind: ControlKind::Choice,
|
|
},
|
|
)
|
|
}));
|
|
}
|
|
let [left, right, middle] = rows[2];
|
|
*p.elements.get_mut(&a).unwrap().steps_mut().unwrap() = if insertion {
|
|
vec![rows[0][0], rows[0][2], rows[0][1]]
|
|
} else {
|
|
vec![]
|
|
};
|
|
*p.elements.get_mut(&b).unwrap().steps_mut().unwrap() = if insertion {
|
|
vec![rows[1][2], rows[1][0], rows[1][1]]
|
|
} else {
|
|
vec![]
|
|
};
|
|
*p.elements.get_mut(&target).unwrap().steps_mut().unwrap() = if insertion {
|
|
vec![left, right]
|
|
} else {
|
|
vec![left, middle, right]
|
|
};
|
|
trace(&mut p, a, target);
|
|
trace(&mut p, b, target);
|
|
let facet = |index: usize| FacetSelector::Member {
|
|
collection: ReferenceCollection::Steps,
|
|
keys: KeyPair {
|
|
oa: Some(rows[index][if insertion { 2 } else { index }]),
|
|
sa: Some(rows[2][if insertion { 2 } else { index }]),
|
|
},
|
|
};
|
|
let mut r = request(a, target, vec![(facet(0), SemanticDecision::TakeSource)]);
|
|
r.transition.sources.push(b);
|
|
r.transition.mappings.push(TransitionMapping {
|
|
source: b,
|
|
target: Some(target),
|
|
target_kind: None,
|
|
});
|
|
r.reviews.push(SemanticReviewRequest {
|
|
source: b,
|
|
resolutions: vec![FacetResolution {
|
|
facet: facet(1),
|
|
decision: SemanticDecision::TakeSource,
|
|
}],
|
|
reset: vec![],
|
|
});
|
|
for row in &rows[..2] {
|
|
for index in 0..3 {
|
|
r.references.insert(row[index], rows[2][index]);
|
|
}
|
|
}
|
|
for reversed in [false, true] {
|
|
let mut r = r.clone();
|
|
if reversed {
|
|
r.reviews.reverse();
|
|
}
|
|
let plan = plan_semantic_transition(&p, r).unwrap();
|
|
if insertion {
|
|
assert!(
|
|
plan.conflicts
|
|
.iter()
|
|
.any(|c| c.contains("Contradictory anchored orders")),
|
|
"{:?}",
|
|
plan.conflicts
|
|
);
|
|
} else {
|
|
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
|
let q = apply_semantic_transition(&p, &plan).unwrap();
|
|
assert_eq!(q.elements[&target].steps().unwrap(), &vec![middle]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn target_only_artifact_move_records_owner_and_metadata_in_either_order() {
|
|
for rename in [false, true] {
|
|
for reversed in [false, true] {
|
|
let mut p = Project::empty();
|
|
let oa = add(&mut p, Kind::Activity, Layer::Oa, "oa");
|
|
let sa = add(&mut p, Kind::Function, Layer::Sa, "sa");
|
|
let other = add(&mut p, Kind::Function, Layer::Sa, "other");
|
|
let t = trace(&mut p, oa, sa);
|
|
let b = artifact(&mut p, sa, "local");
|
|
let keys = KeyPair {
|
|
oa: None,
|
|
sa: Some(b),
|
|
};
|
|
let owner = FacetSelector::Artifact {
|
|
keys: keys.clone(),
|
|
facet: ArtifactFacet::Owner,
|
|
};
|
|
let name = FacetSelector::Artifact {
|
|
keys: keys.clone(),
|
|
facet: ArtifactFacet::Name,
|
|
};
|
|
let mut r = request(oa, sa, vec![(owner.clone(), SemanticDecision::Candidate)]);
|
|
let mut candidate = p.artifact_links[&b].clone();
|
|
candidate.owner = other;
|
|
if rename {
|
|
candidate.name = "moved local".into();
|
|
r.reviews[0].resolutions.push(FacetResolution {
|
|
facet: name.clone(),
|
|
decision: SemanticDecision::Candidate,
|
|
});
|
|
}
|
|
if reversed {
|
|
r.reviews[0].resolutions.reverse();
|
|
}
|
|
r.candidates
|
|
.push(SemanticRecord::Artifact(candidate.clone()));
|
|
let q = applied(&p, r);
|
|
assert_eq!(q.artifact_links[&b], candidate);
|
|
let baseline = q.traces[&t]
|
|
.baseline
|
|
.as_ref()
|
|
.unwrap()
|
|
.semantic
|
|
.as_ref()
|
|
.unwrap();
|
|
assert!(baseline.facets.iter().any(|f| matches!(f, FacetReview::Artifact { review: ArtifactReview::Owner(Reviewed { oa: Observed::NotApplicable, sa: Observed::Value(id) }), .. } if *id == other)));
|
|
assert_eq!(baseline.facets.len(), if rename { 2 } else { 1 });
|
|
let invalid = plan_semantic_transition(
|
|
&q,
|
|
request(oa, sa, vec![(name, SemanticDecision::KeepTarget)]),
|
|
)
|
|
.unwrap();
|
|
assert!(
|
|
invalid
|
|
.conflicts
|
|
.iter()
|
|
.any(|c| c.contains("initially attached owner")),
|
|
"{:?}",
|
|
invalid.conflicts
|
|
);
|
|
assert!(
|
|
validation::validate(&q)
|
|
.iter()
|
|
.all(|issue| issue.severity != validation::Severity::Error)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn retired_source_branch_retains_historical_member_pair_in_both_directions() {
|
|
for reverse in [false, true] {
|
|
let mut p = Project::empty();
|
|
let oa = add(&mut p, Kind::DataType, Layer::Oa, "oa");
|
|
let sa = add(&mut p, Kind::DataType, Layer::Sa, "sa");
|
|
enumeration(&mut p, oa, &["X"]);
|
|
enumeration(&mut p, sa, &["X"]);
|
|
let t = trace(&mut p, oa, sa);
|
|
let literal = FacetSelector::Literal(KeyPair {
|
|
oa: Some("X".into()),
|
|
sa: Some("X".into()),
|
|
});
|
|
p = applied(
|
|
&p,
|
|
request(
|
|
oa,
|
|
sa,
|
|
vec![(literal.clone(), SemanticDecision::KeepTarget)],
|
|
),
|
|
);
|
|
let (source, target) = if reverse { (sa, oa) } else { (oa, sa) };
|
|
data(&mut p, source, 0, 10, "");
|
|
let mut r = request(
|
|
source,
|
|
target,
|
|
vec![(
|
|
FacetSelector::Data(DataFacet::Constructor),
|
|
SemanticDecision::KeepTarget,
|
|
)],
|
|
);
|
|
if reverse {
|
|
r.transition.direction = TransitionDirection::SaToOa;
|
|
}
|
|
let q = applied(&p, r);
|
|
let facts = semantic_facets(&q, &q.traces[&t], &BTreeMap::new()).unwrap();
|
|
let literals = facts
|
|
.iter()
|
|
.filter(|fact| matches!(fact.selector, FacetSelector::Literal(_)))
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(literals.len(), 1);
|
|
assert_eq!(literals[0].selector, literal);
|
|
assert_eq!(literals[0].drift, Drift::Unchanged);
|
|
let FacetReview::Literal { review, .. } = &literals[0].current else {
|
|
panic!()
|
|
};
|
|
assert_eq!(
|
|
if reverse { &review.sa } else { &review.oa },
|
|
&Observed::NotApplicable
|
|
);
|
|
assert_eq!(
|
|
if reverse { &review.oa } else { &review.sa },
|
|
&Observed::Value(true)
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn constructor_closure_preserves_target_only_literals_in_both_directions() {
|
|
for reverse in [false, true] {
|
|
let mut p = Project::empty();
|
|
let oa = add(&mut p, Kind::DataType, Layer::Oa, "oa");
|
|
let sa = add(&mut p, Kind::DataType, Layer::Sa, "sa");
|
|
let (source, target) = if reverse { (sa, oa) } else { (oa, sa) };
|
|
enumeration(&mut p, target, &["local"]);
|
|
let t = trace(&mut p, oa, sa);
|
|
let facts = semantic_facets(&p, &p.traces[&t], &BTreeMap::new()).unwrap();
|
|
p.traces
|
|
.get_mut(&t)
|
|
.unwrap()
|
|
.baseline
|
|
.as_mut()
|
|
.unwrap()
|
|
.semantic = Some(SemanticBaseline {
|
|
facets: facts.into_iter().map(|f| f.current).collect(),
|
|
});
|
|
enumeration(&mut p, source, &["incoming"]);
|
|
let mut r = request(
|
|
source,
|
|
target,
|
|
vec![(
|
|
FacetSelector::Data(DataFacet::Constructor),
|
|
SemanticDecision::TakeSource,
|
|
)],
|
|
);
|
|
if reverse {
|
|
r.transition.direction = TransitionDirection::SaToOa;
|
|
}
|
|
let q = applied(&p, r);
|
|
assert!(
|
|
matches!(q.elements[&target].shared(), Some(SharedBody::DataType {
|
|
definition: DataTypeDefinition::Enumeration { literals }, ..
|
|
}) if literals == &["incoming", "local"]),
|
|
"reverse={reverse}"
|
|
);
|
|
let base = q.traces[&t]
|
|
.baseline
|
|
.as_ref()
|
|
.unwrap()
|
|
.semantic
|
|
.as_ref()
|
|
.unwrap();
|
|
validate_semantic_baseline(base).unwrap();
|
|
let incoming = FacetSelector::Literal(KeyPair {
|
|
oa: Some("incoming".into()),
|
|
sa: Some("incoming".into()),
|
|
});
|
|
assert_eq!(
|
|
base.facets
|
|
.iter()
|
|
.filter(|f| f.selector() == incoming)
|
|
.count(),
|
|
1
|
|
);
|
|
let facts = semantic_facets(&q, &q.traces[&t], &BTreeMap::new()).unwrap();
|
|
assert!(
|
|
facts
|
|
.iter()
|
|
.filter(|f| matches!(f.selector, FacetSelector::Literal(_)))
|
|
.all(|f| f.drift == Drift::Unchanged)
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn constructor_closure_pairs_signal_members_and_requires_history_reset_in_both_directions() {
|
|
for reverse in [false, true] {
|
|
for historical in [false, true] {
|
|
let mut p = Project::empty();
|
|
let oa = add(&mut p, Kind::Event, Layer::Oa, "oa");
|
|
let sa = add(&mut p, Kind::Event, Layer::Sa, "sa");
|
|
let oa_item = add(&mut p, Kind::ExchangeItem, Layer::Oa, "oa_item");
|
|
let sa_item = add(&mut p, Kind::ExchangeItem, Layer::Sa, "sa_item");
|
|
let (source, target, source_item) = if reverse {
|
|
(sa, oa, sa_item)
|
|
} else {
|
|
(oa, sa, oa_item)
|
|
};
|
|
set(
|
|
&mut p,
|
|
source,
|
|
SharedBody::Event {
|
|
body: EventBody::Signal {
|
|
items: vec![source_item],
|
|
},
|
|
},
|
|
);
|
|
set(
|
|
&mut p,
|
|
target,
|
|
SharedBody::Event {
|
|
body: EventBody::Change {
|
|
expression: "old".into(),
|
|
},
|
|
},
|
|
);
|
|
let t = trace(&mut p, oa, sa);
|
|
let previous = FacetSelector::Member {
|
|
collection: ReferenceCollection::SignalItems,
|
|
keys: if reverse {
|
|
KeyPair {
|
|
oa: None,
|
|
sa: Some(sa_item),
|
|
}
|
|
} else {
|
|
KeyPair {
|
|
oa: Some(oa_item),
|
|
sa: None,
|
|
}
|
|
},
|
|
};
|
|
if historical {
|
|
let facts = semantic_facets(&p, &p.traces[&t], &BTreeMap::new()).unwrap();
|
|
let old = facts
|
|
.into_iter()
|
|
.find(|f| f.selector == previous)
|
|
.unwrap()
|
|
.current;
|
|
p.traces
|
|
.get_mut(&t)
|
|
.unwrap()
|
|
.baseline
|
|
.as_mut()
|
|
.unwrap()
|
|
.semantic = Some(SemanticBaseline { facets: vec![old] });
|
|
}
|
|
let mut r = request(
|
|
source,
|
|
target,
|
|
vec![(
|
|
FacetSelector::Event(EventFacet::Constructor),
|
|
SemanticDecision::TakeSource,
|
|
)],
|
|
);
|
|
if reverse {
|
|
r.transition.direction = TransitionDirection::SaToOa;
|
|
}
|
|
r.references.insert(oa_item, sa_item);
|
|
if historical {
|
|
let invalid = plan_semantic_transition(&p, r.clone()).unwrap();
|
|
assert!(
|
|
!invalid.conflicts.is_empty(),
|
|
"Established one-sided history requires reset: reverse={reverse}"
|
|
);
|
|
r.reviews[0].reset.push(previous);
|
|
}
|
|
let q = applied(&p, r);
|
|
let target_item = if reverse { oa_item } else { sa_item };
|
|
assert!(
|
|
matches!(q.elements[&target].shared(), Some(SharedBody::Event { body: EventBody::Signal { items } }) if items == &[target_item])
|
|
);
|
|
let base = q.traces[&t]
|
|
.baseline
|
|
.as_ref()
|
|
.unwrap()
|
|
.semantic
|
|
.as_ref()
|
|
.unwrap();
|
|
validate_semantic_baseline(base).unwrap();
|
|
let members: Vec<_> = base
|
|
.facets
|
|
.iter()
|
|
.filter(|f| {
|
|
matches!(
|
|
f.selector(),
|
|
FacetSelector::Member {
|
|
collection: ReferenceCollection::SignalItems,
|
|
..
|
|
}
|
|
)
|
|
})
|
|
.collect();
|
|
assert_eq!(
|
|
members.len(),
|
|
1,
|
|
"reverse={reverse}, historical={historical}"
|
|
);
|
|
assert_eq!(
|
|
members[0].selector(),
|
|
FacetSelector::Member {
|
|
collection: ReferenceCollection::SignalItems,
|
|
keys: KeyPair {
|
|
oa: Some(oa_item),
|
|
sa: Some(sa_item)
|
|
}
|
|
}
|
|
);
|
|
assert!(
|
|
validation::validate(&q)
|
|
.iter()
|
|
.all(|issue| issue.severity != validation::Severity::Error)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn constructor_closure_does_not_restore_previously_corresponding_removed_members() {
|
|
for reverse in [false, true] {
|
|
let mut p = Project::empty();
|
|
let oa = add(&mut p, Kind::DataType, Layer::Oa, "oa");
|
|
let sa = add(&mut p, Kind::DataType, Layer::Sa, "sa");
|
|
let (source, target) = if reverse { (sa, oa) } else { (oa, sa) };
|
|
enumeration(&mut p, source, &["paired"]);
|
|
let t = trace(&mut p, oa, sa);
|
|
let constructor = FacetSelector::Data(DataFacet::Constructor);
|
|
let directed = |facets| {
|
|
let mut r = request(source, target, facets);
|
|
if reverse {
|
|
r.transition.direction = TransitionDirection::SaToOa;
|
|
}
|
|
r
|
|
};
|
|
p = applied(
|
|
&p,
|
|
directed(vec![(constructor.clone(), SemanticDecision::KeepTarget)]),
|
|
);
|
|
if reverse {
|
|
// Explicitly establish the counterpart key while its target branch
|
|
// is absent, rather than silently changing the one-sided history.
|
|
let mut r = directed(vec![(
|
|
FacetSelector::Literal(KeyPair {
|
|
oa: Some("paired".into()),
|
|
sa: Some("paired".into()),
|
|
}),
|
|
SemanticDecision::KeepTarget,
|
|
)]);
|
|
r.reviews[0].reset.push(FacetSelector::Literal(KeyPair {
|
|
oa: None,
|
|
sa: Some("paired".into()),
|
|
}));
|
|
p = applied(&p, r);
|
|
}
|
|
enumeration(&mut p, source, &["incoming"]);
|
|
enumeration(&mut p, target, &["paired", "local"]);
|
|
let q = applied(
|
|
&p,
|
|
directed(vec![(constructor, SemanticDecision::TakeSource)]),
|
|
);
|
|
assert!(
|
|
matches!(q.elements[&target].shared(), Some(SharedBody::DataType {
|
|
definition: DataTypeDefinition::Enumeration { literals }, ..
|
|
}) if literals == &["incoming", "local"]),
|
|
"reverse={reverse}"
|
|
);
|
|
validate_semantic_baseline(
|
|
q.traces[&t]
|
|
.baseline
|
|
.as_ref()
|
|
.unwrap()
|
|
.semantic
|
|
.as_ref()
|
|
.unwrap(),
|
|
)
|
|
.unwrap();
|
|
}
|
|
}
|
|
|
|
fn reverse_legacy_port_fixture() -> (Project, Id, Id, Id) {
|
|
let mut p = Project::empty();
|
|
let oa_owner = add(&mut p, Kind::Actor, Layer::Oa, "oa owner");
|
|
let sa_owner = add(&mut p, Kind::SystemActor, Layer::Sa, "sa owner");
|
|
let oa = Id::new_v4();
|
|
let sa = Id::new_v4();
|
|
let mut legacy = make_port(&p, oa, oa_owner, "oa port", PortContract::Legacy).unwrap();
|
|
*legacy.shared_mut().unwrap() = SharedBody::Port {
|
|
contract: PortContract::Legacy,
|
|
legacy: Some(LegacyPortContract {
|
|
direction: PortDirection::InOut,
|
|
interface: None,
|
|
}),
|
|
};
|
|
p.elements.insert(oa, legacy);
|
|
let typed = make_port(
|
|
&p,
|
|
sa,
|
|
sa_owner,
|
|
"sa port",
|
|
PortContract::Component {
|
|
kind: ComponentPortKind::Flow,
|
|
orientation: ComponentPortOrientation::Out,
|
|
},
|
|
)
|
|
.unwrap();
|
|
p.elements.insert(sa, typed);
|
|
let t = trace(&mut p, oa, sa);
|
|
(p, oa, sa, t)
|
|
}
|
|
|
|
#[test]
|
|
fn reverse_existing_port_review_retains_legacy_and_roundtrips_typed_history() {
|
|
for decision in [SemanticDecision::KeepTarget, SemanticDecision::Candidate] {
|
|
let (p, oa, sa, t) = reverse_legacy_port_fixture();
|
|
let mut r = request(
|
|
sa,
|
|
oa,
|
|
vec![(FacetSelector::Port(PortFacet::Constructor), decision)],
|
|
);
|
|
r.transition.direction = TransitionDirection::SaToOa;
|
|
r.traces.insert(sa, t);
|
|
let expected_direction = if decision == SemanticDecision::Candidate {
|
|
let mut candidate = p.elements[&oa].clone();
|
|
let Some(SharedBody::Port {
|
|
legacy: Some(old), ..
|
|
}) = candidate.shared_mut()
|
|
else {
|
|
panic!()
|
|
};
|
|
old.direction = PortDirection::In;
|
|
r.candidates.push(SemanticRecord::Element(candidate));
|
|
PortDirection::In
|
|
} else {
|
|
PortDirection::InOut
|
|
};
|
|
let q = applied(&p, r);
|
|
assert_eq!(q.elements[&sa], p.elements[&sa]);
|
|
assert_eq!(q.elements.len(), p.elements.len());
|
|
assert!(matches!(q.elements[&oa].shared(), Some(SharedBody::Port {
|
|
contract: PortContract::Legacy, legacy: Some(LegacyPortContract { direction, interface: None }),
|
|
}) if *direction == expected_direction));
|
|
let baseline = q.traces[&t]
|
|
.baseline
|
|
.as_ref()
|
|
.unwrap()
|
|
.semantic
|
|
.as_ref()
|
|
.unwrap();
|
|
validate_semantic_baseline(baseline).unwrap();
|
|
assert!(baseline.facets.iter().any(|f| matches!(
|
|
f,
|
|
FacetReview::Port(PortReview::Constructor(Reviewed {
|
|
oa: Observed::Value(PortConstructor::Legacy),
|
|
sa: Observed::Value(PortConstructor::Component),
|
|
}))
|
|
)));
|
|
assert_eq!(language::parse(&language::format(&q)).unwrap(), q);
|
|
assert_eq!(
|
|
persistence::decode(&serde_json::to_vec(&q).unwrap()).unwrap(),
|
|
q
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn reverse_existing_port_review_rejects_typed_oa_proposals_without_mutation() {
|
|
for decision in [SemanticDecision::TakeSource, SemanticDecision::Candidate] {
|
|
let (p, oa, sa, _) = reverse_legacy_port_fixture();
|
|
let snapshot = p.clone();
|
|
let mut r = request(
|
|
sa,
|
|
oa,
|
|
vec![(FacetSelector::Port(PortFacet::Constructor), decision)],
|
|
);
|
|
r.transition.direction = TransitionDirection::SaToOa;
|
|
if decision == SemanticDecision::Candidate {
|
|
let mut candidate = p.elements[&oa].clone();
|
|
*candidate.shared_mut().unwrap() = p.elements[&sa].shared().unwrap().clone();
|
|
r.candidates.push(SemanticRecord::Element(candidate));
|
|
}
|
|
let plan = plan_semantic_transition(&p, r).unwrap();
|
|
assert!(!plan.conflicts.is_empty());
|
|
assert!(
|
|
plan.conflicts
|
|
.iter()
|
|
.all(|c| !c.contains("outside selected transition")),
|
|
"Must reach structural validation: {:?}",
|
|
plan.conflicts
|
|
);
|
|
assert!(prepare_semantic_transition(&p, &plan).is_err());
|
|
assert_eq!(p, snapshot);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn reverse_new_port_counterpart_remains_an_explicit_sa_only_omission() {
|
|
let (p, _, sa, _) = reverse_legacy_port_fixture();
|
|
let mut r = TransitionRequest::new(TransitionDirection::SaToOa, vec![sa]);
|
|
r.mappings.push(TransitionMapping {
|
|
source: sa,
|
|
target: None,
|
|
target_kind: Some(Kind::Port),
|
|
});
|
|
let plan = plan_transition(&p, &r).unwrap();
|
|
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
|
assert!(plan.changes.is_empty());
|
|
assert!(plan.warnings.iter().any(|w| w.contains("SA-only omission")));
|
|
assert_eq!(apply_transition(&p, &plan).unwrap(), p);
|
|
}
|