516 lines
18 KiB
Rust
516 lines
18 KiB
Rust
use oa_core::{
|
|
transitions::*,
|
|
validation::{self, MessageEndpointResolution as Resolution, Severity},
|
|
*,
|
|
};
|
|
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 fixture() -> (Project, Id, Id, Id) {
|
|
let mut p = Project::empty();
|
|
let a = add(&mut p, Kind::Entity, Layer::Oa, "Rescue team");
|
|
let b = add(&mut p, Kind::Actor, Layer::Oa, "Control centre");
|
|
let mean = p.relate(RelationKind::CommunicationMean, "Radio", a, b);
|
|
(p, a, b, mean)
|
|
}
|
|
fn errors(p: &Project) -> Vec<validation::Issue> {
|
|
validation::validate(p)
|
|
.into_iter()
|
|
.filter(|i| i.severity == Severity::Error)
|
|
.collect()
|
|
}
|
|
fn valid(p: &Project) {
|
|
assert!(errors(p).is_empty(), "{:?}", errors(p));
|
|
}
|
|
fn oriented(p: &mut Project, id: Id, value: bool) {
|
|
if let RelationBody::CommunicationMean { oriented, .. } =
|
|
&mut p.relations.get_mut(&id).unwrap().body
|
|
{
|
|
*oriented = value;
|
|
}
|
|
}
|
|
fn items(p: &mut Project, id: Id, values: Vec<Id>) {
|
|
match &mut p.relations.get_mut(&id).unwrap().body {
|
|
RelationBody::CommunicationMean { items, .. } | RelationBody::Interaction { items, .. } => {
|
|
*items = values
|
|
}
|
|
_ => panic!(),
|
|
}
|
|
}
|
|
fn has(p: &Project, id: Id, code: &str, severity: Severity) -> bool {
|
|
validation::validate(p)
|
|
.iter()
|
|
.any(|i| i.element == Some(id) && i.code == code && i.severity == severity)
|
|
}
|
|
fn line(p: &mut Project, scenario: Id, represented: Id) -> Id {
|
|
if let Some(SharedBody::Scenario { viewpoint, .. }) =
|
|
p.elements.get_mut(&scenario).unwrap().shared_mut()
|
|
{
|
|
*viewpoint = ScenarioViewpoint::ParticipantInteraction;
|
|
}
|
|
let id = p
|
|
.add_element(
|
|
Kind::Lifeline,
|
|
Layer::Oa,
|
|
&format!("line_{represented}"),
|
|
Some(scenario),
|
|
)
|
|
.unwrap();
|
|
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Lifeline {
|
|
represented: Some(represented),
|
|
};
|
|
id
|
|
}
|
|
fn message(
|
|
mean: Id,
|
|
sender: Id,
|
|
receiver: Id,
|
|
kind: MessageKind,
|
|
reply_to: Option<Id>,
|
|
) -> OccurrenceBody {
|
|
OccurrenceBody::Exchange {
|
|
exchange: Some(mean),
|
|
sender: Some(sender),
|
|
receiver: Some(receiver),
|
|
message_kind: kind,
|
|
reply_to,
|
|
values: vec![],
|
|
}
|
|
}
|
|
fn occurrence(p: &mut Project, scenario: Id, body: OccurrenceBody) -> Id {
|
|
let id = p
|
|
.add_element(Kind::Occurrence, Layer::Oa, "Message", Some(scenario))
|
|
.unwrap();
|
|
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Occurrence {
|
|
body,
|
|
precondition: String::new(),
|
|
postcondition: String::new(),
|
|
};
|
|
let steps = p.elements.get_mut(&scenario).unwrap().steps_mut().unwrap();
|
|
if !steps.contains(&id) {
|
|
steps.push(id);
|
|
}
|
|
id
|
|
}
|
|
|
|
#[test]
|
|
fn communication_means_are_distinct_oa_participant_relations_and_strict_source6_records() {
|
|
let (mut p, a, b, mean) = fixture();
|
|
valid(&p);
|
|
assert!(matches!(
|
|
p.relations[&mean].body,
|
|
RelationBody::CommunicationMean {
|
|
oriented: false,
|
|
..
|
|
}
|
|
));
|
|
p.relate(RelationKind::CommunicationMean, "Telephone", a, b);
|
|
valid(&p);
|
|
let text = language::format(&p);
|
|
assert!(text.contains("communication_mean"));
|
|
assert!(text.starts_with("arcadia 6;"));
|
|
assert_eq!(language::parse(&text).unwrap(), p);
|
|
let bytes = serde_json::to_vec(&p).unwrap();
|
|
assert_eq!(persistence::decode(&bytes).unwrap(), p);
|
|
for version in 1..=5 {
|
|
let mut old = serde_json::to_value(&p).unwrap();
|
|
old["schema_version"] = version.into();
|
|
assert!(
|
|
persistence::decode(&serde_json::to_vec(&old).unwrap()).is_err(),
|
|
"schema {version}"
|
|
);
|
|
assert!(
|
|
language::parse(&text.replacen("arcadia 6;", &format!("arcadia {version};"), 1))
|
|
.is_err(),
|
|
"source {version}"
|
|
);
|
|
}
|
|
let mut bad = serde_json::to_value(&p).unwrap();
|
|
bad["relations"][mean.to_string()]["body"]["CommunicationMean"]["typo"] = true.into();
|
|
assert!(persistence::decode(&serde_json::to_vec(&bad).unwrap()).is_err());
|
|
for endpoint in [
|
|
a,
|
|
add(&mut p, Kind::Role, Layer::Oa, "Role"),
|
|
add(&mut p, Kind::System, Layer::Sa, "System"),
|
|
] {
|
|
let mut q = p.clone();
|
|
q.relations
|
|
.get_mut(&mean)
|
|
.unwrap()
|
|
.body
|
|
.set_endpoints(a, endpoint);
|
|
assert!(!errors(&q).is_empty());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn communication_schema5_upgrade_preserves_legacy_data_and_exact_source_trivia() {
|
|
let p = Project::demo();
|
|
let mut json = serde_json::to_value(&p).unwrap();
|
|
json["schema_version"] = 5.into();
|
|
for d in json["diagrams"].as_object_mut().unwrap().values_mut() {
|
|
d.as_object_mut().unwrap().remove("view_options");
|
|
}
|
|
let result = persistence::decode_with_report(&serde_json::to_vec(&json).unwrap()).unwrap();
|
|
assert_eq!(result.project, p);
|
|
assert_eq!(result.migration.unwrap().from_schema, 5);
|
|
let mut without_views = p.clone();
|
|
without_views.diagrams.clear();
|
|
let old =
|
|
language::format(&without_views).replacen("arcadia 6;", "arcadia // old contract\n5;", 1);
|
|
let bundle = source_project::SourceBundle {
|
|
entry: "entry.arcadia".into(),
|
|
files: BTreeMap::from([("entry.arcadia".into(), old.clone())]),
|
|
};
|
|
let compiled =
|
|
source_project::compile_bundle(&bundle, source_project::ProjectLimits::default()).unwrap();
|
|
assert_eq!(
|
|
compiled.bundle.files["entry.arcadia"],
|
|
old.replacen("\n5;", "\n6;", 1)
|
|
);
|
|
assert_eq!(compiled.decoded.project, without_views);
|
|
}
|
|
|
|
#[test]
|
|
fn communication_allocation_checks_whole_partial_pair_and_does_not_guess_roles() {
|
|
let (mut p, a, b, mean) = fixture();
|
|
let x = add(&mut p, Kind::Activity, Layer::Oa, "Send");
|
|
let y = add(&mut p, Kind::Activity, Layer::Oa, "Receive");
|
|
let work = p.relate(RelationKind::Interaction, "Report", x, y);
|
|
let allocation = p.relate(
|
|
RelationKind::ExchangeAllocation,
|
|
"Radio carries report",
|
|
mean,
|
|
work,
|
|
);
|
|
let role = add(&mut p, Kind::Role, Layer::Oa, "Communicator");
|
|
p.relate(RelationKind::RoleRealization, "Role", a, role);
|
|
p.relate(RelationKind::RoleAssignment, "Responsibility", role, x);
|
|
valid(&p);
|
|
assert!(has(
|
|
&p,
|
|
allocation,
|
|
"incomplete_exchange_allocation",
|
|
Severity::Warning
|
|
));
|
|
p.relate(RelationKind::Allocation, "Actual receiver", a, y);
|
|
valid(&p); // reverse candidate survives
|
|
oriented(&mut p, mean, true);
|
|
assert!(has(
|
|
&p,
|
|
allocation,
|
|
"exchange_allocation_performer",
|
|
Severity::Error
|
|
));
|
|
oriented(&mut p, mean, false);
|
|
p.relate(RelationKind::Allocation, "Actual sender", b, x);
|
|
valid(&p);
|
|
let mut q = p.clone();
|
|
q.relations.get_mut(&mean).unwrap().body.set_endpoints(b, a);
|
|
valid(&q);
|
|
let third = add(&mut q, Kind::Entity, Layer::Oa, "Third");
|
|
q.relations
|
|
.get_mut(&mean)
|
|
.unwrap()
|
|
.body
|
|
.set_endpoints(a, third);
|
|
assert!(has(
|
|
&q,
|
|
allocation,
|
|
"exchange_allocation_performer",
|
|
Severity::Error
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn communication_allocation_items_use_specialization_and_empty_is_not_wildcard() {
|
|
let (mut p, a, b, mean) = fixture();
|
|
let x = add(&mut p, Kind::Activity, Layer::Oa, "Send");
|
|
let y = add(&mut p, Kind::Activity, Layer::Oa, "Receive");
|
|
p.relate(RelationKind::Allocation, "Send allocation", a, x);
|
|
p.relate(RelationKind::Allocation, "Receive allocation", b, y);
|
|
let work = p.relate(RelationKind::Interaction, "Report", x, y);
|
|
let allocation = p.relate(RelationKind::ExchangeAllocation, "Carried", mean, work);
|
|
let base = add(&mut p, Kind::ExchangeItem, Layer::Oa, "Report item");
|
|
let specialized = add(&mut p, Kind::ExchangeItem, Layer::Oa, "Distress item");
|
|
if let Some(SharedBody::ExchangeItem { supertype, .. }) =
|
|
p.elements.get_mut(&specialized).unwrap().shared_mut()
|
|
{
|
|
*supertype = Some(base);
|
|
}
|
|
items(&mut p, work, vec![specialized]);
|
|
assert!(has(
|
|
&p,
|
|
allocation,
|
|
"exchange_allocation_items",
|
|
Severity::Error
|
|
));
|
|
items(&mut p, mean, vec![base]);
|
|
valid(&p);
|
|
items(&mut p, work, vec![base]);
|
|
items(&mut p, mean, vec![specialized]);
|
|
assert!(has(
|
|
&p,
|
|
allocation,
|
|
"exchange_allocation_items",
|
|
Severity::Error
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn communication_scenario_reverse_call_and_reply_follow_actual_direction() {
|
|
let (mut p, a, b, mean) = fixture();
|
|
let scenario = add(&mut p, Kind::Scenario, Layer::Oa, "Radio conversation");
|
|
let la = line(&mut p, scenario, a);
|
|
let lb = line(&mut p, scenario, b);
|
|
let call_body = message(mean, lb, la, MessageKind::Call, None);
|
|
let call = occurrence(&mut p, scenario, call_body.clone());
|
|
let reply_body = message(mean, la, lb, MessageKind::Reply, Some(call));
|
|
occurrence(&mut p, scenario, reply_body.clone());
|
|
assert_eq!(
|
|
validation::resolve_message_endpoints(&p, &call_body),
|
|
Resolution::Resolved {
|
|
source: b,
|
|
target: a
|
|
}
|
|
);
|
|
assert_eq!(
|
|
validation::resolve_message_endpoints(&p, &reply_body),
|
|
Resolution::Resolved {
|
|
source: a,
|
|
target: b
|
|
}
|
|
);
|
|
valid(&p);
|
|
let mut edited = p.relations[&mean].clone();
|
|
if let RelationBody::CommunicationMean { oriented, .. } = &mut edited.body {
|
|
*oriented = true;
|
|
}
|
|
assert!(prepare_command(&p, &Command::EditRelation(mean, edited)).is_err());
|
|
valid(&p);
|
|
// The established operation-response exception also allows same-means Reply on oriented means.
|
|
if let Some(SharedBody::Occurrence { body, .. }) =
|
|
p.elements.get_mut(&call).unwrap().shared_mut()
|
|
{
|
|
*body = message(mean, la, lb, MessageKind::Call, None);
|
|
}
|
|
oriented(&mut p, mean, true);
|
|
assert_eq!(
|
|
validation::resolve_message_endpoints(
|
|
&p,
|
|
&message(mean, lb, la, MessageKind::Reply, Some(call))
|
|
),
|
|
Resolution::Resolved {
|
|
source: b,
|
|
target: a
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn communication_scenario_ambiguous_and_incomplete_bindings_never_guess() {
|
|
let (mut p, a, b, mean) = fixture();
|
|
let scenario = add(&mut p, Kind::Scenario, Layer::Oa, "Roles");
|
|
let role = add(&mut p, Kind::Role, Layer::Oa, "Shared role");
|
|
p.relate(RelationKind::RoleRealization, "A role", a, role);
|
|
p.relate(RelationKind::RoleRealization, "B role", b, role);
|
|
let one = line(&mut p, scenario, role);
|
|
let two = line(&mut p, scenario, role);
|
|
let mut body = message(mean, one, two, MessageKind::Asynchronous, None);
|
|
assert_eq!(
|
|
validation::resolve_message_endpoints(&p, &body),
|
|
Resolution::Ambiguous
|
|
);
|
|
assert_eq!(validation::message_endpoints(&p, &body), None);
|
|
if let OccurrenceBody::Exchange { receiver, .. } = &mut body {
|
|
*receiver = None;
|
|
}
|
|
assert_eq!(
|
|
validation::resolve_message_endpoints(&p, &body),
|
|
Resolution::Incomplete
|
|
);
|
|
let third = add(&mut p, Kind::Entity, Layer::Oa, "Other");
|
|
let other = line(&mut p, scenario, third);
|
|
if let OccurrenceBody::Exchange { sender, .. } = &mut body {
|
|
*sender = Some(other);
|
|
}
|
|
assert_eq!(
|
|
validation::resolve_message_endpoints(&p, &body),
|
|
Resolution::Conflict
|
|
);
|
|
}
|
|
|
|
fn transition_fixture() -> (Project, Id, Id, Id, TransitionRequest) {
|
|
let (mut p, a, b, mean) = fixture();
|
|
let sa = add(&mut p, Kind::System, Layer::Sa, "System");
|
|
let sb = add(&mut p, Kind::SystemActor, Layer::Sa, "External");
|
|
let mut r = TransitionRequest::new(TransitionDirection::OaToSa, vec![mean]);
|
|
r.mappings = vec![
|
|
TransitionMapping {
|
|
source: a,
|
|
target: Some(sa),
|
|
target_kind: None,
|
|
},
|
|
TransitionMapping {
|
|
source: b,
|
|
target: Some(sb),
|
|
target_kind: None,
|
|
},
|
|
];
|
|
let port = |name: &str| CommunicationPortChoice::Create {
|
|
id: Id::new_v4(),
|
|
alias: name.into(),
|
|
name: name.into(),
|
|
description: "Explicit technical choice".into(),
|
|
kind: ComponentPortKind::Standard,
|
|
orientation: ComponentPortOrientation::Unset,
|
|
};
|
|
r.communication_endpoints
|
|
.push(CommunicationEndpointDecision {
|
|
source: mean,
|
|
source_port: port("radio_source"),
|
|
target_port: port("radio_target"),
|
|
});
|
|
(p, mean, sa, sb, r)
|
|
}
|
|
|
|
#[test]
|
|
fn communication_transition_requires_explicit_ports_and_preserves_participant_mapping() {
|
|
let (p, mean, sa, sb, r) = transition_fixture();
|
|
let mut incomplete = r.clone();
|
|
incomplete.communication_endpoints.clear();
|
|
assert!(
|
|
!plan_transition(&p, &incomplete)
|
|
.unwrap()
|
|
.conflicts
|
|
.is_empty()
|
|
);
|
|
let plan = plan_transition(&p, &r).unwrap();
|
|
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
|
let q = apply_transition(&p, &plan).unwrap();
|
|
valid(&q);
|
|
let trace = q.traces.values().find(|t| t.oa == mean).unwrap();
|
|
assert_eq!(trace.kind, TraceKind::Exchange);
|
|
let boundary = &q.relations[&trace.sa];
|
|
assert_eq!(boundary.kind(), RelationKind::BoundaryExchange);
|
|
assert_eq!(q.elements[&boundary.source()].parent, Some(sa));
|
|
assert_eq!(q.elements[&boundary.target()].parent, Some(sb));
|
|
assert_eq!(q.relations[&mean], p.relations[&mean]);
|
|
assert_eq!(q.elements.len(), p.elements.len() + 2);
|
|
let mut collision = r.clone();
|
|
collision.mappings[1].target = Some(sa);
|
|
assert!(
|
|
!plan_transition(&p, &collision)
|
|
.unwrap()
|
|
.conflicts
|
|
.is_empty()
|
|
);
|
|
let mut duplicate = r.clone();
|
|
duplicate
|
|
.communication_endpoints
|
|
.push(r.communication_endpoints[0].clone());
|
|
assert!(
|
|
!plan_transition(&p, &duplicate)
|
|
.unwrap()
|
|
.conflicts
|
|
.is_empty()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn communication_reverse_transition_projects_owners_without_oa_ports() {
|
|
let (p, mean, sa, sb, r) = transition_fixture();
|
|
let q = apply_transition(&p, &plan_transition(&p, &r).unwrap()).unwrap();
|
|
let boundary = q.traces.values().find(|t| t.oa == mean).unwrap().sa;
|
|
let mut standalone = q.clone();
|
|
standalone.traces.clear();
|
|
let mut reverse = TransitionRequest::new(TransitionDirection::SaToOa, vec![boundary]);
|
|
reverse.mappings = vec![
|
|
TransitionMapping {
|
|
source: sa,
|
|
target: Some(p.relations[&mean].source()),
|
|
target_kind: None,
|
|
},
|
|
TransitionMapping {
|
|
source: sb,
|
|
target: Some(p.relations[&mean].target()),
|
|
target_kind: None,
|
|
},
|
|
];
|
|
let plan = plan_transition(&standalone, &reverse).unwrap();
|
|
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
|
let result = apply_transition(&standalone, &plan).unwrap();
|
|
assert_eq!(result.elements, standalone.elements);
|
|
let counterpart = &result.relations[&result
|
|
.traces
|
|
.values()
|
|
.find(|t| t.sa == boundary)
|
|
.unwrap()
|
|
.oa];
|
|
assert_eq!(counterpart.kind(), RelationKind::CommunicationMean);
|
|
assert_eq!(
|
|
counterpart.body.endpoints(),
|
|
p.relations[&mean].body.endpoints()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn communication_semantic_endpoint_take_needs_scoped_decision_and_exact_history() {
|
|
let (p, mean, _, _, r) = transition_fixture();
|
|
let q = apply_transition(&p, &plan_transition(&p, &r).unwrap()).unwrap();
|
|
let trace = q.traces.values().find(|t| t.oa == mean).unwrap();
|
|
let boundary = trace.sa;
|
|
let mut transition = TransitionRequest::new(TransitionDirection::OaToSa, vec![mean]);
|
|
transition.mappings = r.mappings.clone();
|
|
transition.mappings.push(TransitionMapping {
|
|
source: mean,
|
|
target: Some(boundary),
|
|
target_kind: None,
|
|
});
|
|
let mut request = SemanticTransitionRequest::new(transition);
|
|
request.references = r
|
|
.mappings
|
|
.iter()
|
|
.map(|m| (m.source, m.target.unwrap()))
|
|
.collect();
|
|
request.reviews.push(SemanticReviewRequest {
|
|
source: mean,
|
|
resolutions: vec![FacetResolution {
|
|
facet: FacetSelector::Relation(RelationFacet::Source),
|
|
decision: SemanticDecision::TakeSource,
|
|
}],
|
|
reset: vec![],
|
|
});
|
|
assert!(
|
|
!plan_semantic_transition(&q, request.clone())
|
|
.unwrap()
|
|
.conflicts
|
|
.is_empty()
|
|
);
|
|
request
|
|
.transition
|
|
.communication_endpoints
|
|
.push(CommunicationEndpointDecision {
|
|
source: mean,
|
|
source_port: CommunicationPortChoice::Reuse {
|
|
port: q.relations[&boundary].source(),
|
|
},
|
|
target_port: CommunicationPortChoice::Reuse {
|
|
port: q.relations[&boundary].target(),
|
|
},
|
|
});
|
|
let plan = plan_semantic_transition(&q, request).unwrap();
|
|
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
|
let result = apply_semantic_transition(&q, &plan).unwrap();
|
|
let baseline = result.traces[&trace.id]
|
|
.baseline
|
|
.as_ref()
|
|
.unwrap()
|
|
.semantic
|
|
.as_ref()
|
|
.unwrap();
|
|
assert!(baseline.facets.iter().any(|f|matches!(f,FacetReview::Relation(RelationReview::Source(Reviewed{oa:Observed::Value(a),sa:Observed::Value(b)})) if *a==p.relations[&mean].source()&&*b==q.relations[&boundary].source())));
|
|
}
|